How do I programmatically create new groups with specific set of rights on XWiki? - wiki

I'm writing my own XWiki Authenticator (that extends XWikiAuthServiceImpl) and therein, I want to create few groups, each with different sets of rights. How do I do it programmatically?
Example,
XWiki.MyStandardGroup - view, edit, comment
XWiki.MyClassicGroup - view, edit, comment, script
XWiki.MyAdminGroup - view, edit, commit, script, delete, admin
Also, I create the users programmatically. How do I give different access rights to different sets of users?
On the conceptual level, how do users (with rights) work with pages (with rights)? I tried to read the following docs:
Access Rights
Permission types
They dont seem to explain these — or maybe, they do but written in a complex way without any concrete examples which makes it difficult to get the idea of how rights on different entities (pages, users and groups) work together. Here are some text from the Access Rights which needs example to be understood:
When a right has been allowed at a given level, it gets implicitly denied to anyone else at the same level. This only applies to the right allowed. If only "View" is set to a user/group at this level, all other rights like "Edit" are still inherited. Using this implicit deny behavior is recommended over applying explicit denial.
What does the bold part even mean? I think the term level is used in different sense on different bullet points under the same Basic rules section.

I feel there are three questions in this post:
How do I create Users and Groups programatically?
How does the Access Rights system work?
Is there an example for the text quoted from the access rights page
First an answer to the second one.
How does the Access Rights system work - with example
There are a fixed number of rights in XWiki, like view, edit, etc.
Users can get these rights assigned directly to them, or they can be member of a group, and the group has these rights assigned to them. This assignment of rights can happen in different places (which are called "levels" in the documentation).
The "level" structure is as follows:
Wiki levels
First there is the main wiki (that gets pre-installed when you install the wiki). Then there might be more wikis, called "sub-wikis", which you can create manually (via the "Wikis" secton from the "Burger" menu on the top right of every wiki page). This is a simple two layer hierarchy:
main wiki (always exists)
|
|--- subwiki1
|
|--- subwiki2
|
|--- subwiki3
Subwikis cannot be nested. I am not going into details why you might want them; oen can often go without them.
Users and groups can exist in the main wiki (which means their profile pages are located in the main wiki), or they can exist in subwikis (i.e. their profile pages are there.)
Users and Groups from the main wiki are visible in all subwikis (and can get rights assigned to them), but not the other way round - a user located in a subwiki cannot get special rights in the main wiki (and also not in another subwiki). If such users access the main wiki, they are treated as the anonymous user. They can only log in to the subwiki.
Page levels
Second, (nearly) all data in the wiki is stored in pages. These pages are also nested, and since XWiki 7.x they can be nested arbitrarily deep. This is the other part of the "levels" structure.
For every wiki, there is a set of "top level" pages, both preinstalled and user created.
Then there are pages which are children of these "top level" pages, which in turn can have children, and so on.
As an additional complication, not all pages can have subpages. By historical convention these pages with a full name ending in WebHome can have children pages, others can not. This is probably transparent to the user, but important for the programmer.
There is no single "root" page to start the hierarchy. As an example, for one wiki the structure might look like:
Top level Third Level
Second Level Fourth Level
Main.WebHome (preinstalled "Start" page)
|
|------ Main.Search (preinstalled search page, no subpages)
|
|------ Main.SomePage.WebHome (user created page, can have children)
Sandbox.WebHome (preinstalled playground page)
|
|------ Sandbox.TestPage1 (preinstalled demo page, no subpages)
|
|------ Sandbox.TestPage2 (preinstalled demo page, no subpages)
|
|------ Sandbox.TestPage3 (preinstalled demo page, no subpages)
|
|------ Sandbox.SomePage.WebHome (user created 2nd level page, can have children)
Documentation.WebHome (user created top level page)
|
|------ Documentation.Topic1.WebHome (user created 2nd level page, can have children)
| |
| |------ Documentation.Topic1.SubTopic1.WebHome (user created 3rd lvl page, can have children, too)
| |
| |------ Documentation.Topic1.SubTopic2.WebHome (user created 3rd lvl page, can have children, too)
| |
| |------ Documentation.Topic1.SubTopic3.WebHome (user created 3rd lvl page, can have children, too)
| | |
| | |------ Documentation.Topic1.SubTopic3.EvenMore.WebHome (user created 4th lvl page, can have children)
| |
| .
| .
| |
| |------ Documentation.Topic1.SubTopicN.WebHome (user created 3rd lvl page, can have children, too)
|
|------ Documentation.Topic2.WebHome (user created 2nd lvl page, can have children)
.
.
.
|
|------ Documentation.TopicN.WebHome (user created 2nd lvl page, can have children)
....
Granting rights
You now can grant a right to user or group on every page in this hierarchy by adding an Object of type XWiki.XWikiRights to the page itself, specifying the list of rights to grant (confusingly stored in the attribute levels of that object), the list of users and/or groups to grant the right to, and a allow/deny flag ... which we will come to later. How to do that programatically is discussed in the question: Set user and group rights to document in XWiki
In that case the right is only granted for the page itself, not its subpages. If you give the right edit on the page Main.WebHome to the group XWiki.HomepageEditorsGroup, then only members of this group can edit the page, but this does not affect subpages like Main.Search or Main.SomePage.WebHome.
That the attribute levels here actually stores the rights is maybe confusing - again this is another historical decision. (The software is developed since 15 years or so and the developers are commited to keep backwards compatibility). What ever the attribute is named, these are rights, and not the levels the documentation talks about.
To go on with the rights management: You can also grant a right on a page and all its subpages. This only works for pages which can have subpages. Technically this is done by adding an object of type XWiki.XWikiGlobalRights ... but not to the page itself, but to a subpage named WebPreferences. (Historical decision, again.)
So if you want to grant the view right to the group XWiki.Topic1ViewerGroup on the page Documentation.Topic1.WebHome and its subpages like Documentation.Topic1.SubTopic1.WebHome or Documentation.Topic1.SubTopic3.EvenMore.WebHome, then you take the page Documentation.Topic1.WebPreferences (creating it if it does not exist), and add an object of type XWiki.XWikiGlobalRights to it, with the attributes:
level : view
groups : XWiki.Topic1ViewerGroup
allow: 1
How the rights are checked
Now the check for a specific right usually looks at a given page itself, then looks at the WebPreferences for that page, then at the WebPreferences of the parent page, and so on. (It is "going up the levels".)
The check stops as soon as it finds a "rights" object covering the right in question.
If no matching "rights" object has been found up to the top level page, then the wiki is checked. Rights on the wiki level are stored in the special page XWiki.XWikiPreferences, again as objects of class XWiki.XWikiGlobalRights.
Finally if the wiki happens to be a subwiki, the global righs on the main wiki might be consulted - again on the page names XWiki.XWikiPreferences, but this time in the main wiki.
Example 1: check for view right on Documentation.Topic1.SubTopic3.WebHome
Documentation.Topic1.SubTopic3.WebHome has no XWiki.XWikiRights - no decision
Documentation.Topic1.SubTopic3.WebPreferences has no XWiki.XWikiGlobalRights - no decision
Documentation.Topic1.WebPreferences has an XWiki.XWikiGlobalRights for view - stop to make a decision
Result: if the current user is in the group XWiki.Topic1ViewerGroup, she/he can view the page, otherwise not
Example 2: check for edit right on Main.WebHome
Main.WebHome has a XWiki.XWikiRights for edit - stop to make a decision
Result: only users in the XWiki.HomepageEditorsGroup can edit, others do not
Example 3: check for edit right on Main.SomePage.WebHome
Main.SomePage.WebHome has no XWiki.XWikiRights - no decision
Main.SomePage.WebPreferences has no XWiki.XWikiGlobalRights - no decision
up the page hierarchy: Main.WebPreferences has no XWiki.XWikiGlobalRights - no decision either
(that Main.WebHome has a XWiki.XWikiRights is not consulted, as the right applies only to the page itself)
up the page hierarchy: we are already at a top-level page, so go to the wiki instead
i.e. check XWiki.XWikiPreferences for a XWiki.XWikiGlobalRights for edit
usually there is an allow : 1 for the XWiki.XWikiAllGroup which means edit is allowed for all users
if there is no such settings, and we are in a subwiki: go up the wiki hierarchy and check the XWiki.XWikiPreferences of the main wiki
if even there is no decision made, the edit right is not allowed
admin is a special case
As a simplification for the users, but complication for the concept, the admin right works the other way round: if the admin right is granted on the wiki level, it is valid on all pages. Even more, it implicitely grants all the other rights, like view and edit. (The reason for this is that users too often locked themselves out before this special rule was introduced.)
How does the "implicit deny" work?
Now to the quote:
When a right has been allowed at a given level, it gets implicitly denied to anyone else at the same level. This only applies to the right allowed. If only "View" is set to a user/group at this level, all other rights like "Edit" are still inherited. Using this implicit deny behavior is recommended over applying explicit denial.
I try to explain by example, too:
In the Example 1 above I wrote:
Documentation.Topic1.WebPreferences has an XWiki.XWikiGlobalRights for view - stop to make a decision
Result: if the current user is in the group XWiki.Topic1ViewerGroup, she/he can view the page, otherwise not
Here the result is either:
allow the user to view the page (and its sub pages), if the user is member of the XWiki.Topic1ViewerGroup
deny the user the right to view the page (and its sub pages), if the user is not member of the XWiki.Topic1ViewerGroup (i.e is "everyone else")
That is, no matter what rights the user might have otherwise - as soon as the right is set here, then only the users fulfilling the criterion in the settings are allowed to view. Everybody else is out. This is an "implicit deny".
As an alternative, assume someone has set a rights object on Sandbox.WebPreferences (i.e. affecting the "Sandbox" and all subpages):
level : edit
groups : XWiki.Topic1ViewerGroup
allow: 1
and onSandbox.SomePage.WebHome (i.e. affecting this sub page only):
level : edit
groups : XWiki.Topic1ViewerGroup
allow: 0
The setting allow: 0 is an "explicit deny": as soon as you are member of the XWiki.Topic1ViewerGroup, you are not allowed to edit this page. The fact that there is an allow: 1 on a higher level in the page hierarchy (on "Sandbox" and all sub pages) does not matter, beacuse it is not on the same level.
How to do that programatically?
First, the groups should be created as "terminal" sub pages (i.e. pages not having children) in the XWiki space, like XWiki.MyCustomGroup. However they seem to work wherever you want to create them.
On the other hand, users must be created as pages XWiki.<LoginName> as unfortunately there is a lot of code around that expects users to be in this location and nowhere else.
After having created the page (in the API they are called Document), add an object of the proper class to the page, set the attributes you want and save the page.
When looking at your requirements, it does not look like you want to grant the rights to the groups in any special place in the page hierarchy; so I assume they will be set on the wiki level. Thus no need to understand all the other explanations; just grab the XWiki.XWikiPreferences page and add the required XWiki.XWikiGlobalRights there.
I recommend using an MandatoryDocumentInitializer for this; there is a nice example in the code base which makes sure the XWikiAllGroup is always present.
This interface is meant to ensure that a single page is present in the wiki, but nobody keeps you from checking that other pages are set up properly, too. The only thing you need to have in mind is that the other pages are not saved automatically, but you can do that manually with the XWiki.saveDocument method.
To create a user, there is a convenience method XWiki.createUser(String userName, Map values, XWikiContext context) in the XWiki class. The values map contains the values for the attributes to be set on the new user; you can check which attributes are available on the XWiki.XWikiUsers page in your wiki.
To create a group, you can borrow code from the example above. Note that to create a new empty group, one adds an object of type XWiki.XWikiGroups; to add members to the group one should add one more object of type XWiki.XWikiGroups for each user and set the member attribute to the full name of the user (i.e. including the 'XWiki.` prefix).
So the class might start with:
#Component
#Named("XWiki.MyStandardGroup")
public class MyUserAndGroupsInitializer implements MandatoryDocumentInitializer
{
private static final String GROUP_CLASS_NAME = "XWikiGroups";
private static final String MEMBER_ATTR = "member";
private static final String RIGHTS_CLASS_NAME = "XWikiGlobalRights";
private static final String GROUPS_ATTR = "groups";
private static final String USERS_ATTR = "users";
private static final String RIGHTS_ATTR = "levels"; // ;)
private static final String ALLOW_ATTR = "allow";
#Inject
Provider<XWikiContext> contextProvider;
#Inject
org.slf4j.Logger logger;
The #Named contains by convention the name of the page the initializer cares about. That avoids name clashes between initializers on the one hand and allows to overwrite an existing initializer for a page, if wanted. You can choose a differetn name here if you prefer.
The #Injected compontents are an accessor to the current "context", which allows us to access the data in the current wiki and maintans a database connection in the background. A logger cannot hurt either.
As we need to implement the MandatoryDocumentInitializer, we first need to tell the location of one of the pages we care about:
#Override
public EntityReference getDocumentReference()
{
return new LocalDocumentReference(XWiki.SYSTEM_SPACE, "MyStandardGroup");
}
This makes XWiki pass us in the page as a parameter in the next method; we should return true here if that page needs to be saved afterwards. As we do e verything by ourselves, we can as well return false always.
#Override
public boolean updateDocument(XWikiDocument document)
{
logger.info("try to create users/groups");
try {
// here create your users
// and your groups
} catch (XWikiException xe) {
// as we are not allowed to let this through:
logger.error("failed to create groups", xe);
}
return false;
}
That is it, basically. Oh, some possibly useful helpers:
Adding users is relatively easy:
private void createUser(String userFullName) throws XWikiException
{
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
Map<String,String> values = new HashMap<>();
values.put("last_name", userFullName);
values.put("password", "staple battery horses correct");
int result = xwiki.createUser(userName, values, context);
if (result > 0) {
logger.info("user [{}] created", userFullName);
} else {
logger.debug("user [{}] aleady exists", userFullName);
}
}
ok, maybe not that simple, but you can start with that one.
It is nearly the same for groups:
// pass in rights as comma separated string, e.g.: "view,comment,edit"
// members should be the full page name of the user, including the "XWiki." part
private void createGroup(String group, String rights, String... members) throws XWikiException
{
logger.info("try to create group [{}]", group);
XWikiDocument groupDoc = checkDocument(XWiki.SYSTEM_SPACE + '.' + group);
if (groupDoc.isNew()) {
addUserToGroup(groupDoc, "");
for (String member : members) {
addUserToGroup(groupDoc, member);
}
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
xwiki.saveDocument(groupDoc, "created", false, context);
logger.info("group [{}] created", group);
}
setRightsForGroup(groupDoc, rights);
}
and adding users to the group is also easy:
// return true if group needs to be saved afterwards
private boolean addUserToGroup(XWikiDocument groupDoc, String userName) throws XWikiException
{
XWikiContext context = contextProvider.get();
LocalDocumentReference groupClassReference = new LocalDocumentReference(XWiki.SYSTEM_SPACE, GROUP_CLASS_NAME);
// first check if the user is already member of the group
if (groupDoc.getXObject(groupClassReference, MEMBER_ATTR, userName, false) != null) {
// is already member, no changes necessary
logger.debug("user [{}] is already member of group [{}]", userName, groupDoc.getFullName());
return false;
}
logger.info("add user [{}] to group [{}]", userName, groupDoc.getFullName());
BaseObject newGroupEntry = groupDoc.newXObject(groupClassReference, context);
newGroupEntry.setStringValue(MEMBER_ATTR, userName);
return true;
}
... if it were not for the rights settings that I have moved into a separate helper
// set rights settings for group if it is not set yet; saves the result right away
private void setRightsForGroup(XWikiDocument groupDoc, String rights) throws XWikiException
{
XWikiContext context = contextProvider.get();
XWiki xwiki = context.getWiki();
LocalDocumentReference rightsClassReference = new LocalDocumentReference(XWiki.SYSTEM_SPACE, RIGHTS_CLASS_NAME);
String groupName = groupDoc.getFullName();
// check if the right is already set in the XWikiPreferences.
// here we need to loop over all values instead
XWikiDocument xwikiPrefDocument = xwiki.getDocument(new DocumentReference(context.getWikiId(), XWiki.SYSTEM_SPACE, "XWikiPreferences"), context);
boolean found = false;
for (BaseObject rightsSetting : xwikiPrefDocument.getXObjects(rightsClassReference)) {
if (rights.contentEquals(rightsSetting.getStringValue(RIGHTS_ATTR))
&& rightsSetting.getIntValue(ALLOW_ATTR) == 1) {
// this is the right setting!
String groups = rightsSetting.getStringValue(GROUPS_ATTR);
if (!groups.contains(groupName)) {
// our group is missing: add group and save
rightsSetting.setStringValue(GROUPS_ATTR, groups + ',' + groupName);
xwiki.saveDocument(xwikiPrefDocument, "add rights for group [" + groupName + "]", true, context);
logger.info("amended rights for group [{}]", groupName);
} else {
logger.info("rights for group [{}] already set", groupName);
}
found = true;
break;
}
}
if (!found) {
BaseObject newRightsSetting = xwikiPrefDocument.newXObject(rightsClassReference, context);
newRightsSetting.setStringValue(RIGHTS_ATTR, rights);
newRightsSetting.setIntValue(ALLOW_ATTR, 1);
newRightsSetting.setLargeStringValue(GROUPS_ATTR, groupName);
if (newRightsSetting.getIntValue(ALLOW_ATTR) != 1) {
logger.error("adding rights of class [{}] for group [{}] failed!", rightsClassReference, context);
}
xwiki.saveDocument(xwikiPrefDocument, "add rights for group [" + groupName + "]", true, context);
logger.info("added new rights for group [{}]", groupName);
}
}
I have also used a checkDocument helper, which is basically the same as the updateDocument in the XWikiAllGroupInitializer, except that the name is input and the tediously newly set up page is the return value.
You might want to read the Component Guide to understand how the necessary dependencies get injected. Especially you will need to add the full class name of the initializer to the src/main/resources/META-INF/components.txt for the initializer to get activated.
Backup your database before you try this out. Except a few tries before everything is properly set up, and nothing gets saved unnecessarily on each wiki restart.
Also fiddle with the WEB-INF/classes/logback.xml to set the level to INFO for your package, if you want to see the log messages.
Some random other advice
Instead of managing your users programatically you might consider storing then in a LDAP Server and use this for authentication with the LDAP Authenticator. (You still need to create the groups and manage their rights, however)
While developing I found it very useful to have the Scripting Reference Documentation extension installed in my development wiki. It is no a replacement for any documentation, but being able to brwose the API Javadoc interactively somehow helps me a lot.
The Admin Tools extension has a page that shows you all rights granted in the current wiki where this extension is installed. (Go to .../xwiki/bin/view/Admin/ and click "Show Rights".)

Related

How to hide Creation Date from WHOIS?

Amazon Route53 hides most of the contact information by default, however that doesn't include the Creation Date property. Is there an option to hide that piece of information as well? I do not want the public to know when I created my website.
Unfortunately, you did not mention the top level domain (TLD) you're referring to. In case of e.g. .com (or another gTLD), you can not avoid having the creation date in WHOIS. See Appendix F of the Temporary Specification for gTLD Registration Data, being effective as of 25 May 2018, for more details.

Sitecore to prevent removing component

I have a page that inserts some predefined components in the placeholders, when user creat it (like gallery and header for example).
I also want web masters to give a freedom to insert other possible components to the placeholders around the predefined components, but I want to prohibit somehow to edit/delete the predefined components (gallery and header) from the page.
However, a content editor can add another gallery component (technically possible) to the same page, and delete it if he wants to. But predefined components should just not be touched (at least be removed from the placeholders. In other words - I don't want to have a delete button in the Experience editor next to the predefined components).
There is a way to fully prohibit editing the component rendering, but I still want web masters to insert components by themselves if they want. I just want them to do not modify the components that come when page is created.
As I understand this can be done somethere on the RenderingReference level, but I cannot figure it out how. Could you give a tip please?
Okay, I have found a way to do this.
Just in case if somebody will need it.
First, we need to create a Processor to catche page renderings in the Experience Editor.
public class CheckLockedComponentsHandler : GetClientActionsChromeData
{
public new void Process(GetChromeDataArgs args)
{
if (args.ChromeType == "rendering")
{
if (args.CustomData["renderingReference"] is RenderingReference rf && !string.IsNullOrEmpty(rf.Settings.Parameters))
{
var rmParams = WebUtil.ParseUrlParameters(rf.Settings.Parameters);
args.ChromeData.Custom["editable"] = string.IsNullOrEmpty(rmParams["editable"]) ? "true" : rmParams["editable"];
}
}
}
}
When, we add a property editable = false when creating a locked component.
And finally we need to register our processor in the configuration
<pipelines>
<getChromeData>
<processor type="CheckLockedComponentsHandler,MyLib" patch:after="*[last()]"/>
</processor>
<getChromeData>
</pipelines>

Sitecore multisites website URL routing [duplicate]

This question already has answers here:
How to generate custom unique ID
(2 answers)
Closed 8 years ago.
I am working on sitecore multisites . I have multiple websites ex test1, test2, test3, test4. All are configured in webconfig. Means test1, test2 ,test3 and so on . So URL //test1 will always point to there local home folder which will be inside of test1/home.
But as per my requirement i have one global folder which are separate from all websites but pages inside of this will be common for all websites.
Ex:
sitecore/Root/Global/Category/A
sitecore/Root/test1
sitecore/Root/test2
sitecore/Root/test3
Now i am not able to get page A if i am in //test1 , and want to access page A and URL should be to //test1/Category/A.
Please help.
I'm not usually a fan of re-posting an exact answer that I posted previously, especially when I posted the original barely over a week ago, but the following is from this post.
Every page that is managed in Sitecore is a Sitecore Item. As such, you should be able to just navigate to the name of the player item. If you were trying to say in your post that category items are stored in globals and not as pages, then you are left with the following options:
Query String: test1/Category?categoryId={ID of Category}
If this is the route that you choose to take then I would suggest using the category item's Sitecore ID for the value of the query string parameter.
If you have other IDs then put the specified ID there, however it would be easiest with Sitecore IDs
What you would then do is get the item with the ID specified in the query string parameter (or get the item with the category ID specified in the query string parameter, depending on which route you take) and display the data for that category on the page
Sitecore.Context.Database.GetItem(Request.QueryString["categoryId"])
categoryItems.FirstOrDefault(categoryItem => categoryItem["Category ID"] == Request.QueryString["categoryId"])
Note that this assumes that the Category ID is a field, not the Sitecore ID
If it is the Sitecore ID then change the lambda to use categoryItem.ID == new ID(Request.QueryString["categoryId"]
Regardless of which one you use, I suggest adding null checks to the QueryString getter
Sublayout Parameters
If you use this method, the query string will not change and people will not be able to get to the page from the direct URL
The process is the same as for query strings, except that you are using Sublayout parameters instead
Note that you must set these in a parent sublayout or in Sitecore (which means that you have a separate page for each player - i.e. this would be a bad solution)
Virtual Items
This is really what I think you are looking for
This can be a lot of work if you do it from scratch, or you can use the Wildcard Module
If you do it from scratch, you will need a custom pipeline and/or processor for handling the requests
If the Wildcard Module isn't going to work, and if this is the case for all sites in your Sitecroe instance, you could write a custom item resolver and insert it in the httpBeginRequest pipeline right after the built in item resolver.
This is sort of from memory, but should get you started:
namespace Example
{
public class CategoryItemRewsolver : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
if (Sitecore.Context.Item != null) return; // Item has already been resolved
if (args.Context.Request.Path.StartsWith("Category"))
{
Sitecore.Context.Item = Sitecore.Context.Database.GetItem("sitecore/Root/Global" + args.Context.Request.Path);
}
}
}
}
You'll obviously want to replace paths with your own (And possibly allow them to be configured through the .config file).
Then patch this into the pipeline:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor patch:after="*[#type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Example.CategoryItemResolver,Example" />
</httpRequestBegin>
</pipelines>
</sitecore>
</configuration>

Mediawiki mass user delete/merge/block

I have 500 or so spambots and about 5 actual registered users on my wiki. I have used nuke to delete their pages but they just keep reposting. I have spambot registration under control using reCaptcha. Now, I just need a way to delete/block/merge about 500 users at once.
You could just delete the accounts from the user table manually, or at least disable their authentication info with a query such as:
UPDATE /*_*/user SET
user_password = '',
user_newpassword = '',
user_email = '',
user_token = ''
WHERE
/* condition to select the users you want to nuke */
(Replace /*_*/ with your $wgDBprefix, if any. Oh, and do make a backup first.)
Wiping out the user_password and user_newpassword fields prevents the user from logging in. Also wiping out user_email prevents them from requesting a new password via email, and wiping out user_token drops any active sessions they may have.
Update: Since I first posted this, I've had further experience of cleaning up large numbers of spam users and content from a MediaWiki installation. I've documented the method I used (which basically involves first deleting the users from the database, then wiping out up all the now-orphaned revisions, and finally running rebuildall.php to fix the link tables) in this answer on Webmasters Stack Exchange.
Alternatively, you might also find Extension:RegexBlock useful:
"RegexBlock is an extension that adds special page with the interface for blocking, viewing and unblocking user names and IP addresses using regular expressions."
There are risks involved in applying the solution in the accepted answer. The approach may damage your database! It incompletely removes users, doing nothing to preserve referential integrity, and will almost certainly cause display errors.
Here a much better solution is presented (a prerequisite is that you have installed the User merge extension):
I have a little awkward way to accomplish the bulk merge through a
work-around. Hope someone would find it useful! (Must have a little
string concatenation skills in spreadsheets; or one may use a python
or similar script; or use a text editor with bulk replacement
features)
Prepare a list of all SPAMuserIDs, store them in a spreadsheet or textfile. The list may be
prepared from the user creation logs. If you do have the
dB access, the Wiki_user table can be imported into a local list.
The post method used for submitting the Merge & Delete User form (by clicking the button) should be converted to a get method. This
will get us a long URL. See the second comment (by Matthew Simoneau)
dated 13/Jan/2009) at
http://www.mathworks.com/matlabcentral/newsreader/view_thread/242300
for the method.
The resulting URL string should be something like below:
http: //(Your Wiki domain)/Special:UserMerge?olduser=(OldUserNameHere)&newuser=(NewUserNameHere)&deleteuser=1&token=0d30d8b4033a9a523b9574ccf73abad8%2B\
Now, divide this URL into four sections:
A: http: //(Your Wiki domain)/Special:UserMerge?olduser=
B: (OldUserNameHere)
C: &newuser=(NewUserNameHere)&deleteuser=1
D: &token=0d30d8b4033a9a523b9574ccf73abad8%2B\
Now using a text editor or spreadsheet, prefix each spam userIDs with part A and Suffix each with Part C and D. Part C will include the
NewUser(which is a specially created single dummy userID). The Part D,
the Token string is a session-dependent token that will be changed per
user per session. So you will need to get a new token every time a new
session/batch of work is required.
With the above step, you should get a long list of URLs, each good to do a Merge&Delete operation for one user. We can now create a
simple HTML file, view it and use a batch downloader like DownThemAll
in Firefox.
Add two more pieces " Linktext" to each line at
beginning and end. Also add at top and at
bottom and save the file as (for eg:) userlist.html
Open the file in Firefox, use DownThemAll add-on and download all the files! Effectively, you are visiting the Merge&Delete page for
each user and clicking the button!
Although this might look a lengthy and tricky job at first, once you
follow this method, you can remove tens of thousands of users without
much manual efforts.
You can verify if the operation is going well by opening some of the
downloaded html files (or by looking through the recent changes in
another window).
One advantage is that it does not directly edit the
MySQL pages. Nor does it require direct database access.
I did a bit of rewriting to the quoted text, since the original text contains some flaws.

REST URIs and operations on an object that can be commented on, tagged, rated, etc

I'm doing research into a web API for my company, and it's starting to look like we might implement a RESTful one. I've read a couple of books about this now (O'Reilly's "RESTful web services" seeming the most useful) and have come up with the following set of URIs and operations for an object that can be commented on, tagged, and rated.
It doesn't really matter what the object is, as this scenario applies to many things on the net, but for the sake of argument lets say it's a movie.
Some of these seem to fit quite naturally, but others seem a bit forced (rating and tagging particularly) so does anybody have any suggestions about how these could be improved? I'll list them with the URI and then the supported verbs, and what I propose they would do.
/movies
GET = List movies
/movies/5
GET = Get movie 5
/movies/5/comments
GET = List comments on movie 5
POST = Create a new comment on movie 5
/movies/5/comments/8
GET = Get comment 8 on movie 5
POST = Reply to comment 8 on movie 5
PUT = Update comment 8 on movie 5
/movies/5/comments/8/flag
GET = Check whether the movies is flagged as inappropriate (404 if not)
PUT = Flag movie as inappropriate
/movies/5/rating
GET = Get the rating of the movie
POST = Add the user rating of the movie to the overall rating
Edit: My intention is that the movie object would contain its rating as a property, so I wouldn't really expect the GET method to be used here. The URI really exists so that the rating can be an individual resource that can be updated using the POST verb. I'm not sure if this is the best way of doing it, but I can't think of a better one
/movies/5/tags/tagname
GET = Check whether the movies is tagged with tagname (404 if not; but if it is tagged with the tag name should it return the actual tag resource by redirecting to something like /tags/tagname?)
PUT = Add tag tagname to the movie, creating the tag resource /tags/tagname if required
DELETE = Remove tag tagname from the movie, deleting the tag resource tags/tagname if nothing is tagged with it after this removal
Note that these wouldn't be the entire URIs, for example the URI to list the movies would support filtering, paging and sorting. For this I was planning on something like:
/movies/action;90s/rating,desc/20-40
Where:
action;90s is a semi-colon delimited set of filter criteria
rating,desc is the sort order and direction
20-40 is the range of item indices to get
Any comments about this API scheme too?
Edit #1
This post is getting quite long now! After reading some of the answers and comments, this is the changes from above I'm planning on making:
Tags will be handled as a group rather than individually, so they will be at:
/movies/5/tags
GET = List tags
POST = Union of specified tags and existing tags
PUT = Replace any current tags with specified tags
DELETE = Delete all tags
I'm still really not sure how to handle flagging a comment though. One option is that instead of POSTing to a comment replying to it, a comment object will include its parent so it can be POSTed to the general URI, i.e.
/movie/5/comment
POST = Create a new comment (which may be a reply to a comment)
I could then use the POST to a comment to flag it. But this still doesn't feel quite right.
/movie/5/comment/8
POST = Flag comment
Most of what you have looks good. There were just a couple of strange things I saw. When I put my URLs together, I try to follow these four principles.
Peel the onion
If you make the R in REST really be a resource then the resource URL should be able to be peeled back and still be meaningful. If it doesn't make sense you should rethink how to organize the resource. So in the case below, each makes sense. I am either looking at a specific item, or a collection of items.
/movies/horror/10/
/movies/horror/
/movies/
The following seems funny to me because flag isn't a resource, it's a property of the movie.
/movies/5/comments/8/flag -> Funny
/movies/5/comments/8/ -> Gives me all properties of comment including flag
Define the View
The last peice of the URL describes how to show the resource. The URL /movies/horror/ tells me I will have a collection of movies refined by horror. But there might be different ways I want to display that collection.
/movies/horror/simple
/movies/horror/expanded
The simple view might just be the title and an image. The expanded view would give a lot more information like description, synopsis, and ratings.
Helpers
After the resource has been limited and the proper view figured out, query string parameters are used to help the UI with the little stuff. The most common query string parameters I use are
p => Page
n => number of items to display
sortby => field to sort by
asc => sort ascending
So I could end up with a URL like
/movies/horror/default?p=12&n=50&sortby=name
This will give me the list of movies limited to horror movies with the default view; starting on page 12 with 50 movies per page where the movies are sorted by name.
Actions
The last thing needed are your action on the resource. The action are either collection based or item based.
/movies/horror/
GET -> Get resources as a list
POST -> Create, Update
/movies/horror/10/
GET -> Get resource as item
POST -> Update
I hope this helps.
I disagree with the edit. Queries should be defined by querystrings as per Martijn Laarman's post. i.e.:
/movies?genre=action&timeframe=90s&lbound=20&ubound=40&order=desc
Well, the way I see it some of the information you return now as objects could simply be added to the metadata of its parent object.
For instance, rating could be part of the response of /movies/5
<movie>
<title>..</title>
..
<rating url="movies/ratings/4">4</rating>
<tags>
<tag url="movies/tags/creative">creative</tag>
...
Removing a tag simply means posting the above response without that tag.
Also queries should go in URL variables, I believe:
/movies/?startsWith=Forrest%20G&orderBy=DateAdded
Based on my understanding of ROA (I'm only on chapter five of RESTful Web Services) it looks good to me.
This is an awesome initial draft for a spec of a REST API. The next step would to specify expected return codes (like you did with "404 No Tag Available"), acceptable Content-Types, and available content-types (e.g., HTML, JSON). Doing that should expose any additional chinks you'll need to hammer out.
#Nelson LaQuet:
Using the HTTP methods as they are actually defined gives you the safety of knowing that executing a GET on anything on a web site or service won't eat your data or otherwise mangle it. As an example (pointed out in RESTful Web Services) Google's Web Accelerator expects this behaviour -- as stated in the FAQ -- and presumably other services do too.
Also it gets you idempotency for free. That is doing a GET, DELETE, HEAD or PUT on a resource more than once is the same as doing it only once. Thus if your request fails then all you have to do is run it again.
This is not REST.
A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server). Servers must have the freedom to control their own namespace. Instead, allow servers to instruct clients on how to construct appropriate URIs, such as is done in HTML forms and URI templates, by defining those instructions within media types and link relations. [Failure here implies that clients are assuming a resource structure due to out-of band information, such as a domain-specific standard, which is the data-oriented equivalent to RPC's functional coupling].
http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven