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') . ': '; |
| 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) . ' ' . Xml::label( $tag, 'collaborative-watchlist-filtertag-' . $tag ) . ' '; |
| 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' )) . ' '; |
| 335 | + $form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . ' '; |
| 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' )) . ' '; |
| 47 | + $result .= Xml::label( wfMsg('collabwatchlisttagcomment'), 'tagcomment' ) . ' ' . Xml::input( 'tagcomment' ) . ' '; |
| 48 | + if( $this->user->useRCPatrol() ) |
| 49 | + $result .= Xml::checkLabel( wfMsg('collabwatchlistpatrol'), 'setpatrolled', 'setpatrolled', true ) . ' '; |
| 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 .= ' ' . $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 .= ' '; |
| 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 ) . ' ' . $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 ) . ' ' . $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' ) . ' ' . Xml::input( 'listname' ) . ' '; |
| 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 .= '    '; // 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 | +); |