]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/WatchlistEditor.php
MediaWiki 1.15.4-scripts
[autoinstalls/mediawiki.git] / includes / WatchlistEditor.php
1 <?php
2
3 /**
4  * Provides the UI through which users can perform editing
5  * operations on their watchlist
6  *
7  * @ingroup Watchlist
8  * @author Rob Church <robchur@gmail.com>
9  */
10 class WatchlistEditor {
11
12         /**
13          * Editing modes
14          */
15         const EDIT_CLEAR = 1;
16         const EDIT_RAW = 2;
17         const EDIT_NORMAL = 3;
18
19         /**
20          * Main execution point
21          *
22          * @param $user User
23          * @param $output OutputPage
24          * @param $request WebRequest
25          * @param $mode int
26          */
27         public function execute( $user, $output, $request, $mode ) {
28                 global $wgUser;
29                 if( wfReadOnly() ) {
30                         $output->readOnlyPage();
31                         return;
32                 }
33                 switch( $mode ) {
34                         case self::EDIT_CLEAR:
35                                 // The "Clear" link scared people too much.
36                                 // Pass on to the raw editor, from which it's very easy to clear.
37                         case self::EDIT_RAW:
38                                 $output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) );
39                                 if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
40                                         $wanted = $this->extractTitles( $request->getText( 'titles' ) );
41                                         $current = $this->getWatchlist( $user );
42                                         if( count( $wanted ) > 0 ) {
43                                                 $toWatch = array_diff( $wanted, $current );
44                                                 $toUnwatch = array_diff( $current, $wanted );
45                                                 $this->watchTitles( $toWatch, $user );
46                                                 $this->unwatchTitles( $toUnwatch, $user );
47                                                 $user->invalidateCache();
48                                                 if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
49                                                         $output->addHTML( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) );
50                                                 if( ( $count = count( $toWatch ) ) > 0 ) {
51                                                         $output->addHTML( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) );
52                                                         $this->showTitles( $toWatch, $output, $wgUser->getSkin() );
53                                                 }
54                                                 if( ( $count = count( $toUnwatch ) ) > 0 ) {
55                                                         $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) );
56                                                         $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() );
57                                                 }
58                                         } else {
59                                                 $this->clearWatchlist( $user );
60                                                 $user->invalidateCache();
61                                                 $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) );
62                                                 $this->showTitles( $current, $output, $wgUser->getSkin() );
63                                         }
64                                 }
65                                 $this->showRawForm( $output, $user );
66                                 break;
67                         case self::EDIT_NORMAL:
68                                 $output->setPageTitle( wfMsg( 'watchlistedit-normal-title' ) );
69                                 if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
70                                         $titles = $this->extractTitles( $request->getArray( 'titles' ) );
71                                         $this->unwatchTitles( $titles, $user );
72                                         $user->invalidateCache();
73                                         $output->addHTML( wfMsgExt( 'watchlistedit-normal-done', 'parse',
74                                                 $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
75                                         $this->showTitles( $titles, $output, $wgUser->getSkin() );
76                                 }
77                                 $this->showNormalForm( $output, $user );
78                 }
79         }
80
81         /**
82          * Check the edit token from a form submission
83          *
84          * @param $request WebRequest
85          * @param $user User
86          * @return bool
87          */
88         private function checkToken( $request, $user ) {
89                 return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' );
90         }
91
92         /**
93          * Extract a list of titles from a blob of text, returning
94          * (prefixed) strings; unwatchable titles are ignored
95          *
96          * @param $list mixed
97          * @return array
98          */
99         private function extractTitles( $list ) {
100                 $titles = array();
101                 if( !is_array( $list ) ) {
102                         $list = explode( "\n", trim( $list ) );
103                         if( !is_array( $list ) )
104                                 return array();
105                 }
106                 foreach( $list as $text ) {
107                         $text = trim( $text );
108                         if( strlen( $text ) > 0 ) {
109                                 $title = Title::newFromText( $text );
110                                 if( $title instanceof Title && $title->isWatchable() )
111                                         $titles[] = $title->getPrefixedText();
112                         }
113                 }
114                 return array_unique( $titles );
115         }
116
117         /**
118          * Print out a list of linked titles
119          *
120          * $titles can be an array of strings or Title objects; the former
121          * is preferred, since Titles are very memory-heavy
122          *
123          * @param $titles An array of strings, or Title objects
124          * @param $output OutputPage
125          * @param $skin Skin
126          */
127         private function showTitles( $titles, $output, $skin ) {
128                 $talk = wfMsgHtml( 'talkpagelinktext' );
129                 // Do a batch existence check
130                 $batch = new LinkBatch();
131                 foreach( $titles as $title ) {
132                         if( !$title instanceof Title )
133                                 $title = Title::newFromText( $title );
134                         if( $title instanceof Title ) {
135                                 $batch->addObj( $title );
136                                 $batch->addObj( $title->getTalkPage() );
137                         }
138                 }
139                 $batch->execute();
140                 // Print out the list
141                 $output->addHTML( "<ul>\n" );
142                 foreach( $titles as $title ) {
143                         if( !$title instanceof Title )
144                                 $title = Title::newFromText( $title );
145                         if( $title instanceof Title ) {
146                                 $output->addHTML( "<li>" . $skin->makeLinkObj( $title )
147                                 . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" );
148                         }
149                 }
150                 $output->addHTML( "</ul>\n" );
151         }
152
153         /**
154          * Count the number of titles on a user's watchlist, excluding talk pages
155          *
156          * @param $user User
157          * @return int
158          */
159         private function countWatchlist( $user ) {
160                 $dbr = wfGetDB( DB_MASTER );
161                 $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ );
162                 $row = $dbr->fetchObject( $res );
163                 return ceil( $row->count / 2 ); // Paranoia
164         }
165
166         /**
167          * Prepare a list of titles on a user's watchlist (excluding talk pages)
168          * and return an array of (prefixed) strings
169          *
170          * @param $user User
171          * @return array
172          */
173         private function getWatchlist( $user ) {
174                 $list = array();
175                 $dbr = wfGetDB( DB_MASTER );
176                 $res = $dbr->select(
177                         'watchlist',
178                         '*',
179                         array(
180                                 'wl_user' => $user->getId(),
181                         ),
182                         __METHOD__
183                 );
184                 if( $res->numRows() > 0 ) {
185                         while( $row = $res->fetchObject() ) {
186                                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
187                                 if( $title instanceof Title && !$title->isTalkPage() )
188                                         $list[] = $title->getPrefixedText();
189                         }
190                         $res->free();
191                 }
192                 return $list;
193         }
194
195         /**
196          * Get a list of titles on a user's watchlist, excluding talk pages,
197          * and return as a two-dimensional array with namespace, title and
198          * redirect status
199          *
200          * @param $user User
201          * @return array
202          */
203         private function getWatchlistInfo( $user ) {
204                 $titles = array();
205                 $dbr = wfGetDB( DB_MASTER );
206                 $uid = intval( $user->getId() );
207                 list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' );
208                 $sql = "SELECT wl_namespace, wl_title, page_id, page_len, page_is_redirect
209                         FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace
210                         AND wl_title = page_title ) WHERE wl_user = {$uid}";
211                 $res = $dbr->query( $sql, __METHOD__ );
212                 if( $res && $dbr->numRows( $res ) > 0 ) {
213                         $cache = LinkCache::singleton();
214                         while( $row = $dbr->fetchObject( $res ) ) {
215                                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
216                                 if( $title instanceof Title ) {
217                                         // Update the link cache while we're at it
218                                         if( $row->page_id ) {
219                                                 $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
220                                         } else {
221                                                 $cache->addBadLinkObj( $title );
222                                         }
223                                         // Ignore non-talk
224                                         if( !$title->isTalkPage() )
225                                                 $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect;
226                                 }
227                         }
228                 }
229                 return $titles;
230         }
231
232         /**
233          * Show a message indicating the number of items on the user's watchlist,
234          * and return this count for additional checking
235          *
236          * @param $output OutputPage
237          * @param $user User
238          * @return int
239          */
240         private function showItemCount( $output, $user ) {
241                 if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
242                         $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse',
243                                 $GLOBALS['wgLang']->formatNum( $count ) ) );
244                 } else {
245                         $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
246                 }
247                 return $count;
248         }
249
250         /**
251          * Remove all titles from a user's watchlist
252          *
253          * @param $user User
254          */
255         private function clearWatchlist( $user ) {
256                 $dbw = wfGetDB( DB_MASTER );
257                 $dbw->delete( 'watchlist', array( 'wl_user' => $user->getId() ), __METHOD__ );
258         }
259
260         /**
261          * Add a list of titles to a user's watchlist
262          *
263          * $titles can be an array of strings or Title objects; the former
264          * is preferred, since Titles are very memory-heavy
265          *
266          * @param $titles An array of strings, or Title objects
267          * @param $user User
268          */
269         private function watchTitles( $titles, $user ) {
270                 $dbw = wfGetDB( DB_MASTER );
271                 $rows = array();
272                 foreach( $titles as $title ) {
273                         if( !$title instanceof Title )
274                                 $title = Title::newFromText( $title );
275                         if( $title instanceof Title ) {
276                                 $rows[] = array(
277                                         'wl_user' => $user->getId(),
278                                         'wl_namespace' => ( $title->getNamespace() & ~1 ),
279                                         'wl_title' => $title->getDBkey(),
280                                         'wl_notificationtimestamp' => null,
281                                 );
282                                 $rows[] = array(
283                                         'wl_user' => $user->getId(),
284                                         'wl_namespace' => ( $title->getNamespace() | 1 ),
285                                         'wl_title' => $title->getDBkey(),
286                                         'wl_notificationtimestamp' => null,
287                                 );
288                         }
289                 }
290                 $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
291         }
292
293         /**
294          * Remove a list of titles from a user's watchlist
295          *
296          * $titles can be an array of strings or Title objects; the former
297          * is preferred, since Titles are very memory-heavy
298          *
299          * @param $titles An array of strings, or Title objects
300          * @param $user User
301          */
302         private function unwatchTitles( $titles, $user ) {
303                 $dbw = wfGetDB( DB_MASTER );
304                 foreach( $titles as $title ) {
305                         if( !$title instanceof Title )
306                                 $title = Title::newFromText( $title );
307                         if( $title instanceof Title ) {
308                                 $dbw->delete(
309                                         'watchlist',
310                                         array(
311                                                 'wl_user' => $user->getId(),
312                                                 'wl_namespace' => ( $title->getNamespace() & ~1 ),
313                                                 'wl_title' => $title->getDBkey(),
314                                         ),
315                                         __METHOD__
316                                 );
317                                 $dbw->delete(
318                                         'watchlist',
319                                         array(
320                                                 'wl_user' => $user->getId(),
321                                                 'wl_namespace' => ( $title->getNamespace() | 1 ),
322                                                 'wl_title' => $title->getDBkey(),
323                                         ),
324                                         __METHOD__
325                                 );
326                                 $article = new Article($title);
327                                 wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
328                         }
329                 }
330         }
331
332         /**
333          * Show the standard watchlist editing form
334          *
335          * @param $output OutputPage
336          * @param $user User
337          */
338         private function showNormalForm( $output, $user ) {
339                 global $wgUser;
340                 if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
341                         $self = SpecialPage::getTitleFor( 'Watchlist' );
342                         $form  = Xml::openElement( 'form', array( 'method' => 'post',
343                                 'action' => $self->getLocalUrl( 'action=edit' ) ) );
344                         $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
345                         $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'watchlistedit-normal-legend' ) . "</legend>";
346                         $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' );
347                         $form .= $this->buildRemoveList( $user, $wgUser->getSkin() );
348                         $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>';
349                         $form .= '</fieldset></form>';
350                         $output->addHTML( $form );
351                 }
352         }
353
354         /**
355          * Build the part of the standard watchlist editing form with the actual
356          * title selection checkboxes and stuff.  Also generates a table of
357          * contents if there's more than one heading.
358          *
359          * @param $user User
360          * @param $skin Skin (really, Linker)
361          */
362         private function buildRemoveList( $user, $skin ) {
363                 $list = "";
364                 $toc = $skin->tocIndent();
365                 $tocLength = 0;
366                 foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) {
367                         $tocLength++;
368                         $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
369                         $anchor = "editwatchlist-ns" . $namespace;
370
371                         $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
372                         $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
373
374                         $list .= "<ul>\n";
375                         foreach( $pages as $dbkey => $redirect ) {
376                                 $title = Title::makeTitleSafe( $namespace, $dbkey );
377                                 $list .= $this->buildRemoveLine( $title, $redirect, $skin );
378                         }
379                         $list .= "</ul>\n";
380                 }
381                 // ISSUE: omit the TOC if the total number of titles is low?
382                 if( $tocLength > 1 ) {
383                         $list = $skin->tocList( $toc ) . $list;
384                 }
385                 return $list;
386         }
387
388         /**
389          * Get the correct "heading" for a namespace
390          *
391          * @param $namespace int
392          * @return string
393          */
394         private function getNamespaceHeading( $namespace ) {
395                 return $namespace == NS_MAIN
396                         ? wfMsgHtml( 'blanknamespace' )
397                         : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
398         }
399
400         /**
401          * Build a single list item containing a check box selecting a title
402          * and a link to that title, with various additional bits
403          *
404          * @param $title Title
405          * @param $redirect bool
406          * @param $skin Skin
407          * @return string
408          */
409         private function buildRemoveLine( $title, $redirect, $skin ) {
410                 global $wgLang;
411
412                 $link = $skin->makeLinkObj( $title );
413                 if( $redirect )
414                         $link = '<span class="watchlistredir">' . $link . '</span>';
415                 $tools[] = $skin->makeLinkObj( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
416                 if( $title->exists() ) {
417                         $tools[] = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'history_short' ), 'action=history' );
418                 }
419                 if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
420                         $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) );
421                 }
422                 return "<li>"
423                         . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
424                         . $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n";
425                 }
426
427         /**
428          * Show a form for editing the watchlist in "raw" mode
429          *
430          * @param $output OutputPage
431          * @param $user User
432          */
433         public function showRawForm( $output, $user ) {
434                 global $wgUser;
435                 $this->showItemCount( $output, $user );
436                 $self = SpecialPage::getTitleFor( 'Watchlist' );
437                 $form  = Xml::openElement( 'form', array( 'method' => 'post',
438                         'action' => $self->getLocalUrl( 'action=raw' ) ) );
439                 $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
440                 $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
441                 $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
442                 $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
443                 $form .= "<br />\n";
444                 $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
445                         'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
446                 $titles = $this->getWatchlist( $user );
447                 foreach( $titles as $title )
448                         $form .= htmlspecialchars( $title ) . "\n";
449                 $form .= '</textarea>';
450                 $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
451                 $form .= '</fieldset></form>';
452                 $output->addHTML( $form );
453         }
454
455         /**
456          * Determine whether we are editing the watchlist, and if so, what
457          * kind of editing operation
458          *
459          * @param $request WebRequest
460          * @param $par mixed
461          * @return int
462          */
463         public static function getMode( $request, $par ) {
464                 $mode = strtolower( $request->getVal( 'action', $par ) );
465                 switch( $mode ) {
466                         case 'clear':
467                                 return self::EDIT_CLEAR;
468                         case 'raw':
469                                 return self::EDIT_RAW;
470                         case 'edit':
471                                 return self::EDIT_NORMAL;
472                         default:
473                                 return false;
474                 }
475         }
476
477         /**
478          * Build a set of links for convenient navigation
479          * between watchlist viewing and editing modes
480          *
481          * @param $skin Skin to use
482          * @return string
483          */
484         public static function buildTools( $skin ) {
485                 global $wgLang;
486
487                 $tools = array();
488                 $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' );
489                 foreach( $modes as $mode => $subpage ) {
490                         $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) );
491                 }
492                 return $wgLang->pipeList( $tools );
493         }
494 }