Index: maintenance/archives/patch-change_tag_id.sql
===================================================================
--- maintenance/archives/patch-change_tag_id.sql	(revision 0)
+++ maintenance/archives/patch-change_tag_id.sql	(revision 0)
@@ -0,0 +1,5 @@
+-- Add a primary key to the change_tag table in order
+-- to enable us to build the review list extension
+
+ALTER TABLE /*$wgDBprefix*/change_tag
+  ADD ct_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT;
\ No newline at end of file
Index: maintenance/updaters.inc
===================================================================
--- maintenance/updaters.inc	(revision 65812)
+++ maintenance/updaters.inc	(working copy)
@@ -165,6 +165,9 @@
 		array( 'add_index', 'log_search', 'ls_field_val', 'patch-log_search-rename-index.sql' ),
 		array( 'add_index', 'change_tag', 'change_tag_rc_tag', 'patch-change_tag-indexes.sql' ),
 		array( 'add_field', 'redirect',	'rd_interwiki', 'patch-rd_interwiki.sql' ),
+
+		// Next Version
+		array( 'add_field', 'change_tag', 'ct_id', 'patch-change_tag_id.sql' ),
 	),
 
 	'sqlite' => array(
Index: skins/common/shared.css
===================================================================
--- skins/common/shared.css	(revision 65812)
+++ skins/common/shared.css	(working copy)
@@ -806,3 +806,12 @@
 	position: relative;
 	top: -16px;
 }
+
+.mw-collabwatchlist-tag-marker {
+	color: green;
+}
+
+.mw-collabwatchlist-tag-marker-4eyes {
+	background:transparent url(images/feed-icon.png) no-repeat scroll left center;
+	padding-left:16px;
+}
Index: extensions/CollabWatchlist/sql/collabwatchlistrevisiontag.sql
===================================================================
--- extensions/CollabWatchlist/sql/collabwatchlistrevisiontag.sql	(revision 0)
+++ extensions/CollabWatchlist/sql/collabwatchlistrevisiontag.sql	(revision 0)
@@ -0,0 +1,21 @@
+-- (c) Florian Hackenberger, 2009, GPL
+-- Table structure for `CollabWatchlist`
+-- Replace /*$wgDBprefix*/ with the proper prefix
+-- Replace /*$wgDBTableOptions*/ with the correct options
+
+-- Add page tracking the collab watchlist tags for revisions
+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistrevisiontag (
+  -- The id of this entry
+  rrt_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  -- Foreign key to change_tag.ct_id
+  ct_id integer unsigned NOT NULL,
+  -- Foreign key to collabwatchlist.rl_id
+  rl_id integer unsigned NOT NULL,
+  -- Foreign key to user.user_id
+  user_id int(10) unsigned NOT NULL,
+  
+  -- Comment for the tag
+  rrt_comment varchar(255),
+  
+  UNIQUE KEY (ct_id, rl_id)
+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: extensions/CollabWatchlist/sql/collabwatchlisttag.sql
===================================================================
--- extensions/CollabWatchlist/sql/collabwatchlisttag.sql	(revision 0)
+++ extensions/CollabWatchlist/sql/collabwatchlisttag.sql	(revision 0)
@@ -0,0 +1,18 @@
+-- (c) Florian Hackenberger, 2009, GPL
+-- Table structure for `CollabWatchlist`
+-- Replace /*$wgDBprefix*/ with the proper prefix
+-- Replace /*$wgDBTableOptions*/ with the correct options
+
+-- Add table defining collab watchlist tags
+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlisttag (
+  -- The id of this entry
+  rt_id integer unsigned  NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  -- Foreign key to collabwatchlist.rl_id
+  rl_id integer unsigned NOT NULL,
+  -- The name of the collabwatchlist tag (unique)
+  rt_name varbinary(255) NOT NULL,
+  -- Description of the tag
+  rt_description tinyblob NOT NULL default '',
+
+  UNIQUE KEY (rl_id, rt_name)
+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: extensions/CollabWatchlist/sql/collabwatchlistcategory.sql
===================================================================
--- extensions/CollabWatchlist/sql/collabwatchlistcategory.sql	(revision 0)
+++ extensions/CollabWatchlist/sql/collabwatchlistcategory.sql	(revision 0)
@@ -0,0 +1,16 @@
+-- (c) Florian Hackenberger, 2009, GPL
+-- Table structure for `CollabWatchlist`
+-- Replace /*$wgDBprefix*/ with the proper prefix
+-- Replace /*$wgDBTableOptions*/ with the correct options
+
+-- Add table defining the categories for collaborative watchlists
+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistcategory (
+  -- The id of this entry
+  rlc_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  -- Foreign key to collabwatchlist.rl_id
+  rl_id integer unsigned NOT NULL,
+  -- Foreign key to page.page_id
+  cat_page_id integer unsigned NOT NULL,
+  -- Whether the category is subtracted from or added to the collaborative watchlist
+  subtract boolean DEFAULT false
+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: extensions/CollabWatchlist/sql/collabwatchlistuser.sql
===================================================================
--- extensions/CollabWatchlist/sql/collabwatchlistuser.sql	(revision 0)
+++ extensions/CollabWatchlist/sql/collabwatchlistuser.sql	(revision 0)
@@ -0,0 +1,17 @@
+-- (c) Florian Hackenberger, 2009, GPL
+-- Table structure for `CollabWatchlist`
+-- Replace /*$wgDBprefix*/ with the proper prefix
+-- Replace /*$wgDBTableOptions*/ with the correct options
+
+-- Add table defining the collaborative watchlist users
+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistuser (
+  -- The id of this entry
+  rlu_id integer unsigned  NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  -- Foreign key to collabwatchlist.rl_id
+  rl_id integer unsigned NOT NULL,
+  -- Foreign key to user.user_id
+  user_id int(10) unsigned NOT NULL,
+  
+  -- Type of user
+  rlu_type ENUM("OWNER", "USER", "TRUSTED_EDITOR") DEFAULT "OWNER"
+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: extensions/CollabWatchlist/sql/collabwatchlist.sql
===================================================================
--- extensions/CollabWatchlist/sql/collabwatchlist.sql	(revision 0)
+++ extensions/CollabWatchlist/sql/collabwatchlist.sql	(revision 0)
@@ -0,0 +1,17 @@
+-- (c) Florian Hackenberger, 2009, GPL
+-- Table structure for `CollabWatchlist`
+-- Replace /*$wgDBprefix*/ with the proper prefix
+-- Replace /*$wgDBTableOptions*/ with the correct options
+
+
+-- Add table defining a collaborative watchlist
+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlist (
+  -- The id of the collaborative watchlist
+  rl_id integer unsigned  NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  -- The name of the collaborative watchlist (unique)
+  rl_name varbinary(255) NOT NULL,
+  -- Starting date in standard YMDHMS form.
+  rl_start binary(14) NOT NULL default '',
+
+  UNIQUE KEY (rl_name)
+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: extensions/CollabWatchlist/CollabWatchlist.body.php
===================================================================
--- extensions/CollabWatchlist/CollabWatchlist.body.php	(revision 0)
+++ extensions/CollabWatchlist/CollabWatchlist.body.php	(revision 0)
@@ -0,0 +1 @@
+<?php
Index: extensions/CollabWatchlist/CollabWatchlist.i18n.php
===================================================================
--- extensions/CollabWatchlist/CollabWatchlist.i18n.php	(revision 0)
+++ extensions/CollabWatchlist/CollabWatchlist.i18n.php	(revision 0)
@@ -0,0 +1,136 @@
+<?php
+$messages = array();
+ 
+$messages['en'] = array(
+	'collabwatchlist' => 'Collaborative watchlist',
+	'specialcollabwatchlist' => 'Collaborative watchlist special page',
+	'specialcollabwatchlist-desc' => 'Collaborative watchlist special page description',
+	'collabwatchlist-details'    => '{{PLURAL:$1|$1 category/page|$1 categories/pages}} on this collaborative watchlist.',
+	'collabwatchlisttagselect' => 'Tag',
+	'collabwatchlisttagcomment' => 'Comment',
+	'collabwatchlistsettagbutton' => 'Set tag',
+	'collabwatchlist-unset-tag' => 'x',
+	'collabwatchlisttools-view' => 'Display',
+	'collabwatchlisttools-edit' => 'Edit Categories',
+	'collabwatchlisttools-rawCategories' => 'Raw Edit Categories',
+	'collabwatchlisttools-rawTags' => 'Raw Edit Tags',
+	'collabwatchlisttools-rawUsers' => 'Raw Edit Users',
+	'collabwatchlisttools-delete' => 'Delete',
+	'collabwatchlistsall' => 'All lists',
+	'collabwatchlistfiltertags' => 'Filter tags',
+	'collabwatchlistedit-users-raw-submit' => 'Save',
+	'collabwatchlistedit-raw-title' => 'Raw edit categories',
+	'collabwatchlistedit-tags-raw-title' => 'Raw edit tags',
+	'collabwatchlistedit-users-raw-title' => 'Raw edit users',
+	'collabwatchlistedit-users-last-owner' => 'There must at least one owner',
+	'collabwatchlistedit-numitems'       => 'This collaborative watchlist contains {{PLURAL:$1|1 category|$1 categories}}',
+	'collabwatchlistedit-noitems'        => 'This collaborative watchlist contains no categories.',
+	'collabwatchlistedit-tags-numitems'       => 'This collaborative watchlist contains {{PLURAL:$1|1 tag|$1 tags}}',
+	'collabwatchlistedit-tags-noitems'        => 'This collaborative watchlist contains no tags.',
+	'collabwatchlistedit-users-numitems'       => 'This collaborative watchlist contains {{PLURAL:$1|1 user|$1 users}}',
+	'collabwatchlistedit-users-noitems'        => 'This collaborative watchlist contains no users.',
+	'collabwatchlistedit-raw-legend' => 'Raw edit collaborative watchlist categories',
+	'collabwatchlistedit-users-raw-legend' => 'Raw edit collaborative watchlist users',
+	'collabwatchlistedit-tags-raw-legend' => 'Raw edit collaborative watchlist tags',
+	'collabwatchlistedit-raw-explain'    => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
+	'collabwatchlistedit-tags-raw-explain'    => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
+	'collabwatchlistedit-users-raw-explain'    => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
+	'collabwatchlistedit-raw-titles'     => 'Categories:',
+	'collabwatchlistedit-tags-raw-titles'     => 'Tags:',
+	'collabwatchlistedit-users-raw-titles'     => 'Users:',
+	'collabwatchlistedit-normal-title'   => 'Edit categories',
+	'collabwatchlistedit-normal-legend'  => 'Remove categories from watchlist',
+	'collabwatchlistedit-normal-explain' => 'Categories on your watchlist are shown below.',
+	'collabwatchlistedit-tags-raw-submit'    => 'Save',
+	'collabwatchlistedit-normal-done'    => '{{PLURAL:$1|1 category was|$1 categories were}} removed from the collaborative watchlist:',
+	'collabwatchlistedit-tags-raw-done'    => 'The collaborative watchlist has been updated.',
+	'collabwatchlistedit-users-raw-done'    => 'The collaborative watchlist has been updated.',
+	'collabwatchlistedit-tags-raw-added'      => '{{PLURAL:$1|1 tag was|$1 tags were}} added:',
+	'collabwatchlistedit-users-raw-added'      => '{{PLURAL:$1|1 user was|$1 users were}} added:',
+	'collabwatchlistedit-tags-raw-removed'    => '{{PLURAL:$1|1 tag was|$1 tags were}} removed:',
+	'collabwatchlistedit-users-raw-removed'    => '{{PLURAL:$1|1 user was|$1 users were}} removed:',
+	'collabwatchlistinverttags' => 'Invert tag filter',
+	'collabwatchlistpatrol' => 'Patrol edits',
+	'collabwatchlisttools-newList' => 'New collaborative watchlist',
+	'collabwatchlistdelete-legend' => 'Delete a collaborative watchlist',
+	'collabwatchlistdelete-explain' => 'Deleting a collaborative watchlist will remove all traces of the watchlist. Tags which were set on the edits are preserved.',
+	'collabwatchlistdelete-submit' => 'Delete',
+	'collabwatchlistdelete-title' => 'Delete collaborative watchlist',
+	'collabwatchlistedit-set-tags-numitems' => 'This collaborative watchlist has {{PLURAL:$1|1 tag|$1 tags}} set',
+	'collabwatchlistedit-set-tags-noitems' => 'This collaborative watchlist has no tags set',
+	'collabwatchlistnew-legend' => 'Create a new collaborative watchlist',
+	'collabwatchlistnew-explain' => 'The name of the list has to be unique.',
+	'collabwatchlistnew-name' => 'List name',
+	'collabwatchlistnew-submit' => 'Create',
+	'collabwatchlistedit-raw-done' => 'The collaborative watchlist has been updated',
+	'collabwatchlistedit-raw-added' => '{{PLURAL:$1|1 page/category was|$1 pages/categories were}} added:',
+	'collabwatchlistedit-raw-removed' => '{{PLURAL:$1|1 page/category was|$1 pages/categories were}} removed',
+	'collabwatchlistedit-normal-submit' => 'Speichern',
+);
+
+$messages['de'] = array(
+	'collabwatchlist' => 'Kollaborative Beobachtungsliste',
+	'specialcollabwatchlist' => 'Kollaborative Beobachtungsliste Spezialseite',
+	'specialcollabwatchlist-desc' => 'Kollaborative Beobachtungsliste Spezialseite Beschreibung',
+	'collabwatchlist-details'    => '{{PLURAL:$1|$1 Kategorie/Seite|$1 Kategorien/Seiten}} auf dieser kollaborativen Beobachtungsliste.',
+	'collabwatchlisttagselect' => 'Tag',
+	'collabwatchlisttagcomment' => 'Kommentar',
+	'collabwatchlistsettagbutton' => 'Tag setzen',
+	'collabwatchlist-unset-tag' => 'x',
+	'collabwatchlisttools-view' => 'Anzeigen',
+	'collabwatchlisttools-edit' => 'Kategorien bearbeiten',
+	'collabwatchlisttools-rawCategories' => 'Listenformat Kategorien bearbeiten',
+	'collabwatchlisttools-rawTags' => 'Listenformat Tags bearbeiten',
+	'collabwatchlisttools-rawUsers' => 'Listenformat Benutzer bearbeiten',
+	'collabwatchlisttools-delete' => 'Löschen',
+	'collabwatchlistsall' => 'Alle Listen',
+	'collabwatchlistfiltertags' => 'Tags filtern',
+	'collabwatchlistedit-users-raw-submit' => 'Speichern',
+	'collabwatchlistedit-raw-title' => 'Kategorien im Listenformat bearbeiten',
+	'collabwatchlistedit-tags-raw-title' => 'Tags im Listenformat bearbeiten',
+	'collabwatchlistedit-users-raw-title' => 'Benutzer im Listenformat bearbeiten',
+	'collabwatchlistedit-users-last-owner' => 'Es muss zumindest einen Owner geben',
+	'collabwatchlistedit-numitems'       => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Kategorie |$1 Kategorien}}',
+	'collabwatchlistedit-noitems'        => 'Diese kollaborative Beobachtungsliste ist leer.',
+	'collabwatchlistedit-tags-numitems'       => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Tag |$1 Tags}}',
+	'collabwatchlistedit-tags-noitems'        => 'Diese kollaborative Beobachtungsliste ist leer.',
+	'collabwatchlistedit-users-numitems'       => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Benutzer |$1 Benutzer}}',
+	'collabwatchlistedit-users-noitems'        => 'Diese kollaborative Beobachtungsliste ist leer.',
+	'collabwatchlistedit-raw-legend' => 'Kategorien der kollaborativen Beobachtungsliste im Listenformat bearbeiten',
+	'collabwatchlistedit-users-raw-legend' => 'Benutzer der Kollaborativen Beobachtungsliste im Listenformat bearbeiten',
+	'collabwatchlistedit-tags-raw-legend' => 'Tags der kollaborativen Beobachtungsliste im Listenformat bearbeiten',
+	'collabwatchlistedit-raw-explain'    => 'Dies sind die Kategorien der kollaborativen Beobachtungsliste im Listenformat. Die Einträge können zeilenweise gelöscht oder hinzugefügt werden.',
+	'collabwatchlistedit-tags-raw-explain'    => 'Dies sind die Tags der kollaborativen Beobachtungsliste im Listenformat. Die Einträge können zeilenweise gelöscht oder hinzugefügt werden.',
+	'collabwatchlistedit-users-raw-explain'    => 'Dies sind die Benutzer der kollaborativen Beobachtungsliste im Listenformat. Die Einträge können zeilenweise gelöscht oder hinzugefügt werden.',
+	'collabwatchlistedit-raw-titles'     => 'Kategorien:',
+	'collabwatchlistedit-tags-raw-titles'     => 'Tags:',
+	'collabwatchlistedit-users-raw-titles'     => 'Benutzer:',
+	'collabwatchlistedit-normal-title'   => 'Kategorien bearbeiten',
+	'collabwatchlistedit-normal-legend'  => 'Kategorien von der Beobachtungsliste entfernen',
+	'collabwatchlistedit-normal-explain' => 'Dies sind die Kategorien der kollaborativen Beobachtungsliste. Um Einträge zu entfernen, markiere die Kästchen neben den Einträgen und klicke am Ende der Seite auf „Einträge entfernen“.',
+	'collabwatchlistedit-tags-raw-submit'    => 'Speichern',
+	'collabwatchlistedit-normal-done'    => '{{PLURAL:$1|1 Kategorie wurde|$1 Kategorien wurden}} von der kollaborativen Beobachtungsliste entfernt:',
+	'collabwatchlistedit-tags-raw-done'    => 'Die kollaborative  Beobachtungsliste wurde gespeichert.',
+	'collabwatchlistedit-users-raw-done'    => 'Die kollaborative  Beobachtungsliste wurde gespeichert.',
+	'collabwatchlistedit-tags-raw-added'      => '{{PLURAL:$1|1 Tag wurde|$1 Tags wurden}} hinzugefügt:',
+	'collabwatchlistedit-users-raw-added'      => '{{PLURAL:$1|1 Benutzer wurde|$1 Benutzer wurden}} hinzugefügt:',
+	'collabwatchlistedit-tags-raw-removed'    => '{{PLURAL:$1|1 Tag wurde|$1 Tags wurden}} entfernt:',
+	'collabwatchlistedit-users-raw-removed'    => '{{PLURAL:$1|1 Benutzer wurde|$1 Benutzer wurden}} entfernt:',
+	'collabwatchlistinverttags' => 'Tag Filter umkehren',
+	'collabwatchlistpatrol' => 'Änderungen kontrolliert',
+	'collabwatchlisttools-newList' => 'Neue kollaborative Beobachtungsliste',
+	'collabwatchlistdelete-legend' => 'Kollaborative Beobachtungsliste löschen',
+	'collabwatchlistdelete-explain' => 'Beim Löschen werden alle Informationen die mit der Beobachtungsliste in Zusammenhang stehen gelöscht. Tags die Änderungen zugewiesen wurden bleiben erhalten.',
+	'collabwatchlistdelete-submit' => 'Löschen',
+	'collabwatchlistdelete-title' => 'Kollaborative Beobachtungsliste löschen',
+	'collabwatchlistedit-set-tags-numitems' => 'Diese kollaborative Beobachtungsliste hat {{PLURAL:$1|1 Tag|$1 Tags}} gesetzt',
+	'collabwatchlistedit-set-tags-noitems' => 'Diese kollaborative Beobachtungsliste hat keine Tags gesetzt',
+	'collabwatchlistnew-legend' => 'Neue kollaborative Beobachtungsliste anlegen',
+	'collabwatchlistnew-explain' => 'Der Name der Liste muss eindeutig sein.',
+	'collabwatchlistnew-name' => 'Name der Liste',
+	'collabwatchlistnew-submit' => 'Anlegen',
+	'collabwatchlistedit-raw-done' => 'The kollaborative Beobachtungsliste wurde gespeichert.',
+	'collabwatchlistedit-raw-added' => '{{PLURAL:$1|1 Kategorie/Seite wurde|$1 Kategorien/Seiten wurden}} hinzugefügt:',
+	'collabwatchlistedit-raw-removed' => '{{PLURAL:$1|1 Kategorie/Seite wurde|$1 Kategorien/Seiten wurden}} entfernt:',
+	'collabwatchlistedit-normal-submit' => 'Speichern',
+);
Index: extensions/CollabWatchlist/CollabWatchlist.php
===================================================================
--- extensions/CollabWatchlist/CollabWatchlist.php	(revision 0)
+++ extensions/CollabWatchlist/CollabWatchlist.php	(revision 0)
@@ -0,0 +1,70 @@
+<?php
+# Alert the user that this is not a valid entry point to MediaWiki if they try to access the special pages file directly.
+if (!defined('MEDIAWIKI')) {
+	echo <<<EOT
+To install my extension, put the following line in LocalSettings.php:
+require_once( "\$IP/extensions/CollabWatchlist/CollabWatchlist.php" );
+EOT;
+	exit( 1 );
+}
+
+
+$wgExtensionCredits['specialpage'][] = array(
+	'name' => 'CollabWatchlist',
+	'author' =>'Florian Hackenberger', 
+	'url' => 'http://www.mediawiki.org/wiki/User:Flohack', 
+	'description' => 'Provides collaborative watchlists based on categories',
+	'descriptionmsg' => 'specialcollabwatchlist-desc',
+);
+
+
+# Autoload our classes
+$wgDir = dirname(__FILE__) . '/';
+$wgCollabWatchlistIncludes = $wgDir . 'includes/';
+$wgExtensionMessagesFiles['CollabWatchlist'] = $wgDir . 'CollabWatchlist.i18n.php';
+
+//$wgAutoloadClasses['CollabWatchlist'] = $wgDir . 'CollabWatchlist.body.php'; # Tell MediaWiki to load the extension body.
+$wgAutoloadClasses['SpecialCollabWatchlist'] = $wgCollabWatchlistIncludes . 'SpecialCollabWatchlist.php';
+$wgAutoloadClasses['CollabWatchlistChangesList'] = $wgCollabWatchlistIncludes . 'CollabWatchlistChangesList.php';
+$wgAutoloadClasses['CategoryTreeManip'] = $wgCollabWatchlistIncludes . 'CategoryTreeManip.php';
+$wgAutoloadClasses['CollabWatchlistEditor'] = $wgCollabWatchlistIncludes . 'CollabWatchlistEditor.php';
+#$wgExtensionAliasesFiles['CollabWatchlist'] = $wgDir . 'CollabWatchlist.alias.php';
+
+$wgSpecialPages['Collabwatchlist'] = 'SpecialCollabWatchlist'; # Let MediaWiki know about your new special page.
+
+$wgHooks['LoadExtensionSchemaUpdates'][] = 'fnCollabWatchlistDbSchema';
+
+function fnCollabWatchlistDbSchema() {
+    global $wgExtNewTables;
+    $wgSql = dirname(__FILE__) . '/sql/';
+    $wgExtNewTables[] = array('collabwatchlist',  $wgSql . 'collabwatchlist.sql');
+    $wgExtNewTables[] = array('collabwatchlistuser',  $wgSql . 'collabwatchlistuser.sql');
+    $wgExtNewTables[] = array('collabwatchlistcategory',  $wgSql . 'collabwatchlistcategory.sql');
+    $wgExtNewTables[] = array('collabwatchlistrevisiontag', $wgSql . 'collabwatchlistrevisiontag.sql');
+    $wgExtNewTables[] = array('collabwatchlisttag', $wgSql . 'collabwatchlisttag.sql');
+    return true;
+}
+
+$wgCollabWatchlistNSPrefix = 'CollabWatchlist';
+$wgCollabWatchlistPermissionDeniedPage = 'CollabWatchlistPermissionDenied';
+
+/**#@+
+ * Collaborative watchlist user types
+ * This defines constants for the collabwatchlistuser.rlu_type
+ */
+define( 'COLLABWATCHLISTUSER_OWNER',    'OWNER' ); // owners are allowed to edit the list
+define( 'COLLABWATCHLISTUSER_OWNER_TEXT',    'Owner' ); // owners are allowed to edit the list
+define( 'COLLABWATCHLISTUSER_USER',     'USER' ); // users are allowed to view the list and tag edits
+define( 'COLLABWATCHLISTUSER_USER_TEXT',     'User' ); // users are allowed to view the list and tag edits
+define( 'COLLABWATCHLISTUSER_TRUSTED_EDITOR', 'TRUSTED_EDITOR' ); // trusted editors are used to filter edits which don't require a review
+define( 'COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT', 'TrustedEditor' ); // trusted editors are used to filter edits which don't require a review
+
+function fnCollabWatchlistUserTypeToText( $userType ) {
+	if( $userType === COLLABWATCHLISTUSER_OWNER )
+		return COLLABWATCHLISTUSER_OWNER_TEXT;
+	if( $userType === COLLABWATCHLISTUSER_USER )
+		return COLLABWATCHLISTUSER_USER_TEXT;
+	if( $userType === COLLABWATCHLISTUSER_TRUSTED_EDITOR )
+		return COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT;
+}
+/**#@-*/
\ No newline at end of file
Index: extensions/CollabWatchlist/includes/SpecialCollabWatchlist.php
===================================================================
--- extensions/CollabWatchlist/includes/SpecialCollabWatchlist.php	(revision 0)
+++ extensions/CollabWatchlist/includes/SpecialCollabWatchlist.php	(revision 0)
@@ -0,0 +1,603 @@
+<?php
+class SpecialCollabWatchlist extends SpecialPage {
+	function __construct() {
+		parent::__construct( 'CollabWatchlist' );
+	}
+ 
+	function execute( $par ) {
+		global $wgUser, $wgOut, $wgLang, $wgRequest;
+		global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
+		global $wgEnotifWatchlist;
+		
+		// Add feed links
+		$wlToken = $wgUser->getOption( 'watchlisttoken' );
+		if (!$wlToken) {
+			$wlToken = sha1( mt_rand() . microtime( true ) );
+			$wgUser->setOption( 'watchlisttoken', $wlToken );
+			$wgUser->saveSettings();
+		}
+		
+		global $wgServer, $wgScriptPath, $wgFeedClasses;
+		$apiParams = array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
+							'wlowner' => $wgUser->getName(), 'wltoken' => $wlToken );
+		$feedTemplate = wfScript('api').'?';
+		
+		foreach( $wgFeedClasses as $format => $class ) {
+			$theseParams = $apiParams + array( 'feedformat' => $format );
+			$url = $feedTemplate . wfArrayToCGI( $theseParams );
+			$wgOut->addFeedLink( $format, $url );
+		}
+	
+		$skin = $wgUser->getSkin();
+		$specialTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$wgOut->setRobotPolicy( 'noindex,nofollow' );
+	
+		# Anons don't get a watchlist
+		if( $wgUser->isAnon() ) {
+			$wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
+			$llink = $skin->linkKnown(
+				SpecialPage::getTitleFor( 'Userlogin' ), 
+				wfMsgHtml( 'loginreqlink' ),
+				array(),
+				array( 'returnto' => $specialTitle->getPrefixedText() )
+			);
+			$wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+			return;
+		}
+	
+		$wgOut->setPageTitle( wfMsg( 'collabwatchlist' ) );
+	
+		$listIdsAndNames = CollabWatchlistChangesList::getCollabWatchlistIdAndName($wgUser->getId());
+		$sub  = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
+		$sub .= '<br />' . CollabWatchlistEditor::buildTools( $listIdsAndNames, $wgUser->getSkin() );
+		$wgOut->setSubtitle( $sub );
+	
+		$uid = $wgUser->getId();
+		
+		// The filter form has one checkbox for each tag, build an array
+		$postValues = $wgRequest->getValues();
+		$tagFilter = array();
+		foreach( $postValues as $key => $value ) {
+			if( stripos($key, 'collaborative-watchlist-filtertag-') === 0 ) {
+				$tagFilter[] = $postValues[$key];
+			}
+		}
+		// Alternative syntax for requests from links (show / hide ...)
+		if( empty($tagFilter) ) {
+			$tagFilter = explode('|', $wgRequest->getVal('filterTags'));
+		}
+	
+		$defaults = array(
+		/* float */ 'days'      => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+		/* bool  */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
+		/* bool  */ 'hideBots'  => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
+		/* bool  */ 'hideAnons' => (int)$wgUser->getBoolOption( 'watchlisthideanons' ),
+		/* bool  */ 'hideLiu'   => (int)$wgUser->getBoolOption( 'watchlisthideliu' ),
+		/* bool  */ 'hidePatrolled' => (int)$wgUser->getBoolOption( 'watchlisthidepatrolled' ),
+		/* bool  */ 'hideOwn'   => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
+		/* ?     */ 'globalwatch' => 'all',
+		/* ?     */ 'invert'    => false,
+		/* ?     */ 'invertTags'=> false,
+		/* ?     */ 'filterTags'=> '',
+		);
+	
+		extract($defaults);
+	
+		# Extract variables from the request, falling back to user preferences or
+		# other default values if these don't exist
+		$prefs['days']      = floatval( $wgUser->getOption( 'watchlistdays' ) );
+		$prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
+		$prefs['hidebots']  = $wgUser->getBoolOption( 'watchlisthidebots' );
+		$prefs['hideanons'] = $wgUser->getBoolOption( 'watchlisthideanon' );
+		$prefs['hideliu']   = $wgUser->getBoolOption( 'watchlisthideliu' );
+		$prefs['hideown' ]  = $wgUser->getBoolOption( 'watchlisthideown' );
+		$prefs['hidepatrolled' ] = $wgUser->getBoolOption( 'watchlisthidepatrolled' );
+		$prefs['invertTags' ] = $wgUser->getBoolOption( 'collabwatchlistinverttags' );
+		$prefs['filterTags' ] = $wgUser->getOption( 'collabwatchlistfiltertags' );
+		
+		# Get query variables
+		$days      = $wgRequest->getVal(  'days'     , $prefs['days'] );
+		$hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
+		$hideBots  = $wgRequest->getBool( 'hideBots' , $prefs['hidebots'] );
+		$hideAnons = $wgRequest->getBool( 'hideAnons', $prefs['hideanons'] );
+		$hideLiu   = $wgRequest->getBool( 'hideLiu'  , $prefs['hideliu'] );
+		$hideOwn   = $wgRequest->getBool( 'hideOwn'  , $prefs['hideown'] );
+		$hidePatrolled = $wgRequest->getBool( 'hidePatrolled'  , $prefs['hidepatrolled'] );
+		$filterTags = implode('|', $tagFilter);
+		$invertTags = $wgRequest->getBool( 'invertTags'  , $prefs['invertTags'] );
+		
+		# Get collabwatchlist value, if supplied, and prepare a WHERE fragment
+		$collabWatchlist = $wgRequest->getIntOrNull( 'collabwatchlist' );
+		$invert = $wgRequest->getBool( 'invert' );
+		if( !is_null( $collabWatchlist ) && $collabWatchlist !== 'all') {
+			$collabWatchlist = intval( $collabWatchlist );
+		} else {
+			$collabWatchlist = 0;
+		}
+		
+		if( ( $mode = CollabWatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
+			$editor = new CollabWatchlistEditor();
+			$editor->execute( $collabWatchlist, $listIdsAndNames, $wgOut, $wgRequest, $mode );
+			return;
+		}
+	
+		$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+		$recentchanges = $dbr->tableName( 'recentchanges' );
+	
+		$nitems = $dbr->selectField( 'collabwatchlistcategory', 'COUNT(*)',
+			$collabWatchlist == 0 ? array() : array('rl_id' => $collabWatchlist
+			), __METHOD__ );
+		if( $nitems == 0 ) {
+			$wgOut->addWikiMsg( 'nowatchlist' );
+			return;
+		}
+	
+		// Dump everything here
+		$nondefaults = array();
+	
+		wfAppendToArrayIfNotDefault( 'days'     , $days          , $defaults, $nondefaults);
+		wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
+		wfAppendToArrayIfNotDefault( 'hideBots' , (int)$hideBots , $defaults, $nondefaults);
+		wfAppendToArrayIfNotDefault( 'hideAnons', (int)$hideAnons, $defaults, $nondefaults );
+		wfAppendToArrayIfNotDefault( 'hideLiu'  , (int)$hideLiu  , $defaults, $nondefaults );
+		wfAppendToArrayIfNotDefault( 'hideOwn'  , (int)$hideOwn  , $defaults, $nondefaults);
+		wfAppendToArrayIfNotDefault( 'collabwatchlist', $collabWatchlist     , $defaults, $nondefaults);
+		wfAppendToArrayIfNotDefault( 'hidePatrolled', (int)$hidePatrolled, $defaults, $nondefaults );
+		wfAppendToArrayIfNotDefault( 'filterTags', $filterTags   , $defaults, $nondefaults );
+		wfAppendToArrayIfNotDefault( 'invertTags', $invertTags   , $defaults, $nondefaults );
+		
+		if( $days <= 0 ) {
+			$andcutoff = '';
+		} else {
+			$andcutoff = "rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
+		}
+	
+		# If the watchlist is relatively short, it's simplest to zip
+		# down its entirety and then sort the results.
+	
+		# If it's relatively long, it may be worth our while to zip
+		# through the time-sorted page list checking for watched items.
+	
+		# Up estimate of watched items by 15% to compensate for talk pages...
+	
+		# Toggles
+		$andHideOwn   = $hideOwn   ? "rc_user != $uid" : '';
+		$andHideBots  = $hideBots  ? "rc_bot = 0" : '';
+		$andHideMinor = $hideMinor ? "rc_minor = 0" : '';
+		$andHideLiu   = $hideLiu   ? "rc_user = 0" : '';
+		$andHideAnons = $hideAnons ? "rc_user != 0" : '';
+		$andHidePatrolled = $wgUser->useRCPatrol() && $hidePatrolled ? "rc_patrolled != 1" : '';
+	
+		# Toggle watchlist content (all recent edits or just the latest)
+		if( $wgUser->getOption( 'extendwatchlist' )) {
+			$andLatest='';
+	 		$limitWatchlist = intval( $wgUser->getOption( 'wllimit' ) );
+			$usePage = false;
+		} else {
+		# Top log Ids for a page are not stored
+			$andLatest = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG;
+			$limitWatchlist = 0;
+			$usePage = true;
+		}
+	
+		# Show a message about slave lag, if applicable
+		if( ( $lag = $dbr->getLag() ) > 0 )
+			$wgOut->showLagWarning( $lag );
+	
+		# Create output form
+		$form  = Xml::fieldset( wfMsg( 'watchlist-options' ), false, array( 'id' => 'mw-watchlist-options' ) );
+	
+		# Show watchlist header
+		$form .= wfMsgExt( 'collabwatchlist-details', array( 'parseinline' ), $wgLang->formatNum( $nitems ) );
+	
+		if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
+			$form .= wfMsgExt( 'wlheader-enotif', 'parse' ) . "\n";
+		}
+		$form .= '<hr />';
+		
+		$tables = array( 'recentchanges', 'categorylinks' );
+		$fields = array( "{$recentchanges}.*" );
+		$categoryClause = $this->wlGetFilterClauseForCollabWatchlistIds($collabWatchlist, 'cl_to', 'rc_cur_id');
+		// If this collaborative watchlist does not contain any categories, add a clause which gives
+		// us an empty result
+		$conds = isset($categoryClause) ? array($categoryClause) : array('false');
+		$join_conds = array(
+			'categorylinks' => array('LEFT OUTER JOIN', "rc_cur_id=cl_from"),
+		);
+		if( !empty($tagFilter) ) {
+			// The tag filter causes a query runtime of O(MxN), where M is relative to the number
+			// of recentchanges we select (from a table which is purged periodically, limited to 250)
+			// and N is relative the number of change_tag entries for a recentchange. Doing it 
+			// the other way around (selecting from change_tag first, is probably slower, as the
+			// change_tag table is never purged.
+			// Using the tag_summary table for filtering is difficult, at least I have been unable to
+			// find a common SQL compliant way for using regular expressions which works across Postgre / Mysql
+			// Furthermore, ChangeTags does not seem to prevent tags containing ',' from being set,
+			// which renders tag_summary quite unusable
+			if( $invertTags ) {
+				$filter = 'EXISTS ';
+			} else {
+				$filter = 'NOT EXISTS ';
+			}
+			$filter .= '(select ct_rc_id from change_tag 
+					JOIN collabwatchlistrevisiontag ON collabwatchlistrevisiontag.ct_id = change_tag.ct_id 
+					WHERE ct_rc_id = recentchanges.rc_id AND ct_tag ';
+			if( count($tagFilter) > 1 )
+				$filter .= 'IN (' . $dbr->makeList($tagFilter) . '))';
+			else
+				$filter .= ' = ' . $dbr->addQuotes(current($tagFilter)) . ')';
+			$conds[] = $filter;
+		}
+		$options = array( 'ORDER BY' => 'rc_timestamp DESC' );
+		if( $limitWatchlist ) {
+			$options['LIMIT'] = $limitWatchlist;
+		}
+		if( $andcutoff ) $conds[] = $andcutoff;
+		if( $andLatest ) $conds[] = $andLatest;
+		if( $andHideOwn ) $conds[] = $andHideOwn;
+		if( $andHideBots ) $conds[] = $andHideBots;
+		if( $andHideMinor ) $conds[] = $andHideMinor;
+		if( $andHideLiu ) $conds[] = $andHideLiu;
+		if( $andHideAnons ) $conds[] = $andHideAnons;
+		if( $andHidePatrolled ) $conds[] = $andHidePatrolled;
+	
+		$rollbacker = $wgUser->isAllowed('rollback');
+		if ( $usePage || $rollbacker ) {
+			$tables[] = 'page';
+			$join_conds['page'] = array('LEFT JOIN','rc_cur_id=page.page_id');
+			if ($rollbacker) 
+				$fields[] = 'page_latest';
+		}
+	
+		ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
+		wfRunHooks('SpecialCollabWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
+		
+		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
+		$numRows = $dbr->numRows( $res );
+	
+		/* Start bottom header */
+	
+		$wlInfo = '';
+		if( $days >= 1 ) {
+			$wlInfo = wfMsgExt( 'rcnote', 'parseinline',
+					$wgLang->formatNum( $numRows ),
+					$wgLang->formatNum( $days ),
+					$wgLang->timeAndDate( wfTimestampNow(), true ),
+					$wgLang->date( wfTimestampNow(), true ),
+					$wgLang->time( wfTimestampNow(), true )
+				) . '<br />';
+		} elseif( $days > 0 ) {
+			$wlInfo = wfMsgExt( 'wlnote', 'parseinline',
+					$wgLang->formatNum( $numRows ),
+					$wgLang->formatNum( round($days*24) )
+				) . '<br />';
+		}
+	
+		$cutofflinks = "\n" . $this->wlCutoffLinks( $days, 'CollabWatchlist', $nondefaults ) . "<br />\n";
+	
+		$thisTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
+	
+		# Spit out some control panel links
+		$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideminor', 'hideMinor', $hideMinor );
+		$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidebots', 'hideBots', $hideBots );
+		$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideanons', 'hideAnons', $hideAnons );
+		$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideliu', 'hideLiu', $hideLiu );
+		$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidemine', 'hideOwn', $hideOwn );
+	
+		if( $wgUser->useRCPatrol() ) {
+			$links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidepatr', 'hidePatrolled', $hidePatrolled );
+		}
+	
+		# Namespace filter and put the whole form together.
+		$form .= $wlInfo;
+		$form .= $cutofflinks;
+		$form .= $wgLang->pipeList( $links );
+		$form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $thisTitle->getLocalUrl() ) );
+		$form .= '<hr /><p>';
+		$tagsAndInfo = CollabWatchlistChangesList::getValidTagsAndInfo(array_keys($listIdsAndNames));
+		if(count($tagsAndInfo) > 0) {
+			$form .= wfMsg('collabwatchlistfiltertags') . ':&nbsp;&nbsp;';
+		}
+		foreach( $tagsAndInfo as $tag => $tagInfo ) {
+			$tagAttr = array(
+				'name' => 'collaborative-watchlist-filtertag-' . $tag,
+				'type' => 'checkbox',
+				'value' => $tag);
+			if (in_array($tag, $tagFilter) ) {
+				$tagAttr['checked'] = 'checked';
+			}
+			$form .= Xml::element('input', $tagAttr) . '&nbsp;' . Xml::label( $tag, 'collaborative-watchlist-filtertag-' . $tag ) . '&nbsp;';
+		}
+		if(count($tagsAndInfo) > 0) {
+			$form .= '<br />';
+		}
+		$form .= Xml::checkLabel( wfMsg('collabwatchlistinverttags'), 'invertTags', 'nsinvertTags', $invertTags ) . '<br />';
+		$form .= CollabWatchlistChangesList::collabWatchlistSelector( $listIdsAndNames, $collabWatchlist, '', 'collabwatchlist', wfMsg( 'collabwatchlist' )) . '&nbsp;';
+		$form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . '&nbsp;';
+		$form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
+		$form .= Xml::hidden( 'days', $days );
+		if( $hideMinor )
+			$form .= Xml::hidden( 'hideMinor', 1 );
+		if( $hideBots )
+			$form .= Xml::hidden( 'hideBots', 1 );
+		if( $hideAnons )
+			$form .= Xml::hidden( 'hideAnons', 1 );
+		if( $hideLiu )
+			$form .= Xml::hidden( 'hideLiu', 1 );
+		if( $hideOwn )
+			$form .= Xml::hidden( 'hideOwn', 1 );
+		if( $invert )
+			$form .= Xml::hidden( 'invert', 1 );
+		if( $wgUser->useRCPatrol() )
+			if( $hidePatrolled )
+				$form .= Xml::hidden( 'hidePatrolled', 1);
+		$form .= Xml::closeElement( 'form' );
+		$form .= Xml::closeElement( 'fieldset' );
+		$wgOut->addHTML( $form );
+	
+		$wgOut->addHTML( ChangesList::flagLegend() );
+	
+		# If there's nothing to show, stop here
+		if( $numRows == 0 ) {
+			$wgOut->addWikiMsg( 'watchnochange' );
+			return;
+		}
+	
+		/* End bottom header */
+	
+		/* Do link batch query */
+		$linkBatch = new LinkBatch;
+		while ( $row = $dbr->fetchObject( $res ) ) {
+			$userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
+			if ( $row->rc_user != 0 ) {
+				$linkBatch->add( NS_USER, $userNameUnderscored );
+			}
+			$linkBatch->add( NS_USER_TALK, $userNameUnderscored );
+	
+			$linkBatch->add( $row->rc_namespace, $row->rc_title );
+		}
+		$linkBatch->execute();
+		$dbr->dataSeek( $res, 0 );
+	
+		$list = CollabWatchlistChangesList::newFromUser( $wgUser );
+		$list->setWatchlistDivs();
+		
+		$s = $list->beginRecentChangesList();
+		$counter = 1;
+		while ( $obj = $dbr->fetchObject( $res ) ) {
+			# Make RC entry
+			$rc = RecentChange::newFromRow( $obj );
+			$rc->counter = $counter++;
+	
+			if ( $wgShowUpdatedMarker ) {
+				$updated = $obj->wl_notificationtimestamp;
+			} else {
+				$updated = false;
+			}
+	
+			if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+				$rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
+					'COUNT(*)',
+					array(
+						'wl_namespace' => $obj->rc_namespace,
+						'wl_title' => $obj->rc_title,
+					),
+					__METHOD__ );
+			} else {
+				$rc->numberofWatchingusers = 0;
+			}
+			
+			$tags = $this->wlTagsForRevision($obj->rc_this_oldid, array($collabWatchlist), $invert);
+//			if( isset($tags) ) {
+//				// Filter recentchanges which contain unwanted tags
+//				$tagNames = array();
+//				foreach($tags as $tagInfo) {
+//					$tagNames[] = $tagInfo['ct_tag'];
+//				}
+//				$unwantedTagsFound = array_intersect($tagFilter, $tagNames);
+//				if( !empty($unwantedTagsFound) )
+//					continue;
+//			}
+			$attrs = $rc->getAttributes();
+			$attrs['collabwatchlist_tags'] = $tags;
+			$rc->setAttribs($attrs);
+	
+			$s .= $list->recentChangesLine( $rc, $updated, $counter );
+		}
+		$s .= $list->endRecentChangesList();
+	
+		$dbr->freeResult( $res );
+		$wgOut->addHTML( $s );
+	}
+	
+	function wlShowHideLink( $options, $message, $name, $value ) {
+		global $wgUser;
+	
+		$showLinktext = wfMsgHtml( 'show' );
+		$hideLinktext = wfMsgHtml( 'hide' );
+		$title = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$skin = $wgUser->getSkin();
+	
+		$label = $value ? $showLinktext : $hideLinktext;
+		$options[$name] = 1 - (int) $value;
+	
+		return wfMsgHtml( $message, $skin->linkKnown( $title, $label, array(), $options ) );
+	}
+	
+	
+	function wlHoursLink( $h, $page, $options = array() ) {
+		global $wgUser, $wgLang, $wgContLang;
+	
+		$sk = $wgUser->getSkin();
+		$title = Title::newFromText( $wgContLang->specialPage( $page ) );
+		$options['days'] = ($h / 24.0);
+	
+		$s = $sk->linkKnown(
+			$title,
+			$wgLang->formatNum( $h ),
+			array(),
+			$options
+		);
+	
+		return $s;
+	}
+	
+	function wlDaysLink( $d, $page, $options = array() ) {
+		global $wgUser, $wgLang, $wgContLang;
+	
+		$sk = $wgUser->getSkin();
+		$title = Title::newFromText( $wgContLang->specialPage( $page ) );
+		$options['days'] = $d;
+		$message = ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) );
+	
+		$s = $sk->linkKnown(
+			$title,
+			$message,
+			array(),
+			$options
+		);
+	
+		return $s;
+	}
+	
+	/**
+	 * Returns html
+	 */
+	function wlCutoffLinks( $days, $page = 'CollabWatchlist', $options = array() ) {
+		global $wgLang;
+	
+		$hours = array( 1, 2, 6, 12 );
+		$days = array( 1, 3, 7 );
+		$i = 0;
+		foreach( $hours as $h ) {
+			$hours[$i++] = $this->wlHoursLink( $h, $page, $options );
+		}
+		$i = 0;
+		foreach( $days as $d ) {
+			$days[$i++] = $this->wlDaysLink( $d, $page, $options );
+		}
+		return wfMsgExt('wlshowlast',
+			array('parseinline', 'replaceafter'),
+			$wgLang->pipeList( $hours ),
+			$wgLang->pipeList( $days ),
+			$this->wlDaysLink( 0, $page, $options ) );
+	}
+	
+	/**
+	 * Count the number of items on a user's watchlist
+	 *
+	 * @param $talk Include talk pages
+	 * @return integer
+	 */
+	function wlCountItems( &$user, $talk = true ) {
+		$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+	
+		# Fetch the raw count
+		$res = $dbr->select( 'watchlist', 'COUNT(*) AS count', 
+			array( 'wl_user' => $user->mId ), 'wlCountItems' );
+		$row = $dbr->fetchObject( $res );
+		$count = $row->count;
+		$dbr->freeResult( $res );
+	
+		# Halve to remove talk pages if needed
+		if( !$talk )
+			$count = floor( $count / 2 );
+	
+		return( $count );
+	}
+	
+	/** Returns an array of maps representing collab watchlist tags. The following fields are present
+	 * in each map:
+	 * - rl_id Id of the collaborative watchlist
+	 * - ct_tag Name of the tag
+	 * - collabwatchlistrevisiontag.user_id User which set the tag
+	 * - user_name Username of the user which set the tag
+	 * - rrt_comment Collabwatchlist tag comment
+	 * @param $rev_id
+	 * @param $rl_ids
+	 * @param $invert
+	 * @return unknown_type
+	 */
+	function wlTagsForRevision( $rev_id, $rl_ids = array(), $invert = false, $filterTags = array() ) {
+		// Some DB stuff
+		$dbr = wfGetDB( DB_SLAVE );
+		$cond = array();
+		if( isset($rl_ids) && !(count($rl_ids) == 1 && $rl_ids[0] == 0)) {
+			if( $invert ) {
+				$cond[] = "rl_id NOT IN (" . $dbr->makeList($rl_ids) . ")";
+			}else {
+				$cond = array("rl_id" => $rl_ids);
+			}
+		}
+		if( isset($filterTags) && count($filterTags) > 0) {
+			$cond[] = "ct_tag not in (" . $dbr->makeList($filterTags) . ")";
+		}
+		//$table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array()
+		$res = $dbr->select( array('change_tag', 'collabwatchlistrevisiontag', 'user'), # Tables
+			array('rl_id', 'ct_tag', 'collabwatchlistrevisiontag.user_id', 'user_name', 'rrt_comment'), # Fields
+			array('ct_rev_id' => $rev_id) + $cond,  # Conditions
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'collabwatchlistrevisiontag' => array('JOIN', 'change_tag.ct_id = collabwatchlistrevisiontag.ct_id'),
+					'user' => array('JOIN', 'collabwatchlistrevisiontag.user_id = user.user_id')
+			)
+		);
+		while( $row = $res->fetchObject() ) {
+			$tags[] = get_object_vars( $row );
+		}
+		$dbr->freeResult( $res );
+		return $tags;
+	}
+	
+	function wlGetFilterClauseForCollabWatchlistIds($rl_ids, $catNameCol, $pageIdCol) {
+		$excludedCatPageIds = array();
+		$includedCatPageIds = array();
+		$includedPageIds = array();
+		$dbr = wfGetDB( DB_SLAVE );
+		$res = $dbr->select( array('collabwatchlist', 'collabwatchlistcategory', 'page' ), # Tables
+			array('cat_page_id', 'page_title', 'page_namespace', 'subtract'), # Fields
+			$rl_ids != 0 ? array('collabwatchlist.rl_id' => $rl_ids) : array(),  # Conditions
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'collabwatchlistcategory' => array('JOIN', 'collabwatchlist.rl_id = collabwatchlistcategory.rl_id'),
+					'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
+		);
+		while( $row = $res->fetchObject() ) {
+			if($row->page_namespace == NS_CATEGORY) {
+				if($row->subtract) {
+					$excludedCatPageIds[$row->cat_page_id] = $row->page_title;
+				}else {
+					$includedCatPageIds[$row->cat_page_id] = $row->page_title;
+				}
+			}else {
+				$includedPageIds[$row->cat_page_id] = $row->page_title;
+			}
+		}
+		$dbr->freeResult( $res );
+		
+		if($includedCatPageIds) {
+			$catTree = new CategoryTreeManip();
+			$catTree->initialiseFromCategoryNames(array_values($includedCatPageIds));
+			$catTree->disableCategoryIds(array_keys($excludedCatPageIds));
+			$enabledCategoryNames = $catTree->getEnabledCategoryNames();
+			if(empty($enabledCategoryNames))
+				return;
+			$collabWatchlistClause = '(' . $catNameCol . " IN (" . implode(',', $this->addQuotes($dbr, $enabledCategoryNames)) . ") ";
+			if(!empty($includedPageIds)) {
+				$collabWatchlistClause .= ' OR ' . $pageIdCol . ' IN (' . implode(',', $this->addQuotes($dbr, array_keys($includedPageIds))) . ')';
+			}
+			$collabWatchlistClause .= ')';
+		}else if(!empty($includedPageIds)) {
+			$collabWatchlistClause = $pageIdCol . ' IN (' . implode(',', $this->addQuotes($dbr, array_keys($includedPageIds))) . ')';
+		}
+		return $collabWatchlistClause;
+	}
+	
+	public static function addQuotes($db, $strings) {
+		$result = array();
+		foreach($strings as $string) {
+			$result[] = $db->addQuotes($string);
+		}
+		return $result;
+	}
+}
Index: extensions/CollabWatchlist/includes/CategoryTreeManip.php
===================================================================
--- extensions/CollabWatchlist/includes/CategoryTreeManip.php	(revision 0)
+++ extensions/CollabWatchlist/includes/CategoryTreeManip.php	(revision 0)
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * This class is used to build a category tree and manipulate it.
+ * It currently supports building the tree from a list of categories.
+ * You can then disable categories by id and request a list of
+ * enabled categories and subcategories. This is useful for selecting
+ * pages by categories and their subcategories without specifying the
+ * subcategories.
+ * @author fhackenberger
+ */
+class CategoryTreeManip {
+	
+	var $root;
+	var $name;
+	var $id;
+	var $catPageIdToNode = array();
+	var $parents = array();
+	var $enabled = true;
+	var $children = array();
+	
+	/**
+	 * Constructor
+	 */
+	function __construct($id = NULL, $name = NULL, $root = NULL, $parents = array()) {
+		$this->id = $id;
+		$this->name = $name;
+		if(!is_null($root)) {
+			$this->root = $root;
+		}else {
+			$this->root = $this;
+		}
+		$this->parents = $parents;
+	}
+	
+	private function addChildren($children) {
+		if(!is_array($children))
+			throw new Exception('Argument must be an array');
+		foreach($children as $child) {
+			$this->children[$child->id] = $child;
+		}
+	}
+	
+	private function addParents($parents) {
+		if(!is_array($parents))
+			throw new Exception('Argument must be an array');
+		foreach($parents as $parent) {
+			$this->parents[$parent->id] = $parent;
+		}
+	}
+	
+	/** Disable this category node and all subcategory nodes
+	 * @return
+	 */
+	public function disable() {
+		$this->recursiveDisable();
+	}
+	
+	/** Disable the given categories (by id) and all their subcategories
+	 * 
+	 * @param array $catPageIds The page ids of the categories to disable
+	 * @return
+	 */
+	public function disableCategoryIds($catPageIds) {
+		foreach($catPageIds as $catId) {
+			$node = $this->getNodeForCatPageId($catId);
+			if(isset($node)) {
+				$node->disable();
+			}
+		}
+	}
+	
+	private function recursiveDisable($visitedNodeIds = array()) {
+		if(!$this->enabled || array_key_exists($this->id, $visitedNodeIds))
+			return; # Break the recursion
+		$this->enabled = false;
+		$visitedNodeIds[] = $this->id;
+		foreach($this->children as $cat) {
+			$cat->recursiveDisable($visitedNodeIds);
+		} 
+	}
+	
+	/** Returns a list of enables category names, including
+	 * all subcategories.
+	 * 
+	 * @return array An array of category names
+	 */
+	public function getEnabledCategoryNames() {
+		$enabledNodes = $this->getEnabledNodeMap();
+		$enabledCategories = array();
+		foreach($enabledNodes as $nodeId => $node) {
+			$enabledCategories[] = $node->name;
+		}
+		return $enabledCategories;
+	}
+	
+	/** Returns a map of enabled categories, including
+	 * all subcategories.
+	 * 
+	 * @return array An array mapping from category page ids to CategoryTreeManip objects
+	 */
+	public function getEnabledNodeMap() {
+		return $this->root->recursiveGetEnabledNodeMap();
+	}
+	
+	private function recursiveGetEnabledNodeMap($foundNodes = array()) {
+		if(isset($this->id)) {
+			if(!$this->enabled || array_key_exists($this->id, $foundNodes))
+				return $foundNodes; # Break the recursion
+			$foundNodes[$this->id] = $this;
+		}
+		foreach($this->children as $cat) {
+			$cat->recursiveGetEnabledNodeMap(&$foundNodes);
+		}
+		return $foundNodes;
+	}
+	
+	/** Returns a CategoryTreeManip node, given a category page id
+	 * 
+	 * @param $catPageId The page id of the category to retrieve
+	 * @return CategoryTreeManip The node
+	 */
+	public function getNodeForCatPageId($catPageId) {
+		return $this->root->catPageIdToNode[$catPageId];
+	}
+	
+	private function addNode($node) {
+		$this->root->catPageIdToNode[$node->id] = $node;
+	}
+	
+	/** Build the category tree, given a list of category names.
+	 * All categories and subcategories are enabled by default.
+	 * 
+	 * @param array $catNames An array of strings representing category names
+	 * @return
+	 */
+	public function initialiseFromCategoryNames($catNames) {
+		$dbr = wfGetDB( DB_SLAVE );
+		while($catNames) {
+			$res = $dbr->select( array('categorylinks', 'page' ), # Tables
+				array('cl_to AS parName', 'cl_from AS childId', 'page_title AS childName'), # Fields
+				array('cl_to' => $catNames, 'page_namespace' => NS_CATEGORY),  # Conditions
+				__METHOD__, array(),
+				 # Join conditions
+				array('page' => array('JOIN', 'page_id = cl_from') )
+			);
+			$parentList = array();
+			$childList = array();
+			while( $row = $res->fetchObject() ) {
+				$parentList[$row->parName][] = array($row->childId, $row->childName);
+				$childEntry = $childList[$row->childId];
+				if(!isset($childEntry)) {
+					$childList[$row->childId] = array($row->childName, array($row->parName));
+				}else {
+					$childEntry[1][] = $row->parName;
+				}
+			}
+			$dbr->freeResult( $res );
+			
+			if(!isset($parentNameToNode) && !empty($parentList)) {
+				// Fetch the page ids of the $catNames and add the parent categories if needed
+				$res = $dbr->select( array('page' ), # Tables
+					array('page_id, page_title'), # Fields
+					array('page_title' => array_keys($parentList))  # Conditions
+				);
+				$parentNameToNode = array();
+				while( $row = $res->fetchObject() ) {
+					$node = $this->getNodeForCatPageId($row->page_id);
+					if(!isset($node)) {
+						$node = new CategoryTreeManip($row->page_id, $row->page_title, $this->root);
+						$this->addNode($node);
+						$this->addChildren(array($node));
+					}
+					$parentNameToNode[$row->page_title] = $node;
+				}
+				$dbr->freeResult( $res );
+			}
+			
+			$newChildNameToNode = array();
+			// Add the new child nodes
+			foreach($childList as $childPageId => $childInfo) {
+				$childNode = $this->getNodeForCatPageId($childPageId);
+				if(!isset($childNode)) {
+					$childNode = new CategoryTreeManip($childPageId, $childInfo[0], $root);
+					$this->addNode($childNode);
+					$newChildNameToNode[$childInfo[0]] = $childNode;
+				}
+				foreach($childInfo[1] as $parentName) {
+					$parent = $parentNameToNode[$parentName];
+					$parent->addChildren(array($childNode));
+					$childNode->addParents(array($parent));
+				}
+			}
+			
+			// Prepare for the next loop
+			$parentNameToNode = $newChildNameToNode;
+			$catNames = array_keys($parentNameToNode);
+		}
+	}
+}
\ No newline at end of file
Index: extensions/CollabWatchlist/includes/CollabWatchlistChangesList.php
===================================================================
--- extensions/CollabWatchlist/includes/CollabWatchlistChangesList.php	(revision 0)
+++ extensions/CollabWatchlist/includes/CollabWatchlistChangesList.php	(revision 0)
@@ -0,0 +1,414 @@
+<?php
+/*
+ * Generates a list of changes for a collaborative watchlist. Builds on the EnhancedChangesList
+ */
+class CollabWatchlistChangesList extends EnhancedChangesList {
+	protected $user;
+	protected $tagCheckboxIndex = 0;
+	
+	/**
+	 * Collaborative Watchlist contructor
+	 * @param User $user
+	 * @param Skin $skin
+	 */
+	public function __construct( $skin, $user ) {
+		parent::__construct($skin);
+		$this->user = $user;
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#beginRecentChangesList()
+	 */
+	public function beginRecentChangesList() {
+		global $wgRequest;
+		$gwlSpeciaPageTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$result = Xml::openElement('form', array(
+			'class' => 'mw-collaborative-watchlist-addtag-form',
+			'method' => 'post', 
+			'action' => $gwlSpeciaPageTitle->getLocalUrl( array( 'action' => 'setTags' ))));
+		$result .= Xml::input('redirTarget', false, $wgRequest->getFullRequestURL(), array('type' => 'hidden'));
+		$result .= parent::beginRecentChangesList();
+		return $result;
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#endRecentChangesList()
+	 */
+	public function endRecentChangesList() {
+		global $wgRequest;
+		$collabWatchlist = $wgRequest->getIntOrNull( 'collabwatchlist' );
+		$result = '';
+		$result .= parent::endRecentChangesList();
+		$glWlIdAndName = $this->getCollabWatchlistIdAndName($this->user->getId());
+		$result .= $this->collabWatchlistAndTagSelectors($glWlIdAndName, $collabWatchlist, null, 'collabwatchlist', wfMsg( 'collabwatchlist' )) . '&nbsp;';
+		$result .= Xml::label( wfMsg('collabwatchlisttagcomment'), 'tagcomment' ) . '&nbsp;' . Xml::input( 'tagcomment' ) . '&nbsp;';
+		if( $this->user->useRCPatrol() )
+			$result .= Xml::checkLabel( wfMsg('collabwatchlistpatrol'), 'setpatrolled', 'setpatrolled', true ) . '&nbsp;';
+		$result .= Xml::submitButton(wfMsg( 'collabwatchlistsettagbutton' ));
+		$result .= Xml::closeElement('form');
+		return $result;
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#insertBeforeRCFlags($r, $rcObj)
+	 */
+	protected function insertBeforeRCFlags( &$r, &$rcObj ) {
+		$r .= Xml::element('input', array(
+			'name' => 'collaborative-watchlist-addtag-' . $this->tagCheckboxIndex,
+			'type' => 'checkbox',
+			'value' => ($rcObj->getTitle() . '|' . $rcObj->getAttribute('rc_this_oldid') . '|' . $rcObj->getAttribute('rc_id'))));
+		$this->tagCheckboxIndex++;
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#insertBeforeRCFlagsBlock($r, $block)
+	 */
+	protected function insertBeforeRCFlagsBlock( &$r, &$block ) {
+		$r .= Xml::element('input', array(
+			'name' => 'collaborative-watchlist-addtag-placeholder', 
+			'type' => 'checkbox',
+			'style' => 'visibility: hidden;'));
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/ChangesList#insertRollback($s, $rc)
+	 */
+	public function insertRollback( &$s, &$rc ) {
+		global $wgUser;
+		parent::insertRollback($s, $rc);
+		if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
+			if ($wgUser->isAllowed('edit') ) {
+				$rev = new Revision( array(
+						'id'        => $rc->mAttribs['rc_this_oldid'],
+						'user'      => $rc->mAttribs['rc_user'],
+						'user_text' => $rc->mAttribs['rc_user_text'],
+						'deleted'   => $rc->mAttribs['rc_deleted']
+				) );
+				$undoAfter = $rev->getPrevious();
+				$undoLink = $this->generateUndoLink($this->skin, $rc->getTitle(), $rev, $undoAfter);
+				if( isset($undoLink) )
+					$s .= '&nbsp;' . $undoLink;
+			}
+		}
+	}
+	
+	/**
+	 * Fetch an appropriate changes list class for the specified user
+	 * Some users might want to use an enhanced list format, for instance
+	 *
+	 * @param $user User to fetch the list class for
+	 * @return ChangesList derivative
+	 */
+	public static function newFromUser( &$user ) {
+		$sk = $user->getSkin();
+		$list = NULL;
+		if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) {
+			return new CollabWatchlistChangesList( $sk, $user );
+		} else {
+			return $list;
+		}
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/ChangesList#insertTags($s, $rc, $classes)
+	 */
+	public function insertTags( &$s, &$rc, &$classes ) {
+		if ( !empty($rc->mAttribs['collabwatchlist_tags']) ) {
+			list($tagSummary, $newClasses) = $this->formatReviewSummaryRow( $rc, 'changeslist' );
+			$classes = array_merge( $classes, $newClasses );
+			$s .= ' ' . $tagSummary;
+		}
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#insertHistLink($s, $rc, $title, $params, $sep)
+	 */
+	protected function insertHistLink( &$s, &$rc, $title, $params = array(), $sep = NULL ) {
+		// No history
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#insertCurrAndLastLinks($s, $rc)
+	 */
+	protected function insertCurrAndLastLinks( &$s, &$rc ) {
+		$s .= ' (';
+		$s .= $rc->curlink;
+		$s .= ')';
+	}
+	
+	/**
+	 * (non-PHPdoc)
+	 * @see includes/EnhancedChangesList#insertUserAndTalkLinks($s, $rc)
+	 */
+	protected function insertUserAndTalkLinks( &$s, &$rc ) {
+		$s .= $rc->userlink;
+	}
+	
+	/**
+	 * Insert the tags of the given change 
+	 */
+	private function formatReviewSummaryRow( $rc, $page ) {
+		global $wgRequest;
+		$s = '';
+		if( !$rc )
+			return $s;
+		
+		$attr = $rc->mAttribs;
+		$tagRows = $attr['collabwatchlist_tags'];
+
+		$classes = array();
+
+		$displayTags = array();
+		foreach( $tagRows as $tagRow ) {
+			$tag = $tagRow['ct_tag'];
+			$collabwatchlistTag = Xml::tags(
+				'span',
+				array(	'class' => 'mw-collabwatchlist-tag-marker ' .
+								Sanitizer::escapeClass( "mw-collabwatchlist-tag-marker-$tag" ),
+						'title' => $tagRow['rrt_comment']),
+				ChangeTags::tagDescription( $tag )
+			);
+			$classes[] = Sanitizer::escapeClass( "mw-collabwatchlist-tag-$tag" );
+			
+			/** Insert links to user page, user talk page and eventually a blocking link */
+			$userLink = $this->skin->userLink( $tagRow['user_id'], $tagRow['user_name'] );
+			$delTagTarget = CollabWatchlistEditor::getUnsetTagUrl( $wgRequest->getFullRequestURL(), $attr['rc_title'], $tagRow['rl_id'], $tag, $attr['rc_id'] );
+			$delTagLink = Xml::element('a', array('href' => $delTagTarget, 'class' => 'mw-collabwatchlist-unsettag-' . $tag), wfMsg('collabwatchlist-unset-tag'));
+			$displayTags[] = $collabwatchlistTag . ' ' . $delTagLink . ' ' . $userLink;
+		}
+		$markers = '(' . implode( ', ', $displayTags ) . ')';
+		$markers = Xml::tags( 'span', array( 'class' => 'mw-collabwatchlist-tag-markers' ), $markers );
+		return array( $markers, $classes );
+	}
+	
+	/** Generate a form 'select' element for the collaborative watchlists and a 'select' element for choosing a tag.
+	 * The tag selector reacts on the watchlist selector and displays the relevant tags only, if javascript is enabled.
+	 * 
+	 * @see #collabWatchlistSelector()
+	 * @see #tagSelector()
+	 * @param String $rlLabel The label for the collab watchlist select tag
+	 * @param String $rlElementId The id for the collab watchlist select tag
+	 * @param String $tagLabel The label for the tag selector
+	 * @return A string containing HTML
+	 */
+	public static function collabWatchlistAndTagSelectors($glWlIdAndName, $selected = '', $all = null, $element_name = 'collabwatchlist', $rlLabel = null, $rlElementId = 'collabwatchlist', $tagLabel = null) {
+		global $wgJsMimeType;
+		$tagElementIdBase = 'mw-collaborative-watchlist-addtag-selector';
+		$ret = self::collabWatchlistSelector($glWlIdAndName, $selected, $all, $element_name, $rlLabel, $rlElementId, $tagElementIdBase);
+		$ret .= '&nbsp;';
+		$ret .= self::tagSelector(array_keys($glWlIdAndName), $tagLabel);
+		// Make sure the correct tags for the default selection are set 
+		$ret .= Xml::element( 'script',
+			array(
+				'type' => $wgJsMimeType,
+			),
+			'window.onLoad = onCollabWatchlistSelection(\'' . $tagElementIdBase . '\', document.getElementById(\'' . $rlElementId . '\').value)', false
+		);
+		return $ret;
+	}
+
+	/**
+	 * Build a drop-down box for selecting a collaborative watchlist
+	 * This method optionally adds javascript for changing a tag selector
+	 * depending on the selected review list
+	 *
+	 * @param $glWlIdAndName Mixed: The result from getCollabWatchlistIdAndName()
+	 * @param $selected Mixed: Reviewlist which should be pre-selected
+	 * @param $all Mixed: Value of an item denoting all collaborative watchlists, or null to omit
+	 * @param $element_name String: value of the "name" attribute of the select tag
+	 * @param $label String: optional label to add to the field
+	 * @param $element_id String: optional the id of the select element
+	 * @param $tagElementIdBase String: optional the base id of the collabl watchlist tag selector for javascript functionality.
+	 * @return string
+	 */
+	public static function collabWatchlistSelector( $glWlIdAndName, $selected = '', $all = null, $element_name = 'collabwatchlist', $label = null, $element_id = 'collabwatchlist', $tagElementIdBase = null ) {
+		global $wgContLang, $wgScriptPath, $wgJsMimeType;
+		if(isset($tagElementIdBase)) {
+			$jsPath = "$wgScriptPath/extensions/CollabWatchlist/js";
+			$ret .= Xml::element( 'script', 
+				array(
+					'type' => $wgJsMimeType,
+					'src' => "$jsPath/CollabWatchlist.js",
+				),
+				'', false
+			);
+		}
+		$options = array();
+
+		// Godawful hack... we'll be frequently passed selected namespaces
+		// as strings since PHP is such a shithole.
+		// But we also don't want blanks and nulls and "all"s matching 0,
+		// so let's convert *just* string ints to clean ints.
+		if( preg_match( '/^\d+$/', $selected ) ) {
+			$selected = intval( $selected );
+		}
+
+		if( !is_null( $all ) )
+			$glWlIdAndName = array( $all => wfMsg( 'collabwatchlistsall' ) ) + $glWlIdAndName;
+		foreach( $glWlIdAndName as $index => $name ) {
+			if( $index < NS_MAIN )
+				continue;
+			if( $index === 0 )
+				$name = wfMsg( 'blankcollabwatchlist' );
+			$options[] = Xml::option( $name, $index, $index === $selected, isset($tagElementIdBase) ?
+				array('onclick' => 'onCollabWatchlistSelection("' . $tagElementIdBase . '", this.value)') : 
+				array()
+			);
+		}
+
+		$selectorHtml = Xml::openElement( 'select', array(
+			'id' => $element_id, 'name' => $element_name,
+			'class' => 'collabwatchlistselector', ))
+			. "\n"
+			. implode( "\n", $options )
+			. "\n"
+			. Xml::closeElement( 'select' );
+		if ( !is_null( $label ) ) {
+			$ret .= Xml::label( $label, $element_name ) . '&nbsp;' . $selectorHtml;
+		} else {
+			$ret .= $selectorHtml;
+		}
+		return $ret;
+	}
+	
+	/**
+	 * Build a drop-down box for selecting a collaborative watchlist tag
+	 * 
+	 * @param array $rlIds A list of collaborative watchlist ids
+	 * @param String $label The label for the select tag
+	 * @param String $elemId The id of the select tag
+	 * @return String A string containing HTML
+	 */
+	public static function tagSelector( $rlIds, $label = '', $elemId = 'mw-collaborative-watchlist-addtag-selector' ) {
+		global $wgContLang;
+		$tagsAndInfo = CollabWatchlistChangesList::getValidTagsAndInfo($rlIds);
+		$optionsAll = array();
+		$options = array();
+		foreach( $tagsAndInfo as $tagName => $info ) {
+			$optionsAll[] = Xml::option( $tagName . ' ' . $info['rt_description'], $tagName );
+			foreach( $info['rl_ids'] as $rlId ) {
+				$options[$rlId][] = Xml::option( $tagName, $tagName );
+			}
+		}
+		$ret = Xml::openElement( 'select', array(
+			'id' => $elemId,
+			'name' => 'collabwatchlisttag',
+			'class' => 'mw-collaborative-watchlist-tag-selector')) .
+			implode("\n", $optionsAll) .
+			Xml::closeElement('select');
+		if ( !is_null( $label ) ) {
+			$ret = Xml::label( $label, $elemId ) . '&nbsp;' . $ret;
+		}
+		foreach( $options as $rlId => $optionsRl) {
+			$ret .= Xml::openElement( 'select', array(
+				'style' => 'display: none;',
+				'id' => $elemId . '-' . $rlId,
+				'name' => 'collabwatchlisttag-rl',
+				'class' => 'mw-collaborative-watchlist-tag-selector')) .
+				implode("\n", $optionsRl) .
+				Xml::closeElement('select');
+		}
+		$ret .= Xml::openElement( 'select', array(
+			'style' => 'display: none;',
+			'id' => $elemId . '-empty',
+			'name' => 'collabwatchlisttag-rl',
+			'class' => 'mw-collaborative-watchlist-tag-selector')) .
+		Xml::closeElement('select');
+		
+		return $ret;
+	}
+	
+	/** Returns an array mapping from collab watchlist tag names to information about the tag
+	 * 
+	 * The info is an array with the following keys:
+	 * 'rt_description' The description of the tag
+	 * 'rl_ids' An array of collab watchlist ids the tag belongs to
+	 * @param array $rlIds A list of collab watchlist ids
+	 * @return array Mapping from tag name to info
+	 */
+	public static function getValidTagsAndInfo( $rlIds ) {
+		if(!isset($rlIds) || empty($rlIds)) {
+			return array();
+		}
+		$dbr = wfGetDB( DB_SLAVE );
+		$res = $dbr->select( array('collabwatchlisttag' ), # Tables
+			array('rt_name', 'rt_description', 'rl_id'), # Fields
+			array('rl_id' => $rlIds),  # Conditions
+			__METHOD__
+		);
+		$list = array();
+		while( $row = $res->fetchObject() ) {
+			if(array_key_exists($row->rt_name, $list)) {
+				$list[$row->rt_name]['rl_ids'][] = $row->rl_id;
+			} else {
+				$list[$row->rt_name] = array('rt_description' => $row->rt_description, 'rl_ids' => array($row->rl_id));
+			}
+		}
+		$dbr->freeResult( $res );
+		return $list;
+	}
+	
+	//XXX Cache the result of this method in this class
+	/** Get an array mapping from collab watchlist id to its name, filtering by member type
+	 * The method return only collab watchlist the given user is a member of, restricted by the allowed member types
+	 * @param int $user_id The id of the collab watchlist user
+	 * @param array $member_types A list of allowed membership types
+	 * @return array Mapping from collab watchlist id to its name
+	 */
+	public static function getCollabWatchlistIdAndName( $user_id, $member_types = array(COLLABWATCHLISTUSER_OWNER, COLLABWATCHLISTUSER_USER) ) {
+		$dbr = wfGetDB( DB_SLAVE );
+		$list = array();
+		//$table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array()
+		$res = $dbr->select( array('collabwatchlist', 'collabwatchlistuser' ), # Tables
+			array('collabwatchlist.rl_id', 'rl_name'), # Fields
+			array('rlu_type' => $member_types, 'collabwatchlistuser.user_id' => $user_id),  # Conditions
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'collabwatchlistuser' => array('JOIN', 'collabwatchlist.rl_id = collabwatchlistuser.rl_id') )
+		);
+		while( $row = $res->fetchObject() ) {
+			$list[$row->rl_id] = $row->rl_name;
+		}
+		$dbr->freeResult( $res );
+		return $list;
+	}
+	
+	//XXX Copied from HistoryPage, we should patch HistoryPage to export that functionality
+	// as a static function
+	/**
+	 * @param Skin $skin
+	 * @param Title $title
+	 * @param Revision $revision
+	 * @param Revision $undoAfterRevision
+	 * @return String Undo Link
+	 */
+	public static function generateUndoLink($skin, $title, $revision, $undoAfterRevision) {
+		if( ! $revision instanceof Revision || ! $undoAfterRevision instanceof Revision || 
+			! $title instanceof Title || !$skin instanceof Skin )
+			return null;
+		# Create undo tooltip for the first (=latest) line only
+		$undoTooltip = $revision->isCurrent()
+			? array( 'title' => wfMsg( 'tooltip-undo' ) )
+			: array();
+		$undolink = $skin->link(
+			$title,
+			wfMsgHtml( 'editundo' ),
+			$undoTooltip,
+			array(
+				'action' => 'edit',
+				'undoafter' => $undoAfterRevision->getId(),
+				'undo' => $revision->getId()
+			),
+			array( 'known', 'noclasses' )
+		);
+		return "<span class=\"mw-history-undo\">{$undolink}</span>";
+	}
+}
Index: extensions/CollabWatchlist/includes/CollabWatchlistEditor.php
===================================================================
--- extensions/CollabWatchlist/includes/CollabWatchlistEditor.php	(revision 0)
+++ extensions/CollabWatchlist/includes/CollabWatchlistEditor.php	(revision 0)
@@ -0,0 +1,1346 @@
+<?php
+
+/**
+ * Provides the UI through which users can perform editing
+ * operations on collaborative watchlists
+ *
+ * @ingroup CollabWatchlist
+ * @author Rob Church <robchur@gmail.com>
+ * @author Florian Hackenberger <f.hackenberger@chello.at>
+ */
+class CollabWatchlistEditor {
+
+	/**
+	 * Editing modes
+	 */
+	const EDIT_CLEAR = 1;
+	const CATEGORIES_EDIT_RAW = 2;
+	const EDIT_NORMAL = 3;
+	const TAGS_EDIT_RAW = 4;
+	const SET_TAGS = 5;
+	const UNSET_TAGS = 6;
+	const USERS_EDIT_RAW = 7;
+	const NEW_LIST = 8;
+	const DELETE_LIST = 9;
+	
+	/**
+	 * Main execution point
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @param $listIdsAndNames An array mapping from list id to list name
+	 * @param $output OutputPage
+	 * @param $request WebRequest
+	 * @param $mode int
+	 */
+	public function execute( $rlId, $listIdsAndNames, $output, $request, $mode ) {
+		global $wgUser, $wgCollabWatchlistPermissionDeniedPage;
+		if( wfReadOnly() ) {
+			$output->readOnlyPage();
+			return;
+		}
+		if( ($mode === self::EDIT_CLEAR ||
+			$mode === self::CATEGORIES_EDIT_RAW ||
+			$mode === self::USERS_EDIT_RAW ||
+			$mode === self::EDIT_NORMAL ||
+			$mode === self::TAGS_EDIT_RAW ||
+			$mode === self::DELETE_LIST) && (!isset($rlId) || $rlId === 0) ) {
+			$thisTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
+			$output->redirect( $thisTitle->getLocalURL() );
+			return;
+		}
+		$permissionDeniedTarget = Title::newFromText( $wgCollabWatchlistPermissionDeniedPage )->getLocalURL();
+		switch( $mode ) {
+			case self::EDIT_CLEAR:
+				// The "Clear" link scared people too much.
+				// Pass on to the raw editor, from which it's very easy to clear.
+			case self::CATEGORIES_EDIT_RAW:
+				$output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-raw-title' ) );
+				if( $request->wasPosted() ) {
+					if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$wanted = $this->extractCollabWatchlistCategories( $request->getText( 'titles' ) );
+					$current = $this->getCollabWatchlistCategories( $rlId );
+					if( count( $wanted ) > 0 ) {
+						$toWatch = array_diff( $wanted, $current );
+						$toUnwatch = array_diff( $current, $wanted );
+						$toWatch = $this->watchTitles( $toWatch, $rlId );
+						$this->unwatchTitles( $toUnwatch, $rlId );
+						if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-done', 'parse' ) );
+						if( ( $count = count( $toWatch ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-added', 'parse', $count ) );
+							$this->showTitles( $toWatch, $output, $wgUser->getSkin() );
+						}
+						if( ( $count = count( $toUnwatch ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-removed', 'parse', $count ) );
+							$this->showTitles( $toUnwatch, $output, $wgUser->getSkin() );
+						}
+					} else {
+						$this->clearCollabWatchlist( $rlId );
+						$output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-removed', 'parse', count( $current ) ) );
+						$this->showTitles( $current, $output, $wgUser->getSkin() );
+					}
+				}
+				$this->showRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
+				break;
+			case self::USERS_EDIT_RAW:
+				$output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-users-raw-title' ) );
+				if( $request->wasPosted() ) {
+					if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$wanted = $this->extractCollabWatchlistUsers( $request->getText( 'titles' ) );
+					$current = $this->getCollabWatchlistUsers( $rlId );
+					$isOwnerCb = create_function('$a', 'return stripos($a, "' . COLLABWATCHLISTUSER_OWNER_TEXT . ' ' . '") === 0;');
+					$wantedOwners = array_filter($wanted, $isOwnerCb);
+					if( count( $wantedOwners ) < 1 ) {
+						// Make sure there is at least one owner left
+						$currentOwners = array_filter($current, $isOwnerCb);
+						$reAddedOwner = current($currentOwners);
+						$wanted[] = $reAddedOwner;
+						list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $reAddedOwner );
+						$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-last-owner', 'parse' ) );
+						$this->showTitles( array($titleText), $output, $wgUser->getSkin() );
+					}
+					if( count( $wanted ) > 0 ) {
+						$toAdd = array_diff( $wanted, $current );
+						$toDel = array_diff( $current, $wanted );
+						$toAdd = $this->addUsers( $toAdd, $rlId );
+						$this->delUsers( $toDel, $rlId );
+						if( count( $toAdd ) > 0 || count( $toDel ) > 0 )
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-done', 'parse' ) );
+						if( ( $count = count( $toAdd ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-added', 'parse', $count ) );
+							$this->showTitles( $toAdd, $output, $wgUser->getSkin() );
+						}
+						if( ( $count = count( $toDel ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-removed', 'parse', $count ) );
+							$this->showTitles( $toDel, $output, $wgUser->getSkin() );
+						}
+					} else {
+						$this->clearCollabWatchlist( $rlId );
+						$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-removed', 'parse', count( $current ) ) );
+						$this->showTitles( $current, $output, $wgUser->getSkin() );
+					}
+				}
+				$this->showUsersRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
+				break;
+			case self::TAGS_EDIT_RAW:
+				$output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-tags-raw-title' ) );
+				if( $request->wasPosted() ) {
+					if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$wanted = $this->extractCollabWatchlistTags( $request->getText( 'titles' ) );
+					$current = $this->getCollabWatchlistTags( $rlId );
+					if( count( $wanted ) > 0 ) {
+						$newTags = array_diff_assoc( $wanted, $current );
+						$removeTags = array_diff_assoc( $current, $wanted );
+						$this->removeTags( array_keys($removeTags), $rlId );
+						$this->addTags( $newTags, $rlId );
+						if( count( $newTags ) > 0 || count( $removeTags ) > 0 )
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-done', 'parse' ) );
+						if( ( $count = count( $newTags ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-added', 'parse', $count ) );
+							$this->showTagList( $newTags, $output, $wgUser->getSkin() );
+						}
+						if( ( $count = count( $removeTags ) ) > 0 ) {
+							$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-removed', 'parse', $count ) );
+							$this->showTagList( $removeTags, $output, $wgUser->getSkin() );
+						}
+					} else {
+						$this->clearCollabWatchlist( $rlId );
+						$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-removed', 'parse', count( $current ) ) );
+						$this->showTagList( $current, $output, $wgUser->getSkin() );
+					}
+				}
+				$this->showTagsRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
+				break;
+			case self::EDIT_NORMAL:
+				$output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-normal-title' ) );
+				if( $request->wasPosted() ) {
+					if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$titles = $this->extractCollabWatchlistCategories( $request->getArray( 'titles' ) );
+					$this->unwatchTitles( $titles, $rlId );
+					$output->addHTML( wfMsgExt( 'collabwatchlistedit-normal-done', 'parse',
+						$GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
+					$this->showTitles( $titles, $output, $wgUser->getSkin() );
+				}
+				$this->showNormalForm( $output, $rlId );
+				break;
+			case self::SET_TAGS:
+				$redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
+				if( $request->wasPosted() ) {
+					$rlId = $request->getInt('collabwatchlist', -1);
+					if( ! $this->checkPermissions( $wgUser, $rlId, array(COLLABWATCHLISTUSER_USER, COLLABWATCHLISTUSER_OWNER) ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$redirTarget = $request->getText('redirTarget', $redirTarget);
+					$tagToAdd = $request->getText('collabwatchlisttag');
+					$tagcomment = $request->getText('tagcomment');
+					$setPatrolled = $request->getBool('setpatrolled', false);
+					$pagesToTag = array();
+					if( strlen($tagToAdd) !== 0 && $rlId !== -1 ) {
+						$postValues = $request->getValues();
+						foreach( $postValues as $key => $value ) {
+							if( stripos($key, 'collaborative-watchlist-addtag-') === 0 ) {
+								$pageRevRcId = explode('|', $value);
+								if( count($pageRevRcId) < 3 ) {
+									continue;
+								}
+								$pagesToTag[$pageRevRcId[0]][] = array('rev_id' => $pageRevRcId[1], 'rc_id' => $pageRevRcId[2]);
+							}
+						}
+						$this->setTags( $pagesToTag, $tagToAdd, $wgUser->getId(), $rlId, $tagcomment, $setPatrolled );
+					}
+				}
+				$output->redirect( $redirTarget );
+				break;
+			case self::UNSET_TAGS:
+				$rlId = $request->getInt('collabwatchlist', -1);
+				if( ! $this->checkPermissions( $wgUser, $rlId, array(COLLABWATCHLISTUSER_USER, COLLABWATCHLISTUSER_OWNER) ) ) {
+					$output->redirect( $permissionDeniedTarget );
+					break;
+				}
+				$redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
+				$redirTarget = $request->getText('redirTarget', $redirTarget);
+				$page = $request->getText('collabwatchlistpage');
+				$tagToDel = $request->getText('collabwatchlisttag');
+				$rcId = $request->getInt('collabwatchlistrcid', -1);
+				if( strlen($page) !== 0 && strlen($tagToDel) !== 0 && $rlId !== -1 && $rcId !== -1 ) {
+					$pagesToUntag[$page][] = array('rc_id' => $rcId);
+					$this->unsetTags( $pagesToUntag, $tagToDel, $wgUser->getId(), $rlId );
+				}
+				$output->redirect( $redirTarget );
+				break;
+			case self::NEW_LIST:
+				if( $request->wasPosted() ) {
+					$redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
+					$listId = $this->createNewList($request->getText('listname'));
+					if( isset($listId) ) {
+						$output->redirect( $redirTarget );
+					} else {
+						$output->addHTML( wfMsgExt( 'collabwatchlistnew-name-exists', 'parse' ) );
+					}
+				} else {
+					$this->showNewListForm($output);
+				}
+				break;
+			case self::DELETE_LIST:
+				$output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistdelete-title' ) );
+				$rlId = $request->getInt('collabwatchlist', -1);
+				if( $request->wasPosted() ) {
+					if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
+						$output->redirect( $permissionDeniedTarget );
+						break;
+					}
+					$this->deleteList($rlId);
+					$redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
+					$output->redirect( $redirTarget );
+				} else {
+					$this->showDeleteListForm($output, $rlId);
+				}
+				break;
+		}
+	}
+
+	/**
+	 * Check the edit token from a form submission
+	 *
+	 * @param $request WebRequest
+	 * @param $user User
+	 * @param $rlId Id of the collaborative watchlist to check users against
+	 * @param $memberTypes Which types of members are allowed
+	 * @return bool
+	 */
+	private function checkToken( $request, $user, $rlId, $memberTypes = array(COLLABWATCHLISTUSER_OWNER) ) {
+		$tokenOk = $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' ) && $request->getVal( 'collabwatchlist' ) !== 0;
+		if( $tokenOk === false )
+			return $tokenOk;
+		return $this->checkPermissions( $user, $rlId, $memberTypes );
+	}
+	
+	private function checkPermissions( $user, $rlId, $memberTypes = array(COLLABWATCHLISTUSER_OWNER) ) {
+		// Check permissions
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select( 'collabwatchlistuser',
+			'COUNT(*) AS count', 
+			array( 'rl_id' => $rlId, 'user_id' => $user->getId(), 'rlu_type' => $memberTypes ),
+			__METHOD__ 
+		);
+		$row = $dbr->fetchObject( $res );
+		return $row->count >= 1;
+	}
+
+	/**
+	 * Extract a list of categories from a blob of text, returning
+	 * (prefixed) strings
+	 *
+	 * @param $list mixed
+	 * @return array
+	 */
+	private function extractCollabWatchlistCategories( $list ) {
+		$titles = array();
+		if( !is_array( $list ) ) {
+			$list = explode( "\n", trim( $list ) );
+			if( !is_array( $list ) )
+				return array();
+		}
+		foreach( $list as $text ) {
+			$subtract = false;
+			$text = trim( $text );
+			$titleText = $text;
+			if( stripos($text, '- ') === 0 ) {
+				$subtract = true;
+				$titleText = trim( substr($text, 2) );
+			}
+			if( strlen( $text ) > 0 ) {
+				$title = Title::newFromText( $titleText );
+				if( $title instanceof Title && $title->isWatchable() ) {
+					$titles[] = $subtract ? '- ' . $title->getPrefixedText() : $title->getPrefixedText();
+				}
+			}
+		}
+		return array_unique( $titles );
+	}
+	
+	private function extractTypeTypeTextAndUsername( $typeAndUsernameStr ) {
+		$type = COLLABWATCHLISTUSER_USER;
+		$typeText = COLLABWATCHLISTUSER_USER_TEXT;
+		$text = trim( $typeAndUsernameStr );
+		$titleText = $text;
+		if( stripos($text, COLLABWATCHLISTUSER_OWNER_TEXT . ' ') === 0 ) {
+			$type = COLLABWATCHLISTUSER_OWNER;
+			$typeText = COLLABWATCHLISTUSER_OWNER_TEXT;
+			$titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_OWNER_TEXT . ' ')) );
+		}else if( stripos($text, COLLABWATCHLISTUSER_USER_TEXT . ' ') === 0 ) {
+			$type = COLLABWATCHLISTUSER_USER;
+			$typeText = COLLABWATCHLISTUSER_USER_TEXT;
+			$titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_USER_TEXT . ' ')) );
+		}else if( stripos($text, COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT . ' ') === 0 ) {
+			$type = COLLABWATCHLISTUSER_TRUSTED_EDITOR;
+			$typeText = COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT;
+			$titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT . ' ')) );
+		}
+		return array($type, $typeText, $titleText);
+	}
+	
+	/**
+	 * Extract a list of users from a blob of text, returning
+	 * (prefixed) strings
+	 *
+	 * @param $list mixed
+	 * @return array
+	 */
+	private function extractCollabWatchlistUsers( $list ) {
+		$titles = array();
+		if( !is_array( $list ) ) {
+			$list = explode( "\n", trim( $list ) );
+			if( !is_array( $list ) )
+				return array();
+		}
+		foreach( $list as $text ) {
+			list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $text );
+			if( strlen( $text ) > 0 ) {
+				$user = User::newFromName($titleText);
+				if( $user instanceof User ) {
+					$titles[] = $typeText . ' ' . $user->getName();
+				}
+			}
+		}
+		return array_unique( $titles );
+	}
+	
+	/**
+	 * Extract a list of tags from a blob of text, returning
+	 * (prefixed) strings
+	 *
+	 * @param $list mixed
+	 * @return array
+	 */
+	private function extractCollabWatchlistTags( $list ) {
+		$tags = array();
+		if( !is_array( $list ) ) {
+			$list = explode( "\n", trim( $list ) );
+			if( !is_array( $list ) )
+				return array();
+		}
+		foreach( $list as $text ) {
+			$subtract = false;
+			$text = trim($text);
+			if( strlen( $text ) > 0 ) {
+				$pipepos = stripos($text, '|');
+				$description = '';
+				if( $pipepos > 0 ) {
+					if( ($pipepos + 1) < strlen($text) )
+						$description = trim(substr($text, $pipepos + 1));
+					$text = trim(substr($text, 0, $pipepos));
+				}
+				$tags[$text] = $description;
+			}
+		}
+		return $tags;
+	}
+
+	/**
+	 * Print out a list of linked titles
+	 *
+	 * $titles can be an array of strings or Title objects; the former
+	 * is preferred, since Titles are very memory-heavy
+	 *
+	 * @param $titles An array of strings, or Title objects
+	 * @param $output OutputPage
+	 * @param $skin Skin
+	 */
+	private function showTitles( $titles, $output, $skin ) {
+		$talk = wfMsgHtml( 'talkpagelinktext' );
+		// Do a batch existence check
+		$batch = new LinkBatch();
+		foreach( $titles as $title ) {
+			if( !$title instanceof Title )
+				$title = Title::newFromText( $title );
+			if( $title instanceof Title ) {
+				$batch->addObj( $title );
+				$batch->addObj( $title->getTalkPage() );
+			}
+		}
+		$batch->execute();
+		// Print out the list
+		$output->addHTML( "<ul>\n" );
+		foreach( $titles as $title ) {
+			if( !$title instanceof Title )
+				$title = Title::newFromText( $title );
+			if( $title instanceof Title ) {
+				$output->addHTML( "<li>" . $skin->link( $title )
+				. ' (' . $skin->link( $title->getTalkPage(), $talk ) . ")</li>\n" );
+			}
+		}
+		$output->addHTML( "</ul>\n" );
+	}
+	
+	/**
+	 * Print out a list of tags with description
+	 *
+	 * $titles can be an array of strings or Title objects; the former
+	 * is preferred, since Titles are very memory-heavy
+	 *
+	 * @param $tagsAndDesc An array of strings mapping from tag name to description
+	 * @param $output OutputPage
+	 * @param $skin Skin
+	 */
+	private function showTagList( $tagsAndDesc, $output, $skin ) {
+		// Print out the list
+		$output->addHTML( "<ul>\n" );
+		foreach( $tagsAndDesc as $title => $description ) {
+			$output->addHTML( "<li>" . $title
+			. ' (' . $description . ")</li>\n" );
+		}
+		$output->addHTML( "</ul>\n" );
+	}
+
+	/**
+	 * Count the number of categories on a collaborative watchlist
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return int
+	 */
+	private function countCollabWatchlistCategories( $rlId ) {
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select( 'collabwatchlistcategory', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
+		$row = $dbr->fetchObject( $res );
+		return $row->count;
+	}
+	
+	/**
+	 * Count the number of users on a collaborative watchlist
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return int
+	 */
+	private function countCollabWatchlistUsers( $rlId ) {
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select( 'collabwatchlistuser', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
+		$row = $dbr->fetchObject( $res );
+		return $row->count;
+	}
+	
+	/**
+	 * Count the number of tags on a collaborative watchlist
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return int
+	 */
+	private function countCollabWatchlistTags( $rlId ) {
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select( 'collabwatchlisttag', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
+		$row = $dbr->fetchObject( $res );
+		return $row->count;
+	}
+	
+	/**
+	 * Count the number of set edit tags on a collaborative watchlist
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return int
+	 */
+	private function countCollabWatchlistSetTags( $rlId ) {
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select( 'collabwatchlistrevisiontag', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
+		$row = $dbr->fetchObject( $res );
+		return $row->count;
+	}
+
+	/**
+	 * Prepare a list of categories on a collaborative watchlist
+	 * and return an array of (prefixed) strings
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return array
+	 */
+	private function getCollabWatchlistCategories( $rlId ) {
+		$list = array();
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select(
+			array('collabwatchlistcategory', 'page'),
+			array('page_title', 'page_namespace', 'subtract'),
+			array(
+				'rl_id' => $rlId,
+			),
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
+		);
+		if( $res->numRows() > 0 ) {
+			while( $row = $res->fetchObject() ) {
+				$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+				if( $title instanceof Title && !$title->isTalkPage() )
+					$list[] = $row->subtract ? '- ' . $title->getPrefixedText() : $title->getPrefixedText();
+			}
+			$res->free();
+		}
+		return $list;
+	}
+	
+	/**
+	 * Prepare a list of users on a collaborative watchlist
+	 * and return an array of (prefixed) strings
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return array
+	 */
+	private function getCollabWatchlistUsers( $rlId ) {
+		$list = array();
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select(
+			array('collabwatchlistuser', 'user'),
+			array('user_name', 'rlu_type'),
+			array(
+				'rl_id' => $rlId,
+			),
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'user' => array('JOIN', 'user.user_id = collabwatchlistuser.user_id') )
+		);
+		if( $res->numRows() > 0 ) {
+			while( $row = $res->fetchObject() ) {
+				$typeText = fnCollabWatchlistUserTypeToText($row->rlu_type);
+				$list[] = $typeText . ' ' . $row->user_name;
+			}
+			$res->free();
+		}
+		return $list;
+	}
+	
+	/**
+	 * Prepare a list of tags on a collaborative watchlist
+	 * and return an array of tag names mapping to tag descriptions
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return array
+	 */
+	private function getCollabWatchlistTags( $rlId ) {
+		$list = array();
+		$dbr = wfGetDB( DB_MASTER );
+		$res = $dbr->select(
+			array('collabwatchlisttag'),
+			array('rt_name', 'rt_description'),
+			array(
+				'rl_id' => $rlId,
+			), __METHOD__
+		);
+		if( $res->numRows() > 0 ) {
+			while( $row = $res->fetchObject() ) {
+				$list[$row->rt_name] = $row->rt_description;
+			}
+			$res->free();
+		}
+		return $list;
+	}
+
+	/**
+	 * Get a list of categories on collaborative watchlist, excluding talk pages,
+	 * and return as a two-dimensional array with namespace and title which
+	 * maps to an array with 'redirect' and 'subtract' keys.
+	 *
+	 * @param $rlId Collaborative watchlist id
+	 * @return array
+	 */
+	private function getWatchlistInfo( $rlId ) {
+		$titles = array();
+		$dbr = wfGetDB( DB_MASTER );
+		
+		$res = $dbr->select(
+			array('collabwatchlistcategory', 'page'),
+			array('page_title', 'page_namespace', 'page_id', 'page_len', 'page_is_redirect', 'subtract'),
+			array(
+				'rl_id' => $rlId,
+			),
+			__METHOD__, array(),
+			 # Join conditions
+			array(	'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
+		);
+		
+		if( $res && $dbr->numRows( $res ) > 0 ) {
+			$cache = LinkCache::singleton();
+			while( $row = $dbr->fetchObject( $res ) ) {
+				$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+				if( $title instanceof Title ) {
+					// Update the link cache while we're at it
+					if( $row->page_id ) {
+						$cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
+					} else {
+						$cache->addBadLinkObj( $title );
+					}
+					// Ignore non-talk
+					if( !$title->isTalkPage() )
+						$titles[$row->page_namespace][$row->page_title] = array('redirect' => $row->page_is_redirect, 'subtract' => $row->subtract);
+				}
+			}
+		}
+		return $titles;
+	}
+
+	/**
+	 * Show a message indicating the number of categories on the collaborative watchlist,
+	 * and return this count for additional checking
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId The id of the collaborative watchlist
+	 * @return int
+	 */
+	private function showItemCount( $output, $rlId ) {
+		if( ( $count = $this->countCollabWatchlistCategories( $rlId ) ) > 0 ) {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-numitems', 'parse',
+				$GLOBALS['wgLang']->formatNum( $count ) ) );
+		} else {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-noitems', 'parse' ) );
+		}
+		return $count;
+	}
+	
+	/**
+	 * Show a message indicating the number of categories on the collaborative watchlist,
+	 * and return this count for additional checking
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId The id of the collaborative watchlist
+	 * @return int
+	 */
+	private function showTagItemCount( $output, $rlId ) {
+		if( ( $count = $this->countCollabWatchlistTags( $rlId ) ) > 0 ) {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-numitems', 'parse',
+				$GLOBALS['wgLang']->formatNum( $count ) ) );
+		} else {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-noitems', 'parse' ) );
+		}
+		return $count;
+	}
+	
+	/**
+	 * Show a message indicating the number of set tags for edits on the collaborative watchlist,
+	 * and return this count for additional checking
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId The id of the collaborative watchlist
+	 * @return int
+	 */
+	private function showSetTagsItemCount( $output, $rlId ) {
+		if( ( $count = $this->countCollabWatchlistSetTags( $rlId ) ) > 0 ) {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-set-tags-numitems', 'parse',
+				$GLOBALS['wgLang']->formatNum( $count ) ) );
+		} else {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-set-tags-noitems', 'parse' ) );
+		}
+		return $count;
+	}
+	
+	/**
+	 * Show a message indicating the number of categories on the collaborative watchlist,
+	 * and return this count for additional checking
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId The id of the collaborative watchlist
+	 * @return int
+	 */
+	private function showUserItemCount( $output, $rlId ) {
+		if( ( $count = $this->countCollabWatchlistUsers( $rlId ) ) > 0 ) {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-numitems', 'parse',
+				$GLOBALS['wgLang']->formatNum( $count ) ) );
+		} else {
+			$output->addHTML( wfMsgExt( 'collabwatchlistedit-users-noitems', 'parse' ) );
+		}
+		return $count;
+	}
+	
+	/**
+	 * Remove all categories from a collaborative watchlist
+	 *
+	 * @param $rlId Collaborative watchlist if
+	 */
+	private function clearCollabWatchlist( $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		$dbw->delete( 'collabwatchlistcategory', array( 'rl_id' => $rlId ), __METHOD__ );
+	}
+
+	/**
+	 * Add a list of categories to a collaborative watchlist
+	 *
+	 * $titles is an array of strings, prefixed with '- ', if the
+	 * category is subtracted
+	 *
+	 * @param $titles An array of strings
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function watchTitles( $titles, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		$rows = array();
+		$added = array();
+		foreach( $titles as $title ) {
+			$subtract = false;
+			$title = trim( $title );
+			$titleText = $title;
+			if( stripos($title, '- ') === 0 ) {
+				$subtract = true;
+				$titleText = trim( substr($title, 2) );
+			}
+			$titleObj = Title::newFromText( $titleText );
+			if( $titleObj instanceof Title && $titleObj->exists() ) {
+				$rows[] = array(
+					'rl_id' => $rlId,
+					'cat_page_id' => $titleObj->getArticleID(),
+					'subtract' => $subtract,
+				);
+				$added[] = $title;
+			}
+		}
+		$dbw->insert( 'collabwatchlistcategory', $rows, __METHOD__, 'IGNORE' );
+		return $added;
+	}
+	
+	/**
+	 * Add a list of users to a collaborative watchlist
+	 *
+	 * $titles is an array of strings, prefixed with the user type text and ' '
+	 *
+	 * @param $titles An array of strings
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function addUsers( $users, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		$rows = array();
+		$added = array();
+		foreach( $users as $userString ) {
+			list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $userString );
+			$user = User::newFromName($titleText);
+			if( $user instanceof User && $user->getId() !== 0) {
+				$rows[] = array(
+					'rl_id' => $rlId,
+					'user_id' => $user->getId(),
+					'rlu_type' => $type,
+				);
+				$added[] = $userString;
+			}
+		}
+		$dbw->insert( 'collabwatchlistuser', $rows, __METHOD__, 'IGNORE' );
+		return $added;
+	}
+	
+	private function setTags( $titlesAndTagInfo, $tag, $userId, $rlId, $comment, $setPatrolled = false) {
+		//XXX Attach a hook to delete tags from the collabwatchlistrevisiontag table as soon as the actual tags are deleted from the change_tags table
+		$allowedTagsAndInfo = $this->getCollabWatchlistTags($rlId);
+		if(!array_key_exists($tag, $allowedTagsAndInfo)) {
+			return false;
+		}
+		$dbw = wfGetDB( DB_MASTER );
+		foreach( $titlesAndTagInfo as $title => $infos ) {
+			$rcIds = array();
+			// Add entries for the tag to the change_tags table
+			// optionally mark edit as patrolled
+			foreach( $infos as $infoKey => $info ) {
+				ChangeTags::addTags($tag, $info['rc_id'], $info['rev_id']);
+				$rcIds[] = $info['rc_id'];
+				if( $setPatrolled ) {
+					RecentChange::markPatrolled($info['rc_id']);
+				}
+			}
+			// Add the tagged revisions to the collaborative watchlist
+			$sql = 'INSERT IGNORE INTO collabwatchlistrevisiontag (ct_id, rl_id, user_id, rrt_comment) 
+					SELECT ct_id, ' . $dbw->strencode($rlId) . ',' .
+						$dbw->strencode($userId) . ',' .
+						$dbw->addQuotes($comment) . ' FROM change_tag WHERE ct_tag = ? AND ct_rc_id ';
+			if( count($rcIds) > 1 ) {
+				$sql .= 'IN (' . $dbw->makeList($rcIds) . ')';
+				$params = array( $tag );
+			}else {
+				$sql .= '= ?';
+				$params = array( $tag, $rcIds[0] );
+			}
+			$prepSql = $dbw->prepare($sql);
+			$res = $dbw->execute($prepSql, $params);
+			$dbw->freePrepared($prepSql);
+			return true;
+		}
+	}
+	
+	private function unsetTags( $titlesAndTagInfo, $tag, $userId, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		foreach( $titlesAndTagInfo as $title => $infos ) {
+			$rcIds = array();
+			foreach( $infos as $infoKey => $info ) {
+			// XXX Remove entries for the tag from the change_tags table
+//				ChangeTags::addTags($tag, $info['rc_id'], $info['rev_id']);
+				$rcIds[] = $info['rc_id'];
+			}
+			// Remove the tag from the collaborative watchlist
+			$sql = 'delete collabwatchlistrevisiontag from collabwatchlistrevisiontag JOIN change_tag 
+					ON change_tag.ct_id = collabwatchlistrevisiontag.ct_id 
+					WHERE ct_tag = ? AND ct_rc_id ';
+			if( count($rcIds) > 1 ) {
+				$sql .= 'IN (' . $dbw->makeList($rcIds) . ')';
+				$params = array( $tag );
+			}else {
+				$sql .= '= ?';
+				$params = array( $tag, $rcIds[0] );
+			}
+			$prepSql = $dbw->prepare($sql);
+			$res = $dbw->execute($prepSql, $params);
+			$dbw->freePrepared($prepSql);
+		}
+	}
+	
+	/**
+	 * Add a list of tags to a collaborative watchlist
+	 *
+	 * $titles is an array of strings
+	 *
+	 * @param $titles An array of strings (tag names) mapping to tag descriptions
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function addTags( $titles, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		$rows = array();
+		foreach( $titles as $title => $description ) {
+			$rows[] = array(
+				'rl_id' => $rlId,
+				'rt_name' => $title,
+				'rt_description' => $description,
+			);
+		}
+		$dbw->insert( 'collabwatchlisttag', $rows, __METHOD__, 'IGNORE' );
+	}
+
+	/**
+	 * Remove a list of categories from a collaborative watchlist
+	 *
+	 * $titles is an array of strings, prefixed with '- ', if the
+	 * category is subtracted
+	 *
+	 * @param $titles An array of strings
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function unwatchTitles( $titles, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		foreach( $titles as $title ) {
+			$subtract = false;
+			$title = trim( $title );
+			$titleText = $title;
+			if( stripos($title, '- ') === 0 ) {
+				$subtract = true;
+				$titleText = trim( substr($title, 2) );
+			}
+			$title = Title::newFromText( $titleText );
+			if( $title instanceof Title ) {
+				$dbw->delete(
+					'collabwatchlistcategory',
+					array(
+						'rl_id' => $rlId,
+						'cat_page_id' => $title->getArticleID(),
+						'subtract' => $subtract,
+					),
+					__METHOD__
+				);
+				$article = new Article($title);
+				//XXX Check if we can simply rename the hook, or if we need to register it
+				wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
+			}
+		}
+	}
+	
+	/**
+	 * Remove a list of users from a collaborative watchlist
+	 *
+	 * $titles is an array of strings, prefixed with the user type text and ' '
+	 *
+	 * @param $titles An array of strings
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function delUsers( $users, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		foreach( $users as $userString ) {
+			list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $userString );
+			$user = User::newFromName($titleText);
+			if( $user instanceof User && $user->getId() !== 0) {
+				$dbw->delete(
+					'collabwatchlistuser',
+					array(
+						'rl_id' => $rlId,
+						'user_id' => $user->getId(),
+						'rlu_type' => $type,
+					),
+					__METHOD__
+				);
+			}
+		}
+		//XXX Check if we can simply rename the hook, or if we need to register it
+		//wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
+	}
+	
+	/**
+	 * Remove a list of tags from a collaborative watchlist
+	 *
+	 * $titles is an array of strings
+	 *
+	 * @param $titles An array of strings
+	 * @param $rlId The id of the collaborative watchlist
+	 */
+	private function removeTags( $titles, $rlId ) {
+		$dbw = wfGetDB( DB_MASTER );
+		foreach( $titles as $title ) {
+			$dbw->delete(
+				'collabwatchlisttag',
+				array(
+					'rl_id' => $rlId,
+					'rt_name' => $title,
+				),
+				__METHOD__
+			);
+			//$article = new Article($title);
+			//XXX Check if we can simply rename the hook, or if we need to register it
+			//wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
+		}
+	}
+
+	/**
+	 * Show the standard collaborative watchlist editing form
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId Collaborative watchlist id
+	 */
+	private function showNormalForm( $output, $rlId ) {
+		global $wgUser;
+		if( ( $count = $this->showItemCount( $output, $rlId ) ) > 0 ) {
+			$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+			$form  = Xml::openElement( 'form', array( 'method' => 'post',
+				'action' => $self->getLocalUrl( array( 'action' => 'edit' ) ) ) );
+			$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+			$form .= Xml::hidden( 'collabwatchlist', $rlId );
+			$form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistedit-normal-legend' ) . "</legend>";
+			$form .= wfMsgExt( 'collabwatchlistedit-normal-explain', 'parse' );
+			$form .= $this->buildRemoveList( $rlId, $wgUser->getSkin() );
+			$form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-normal-submit' ) ) . '</p>';
+			$form .= '</fieldset></form>';
+			$output->addHTML( $form );
+		}
+	}
+	
+	private function showNewListForm( $output ) {
+		global $wgUser;
+		$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$form  = Xml::openElement( 'form', array( 'method' => 'post',
+			'action' => $self->getLocalUrl( array( 'action' => 'newList' ) ) ) );
+		$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+		$form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistnew-legend' ) . "</legend>";
+		$form .= wfMsgExt( 'collabwatchlistnew-explain', 'parse' );
+		$form .= Xml::label( wfMsg('collabwatchlistnew-name'), 'listname' ) . '&nbsp;' . Xml::input( 'listname' ) . '&nbsp;';
+		$form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistnew-submit' ) ) . '</p>';
+		$form .= '</fieldset></form>';
+		$output->addHTML( $form );
+	}
+	
+	private function showDeleteListForm( $output, $rlId ) {
+		global $wgUser;
+		$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$form  = Xml::openElement( 'form', array( 'method' => 'post',
+			'action' => $self->getLocalUrl( array( 'action' => 'delete' ) ) ) );
+		$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+		$form .= Xml::hidden( 'collabwatchlist', $rlId );
+		$form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistdelete-legend' ) . "</legend>";
+		$form .= wfMsgExt( 'collabwatchlistdelete-explain', 'parse' );
+		$this->showUserItemCount($output, $rlId);
+		$this->showSetTagsItemCount($output, $rlId);
+		$form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistdelete-submit' ) ) . '</p>';
+		$form .= '</fieldset></form>';
+		$output->addHTML( $form );
+	}
+	
+	private function createNewList($name) {
+		global $wgUser;
+		if( !isset($name) || empty($name) )
+			return;
+		$dbw = wfGetDB( DB_MASTER );
+		$dbw->begin();
+		try {
+			$rl_id = $dbw->nextSequenceValue( 'collabwatchlist_rl_id_seq' );
+			$dbw->insert( 'collabwatchlist', array(
+				'rl_id'           => $rl_id,
+				'rl_name'    => $name,
+				'rl_start'	=> wfTimestamp(TS_ISO_8601),
+			), __METHOD__, 'IGNORE' );
+	
+			$affected = $dbw->affectedRows();
+			if( $affected ) {
+				$newid = $dbw->insertId();
+			}else {
+				return;
+			}
+			$rlu_id = $dbw->nextSequenceValue( 'collabwatchlistuser_rlu_id_seq' );
+			$dbw->insert( 'collabwatchlistuser', array(
+				'rlu_id'	=> $rlu_id,
+				'rl_id'		=> $newid,
+				'user_id'	=> $wgUser->getId(),
+				'rlu_type'	=> COLLABWATCHLISTUSER_OWNER,
+			), __METHOD__, 'IGNORE' );
+			$affected = $dbw->affectedRows();
+			if( ! $affected ) {
+				$dbw->rollback();
+				return;
+			}
+			$dbw->commit();
+			return $newid;
+		}catch(Exception $e) {
+			$dbw->rollback();
+		}
+	}
+	
+	private function deleteList($rlId) {
+		if( !isset($rlId) || empty($rlId) )
+			return;
+		$dbw = wfGetDB( DB_MASTER );
+		$dbw->begin();
+		try {
+			$dbw->delete( 'collabwatchlistrevisiontag', array(
+				'rl_id' => $rlId,
+			), __METHOD__ );
+			$dbw->delete( 'collabwatchlisttag', array(
+				'rl_id' => $rlId,
+			), __METHOD__ );
+			$dbw->delete( 'collabwatchlistcategory', array(
+				'rl_id' => $rlId,
+			), __METHOD__ );
+			$dbw->delete( 'collabwatchlistuser', array(
+				'rl_id' => $rlId,
+			), __METHOD__ );
+			$dbw->delete( 'collabwatchlist', array(
+				'rl_id' => $rlId,
+			), __METHOD__ );
+			$affected = $dbw->affectedRows();
+			if( ! $affected ) {
+				$dbw->rollback();
+				return;
+			}
+			$dbw->commit();
+			return $rlId;
+		}catch(Exception $e) {
+			$dbw->rollback();
+		}
+	}
+
+	/**
+	 * Build the part of the standard collaborative watchlist editing form with the actual
+	 * title selection checkboxes and stuff.  Also generates a table of
+	 * contents if there's more than one heading.
+	 *
+	 * @param $rlId The id of the collaborative watchlist
+	 * @param $skin Skin (really, Linker)
+	 */
+	private function buildRemoveList( $rlId, $skin ) {
+		$list = "";
+		$toc = $skin->tocIndent();
+		$tocLength = 0;
+		foreach( $this->getWatchlistInfo( $rlId ) as $namespace => $pages ) {
+			$tocLength++;
+			$heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
+			$anchor = "editwatchlist-ns" . $namespace;
+
+			$list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
+			$toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
+
+			$list .= "<ul>\n";
+			foreach( $pages as $dbkey => $info ) {
+				$title = Title::makeTitleSafe( $namespace, $dbkey );
+				$list .= $this->buildRemoveLine( $title, $info, $skin );
+			}
+			$list .= "</ul>\n";
+		}
+		// ISSUE: omit the TOC if the total number of titles is low?
+		if( $tocLength > 1 ) {
+			$list = $skin->tocList( $toc ) . $list;
+		}
+		return $list;
+	}
+
+	/**
+	 * Get the correct "heading" for a namespace
+	 *
+	 * @param $namespace int
+	 * @return string
+	 */
+	private function getNamespaceHeading( $namespace ) {
+		return $namespace == NS_MAIN
+			? wfMsgHtml( 'blanknamespace' )
+			: htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
+	}
+
+	/**
+	 * Build a single list item containing a check box selecting a title
+	 * and a link to that title, with various additional bits
+	 *
+	 * @param $title Title
+	 * @param $info array with info about the category ('redirect' and 'subtract' keys)
+	 * @param $skin Skin
+	 * @return string
+	 */
+	private function buildRemoveLine( $title, $catInfo, $skin ) {
+		global $wgLang;
+
+		$link = $skin->link( $title );
+		if( $catInfo['redirect'] )
+			$link = '<span class="watchlistredir">' . $link . '</span>';
+		$tools[] = $skin->link( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
+		if( $title->exists() ) {
+			$tools[] = $skin->link(
+				$title,
+				wfMsgHtml( 'history_short' ),
+				array(),
+				array( 'action' => 'history' ),
+				array( 'known', 'noclasses' )
+			);
+		}
+		if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
+			$tools[] = $skin->link(
+				SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
+				wfMsgHtml( 'contributions' ),
+				array(),
+				array(),
+				array( 'known', 'noclasses' )
+			);
+		}
+		return "<li>"
+			. ($catInfo['subtract'] ? '<span class="collabwatchlistsubtract">- </span>' : '')
+			. Xml::check( 'titles[]', false, array( 'value' => $catInfo['subtract'] ? '- ' . $title->getPrefixedText() : $title->getPrefixedText() ) )
+			. $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n";
+		}
+
+	/**
+	 * Show a form for editing the watchlist in "raw" mode
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId Collaborative watchlist id
+	 * @param $rlName Collaborative watchlist name
+	 */
+	private function showRawForm( $output, $rlId, $rlName ) {
+		global $wgUser;
+		$this->showItemCount( $output, $rlId );
+		$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$form  = Xml::openElement( 'form', array( 'method' => 'post',
+			'action' => $self->getLocalUrl( array( 'action' => 'rawCategories' ) ) ) );
+		$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+		$form .= Xml::hidden( 'collabwatchlist', $rlId );
+		$form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
+		$form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
+		$form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
+		$form .= "<br />\n";
+		$form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
+			'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
+		$categories = $this->getCollabWatchlistCategories( $rlId );
+		foreach( $categories as $category )
+			$form .= htmlspecialchars( $category ) . "\n";
+		$form .= '</textarea>';
+		$form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
+		$form .= '</fieldset></form>';
+		$output->addHTML( $form );
+	}
+	
+	/**
+	 * Show a form for editing the tags of a collaborative watchlist in "raw" mode
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId Collaborative watchlist id
+	 * @param $rlName Collaborative watchlist name
+	 */
+	private function showTagsRawForm( $output, $rlId, $rlName ) {
+		global $wgUser;
+		$this->showTagItemCount( $output, $rlId );
+		$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$form  = Xml::openElement( 'form', array( 'method' => 'post',
+			'action' => $self->getLocalUrl( array( 'action' => 'rawTags' ) ) ) );
+		$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+		$form .= Xml::hidden( 'collabwatchlist', $rlId );
+		$form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'collabwatchlistedit-tags-raw-legend' ) . '</legend>';
+		$form .= wfMsgExt( 'collabwatchlistedit-tags-raw-explain', 'parse' );
+		$form .= Xml::label( wfMsg( 'collabwatchlistedit-tags-raw-titles' ), 'titles' );
+		$form .= "<br />\n";
+		$form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
+			'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
+		$tags = $this->getCollabWatchlistTags( $rlId );
+		foreach( $tags as $tag => $description )
+			$form .= htmlspecialchars( $tag ) . "|" . $description . "\n";
+		$form .= '</textarea>';
+		$form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-tags-raw-submit' ) ) . '</p>';
+		$form .= '</fieldset></form>';
+		$output->addHTML( $form );
+	}
+	
+	/**
+	 * Show a form for editing the users of a collaborative watchlist in "raw" mode
+	 *
+	 * @param $output OutputPage
+	 * @param $rlId Collaborative watchlist id
+	 * @param $rlName Collaborative watchlist name
+	 */
+	private function showUsersRawForm( $output, $rlId, $rlName ) {
+		global $wgUser;
+		$this->showUserItemCount( $output, $rlId );
+		$self = SpecialPage::getTitleFor( 'CollabWatchlist' );
+		$form  = Xml::openElement( 'form', array( 'method' => 'post',
+			'action' => $self->getLocalUrl( array( 'action' => 'rawUsers' ) ) ) );
+		$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
+		$form .= Xml::hidden( 'collabwatchlist', $rlId );
+		$form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'collabwatchlistedit-users-raw-legend' ) . '</legend>';
+		$form .= wfMsgExt( 'collabwatchlistedit-users-raw-explain', 'parse' );
+		$form .= Xml::label( wfMsg( 'collabwatchlistedit-users-raw-titles' ), 'titles' );
+		$form .= "<br />\n";
+		$form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
+			'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
+		$users = $this->getCollabWatchlistUsers( $rlId );
+		foreach( $users as $userString )
+			$form .= htmlspecialchars( $userString ) . "\n";
+		$form .= '</textarea>';
+		$form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-users-raw-submit' ) ) . '</p>';
+		$form .= '</fieldset></form>';
+		$output->addHTML( $form );
+	}
+
+	/**
+	 * Determine whether we are editing the watchlist, and if so, what
+	 * kind of editing operation
+	 *
+	 * @param $request WebRequest
+	 * @param $par mixed
+	 * @return int
+	 */
+	public static function getMode( $request, $par ) {
+		$mode = strtolower( $request->getVal( 'action', $par ) );
+		switch( $mode ) {
+			case 'clear':
+				return self::EDIT_CLEAR;
+			case 'rawcategories':
+				return self::CATEGORIES_EDIT_RAW;
+			case 'rawtags':
+				return self::TAGS_EDIT_RAW;
+			case 'edit':
+				return self::EDIT_NORMAL;
+			case 'settags':
+				return self::SET_TAGS;
+			case 'unsettags':
+				return self::UNSET_TAGS;
+			case 'rawusers':
+				return self::USERS_EDIT_RAW;
+			case 'newlist':
+				return self::NEW_LIST;
+			case 'delete':
+				return self::DELETE_LIST;
+			default:
+				return false;
+		}
+	}
+
+	/**
+	 * Build a set of links for convenient navigation
+	 * between collaborative watchlist viewing and editing modes
+	 *
+	 * @param $listIdsAndNames An array mapping from list ids to list names
+	 * @param $skin Skin to use
+	 * @return string
+	 */
+	public static function buildTools( $listIdsAndNames, $skin ) {
+		global $wgLang, $wgUser;
+		$modes = array( 'view' => false, 'delete' => 'delete', 'edit' => 'edit', 
+			'rawCategories' => 'rawCategories', 'rawTags' => 'rawTags',
+			'rawUsers' => 'rawUsers' );
+		$r = '';
+		// Insert link for new list
+		$r .= $skin->link(
+			SpecialPage::getTitleFor( 'CollabWatchlist', 'newList' ),
+			wfMsgHtml( "collabwatchlisttools-newList" ),
+			array(),
+			array(),
+			array( 'known', 'noclasses' )
+		) . '<br />';
+		if( !isset($listIdsAndNames) || empty($listIdsAndNames) )
+			return $r;
+		foreach( $listIdsAndNames as $listId => $listName) {
+			$tools = array();
+			foreach( $modes as $mode => $subpage ) {
+				// can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
+				$tools[] = $skin->link(
+					SpecialPage::getTitleFor( 'CollabWatchlist', $subpage ),
+					wfMsgHtml( "collabwatchlisttools-{$mode}" ),
+					array(),
+					array( 'collabwatchlist' => $listId ),
+					array( 'known', 'noclasses' )
+				);
+			}
+			$r .= $listName . ' ' . $wgLang->pipeList( $tools ) . '<br />';
+		}
+		return $r;
+	}
+	
+	/** Returns a URL for unsetting a specific tag on a specific edit on a given list
+	 * 
+	 * @param String $redirUrl The url to redirect after the tag was removed
+	 * @param String $pageName The name of the page the tag is set on
+	 * @param int $rlId The id of the collab watchlist
+	 * @param String $tag The tag to remove
+	 * @param int $rcId The id of the edit in the recent changes
+	 * @return String an URL string
+	 */
+	public static function getUnsetTagUrl( $redirUrl, $pageName, $rlId, $tag, $rcId ) {
+		return SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl(array(
+			'action' => 'unsetTags',
+			'redirTarget' => $redirUrl,
+			'collabwatchlisttag' => $tag,
+			'collabwatchlist' => $rlId,
+			'collabwatchlistpage' => $pageName,
+			'collabwatchlistrcid' => $rcId
+		));
+	}
+}
Index: extensions/CollabWatchlist/js/CollabWatchlist.js
===================================================================
--- extensions/CollabWatchlist/js/CollabWatchlist.js	(revision 0)
+++ extensions/CollabWatchlist/js/CollabWatchlist.js	(revision 0)
@@ -0,0 +1,17 @@
+function onCollabWatchlistSelection(tagIdBasename, listId) {
+	// We have multiple <select> tags, one which contains all tags,
+	// one for each collaborative watchlist and an empty one. Upon selection
+	// of a collaborative watchlist we swap the select tags.
+	var allTags = document.getElementById(tagIdBasename);
+	var elem = document.getElementById(tagIdBasename + '-' + listId);
+	if(elem == null) {
+		elem = document.getElementById(tagIdBasename + '-empty');
+		if(elem == null)
+			return;
+	}
+	var clonedElem = elem.cloneNode(true);
+	clonedElem.setAttribute('id', tagIdBasename);
+	clonedElem.setAttribute('name', allTags.getAttribute('name'));
+	clonedElem.style.display = 'inline';
+	allTags.parentNode.replaceChild(clonedElem, allTags);
+}
\ No newline at end of file
Index: includes/ChangesList.php
===================================================================
--- includes/ChangesList.php	(revision 65812)
+++ includes/ChangesList.php	(working copy)
@@ -773,6 +773,45 @@
 		return $ret;
 	}
 
+	protected function insertDiffAndHistLinks( &$s, &$rc ) {
+		$s .= ' ('. $rc->difflink;
+		$this->insertHistLink($s, $rc, $rc->getTitle());
+		$s .= ')';
+	}
+
+	protected function insertHistLink( &$s, &$rc, $title, $params = array(), $sep = NULL ) {
+		$params['action'] = 'history';
+		$s .= isset($sep) ? $sep : $this->message['pipe-separator'] .
+		$this->skin->link(
+		$title,
+		$this->message['hist'],
+		array(),
+		$params,
+		array( 'known', 'noclasses' )
+		);
+	}
+
+	protected function insertBeforeRCFlags( &$r, &$rcObj ) {
+
+	}
+	
+	protected function insertBeforeRCFlagsBlock( &$r, &$block ) {
+
+	}
+
+	protected function insertCurrAndLastLinks( &$s, &$rc ) {
+		$s .= ' (';
+		$s .= $rc->curlink;
+		$s .= $this->message['pipe-separator'];
+		$s .= $rc->lastlink;
+		$s .= ')';
+	}
+
+	protected function insertUserAndTalkLinks( &$s, &$rc ) {
+		$s .= $rc->userlink;
+		$s .= $rc->usertalklink;
+	}
+
 	/**
 	 * Enhanced RC group
 	 */
@@ -852,7 +891,7 @@
 		$tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>";
 		$tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>";
 		$r .= '<td class="mw-enhanced-rc">'.$tl.'&nbsp;';
-
+		$this->insertBeforeRCFlagsBlock($r, $block);
 		# Main line
 		$r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
 
@@ -907,16 +946,9 @@
 			$r .= $this->message['pipe-separator'] . $this->message['hist'] . ')';
 		} else {
 			$params = $queryParams;
-			$params['action'] = 'history';
 
-			$r .= $this->message['pipe-separator'] .
-				$this->skin->link(
-					$block[0]->getTitle(),
-					$this->message['hist'],
-					array(),
-					$params,
-					array( 'known', 'noclasses' )
-				) . ')';
+			$this->insertHistLink($r, $rcObj, $block[0]->getTitle(), $params);
+			$r .= ')';
 		}
 		$r .= ' . . ';
 
@@ -961,6 +993,7 @@
 			#$r .= '<tr><td valign="top">'.$this->spacerArrow();
 			$r .= '<tr><td style="vertical-align:top;font-family:monospace; padding:0px;">';
 			$r .= $this->spacerIndent() . $this->spacerIndent();
+			$this->insertBeforeRCFlags( $r, $rcObj );
 			$r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
 			$r .= '&nbsp;</td><td style="vertical-align:top; padding:0px;"><span style="font-family:monospace">';
 
@@ -994,11 +1027,7 @@
 			$r .= $link . '</span>';
 
 			if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) {
-				$r .= ' (';
-				$r .= $rcObj->curlink;
-				$r .= $this->message['pipe-separator'];
-				$r .= $rcObj->lastlink;
-				$r .= ')';
+				$this->insertCurrAndLastLinks( $r, $rcObj );
 			}
 			$r .= ' . . ';
 
@@ -1007,8 +1036,7 @@
 				$r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ;
 			}
 			# User links
-			$r .= $rcObj->userlink;
-			$r .= $rcObj->usertalklink;
+			$this->insertUserAndTalkLinks( $r, $rcObj );
 			// log action
 			$this->insertAction( $r, $rcObj );
 			// log comment
@@ -1098,6 +1126,7 @@
 
 		$r = '<table class="mw-enhanced-rc"><tr>';
 		$r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . '&nbsp;';
+		$this->insertBeforeRCFlags( $r, $rcObj );
 		# Flag and Timestamp
 		if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
 			$r .= '&nbsp;&nbsp;&nbsp;&nbsp;'; // 4 flags -> 4 spaces
@@ -1121,15 +1150,7 @@
 		}
 		# Diff and hist links
 		if ( $rc_type != RC_LOG ) {
-			$r .= ' ('. $rcObj->difflink . $this->message['pipe-separator'];
-			$query['action'] = 'history';
-			$r .= $this->skin->link(
-				$rcObj->getTitle(),
-				$this->message['hist'],
-				array(),
-				$query,
-				array( 'known', 'noclasses' )
-			) . ')';
+			$this->insertDiffAndHistLinks( $r, $rcObj);
 		}
 		$r .= ' . . ';
 		# Character diff
@@ -1137,7 +1158,8 @@
 			$r .= "$cd . . ";
 		}
 		# User/talk
-		$r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
+		$r .= ' ';
+		$this->insertUserAndTalkLinks($r, $rcObj);
 		# Log action (if any)
 		if( $rc_log_type ) {
 			if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) {
