r91063 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r91062‎ | r91063 | r91064 >
Date:10:12, 29 June 2011
Author:flohack
Status:deferred (Comments)
Tags:
Comment:
Modified paths:
  • /trunk/extensions/CollabWatchlist/CollabWatchlist.alias.php (added) (history)
  • /trunk/extensions/CollabWatchlist/CollabWatchlist.body.php (added) (history)
  • /trunk/extensions/CollabWatchlist/CollabWatchlist.i18n.php (added) (history)
  • /trunk/extensions/CollabWatchlist/CollabWatchlist.php (added) (history)
  • /trunk/extensions/CollabWatchlist/example.css (added) (history)
  • /trunk/extensions/CollabWatchlist/includes (added) (history)
  • /trunk/extensions/CollabWatchlist/includes/CategoryTreeManip.php (added) (history)
  • /trunk/extensions/CollabWatchlist/includes/CollabWatchlistChangesList.php (added) (history)
  • /trunk/extensions/CollabWatchlist/includes/CollabWatchlistEditor.php (added) (history)
  • /trunk/extensions/CollabWatchlist/includes/SpecialCollabWatchlist.php (added) (history)
  • /trunk/extensions/CollabWatchlist/js (added) (history)
  • /trunk/extensions/CollabWatchlist/js/CollabWatchlist.js (added) (history)
  • /trunk/extensions/CollabWatchlist/mediawiki_core.patch (added) (history)
  • /trunk/extensions/CollabWatchlist/sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/collabwatchlist.sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/collabwatchlistcategory.sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/collabwatchlistrevisiontag.sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/collabwatchlisttag.sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/collabwatchlistuser.sql (added) (history)
  • /trunk/extensions/CollabWatchlist/sql/patch-change_tag_id.sql (added) (history)

Diff [purge]

Index: trunk/extensions/CollabWatchlist/sql/patch-change_tag_id.sql
@@ -0,0 +1,5 @@
 2+-- Add a primary key to the change_tag table in order
 3+-- to enable us to build the review list extension
 4+
 5+ALTER TABLE /*$wgDBprefix*/change_tag
 6+ ADD ct_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/sql/collabwatchlistrevisiontag.sql
@@ -0,0 +1,21 @@
 2+-- (c) Florian Hackenberger, 2009, GPL
 3+-- Table structure for `CollabWatchlist`
 4+-- Replace /*$wgDBprefix*/ with the proper prefix
 5+-- Replace /*$wgDBTableOptions*/ with the correct options
 6+
 7+-- Add page tracking the collab watchlist tags for revisions
 8+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistrevisiontag (
 9+ -- The id of this entry
 10+ rrt_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 11+ -- Foreign key to change_tag.ct_id
 12+ ct_id integer unsigned NOT NULL,
 13+ -- Foreign key to collabwatchlist.rl_id
 14+ rl_id integer unsigned NOT NULL,
 15+ -- Foreign key to user.user_id
 16+ user_id int(10) unsigned NOT NULL,
 17+
 18+ -- Comment for the tag
 19+ rrt_comment varchar(255),
 20+
 21+ UNIQUE KEY (ct_id, rl_id)
 22+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/sql/collabwatchlisttag.sql
@@ -0,0 +1,18 @@
 2+-- (c) Florian Hackenberger, 2009, GPL
 3+-- Table structure for `CollabWatchlist`
 4+-- Replace /*$wgDBprefix*/ with the proper prefix
 5+-- Replace /*$wgDBTableOptions*/ with the correct options
 6+
 7+-- Add table defining collab watchlist tags
 8+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlisttag (
 9+ -- The id of this entry
 10+ rt_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 11+ -- Foreign key to collabwatchlist.rl_id
 12+ rl_id integer unsigned NOT NULL,
 13+ -- The name of the collabwatchlist tag (unique)
 14+ rt_name varbinary(255) NOT NULL,
 15+ -- Description of the tag
 16+ rt_description tinyblob NOT NULL default '',
 17+
 18+ UNIQUE KEY (rl_id, rt_name)
 19+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/sql/collabwatchlistcategory.sql
@@ -0,0 +1,16 @@
 2+-- (c) Florian Hackenberger, 2009, GPL
 3+-- Table structure for `CollabWatchlist`
 4+-- Replace /*$wgDBprefix*/ with the proper prefix
 5+-- Replace /*$wgDBTableOptions*/ with the correct options
 6+
 7+-- Add table defining the categories for collaborative watchlists
 8+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistcategory (
 9+ -- The id of this entry
 10+ rlc_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 11+ -- Foreign key to collabwatchlist.rl_id
 12+ rl_id integer unsigned NOT NULL,
 13+ -- Foreign key to page.page_id
 14+ cat_page_id integer unsigned NOT NULL,
 15+ -- Whether the category is subtracted from or added to the collaborative watchlist
 16+ subtract boolean DEFAULT false
 17+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/sql/collabwatchlistuser.sql
@@ -0,0 +1,17 @@
 2+-- (c) Florian Hackenberger, 2009, GPL
 3+-- Table structure for `CollabWatchlist`
 4+-- Replace /*$wgDBprefix*/ with the proper prefix
 5+-- Replace /*$wgDBTableOptions*/ with the correct options
 6+
 7+-- Add table defining the collaborative watchlist users
 8+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlistuser (
 9+ -- The id of this entry
 10+ rlu_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 11+ -- Foreign key to collabwatchlist.rl_id
 12+ rl_id integer unsigned NOT NULL,
 13+ -- Foreign key to user.user_id
 14+ user_id int(10) unsigned NOT NULL,
 15+
 16+ -- Type of user
 17+ rlu_type ENUM("OWNER", "USER", "TRUSTED_EDITOR") DEFAULT "OWNER"
 18+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/sql/collabwatchlist.sql
@@ -0,0 +1,17 @@
 2+-- (c) Florian Hackenberger, 2009, GPL
 3+-- Table structure for `CollabWatchlist`
 4+-- Replace /*$wgDBprefix*/ with the proper prefix
 5+-- Replace /*$wgDBTableOptions*/ with the correct options
 6+
 7+
 8+-- Add table defining a collaborative watchlist
 9+CREATE TABLE IF NOT EXISTS /*$wgDBprefix*/collabwatchlist (
 10+ -- The id of the collaborative watchlist
 11+ rl_id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 12+ -- The name of the collaborative watchlist (unique)
 13+ rl_name varbinary(255) NOT NULL,
 14+ -- Starting date in standard YMDHMS form.
 15+ rl_start binary(14) NOT NULL default '',
 16+
 17+ UNIQUE KEY (rl_name)
 18+) /*$wgDBTableOptions*/;
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/CollabWatchlist.body.php
@@ -0,0 +1 @@
 2+<?php
Index: trunk/extensions/CollabWatchlist/example.css
@@ -0,0 +1,13 @@
 2+@CHARSET "UTF-8";
 3+
 4+/* This is an example how to make a tag display in a special way
 5+ * To use it, include this snippet in your user specific css stylesheet */
 6+.mw-collabwatchlist-tag-marker {
 7+ color: green;
 8+}
 9+
 10+.mw-collabwatchlist-tag-marker-4eyes {
 11+ background: transparent url(images/feed-icon.png) no-repeat scroll left
 12+ center;
 13+ padding-left: 16px;
 14+}
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/CollabWatchlist.i18n.php
@@ -0,0 +1,140 @@
 2+<?php
 3+$messages = array();
 4+
 5+$messages['en'] = array(
 6+ 'collabwatchlist' => 'Collaborative watchlist',
 7+ 'specialcollabwatchlist' => 'Collaborative watchlist special page',
 8+ 'specialcollabwatchlist-desc' => 'Collaborative watchlist special page description',
 9+ 'collabwatchlist-details' => '{{PLURAL:$1|$1 category/page|$1 categories/pages}} on this collaborative watchlist.',
 10+ 'collabwatchlisttagselect' => 'Tag',
 11+ 'collabwatchlisttagcomment' => 'Comment',
 12+ 'collabwatchlistsettagbutton' => 'Set tag',
 13+ 'collabwatchlist-unset-tag' => 'x',
 14+ 'collabwatchlisttools-view' => 'Display',
 15+ 'collabwatchlisttools-edit' => 'Edit Categories',
 16+ 'collabwatchlisttools-rawCategories' => 'Raw Edit Categories',
 17+ 'collabwatchlisttools-rawTags' => 'Raw Edit Tags',
 18+ 'collabwatchlisttools-rawUsers' => 'Raw Edit Users',
 19+ 'collabwatchlisttools-delete' => 'Delete',
 20+ 'collabwatchlistsall' => 'All lists',
 21+ 'collabwatchlistfiltertags' => 'Filter tags',
 22+ 'collabwatchlistedit-users-raw-submit' => 'Save',
 23+ 'collabwatchlistedit-raw-title' => 'Raw edit categories',
 24+ 'collabwatchlistedit-tags-raw-title' => 'Raw edit tags',
 25+ 'collabwatchlistedit-users-raw-title' => 'Raw edit users',
 26+ 'collabwatchlistedit-users-last-owner' => 'There must at least one owner',
 27+ 'collabwatchlistedit-numitems' => 'This collaborative watchlist contains {{PLURAL:$1|1 category|$1 categories}}',
 28+ 'collabwatchlistedit-noitems' => 'This collaborative watchlist contains no categories.',
 29+ 'collabwatchlistedit-tags-numitems' => 'This collaborative watchlist contains {{PLURAL:$1|1 tag|$1 tags}}',
 30+ 'collabwatchlistedit-tags-noitems' => 'This collaborative watchlist contains no tags.',
 31+ 'collabwatchlistedit-users-numitems' => 'This collaborative watchlist contains {{PLURAL:$1|1 user|$1 users}}',
 32+ 'collabwatchlistedit-users-noitems' => 'This collaborative watchlist contains no users.',
 33+ 'collabwatchlistedit-raw-legend' => 'Raw edit collaborative watchlist categories',
 34+ 'collabwatchlistedit-users-raw-legend' => 'Raw edit collaborative watchlist users',
 35+ 'collabwatchlistedit-tags-raw-legend' => 'Raw edit collaborative watchlist tags',
 36+ 'collabwatchlistedit-raw-explain' => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
 37+ 'collabwatchlistedit-tags-raw-explain' => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
 38+ 'collabwatchlistedit-users-raw-explain' => 'Categories on the collaborative watchlist are shown below, and can be edited by adding to and removing from the list',
 39+ 'collabwatchlistedit-raw-titles' => 'Categories:',
 40+ 'collabwatchlistedit-tags-raw-titles' => 'Tags:',
 41+ 'collabwatchlistedit-users-raw-titles' => 'Users:',
 42+ 'collabwatchlistedit-normal-title' => 'Edit categories',
 43+ 'collabwatchlistedit-normal-legend' => 'Remove categories from watchlist',
 44+ 'collabwatchlistedit-normal-explain' => 'Categories on your watchlist are shown below.',
 45+ 'collabwatchlistedit-tags-raw-submit' => 'Save',
 46+ 'collabwatchlistedit-normal-done' => '{{PLURAL:$1|1 category was|$1 categories were}} removed from the collaborative watchlist:',
 47+ 'collabwatchlistedit-tags-raw-done' => 'The collaborative watchlist has been updated.',
 48+ 'collabwatchlistedit-users-raw-done' => 'The collaborative watchlist has been updated.',
 49+ 'collabwatchlistedit-tags-raw-added' => '{{PLURAL:$1|1 tag was|$1 tags were}} added:',
 50+ 'collabwatchlistedit-users-raw-added' => '{{PLURAL:$1|1 user was|$1 users were}} added:',
 51+ 'collabwatchlistedit-tags-raw-removed' => '{{PLURAL:$1|1 tag was|$1 tags were}} removed:',
 52+ 'collabwatchlistedit-users-raw-removed' => '{{PLURAL:$1|1 user was|$1 users were}} removed:',
 53+ 'collabwatchlistinverttags' => 'Invert tag filter',
 54+ 'collabwatchlistpatrol' => 'Patrol edits',
 55+ 'collabwatchlisttools-newList' => 'New collaborative watchlist',
 56+ 'collabwatchlistdelete-legend' => 'Delete a collaborative watchlist',
 57+ 'collabwatchlistdelete-explain' => 'Deleting a collaborative watchlist will remove all traces of the watchlist. Tags which were set on the edits are preserved.',
 58+ 'collabwatchlistdelete-submit' => 'Delete',
 59+ 'collabwatchlistdelete-title' => 'Delete collaborative watchlist',
 60+ 'collabwatchlistedit-set-tags-numitems' => 'This collaborative watchlist has {{PLURAL:$1|1 tag|$1 tags}} set',
 61+ 'collabwatchlistedit-set-tags-noitems' => 'This collaborative watchlist has no tags set',
 62+ 'collabwatchlistnew-legend' => 'Create a new collaborative watchlist',
 63+ 'collabwatchlistnew-explain' => 'The name of the list has to be unique.',
 64+ 'collabwatchlistnew-name' => 'List name',
 65+ 'collabwatchlistnew-submit' => 'Create',
 66+ 'collabwatchlistedit-raw-done' => 'The collaborative watchlist has been updated',
 67+ 'collabwatchlistedit-raw-added' => '{{PLURAL:$1|1 page/category was|$1 pages/categories were}} added:',
 68+ 'collabwatchlistedit-raw-removed' => '{{PLURAL:$1|1 page/category was|$1 pages/categories were}} removed',
 69+ 'collabwatchlistedit-normal-submit' => 'Speichern',
 70+ 'collabwatchlistshowhidelistusers' => '$1 list users',
 71+ 'tog-collabwatchlisthidelistusers' => 'Hide edits from collaborative watchlist users',
 72+);
 73+
 74+$messages['de'] = array(
 75+ 'collabwatchlist' => 'Kollaborative Beobachtungsliste',
 76+ 'specialcollabwatchlist' => 'Kollaborative Beobachtungsliste Spezialseite',
 77+ 'specialcollabwatchlist-desc' => 'Kollaborative Beobachtungsliste Spezialseite Beschreibung',
 78+ 'collabwatchlist-details' => '{{PLURAL:$1|$1 Kategorie/Seite|$1 Kategorien/Seiten}} auf dieser kollaborativen Beobachtungsliste.',
 79+ 'collabwatchlisttagselect' => 'Tag',
 80+ 'collabwatchlisttagcomment' => 'Kommentar',
 81+ 'collabwatchlistsettagbutton' => 'Tag setzen',
 82+ 'collabwatchlist-unset-tag' => 'x',
 83+ 'collabwatchlisttools-view' => 'Anzeigen',
 84+ 'collabwatchlisttools-edit' => 'Kategorien bearbeiten',
 85+ 'collabwatchlisttools-rawCategories' => 'Listenformat Kategorien bearbeiten',
 86+ 'collabwatchlisttools-rawTags' => 'Listenformat Tags bearbeiten',
 87+ 'collabwatchlisttools-rawUsers' => 'Listenformat Benutzer bearbeiten',
 88+ 'collabwatchlisttools-delete' => 'Löschen',
 89+ 'collabwatchlistsall' => 'Alle Listen',
 90+ 'collabwatchlistfiltertags' => 'Tags filtern',
 91+ 'collabwatchlistedit-users-raw-submit' => 'Speichern',
 92+ 'collabwatchlistedit-raw-title' => 'Kategorien im Listenformat bearbeiten',
 93+ 'collabwatchlistedit-tags-raw-title' => 'Tags im Listenformat bearbeiten',
 94+ 'collabwatchlistedit-users-raw-title' => 'Benutzer im Listenformat bearbeiten',
 95+ 'collabwatchlistedit-users-last-owner' => 'Es muss zumindest einen Owner geben',
 96+ 'collabwatchlistedit-numitems' => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Kategorie |$1 Kategorien}}',
 97+ 'collabwatchlistedit-noitems' => 'Diese kollaborative Beobachtungsliste ist leer.',
 98+ 'collabwatchlistedit-tags-numitems' => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Tag |$1 Tags}}',
 99+ 'collabwatchlistedit-tags-noitems' => 'Diese kollaborative Beobachtungsliste ist leer.',
 100+ 'collabwatchlistedit-users-numitems' => 'Diese kollaborative Beobachtungsliste enthält {{PLURAL:$1|1 Benutzer |$1 Benutzer}}',
 101+ 'collabwatchlistedit-users-noitems' => 'Diese kollaborative Beobachtungsliste ist leer.',
 102+ 'collabwatchlistedit-raw-legend' => 'Kategorien der kollaborativen Beobachtungsliste im Listenformat bearbeiten',
 103+ 'collabwatchlistedit-users-raw-legend' => 'Benutzer der Kollaborativen Beobachtungsliste im Listenformat bearbeiten',
 104+ 'collabwatchlistedit-tags-raw-legend' => 'Tags der kollaborativen Beobachtungsliste im Listenformat bearbeiten',
 105+ 'collabwatchlistedit-raw-explain' => 'Dies sind die Kategorien der kollaborativen Beobachtungsliste im Listenformat. Die Einträge können zeilenweise gelöscht oder hinzugefügt werden.',
 106+ '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.',
 107+ '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.',
 108+ 'collabwatchlistedit-raw-titles' => 'Kategorien:',
 109+ 'collabwatchlistedit-tags-raw-titles' => 'Tags:',
 110+ 'collabwatchlistedit-users-raw-titles' => 'Benutzer:',
 111+ 'collabwatchlistedit-normal-title' => 'Kategorien bearbeiten',
 112+ 'collabwatchlistedit-normal-legend' => 'Kategorien von der Beobachtungsliste entfernen',
 113+ '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“.',
 114+ 'collabwatchlistedit-tags-raw-submit' => 'Speichern',
 115+ 'collabwatchlistedit-normal-done' => '{{PLURAL:$1|1 Kategorie wurde|$1 Kategorien wurden}} von der kollaborativen Beobachtungsliste entfernt:',
 116+ 'collabwatchlistedit-tags-raw-done' => 'Die kollaborative Beobachtungsliste wurde gespeichert.',
 117+ 'collabwatchlistedit-users-raw-done' => 'Die kollaborative Beobachtungsliste wurde gespeichert.',
 118+ 'collabwatchlistedit-tags-raw-added' => '{{PLURAL:$1|1 Tag wurde|$1 Tags wurden}} hinzugefügt:',
 119+ 'collabwatchlistedit-users-raw-added' => '{{PLURAL:$1|1 Benutzer wurde|$1 Benutzer wurden}} hinzugefügt:',
 120+ 'collabwatchlistedit-tags-raw-removed' => '{{PLURAL:$1|1 Tag wurde|$1 Tags wurden}} entfernt:',
 121+ 'collabwatchlistedit-users-raw-removed' => '{{PLURAL:$1|1 Benutzer wurde|$1 Benutzer wurden}} entfernt:',
 122+ 'collabwatchlistinverttags' => 'Tag Filter umkehren',
 123+ 'collabwatchlistpatrol' => 'Änderungen kontrolliert',
 124+ 'collabwatchlisttools-newList' => 'Neue kollaborative Beobachtungsliste',
 125+ 'collabwatchlistdelete-legend' => 'Kollaborative Beobachtungsliste löschen',
 126+ 'collabwatchlistdelete-explain' => 'Beim Löschen werden alle Informationen die mit der Beobachtungsliste in Zusammenhang stehen gelöscht. Tags die Änderungen zugewiesen wurden bleiben erhalten.',
 127+ 'collabwatchlistdelete-submit' => 'Löschen',
 128+ 'collabwatchlistdelete-title' => 'Kollaborative Beobachtungsliste löschen',
 129+ 'collabwatchlistedit-set-tags-numitems' => 'Diese kollaborative Beobachtungsliste hat {{PLURAL:$1|1 Tag|$1 Tags}} gesetzt',
 130+ 'collabwatchlistedit-set-tags-noitems' => 'Diese kollaborative Beobachtungsliste hat keine Tags gesetzt',
 131+ 'collabwatchlistnew-legend' => 'Neue kollaborative Beobachtungsliste anlegen',
 132+ 'collabwatchlistnew-explain' => 'Der Name der Liste muss eindeutig sein.',
 133+ 'collabwatchlistnew-name' => 'Name der Liste',
 134+ 'collabwatchlistnew-submit' => 'Anlegen',
 135+ 'collabwatchlistedit-raw-done' => 'The kollaborative Beobachtungsliste wurde gespeichert.',
 136+ 'collabwatchlistedit-raw-added' => '{{PLURAL:$1|1 Kategorie/Seite wurde|$1 Kategorien/Seiten wurden}} hinzugefügt:',
 137+ 'collabwatchlistedit-raw-removed' => '{{PLURAL:$1|1 Kategorie/Seite wurde|$1 Kategorien/Seiten wurden}} entfernt:',
 138+ 'collabwatchlistedit-normal-submit' => 'Speichern',
 139+ 'collabwatchlistshowhidelistusers' => 'Listenbenutzer $1',
 140+ 'tog-collabwatchlisthidelistusers' => 'Bearbeitungen von Benutzern der kollaborativen Beobachtungsliste ausblenden',
 141+);
Index: trunk/extensions/CollabWatchlist/CollabWatchlist.php
@@ -0,0 +1,83 @@
 2+<?php
 3+# Alert the user that this is not a valid entry point to MediaWiki if they try to access the special pages file directly.
 4+if (!defined('MEDIAWIKI')) {
 5+ echo <<<EOT
 6+To install the CollabWatchlist extension, put the following line in LocalSettings.php:
 7+require_once( "\$IP/extensions/CollabWatchlist/CollabWatchlist.php" );
 8+EOT;
 9+ exit( 1 );
 10+}
 11+
 12+
 13+$wgExtensionCredits['specialpage'][] = array(
 14+ 'name' => 'CollabWatchlist',
 15+ 'author' =>'Florian Hackenberger',
 16+ 'url' => 'http://www.mediawiki.org/wiki/User:Flohack',
 17+ 'description' => 'Provides collaborative watchlists based on categories',
 18+ 'descriptionmsg' => 'specialcollabwatchlist-desc',
 19+ 'version' => '0.9.0',
 20+);
 21+
 22+
 23+# Autoload our classes
 24+$wgDir = dirname(__FILE__) . '/';
 25+$wgCollabWatchlistIncludes = $wgDir . 'includes/';
 26+$wgExtensionMessagesFiles['CollabWatchlist'] = $wgDir . 'CollabWatchlist.i18n.php';
 27+$wgExtensionAliasesFiles['CollabWatchlist'] = $wgDir . 'CollabWatchlist.alias.php';
 28+
 29+//$wgAutoloadClasses['CollabWatchlist'] = $wgDir . 'CollabWatchlist.body.php'; # Tell MediaWiki to load the extension body.
 30+$wgAutoloadClasses['SpecialCollabWatchlist'] = $wgCollabWatchlistIncludes . 'SpecialCollabWatchlist.php';
 31+$wgAutoloadClasses['CollabWatchlistChangesList'] = $wgCollabWatchlistIncludes . 'CollabWatchlistChangesList.php';
 32+$wgAutoloadClasses['CategoryTreeManip'] = $wgCollabWatchlistIncludes . 'CategoryTreeManip.php';
 33+$wgAutoloadClasses['CollabWatchlistEditor'] = $wgCollabWatchlistIncludes . 'CollabWatchlistEditor.php';
 34+
 35+$wgSpecialPages['Collabwatchlist'] = 'SpecialCollabWatchlist'; # Let MediaWiki know about your new special page.
 36+$wgSpecialPageGroups['Collabwatchlist'] = 'other';
 37+
 38+$wgHooks['LoadExtensionSchemaUpdates'][] = 'fnCollabWatchlistDbSchema';
 39+$wgHooks['GetPreferences'][] = 'fnCollabWatchlistPreferences';
 40+
 41+function fnCollabWatchlistDbSchema() {
 42+ global $wgExtNewTables;
 43+ $wgSql = dirname(__FILE__) . '/sql/';
 44+ $wgExtNewTables[] = array('collabwatchlist', $wgSql . 'collabwatchlist.sql');
 45+ $wgExtNewTables[] = array('collabwatchlistuser', $wgSql . 'collabwatchlistuser.sql');
 46+ $wgExtNewTables[] = array('collabwatchlistcategory', $wgSql . 'collabwatchlistcategory.sql');
 47+ $wgExtNewTables[] = array('collabwatchlistrevisiontag', $wgSql . 'collabwatchlistrevisiontag.sql');
 48+ $wgExtNewTables[] = array('collabwatchlisttag', $wgSql . 'collabwatchlisttag.sql');
 49+ $wgExtNewFields[] = array('change_tag', 'ct_id', $wgSql . 'patch-change_tag_id.sql');
 50+ return true;
 51+}
 52+
 53+function fnCollabWatchlistPreferences( $user, &$preferences ) {
 54+ $preferences['collabwatchlisthidelistuser'] = array(
 55+ 'type' => 'toggle',
 56+ 'label-message' => 'tog-collabwatchlisthidelistusers',
 57+ 'section' => 'watchlist/advancedwatchlist',
 58+ );
 59+ return true;
 60+}
 61+
 62+$wgCollabWatchlistNSPrefix = 'CollabWatchlist';
 63+$wgCollabWatchlistPermissionDeniedPage = 'CollabWatchlistPermissionDenied';
 64+
 65+/**#@+
 66+ * Collaborative watchlist user types
 67+ * This defines constants for the collabwatchlistuser.rlu_type
 68+ */
 69+define( 'COLLABWATCHLISTUSER_OWNER', 'OWNER' ); // owners are allowed to edit the list
 70+define( 'COLLABWATCHLISTUSER_OWNER_TEXT', 'Owner' ); // owners are allowed to edit the list
 71+define( 'COLLABWATCHLISTUSER_USER', 'USER' ); // users are allowed to view the list and tag edits
 72+define( 'COLLABWATCHLISTUSER_USER_TEXT', 'User' ); // users are allowed to view the list and tag edits
 73+define( 'COLLABWATCHLISTUSER_TRUSTED_EDITOR', 'TRUSTED_EDITOR' ); // trusted editors are used to filter edits which don't require a review
 74+define( 'COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT', 'TrustedEditor' ); // trusted editors are used to filter edits which don't require a review
 75+
 76+function fnCollabWatchlistUserTypeToText( $userType ) {
 77+ if( $userType === COLLABWATCHLISTUSER_OWNER )
 78+ return COLLABWATCHLISTUSER_OWNER_TEXT;
 79+ if( $userType === COLLABWATCHLISTUSER_USER )
 80+ return COLLABWATCHLISTUSER_USER_TEXT;
 81+ if( $userType === COLLABWATCHLISTUSER_TRUSTED_EDITOR )
 82+ return COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT;
 83+}
 84+/**#@-*/
Index: trunk/extensions/CollabWatchlist/includes/SpecialCollabWatchlist.php
@@ -0,0 +1,633 @@
 2+<?php
 3+class SpecialCollabWatchlist extends SpecialPage {
 4+ function __construct() {
 5+ parent::__construct( 'CollabWatchlist' );
 6+ }
 7+
 8+ function execute( $par ) {
 9+ global $wgUser, $wgOut, $wgLang, $wgRequest;
 10+ global $wgRCShowWatchingUsers, $wgEnotifWatchlist;
 11+ global $wgEnotifWatchlist;
 12+
 13+ // Add feed links
 14+ $wlToken = $wgUser->getOption( 'watchlisttoken' );
 15+ if (!$wlToken) {
 16+ $wlToken = sha1( mt_rand() . microtime( true ) );
 17+ $wgUser->setOption( 'watchlisttoken', $wlToken );
 18+ $wgUser->saveSettings();
 19+ }
 20+
 21+ global $wgServer, $wgScriptPath, $wgFeedClasses;
 22+ $apiParams = array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
 23+ 'wlowner' => $wgUser->getName(), 'wltoken' => $wlToken );
 24+ $feedTemplate = wfScript('api').'?';
 25+
 26+ foreach( $wgFeedClasses as $format => $class ) {
 27+ $theseParams = $apiParams + array( 'feedformat' => $format );
 28+ $url = $feedTemplate . wfArrayToCGI( $theseParams );
 29+ $wgOut->addFeedLink( $format, $url );
 30+ }
 31+
 32+ $skin = $wgUser->getSkin();
 33+ $specialTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
 34+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
 35+
 36+ # Anons don't get a watchlist
 37+ if( $wgUser->isAnon() ) {
 38+ $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
 39+ $llink = $skin->linkKnown(
 40+ SpecialPage::getTitleFor( 'Userlogin' ),
 41+ wfMsgHtml( 'loginreqlink' ),
 42+ array(),
 43+ array( 'returnto' => $specialTitle->getPrefixedText() )
 44+ );
 45+ $wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
 46+ return;
 47+ }
 48+
 49+ $wgOut->setPageTitle( wfMsg( 'collabwatchlist' ) );
 50+
 51+ $listIdsAndNames = CollabWatchlistChangesList::getCollabWatchlistIdAndName($wgUser->getId());
 52+ $sub = wfMsgExt(
 53+ 'watchlistfor2',
 54+ array( 'parseinline', 'replaceafter' ),
 55+ $wgUser->getName(),
 56+ ''
 57+ );
 58+ $sub .= '<br />' . CollabWatchlistEditor::buildTools( $listIdsAndNames, $wgUser->getSkin() );
 59+ $wgOut->setSubtitle( $sub );
 60+
 61+ $uid = $wgUser->getId();
 62+
 63+ // The filter form has one checkbox for each tag, build an array
 64+ $postValues = $wgRequest->getValues();
 65+ $tagFilter = array();
 66+ foreach( $postValues as $key => $value ) {
 67+ if( stripos($key, 'collaborative-watchlist-filtertag-') === 0 ) {
 68+ $tagFilter[] = $postValues[$key];
 69+ }
 70+ }
 71+ // Alternative syntax for requests from links (show / hide ...)
 72+ if( empty($tagFilter) ) {
 73+ $tagFilter = explode('|', $wgRequest->getVal('filterTags'));
 74+ }
 75+
 76+ $defaults = array(
 77+ /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
 78+ /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
 79+ /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
 80+ /* bool */ 'hideAnons' => (int)$wgUser->getBoolOption( 'watchlisthideanons' ),
 81+ /* bool */ 'hideLiu' => (int)$wgUser->getBoolOption( 'watchlisthideliu' ),
 82+ /* bool */ 'hideListUser' => (int)$wgUser->getBoolOption( 'collabwatchlisthidelistuser' ),
 83+ /* bool */ 'hidePatrolled' => (int)$wgUser->getBoolOption( 'watchlisthidepatrolled' ),
 84+ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
 85+ /* int */ 'collabwatchlist' => 0,
 86+ /* ? */ 'globalwatch' => 'all',
 87+ /* ? */ 'invert' => false,
 88+ /* ? */ 'invertTags'=> false,
 89+ /* ? */ 'filterTags'=> '',
 90+ );
 91+
 92+ extract($defaults);
 93+
 94+ # Extract variables from the request, falling back to user preferences or
 95+ # other default values if these don't exist
 96+ $prefs['days'] = floatval( $wgUser->getOption( 'watchlistdays' ) );
 97+ $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
 98+ $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
 99+ $prefs['hideanons'] = $wgUser->getBoolOption( 'watchlisthideanon' );
 100+ $prefs['hideliu'] = $wgUser->getBoolOption( 'watchlisthideliu' );
 101+ $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
 102+ $prefs['hidelistuser'] = $wgUser->getBoolOption( 'collabwatchlisthidelistuser' );
 103+ $prefs['hidepatrolled' ] = $wgUser->getBoolOption( 'watchlisthidepatrolled' );
 104+ $prefs['invertTags' ] = $wgUser->getBoolOption( 'collabwatchlistinverttags' );
 105+ $prefs['filterTags' ] = $wgUser->getOption( 'collabwatchlistfiltertags' );
 106+
 107+ # Get query variables
 108+ $days = $wgRequest->getVal( 'days' , $prefs['days'] );
 109+ $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
 110+ $hideBots = $wgRequest->getBool( 'hideBots' , $prefs['hidebots'] );
 111+ $hideAnons = $wgRequest->getBool( 'hideAnons', $prefs['hideanons'] );
 112+ $hideLiu = $wgRequest->getBool( 'hideLiu' , $prefs['hideliu'] );
 113+ $hideOwn = $wgRequest->getBool( 'hideOwn' , $prefs['hideown'] );
 114+ $hideListUser = $wgRequest->getBool( 'hideListUser', $prefs['hidelistuser'] );
 115+ $hidePatrolled = $wgRequest->getBool( 'hidePatrolled' , $prefs['hidepatrolled'] );
 116+ $filterTags = implode('|', $tagFilter);
 117+ $invertTags = $wgRequest->getBool( 'invertTags' , $prefs['invertTags'] );
 118+
 119+ # Get collabwatchlist value, if supplied, and prepare a WHERE fragment
 120+ $collabWatchlist = $wgRequest->getIntOrNull( 'collabwatchlist' );
 121+ $invert = $wgRequest->getBool( 'invert' );
 122+ if( !is_null( $collabWatchlist ) && $collabWatchlist !== 'all') {
 123+ $collabWatchlist = intval( $collabWatchlist );
 124+ } else {
 125+ $collabWatchlist = 0;
 126+ return;
 127+ }
 128+ if(array_key_exists($collabWatchlist, $listIdsAndNames)) {
 129+ $wgOut->addHTML( Xml::element('h2', null, $listIdsAndNames[$collabWatchlist]) );
 130+ }
 131+
 132+ if( ( $mode = CollabWatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
 133+ $editor = new CollabWatchlistEditor();
 134+ $editor->execute( $collabWatchlist, $listIdsAndNames, $wgOut, $wgRequest, $mode );
 135+ return;
 136+ }
 137+
 138+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
 139+ $recentchanges = $dbr->tableName( 'recentchanges' );
 140+
 141+ $nitems = $dbr->selectField( 'collabwatchlistcategory', 'COUNT(*)',
 142+ $collabWatchlist == 0 ? array() : array('rl_id' => $collabWatchlist
 143+ ), __METHOD__ );
 144+ if( $nitems == 0 ) {
 145+ $wgOut->addWikiMsg( 'nowatchlist' );
 146+ return;
 147+ }
 148+
 149+ // Dump everything here
 150+ $nondefaults = array();
 151+
 152+ wfAppendToArrayIfNotDefault( 'days' , $days , $defaults, $nondefaults);
 153+ wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
 154+ wfAppendToArrayIfNotDefault( 'hideBots' , (int)$hideBots , $defaults, $nondefaults);
 155+ wfAppendToArrayIfNotDefault( 'hideAnons', (int)$hideAnons, $defaults, $nondefaults );
 156+ wfAppendToArrayIfNotDefault( 'hideLiu' , (int)$hideLiu , $defaults, $nondefaults );
 157+ wfAppendToArrayIfNotDefault( 'hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
 158+ wfAppendToArrayIfNotDefault( 'hideListUser', (int)$hideListUser, $defaults, $nondefaults);
 159+ wfAppendToArrayIfNotDefault( 'collabwatchlist', $collabWatchlist, $defaults, $nondefaults);
 160+ wfAppendToArrayIfNotDefault( 'hidePatrolled', (int)$hidePatrolled, $defaults, $nondefaults );
 161+ wfAppendToArrayIfNotDefault( 'filterTags', $filterTags , $defaults, $nondefaults );
 162+ wfAppendToArrayIfNotDefault( 'invertTags', $invertTags , $defaults, $nondefaults );
 163+ wfAppendToArrayIfNotDefault( 'invert', $invert , $defaults, $nondefaults );
 164+
 165+ if( $days <= 0 ) {
 166+ $andcutoff = '';
 167+ } else {
 168+ $andcutoff = "rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
 169+ }
 170+
 171+ # If the watchlist is relatively short, it's simplest to zip
 172+ # down its entirety and then sort the results.
 173+
 174+ # If it's relatively long, it may be worth our while to zip
 175+ # through the time-sorted page list checking for watched items.
 176+
 177+ # Up estimate of watched items by 15% to compensate for talk pages...
 178+
 179+ # Toggles
 180+ $andHideOwn = $hideOwn ? "rc_user != $uid" : '';
 181+ $andHideBots = $hideBots ? "rc_bot = 0" : '';
 182+ $andHideMinor = $hideMinor ? "rc_minor = 0" : '';
 183+ $andHideLiu = $hideLiu ? "rc_user = 0" : '';
 184+ $andHideAnons = $hideAnons ? "rc_user != 0" : '';
 185+ $andHideListUser = $hideListUser ? $this->wlGetFilterClauseListUser($collabWatchlist) : '';
 186+ $andHidePatrolled = $wgUser->useRCPatrol() && $hidePatrolled ? "rc_patrolled != 1" : '';
 187+
 188+ # Toggle watchlist content (all recent edits or just the latest)
 189+ if( $wgUser->getOption( 'extendwatchlist' )) {
 190+ $andLatest='';
 191+ $limitWatchlist = intval( $wgUser->getOption( 'wllimit' ) );
 192+ $usePage = false;
 193+ } else {
 194+ # Top log Ids for a page are not stored
 195+ $andLatest = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG;
 196+ $limitWatchlist = 0;
 197+ $usePage = true;
 198+ }
 199+
 200+ # Show a message about slave lag, if applicable
 201+ if( ( $lag = $dbr->getLag() ) > 0 )
 202+ $wgOut->showLagWarning( $lag );
 203+
 204+ # Create output form
 205+ $form = Xml::fieldset( wfMsg( 'watchlist-options' ), false, array( 'id' => 'mw-watchlist-options' ) );
 206+
 207+ # Show watchlist header
 208+ $form .= wfMsgExt( 'collabwatchlist-details', array( 'parseinline' ), $wgLang->formatNum( $nitems ) );
 209+
 210+ if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
 211+ $form .= wfMsgExt( 'wlheader-enotif', 'parse' ) . "\n";
 212+ }
 213+ $form .= '<hr />';
 214+
 215+ $tables = array( 'recentchanges', 'categorylinks' );
 216+ $fields = array( "{$recentchanges}.*" );
 217+ $categoryClause = $this->wlGetFilterClauseForCollabWatchlistIds($collabWatchlist, 'cl_to', 'rc_cur_id');
 218+ // If this collaborative watchlist does not contain any categories, add a clause which gives
 219+ // us an empty result
 220+ $conds = isset($categoryClause) ? array($categoryClause) : array('false');
 221+ $join_conds = array(
 222+ 'categorylinks' => array('LEFT OUTER JOIN', "rc_cur_id=cl_from"),
 223+ );
 224+ if( !empty($tagFilter) ) {
 225+ // The tag filter causes a query runtime of O(MxN), where M is relative to the number
 226+ // of recentchanges we select (from a table which is purged periodically, limited to 250)
 227+ // and N is relative the number of change_tag entries for a recentchange. Doing it
 228+ // the other way around (selecting from change_tag first, is probably slower, as the
 229+ // change_tag table is never purged.
 230+ // Using the tag_summary table for filtering is difficult, at least I have been unable to
 231+ // find a common SQL compliant way for using regular expressions which works across Postgre / Mysql
 232+ // Furthermore, ChangeTags does not seem to prevent tags containing ',' from being set,
 233+ // which renders tag_summary quite unusable
 234+ if( $invertTags ) {
 235+ $filter = 'EXISTS ';
 236+ } else {
 237+ $filter = 'NOT EXISTS ';
 238+ }
 239+ $filter .= '(select ct_rc_id from change_tag
 240+ JOIN collabwatchlistrevisiontag ON collabwatchlistrevisiontag.ct_id = change_tag.ct_id
 241+ WHERE ct_rc_id = recentchanges.rc_id AND ct_tag ';
 242+ if( count($tagFilter) > 1 )
 243+ $filter .= 'IN (' . $dbr->makeList($tagFilter) . '))';
 244+ else
 245+ $filter .= ' = ' . $dbr->addQuotes(current($tagFilter)) . ')';
 246+ $conds[] = $filter;
 247+ }
 248+ $options = array( 'ORDER BY' => 'rc_timestamp DESC' );
 249+ if( $limitWatchlist ) {
 250+ $options['LIMIT'] = $limitWatchlist;
 251+ }
 252+ if( $andcutoff ) $conds[] = $andcutoff;
 253+ if( $andLatest ) $conds[] = $andLatest;
 254+ if( $andHideOwn ) $conds[] = $andHideOwn;
 255+ if( $andHideBots ) $conds[] = $andHideBots;
 256+ if( $andHideMinor ) $conds[] = $andHideMinor;
 257+ if( $andHideLiu ) $conds[] = $andHideLiu;
 258+ if( $andHideAnons ) $conds[] = $andHideAnons;
 259+ if( $andHideListUser ) $conds[] = $andHideListUser;
 260+ if( $andHidePatrolled ) $conds[] = $andHidePatrolled;
 261+
 262+ $rollbacker = $wgUser->isAllowed('rollback');
 263+ if ( $usePage || $rollbacker ) {
 264+ $tables[] = 'page';
 265+ $join_conds['page'] = array('LEFT JOIN','rc_cur_id=page.page_id');
 266+ if ($rollbacker)
 267+ $fields[] = 'page_latest';
 268+ }
 269+
 270+ ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
 271+ wfRunHooks('SpecialCollabWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
 272+
 273+ $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
 274+ $numRows = $dbr->numRows( $res );
 275+
 276+ /* Start bottom header */
 277+
 278+ $wlInfo = '';
 279+ if( $days >= 1 ) {
 280+ $wlInfo = wfMsgExt( 'rcnote', 'parseinline',
 281+ $wgLang->formatNum( $numRows ),
 282+ $wgLang->formatNum( $days ),
 283+ $wgLang->timeAndDate( wfTimestampNow(), true ),
 284+ $wgLang->date( wfTimestampNow(), true ),
 285+ $wgLang->time( wfTimestampNow(), true )
 286+ ) . '<br />';
 287+ } elseif( $days > 0 ) {
 288+ $wlInfo = wfMsgExt( 'wlnote', 'parseinline',
 289+ $wgLang->formatNum( $numRows ),
 290+ $wgLang->formatNum( round($days*24) )
 291+ ) . '<br />';
 292+ }
 293+
 294+ $cutofflinks = "\n" . $this->wlCutoffLinks( $days, 'CollabWatchlist', $nondefaults ) . "<br />\n";
 295+
 296+ $thisTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
 297+
 298+ # Spit out some control panel links
 299+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideminor', 'hideMinor', $hideMinor );
 300+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidebots', 'hideBots', $hideBots );
 301+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideanons', 'hideAnons', $hideAnons );
 302+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhideliu', 'hideLiu', $hideLiu );
 303+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidemine', 'hideOwn', $hideOwn );
 304+ $links[] = $this->wlShowHideLink( $nondefaults, 'collabwatchlistshowhidelistusers', 'hideListUser', $hideListUser );
 305+
 306+ if( $wgUser->useRCPatrol() ) {
 307+ $links[] = $this->wlShowHideLink( $nondefaults, 'rcshowhidepatr', 'hidePatrolled', $hidePatrolled );
 308+ }
 309+
 310+ # Namespace filter and put the whole form together.
 311+ $form .= $wlInfo;
 312+ $form .= $cutofflinks;
 313+ $form .= $wgLang->pipeList( $links );
 314+ $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $thisTitle->getLocalUrl() ) );
 315+ $form .= '<hr /><p>';
 316+ $tagsAndInfo = CollabWatchlistChangesList::getValidTagsAndInfo(array_keys($listIdsAndNames));
 317+ if(count($tagsAndInfo) > 0) {
 318+ $form .= wfMsg('collabwatchlistfiltertags') . ':&nbsp;&nbsp;';
 319+ }
 320+ foreach( $tagsAndInfo as $tag => $tagInfo ) {
 321+ $tagAttr = array(
 322+ 'name' => 'collaborative-watchlist-filtertag-' . $tag,
 323+ 'type' => 'checkbox',
 324+ 'value' => $tag);
 325+ if (in_array($tag, $tagFilter) ) {
 326+ $tagAttr['checked'] = 'checked';
 327+ }
 328+ $form .= Xml::element('input', $tagAttr) . '&nbsp;' . Xml::label( $tag, 'collaborative-watchlist-filtertag-' . $tag ) . '&nbsp;';
 329+ }
 330+ if(count($tagsAndInfo) > 0) {
 331+ $form .= '<br />';
 332+ }
 333+ $form .= Xml::checkLabel( wfMsg('collabwatchlistinverttags'), 'invertTags', 'nsinvertTags', $invertTags ) . '<br />';
 334+ $form .= CollabWatchlistChangesList::collabWatchlistSelector( $listIdsAndNames, $collabWatchlist, '', 'collabwatchlist', wfMsg( 'collabwatchlist' )) . '&nbsp;';
 335+ $form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . '&nbsp;';
 336+ $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
 337+ $form .= Html::hidden( 'days', $days );
 338+ if( $hideMinor )
 339+ $form .= Html::hidden( 'hideMinor', 1 );
 340+ if( $hideBots )
 341+ $form .= Html::hidden( 'hideBots', 1 );
 342+ if( $hideAnons )
 343+ $form .= Html::hidden( 'hideAnons', 1 );
 344+ if( $hideLiu )
 345+ $form .= Html::hidden( 'hideLiu', 1 );
 346+ if( $hideOwn )
 347+ $form .= Html::hidden( 'hideOwn', 1 );
 348+ if( $hideListUser )
 349+ $form .= Html::hidden( 'hideListUser', 1 );
 350+ if( $wgUser->useRCPatrol() )
 351+ if( $hidePatrolled )
 352+ $form .= Html::hidden( 'hidePatrolled', 1);
 353+ $form .= Xml::closeElement( 'form' );
 354+ $form .= Xml::closeElement( 'fieldset' );
 355+ $wgOut->addHTML( $form );
 356+
 357+ # If there's nothing to show, stop here
 358+ if( $numRows == 0 ) {
 359+ $wgOut->addWikiMsg( 'watchnochange' );
 360+ return;
 361+ }
 362+
 363+ /* End bottom header */
 364+
 365+ /* Do link batch query */
 366+ $linkBatch = new LinkBatch;
 367+ while ( $row = $dbr->fetchObject( $res ) ) {
 368+ $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
 369+ if ( $row->rc_user != 0 ) {
 370+ $linkBatch->add( NS_USER, $userNameUnderscored );
 371+ }
 372+ $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
 373+
 374+ $linkBatch->add( $row->rc_namespace, $row->rc_title );
 375+ }
 376+ $linkBatch->execute();
 377+ $dbr->dataSeek( $res, 0 );
 378+
 379+ $list = CollabWatchlistChangesList::newFromUser( $wgUser );
 380+ $list->setWatchlistDivs();
 381+
 382+ $s = $list->beginRecentChangesList();
 383+ $counter = 1;
 384+ while ( $obj = $dbr->fetchObject( $res ) ) {
 385+ # Make RC entry
 386+ $rc = RecentChange::newFromRow( $obj );
 387+ $rc->counter = $counter++;
 388+
 389+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
 390+ $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
 391+ 'COUNT(*)',
 392+ array(
 393+ 'wl_namespace' => $obj->rc_namespace,
 394+ 'wl_title' => $obj->rc_title,
 395+ ),
 396+ __METHOD__ );
 397+ } else {
 398+ $rc->numberofWatchingusers = 0;
 399+ }
 400+
 401+ $tags = $this->wlTagsForRevision($obj->rc_this_oldid, array($collabWatchlist), $invert);
 402+// if( isset($tags) ) {
 403+// // Filter recentchanges which contain unwanted tags
 404+// $tagNames = array();
 405+// foreach($tags as $tagInfo) {
 406+// $tagNames[] = $tagInfo['ct_tag'];
 407+// }
 408+// $unwantedTagsFound = array_intersect($tagFilter, $tagNames);
 409+// if( !empty($unwantedTagsFound) )
 410+// continue;
 411+// }
 412+ $attrs = $rc->getAttributes();
 413+ $attrs['collabwatchlist_tags'] = $tags;
 414+ $rc->setAttribs($attrs);
 415+
 416+ $s .= $list->recentChangesLine( $rc, false, $counter );
 417+ }
 418+ $s .= $list->endRecentChangesList();
 419+
 420+ $dbr->freeResult( $res );
 421+ $wgOut->addHTML( $s );
 422+ }
 423+
 424+ function wlShowHideLink( $options, $message, $name, $value ) {
 425+ global $wgUser;
 426+
 427+ $showLinktext = wfMsgHtml( 'show' );
 428+ $hideLinktext = wfMsgHtml( 'hide' );
 429+ $title = SpecialPage::getTitleFor( 'CollabWatchlist' );
 430+ $skin = $wgUser->getSkin();
 431+
 432+ $label = $value ? $showLinktext : $hideLinktext;
 433+ $options[$name] = 1 - (int) $value;
 434+
 435+ return wfMsgHtml( $message, $skin->linkKnown( $title, $label, array(), $options ) );
 436+ }
 437+
 438+
 439+ function wlHoursLink( $h, $page, $options = array() ) {
 440+ global $wgUser, $wgLang, $wgContLang;
 441+
 442+ $sk = $wgUser->getSkin();
 443+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
 444+ $options['days'] = ($h / 24.0);
 445+
 446+ $s = $sk->linkKnown(
 447+ $title,
 448+ $wgLang->formatNum( $h ),
 449+ array(),
 450+ $options
 451+ );
 452+
 453+ return $s;
 454+ }
 455+
 456+ function wlDaysLink( $d, $page, $options = array() ) {
 457+ global $wgUser, $wgLang, $wgContLang;
 458+
 459+ $sk = $wgUser->getSkin();
 460+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
 461+ $options['days'] = $d;
 462+ $message = ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) );
 463+
 464+ $s = $sk->linkKnown(
 465+ $title,
 466+ $message,
 467+ array(),
 468+ $options
 469+ );
 470+
 471+ return $s;
 472+ }
 473+
 474+ /**
 475+ * Returns html
 476+ */
 477+ function wlCutoffLinks( $days, $page = 'CollabWatchlist', $options = array() ) {
 478+ global $wgLang;
 479+
 480+ $hours = array( 1, 2, 6, 12 );
 481+ $days = array( 1, 3, 7 );
 482+ $i = 0;
 483+ foreach( $hours as $h ) {
 484+ $hours[$i++] = $this->wlHoursLink( $h, $page, $options );
 485+ }
 486+ $i = 0;
 487+ foreach( $days as $d ) {
 488+ $days[$i++] = $this->wlDaysLink( $d, $page, $options );
 489+ }
 490+ return wfMsgExt('wlshowlast',
 491+ array('parseinline', 'replaceafter'),
 492+ $wgLang->pipeList( $hours ),
 493+ $wgLang->pipeList( $days ),
 494+ $this->wlDaysLink( 0, $page, $options ) );
 495+ }
 496+
 497+ /**
 498+ * Count the number of items on a user's watchlist
 499+ *
 500+ * @param $talk Include talk pages
 501+ * @return integer
 502+ */
 503+ function wlCountItems( &$user, $talk = true ) {
 504+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
 505+
 506+ # Fetch the raw count
 507+ $res = $dbr->select( 'watchlist', 'COUNT(*) AS count',
 508+ array( 'wl_user' => $user->mId ), 'wlCountItems' );
 509+ $row = $dbr->fetchObject( $res );
 510+ $count = $row->count;
 511+ $dbr->freeResult( $res );
 512+
 513+ # Halve to remove talk pages if needed
 514+ if( !$talk )
 515+ $count = floor( $count / 2 );
 516+
 517+ return( $count );
 518+ }
 519+
 520+ /** Returns an array of maps representing collab watchlist tags. The following fields are present
 521+ * in each map:
 522+ * - rl_id Id of the collaborative watchlist
 523+ * - ct_tag Name of the tag
 524+ * - collabwatchlistrevisiontag.user_id User which set the tag
 525+ * - user_name Username of the user which set the tag
 526+ * - rrt_comment Collabwatchlist tag comment
 527+ * @param $rev_id
 528+ * @param $rl_ids
 529+ * @param $invert
 530+ * @return unknown_type
 531+ */
 532+ function wlTagsForRevision( $rev_id, $rl_ids = array(), $invert = false, $filterTags = array() ) {
 533+ // Some DB stuff
 534+ $dbr = wfGetDB( DB_SLAVE );
 535+ $cond = array();
 536+ if( isset($rl_ids) && !(count($rl_ids) == 1 && $rl_ids[0] == 0)) {
 537+ if( $invert ) {
 538+ $cond[] = "rl_id NOT IN (" . $dbr->makeList($rl_ids) . ")";
 539+ }else {
 540+ $cond = array("rl_id" => $rl_ids);
 541+ }
 542+ }
 543+ if( isset($filterTags) && count($filterTags) > 0) {
 544+ $cond[] = "ct_tag not in (" . $dbr->makeList($filterTags) . ")";
 545+ }
 546+ //$table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array()
 547+ $res = $dbr->select( array('change_tag', 'collabwatchlistrevisiontag', 'user'), # Tables
 548+ array('rl_id', 'ct_tag', 'collabwatchlistrevisiontag.user_id', 'user_name', 'rrt_comment'), # Fields
 549+ array('ct_rev_id' => $rev_id) + $cond, # Conditions
 550+ __METHOD__, array(),
 551+ # Join conditions
 552+ array( 'collabwatchlistrevisiontag' => array('JOIN', 'change_tag.ct_id = collabwatchlistrevisiontag.ct_id'),
 553+ 'user' => array('JOIN', 'collabwatchlistrevisiontag.user_id = user.user_id')
 554+ )
 555+ );
 556+ $tags = array();
 557+ while( $row = $res->fetchObject() ) {
 558+ $tags[] = get_object_vars( $row );
 559+ }
 560+ $dbr->freeResult( $res );
 561+ return $tags;
 562+ }
 563+
 564+ function wlGetFilterClauseForCollabWatchlistIds($rl_ids, $catNameCol, $pageIdCol) {
 565+ $excludedCatPageIds = array();
 566+ $includedCatPageIds = array();
 567+ $includedPageIds = array();
 568+ $dbr = wfGetDB( DB_SLAVE );
 569+ $res = $dbr->select( array('collabwatchlist', 'collabwatchlistcategory', 'page' ), # Tables
 570+ array('cat_page_id', 'page_title', 'page_namespace', 'subtract'), # Fields
 571+ $rl_ids != 0 ? array('collabwatchlist.rl_id' => $rl_ids) : array(), # Conditions
 572+ __METHOD__, array(),
 573+ # Join conditions
 574+ array( 'collabwatchlistcategory' => array('JOIN', 'collabwatchlist.rl_id = collabwatchlistcategory.rl_id'),
 575+ 'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
 576+ );
 577+ while( $row = $res->fetchObject() ) {
 578+ if($row->page_namespace == NS_CATEGORY) {
 579+ if($row->subtract) {
 580+ $excludedCatPageIds[$row->cat_page_id] = $row->page_title;
 581+ }else {
 582+ $includedCatPageIds[$row->cat_page_id] = $row->page_title;
 583+ }
 584+ }else {
 585+ $includedPageIds[$row->cat_page_id] = $row->page_title;
 586+ }
 587+ }
 588+ $dbr->freeResult( $res );
 589+
 590+ if($includedCatPageIds) {
 591+ $catTree = new CategoryTreeManip();
 592+ $catTree->initialiseFromCategoryNames(array_values($includedCatPageIds));
 593+ $catTree->disableCategoryIds(array_keys($excludedCatPageIds));
 594+ $enabledCategoryNames = $catTree->getEnabledCategoryNames();
 595+ if(empty($enabledCategoryNames))
 596+ return;
 597+ $collabWatchlistClause = '(' . $catNameCol . " IN (" . implode(',', $this->addQuotes($dbr, $enabledCategoryNames)) . ") ";
 598+ if(!empty($includedPageIds)) {
 599+ $collabWatchlistClause .= ' OR ' . $pageIdCol . ' IN (' . implode(',', $this->addQuotes($dbr, array_keys($includedPageIds))) . ')';
 600+ }
 601+ $collabWatchlistClause .= ')';
 602+ }else if(!empty($includedPageIds)) {
 603+ $collabWatchlistClause = $pageIdCol . ' IN (' . implode(',', $this->addQuotes($dbr, array_keys($includedPageIds))) . ')';
 604+ }
 605+ return $collabWatchlistClause;
 606+ }
 607+
 608+ function wlGetFilterClauseListUser($rl_id) {
 609+ $excludedUserIds = array();
 610+ $dbr = wfGetDB( DB_SLAVE );
 611+ $res = $dbr->select( 'collabwatchlistuser', # Tables
 612+ 'user_id', # Fields
 613+ array('collabwatchlistuser.rl_id' => $rl_id) # Conditions
 614+ );
 615+ $clause = '';
 616+ while( $row = $res->fetchObject() ) {
 617+ $excludedUserIds[] = $row->user_id;
 618+ }
 619+ if($res->numRows() > 0) {
 620+ $clause = '( rc_user NOT IN (';
 621+ $clause .= implode(',', $this->addQuotes($dbr, $excludedUserIds)) . ') )';
 622+ }
 623+ $dbr->freeResult( $res );
 624+ return $clause;
 625+ }
 626+
 627+ public static function addQuotes($db, $strings) {
 628+ $result = array();
 629+ foreach($strings as $string) {
 630+ $result[] = $db->addQuotes($string);
 631+ }
 632+ return $result;
 633+ }
 634+}
Index: trunk/extensions/CollabWatchlist/includes/CategoryTreeManip.php
@@ -0,0 +1,201 @@
 2+<?php
 3+
 4+/**
 5+ * This class is used to build a category tree and manipulate it.
 6+ * It currently supports building the tree from a list of categories.
 7+ * You can then disable categories by id and request a list of
 8+ * enabled categories and subcategories. This is useful for selecting
 9+ * pages by categories and their subcategories without specifying the
 10+ * subcategories.
 11+ * @author fhackenberger
 12+ */
 13+class CategoryTreeManip {
 14+
 15+ var $root;
 16+ var $name;
 17+ var $id;
 18+ var $catPageIdToNode = array();
 19+ var $parents = array();
 20+ var $enabled = true;
 21+ var $children = array();
 22+
 23+ /**
 24+ * Constructor
 25+ */
 26+ function __construct($id = NULL, $name = NULL, $root = NULL, $parents = array()) {
 27+ $this->id = $id;
 28+ $this->name = $name;
 29+ if(!is_null($root)) {
 30+ $this->root = $root;
 31+ }else {
 32+ $this->root = $this;
 33+ }
 34+ $this->parents = $parents;
 35+ }
 36+
 37+ private function addChildren($children) {
 38+ if(!is_array($children))
 39+ throw new Exception('Argument must be an array');
 40+ foreach($children as $child) {
 41+ $this->children[$child->id] = $child;
 42+ }
 43+ }
 44+
 45+ private function addParents($parents) {
 46+ if(!is_array($parents))
 47+ throw new Exception('Argument must be an array');
 48+ foreach($parents as $parent) {
 49+ $this->parents[$parent->id] = $parent;
 50+ }
 51+ }
 52+
 53+ /** Disable this category node and all subcategory nodes
 54+ * @return
 55+ */
 56+ public function disable() {
 57+ $this->recursiveDisable();
 58+ }
 59+
 60+ /** Disable the given categories (by id) and all their subcategories
 61+ *
 62+ * @param array $catPageIds The page ids of the categories to disable
 63+ * @return
 64+ */
 65+ public function disableCategoryIds($catPageIds) {
 66+ foreach($catPageIds as $catId) {
 67+ $node = $this->getNodeForCatPageId($catId);
 68+ if(isset($node)) {
 69+ $node->disable();
 70+ }
 71+ }
 72+ }
 73+
 74+ private function recursiveDisable($visitedNodeIds = array()) {
 75+ if(!$this->enabled || array_key_exists($this->id, $visitedNodeIds))
 76+ return; # Break the recursion
 77+ $this->enabled = false;
 78+ $visitedNodeIds[] = $this->id;
 79+ foreach($this->children as $cat) {
 80+ $cat->recursiveDisable($visitedNodeIds);
 81+ }
 82+ }
 83+
 84+ /** Returns a list of enables category names, including
 85+ * all subcategories.
 86+ *
 87+ * @return array An array of category names
 88+ */
 89+ public function getEnabledCategoryNames() {
 90+ $enabledNodes = $this->getEnabledNodeMap();
 91+ $enabledCategories = array();
 92+ foreach($enabledNodes as $nodeId => $node) {
 93+ $enabledCategories[] = $node->name;
 94+ }
 95+ return $enabledCategories;
 96+ }
 97+
 98+ /** Returns a map of enabled categories, including
 99+ * all subcategories.
 100+ *
 101+ * @return array An array mapping from category page ids to CategoryTreeManip objects
 102+ */
 103+ public function getEnabledNodeMap() {
 104+ return $this->root->recursiveGetEnabledNodeMap();
 105+ }
 106+
 107+ private function recursiveGetEnabledNodeMap(&$foundNodes = array()) {
 108+ if(isset($this->id)) {
 109+ if(!$this->enabled || array_key_exists($this->id, $foundNodes))
 110+ return $foundNodes; # Break the recursion
 111+ $foundNodes[$this->id] = $this;
 112+ }
 113+ foreach($this->children as $cat) {
 114+ $cat->recursiveGetEnabledNodeMap($foundNodes);
 115+ }
 116+ return $foundNodes;
 117+ }
 118+
 119+ /** Returns a CategoryTreeManip node, given a category page id
 120+ *
 121+ * @param $catPageId The page id of the category to retrieve
 122+ * @return CategoryTreeManip The node
 123+ */
 124+ public function getNodeForCatPageId($catPageId) {
 125+ if(array_key_exists($catPageId, $this->root->catPageIdToNode))
 126+ return $this->root->catPageIdToNode[$catPageId];
 127+ }
 128+
 129+ private function addNode($node) {
 130+ $this->root->catPageIdToNode[$node->id] = $node;
 131+ }
 132+
 133+ /** Build the category tree, given a list of category names.
 134+ * All categories and subcategories are enabled by default.
 135+ *
 136+ * @param array $catNames An array of strings representing category names
 137+ * @return
 138+ */
 139+ public function initialiseFromCategoryNames($catNames) {
 140+ $dbr = wfGetDB( DB_SLAVE );
 141+ while($catNames) {
 142+ $res = $dbr->select( array('categorylinks', 'page' ), # Tables
 143+ array('cl_to AS parName', 'cl_from AS childId', 'page_title AS childName'), # Fields
 144+ array('cl_to' => $catNames, 'page_namespace' => NS_CATEGORY), # Conditions
 145+ __METHOD__, array(),
 146+ # Join conditions
 147+ array('page' => array('JOIN', 'page_id = cl_from') )
 148+ );
 149+ $parentList = array();
 150+ $childList = array();
 151+ while( $row = $res->fetchObject() ) {
 152+ $parentList[$row->parName][] = array($row->childId, $row->childName);
 153+ if(array_key_exists($row->childId, $childList)) {
 154+ $childEntry = $childList[$row->childId];
 155+ $childEntry[1][] = $row->parName;
 156+ }else {
 157+ $childList[$row->childId] = array($row->childName, array($row->parName));
 158+ }
 159+ }
 160+ $dbr->freeResult( $res );
 161+
 162+ if(!isset($parentNameToNode) && !empty($parentList)) {
 163+ // Fetch the page ids of the $catNames and add the parent categories if needed
 164+ $res = $dbr->select( array('page' ), # Tables
 165+ array('page_id, page_title'), # Fields
 166+ array('page_title' => array_keys($parentList)) # Conditions
 167+ );
 168+ $parentNameToNode = array();
 169+ while( $row = $res->fetchObject() ) {
 170+ $node = $this->getNodeForCatPageId($row->page_id);
 171+ if(!isset($node)) {
 172+ $node = new CategoryTreeManip($row->page_id, $row->page_title, $this->root);
 173+ $this->addNode($node);
 174+ $this->addChildren(array($node));
 175+ }
 176+ $parentNameToNode[$row->page_title] = $node;
 177+ }
 178+ $dbr->freeResult( $res );
 179+ }
 180+
 181+ $newChildNameToNode = array();
 182+ // Add the new child nodes
 183+ foreach($childList as $childPageId => $childInfo) {
 184+ $childNode = $this->getNodeForCatPageId($childPageId);
 185+ if(!isset($childNode)) {
 186+ $childNode = new CategoryTreeManip($childPageId, $childInfo[0], $this->root);
 187+ $this->addNode($childNode);
 188+ $newChildNameToNode[$childInfo[0]] = $childNode;
 189+ }
 190+ foreach($childInfo[1] as $parentName) {
 191+ $parent = $parentNameToNode[$parentName];
 192+ $parent->addChildren(array($childNode));
 193+ $childNode->addParents(array($parent));
 194+ }
 195+ }
 196+
 197+ // Prepare for the next loop
 198+ $parentNameToNode = $newChildNameToNode;
 199+ $catNames = array_keys($parentNameToNode);
 200+ }
 201+ }
 202+}
Index: trunk/extensions/CollabWatchlist/includes/CollabWatchlistChangesList.php
@@ -0,0 +1,416 @@
 2+<?php
 3+/*
 4+ * Generates a list of changes for a collaborative watchlist. Builds on the EnhancedChangesList
 5+ */
 6+class CollabWatchlistChangesList extends EnhancedChangesList {
 7+ protected $user;
 8+ protected $tagCheckboxIndex = 0;
 9+
 10+ /**
 11+ * Collaborative Watchlist contructor
 12+ * @param User $user
 13+ * @param Skin $skin
 14+ */
 15+ public function __construct( $skin, $user ) {
 16+ parent::__construct($skin);
 17+ $this->user = $user;
 18+ }
 19+
 20+ /**
 21+ * (non-PHPdoc)
 22+ * @see includes/EnhancedChangesList#beginRecentChangesList()
 23+ */
 24+ public function beginRecentChangesList() {
 25+ global $wgRequest;
 26+ $gwlSpeciaPageTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
 27+ $result = Xml::openElement('form', array(
 28+ 'class' => 'mw-collaborative-watchlist-addtag-form',
 29+ 'method' => 'post',
 30+ 'action' => $gwlSpeciaPageTitle->getLocalUrl( array( 'action' => 'setTags' ))));
 31+ $result .= Xml::input('redirTarget', false, $wgRequest->getFullRequestURL(), array('type' => 'hidden'));
 32+ $result .= parent::beginRecentChangesList();
 33+ return $result;
 34+ }
 35+
 36+ /**
 37+ * (non-PHPdoc)
 38+ * @see includes/EnhancedChangesList#endRecentChangesList()
 39+ */
 40+ public function endRecentChangesList() {
 41+ global $wgRequest;
 42+ $collabWatchlist = $wgRequest->getIntOrNull( 'collabwatchlist' );
 43+ $result = '';
 44+ $result .= parent::endRecentChangesList();
 45+ $glWlIdAndName = $this->getCollabWatchlistIdAndName($this->user->getId());
 46+ $result .= $this->collabWatchlistAndTagSelectors($glWlIdAndName, $collabWatchlist, null, 'collabwatchlist', wfMsg( 'collabwatchlist' )) . '&nbsp;';
 47+ $result .= Xml::label( wfMsg('collabwatchlisttagcomment'), 'tagcomment' ) . '&nbsp;' . Xml::input( 'tagcomment' ) . '&nbsp;';
 48+ if( $this->user->useRCPatrol() )
 49+ $result .= Xml::checkLabel( wfMsg('collabwatchlistpatrol'), 'setpatrolled', 'setpatrolled', true ) . '&nbsp;';
 50+ $result .= Xml::submitButton(wfMsg( 'collabwatchlistsettagbutton' ));
 51+ $result .= Xml::closeElement('form');
 52+ return $result;
 53+ }
 54+
 55+ /**
 56+ * (non-PHPdoc)
 57+ * @see includes/EnhancedChangesList#insertBeforeRCFlags($r, $rcObj)
 58+ */
 59+ protected function insertBeforeRCFlags( &$r, &$rcObj ) {
 60+ $r .= Xml::element('input', array(
 61+ 'name' => 'collaborative-watchlist-addtag-' . $this->tagCheckboxIndex,
 62+ 'type' => 'checkbox',
 63+ 'value' => ($rcObj->getTitle() . '|' . $rcObj->getAttribute('rc_this_oldid') . '|' . $rcObj->getAttribute('rc_id'))));
 64+ $this->tagCheckboxIndex++;
 65+ }
 66+
 67+ /**
 68+ * (non-PHPdoc)
 69+ * @see includes/EnhancedChangesList#insertBeforeRCFlagsBlock($r, $block)
 70+ */
 71+ protected function insertBeforeRCFlagsBlock( &$r, &$block ) {
 72+ $r .= Xml::element('input', array(
 73+ 'name' => 'collaborative-watchlist-addtag-placeholder',
 74+ 'type' => 'checkbox',
 75+ 'style' => 'visibility: hidden;'));
 76+ }
 77+
 78+ /**
 79+ * (non-PHPdoc)
 80+ * @see includes/ChangesList#insertRollback($s, $rc)
 81+ */
 82+ public function insertRollback( &$s, &$rc ) {
 83+ global $wgUser;
 84+ parent::insertRollback($s, $rc);
 85+ if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
 86+ if ($wgUser->isAllowed('edit') ) {
 87+ $rev = new Revision( array(
 88+ 'id' => $rc->mAttribs['rc_this_oldid'],
 89+ 'user' => $rc->mAttribs['rc_user'],
 90+ 'user_text' => $rc->mAttribs['rc_user_text'],
 91+ 'deleted' => $rc->mAttribs['rc_deleted']
 92+ ) );
 93+ $undoAfter = $rev->getPrevious();
 94+ $undoLink = $this->generateUndoLink($this->skin, $rc->getTitle(), $rev, $undoAfter);
 95+ if( isset($undoLink) )
 96+ $s .= '&nbsp;' . $undoLink;
 97+ }
 98+ }
 99+ }
 100+
 101+ /**
 102+ * Fetch an appropriate changes list class for the specified user
 103+ * Some users might want to use an enhanced list format, for instance
 104+ *
 105+ * @param $user User to fetch the list class for
 106+ * @return ChangesList derivative
 107+ */
 108+ public static function newFromUser( &$user ) {
 109+ $sk = $user->getSkin();
 110+ $list = NULL;
 111+ if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) {
 112+ return new CollabWatchlistChangesList( $sk, $user );
 113+ } else {
 114+ return $list;
 115+ }
 116+ }
 117+
 118+ /**
 119+ * (non-PHPdoc)
 120+ * @see includes/ChangesList#insertTags($s, $rc, $classes)
 121+ */
 122+ public function insertTags( &$s, &$rc, &$classes ) {
 123+ if ( !empty($rc->mAttribs['collabwatchlist_tags']) ) {
 124+ list($tagSummary, $newClasses) = $this->formatReviewSummaryRow( $rc, 'changeslist' );
 125+ $classes = array_merge( $classes, $newClasses );
 126+ $s .= ' ' . $tagSummary;
 127+ }
 128+ }
 129+
 130+ /**
 131+ * (non-PHPdoc)
 132+ * @see includes/EnhancedChangesList#insertHistLink($s, $rc, $title, $params, $sep)
 133+ */
 134+ protected function insertHistLink( &$s, &$rc, $title, $params = array(), $sep = NULL ) {
 135+ // No history
 136+ }
 137+
 138+ /**
 139+ * (non-PHPdoc)
 140+ * @see includes/EnhancedChangesList#insertCurrAndLastLinks($s, $rc)
 141+ */
 142+ protected function insertCurrAndLastLinks( &$s, &$rc ) {
 143+ $s .= ' (';
 144+ $s .= $rc->curlink;
 145+ $s .= ')';
 146+ }
 147+
 148+ /**
 149+ * (non-PHPdoc)
 150+ * @see includes/EnhancedChangesList#insertUserAndTalkLinks($s, $rc)
 151+ */
 152+ protected function insertUserAndTalkLinks( &$s, &$rc ) {
 153+ $s .= $rc->userlink;
 154+ }
 155+
 156+ /**
 157+ * Insert the tags of the given change
 158+ */
 159+ private function formatReviewSummaryRow( $rc, $page ) {
 160+ global $wgRequest;
 161+ $s = '';
 162+ if( !$rc )
 163+ return $s;
 164+
 165+ $attr = $rc->mAttribs;
 166+ $tagRows = $attr['collabwatchlist_tags'];
 167+
 168+ $classes = array();
 169+
 170+ $displayTags = array();
 171+ foreach( $tagRows as $tagRow ) {
 172+ $tag = $tagRow['ct_tag'];
 173+ $collabwatchlistTag = Xml::tags(
 174+ 'span',
 175+ array( 'class' => 'mw-collabwatchlist-tag-marker ' .
 176+ Sanitizer::escapeClass( "mw-collabwatchlist-tag-marker-$tag" ),
 177+ 'title' => $tagRow['rrt_comment']),
 178+ ChangeTags::tagDescription( $tag )
 179+ );
 180+ $classes[] = Sanitizer::escapeClass( "mw-collabwatchlist-tag-$tag" );
 181+
 182+ /** Insert links to user page, user talk page and eventually a blocking link */
 183+ $userLink = $this->skin->userLink( $tagRow['user_id'], $tagRow['user_name'] );
 184+ $delTagTarget = CollabWatchlistEditor::getUnsetTagUrl( $wgRequest->getFullRequestURL(), $attr['rc_title'], $tagRow['rl_id'], $tag, $attr['rc_id'] );
 185+ $delTagLink = Xml::element('a', array('href' => $delTagTarget, 'class' => 'mw-collabwatchlist-unsettag-' . $tag), wfMsg('collabwatchlist-unset-tag'));
 186+ $displayTags[] = $collabwatchlistTag . ' ' . $delTagLink . ' ' . $userLink;
 187+ }
 188+ $markers = '(' . implode( ', ', $displayTags ) . ')';
 189+ $markers = Xml::tags( 'span', array( 'class' => 'mw-collabwatchlist-tag-markers' ), $markers );
 190+ return array( $markers, $classes );
 191+ }
 192+
 193+ /** Generate a form 'select' element for the collaborative watchlists and a 'select' element for choosing a tag.
 194+ * The tag selector reacts on the watchlist selector and displays the relevant tags only, if javascript is enabled.
 195+ *
 196+ * @see #collabWatchlistSelector()
 197+ * @see #tagSelector()
 198+ * @param String $rlLabel The label for the collab watchlist select tag
 199+ * @param String $rlElementId The id for the collab watchlist select tag
 200+ * @param String $tagLabel The label for the tag selector
 201+ * @return A string containing HTML
 202+ */
 203+ public static function collabWatchlistAndTagSelectors($glWlIdAndName, $selected = '', $all = null, $element_name = 'collabwatchlist', $rlLabel = null, $rlElementId = 'collabwatchlist', $tagLabel = null) {
 204+ global $wgJsMimeType;
 205+ $tagElementIdBase = 'mw-collaborative-watchlist-addtag-selector';
 206+ $ret = self::collabWatchlistSelector($glWlIdAndName, $selected, $all, $element_name, $rlLabel, $rlElementId, $tagElementIdBase);
 207+ $ret .= '&nbsp;';
 208+ $ret .= self::tagSelector(array_keys($glWlIdAndName), $tagLabel);
 209+ // Make sure the correct tags for the default selection are set
 210+ $ret .= Xml::element( 'script',
 211+ array(
 212+ 'type' => $wgJsMimeType,
 213+ ),
 214+ 'window.onLoad = onCollabWatchlistSelection(\'' . $tagElementIdBase . '\', document.getElementById(\'' . $rlElementId . '\').value)', false
 215+ );
 216+ return $ret;
 217+ }
 218+
 219+ /**
 220+ * Build a drop-down box for selecting a collaborative watchlist
 221+ * This method optionally adds javascript for changing a tag selector
 222+ * depending on the selected review list
 223+ *
 224+ * @param $glWlIdAndName Mixed: The result from getCollabWatchlistIdAndName()
 225+ * @param $selected Mixed: Reviewlist which should be pre-selected
 226+ * @param $all Mixed: Value of an item denoting all collaborative watchlists, or null to omit
 227+ * @param $element_name String: value of the "name" attribute of the select tag
 228+ * @param $label String: optional label to add to the field
 229+ * @param $element_id String: optional the id of the select element
 230+ * @param $tagElementIdBase String: optional the base id of the collabl watchlist tag selector for javascript functionality.
 231+ * @return string
 232+ */
 233+ public static function collabWatchlistSelector( $glWlIdAndName, $selected = '', $all = null, $element_name = 'collabwatchlist', $label = null, $element_id = 'collabwatchlist', $tagElementIdBase = null ) {
 234+ global $wgContLang, $wgScriptPath, $wgJsMimeType;
 235+ $ret = '';
 236+ if(isset($tagElementIdBase)) {
 237+ $jsPath = "$wgScriptPath/extensions/CollabWatchlist/js";
 238+ $ret .= Xml::element( 'script',
 239+ array(
 240+ 'type' => $wgJsMimeType,
 241+ 'src' => "$jsPath/CollabWatchlist.js",
 242+ ),
 243+ '', false
 244+ );
 245+ }
 246+ $options = array();
 247+
 248+ // Godawful hack... we'll be frequently passed selected namespaces
 249+ // as strings since PHP is such a shithole.
 250+ // But we also don't want blanks and nulls and "all"s matching 0,
 251+ // so let's convert *just* string ints to clean ints.
 252+ if( preg_match( '/^\d+$/', $selected ) ) {
 253+ $selected = intval( $selected );
 254+ }
 255+
 256+ if( !is_null( $all ) )
 257+ $glWlIdAndName = array( $all => wfMsg( 'collabwatchlistsall' ) ) + $glWlIdAndName;
 258+ foreach( $glWlIdAndName as $index => $name ) {
 259+ if( $index < NS_MAIN )
 260+ continue;
 261+ if( $index === 0 )
 262+ $name = wfMsg( 'blankcollabwatchlist' );
 263+ $options[] = Xml::option( $name, $index, $index === $selected, isset($tagElementIdBase) ?
 264+ array('onclick' => 'onCollabWatchlistSelection("' . $tagElementIdBase . '", this.value)') :
 265+ array()
 266+ );
 267+ }
 268+
 269+ $selectorHtml = Xml::openElement( 'select', array(
 270+ 'id' => $element_id, 'name' => $element_name,
 271+ 'class' => 'collabwatchlistselector', ))
 272+ . "\n"
 273+ . implode( "\n", $options )
 274+ . "\n"
 275+ . Xml::closeElement( 'select' );
 276+ if ( !is_null( $label ) ) {
 277+ $ret .= Xml::label( $label, $element_name ) . '&nbsp;' . $selectorHtml;
 278+ } else {
 279+ $ret .= $selectorHtml;
 280+ }
 281+ return $ret;
 282+ }
 283+
 284+ /**
 285+ * Build a drop-down box for selecting a collaborative watchlist tag
 286+ *
 287+ * @param array $rlIds A list of collaborative watchlist ids
 288+ * @param String $label The label for the select tag
 289+ * @param String $elemId The id of the select tag
 290+ * @return String A string containing HTML
 291+ */
 292+ public static function tagSelector( $rlIds, $label = '', $elemId = 'mw-collaborative-watchlist-addtag-selector' ) {
 293+ global $wgContLang;
 294+ $tagsAndInfo = CollabWatchlistChangesList::getValidTagsAndInfo($rlIds);
 295+ $optionsAll = array();
 296+ $options = array();
 297+ foreach( $tagsAndInfo as $tagName => $info ) {
 298+ $optionsAll[] = Xml::option( $tagName . ' ' . $info['rt_description'], $tagName );
 299+ foreach( $info['rl_ids'] as $rlId ) {
 300+ $options[$rlId][] = Xml::option( $tagName, $tagName );
 301+ }
 302+ }
 303+ $ret = Xml::openElement( 'select', array(
 304+ 'id' => $elemId,
 305+ 'name' => 'collabwatchlisttag',
 306+ 'class' => 'mw-collaborative-watchlist-tag-selector')) .
 307+ implode("\n", $optionsAll) .
 308+ Xml::closeElement('select');
 309+ if ( !is_null( $label ) ) {
 310+ $ret = Xml::label( $label, $elemId ) . '&nbsp;' . $ret;
 311+ }
 312+ foreach( $options as $rlId => $optionsRl) {
 313+ $ret .= Xml::openElement( 'select', array(
 314+ 'style' => 'display: none;',
 315+ 'id' => $elemId . '-' . $rlId,
 316+ 'name' => 'collabwatchlisttag-rl',
 317+ 'class' => 'mw-collaborative-watchlist-tag-selector')) .
 318+ implode("\n", $optionsRl) .
 319+ Xml::closeElement('select');
 320+ }
 321+ $ret .= Xml::openElement( 'select', array(
 322+ 'style' => 'display: none;',
 323+ 'id' => $elemId . '-empty',
 324+ 'name' => 'collabwatchlisttag-rl',
 325+ 'class' => 'mw-collaborative-watchlist-tag-selector')) .
 326+ Xml::closeElement('select');
 327+
 328+ return $ret;
 329+ }
 330+
 331+ /** Returns an array mapping from collab watchlist tag names to information about the tag
 332+ *
 333+ * The info is an array with the following keys:
 334+ * 'rt_description' The description of the tag
 335+ * 'rl_ids' An array of collab watchlist ids the tag belongs to
 336+ * @param array $rlIds A list of collab watchlist ids
 337+ * @return array Mapping from tag name to info
 338+ */
 339+ public static function getValidTagsAndInfo( $rlIds ) {
 340+ if(!isset($rlIds) || empty($rlIds)) {
 341+ return array();
 342+ }
 343+ $dbr = wfGetDB( DB_SLAVE );
 344+ $res = $dbr->select( array('collabwatchlisttag' ), # Tables
 345+ array('rt_name', 'rt_description', 'rl_id'), # Fields
 346+ array('rl_id' => $rlIds), # Conditions
 347+ __METHOD__
 348+ );
 349+ $list = array();
 350+ while( $row = $res->fetchObject() ) {
 351+ if(array_key_exists($row->rt_name, $list)) {
 352+ $list[$row->rt_name]['rl_ids'][] = $row->rl_id;
 353+ } else {
 354+ $list[$row->rt_name] = array('rt_description' => $row->rt_description, 'rl_ids' => array($row->rl_id));
 355+ }
 356+ }
 357+ $dbr->freeResult( $res );
 358+ return $list;
 359+ }
 360+
 361+ //XXX Cache the result of this method in this class
 362+ /** Get an array mapping from collab watchlist id to its name, filtering by member type
 363+ * The method return only collab watchlist the given user is a member of, restricted by the allowed member types
 364+ * @param int $user_id The id of the collab watchlist user
 365+ * @param array $member_types A list of allowed membership types
 366+ * @return array Mapping from collab watchlist id to its name
 367+ */
 368+ public static function getCollabWatchlistIdAndName( $user_id, $member_types = array(COLLABWATCHLISTUSER_OWNER, COLLABWATCHLISTUSER_USER) ) {
 369+ global $wgDBprefix;
 370+ $dbr = wfGetDB( DB_SLAVE );
 371+ $list = array();
 372+ //$table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array()
 373+ $res = $dbr->select( array('collabwatchlist', 'collabwatchlistuser' ), # Tables
 374+ array($wgDBprefix . 'collabwatchlist.rl_id', 'rl_name'), # Fields
 375+ array('rlu_type' => $member_types, $wgDBprefix . 'collabwatchlistuser.user_id' => $user_id), # Conditions
 376+ __METHOD__, array(),
 377+ # Join conditions
 378+ array( 'collabwatchlistuser' => array('JOIN', $wgDBprefix . 'collabwatchlist.rl_id = ' . $wgDBprefix . 'collabwatchlistuser.rl_id') )
 379+ );
 380+ while( $row = $res->fetchObject() ) {
 381+ $list[$row->rl_id] = $row->rl_name;
 382+ }
 383+ $dbr->freeResult( $res );
 384+ return $list;
 385+ }
 386+
 387+ //XXX Copied from HistoryPage, we should patch HistoryPage to export that functionality
 388+ // as a static function
 389+ /**
 390+ * @param Skin $skin
 391+ * @param Title $title
 392+ * @param Revision $revision
 393+ * @param Revision $undoAfterRevision
 394+ * @return String Undo Link
 395+ */
 396+ public static function generateUndoLink($skin, $title, $revision, $undoAfterRevision) {
 397+ if( ! $revision instanceof Revision || ! $undoAfterRevision instanceof Revision ||
 398+ ! $title instanceof Title || !$skin instanceof Skin )
 399+ return null;
 400+ # Create undo tooltip for the first (=latest) line only
 401+ $undoTooltip = $revision->isCurrent()
 402+ ? array( 'title' => wfMsg( 'tooltip-undo' ) )
 403+ : array();
 404+ $undolink = $skin->link(
 405+ $title,
 406+ wfMsgHtml( 'editundo' ),
 407+ $undoTooltip,
 408+ array(
 409+ 'action' => 'edit',
 410+ 'undoafter' => $undoAfterRevision->getId(),
 411+ 'undo' => $revision->getId()
 412+ ),
 413+ array( 'known', 'noclasses' )
 414+ );
 415+ return "<span class=\"mw-history-undo\">{$undolink}</span>";
 416+ }
 417+}
Index: trunk/extensions/CollabWatchlist/includes/CollabWatchlistEditor.php
@@ -0,0 +1,1346 @@
 2+<?php
 3+
 4+/**
 5+ * Provides the UI through which users can perform editing
 6+ * operations on collaborative watchlists
 7+ *
 8+ * @ingroup CollabWatchlist
 9+ * @author Rob Church <robchur@gmail.com>
 10+ * @author Florian Hackenberger <f.hackenberger@chello.at>
 11+ */
 12+class CollabWatchlistEditor {
 13+
 14+ /**
 15+ * Editing modes
 16+ */
 17+ const EDIT_CLEAR = 1;
 18+ const CATEGORIES_EDIT_RAW = 2;
 19+ const EDIT_NORMAL = 3;
 20+ const TAGS_EDIT_RAW = 4;
 21+ const SET_TAGS = 5;
 22+ const UNSET_TAGS = 6;
 23+ const USERS_EDIT_RAW = 7;
 24+ const NEW_LIST = 8;
 25+ const DELETE_LIST = 9;
 26+
 27+ /**
 28+ * Main execution point
 29+ *
 30+ * @param $rlId Collaborative watchlist id
 31+ * @param $listIdsAndNames An array mapping from list id to list name
 32+ * @param $output OutputPage
 33+ * @param $request WebRequest
 34+ * @param $mode int
 35+ */
 36+ public function execute( $rlId, $listIdsAndNames, $output, $request, $mode ) {
 37+ global $wgUser, $wgCollabWatchlistPermissionDeniedPage;
 38+ if( wfReadOnly() ) {
 39+ $output->readOnlyPage();
 40+ return;
 41+ }
 42+ if( ($mode === self::EDIT_CLEAR ||
 43+ $mode === self::CATEGORIES_EDIT_RAW ||
 44+ $mode === self::USERS_EDIT_RAW ||
 45+ $mode === self::EDIT_NORMAL ||
 46+ $mode === self::TAGS_EDIT_RAW ||
 47+ $mode === self::DELETE_LIST) && (!isset($rlId) || $rlId === 0) ) {
 48+ $thisTitle = SpecialPage::getTitleFor( 'CollabWatchlist' );
 49+ $output->redirect( $thisTitle->getLocalURL() );
 50+ return;
 51+ }
 52+ $permissionDeniedTarget = Title::newFromText( $wgCollabWatchlistPermissionDeniedPage )->getLocalURL();
 53+ switch( $mode ) {
 54+ case self::EDIT_CLEAR:
 55+ // The "Clear" link scared people too much.
 56+ // Pass on to the raw editor, from which it's very easy to clear.
 57+ case self::CATEGORIES_EDIT_RAW:
 58+ $output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-raw-title' ) );
 59+ if( $request->wasPosted() ) {
 60+ if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
 61+ $output->redirect( $permissionDeniedTarget );
 62+ break;
 63+ }
 64+ $wanted = $this->extractCollabWatchlistCategories( $request->getText( 'titles' ) );
 65+ $current = $this->getCollabWatchlistCategories( $rlId );
 66+ if( count( $wanted ) > 0 ) {
 67+ $toWatch = array_diff( $wanted, $current );
 68+ $toUnwatch = array_diff( $current, $wanted );
 69+ $toWatch = $this->watchTitles( $toWatch, $rlId );
 70+ $this->unwatchTitles( $toUnwatch, $rlId );
 71+ if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
 72+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-done', 'parse' ) );
 73+ if( ( $count = count( $toWatch ) ) > 0 ) {
 74+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-added', 'parse', $count ) );
 75+ $this->showTitles( $toWatch, $output, $wgUser->getSkin() );
 76+ }
 77+ if( ( $count = count( $toUnwatch ) ) > 0 ) {
 78+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-removed', 'parse', $count ) );
 79+ $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() );
 80+ }
 81+ } else {
 82+ $this->clearCollabWatchlist( $rlId );
 83+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-raw-removed', 'parse', count( $current ) ) );
 84+ $this->showTitles( $current, $output, $wgUser->getSkin() );
 85+ }
 86+ }
 87+ $this->showRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
 88+ break;
 89+ case self::USERS_EDIT_RAW:
 90+ $output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-users-raw-title' ) );
 91+ if( $request->wasPosted() ) {
 92+ if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
 93+ $output->redirect( $permissionDeniedTarget );
 94+ break;
 95+ }
 96+ $wanted = $this->extractCollabWatchlistUsers( $request->getText( 'titles' ) );
 97+ $current = $this->getCollabWatchlistUsers( $rlId );
 98+ $isOwnerCb = create_function('$a', 'return stripos($a, "' . COLLABWATCHLISTUSER_OWNER_TEXT . ' ' . '") === 0;');
 99+ $wantedOwners = array_filter($wanted, $isOwnerCb);
 100+ if( count( $wantedOwners ) < 1 ) {
 101+ // Make sure there is at least one owner left
 102+ $currentOwners = array_filter($current, $isOwnerCb);
 103+ $reAddedOwner = current($currentOwners);
 104+ $wanted[] = $reAddedOwner;
 105+ list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $reAddedOwner );
 106+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-last-owner', 'parse' ) );
 107+ $this->showTitles( array($titleText), $output, $wgUser->getSkin() );
 108+ }
 109+ if( count( $wanted ) > 0 ) {
 110+ $toAdd = array_diff( $wanted, $current );
 111+ $toDel = array_diff( $current, $wanted );
 112+ $toAdd = $this->addUsers( $toAdd, $rlId );
 113+ $this->delUsers( $toDel, $rlId );
 114+ if( count( $toAdd ) > 0 || count( $toDel ) > 0 )
 115+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-done', 'parse' ) );
 116+ if( ( $count = count( $toAdd ) ) > 0 ) {
 117+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-added', 'parse', $count ) );
 118+ $this->showTitles( $toAdd, $output, $wgUser->getSkin() );
 119+ }
 120+ if( ( $count = count( $toDel ) ) > 0 ) {
 121+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-removed', 'parse', $count ) );
 122+ $this->showTitles( $toDel, $output, $wgUser->getSkin() );
 123+ }
 124+ } else {
 125+ $this->clearCollabWatchlist( $rlId );
 126+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-raw-removed', 'parse', count( $current ) ) );
 127+ $this->showTitles( $current, $output, $wgUser->getSkin() );
 128+ }
 129+ }
 130+ $this->showUsersRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
 131+ break;
 132+ case self::TAGS_EDIT_RAW:
 133+ $output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-tags-raw-title' ) );
 134+ if( $request->wasPosted() ) {
 135+ if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
 136+ $output->redirect( $permissionDeniedTarget );
 137+ break;
 138+ }
 139+ $wanted = $this->extractCollabWatchlistTags( $request->getText( 'titles' ) );
 140+ $current = $this->getCollabWatchlistTags( $rlId );
 141+ if( count( $wanted ) > 0 ) {
 142+ $newTags = array_diff_assoc( $wanted, $current );
 143+ $removeTags = array_diff_assoc( $current, $wanted );
 144+ $this->removeTags( array_keys($removeTags), $rlId );
 145+ $this->addTags( $newTags, $rlId );
 146+ if( count( $newTags ) > 0 || count( $removeTags ) > 0 )
 147+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-done', 'parse' ) );
 148+ if( ( $count = count( $newTags ) ) > 0 ) {
 149+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-added', 'parse', $count ) );
 150+ $this->showTagList( $newTags, $output, $wgUser->getSkin() );
 151+ }
 152+ if( ( $count = count( $removeTags ) ) > 0 ) {
 153+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-removed', 'parse', $count ) );
 154+ $this->showTagList( $removeTags, $output, $wgUser->getSkin() );
 155+ }
 156+ } else {
 157+ $this->clearCollabWatchlist( $rlId );
 158+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-raw-removed', 'parse', count( $current ) ) );
 159+ $this->showTagList( $current, $output, $wgUser->getSkin() );
 160+ }
 161+ }
 162+ $this->showTagsRawForm( $output, $rlId, $listIdsAndNames[$rlId] );
 163+ break;
 164+ case self::EDIT_NORMAL:
 165+ $output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistedit-normal-title' ) );
 166+ if( $request->wasPosted() ) {
 167+ if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
 168+ $output->redirect( $permissionDeniedTarget );
 169+ break;
 170+ }
 171+ $titles = $this->extractCollabWatchlistCategories( $request->getArray( 'titles' ) );
 172+ $this->unwatchTitles( $titles, $rlId );
 173+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-normal-done', 'parse',
 174+ $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
 175+ $this->showTitles( $titles, $output, $wgUser->getSkin() );
 176+ }
 177+ $this->showNormalForm( $output, $rlId );
 178+ break;
 179+ case self::SET_TAGS:
 180+ $redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
 181+ if( $request->wasPosted() ) {
 182+ $rlId = $request->getInt('collabwatchlist', -1);
 183+ if( ! $this->checkPermissions( $wgUser, $rlId, array(COLLABWATCHLISTUSER_USER, COLLABWATCHLISTUSER_OWNER) ) ) {
 184+ $output->redirect( $permissionDeniedTarget );
 185+ break;
 186+ }
 187+ $redirTarget = $request->getText('redirTarget', $redirTarget);
 188+ $tagToAdd = $request->getText('collabwatchlisttag');
 189+ $tagcomment = $request->getText('tagcomment');
 190+ $setPatrolled = $request->getBool('setpatrolled', false);
 191+ $pagesToTag = array();
 192+ if( strlen($tagToAdd) !== 0 && $rlId !== -1 ) {
 193+ $postValues = $request->getValues();
 194+ foreach( $postValues as $key => $value ) {
 195+ if( stripos($key, 'collaborative-watchlist-addtag-') === 0 ) {
 196+ $pageRevRcId = explode('|', $value);
 197+ if( count($pageRevRcId) < 3 ) {
 198+ continue;
 199+ }
 200+ $pagesToTag[$pageRevRcId[0]][] = array('rev_id' => $pageRevRcId[1], 'rc_id' => $pageRevRcId[2]);
 201+ }
 202+ }
 203+ $this->setTags( $pagesToTag, $tagToAdd, $wgUser->getId(), $rlId, $tagcomment, $setPatrolled );
 204+ }
 205+ }
 206+ $output->redirect( $redirTarget );
 207+ break;
 208+ case self::UNSET_TAGS:
 209+ $rlId = $request->getInt('collabwatchlist', -1);
 210+ if( ! $this->checkPermissions( $wgUser, $rlId, array(COLLABWATCHLISTUSER_USER, COLLABWATCHLISTUSER_OWNER) ) ) {
 211+ $output->redirect( $permissionDeniedTarget );
 212+ break;
 213+ }
 214+ $redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
 215+ $redirTarget = $request->getText('redirTarget', $redirTarget);
 216+ $page = $request->getText('collabwatchlistpage');
 217+ $tagToDel = $request->getText('collabwatchlisttag');
 218+ $rcId = $request->getInt('collabwatchlistrcid', -1);
 219+ if( strlen($page) !== 0 && strlen($tagToDel) !== 0 && $rlId !== -1 && $rcId !== -1 ) {
 220+ $pagesToUntag[$page][] = array('rc_id' => $rcId);
 221+ $this->unsetTags( $pagesToUntag, $tagToDel, $wgUser->getId(), $rlId );
 222+ }
 223+ $output->redirect( $redirTarget );
 224+ break;
 225+ case self::NEW_LIST:
 226+ if( $request->wasPosted() ) {
 227+ $redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
 228+ $listId = $this->createNewList($request->getText('listname'));
 229+ if( isset($listId) ) {
 230+ $output->redirect( $redirTarget );
 231+ } else {
 232+ $output->addHTML( wfMsgExt( 'collabwatchlistnew-name-exists', 'parse' ) );
 233+ }
 234+ } else {
 235+ $this->showNewListForm($output);
 236+ }
 237+ break;
 238+ case self::DELETE_LIST:
 239+ $output->setPageTitle( $listIdsAndNames[$rlId] . ' ' . wfMsg( 'collabwatchlistdelete-title' ) );
 240+ $rlId = $request->getInt('collabwatchlist', -1);
 241+ if( $request->wasPosted() ) {
 242+ if( ! $this->checkToken( $request, $wgUser, $rlId ) ) {
 243+ $output->redirect( $permissionDeniedTarget );
 244+ break;
 245+ }
 246+ $this->deleteList($rlId);
 247+ $redirTarget = SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl();
 248+ $output->redirect( $redirTarget );
 249+ } else {
 250+ $this->showDeleteListForm($output, $rlId);
 251+ }
 252+ break;
 253+ }
 254+ }
 255+
 256+ /**
 257+ * Check the edit token from a form submission
 258+ *
 259+ * @param $request WebRequest
 260+ * @param $user User
 261+ * @param $rlId Id of the collaborative watchlist to check users against
 262+ * @param $memberTypes Which types of members are allowed
 263+ * @return bool
 264+ */
 265+ private function checkToken( $request, $user, $rlId, $memberTypes = array(COLLABWATCHLISTUSER_OWNER) ) {
 266+ $tokenOk = $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' ) && $request->getVal( 'collabwatchlist' ) !== 0;
 267+ if( $tokenOk === false )
 268+ return $tokenOk;
 269+ return $this->checkPermissions( $user, $rlId, $memberTypes );
 270+ }
 271+
 272+ private function checkPermissions( $user, $rlId, $memberTypes = array(COLLABWATCHLISTUSER_OWNER) ) {
 273+ // Check permissions
 274+ $dbr = wfGetDB( DB_MASTER );
 275+ $res = $dbr->select( 'collabwatchlistuser',
 276+ 'COUNT(*) AS count',
 277+ array( 'rl_id' => $rlId, 'user_id' => $user->getId(), 'rlu_type' => $memberTypes ),
 278+ __METHOD__
 279+ );
 280+ $row = $dbr->fetchObject( $res );
 281+ return $row->count >= 1;
 282+ }
 283+
 284+ /**
 285+ * Extract a list of categories from a blob of text, returning
 286+ * (prefixed) strings
 287+ *
 288+ * @param $list mixed
 289+ * @return array
 290+ */
 291+ private function extractCollabWatchlistCategories( $list ) {
 292+ $titles = array();
 293+ if( !is_array( $list ) ) {
 294+ $list = explode( "\n", trim( $list ) );
 295+ if( !is_array( $list ) )
 296+ return array();
 297+ }
 298+ foreach( $list as $text ) {
 299+ $subtract = false;
 300+ $text = trim( $text );
 301+ $titleText = $text;
 302+ if( stripos($text, '- ') === 0 ) {
 303+ $subtract = true;
 304+ $titleText = trim( substr($text, 2) );
 305+ }
 306+ if( strlen( $text ) > 0 ) {
 307+ $title = Title::newFromText( $titleText );
 308+ if( $title instanceof Title && $title->isWatchable() ) {
 309+ $titles[] = $subtract ? '- ' . $title->getPrefixedText() : $title->getPrefixedText();
 310+ }
 311+ }
 312+ }
 313+ return array_unique( $titles );
 314+ }
 315+
 316+ private function extractTypeTypeTextAndUsername( $typeAndUsernameStr ) {
 317+ $type = COLLABWATCHLISTUSER_USER;
 318+ $typeText = COLLABWATCHLISTUSER_USER_TEXT;
 319+ $text = trim( $typeAndUsernameStr );
 320+ $titleText = $text;
 321+ if( stripos($text, COLLABWATCHLISTUSER_OWNER_TEXT . ' ') === 0 ) {
 322+ $type = COLLABWATCHLISTUSER_OWNER;
 323+ $typeText = COLLABWATCHLISTUSER_OWNER_TEXT;
 324+ $titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_OWNER_TEXT . ' ')) );
 325+ }else if( stripos($text, COLLABWATCHLISTUSER_USER_TEXT . ' ') === 0 ) {
 326+ $type = COLLABWATCHLISTUSER_USER;
 327+ $typeText = COLLABWATCHLISTUSER_USER_TEXT;
 328+ $titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_USER_TEXT . ' ')) );
 329+ }else if( stripos($text, COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT . ' ') === 0 ) {
 330+ $type = COLLABWATCHLISTUSER_TRUSTED_EDITOR;
 331+ $typeText = COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT;
 332+ $titleText = trim( substr($text, strlen(COLLABWATCHLISTUSER_TRUSTED_EDITOR_TEXT . ' ')) );
 333+ }
 334+ return array($type, $typeText, $titleText);
 335+ }
 336+
 337+ /**
 338+ * Extract a list of users from a blob of text, returning
 339+ * (prefixed) strings
 340+ *
 341+ * @param $list mixed
 342+ * @return array
 343+ */
 344+ private function extractCollabWatchlistUsers( $list ) {
 345+ $titles = array();
 346+ if( !is_array( $list ) ) {
 347+ $list = explode( "\n", trim( $list ) );
 348+ if( !is_array( $list ) )
 349+ return array();
 350+ }
 351+ foreach( $list as $text ) {
 352+ list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $text );
 353+ if( strlen( $text ) > 0 ) {
 354+ $user = User::newFromName($titleText);
 355+ if( $user instanceof User ) {
 356+ $titles[] = $typeText . ' ' . $user->getName();
 357+ }
 358+ }
 359+ }
 360+ return array_unique( $titles );
 361+ }
 362+
 363+ /**
 364+ * Extract a list of tags from a blob of text, returning
 365+ * (prefixed) strings
 366+ *
 367+ * @param $list mixed
 368+ * @return array
 369+ */
 370+ private function extractCollabWatchlistTags( $list ) {
 371+ $tags = array();
 372+ if( !is_array( $list ) ) {
 373+ $list = explode( "\n", trim( $list ) );
 374+ if( !is_array( $list ) )
 375+ return array();
 376+ }
 377+ foreach( $list as $text ) {
 378+ $subtract = false;
 379+ $text = trim($text);
 380+ if( strlen( $text ) > 0 ) {
 381+ $pipepos = stripos($text, '|');
 382+ $description = '';
 383+ if( $pipepos > 0 ) {
 384+ if( ($pipepos + 1) < strlen($text) )
 385+ $description = trim(substr($text, $pipepos + 1));
 386+ $text = trim(substr($text, 0, $pipepos));
 387+ }
 388+ $tags[$text] = $description;
 389+ }
 390+ }
 391+ return $tags;
 392+ }
 393+
 394+ /**
 395+ * Print out a list of linked titles
 396+ *
 397+ * $titles can be an array of strings or Title objects; the former
 398+ * is preferred, since Titles are very memory-heavy
 399+ *
 400+ * @param $titles An array of strings, or Title objects
 401+ * @param $output OutputPage
 402+ * @param $skin Skin
 403+ */
 404+ private function showTitles( $titles, $output, $skin ) {
 405+ $talk = wfMsgHtml( 'talkpagelinktext' );
 406+ // Do a batch existence check
 407+ $batch = new LinkBatch();
 408+ foreach( $titles as $title ) {
 409+ if( !$title instanceof Title )
 410+ $title = Title::newFromText( $title );
 411+ if( $title instanceof Title ) {
 412+ $batch->addObj( $title );
 413+ $batch->addObj( $title->getTalkPage() );
 414+ }
 415+ }
 416+ $batch->execute();
 417+ // Print out the list
 418+ $output->addHTML( "<ul>\n" );
 419+ foreach( $titles as $title ) {
 420+ if( !$title instanceof Title )
 421+ $title = Title::newFromText( $title );
 422+ if( $title instanceof Title ) {
 423+ $output->addHTML( "<li>" . $skin->link( $title )
 424+ . ' (' . $skin->link( $title->getTalkPage(), $talk ) . ")</li>\n" );
 425+ }
 426+ }
 427+ $output->addHTML( "</ul>\n" );
 428+ }
 429+
 430+ /**
 431+ * Print out a list of tags with description
 432+ *
 433+ * $titles can be an array of strings or Title objects; the former
 434+ * is preferred, since Titles are very memory-heavy
 435+ *
 436+ * @param $tagsAndDesc An array of strings mapping from tag name to description
 437+ * @param $output OutputPage
 438+ * @param $skin Skin
 439+ */
 440+ private function showTagList( $tagsAndDesc, $output, $skin ) {
 441+ // Print out the list
 442+ $output->addHTML( "<ul>\n" );
 443+ foreach( $tagsAndDesc as $title => $description ) {
 444+ $output->addHTML( "<li>" . $title
 445+ . ' (' . $description . ")</li>\n" );
 446+ }
 447+ $output->addHTML( "</ul>\n" );
 448+ }
 449+
 450+ /**
 451+ * Count the number of categories on a collaborative watchlist
 452+ *
 453+ * @param $rlId Collaborative watchlist id
 454+ * @return int
 455+ */
 456+ private function countCollabWatchlistCategories( $rlId ) {
 457+ $dbr = wfGetDB( DB_MASTER );
 458+ $res = $dbr->select( 'collabwatchlistcategory', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
 459+ $row = $dbr->fetchObject( $res );
 460+ return $row->count;
 461+ }
 462+
 463+ /**
 464+ * Count the number of users on a collaborative watchlist
 465+ *
 466+ * @param $rlId Collaborative watchlist id
 467+ * @return int
 468+ */
 469+ private function countCollabWatchlistUsers( $rlId ) {
 470+ $dbr = wfGetDB( DB_MASTER );
 471+ $res = $dbr->select( 'collabwatchlistuser', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
 472+ $row = $dbr->fetchObject( $res );
 473+ return $row->count;
 474+ }
 475+
 476+ /**
 477+ * Count the number of tags on a collaborative watchlist
 478+ *
 479+ * @param $rlId Collaborative watchlist id
 480+ * @return int
 481+ */
 482+ private function countCollabWatchlistTags( $rlId ) {
 483+ $dbr = wfGetDB( DB_MASTER );
 484+ $res = $dbr->select( 'collabwatchlisttag', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
 485+ $row = $dbr->fetchObject( $res );
 486+ return $row->count;
 487+ }
 488+
 489+ /**
 490+ * Count the number of set edit tags on a collaborative watchlist
 491+ *
 492+ * @param $rlId Collaborative watchlist id
 493+ * @return int
 494+ */
 495+ private function countCollabWatchlistSetTags( $rlId ) {
 496+ $dbr = wfGetDB( DB_MASTER );
 497+ $res = $dbr->select( 'collabwatchlistrevisiontag', 'COUNT(*) AS count', array( 'rl_id' => $rlId ), __METHOD__ );
 498+ $row = $dbr->fetchObject( $res );
 499+ return $row->count;
 500+ }
 501+
 502+ /**
 503+ * Prepare a list of categories on a collaborative watchlist
 504+ * and return an array of (prefixed) strings
 505+ *
 506+ * @param $rlId Collaborative watchlist id
 507+ * @return array
 508+ */
 509+ private function getCollabWatchlistCategories( $rlId ) {
 510+ $list = array();
 511+ $dbr = wfGetDB( DB_MASTER );
 512+ $res = $dbr->select(
 513+ array('collabwatchlistcategory', 'page'),
 514+ array('page_title', 'page_namespace', 'subtract'),
 515+ array(
 516+ 'rl_id' => $rlId,
 517+ ),
 518+ __METHOD__, array(),
 519+ # Join conditions
 520+ array( 'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
 521+ );
 522+ if( $res->numRows() > 0 ) {
 523+ while( $row = $res->fetchObject() ) {
 524+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
 525+ if( $title instanceof Title && !$title->isTalkPage() )
 526+ $list[] = $row->subtract ? '- ' . $title->getPrefixedText() : $title->getPrefixedText();
 527+ }
 528+ $res->free();
 529+ }
 530+ return $list;
 531+ }
 532+
 533+ /**
 534+ * Prepare a list of users on a collaborative watchlist
 535+ * and return an array of (prefixed) strings
 536+ *
 537+ * @param $rlId Collaborative watchlist id
 538+ * @return array
 539+ */
 540+ private function getCollabWatchlistUsers( $rlId ) {
 541+ $list = array();
 542+ $dbr = wfGetDB( DB_MASTER );
 543+ $res = $dbr->select(
 544+ array('collabwatchlistuser', 'user'),
 545+ array('user_name', 'rlu_type'),
 546+ array(
 547+ 'rl_id' => $rlId,
 548+ ),
 549+ __METHOD__, array(),
 550+ # Join conditions
 551+ array( 'user' => array('JOIN', 'user.user_id = collabwatchlistuser.user_id') )
 552+ );
 553+ if( $res->numRows() > 0 ) {
 554+ while( $row = $res->fetchObject() ) {
 555+ $typeText = fnCollabWatchlistUserTypeToText($row->rlu_type);
 556+ $list[] = $typeText . ' ' . $row->user_name;
 557+ }
 558+ $res->free();
 559+ }
 560+ return $list;
 561+ }
 562+
 563+ /**
 564+ * Prepare a list of tags on a collaborative watchlist
 565+ * and return an array of tag names mapping to tag descriptions
 566+ *
 567+ * @param $rlId Collaborative watchlist id
 568+ * @return array
 569+ */
 570+ private function getCollabWatchlistTags( $rlId ) {
 571+ $list = array();
 572+ $dbr = wfGetDB( DB_MASTER );
 573+ $res = $dbr->select(
 574+ array('collabwatchlisttag'),
 575+ array('rt_name', 'rt_description'),
 576+ array(
 577+ 'rl_id' => $rlId,
 578+ ), __METHOD__
 579+ );
 580+ if( $res->numRows() > 0 ) {
 581+ while( $row = $res->fetchObject() ) {
 582+ $list[$row->rt_name] = $row->rt_description;
 583+ }
 584+ $res->free();
 585+ }
 586+ return $list;
 587+ }
 588+
 589+ /**
 590+ * Get a list of categories on collaborative watchlist, excluding talk pages,
 591+ * and return as a two-dimensional array with namespace and title which
 592+ * maps to an array with 'redirect' and 'subtract' keys.
 593+ *
 594+ * @param $rlId Collaborative watchlist id
 595+ * @return array
 596+ */
 597+ private function getWatchlistInfo( $rlId ) {
 598+ $titles = array();
 599+ $dbr = wfGetDB( DB_MASTER );
 600+
 601+ $res = $dbr->select(
 602+ array('collabwatchlistcategory', 'page'),
 603+ array('page_title', 'page_namespace', 'page_id', 'page_len', 'page_is_redirect', 'subtract'),
 604+ array(
 605+ 'rl_id' => $rlId,
 606+ ),
 607+ __METHOD__, array(),
 608+ # Join conditions
 609+ array( 'page' => array('JOIN', 'page.page_id = collabwatchlistcategory.cat_page_id') )
 610+ );
 611+
 612+ if( $res && $dbr->numRows( $res ) > 0 ) {
 613+ $cache = LinkCache::singleton();
 614+ while( $row = $dbr->fetchObject( $res ) ) {
 615+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
 616+ if( $title instanceof Title ) {
 617+ // Update the link cache while we're at it
 618+ if( $row->page_id ) {
 619+ $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
 620+ } else {
 621+ $cache->addBadLinkObj( $title );
 622+ }
 623+ // Ignore non-talk
 624+ if( !$title->isTalkPage() )
 625+ $titles[$row->page_namespace][$row->page_title] = array('redirect' => $row->page_is_redirect, 'subtract' => $row->subtract);
 626+ }
 627+ }
 628+ }
 629+ return $titles;
 630+ }
 631+
 632+ /**
 633+ * Show a message indicating the number of categories on the collaborative watchlist,
 634+ * and return this count for additional checking
 635+ *
 636+ * @param $output OutputPage
 637+ * @param $rlId The id of the collaborative watchlist
 638+ * @return int
 639+ */
 640+ private function showItemCount( $output, $rlId ) {
 641+ if( ( $count = $this->countCollabWatchlistCategories( $rlId ) ) > 0 ) {
 642+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-numitems', 'parse',
 643+ $GLOBALS['wgLang']->formatNum( $count ) ) );
 644+ } else {
 645+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-noitems', 'parse' ) );
 646+ }
 647+ return $count;
 648+ }
 649+
 650+ /**
 651+ * Show a message indicating the number of categories on the collaborative watchlist,
 652+ * and return this count for additional checking
 653+ *
 654+ * @param $output OutputPage
 655+ * @param $rlId The id of the collaborative watchlist
 656+ * @return int
 657+ */
 658+ private function showTagItemCount( $output, $rlId ) {
 659+ if( ( $count = $this->countCollabWatchlistTags( $rlId ) ) > 0 ) {
 660+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-numitems', 'parse',
 661+ $GLOBALS['wgLang']->formatNum( $count ) ) );
 662+ } else {
 663+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-tags-noitems', 'parse' ) );
 664+ }
 665+ return $count;
 666+ }
 667+
 668+ /**
 669+ * Show a message indicating the number of set tags for edits on the collaborative watchlist,
 670+ * and return this count for additional checking
 671+ *
 672+ * @param $output OutputPage
 673+ * @param $rlId The id of the collaborative watchlist
 674+ * @return int
 675+ */
 676+ private function showSetTagsItemCount( $output, $rlId ) {
 677+ if( ( $count = $this->countCollabWatchlistSetTags( $rlId ) ) > 0 ) {
 678+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-set-tags-numitems', 'parse',
 679+ $GLOBALS['wgLang']->formatNum( $count ) ) );
 680+ } else {
 681+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-set-tags-noitems', 'parse' ) );
 682+ }
 683+ return $count;
 684+ }
 685+
 686+ /**
 687+ * Show a message indicating the number of categories on the collaborative watchlist,
 688+ * and return this count for additional checking
 689+ *
 690+ * @param $output OutputPage
 691+ * @param $rlId The id of the collaborative watchlist
 692+ * @return int
 693+ */
 694+ private function showUserItemCount( $output, $rlId ) {
 695+ if( ( $count = $this->countCollabWatchlistUsers( $rlId ) ) > 0 ) {
 696+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-numitems', 'parse',
 697+ $GLOBALS['wgLang']->formatNum( $count ) ) );
 698+ } else {
 699+ $output->addHTML( wfMsgExt( 'collabwatchlistedit-users-noitems', 'parse' ) );
 700+ }
 701+ return $count;
 702+ }
 703+
 704+ /**
 705+ * Remove all categories from a collaborative watchlist
 706+ *
 707+ * @param $rlId Collaborative watchlist if
 708+ */
 709+ private function clearCollabWatchlist( $rlId ) {
 710+ $dbw = wfGetDB( DB_MASTER );
 711+ $dbw->delete( 'collabwatchlistcategory', array( 'rl_id' => $rlId ), __METHOD__ );
 712+ }
 713+
 714+ /**
 715+ * Add a list of categories to a collaborative watchlist
 716+ *
 717+ * $titles is an array of strings, prefixed with '- ', if the
 718+ * category is subtracted
 719+ *
 720+ * @param $titles An array of strings
 721+ * @param $rlId The id of the collaborative watchlist
 722+ */
 723+ private function watchTitles( $titles, $rlId ) {
 724+ $dbw = wfGetDB( DB_MASTER );
 725+ $rows = array();
 726+ $added = array();
 727+ foreach( $titles as $title ) {
 728+ $subtract = false;
 729+ $title = trim( $title );
 730+ $titleText = $title;
 731+ if( stripos($title, '- ') === 0 ) {
 732+ $subtract = true;
 733+ $titleText = trim( substr($title, 2) );
 734+ }
 735+ $titleObj = Title::newFromText( $titleText );
 736+ if( $titleObj instanceof Title && $titleObj->exists() ) {
 737+ $rows[] = array(
 738+ 'rl_id' => $rlId,
 739+ 'cat_page_id' => $titleObj->getArticleID(),
 740+ 'subtract' => $subtract,
 741+ );
 742+ $added[] = $title;
 743+ }
 744+ }
 745+ $dbw->insert( 'collabwatchlistcategory', $rows, __METHOD__, 'IGNORE' );
 746+ return $added;
 747+ }
 748+
 749+ /**
 750+ * Add a list of users to a collaborative watchlist
 751+ *
 752+ * $titles is an array of strings, prefixed with the user type text and ' '
 753+ *
 754+ * @param $titles An array of strings
 755+ * @param $rlId The id of the collaborative watchlist
 756+ */
 757+ private function addUsers( $users, $rlId ) {
 758+ $dbw = wfGetDB( DB_MASTER );
 759+ $rows = array();
 760+ $added = array();
 761+ foreach( $users as $userString ) {
 762+ list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $userString );
 763+ $user = User::newFromName($titleText);
 764+ if( $user instanceof User && $user->getId() !== 0) {
 765+ $rows[] = array(
 766+ 'rl_id' => $rlId,
 767+ 'user_id' => $user->getId(),
 768+ 'rlu_type' => $type,
 769+ );
 770+ $added[] = $userString;
 771+ }
 772+ }
 773+ $dbw->insert( 'collabwatchlistuser', $rows, __METHOD__, 'IGNORE' );
 774+ return $added;
 775+ }
 776+
 777+ private function setTags( $titlesAndTagInfo, $tag, $userId, $rlId, $comment, $setPatrolled = false) {
 778+ //XXX Attach a hook to delete tags from the collabwatchlistrevisiontag table as soon as the actual tags are deleted from the change_tags table
 779+ $allowedTagsAndInfo = $this->getCollabWatchlistTags($rlId);
 780+ if(!array_key_exists($tag, $allowedTagsAndInfo)) {
 781+ return false;
 782+ }
 783+ $dbw = wfGetDB( DB_MASTER );
 784+ foreach( $titlesAndTagInfo as $title => $infos ) {
 785+ $rcIds = array();
 786+ // Add entries for the tag to the change_tags table
 787+ // optionally mark edit as patrolled
 788+ foreach( $infos as $infoKey => $info ) {
 789+ ChangeTags::addTags($tag, $info['rc_id'], $info['rev_id']);
 790+ $rcIds[] = $info['rc_id'];
 791+ if( $setPatrolled ) {
 792+ RecentChange::markPatrolled($info['rc_id']);
 793+ }
 794+ }
 795+ // Add the tagged revisions to the collaborative watchlist
 796+ $sql = 'INSERT IGNORE INTO collabwatchlistrevisiontag (ct_id, rl_id, user_id, rrt_comment)
 797+ SELECT ct_id, ' . $dbw->strencode($rlId) . ',' .
 798+ $dbw->strencode($userId) . ',' .
 799+ $dbw->addQuotes($comment) . ' FROM change_tag WHERE ct_tag = ? AND ct_rc_id ';
 800+ if( count($rcIds) > 1 ) {
 801+ $sql .= 'IN (' . $dbw->makeList($rcIds) . ')';
 802+ $params = array( $tag );
 803+ }else {
 804+ $sql .= '= ?';
 805+ $params = array( $tag, $rcIds[0] );
 806+ }
 807+ $prepSql = $dbw->prepare($sql);
 808+ $res = $dbw->execute($prepSql, $params);
 809+ $dbw->freePrepared($prepSql);
 810+ return true;
 811+ }
 812+ }
 813+
 814+ private function unsetTags( $titlesAndTagInfo, $tag, $userId, $rlId ) {
 815+ $dbw = wfGetDB( DB_MASTER );
 816+ foreach( $titlesAndTagInfo as $title => $infos ) {
 817+ $rcIds = array();
 818+ foreach( $infos as $infoKey => $info ) {
 819+ // XXX Remove entries for the tag from the change_tags table
 820+// ChangeTags::addTags($tag, $info['rc_id'], $info['rev_id']);
 821+ $rcIds[] = $info['rc_id'];
 822+ }
 823+ // Remove the tag from the collaborative watchlist
 824+ $sql = 'delete collabwatchlistrevisiontag from collabwatchlistrevisiontag JOIN change_tag
 825+ ON change_tag.ct_id = collabwatchlistrevisiontag.ct_id
 826+ WHERE ct_tag = ? AND ct_rc_id ';
 827+ if( count($rcIds) > 1 ) {
 828+ $sql .= 'IN (' . $dbw->makeList($rcIds) . ')';
 829+ $params = array( $tag );
 830+ }else {
 831+ $sql .= '= ?';
 832+ $params = array( $tag, $rcIds[0] );
 833+ }
 834+ $prepSql = $dbw->prepare($sql);
 835+ $res = $dbw->execute($prepSql, $params);
 836+ $dbw->freePrepared($prepSql);
 837+ }
 838+ }
 839+
 840+ /**
 841+ * Add a list of tags to a collaborative watchlist
 842+ *
 843+ * $titles is an array of strings
 844+ *
 845+ * @param $titles An array of strings (tag names) mapping to tag descriptions
 846+ * @param $rlId The id of the collaborative watchlist
 847+ */
 848+ private function addTags( $titles, $rlId ) {
 849+ $dbw = wfGetDB( DB_MASTER );
 850+ $rows = array();
 851+ foreach( $titles as $title => $description ) {
 852+ $rows[] = array(
 853+ 'rl_id' => $rlId,
 854+ 'rt_name' => $title,
 855+ 'rt_description' => $description,
 856+ );
 857+ }
 858+ $dbw->insert( 'collabwatchlisttag', $rows, __METHOD__, 'IGNORE' );
 859+ }
 860+
 861+ /**
 862+ * Remove a list of categories from a collaborative watchlist
 863+ *
 864+ * $titles is an array of strings, prefixed with '- ', if the
 865+ * category is subtracted
 866+ *
 867+ * @param $titles An array of strings
 868+ * @param $rlId The id of the collaborative watchlist
 869+ */
 870+ private function unwatchTitles( $titles, $rlId ) {
 871+ $dbw = wfGetDB( DB_MASTER );
 872+ foreach( $titles as $title ) {
 873+ $subtract = false;
 874+ $title = trim( $title );
 875+ $titleText = $title;
 876+ if( stripos($title, '- ') === 0 ) {
 877+ $subtract = true;
 878+ $titleText = trim( substr($title, 2) );
 879+ }
 880+ $title = Title::newFromText( $titleText );
 881+ if( $title instanceof Title ) {
 882+ $dbw->delete(
 883+ 'collabwatchlistcategory',
 884+ array(
 885+ 'rl_id' => $rlId,
 886+ 'cat_page_id' => $title->getArticleID(),
 887+ 'subtract' => $subtract,
 888+ ),
 889+ __METHOD__
 890+ );
 891+ $article = new Article($title);
 892+ //XXX Check if we can simply rename the hook, or if we need to register it
 893+ wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
 894+ }
 895+ }
 896+ }
 897+
 898+ /**
 899+ * Remove a list of users from a collaborative watchlist
 900+ *
 901+ * $titles is an array of strings, prefixed with the user type text and ' '
 902+ *
 903+ * @param $titles An array of strings
 904+ * @param $rlId The id of the collaborative watchlist
 905+ */
 906+ private function delUsers( $users, $rlId ) {
 907+ $dbw = wfGetDB( DB_MASTER );
 908+ foreach( $users as $userString ) {
 909+ list($type, $typeText, $titleText) = $this->extractTypeTypeTextAndUsername( $userString );
 910+ $user = User::newFromName($titleText);
 911+ if( $user instanceof User && $user->getId() !== 0) {
 912+ $dbw->delete(
 913+ 'collabwatchlistuser',
 914+ array(
 915+ 'rl_id' => $rlId,
 916+ 'user_id' => $user->getId(),
 917+ 'rlu_type' => $type,
 918+ ),
 919+ __METHOD__
 920+ );
 921+ }
 922+ }
 923+ //XXX Check if we can simply rename the hook, or if we need to register it
 924+ //wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
 925+ }
 926+
 927+ /**
 928+ * Remove a list of tags from a collaborative watchlist
 929+ *
 930+ * $titles is an array of strings
 931+ *
 932+ * @param $titles An array of strings
 933+ * @param $rlId The id of the collaborative watchlist
 934+ */
 935+ private function removeTags( $titles, $rlId ) {
 936+ $dbw = wfGetDB( DB_MASTER );
 937+ foreach( $titles as $title ) {
 938+ $dbw->delete(
 939+ 'collabwatchlisttag',
 940+ array(
 941+ 'rl_id' => $rlId,
 942+ 'rt_name' => $title,
 943+ ),
 944+ __METHOD__
 945+ );
 946+ //$article = new Article($title);
 947+ //XXX Check if we can simply rename the hook, or if we need to register it
 948+ //wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
 949+ }
 950+ }
 951+
 952+ /**
 953+ * Show the standard collaborative watchlist editing form
 954+ *
 955+ * @param $output OutputPage
 956+ * @param $rlId Collaborative watchlist id
 957+ */
 958+ private function showNormalForm( $output, $rlId ) {
 959+ global $wgUser;
 960+ if( ( $count = $this->showItemCount( $output, $rlId ) ) > 0 ) {
 961+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 962+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 963+ 'action' => $self->getLocalUrl( array( 'action' => 'edit' ) ) ) );
 964+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 965+ $form .= Html::hidden( 'collabwatchlist', $rlId );
 966+ $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistedit-normal-legend' ) . "</legend>";
 967+ $form .= wfMsgExt( 'collabwatchlistedit-normal-explain', 'parse' );
 968+ $form .= $this->buildRemoveList( $rlId, $wgUser->getSkin() );
 969+ $form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-normal-submit' ) ) . '</p>';
 970+ $form .= '</fieldset></form>';
 971+ $output->addHTML( $form );
 972+ }
 973+ }
 974+
 975+ private function showNewListForm( $output ) {
 976+ global $wgUser;
 977+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 978+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 979+ 'action' => $self->getLocalUrl( array( 'action' => 'newList' ) ) ) );
 980+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 981+ $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistnew-legend' ) . "</legend>";
 982+ $form .= wfMsgExt( 'collabwatchlistnew-explain', 'parse' );
 983+ $form .= Xml::label( wfMsg('collabwatchlistnew-name'), 'listname' ) . '&nbsp;' . Xml::input( 'listname' ) . '&nbsp;';
 984+ $form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistnew-submit' ) ) . '</p>';
 985+ $form .= '</fieldset></form>';
 986+ $output->addHTML( $form );
 987+ }
 988+
 989+ private function showDeleteListForm( $output, $rlId ) {
 990+ global $wgUser;
 991+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 992+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 993+ 'action' => $self->getLocalUrl( array( 'action' => 'delete' ) ) ) );
 994+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 995+ $form .= Html::hidden( 'collabwatchlist', $rlId );
 996+ $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'collabwatchlistdelete-legend' ) . "</legend>";
 997+ $form .= wfMsgExt( 'collabwatchlistdelete-explain', 'parse' );
 998+ $this->showUserItemCount($output, $rlId);
 999+ $this->showSetTagsItemCount($output, $rlId);
 1000+ $form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistdelete-submit' ) ) . '</p>';
 1001+ $form .= '</fieldset></form>';
 1002+ $output->addHTML( $form );
 1003+ }
 1004+
 1005+ private function createNewList($name) {
 1006+ global $wgUser;
 1007+ if( !isset($name) || empty($name) )
 1008+ return;
 1009+ $dbw = wfGetDB( DB_MASTER );
 1010+ $dbw->begin();
 1011+ try {
 1012+ $rl_id = $dbw->nextSequenceValue( 'collabwatchlist_rl_id_seq' );
 1013+ $dbw->insert( 'collabwatchlist', array(
 1014+ 'rl_id' => $rl_id,
 1015+ 'rl_name' => $name,
 1016+ 'rl_start' => wfTimestamp(TS_ISO_8601),
 1017+ ), __METHOD__, 'IGNORE' );
 1018+
 1019+ $affected = $dbw->affectedRows();
 1020+ if( $affected ) {
 1021+ $newid = $dbw->insertId();
 1022+ }else {
 1023+ return;
 1024+ }
 1025+ $rlu_id = $dbw->nextSequenceValue( 'collabwatchlistuser_rlu_id_seq' );
 1026+ $dbw->insert( 'collabwatchlistuser', array(
 1027+ 'rlu_id' => $rlu_id,
 1028+ 'rl_id' => $newid,
 1029+ 'user_id' => $wgUser->getId(),
 1030+ 'rlu_type' => COLLABWATCHLISTUSER_OWNER,
 1031+ ), __METHOD__, 'IGNORE' );
 1032+ $affected = $dbw->affectedRows();
 1033+ if( ! $affected ) {
 1034+ $dbw->rollback();
 1035+ return;
 1036+ }
 1037+ $dbw->commit();
 1038+ return $newid;
 1039+ }catch(Exception $e) {
 1040+ $dbw->rollback();
 1041+ }
 1042+ }
 1043+
 1044+ private function deleteList($rlId) {
 1045+ if( !isset($rlId) || empty($rlId) )
 1046+ return;
 1047+ $dbw = wfGetDB( DB_MASTER );
 1048+ $dbw->begin();
 1049+ try {
 1050+ $dbw->delete( 'collabwatchlistrevisiontag', array(
 1051+ 'rl_id' => $rlId,
 1052+ ), __METHOD__ );
 1053+ $dbw->delete( 'collabwatchlisttag', array(
 1054+ 'rl_id' => $rlId,
 1055+ ), __METHOD__ );
 1056+ $dbw->delete( 'collabwatchlistcategory', array(
 1057+ 'rl_id' => $rlId,
 1058+ ), __METHOD__ );
 1059+ $dbw->delete( 'collabwatchlistuser', array(
 1060+ 'rl_id' => $rlId,
 1061+ ), __METHOD__ );
 1062+ $dbw->delete( 'collabwatchlist', array(
 1063+ 'rl_id' => $rlId,
 1064+ ), __METHOD__ );
 1065+ $affected = $dbw->affectedRows();
 1066+ if( ! $affected ) {
 1067+ $dbw->rollback();
 1068+ return;
 1069+ }
 1070+ $dbw->commit();
 1071+ return $rlId;
 1072+ }catch(Exception $e) {
 1073+ $dbw->rollback();
 1074+ }
 1075+ }
 1076+
 1077+ /**
 1078+ * Build the part of the standard collaborative watchlist editing form with the actual
 1079+ * title selection checkboxes and stuff. Also generates a table of
 1080+ * contents if there's more than one heading.
 1081+ *
 1082+ * @param $rlId The id of the collaborative watchlist
 1083+ * @param $skin Skin (really, Linker)
 1084+ */
 1085+ private function buildRemoveList( $rlId, $skin ) {
 1086+ $list = "";
 1087+ $toc = $skin->tocIndent();
 1088+ $tocLength = 0;
 1089+ foreach( $this->getWatchlistInfo( $rlId ) as $namespace => $pages ) {
 1090+ $tocLength++;
 1091+ $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
 1092+ $anchor = "editwatchlist-ns" . $namespace;
 1093+
 1094+ $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
 1095+ $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
 1096+
 1097+ $list .= "<ul>\n";
 1098+ foreach( $pages as $dbkey => $info ) {
 1099+ $title = Title::makeTitleSafe( $namespace, $dbkey );
 1100+ $list .= $this->buildRemoveLine( $title, $info, $skin );
 1101+ }
 1102+ $list .= "</ul>\n";
 1103+ }
 1104+ // ISSUE: omit the TOC if the total number of titles is low?
 1105+ if( $tocLength > 1 ) {
 1106+ $list = $skin->tocList( $toc ) . $list;
 1107+ }
 1108+ return $list;
 1109+ }
 1110+
 1111+ /**
 1112+ * Get the correct "heading" for a namespace
 1113+ *
 1114+ * @param $namespace int
 1115+ * @return string
 1116+ */
 1117+ private function getNamespaceHeading( $namespace ) {
 1118+ return $namespace == NS_MAIN
 1119+ ? wfMsgHtml( 'blanknamespace' )
 1120+ : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
 1121+ }
 1122+
 1123+ /**
 1124+ * Build a single list item containing a check box selecting a title
 1125+ * and a link to that title, with various additional bits
 1126+ *
 1127+ * @param $title Title
 1128+ * @param $info array with info about the category ('redirect' and 'subtract' keys)
 1129+ * @param $skin Skin
 1130+ * @return string
 1131+ */
 1132+ private function buildRemoveLine( $title, $catInfo, $skin ) {
 1133+ global $wgLang;
 1134+
 1135+ $link = $skin->link( $title );
 1136+ if( $catInfo['redirect'] )
 1137+ $link = '<span class="watchlistredir">' . $link . '</span>';
 1138+ $tools[] = $skin->link( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
 1139+ if( $title->exists() ) {
 1140+ $tools[] = $skin->link(
 1141+ $title,
 1142+ wfMsgHtml( 'history_short' ),
 1143+ array(),
 1144+ array( 'action' => 'history' ),
 1145+ array( 'known', 'noclasses' )
 1146+ );
 1147+ }
 1148+ if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
 1149+ $tools[] = $skin->link(
 1150+ SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
 1151+ wfMsgHtml( 'contributions' ),
 1152+ array(),
 1153+ array(),
 1154+ array( 'known', 'noclasses' )
 1155+ );
 1156+ }
 1157+ return "<li>"
 1158+ . ($catInfo['subtract'] ? '<span class="collabwatchlistsubtract">- </span>' : '')
 1159+ . Xml::check( 'titles[]', false, array( 'value' => $catInfo['subtract'] ? '- ' . $title->getPrefixedText() : $title->getPrefixedText() ) )
 1160+ . $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n";
 1161+ }
 1162+
 1163+ /**
 1164+ * Show a form for editing the watchlist in "raw" mode
 1165+ *
 1166+ * @param $output OutputPage
 1167+ * @param $rlId Collaborative watchlist id
 1168+ * @param $rlName Collaborative watchlist name
 1169+ */
 1170+ private function showRawForm( $output, $rlId, $rlName ) {
 1171+ global $wgUser;
 1172+ $this->showItemCount( $output, $rlId );
 1173+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 1174+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 1175+ 'action' => $self->getLocalUrl( array( 'action' => 'rawCategories' ) ) ) );
 1176+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 1177+ $form .= Html::hidden( 'collabwatchlist', $rlId );
 1178+ $form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
 1179+ $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
 1180+ $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
 1181+ $form .= "<br />\n";
 1182+ $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
 1183+ 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
 1184+ $categories = $this->getCollabWatchlistCategories( $rlId );
 1185+ foreach( $categories as $category )
 1186+ $form .= htmlspecialchars( $category ) . "\n";
 1187+ $form .= '</textarea>';
 1188+ $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
 1189+ $form .= '</fieldset></form>';
 1190+ $output->addHTML( $form );
 1191+ }
 1192+
 1193+ /**
 1194+ * Show a form for editing the tags of a collaborative watchlist in "raw" mode
 1195+ *
 1196+ * @param $output OutputPage
 1197+ * @param $rlId Collaborative watchlist id
 1198+ * @param $rlName Collaborative watchlist name
 1199+ */
 1200+ private function showTagsRawForm( $output, $rlId, $rlName ) {
 1201+ global $wgUser;
 1202+ $this->showTagItemCount( $output, $rlId );
 1203+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 1204+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 1205+ 'action' => $self->getLocalUrl( array( 'action' => 'rawTags' ) ) ) );
 1206+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 1207+ $form .= Html::hidden( 'collabwatchlist', $rlId );
 1208+ $form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'collabwatchlistedit-tags-raw-legend' ) . '</legend>';
 1209+ $form .= wfMsgExt( 'collabwatchlistedit-tags-raw-explain', 'parse' );
 1210+ $form .= Xml::label( wfMsg( 'collabwatchlistedit-tags-raw-titles' ), 'titles' );
 1211+ $form .= "<br />\n";
 1212+ $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
 1213+ 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
 1214+ $tags = $this->getCollabWatchlistTags( $rlId );
 1215+ foreach( $tags as $tag => $description )
 1216+ $form .= htmlspecialchars( $tag ) . "|" . $description . "\n";
 1217+ $form .= '</textarea>';
 1218+ $form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-tags-raw-submit' ) ) . '</p>';
 1219+ $form .= '</fieldset></form>';
 1220+ $output->addHTML( $form );
 1221+ }
 1222+
 1223+ /**
 1224+ * Show a form for editing the users of a collaborative watchlist in "raw" mode
 1225+ *
 1226+ * @param $output OutputPage
 1227+ * @param $rlId Collaborative watchlist id
 1228+ * @param $rlName Collaborative watchlist name
 1229+ */
 1230+ private function showUsersRawForm( $output, $rlId, $rlName ) {
 1231+ global $wgUser;
 1232+ $this->showUserItemCount( $output, $rlId );
 1233+ $self = SpecialPage::getTitleFor( 'CollabWatchlist' );
 1234+ $form = Xml::openElement( 'form', array( 'method' => 'post',
 1235+ 'action' => $self->getLocalUrl( array( 'action' => 'rawUsers' ) ) ) );
 1236+ $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
 1237+ $form .= Html::hidden( 'collabwatchlist', $rlId );
 1238+ $form .= '<fieldset><legend>' . $rlName . ' ' . wfMsgHtml( 'collabwatchlistedit-users-raw-legend' ) . '</legend>';
 1239+ $form .= wfMsgExt( 'collabwatchlistedit-users-raw-explain', 'parse' );
 1240+ $form .= Xml::label( wfMsg( 'collabwatchlistedit-users-raw-titles' ), 'titles' );
 1241+ $form .= "<br />\n";
 1242+ $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
 1243+ 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
 1244+ $users = $this->getCollabWatchlistUsers( $rlId );
 1245+ foreach( $users as $userString )
 1246+ $form .= htmlspecialchars( $userString ) . "\n";
 1247+ $form .= '</textarea>';
 1248+ $form .= '<p>' . Xml::submitButton( wfMsg( 'collabwatchlistedit-users-raw-submit' ) ) . '</p>';
 1249+ $form .= '</fieldset></form>';
 1250+ $output->addHTML( $form );
 1251+ }
 1252+
 1253+ /**
 1254+ * Determine whether we are editing the watchlist, and if so, what
 1255+ * kind of editing operation
 1256+ *
 1257+ * @param $request WebRequest
 1258+ * @param $par mixed
 1259+ * @return int
 1260+ */
 1261+ public static function getMode( $request, $par ) {
 1262+ $mode = strtolower( $request->getVal( 'action', $par ) );
 1263+ switch( $mode ) {
 1264+ case 'clear':
 1265+ return self::EDIT_CLEAR;
 1266+ case 'rawcategories':
 1267+ return self::CATEGORIES_EDIT_RAW;
 1268+ case 'rawtags':
 1269+ return self::TAGS_EDIT_RAW;
 1270+ case 'edit':
 1271+ return self::EDIT_NORMAL;
 1272+ case 'settags':
 1273+ return self::SET_TAGS;
 1274+ case 'unsettags':
 1275+ return self::UNSET_TAGS;
 1276+ case 'rawusers':
 1277+ return self::USERS_EDIT_RAW;
 1278+ case 'newlist':
 1279+ return self::NEW_LIST;
 1280+ case 'delete':
 1281+ return self::DELETE_LIST;
 1282+ default:
 1283+ return false;
 1284+ }
 1285+ }
 1286+
 1287+ /**
 1288+ * Build a set of links for convenient navigation
 1289+ * between collaborative watchlist viewing and editing modes
 1290+ *
 1291+ * @param $listIdsAndNames An array mapping from list ids to list names
 1292+ * @param $skin Skin to use
 1293+ * @return string
 1294+ */
 1295+ public static function buildTools( $listIdsAndNames, $skin ) {
 1296+ global $wgLang, $wgUser;
 1297+ $modes = array( 'view' => false, 'delete' => 'delete', 'edit' => 'edit',
 1298+ 'rawCategories' => 'rawCategories', 'rawTags' => 'rawTags',
 1299+ 'rawUsers' => 'rawUsers' );
 1300+ $r = '';
 1301+ // Insert link for new list
 1302+ $r .= $skin->link(
 1303+ SpecialPage::getTitleFor( 'CollabWatchlist', 'newList' ),
 1304+ wfMsgHtml( "collabwatchlisttools-newList" ),
 1305+ array(),
 1306+ array(),
 1307+ array( 'known', 'noclasses' )
 1308+ ) . '<br />';
 1309+ if( !isset($listIdsAndNames) || empty($listIdsAndNames) )
 1310+ return $r;
 1311+ foreach( $listIdsAndNames as $listId => $listName) {
 1312+ $tools = array();
 1313+ foreach( $modes as $mode => $subpage ) {
 1314+ // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
 1315+ $tools[] = $skin->link(
 1316+ SpecialPage::getTitleFor( 'CollabWatchlist', $subpage ),
 1317+ wfMsgHtml( "collabwatchlisttools-{$mode}" ),
 1318+ array(),
 1319+ array( 'collabwatchlist' => $listId ),
 1320+ array( 'known', 'noclasses' )
 1321+ );
 1322+ }
 1323+ $r .= $listName . ' ' . $wgLang->pipeList( $tools ) . '<br />';
 1324+ }
 1325+ return $r;
 1326+ }
 1327+
 1328+ /** Returns a URL for unsetting a specific tag on a specific edit on a given list
 1329+ *
 1330+ * @param String $redirUrl The url to redirect after the tag was removed
 1331+ * @param String $pageName The name of the page the tag is set on
 1332+ * @param int $rlId The id of the collab watchlist
 1333+ * @param String $tag The tag to remove
 1334+ * @param int $rcId The id of the edit in the recent changes
 1335+ * @return String an URL string
 1336+ */
 1337+ public static function getUnsetTagUrl( $redirUrl, $pageName, $rlId, $tag, $rcId ) {
 1338+ return SpecialPage::getTitleFor( 'CollabWatchlist' )->getLocalUrl(array(
 1339+ 'action' => 'unsetTags',
 1340+ 'redirTarget' => $redirUrl,
 1341+ 'collabwatchlisttag' => $tag,
 1342+ 'collabwatchlist' => $rlId,
 1343+ 'collabwatchlistpage' => $pageName,
 1344+ 'collabwatchlistrcid' => $rcId
 1345+ ));
 1346+ }
 1347+}
Index: trunk/extensions/CollabWatchlist/mediawiki_core.patch
@@ -0,0 +1,144 @@
 2+Index: includes/ChangesList.php
 3+===================================================================
 4+--- includes/ChangesList.php (revision 90542)
 5+@@ -801,6 +801,45 @@
 6+ return $ret;
 7+ }
 8+
 9++ protected function insertDiffAndHistLinks( &$s, &$rc ) {
 10++ $s .= ' ('. $rc->difflink;
 11++ $this->insertHistLink($s, $rc, $rc->getTitle());
 12++ $s .= ')';
 13++ }
 14++
 15++ protected function insertHistLink( &$s, &$rc, $title, $params = array(), $sep = NULL ) {
 16++ $params['action'] = 'history';
 17++ $s .= isset($sep) ? $sep : $this->message['pipe-separator'] .
 18++ $this->skin->link(
 19++ $title,
 20++ $this->message['hist'],
 21++ array(),
 22++ $params,
 23++ array( 'known', 'noclasses' )
 24++ );
 25++ }
 26++
 27++ protected function insertBeforeRCFlags( &$r, &$rcObj ) {
 28++
 29++ }
 30++
 31++ protected function insertBeforeRCFlagsBlock( &$r, &$block ) {
 32++
 33++ }
 34++
 35++ protected function insertCurrAndLastLinks( &$s, &$rc ) {
 36++ $s .= ' (';
 37++ $s .= $rc->curlink;
 38++ $s .= $this->message['pipe-separator'];
 39++ $s .= $rc->lastlink;
 40++ $s .= ')';
 41++ }
 42++
 43++ protected function insertUserAndTalkLinks( &$s, &$rc ) {
 44++ $s .= $rc->userlink;
 45++ $s .= $rc->usertalklink;
 46++ }
 47++
 48+ /**
 49+ * Enhanced RC group
 50+ */
 51+@@ -888,7 +927,7 @@
 52+ . "<a href='#' title='$closeTitle'>{$this->downArrow()}</a>"
 53+ . "</span></span>";
 54+ $r .= "<td>$tl</td>";
 55+-
 56++ $this->insertBeforeRCFlagsBlock($r, $block);
 57+ # Main line
 58+ $r .= '<td class="mw-enhanced-rc">' . $this->recentChangesFlags( array(
 59+ 'newpage' => $isnew,
 60+@@ -948,16 +987,9 @@
 61+ $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')';
 62+ } else {
 63+ $params = $queryParams;
 64+- $params['action'] = 'history';
 65+
 66+- $r .= $this->message['pipe-separator'] .
 67+- $this->skin->link(
 68+- $block[0]->getTitle(),
 69+- $this->message['hist'],
 70+- array(),
 71+- $params,
 72+- array( 'known', 'noclasses' )
 73+- ) . ')';
 74++ $this->insertHistLink($r, $rcObj, $block[0]->getTitle(), $params);
 75++ $r .= ')';
 76+ }
 77+ $r .= ' . . ';
 78+
 79+@@ -994,6 +1026,7 @@
 80+
 81+ #$r .= '<tr><td valign="top">'.$this->spacerArrow();
 82+ $r .= '<tr><td></td><td class="mw-enhanced-rc">';
 83++ $this->insertBeforeRCFlags( $r, $rcObj );
 84+ $r .= $this->recentChangesFlags( array(
 85+ 'newpage' => $rcObj->mAttribs['rc_new'],
 86+ 'minor' => $rcObj->mAttribs['rc_minor'],
 87+@@ -1032,11 +1065,7 @@
 88+ $r .= $link . '</span>';
 89+
 90+ if ( !$type == RC_LOG || $type == RC_NEW ) {
 91+- $r .= ' (';
 92+- $r .= $rcObj->curlink;
 93+- $r .= $this->message['pipe-separator'];
 94+- $r .= $rcObj->lastlink;
 95+- $r .= ')';
 96++ $this->insertCurrAndLastLinks( $r, $rcObj );
 97+ }
 98+ $r .= ' . . ';
 99+
 100+@@ -1046,8 +1075,7 @@
 101+ }
 102+
 103+ # User links
 104+- $r .= $rcObj->userlink;
 105+- $r .= $rcObj->usertalklink;
 106++ $this->insertUserAndTalkLinks( $r, $rcObj );
 107+ // log action
 108+ $this->insertAction( $r, $rcObj );
 109+ // log comment
 110+@@ -1135,6 +1163,7 @@
 111+ Html::openElement( 'tr' );
 112+
 113+ $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow();
 114++ $this->insertBeforeRCFlags( $r, $rcObj );
 115+ # Flag and Timestamp
 116+ if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) {
 117+ $r .= '&#160;&#160;&#160;&#160;'; // 4 flags -> 4 spaces
 118+@@ -1163,15 +1192,7 @@
 119+ }
 120+ # Diff and hist links
 121+ if ( $type != RC_LOG ) {
 122+- $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator'];
 123+- $query['action'] = 'history';
 124+- $r .= $this->skin->link(
 125+- $rcObj->getTitle(),
 126+- $this->message['hist'],
 127+- array(),
 128+- $query,
 129+- array( 'known', 'noclasses' )
 130+- ) . ')';
 131++ $this->insertDiffAndHistLinks( $r, $rcObj);
 132+ }
 133+ $r .= ' . . ';
 134+ # Character diff
 135+@@ -1179,7 +1200,8 @@
 136+ $r .= "$cd . . ";
 137+ }
 138+ # User/talk
 139+- $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
 140++ $r .= ' ';
 141++ $this->insertUserAndTalkLinks($r, $rcObj);
 142+ # Log action (if any)
 143+ if( $logType ) {
 144+ if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) {
Index: trunk/extensions/CollabWatchlist/js/CollabWatchlist.js
@@ -0,0 +1,17 @@
 2+function onCollabWatchlistSelection(tagIdBasename, listId) {
 3+ // We have multiple <select> tags, one which contains all tags,
 4+ // one for each collaborative watchlist and an empty one. Upon selection
 5+ // of a collaborative watchlist we swap the select tags.
 6+ var allTags = document.getElementById(tagIdBasename);
 7+ var elem = document.getElementById(tagIdBasename + '-' + listId);
 8+ if(elem == null) {
 9+ elem = document.getElementById(tagIdBasename + '-empty');
 10+ if(elem == null)
 11+ return;
 12+ }
 13+ var clonedElem = elem.cloneNode(true);
 14+ clonedElem.setAttribute('id', tagIdBasename);
 15+ clonedElem.setAttribute('name', allTags.getAttribute('name'));
 16+ clonedElem.style.display = 'inline';
 17+ allTags.parentNode.replaceChild(clonedElem, allTags);
 18+}
\ No newline at end of file
Index: trunk/extensions/CollabWatchlist/CollabWatchlist.alias.php
@@ -0,0 +1,22 @@
 2+<?php
 3+/**
 4+ * Aliases for special pages for extension CollabWatchlist
 5+ *
 6+ * @addtogroup Extensions
 7+ */
 8+
 9+$aliases = array();
 10+
 11+/** English
 12+ * @author Aaron Schulz
 13+ */
 14+$aliases['en'] = array(
 15+ 'Collabwatchlist' => array( 'Collaborative watchlist' ),
 16+);
 17+
 18+/** German (Deutsch)
 19+ * @author Raimond Spekking
 20+ */
 21+$aliases['de'] = array(
 22+ 'Collabwatchlist' => array( 'Kollaborative Beobachtungsliste' ),
 23+);

Sign-offs

UserFlagDate
Nikerabbitinspected11:11, 29 June 2011

Follow-up revisions

RevisionCommit summaryAuthorDate
r91075svn:eol-style native for r91063reedy15:18, 29 June 2011
r93183* Followup r91063: Tweak extensions credit and message file for consistency....raymond14:28, 26 July 2011

Comments

#Comment by P858snake (talk | contribs)   10:16, 29 June 2011
#Comment by Nikerabbit (talk | contribs)   11:11, 29 June 2011

The code looks good overall. Some small notes:

  • Coding conventions recommend writing null in lowercase
  • It would of course be cool if core code would not need to be patched
  • $wgUser->getSkin() has been recently deprecated, but can still be used to achieve BC with older MW versions
  • In wlCountItems $db->selectField might be a convenient shortcut
  • while( $row = $res->fetchObject() ) can usually be replaced with foreach ( $res as row )
  • In wlGetFilterClauseListUser it should be possible to use more database functions like ->makeList, or 'field!' => array() syntax supported in trunk
  • &nbsp; should be replaced with &#160;, although Xml::inputLabel and such might actually suit better in some cases
  • Maybe Exceptions should be replaced with MWExceptions?


What is the intention of this code:

+		$title = Title::newFromText( $wgContLang->specialPage( $page ) );

If I understand it correctly you can use SpecialPage::getTitleFor( $page ) instead.

#Comment by MarkAHershberger (talk | contribs)   03:58, 7 November 2011
+	public static function newFromUser( &$user ) {

Gives "Strict standards: Declaration of CollabWatchlistChangesList::newFromUser() should be compatible with that of ChangesList::newFromUser()"

Status & tagging log