MediaWiki  REL1_23
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 
00074         $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
00075             ->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
00076 
00077         # B/C: $mode used to be waaay down the parameter list, and the first parameter
00078         # was $wgUser
00079         if ( $mode instanceof User ) {
00080             $args = func_get_args();
00081             if ( count( $args ) >= 4 ) {
00082                 $mode = $args[3];
00083             }
00084         }
00085         $mode = self::getMode( $this->getRequest(), $mode );
00086 
00087         switch ( $mode ) {
00088             case self::EDIT_RAW:
00089                 $out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
00090                 $form = $this->getRawForm();
00091                 if ( $form->show() ) {
00092                     $out->addHTML( $this->successMessage );
00093                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00094                 }
00095                 break;
00096             case self::EDIT_CLEAR:
00097                 $out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) );
00098                 $form = $this->getClearForm();
00099                 if ( $form->show() ) {
00100                     $out->addHTML( $this->successMessage );
00101                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00102                 }
00103                 break;
00104 
00105             case self::EDIT_NORMAL:
00106             default:
00107                 $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
00108                 $form = $this->getNormalForm();
00109                 if ( $form->show() ) {
00110                     $out->addHTML( $this->successMessage );
00111                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00112                 } elseif ( $this->toc !== false ) {
00113                     $out->prependHTML( $this->toc );
00114                 }
00115                 break;
00116         }
00117     }
00118 
00126     private function extractTitles( $list ) {
00127         $list = explode( "\n", trim( $list ) );
00128         if ( !is_array( $list ) ) {
00129             return array();
00130         }
00131 
00132         $titles = array();
00133 
00134         foreach ( $list as $text ) {
00135             $text = trim( $text );
00136             if ( strlen( $text ) > 0 ) {
00137                 $title = Title::newFromText( $text );
00138                 if ( $title instanceof Title && $title->isWatchable() ) {
00139                     $titles[] = $title;
00140                 }
00141             }
00142         }
00143 
00144         GenderCache::singleton()->doTitlesArray( $titles );
00145 
00146         $list = array();
00148         foreach ( $titles as $title ) {
00149             $list[] = $title->getPrefixedText();
00150         }
00151 
00152         return array_unique( $list );
00153     }
00154 
00155     public function submitRaw( $data ) {
00156         $wanted = $this->extractTitles( $data['Titles'] );
00157         $current = $this->getWatchlist();
00158 
00159         if ( count( $wanted ) > 0 ) {
00160             $toWatch = array_diff( $wanted, $current );
00161             $toUnwatch = array_diff( $current, $wanted );
00162             $this->watchTitles( $toWatch );
00163             $this->unwatchTitles( $toUnwatch );
00164             $this->getUser()->invalidateCache();
00165 
00166             if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
00167                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00168             } else {
00169                 return false;
00170             }
00171 
00172             if ( count( $toWatch ) > 0 ) {
00173                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' )
00174                     ->numParams( count( $toWatch ) )->parse();
00175                 $this->showTitles( $toWatch, $this->successMessage );
00176             }
00177 
00178             if ( count( $toUnwatch ) > 0 ) {
00179                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
00180                     ->numParams( count( $toUnwatch ) )->parse();
00181                 $this->showTitles( $toUnwatch, $this->successMessage );
00182             }
00183         } else {
00184             $this->clearWatchlist();
00185             $this->getUser()->invalidateCache();
00186 
00187             if ( count( $current ) > 0 ) {
00188                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00189             } else {
00190                 return false;
00191             }
00192 
00193             $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
00194                 ->numParams( count( $current ) )->parse();
00195             $this->showTitles( $current, $this->successMessage );
00196         }
00197 
00198         return true;
00199     }
00200 
00201       public function submitClear( $data ) {
00202         $current = $this->getWatchlist();
00203         $this->clearWatchlist();
00204         $this->getUser()->invalidateCache();
00205             $this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse();
00206         $this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' )
00207             ->numParams( count( $current ) )->parse();
00208         $this->showTitles( $current, $this->successMessage );
00209 
00210         return true;
00211 }
00212 
00222     private function showTitles( $titles, &$output ) {
00223         $talk = $this->msg( 'talkpagelinktext' )->escaped();
00224         // Do a batch existence check
00225         $batch = new LinkBatch();
00226         if (count($titles) >= 100) {
00227             $output = wfMessage( 'watchlistedit-too-many' )->parse();
00228             return;
00229         }
00230         foreach ( $titles as $title ) {
00231             if ( !$title instanceof Title ) {
00232                 $title = Title::newFromText( $title );
00233             }
00234 
00235             if ( $title instanceof Title ) {
00236                 $batch->addObj( $title );
00237                 $batch->addObj( $title->getTalkPage() );
00238             }
00239         }
00240 
00241         $batch->execute();
00242 
00243         // Print out the list
00244         $output .= "<ul>\n";
00245 
00246         foreach ( $titles as $title ) {
00247             if ( !$title instanceof Title ) {
00248                 $title = Title::newFromText( $title );
00249             }
00250 
00251             if ( $title instanceof Title ) {
00252                 $output .= "<li>"
00253                     . Linker::link( $title )
00254                     . ' (' . Linker::link( $title->getTalkPage(), $talk )
00255                     . ")</li>\n";
00256             }
00257         }
00258 
00259         $output .= "</ul>\n";
00260     }
00261 
00268     private function getWatchlist() {
00269         $list = array();
00270         $dbr = wfGetDB( DB_MASTER );
00271 
00272         $res = $dbr->select(
00273             'watchlist',
00274             array(
00275                 'wl_namespace', 'wl_title'
00276             ), array(
00277                 'wl_user' => $this->getUser()->getId(),
00278             ),
00279             __METHOD__
00280         );
00281 
00282         if ( $res->numRows() > 0 ) {
00283             $titles = array();
00284             foreach ( $res as $row ) {
00285                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
00286 
00287                 if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title )
00288                     && !$title->isTalkPage()
00289                 ) {
00290                     $titles[] = $title;
00291                 }
00292             }
00293             $res->free();
00294 
00295             GenderCache::singleton()->doTitlesArray( $titles );
00296 
00297             foreach ( $titles as $title ) {
00298                 $list[] = $title->getPrefixedText();
00299             }
00300         }
00301 
00302         $this->cleanupWatchlist();
00303 
00304         return $list;
00305     }
00306 
00313     private function getWatchlistInfo() {
00314         $titles = array();
00315         $dbr = wfGetDB( DB_MASTER );
00316 
00317         $res = $dbr->select(
00318             array( 'watchlist' ),
00319             array( 'wl_namespace', 'wl_title' ),
00320             array( 'wl_user' => $this->getUser()->getId() ),
00321             __METHOD__,
00322             array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) )
00323         );
00324 
00325         $lb = new LinkBatch();
00326 
00327         foreach ( $res as $row ) {
00328             $lb->add( $row->wl_namespace, $row->wl_title );
00329             if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
00330                 $titles[$row->wl_namespace][$row->wl_title] = 1;
00331             }
00332         }
00333 
00334         $lb->execute();
00335 
00336         return $titles;
00337     }
00338 
00347     private function checkTitle( $title, $namespace, $dbKey ) {
00348         if ( $title
00349             && ( $title->isExternal()
00350                 || $title->getNamespace() < 0
00351             )
00352         ) {
00353             $title = false; // unrecoverable
00354         }
00355 
00356         if ( !$title
00357             || $title->getNamespace() != $namespace
00358             || $title->getDBkey() != $dbKey
00359         ) {
00360             $this->badItems[] = array( $title, $namespace, $dbKey );
00361         }
00362 
00363         return (bool)$title;
00364     }
00365 
00369     private function cleanupWatchlist() {
00370         if ( !count( $this->badItems ) ) {
00371             return; //nothing to do
00372         }
00373 
00374         $dbw = wfGetDB( DB_MASTER );
00375         $user = $this->getUser();
00376 
00377         foreach ( $this->badItems as $row ) {
00378             list( $title, $namespace, $dbKey ) = $row;
00379             $action = $title ? 'cleaning up' : 'deleting';
00380             wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
00381 
00382             $dbw->delete( 'watchlist',
00383                 array(
00384                     'wl_user' => $user->getId(),
00385                     'wl_namespace' => $namespace,
00386                     'wl_title' => $dbKey,
00387                 ),
00388                 __METHOD__
00389             );
00390 
00391             // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
00392             if ( $title ) {
00393                 $user->addWatch( $title );
00394             }
00395         }
00396     }
00397 
00401     private function clearWatchlist() {
00402         $dbw = wfGetDB( DB_MASTER );
00403         $dbw->delete(
00404             'watchlist',
00405             array( 'wl_user' => $this->getUser()->getId() ),
00406             __METHOD__
00407         );
00408     }
00409 
00418     private function watchTitles( $titles ) {
00419         $dbw = wfGetDB( DB_MASTER );
00420         $rows = array();
00421 
00422         foreach ( $titles as $title ) {
00423             if ( !$title instanceof Title ) {
00424                 $title = Title::newFromText( $title );
00425             }
00426 
00427             if ( $title instanceof Title ) {
00428                 $rows[] = array(
00429                     'wl_user' => $this->getUser()->getId(),
00430                     'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00431                     'wl_title' => $title->getDBkey(),
00432                     'wl_notificationtimestamp' => null,
00433                 );
00434                 $rows[] = array(
00435                     'wl_user' => $this->getUser()->getId(),
00436                     'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00437                     'wl_title' => $title->getDBkey(),
00438                     'wl_notificationtimestamp' => null,
00439                 );
00440             }
00441         }
00442 
00443         $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
00444     }
00445 
00454     private function unwatchTitles( $titles ) {
00455         $dbw = wfGetDB( DB_MASTER );
00456 
00457         foreach ( $titles as $title ) {
00458             if ( !$title instanceof Title ) {
00459                 $title = Title::newFromText( $title );
00460             }
00461 
00462             if ( $title instanceof Title ) {
00463                 $dbw->delete(
00464                     'watchlist',
00465                     array(
00466                         'wl_user' => $this->getUser()->getId(),
00467                         'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00468                         'wl_title' => $title->getDBkey(),
00469                     ),
00470                     __METHOD__
00471                 );
00472 
00473                 $dbw->delete(
00474                     'watchlist',
00475                     array(
00476                         'wl_user' => $this->getUser()->getId(),
00477                         'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00478                         'wl_title' => $title->getDBkey(),
00479                     ),
00480                     __METHOD__
00481                 );
00482 
00483                 $page = WikiPage::factory( $title );
00484                 wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) );
00485             }
00486         }
00487     }
00488 
00489     public function submitNormal( $data ) {
00490         $removed = array();
00491 
00492         foreach ( $data as $titles ) {
00493             $this->unwatchTitles( $titles );
00494             $removed = array_merge( $removed, $titles );
00495         }
00496 
00497         if ( count( $removed ) > 0 ) {
00498             $this->successMessage = $this->msg( 'watchlistedit-normal-done'
00499             )->numParams( count( $removed ) )->parse();
00500             $this->showTitles( $removed, $this->successMessage );
00501 
00502             return true;
00503         } else {
00504             return false;
00505         }
00506     }
00507 
00513     protected function getNormalForm() {
00514         global $wgContLang;
00515 
00516         $fields = array();
00517         $count = 0;
00518 
00519         foreach ( $this->getWatchlistInfo() as $namespace => $pages ) {
00520             if ( $namespace >= 0 ) {
00521                 $fields['TitlesNs' . $namespace] = array(
00522                     'class' => 'EditWatchlistCheckboxSeriesField',
00523                     'options' => array(),
00524                     'section' => "ns$namespace",
00525                 );
00526             }
00527 
00528             foreach ( array_keys( $pages ) as $dbkey ) {
00529                 $title = Title::makeTitleSafe( $namespace, $dbkey );
00530 
00531                 if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
00532                     $text = $this->buildRemoveLine( $title );
00533                     $fields['TitlesNs' . $namespace]['options'][$text] = $title->getPrefixedText();
00534                     $count++;
00535                 }
00536             }
00537         }
00538         $this->cleanupWatchlist();
00539 
00540         if ( count( $fields ) > 1 && $count > 30 ) {
00541             $this->toc = Linker::tocIndent();
00542             $tocLength = 0;
00543 
00544             foreach ( $fields as $data ) {
00545                 # strip out the 'ns' prefix from the section name:
00546                 $ns = substr( $data['section'], 2 );
00547 
00548                 $nsText = ( $ns == NS_MAIN )
00549                     ? $this->msg( 'blanknamespace' )->escaped()
00550                     : htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
00551                 $this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
00552                     $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
00553             }
00554 
00555             $this->toc = Linker::tocList( $this->toc );
00556         } else {
00557             $this->toc = false;
00558         }
00559 
00560         $context = new DerivativeContext( $this->getContext() );
00561         $context->setTitle( $this->getPageTitle() ); // Remove subpage
00562         $form = new EditWatchlistNormalHTMLForm( $fields, $context );
00563         $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
00564         # Used message keys: 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
00565         $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
00566         $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
00567         $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
00568         $form->setSubmitCallback( array( $this, 'submitNormal' ) );
00569 
00570         return $form;
00571     }
00572 
00579     private function buildRemoveLine( $title ) {
00580         $link = Linker::link( $title );
00581 
00582         if ( $title->isRedirect() ) {
00583             // Linker already makes class mw-redirect, so this is redundant
00584             $link = '<span class="watchlistredir">' . $link . '</span>';
00585         }
00586 
00587         $tools[] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() );
00588 
00589         if ( $title->exists() ) {
00590             $tools[] = Linker::linkKnown(
00591                 $title,
00592                 $this->msg( 'history_short' )->escaped(),
00593                 array(),
00594                 array( 'action' => 'history' )
00595             );
00596         }
00597 
00598         if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
00599             $tools[] = Linker::linkKnown(
00600                 SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
00601                 $this->msg( 'contributions' )->escaped()
00602             );
00603         }
00604 
00605         wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $title->isRedirect(), $this->getSkin() ) );
00606 
00607         return $link . " (" . $this->getLanguage()->pipeList( $tools ) . ")";
00608     }
00609 
00615     protected function getRawForm() {
00616         $titles = implode( $this->getWatchlist(), "\n" );
00617         $fields = array(
00618             'Titles' => array(
00619                 'type' => 'textarea',
00620                 'label-message' => 'watchlistedit-raw-titles',
00621                 'default' => $titles,
00622             ),
00623         );
00624         $context = new DerivativeContext( $this->getContext() );
00625         $context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
00626         $form = new HTMLForm( $fields, $context );
00627         $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
00628         # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
00629         $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
00630         $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
00631         $form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
00632         $form->setSubmitCallback( array( $this, 'submitRaw' ) );
00633 
00634         return $form;
00635     }
00636 
00642     protected function getClearForm() {
00643         $context = new DerivativeContext( $this->getContext() );
00644         $context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
00645         $form = new HTMLForm( array(), $context );
00646         $form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
00647         # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
00648         $form->setSubmitTooltip( 'watchlistedit-clear-submit' );
00649         $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
00650         $form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
00651         $form->setSubmitCallback( array( $this, 'submitClear' ) );
00652 
00653         return $form;
00654     }
00655 
00664     public static function getMode( $request, $par ) {
00665         $mode = strtolower( $request->getVal( 'action', $par ) );
00666 
00667         switch ( $mode ) {
00668             case 'clear':
00669             case self::EDIT_CLEAR:
00670                 return self::EDIT_CLEAR;
00671             case 'raw':
00672             case self::EDIT_RAW:
00673                 return self::EDIT_RAW;
00674             case 'edit':
00675             case self::EDIT_NORMAL:
00676                 return self::EDIT_NORMAL;
00677             default:
00678                 return false;
00679         }
00680     }
00681 
00689     public static function buildTools( $unused ) {
00690         global $wgLang;
00691 
00692         $tools = array();
00693         $modes = array(
00694             'view' => array( 'Watchlist', false ),
00695             'edit' => array( 'EditWatchlist', false ),
00696             'raw' => array( 'EditWatchlist', 'raw' ),
00697             'clear' => array( 'EditWatchlist', 'clear' ),
00698         );
00699 
00700         foreach ( $modes as $mode => $arr ) {
00701             // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
00702             $tools[] = Linker::linkKnown(
00703                 SpecialPage::getTitleFor( $arr[0], $arr[1] ),
00704                 wfMessage( "watchlisttools-{$mode}" )->escaped()
00705             );
00706         }
00707 
00708         return Html::rawElement(
00709             'span',
00710             array( 'class' => 'mw-watchlist-toollinks' ),
00711             wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text()
00712         );
00713     }
00714 }
00715 
00716 # B/C since 1.18
00717 class WatchlistEditor extends SpecialEditWatchlist {
00718 }
00719 
00723 class EditWatchlistNormalHTMLForm extends HTMLForm {
00724     public function getLegend( $namespace ) {
00725         $namespace = substr( $namespace, 2 );
00726 
00727         return $namespace == NS_MAIN
00728             ? $this->msg( 'blanknamespace' )->escaped()
00729             : htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
00730     }
00731 
00732     public function getBody() {
00733         return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
00734     }
00735 }
00736 
00737 class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
00749     function validate( $value, $alldata ) {
00750         // Need to call into grandparent to be a good citizen. :)
00751         return HTMLFormField::validate( $value, $alldata );
00752     }
00753 }