]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/WatchlistEditor.php
MediaWiki 1.17.0
[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                 }
107                 foreach( $list as $text ) {
108                         $text = trim( $text );
109                         if( strlen( $text ) > 0 ) {
110                                 $title = Title::newFromText( $text );
111                                 if( $title instanceof Title && $title->isWatchable() ) {
112                                         $titles[] = $title->getPrefixedText();
113                                 }
114                         }
115                 }
116                 return array_unique( $titles );
117         }
118
119         /**
120          * Print out a list of linked titles
121          *
122          * $titles can be an array of strings or Title objects; the former
123          * is preferred, since Titles are very memory-heavy
124          *
125          * @param $titles An array of strings, or Title objects
126          * @param $output OutputPage
127          * @param $skin Skin
128          */
129         private function showTitles( $titles, $output, $skin ) {
130                 $talk = wfMsgHtml( 'talkpagelinktext' );
131                 // Do a batch existence check
132                 $batch = new LinkBatch();
133                 foreach( $titles as $title ) {
134                         if( !$title instanceof Title ) {
135                                 $title = Title::newFromText( $title );
136                         }
137                         if( $title instanceof Title ) {
138                                 $batch->addObj( $title );
139                                 $batch->addObj( $title->getTalkPage() );
140                         }
141                 }
142                 $batch->execute();
143                 // Print out the list
144                 $output->addHTML( "<ul>\n" );
145                 foreach( $titles as $title ) {
146                         if( !$title instanceof Title ) {
147                                 $title = Title::newFromText( $title );
148                         }
149                         if( $title instanceof Title ) {
150                                 $output->addHTML( "<li>" . $skin->link( $title )
151                                 . ' (' . $skin->link( $title->getTalkPage(), $talk ) . ")</li>\n" );
152                         }
153                 }
154                 $output->addHTML( "</ul>\n" );
155         }
156
157         /**
158          * Count the number of titles on a user's watchlist, excluding talk pages
159          *
160          * @param $user User
161          * @return int
162          */
163         private function countWatchlist( $user ) {
164                 $dbr = wfGetDB( DB_MASTER );
165                 $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ );
166                 $row = $dbr->fetchObject( $res );
167                 return ceil( $row->count / 2 ); // Paranoia
168         }
169
170         /**
171          * Prepare a list of titles on a user's watchlist (excluding talk pages)
172          * and return an array of (prefixed) strings
173          *
174          * @param $user User
175          * @return array
176          */
177         private function getWatchlist( $user ) {
178                 $list = array();
179                 $dbr = wfGetDB( DB_MASTER );
180                 $res = $dbr->select(
181                         'watchlist',
182                         '*',
183                         array(
184                                 'wl_user' => $user->getId(),
185                         ),
186                         __METHOD__
187                 );
188                 if( $res->numRows() > 0 ) {
189                         foreach ( $res as $row ) {
190                                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
191                                 if( $title instanceof Title && !$title->isTalkPage() )
192                                         $list[] = $title->getPrefixedText();
193                         }
194                         $res->free();
195                 }
196                 return $list;
197         }
198
199         /**
200          * Get a list of titles on a user's watchlist, excluding talk pages,
201          * and return as a two-dimensional array with namespace, title and
202          * redirect status
203          *
204          * @param $user User
205          * @return array
206          */
207         private function getWatchlistInfo( $user ) {
208                 $titles = array();
209                 $dbr = wfGetDB( DB_MASTER );
210                 $uid = intval( $user->getId() );
211                 list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' );
212                 $sql = "SELECT wl_namespace, wl_title, page_id, page_len, page_is_redirect, page_latest
213                         FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace
214                         AND wl_title = page_title ) WHERE wl_user = {$uid}";
215                 if ( ! $dbr->implicitOrderby() ) {
216                         $sql .= " ORDER BY wl_namespace, wl_title";
217                 }
218                 $res = $dbr->query( $sql, __METHOD__ );
219                 if( $res && $dbr->numRows( $res ) > 0 ) {
220                         $cache = LinkCache::singleton();
221                         foreach ( $res as $row ) {                      
222                                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
223                                 if( $title instanceof Title ) {
224                                         // Update the link cache while we're at it
225                                         if( $row->page_id ) {
226                                                 $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect, $row->page_latest );
227                                         } else {
228                                                 $cache->addBadLinkObj( $title );
229                                         }
230                                         // Ignore non-talk
231                                         if( !$title->isTalkPage() ) {
232                                                 $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect;
233                                         }
234                                 }
235                         }
236                 }
237                 return $titles;
238         }
239
240         /**
241          * Show a message indicating the number of items on the user's watchlist,
242          * and return this count for additional checking
243          *
244          * @param $output OutputPage
245          * @param $user User
246          * @return int
247          */
248         private function showItemCount( $output, $user ) {
249                 if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
250                         $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse',
251                                 $GLOBALS['wgLang']->formatNum( $count ) ) );
252                 } else {
253                         $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
254                 }
255                 return $count;
256         }
257
258         /**
259          * Remove all titles from a user's watchlist
260          *
261          * @param $user User
262          */
263         private function clearWatchlist( $user ) {
264                 $dbw = wfGetDB( DB_MASTER );
265                 $dbw->delete( 'watchlist', array( 'wl_user' => $user->getId() ), __METHOD__ );
266         }
267
268         /**
269          * Add a list of titles to a user's watchlist
270          *
271          * $titles can be an array of strings or Title objects; the former
272          * is preferred, since Titles are very memory-heavy
273          *
274          * @param $titles An array of strings, or Title objects
275          * @param $user User
276          */
277         private function watchTitles( $titles, $user ) {
278                 $dbw = wfGetDB( DB_MASTER );
279                 $rows = array();
280                 foreach( $titles as $title ) {
281                         if( !$title instanceof Title ) {
282                                 $title = Title::newFromText( $title );
283                         }
284                         if( $title instanceof Title ) {
285                                 $rows[] = array(
286                                         'wl_user' => $user->getId(),
287                                         'wl_namespace' => ( $title->getNamespace() & ~1 ),
288                                         'wl_title' => $title->getDBkey(),
289                                         'wl_notificationtimestamp' => null,
290                                 );
291                                 $rows[] = array(
292                                         'wl_user' => $user->getId(),
293                                         'wl_namespace' => ( $title->getNamespace() | 1 ),
294                                         'wl_title' => $title->getDBkey(),
295                                         'wl_notificationtimestamp' => null,
296                                 );
297                         }
298                 }
299                 $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
300         }
301
302         /**
303          * Remove a list of titles from a user's watchlist
304          *
305          * $titles can be an array of strings or Title objects; the former
306          * is preferred, since Titles are very memory-heavy
307          *
308          * @param $titles An array of strings, or Title objects
309          * @param $user User
310          */
311         private function unwatchTitles( $titles, $user ) {
312                 $dbw = wfGetDB( DB_MASTER );
313                 foreach( $titles as $title ) {
314                         if( !$title instanceof Title ) {
315                                 $title = Title::newFromText( $title );
316                         }
317                         if( $title instanceof Title ) {
318                                 $dbw->delete(
319                                         'watchlist',
320                                         array(
321                                                 'wl_user' => $user->getId(),
322                                                 'wl_namespace' => ( $title->getNamespace() & ~1 ),
323                                                 'wl_title' => $title->getDBkey(),
324                                         ),
325                                         __METHOD__
326                                 );
327                                 $dbw->delete(
328                                         'watchlist',
329                                         array(
330                                                 'wl_user' => $user->getId(),
331                                                 'wl_namespace' => ( $title->getNamespace() | 1 ),
332                                                 'wl_title' => $title->getDBkey(),
333                                         ),
334                                         __METHOD__
335                                 );
336                                 $article = new Article($title);
337                                 wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
338                         }
339                 }
340         }
341
342         /**
343          * Show the standard watchlist editing form
344          *
345          * @param $output OutputPage
346          * @param $user User
347          */
348         private function showNormalForm( $output, $user ) {
349                 global $wgUser;
350                 $count = $this->showItemCount( $output, $user );
351                 if( $count > 0 ) {
352                         $self = SpecialPage::getTitleFor( 'Watchlist' );
353                         $form  = Xml::openElement( 'form', array( 'method' => 'post',
354                                 'action' => $self->getLocalUrl( array( 'action' => 'edit' ) ) ) );
355                         $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
356                         $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'watchlistedit-normal-legend' ) . "</legend>";
357                         $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' );
358                         $form .= $this->buildRemoveList( $user, $wgUser->getSkin() );
359                         $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>';
360                         $form .= '</fieldset></form>';
361                         $output->addHTML( $form );
362                 }
363         }
364
365         /**
366          * Build the part of the standard watchlist editing form with the actual
367          * title selection checkboxes and stuff.  Also generates a table of
368          * contents if there's more than one heading.
369          *
370          * @param $user User
371          * @param $skin Skin (really, Linker)
372          */
373         private function buildRemoveList( $user, $skin ) {
374                 $list = "";
375                 $toc = $skin->tocIndent();
376                 $tocLength = 0;
377                 foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) {
378                         $tocLength++;
379                         $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
380                         $anchor = "editwatchlist-ns" . $namespace;
381
382                         $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
383                         $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
384
385                         $list .= "<ul>\n";
386                         foreach( $pages as $dbkey => $redirect ) {
387                                 $title = Title::makeTitleSafe( $namespace, $dbkey );
388                                 $list .= $this->buildRemoveLine( $title, $redirect, $skin );
389                         }
390                         $list .= "</ul>\n";
391                 }
392                 // ISSUE: omit the TOC if the total number of titles is low?
393                 if( $tocLength > 1 ) {
394                         $list = $skin->tocList( $toc ) . $list;
395                 }
396                 return $list;
397         }
398
399         /**
400          * Get the correct "heading" for a namespace
401          *
402          * @param $namespace int
403          * @return string
404          */
405         private function getNamespaceHeading( $namespace ) {
406                 return $namespace == NS_MAIN
407                         ? wfMsgHtml( 'blanknamespace' )
408                         : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
409         }
410
411         /**
412          * Build a single list item containing a check box selecting a title
413          * and a link to that title, with various additional bits
414          *
415          * @param $title Title
416          * @param $redirect bool
417          * @param $skin Skin
418          * @return string
419          */
420         private function buildRemoveLine( $title, $redirect, $skin ) {
421                 global $wgLang;
422
423                 $link = $skin->link( $title );
424                 if( $redirect ) {
425                         $link = '<span class="watchlistredir">' . $link . '</span>';
426                 }
427                 $tools[] = $skin->link( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
428                 if( $title->exists() ) {
429                         $tools[] = $skin->link(
430                                 $title,
431                                 wfMsgHtml( 'history_short' ),
432                                 array(),
433                                 array( 'action' => 'history' ),
434                                 array( 'known', 'noclasses' )
435                         );
436                 }
437                 if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
438                         $tools[] = $skin->link(
439                                 SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
440                                 wfMsgHtml( 'contributions' ),
441                                 array(),
442                                 array(),
443                                 array( 'known', 'noclasses' )
444                         );
445                 }
446
447                 wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $redirect, $skin ) );
448
449                 return "<li>"
450                         . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
451                         . $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n";
452         }
453
454         /**
455          * Show a form for editing the watchlist in "raw" mode
456          *
457          * @param $output OutputPage
458          * @param $user User
459          */
460         public function showRawForm( $output, $user ) {
461                 global $wgUser;
462                 $this->showItemCount( $output, $user );
463                 $self = SpecialPage::getTitleFor( 'Watchlist' );
464                 $form = Xml::openElement( 'form', array( 'method' => 'post',
465                         'action' => $self->getLocalUrl( array( 'action' => 'raw' ) ) ) );
466                 $form .= Html::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
467                 $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
468                 $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
469                 $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
470                 $form .= "<br />\n";
471                 $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
472                         'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
473                 $titles = $this->getWatchlist( $user );
474                 foreach( $titles as $title ) {
475                         $form .= htmlspecialchars( $title ) . "\n";
476                 }
477                 $form .= '</textarea>';
478                 $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
479                 $form .= '</fieldset></form>';
480                 $output->addHTML( $form );
481         }
482
483         /**
484          * Determine whether we are editing the watchlist, and if so, what
485          * kind of editing operation
486          *
487          * @param $request WebRequest
488          * @param $par mixed
489          * @return int
490          */
491         public static function getMode( $request, $par ) {
492                 $mode = strtolower( $request->getVal( 'action', $par ) );
493                 switch( $mode ) {
494                         case 'clear':
495                                 return self::EDIT_CLEAR;
496                         case 'raw':
497                                 return self::EDIT_RAW;
498                         case 'edit':
499                                 return self::EDIT_NORMAL;
500                         default:
501                                 return false;
502                 }
503         }
504
505         /**
506          * Build a set of links for convenient navigation
507          * between watchlist viewing and editing modes
508          *
509          * @param $skin Skin to use
510          * @return string
511          */
512         public static function buildTools( $skin ) {
513                 global $wgLang;
514
515                 $tools = array();
516                 $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' );
517                 foreach( $modes as $mode => $subpage ) {
518                         // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
519                         $tools[] = $skin->linkKnown(
520                                 SpecialPage::getTitleFor( 'Watchlist', $subpage ),
521                                 wfMsgHtml( "watchlisttools-{$mode}" )
522                         );
523                 }
524                 return Html::rawElement( 'span',
525                                         array( 'class' => 'mw-watchlist-toollinks' ),
526                                         wfMsg( 'parentheses', $wgLang->pipeList( $tools ) ) );
527         }
528 }