And even more documentation, the last of this batch
[lhc/web/wiklou.git] / includes / specials / SpecialEditWatchlist.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 SpecialEditWatchlist extends UnlistedSpecialPage {
11
12 /**
13 * Editing modes
14 */
15 const EDIT_CLEAR = 1;
16 const EDIT_RAW = 2;
17 const EDIT_NORMAL = 3;
18
19 protected $successMessage;
20
21 public function __construct(){
22 parent::__construct( 'EditWatchlist' );
23 }
24
25 /**
26 * Main execution point
27 *
28 * @param $mode int
29 */
30 public function execute( $mode ) {
31 global $wgUser, $wgOut, $wgRequest;
32 if( wfReadOnly() ) {
33 $wgOut->readOnlyPage();
34 return;
35 }
36
37 # Anons don't get a watchlist
38 if( $wgUser->isAnon() ) {
39 $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
40 $llink = $wgUser->getSkin()->linkKnown(
41 SpecialPage::getTitleFor( 'Userlogin' ),
42 wfMsgHtml( 'loginreqlink' ),
43 array(),
44 array( 'returnto' => $this->getTitle()->getPrefixedText() )
45 );
46 $wgOut->addWikiMsgArray( 'watchlistanontext', array( $llink ), array( 'replaceafter' ) );
47 return;
48 }
49
50 $sub = wfMsgExt(
51 'watchlistfor2',
52 array( 'parseinline', 'replaceafter' ),
53 $wgUser->getName(),
54 SpecialEditWatchlist::buildTools( $wgUser->getSkin() )
55 );
56 $wgOut->setSubtitle( $sub );
57
58 # B/C: $mode used to be waaay down the parameter list, and the first parameter
59 # was $wgUser
60 if( $mode instanceof User ){
61 $args = func_get_args();
62 if( count( $args >= 4 ) ){
63 $mode = $args[3];
64 }
65 }
66 $mode = self::getMode( $wgRequest, $mode );
67
68 switch( $mode ) {
69 case self::EDIT_CLEAR:
70 // The "Clear" link scared people too much.
71 // Pass on to the raw editor, from which it's very easy to clear.
72
73 case self::EDIT_RAW:
74 $wgOut->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) );
75 $form = $this->getRawForm( $wgUser );
76 if( $form->show() ){
77 $wgOut->addHTML( $this->successMessage );
78 $wgOut->returnToMain();
79 }
80 break;
81
82 case self::EDIT_NORMAL:
83 default:
84 $wgOut->setPageTitle( wfMsg( 'watchlistedit-normal-title' ) );
85 $form = $this->getNormalForm( $wgUser );
86 if( $form->show() ){
87 $wgOut->addHTML( $this->successMessage );
88 $wgOut->returnToMain();
89 }
90 break;
91 }
92 }
93
94 /**
95 * Extract a list of titles from a blob of text, returning
96 * (prefixed) strings; unwatchable titles are ignored
97 *
98 * @param $list String
99 * @return array
100 */
101 private function extractTitles( $list ) {
102 $titles = array();
103 $list = explode( "\n", trim( $list ) );
104 if( !is_array( $list ) ) {
105 return array();
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 public function submitRaw( $data ){
120 global $wgUser, $wgLang;
121 $wanted = $this->extractTitles( $data['Titles'] );
122 $current = $this->getWatchlist( $wgUser );
123
124 if( count( $wanted ) > 0 ) {
125 $toWatch = array_diff( $wanted, $current );
126 $toUnwatch = array_diff( $current, $wanted );
127 $this->watchTitles( $toWatch, $wgUser );
128 $this->unwatchTitles( $toUnwatch, $wgUser );
129 $wgUser->invalidateCache();
130
131 if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ){
132 $this->successMessage = wfMessage( 'watchlistedit-raw-done' )->parse();
133 } else {
134 return false;
135 }
136
137 if( count( $toWatch ) > 0 ) {
138 $this->successMessage .= wfMessage(
139 'watchlistedit-raw-added',
140 $wgLang->formatNum( count( $toWatch ) )
141 );
142 $this->showTitles( $toWatch, $this->successMessage, $wgUser->getSkin() );
143 }
144
145 if( count( $toUnwatch ) > 0 ) {
146 $this->successMessage .= wfMessage(
147 'watchlistedit-raw-removed',
148 $wgLang->formatNum( count( $toUnwatch ) )
149 );
150 $this->showTitles( $toUnwatch, $this->successMessage, $wgUser->getSkin() );
151 }
152 } else {
153 $this->clearWatchlist( $wgUser );
154 $wgUser->invalidateCache();
155 $this->successMessage .= wfMessage(
156 'watchlistedit-raw-removed',
157 $wgLang->formatNum( count( $current ) )
158 );
159 $this->showTitles( $current, $this->successMessage, $wgUser->getSkin() );
160 }
161 return true;
162 }
163
164 /**
165 * Print out a list of linked titles
166 *
167 * $titles can be an array of strings or Title objects; the former
168 * is preferred, since Titles are very memory-heavy
169 *
170 * @param $titles array of strings, or Title objects
171 * @param $output String
172 * @param $skin Skin
173 */
174 private function showTitles( $titles, &$output, $skin ) {
175 $talk = wfMsgHtml( 'talkpagelinktext' );
176 // Do a batch existence check
177 $batch = new LinkBatch();
178 foreach( $titles as $title ) {
179 if( !$title instanceof Title ) {
180 $title = Title::newFromText( $title );
181 }
182 if( $title instanceof Title ) {
183 $batch->addObj( $title );
184 $batch->addObj( $title->getTalkPage() );
185 }
186 }
187 $batch->execute();
188 // Print out the list
189 $output .= "<ul>\n";
190 foreach( $titles as $title ) {
191 if( !$title instanceof Title ) {
192 $title = Title::newFromText( $title );
193 }
194 if( $title instanceof Title ) {
195 $output .= "<li>"
196 . $skin->link( $title )
197 . ' (' . $skin->link( $title->getTalkPage(), $talk )
198 . ")</li>\n";
199 }
200 }
201 $output .= "</ul>\n";
202 }
203
204 /**
205 * Count the number of titles on a user's watchlist, excluding talk pages
206 *
207 * @param $user User
208 * @return int
209 */
210 private function countWatchlist( $user ) {
211 $dbr = wfGetDB( DB_MASTER );
212 $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ );
213 $row = $dbr->fetchObject( $res );
214 return ceil( $row->count / 2 ); // Paranoia
215 }
216
217 /**
218 * Prepare a list of titles on a user's watchlist (excluding talk pages)
219 * and return an array of (prefixed) strings
220 *
221 * @param $user User
222 * @return array
223 */
224 private function getWatchlist( $user ) {
225 $list = array();
226 $dbr = wfGetDB( DB_MASTER );
227 $res = $dbr->select(
228 'watchlist',
229 '*',
230 array(
231 'wl_user' => $user->getId(),
232 ),
233 __METHOD__
234 );
235 if( $res->numRows() > 0 ) {
236 foreach ( $res as $row ) {
237 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
238 if( $title instanceof Title && !$title->isTalkPage() )
239 $list[] = $title->getPrefixedText();
240 }
241 $res->free();
242 }
243 return $list;
244 }
245
246 /**
247 * Get a list of titles on a user's watchlist, excluding talk pages,
248 * and return as a two-dimensional array with namespace, title and
249 * redirect status
250 *
251 * @param $user User
252 * @return array
253 */
254 private function getWatchlistInfo( $user ) {
255 $titles = array();
256 $dbr = wfGetDB( DB_MASTER );
257
258 $res = $dbr->select(
259 array( 'watchlist', 'page' ),
260 array(
261 'wl_namespace',
262 'wl_title',
263 'page_id',
264 'page_len',
265 'page_is_redirect',
266 'page_latest'
267 ),
268 array( 'wl_user' => $user->getId() ),
269 __METHOD__,
270 array( 'ORDER BY' => 'wl_namespace, wl_title' ),
271 array( 'page' => array(
272 'LEFT JOIN',
273 'wl_namespace = page_namespace AND wl_title = page_title'
274 ) )
275 );
276
277 if( $res && $dbr->numRows( $res ) > 0 ) {
278 $cache = LinkCache::singleton();
279 foreach ( $res as $row ) {
280 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
281 if( $title instanceof Title ) {
282 // Update the link cache while we're at it
283 if( $row->page_id ) {
284 $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect, $row->page_latest );
285 } else {
286 $cache->addBadLinkObj( $title );
287 }
288 // Ignore non-talk
289 if( !$title->isTalkPage() ) {
290 $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect;
291 }
292 }
293 }
294 }
295 return $titles;
296 }
297
298 /**
299 * Show a message indicating the number of items on the user's watchlist,
300 * and return this count for additional checking
301 *
302 * @param $output OutputPage
303 * @param $user User
304 * @return int
305 */
306 private function showItemCount( $output, $user ) {
307 if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
308 $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse',
309 $GLOBALS['wgLang']->formatNum( $count ) ) );
310 } else {
311 $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
312 }
313 return $count;
314 }
315
316 /**
317 * Remove all titles from a user's watchlist
318 *
319 * @param $user User
320 */
321 private function clearWatchlist( $user ) {
322 $dbw = wfGetDB( DB_MASTER );
323 $dbw->delete(
324 'watchlist',
325 array( 'wl_user' => $user->getId() ),
326 __METHOD__
327 );
328 }
329
330 /**
331 * Add a list of titles to a user's watchlist
332 *
333 * $titles can be an array of strings or Title objects; the former
334 * is preferred, since Titles are very memory-heavy
335 *
336 * @param $titles Array of strings, or Title objects
337 * @param $user User
338 */
339 private function watchTitles( $titles, $user ) {
340 $dbw = wfGetDB( DB_MASTER );
341 $rows = array();
342 foreach( $titles as $title ) {
343 if( !$title instanceof Title ) {
344 $title = Title::newFromText( $title );
345 }
346 if( $title instanceof Title ) {
347 $rows[] = array(
348 'wl_user' => $user->getId(),
349 'wl_namespace' => ( $title->getNamespace() & ~1 ),
350 'wl_title' => $title->getDBkey(),
351 'wl_notificationtimestamp' => null,
352 );
353 $rows[] = array(
354 'wl_user' => $user->getId(),
355 'wl_namespace' => ( $title->getNamespace() | 1 ),
356 'wl_title' => $title->getDBkey(),
357 'wl_notificationtimestamp' => null,
358 );
359 }
360 }
361 $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
362 }
363
364 /**
365 * Remove a list of titles from a user's watchlist
366 *
367 * $titles can be an array of strings or Title objects; the former
368 * is preferred, since Titles are very memory-heavy
369 *
370 * @param $titles Array of strings, or Title objects
371 * @param $user User
372 */
373 private function unwatchTitles( $titles, $user ) {
374 $dbw = wfGetDB( DB_MASTER );
375 foreach( $titles as $title ) {
376 if( !$title instanceof Title ) {
377 $title = Title::newFromText( $title );
378 }
379 if( $title instanceof Title ) {
380 $dbw->delete(
381 'watchlist',
382 array(
383 'wl_user' => $user->getId(),
384 'wl_namespace' => ( $title->getNamespace() & ~1 ),
385 'wl_title' => $title->getDBkey(),
386 ),
387 __METHOD__
388 );
389 $dbw->delete(
390 'watchlist',
391 array(
392 'wl_user' => $user->getId(),
393 'wl_namespace' => ( $title->getNamespace() | 1 ),
394 'wl_title' => $title->getDBkey(),
395 ),
396 __METHOD__
397 );
398 $article = new Article($title);
399 wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
400 }
401 }
402 }
403
404 public function submitNormal( $data ) {
405 global $wgUser;
406 $removed = array();
407
408 foreach( $data as $titles ) {
409 $this->unwatchTitles( $titles, $wgUser );
410 $removed += $titles;
411 }
412
413 if( count( $removed ) > 0 ) {
414 global $wgLang;
415 $this->successMessage = wfMessage(
416 'watchlistedit-normal-done',
417 $wgLang->formatNum( count( $removed ) )
418 );
419 $this->showTitles( $removed, $this->successMessage, $wgUser->getSkin() );
420 return true;
421 } else {
422 return false;
423 }
424 }
425
426 /**
427 * Get the standard watchlist editing form
428 *
429 * @param $user User
430 * @return HTMLForm
431 */
432 protected function getNormalForm( $user ){
433 global $wgContLang;
434 $skin = $user->getSkin();
435 $fields = array();
436
437 foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ){
438
439 $namespace == NS_MAIN
440 ? wfMsgHtml( 'blanknamespace' )
441 : htmlspecialchars( $wgContLang->getFormattedNsText( $namespace ) );
442
443 $fields['TitlesNs'.$namespace] = array(
444 'type' => 'multiselect',
445 'options' => array(),
446 'section' => "ns$namespace",
447 );
448
449 foreach( $pages as $dbkey => $redirect ){
450 $title = Title::makeTitleSafe( $namespace, $dbkey );
451 $text = $this->buildRemoveLine( $title, $redirect, $skin );
452 $fields['TitlesNs'.$namespace]['options'][$text] = $title->getEscapedText();
453 }
454 }
455
456 $form = new EditWatchlistNormalHTMLForm( $fields );
457 $form->setTitle( $this->getTitle() );
458 $form->setSubmitText( wfMessage( 'watchlistedit-normal-submit' )->text() );
459 $form->setWrapperLegend( wfMessage( 'watchlistedit-normal-legend' )->text() );
460 $form->addHeaderText( wfMessage( 'watchlistedit-normal-explain' )->parse() );
461 $form->setSubmitCallback( array( $this, 'submitNormal' ) );
462 return $form;
463 }
464
465 /**
466 * Build the label for a checkbox, with a link to the title, and various additional bits
467 *
468 * @param $title Title
469 * @param $redirect bool
470 * @param $skin Skin
471 * @return string
472 */
473 private function buildRemoveLine( $title, $redirect, $skin ) {
474 global $wgLang;
475
476 $link = $skin->link( $title );
477 if( $redirect ) {
478 $link = '<span class="watchlistredir">' . $link . '</span>';
479 }
480 $tools[] = $skin->link( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
481 if( $title->exists() ) {
482 $tools[] = $skin->link(
483 $title,
484 wfMsgHtml( 'history_short' ),
485 array(),
486 array( 'action' => 'history' ),
487 array( 'known', 'noclasses' )
488 );
489 }
490 if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
491 $tools[] = $skin->link(
492 SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
493 wfMsgHtml( 'contributions' ),
494 array(),
495 array(),
496 array( 'known', 'noclasses' )
497 );
498 }
499
500 wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $redirect, $skin ) );
501
502 return $link . " (" . $wgLang->pipeList( $tools ) . ")";
503 }
504
505 /**
506 * Get a form for editing the watchlist in "raw" mode
507 *
508 * @param $user User
509 * @return HTMLForm
510 */
511 protected function getRawForm( $user ){
512 $titles = implode( array_map( 'htmlspecialchars', $this->getWatchlist( $user ) ), "\n" );
513 $fields = array(
514 'Titles' => array(
515 'type' => 'textarea',
516 'label-message' => 'watchlistedit-raw-titles',
517 'default' => $titles,
518 ),
519 );
520 $form = new HTMLForm( $fields );
521 $form->setTitle( $this->getTitle( 'raw' ) );
522 $form->setSubmitText( wfMessage( 'watchlistedit-raw-submit' )->text() );
523 $form->setWrapperLegend( wfMessage( 'watchlistedit-raw-legend' )->text() );
524 $form->addHeaderText( wfMessage( 'watchlistedit-raw-explain' )->parse() );
525 $form->setSubmitCallback( array( $this, 'submitRaw' ) );
526 return $form;
527 }
528
529 /**
530 * Determine whether we are editing the watchlist, and if so, what
531 * kind of editing operation
532 *
533 * @param $request WebRequest
534 * @param $par mixed
535 * @return int
536 */
537 public static function getMode( $request, $par ) {
538 $mode = strtolower( $request->getVal( 'action', $par ) );
539 switch( $mode ) {
540 case 'clear':
541 case self::EDIT_CLEAR:
542 return self::EDIT_CLEAR;
543
544 case 'raw':
545 case self::EDIT_RAW:
546 return self::EDIT_RAW;
547
548 case 'edit':
549 case self::EDIT_NORMAL:
550 return self::EDIT_NORMAL;
551
552 default:
553 return false;
554 }
555 }
556
557 /**
558 * Build a set of links for convenient navigation
559 * between watchlist viewing and editing modes
560 *
561 * @param $skin Skin to use
562 * @return string
563 */
564 public static function buildTools( $skin ) {
565 global $wgLang;
566
567 $tools = array();
568 $modes = array(
569 'view' => array( 'Watchlist', false ),
570 'edit' => array( 'EditWatchlist', false ),
571 'raw' => array( 'EditWatchlist', 'raw' ),
572 );
573 foreach( $modes as $mode => $arr ) {
574 // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
575 $tools[] = $skin->linkKnown(
576 SpecialPage::getTitleFor( $arr[0], $arr[1] ),
577 wfMsgHtml( "watchlisttools-{$mode}" )
578 );
579 }
580 return Html::rawElement( 'span',
581 array( 'class' => 'mw-watchlist-toollinks' ),
582 wfMsg( 'parentheses', $wgLang->pipeList( $tools ) ) );
583 }
584 }
585
586 # B/C since 1.18
587 class WatchlistEditor extends SpecialEditWatchlist {}
588
589 /**
590 * Extend HTMLForm purely so we can have a more sane way of getting the section headers
591 */
592 class EditWatchlistNormalHTMLForm extends HTMLForm {
593 public function getLegend( $namespace ){
594 global $wgLang;
595 $namespace = substr( $namespace, 2 );
596 return $namespace == NS_MAIN
597 ? wfMsgHtml( 'blanknamespace' )
598 : htmlspecialchars( $wgLang->getFormattedNsText( $namespace ) );
599 }
600 }