MediaWiki  REL1_24
SpecialEditWatchlist.php
Go to the documentation of this file.
00001 <?php
00037 class SpecialEditWatchlist extends UnlistedSpecialPage {
00042     const EDIT_CLEAR = 1;
00043     const EDIT_RAW = 2;
00044     const EDIT_NORMAL = 3;
00045 
00046     protected $successMessage;
00047 
00048     protected $toc;
00049 
00050     private $badItems = array();
00051 
00052     public function __construct() {
00053         parent::__construct( 'EditWatchlist', 'editmywatchlist' );
00054     }
00055 
00061     public function execute( $mode ) {
00062         $this->setHeaders();
00063 
00064         # Anons don't get a watchlist
00065         $this->requireLogin( 'watchlistanontext' );
00066 
00067         $out = $this->getOutput();
00068 
00069         $this->checkPermissions();
00070         $this->checkReadOnly();
00071 
00072         $this->outputHeader();
00073         $this->outputSubtitle();
00074 
00075         # B/C: $mode used to be waaay down the parameter list, and the first parameter
00076         # was $wgUser
00077         if ( $mode instanceof User ) {
00078             $args = func_get_args();
00079             if ( count( $args ) >= 4 ) {
00080                 $mode = $args[3];
00081             }
00082         }
00083         $mode = self::getMode( $this->getRequest(), $mode );
00084 
00085         switch ( $mode ) {
00086             case self::EDIT_RAW:
00087                 $out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
00088                 $form = $this->getRawForm();
00089                 if ( $form->show() ) {
00090                     $out->addHTML( $this->successMessage );
00091                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00092                 }
00093                 break;
00094             case self::EDIT_CLEAR:
00095                 $out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) );
00096                 $form = $this->getClearForm();
00097                 if ( $form->show() ) {
00098                     $out->addHTML( $this->successMessage );
00099                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00100                 }
00101                 break;
00102 
00103             case self::EDIT_NORMAL:
00104             default:
00105             $this->executeViewEditWatchlist();
00106                 break;
00107         }
00108     }
00109 
00113     protected function outputSubtitle() {
00114         $out = $this->getOutput();
00115         $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
00116             ->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
00117     }
00118 
00123     protected function executeViewEditWatchlist() {
00124         $out = $this->getOutput();
00125         $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
00126         $form = $this->getNormalForm();
00127         if ( $form->show() ) {
00128             $out->addHTML( $this->successMessage );
00129             $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00130         } elseif ( $this->toc !== false ) {
00131             $out->prependHTML( $this->toc );
00132             $out->addModules( 'mediawiki.toc' );
00133         }
00134     }
00135 
00143     public function prefixSearchSubpages( $search, $limit = 10 ) {
00144         return self::prefixSearchArray(
00145             $search,
00146             $limit,
00147             // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added
00148             // here and there - no 'edit' here, because that the default for this page
00149             array(
00150                 'clear',
00151                 'raw',
00152             )
00153         );
00154     }
00155 
00163     private function extractTitles( $list ) {
00164         $list = explode( "\n", trim( $list ) );
00165         if ( !is_array( $list ) ) {
00166             return array();
00167         }
00168 
00169         $titles = array();
00170 
00171         foreach ( $list as $text ) {
00172             $text = trim( $text );
00173             if ( strlen( $text ) > 0 ) {
00174                 $title = Title::newFromText( $text );
00175                 if ( $title instanceof Title && $title->isWatchable() ) {
00176                     $titles[] = $title;
00177                 }
00178             }
00179         }
00180 
00181         GenderCache::singleton()->doTitlesArray( $titles );
00182 
00183         $list = array();
00185         foreach ( $titles as $title ) {
00186             $list[] = $title->getPrefixedText();
00187         }
00188 
00189         return array_unique( $list );
00190     }
00191 
00192     public function submitRaw( $data ) {
00193         $wanted = $this->extractTitles( $data['Titles'] );
00194         $current = $this->getWatchlist();
00195 
00196         if ( count( $wanted ) > 0 ) {
00197             $toWatch = array_diff( $wanted, $current );
00198             $toUnwatch = array_diff( $current, $wanted );
00199             $this->watchTitles( $toWatch );
00200             $this->unwatchTitles( $toUnwatch );
00201             $this->getUser()->invalidateCache();
00202 
00203             if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
00204                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00205             } else {
00206                 return false;
00207             }
00208 
00209             if ( count( $toWatch ) > 0 ) {
00210                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' )
00211                     ->numParams( count( $toWatch ) )->parse();
00212                 $this->showTitles( $toWatch, $this->successMessage );
00213             }
00214 
00215             if ( count( $toUnwatch ) > 0 ) {
00216                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
00217                     ->numParams( count( $toUnwatch ) )->parse();
00218                 $this->showTitles( $toUnwatch, $this->successMessage );
00219             }
00220         } else {
00221             $this->clearWatchlist();
00222             $this->getUser()->invalidateCache();
00223 
00224             if ( count( $current ) > 0 ) {
00225                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00226             } else {
00227                 return false;
00228             }
00229 
00230             $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
00231                 ->numParams( count( $current ) )->parse();
00232             $this->showTitles( $current, $this->successMessage );
00233         }
00234 
00235         return true;
00236     }
00237 
00238     public function submitClear( $data ) {
00239         $current = $this->getWatchlist();
00240         $this->clearWatchlist();
00241         $this->getUser()->invalidateCache();
00242         $this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse();
00243         $this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' )
00244             ->numParams( count( $current ) )->parse();
00245         $this->showTitles( $current, $this->successMessage );
00246 
00247         return true;
00248     }
00249 
00259     private function showTitles( $titles, &$output ) {
00260         $talk = $this->msg( 'talkpagelinktext' )->escaped();
00261         // Do a batch existence check
00262         $batch = new LinkBatch();
00263         if ( count( $titles ) >= 100 ) {
00264             $output = wfMessage( 'watchlistedit-too-many' )->parse();
00265             return;
00266         }
00267         foreach ( $titles as $title ) {
00268             if ( !$title instanceof Title ) {
00269                 $title = Title::newFromText( $title );
00270             }
00271 
00272             if ( $title instanceof Title ) {
00273                 $batch->addObj( $title );
00274                 $batch->addObj( $title->getTalkPage() );
00275             }
00276         }
00277 
00278         $batch->execute();
00279 
00280         // Print out the list
00281         $output .= "<ul>\n";
00282 
00283         foreach ( $titles as $title ) {
00284             if ( !$title instanceof Title ) {
00285                 $title = Title::newFromText( $title );
00286             }
00287 
00288             if ( $title instanceof Title ) {
00289                 $output .= "<li>"
00290                     . Linker::link( $title )
00291                     . ' (' . Linker::link( $title->getTalkPage(), $talk )
00292                     . ")</li>\n";
00293             }
00294         }
00295 
00296         $output .= "</ul>\n";
00297     }
00298 
00305     private function getWatchlist() {
00306         $list = array();
00307         $dbr = wfGetDB( DB_MASTER );
00308 
00309         $res = $dbr->select(
00310             'watchlist',
00311             array(
00312                 'wl_namespace', 'wl_title'
00313             ), array(
00314                 'wl_user' => $this->getUser()->getId(),
00315             ),
00316             __METHOD__
00317         );
00318 
00319         if ( $res->numRows() > 0 ) {
00320             $titles = array();
00321             foreach ( $res as $row ) {
00322                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
00323 
00324                 if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title )
00325                     && !$title->isTalkPage()
00326                 ) {
00327                     $titles[] = $title;
00328                 }
00329             }
00330             $res->free();
00331 
00332             GenderCache::singleton()->doTitlesArray( $titles );
00333 
00334             foreach ( $titles as $title ) {
00335                 $list[] = $title->getPrefixedText();
00336             }
00337         }
00338 
00339         $this->cleanupWatchlist();
00340 
00341         return $list;
00342     }
00343 
00350     protected function getWatchlistInfo() {
00351         $titles = array();
00352         $dbr = wfGetDB( DB_MASTER );
00353 
00354         $res = $dbr->select(
00355             array( 'watchlist' ),
00356             array( 'wl_namespace', 'wl_title' ),
00357             array( 'wl_user' => $this->getUser()->getId() ),
00358             __METHOD__,
00359             array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) )
00360         );
00361 
00362         $lb = new LinkBatch();
00363 
00364         foreach ( $res as $row ) {
00365             $lb->add( $row->wl_namespace, $row->wl_title );
00366             if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
00367                 $titles[$row->wl_namespace][$row->wl_title] = 1;
00368             }
00369         }
00370 
00371         $lb->execute();
00372 
00373         return $titles;
00374     }
00375 
00384     private function checkTitle( $title, $namespace, $dbKey ) {
00385         if ( $title
00386             && ( $title->isExternal()
00387                 || $title->getNamespace() < 0
00388             )
00389         ) {
00390             $title = false; // unrecoverable
00391         }
00392 
00393         if ( !$title
00394             || $title->getNamespace() != $namespace
00395             || $title->getDBkey() != $dbKey
00396         ) {
00397             $this->badItems[] = array( $title, $namespace, $dbKey );
00398         }
00399 
00400         return (bool)$title;
00401     }
00402 
00406     private function cleanupWatchlist() {
00407         if ( !count( $this->badItems ) ) {
00408             return; //nothing to do
00409         }
00410 
00411         $dbw = wfGetDB( DB_MASTER );
00412         $user = $this->getUser();
00413 
00414         foreach ( $this->badItems as $row ) {
00415             list( $title, $namespace, $dbKey ) = $row;
00416             $action = $title ? 'cleaning up' : 'deleting';
00417             wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
00418 
00419             $dbw->delete( 'watchlist',
00420                 array(
00421                     'wl_user' => $user->getId(),
00422                     'wl_namespace' => $namespace,
00423                     'wl_title' => $dbKey,
00424                 ),
00425                 __METHOD__
00426             );
00427 
00428             // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
00429             if ( $title ) {
00430                 $user->addWatch( $title );
00431             }
00432         }
00433     }
00434 
00438     private function clearWatchlist() {
00439         $dbw = wfGetDB( DB_MASTER );
00440         $dbw->delete(
00441             'watchlist',
00442             array( 'wl_user' => $this->getUser()->getId() ),
00443             __METHOD__
00444         );
00445     }
00446 
00455     private function watchTitles( $titles ) {
00456         $dbw = wfGetDB( DB_MASTER );
00457         $rows = array();
00458 
00459         foreach ( $titles as $title ) {
00460             if ( !$title instanceof Title ) {
00461                 $title = Title::newFromText( $title );
00462             }
00463 
00464             if ( $title instanceof Title ) {
00465                 $rows[] = array(
00466                     'wl_user' => $this->getUser()->getId(),
00467                     'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00468                     'wl_title' => $title->getDBkey(),
00469                     'wl_notificationtimestamp' => null,
00470                 );
00471                 $rows[] = array(
00472                     'wl_user' => $this->getUser()->getId(),
00473                     'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00474                     'wl_title' => $title->getDBkey(),
00475                     'wl_notificationtimestamp' => null,
00476                 );
00477             }
00478         }
00479 
00480         $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
00481     }
00482 
00491     private function unwatchTitles( $titles ) {
00492         $dbw = wfGetDB( DB_MASTER );
00493 
00494         foreach ( $titles as $title ) {
00495             if ( !$title instanceof Title ) {
00496                 $title = Title::newFromText( $title );
00497             }
00498 
00499             if ( $title instanceof Title ) {
00500                 $dbw->delete(
00501                     'watchlist',
00502                     array(
00503                         'wl_user' => $this->getUser()->getId(),
00504                         'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00505                         'wl_title' => $title->getDBkey(),
00506                     ),
00507                     __METHOD__
00508                 );
00509 
00510                 $dbw->delete(
00511                     'watchlist',
00512                     array(
00513                         'wl_user' => $this->getUser()->getId(),
00514                         'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00515                         'wl_title' => $title->getDBkey(),
00516                     ),
00517                     __METHOD__
00518                 );
00519 
00520                 $page = WikiPage::factory( $title );
00521                 wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) );
00522             }
00523         }
00524     }
00525 
00526     public function submitNormal( $data ) {
00527         $removed = array();
00528 
00529         foreach ( $data as $titles ) {
00530             $this->unwatchTitles( $titles );
00531             $removed = array_merge( $removed, $titles );
00532         }
00533 
00534         if ( count( $removed ) > 0 ) {
00535             $this->successMessage = $this->msg( 'watchlistedit-normal-done'
00536             )->numParams( count( $removed ) )->parse();
00537             $this->showTitles( $removed, $this->successMessage );
00538 
00539             return true;
00540         } else {
00541             return false;
00542         }
00543     }
00544 
00550     protected function getNormalForm() {
00551         global $wgContLang;
00552 
00553         $fields = array();
00554         $count = 0;
00555 
00556         // Allow subscribers to manipulate the list of watched pages (or use it
00557         // to preload lots of details at once)
00558         $watchlistInfo = $this->getWatchlistInfo();
00559         wfRunHooks(
00560             'WatchlistEditorBeforeFormRender',
00561             array( &$watchlistInfo )
00562         );
00563 
00564         foreach ( $watchlistInfo as $namespace => $pages ) {
00565             $options = array();
00566 
00567             foreach ( array_keys( $pages ) as $dbkey ) {
00568                 $title = Title::makeTitleSafe( $namespace, $dbkey );
00569 
00570                 if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
00571                     $text = $this->buildRemoveLine( $title );
00572                     $options[$text] = $title->getPrefixedText();
00573                     $count++;
00574                 }
00575             }
00576 
00577             // checkTitle can filter some options out, avoid empty sections
00578             if ( count( $options ) > 0 ) {
00579                 $fields['TitlesNs' . $namespace] = array(
00580                     'class' => 'EditWatchlistCheckboxSeriesField',
00581                     'options' => $options,
00582                     'section' => "ns$namespace",
00583                 );
00584             }
00585         }
00586         $this->cleanupWatchlist();
00587 
00588         if ( count( $fields ) > 1 && $count > 30 ) {
00589             $this->toc = Linker::tocIndent();
00590             $tocLength = 0;
00591 
00592             foreach ( $fields as $data ) {
00593                 # strip out the 'ns' prefix from the section name:
00594                 $ns = substr( $data['section'], 2 );
00595 
00596                 $nsText = ( $ns == NS_MAIN )
00597                     ? $this->msg( 'blanknamespace' )->escaped()
00598                     : htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
00599                 $this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
00600                     $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
00601             }
00602 
00603             $this->toc = Linker::tocList( $this->toc );
00604         } else {
00605             $this->toc = false;
00606         }
00607 
00608         $context = new DerivativeContext( $this->getContext() );
00609         $context->setTitle( $this->getPageTitle() ); // Remove subpage
00610         $form = new EditWatchlistNormalHTMLForm( $fields, $context );
00611         $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
00612         # Used message keys:
00613         # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
00614         $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
00615         $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
00616         $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
00617         $form->setSubmitCallback( array( $this, 'submitNormal' ) );
00618 
00619         return $form;
00620     }
00621 
00628     private function buildRemoveLine( $title ) {
00629         $link = Linker::link( $title );
00630 
00631         $tools['talk'] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() );
00632 
00633         if ( $title->exists() ) {
00634             $tools['history'] = Linker::linkKnown(
00635                 $title,
00636                 $this->msg( 'history_short' )->escaped(),
00637                 array(),
00638                 array( 'action' => 'history' )
00639             );
00640         }
00641 
00642         if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
00643             $tools['contributions'] = Linker::linkKnown(
00644                 SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
00645                 $this->msg( 'contributions' )->escaped()
00646             );
00647         }
00648 
00649         wfRunHooks(
00650             'WatchlistEditorBuildRemoveLine',
00651             array( &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link )
00652         );
00653 
00654         if ( $title->isRedirect() ) {
00655             // Linker already makes class mw-redirect, so this is redundant
00656             $link = '<span class="watchlistredir">' . $link . '</span>';
00657         }
00658 
00659         return $link . " (" . $this->getLanguage()->pipeList( $tools ) . ")";
00660     }
00661 
00667     protected function getRawForm() {
00668         $titles = implode( $this->getWatchlist(), "\n" );
00669         $fields = array(
00670             'Titles' => array(
00671                 'type' => 'textarea',
00672                 'label-message' => 'watchlistedit-raw-titles',
00673                 'default' => $titles,
00674             ),
00675         );
00676         $context = new DerivativeContext( $this->getContext() );
00677         $context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
00678         $form = new HTMLForm( $fields, $context );
00679         $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
00680         # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
00681         $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
00682         $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
00683         $form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
00684         $form->setSubmitCallback( array( $this, 'submitRaw' ) );
00685 
00686         return $form;
00687     }
00688 
00694     protected function getClearForm() {
00695         $context = new DerivativeContext( $this->getContext() );
00696         $context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
00697         $form = new HTMLForm( array(), $context );
00698         $form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
00699         # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
00700         $form->setSubmitTooltip( 'watchlistedit-clear-submit' );
00701         $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
00702         $form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
00703         $form->setSubmitCallback( array( $this, 'submitClear' ) );
00704 
00705         return $form;
00706     }
00707 
00716     public static function getMode( $request, $par ) {
00717         $mode = strtolower( $request->getVal( 'action', $par ) );
00718 
00719         switch ( $mode ) {
00720             case 'clear':
00721             case self::EDIT_CLEAR:
00722                 return self::EDIT_CLEAR;
00723             case 'raw':
00724             case self::EDIT_RAW:
00725                 return self::EDIT_RAW;
00726             case 'edit':
00727             case self::EDIT_NORMAL:
00728                 return self::EDIT_NORMAL;
00729             default:
00730                 return false;
00731         }
00732     }
00733 
00741     public static function buildTools( $unused ) {
00742         global $wgLang;
00743 
00744         $tools = array();
00745         $modes = array(
00746             'view' => array( 'Watchlist', false ),
00747             'edit' => array( 'EditWatchlist', false ),
00748             'raw' => array( 'EditWatchlist', 'raw' ),
00749             'clear' => array( 'EditWatchlist', 'clear' ),
00750         );
00751 
00752         foreach ( $modes as $mode => $arr ) {
00753             // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
00754             $tools[] = Linker::linkKnown(
00755                 SpecialPage::getTitleFor( $arr[0], $arr[1] ),
00756                 wfMessage( "watchlisttools-{$mode}" )->escaped()
00757             );
00758         }
00759 
00760         return Html::rawElement(
00761             'span',
00762             array( 'class' => 'mw-watchlist-toollinks' ),
00763             wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text()
00764         );
00765     }
00766 }
00767 
00771 class EditWatchlistNormalHTMLForm extends HTMLForm {
00772     public function getLegend( $namespace ) {
00773         $namespace = substr( $namespace, 2 );
00774 
00775         return $namespace == NS_MAIN
00776             ? $this->msg( 'blanknamespace' )->escaped()
00777             : htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
00778     }
00779 
00780     public function getBody() {
00781         return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
00782     }
00783 }
00784 
00785 class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
00797     function validate( $value, $alldata ) {
00798         // Need to call into grandparent to be a good citizen. :)
00799         return HTMLFormField::validate( $value, $alldata );
00800     }
00801 }