MediaWiki  REL1_22
SpecialEditWatchlist.php
Go to the documentation of this file.
00001 <?php
00037 class SpecialEditWatchlist extends UnlistedSpecialPage {
00041     const EDIT_CLEAR = 1;
00042     const EDIT_RAW = 2;
00043     const EDIT_NORMAL = 3;
00044 
00045     protected $successMessage;
00046 
00047     protected $toc;
00048 
00049     private $badItems = array();
00050 
00051     public function __construct() {
00052         parent::__construct( 'EditWatchlist', 'editmywatchlist' );
00053     }
00054 
00060     public function execute( $mode ) {
00061         $this->setHeaders();
00062 
00063         $out = $this->getOutput();
00064 
00065         # Anons don't get a watchlist
00066         if ( $this->getUser()->isAnon() ) {
00067             $out->setPageTitle( $this->msg( 'watchnologin' ) );
00068             $llink = Linker::linkKnown(
00069                 SpecialPage::getTitleFor( 'Userlogin' ),
00070                 $this->msg( 'loginreqlink' )->escaped(),
00071                 array(),
00072                 array( 'returnto' => $this->getTitle()->getPrefixedText() )
00073             );
00074             $out->addHTML( $this->msg( 'watchlistanontext' )->rawParams( $llink )->parse() );
00075 
00076             return;
00077         }
00078 
00079         $this->checkPermissions();
00080         $this->checkReadOnly();
00081 
00082         $this->outputHeader();
00083 
00084         $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
00085             ->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
00086 
00087         # B/C: $mode used to be waaay down the parameter list, and the first parameter
00088         # was $wgUser
00089         if ( $mode instanceof User ) {
00090             $args = func_get_args();
00091             if ( count( $args ) >= 4 ) {
00092                 $mode = $args[3];
00093             }
00094         }
00095         $mode = self::getMode( $this->getRequest(), $mode );
00096 
00097         switch ( $mode ) {
00098             case self::EDIT_CLEAR:
00099                 // The "Clear" link scared people too much.
00100                 // Pass on to the raw editor, from which it's very easy to clear.
00101 
00102             case self::EDIT_RAW:
00103                 $out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
00104                 $form = $this->getRawForm();
00105                 if ( $form->show() ) {
00106                     $out->addHTML( $this->successMessage );
00107                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00108                 }
00109                 break;
00110 
00111             case self::EDIT_NORMAL:
00112             default:
00113                 $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
00114                 $form = $this->getNormalForm();
00115                 if ( $form->show() ) {
00116                     $out->addHTML( $this->successMessage );
00117                     $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
00118                 } elseif ( $this->toc !== false ) {
00119                     $out->prependHTML( $this->toc );
00120                 }
00121                 break;
00122         }
00123     }
00124 
00132     private function extractTitles( $list ) {
00133         $list = explode( "\n", trim( $list ) );
00134         if ( !is_array( $list ) ) {
00135             return array();
00136         }
00137 
00138         $titles = array();
00139 
00140         foreach ( $list as $text ) {
00141             $text = trim( $text );
00142             if ( strlen( $text ) > 0 ) {
00143                 $title = Title::newFromText( $text );
00144                 if ( $title instanceof Title && $title->isWatchable() ) {
00145                     $titles[] = $title;
00146                 }
00147             }
00148         }
00149 
00150         GenderCache::singleton()->doTitlesArray( $titles );
00151 
00152         $list = array();
00154         foreach ( $titles as $title ) {
00155             $list[] = $title->getPrefixedText();
00156         }
00157 
00158         return array_unique( $list );
00159     }
00160 
00161     public function submitRaw( $data ) {
00162         $wanted = $this->extractTitles( $data['Titles'] );
00163         $current = $this->getWatchlist();
00164 
00165         if ( count( $wanted ) > 0 ) {
00166             $toWatch = array_diff( $wanted, $current );
00167             $toUnwatch = array_diff( $current, $wanted );
00168             $this->watchTitles( $toWatch );
00169             $this->unwatchTitles( $toUnwatch );
00170             $this->getUser()->invalidateCache();
00171 
00172             if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
00173                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00174             } else {
00175                 return false;
00176             }
00177 
00178             if ( count( $toWatch ) > 0 ) {
00179                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added'
00180                 )->numParams( count( $toWatch ) )->parse();
00181                 $this->showTitles( $toWatch, $this->successMessage );
00182             }
00183 
00184             if ( count( $toUnwatch ) > 0 ) {
00185                 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed'
00186                 )->numParams( count( $toUnwatch ) )->parse();
00187                 $this->showTitles( $toUnwatch, $this->successMessage );
00188             }
00189         } else {
00190             $this->clearWatchlist();
00191             $this->getUser()->invalidateCache();
00192 
00193             if ( count( $current ) > 0 ) {
00194                 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
00195             } else {
00196                 return false;
00197             }
00198 
00199             $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
00200                 ->numParams( count( $current ) )->parse();
00201             $this->showTitles( $current, $this->successMessage );
00202         }
00203 
00204         return true;
00205     }
00206 
00216     private function showTitles( $titles, &$output ) {
00217         $talk = $this->msg( 'talkpagelinktext' )->escaped();
00218         // Do a batch existence check
00219         $batch = new LinkBatch();
00220         foreach ( $titles as $title ) {
00221             if ( !$title instanceof Title ) {
00222                 $title = Title::newFromText( $title );
00223             }
00224 
00225             if ( $title instanceof Title ) {
00226                 $batch->addObj( $title );
00227                 $batch->addObj( $title->getTalkPage() );
00228             }
00229         }
00230 
00231         $batch->execute();
00232 
00233         // Print out the list
00234         $output .= "<ul>\n";
00235 
00236         foreach ( $titles as $title ) {
00237             if ( !$title instanceof Title ) {
00238                 $title = Title::newFromText( $title );
00239             }
00240 
00241             if ( $title instanceof Title ) {
00242                 $output .= "<li>"
00243                     . Linker::link( $title )
00244                     . ' (' . Linker::link( $title->getTalkPage(), $talk )
00245                     . ")</li>\n";
00246             }
00247         }
00248 
00249         $output .= "</ul>\n";
00250     }
00251 
00258     private function getWatchlist() {
00259         $list = array();
00260         $dbr = wfGetDB( DB_MASTER );
00261 
00262         $res = $dbr->select(
00263             'watchlist',
00264             array(
00265                 'wl_namespace', 'wl_title'
00266             ), array(
00267                 'wl_user' => $this->getUser()->getId(),
00268             ),
00269             __METHOD__
00270         );
00271 
00272         if ( $res->numRows() > 0 ) {
00273             $titles = array();
00274             foreach ( $res as $row ) {
00275                 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
00276 
00277                 if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title )
00278                     && !$title->isTalkPage()
00279                 ) {
00280                     $titles[] = $title;
00281                 }
00282             }
00283             $res->free();
00284 
00285             GenderCache::singleton()->doTitlesArray( $titles );
00286 
00287             foreach ( $titles as $title ) {
00288                 $list[] = $title->getPrefixedText();
00289             }
00290         }
00291 
00292         $this->cleanupWatchlist();
00293 
00294         return $list;
00295     }
00296 
00303     private function getWatchlistInfo() {
00304         $titles = array();
00305         $dbr = wfGetDB( DB_MASTER );
00306 
00307         $res = $dbr->select(
00308             array( 'watchlist' ),
00309             array( 'wl_namespace', 'wl_title' ),
00310             array( 'wl_user' => $this->getUser()->getId() ),
00311             __METHOD__,
00312             array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) )
00313         );
00314 
00315         $lb = new LinkBatch();
00316 
00317         foreach ( $res as $row ) {
00318             $lb->add( $row->wl_namespace, $row->wl_title );
00319             if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
00320                 $titles[$row->wl_namespace][$row->wl_title] = 1;
00321             }
00322         }
00323 
00324         $lb->execute();
00325 
00326         return $titles;
00327     }
00328 
00337     private function checkTitle( $title, $namespace, $dbKey ) {
00338         if ( $title
00339             && ( $title->isExternal()
00340                 || $title->getNamespace() < 0
00341             )
00342         ) {
00343             $title = false; // unrecoverable
00344         }
00345 
00346         if ( !$title
00347             || $title->getNamespace() != $namespace
00348             || $title->getDBkey() != $dbKey
00349         ) {
00350             $this->badItems[] = array( $title, $namespace, $dbKey );
00351         }
00352 
00353         return (bool)$title;
00354     }
00355 
00359     private function cleanupWatchlist() {
00360         if ( !count( $this->badItems ) ) {
00361             return; //nothing to do
00362         }
00363 
00364         $dbw = wfGetDB( DB_MASTER );
00365         $user = $this->getUser();
00366 
00367         foreach ( $this->badItems as $row ) {
00368             list( $title, $namespace, $dbKey ) = $row;
00369             $action = $title ? 'cleaning up' : 'deleting';
00370             wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
00371 
00372             $dbw->delete( 'watchlist',
00373                 array(
00374                     'wl_user' => $user->getId(),
00375                     'wl_namespace' => $namespace,
00376                     'wl_title' => $dbKey,
00377                 ),
00378                 __METHOD__
00379             );
00380 
00381             // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
00382             if ( $title ) {
00383                 $user->addWatch( $title );
00384             }
00385         }
00386     }
00387 
00391     private function clearWatchlist() {
00392         $dbw = wfGetDB( DB_MASTER );
00393         $dbw->delete(
00394             'watchlist',
00395             array( 'wl_user' => $this->getUser()->getId() ),
00396             __METHOD__
00397         );
00398     }
00399 
00408     private function watchTitles( $titles ) {
00409         $dbw = wfGetDB( DB_MASTER );
00410         $rows = array();
00411 
00412         foreach ( $titles as $title ) {
00413             if ( !$title instanceof Title ) {
00414                 $title = Title::newFromText( $title );
00415             }
00416 
00417             if ( $title instanceof Title ) {
00418                 $rows[] = array(
00419                     'wl_user' => $this->getUser()->getId(),
00420                     'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00421                     'wl_title' => $title->getDBkey(),
00422                     'wl_notificationtimestamp' => null,
00423                 );
00424                 $rows[] = array(
00425                     'wl_user' => $this->getUser()->getId(),
00426                     'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00427                     'wl_title' => $title->getDBkey(),
00428                     'wl_notificationtimestamp' => null,
00429                 );
00430             }
00431         }
00432 
00433         $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
00434     }
00435 
00444     private function unwatchTitles( $titles ) {
00445         $dbw = wfGetDB( DB_MASTER );
00446 
00447         foreach ( $titles as $title ) {
00448             if ( !$title instanceof Title ) {
00449                 $title = Title::newFromText( $title );
00450             }
00451 
00452             if ( $title instanceof Title ) {
00453                 $dbw->delete(
00454                     'watchlist',
00455                     array(
00456                         'wl_user' => $this->getUser()->getId(),
00457                         'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ),
00458                         'wl_title' => $title->getDBkey(),
00459                     ),
00460                     __METHOD__
00461                 );
00462 
00463                 $dbw->delete(
00464                     'watchlist',
00465                     array(
00466                         'wl_user' => $this->getUser()->getId(),
00467                         'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ),
00468                         'wl_title' => $title->getDBkey(),
00469                     ),
00470                     __METHOD__
00471                 );
00472 
00473                 $page = WikiPage::factory( $title );
00474                 wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) );
00475             }
00476         }
00477     }
00478 
00479     public function submitNormal( $data ) {
00480         $removed = array();
00481 
00482         foreach ( $data as $titles ) {
00483             $this->unwatchTitles( $titles );
00484             $removed = array_merge( $removed, $titles );
00485         }
00486 
00487         if ( count( $removed ) > 0 ) {
00488             $this->successMessage = $this->msg( 'watchlistedit-normal-done'
00489             )->numParams( count( $removed ) )->parse();
00490             $this->showTitles( $removed, $this->successMessage );
00491 
00492             return true;
00493         } else {
00494             return false;
00495         }
00496     }
00497 
00503     protected function getNormalForm() {
00504         global $wgContLang;
00505 
00506         $fields = array();
00507         $count = 0;
00508 
00509         foreach ( $this->getWatchlistInfo() as $namespace => $pages ) {
00510             if ( $namespace >= 0 ) {
00511                 $fields['TitlesNs' . $namespace] = array(
00512                     'class' => 'EditWatchlistCheckboxSeriesField',
00513                     'options' => array(),
00514                     'section' => "ns$namespace",
00515                 );
00516             }
00517 
00518             foreach ( array_keys( $pages ) as $dbkey ) {
00519                 $title = Title::makeTitleSafe( $namespace, $dbkey );
00520 
00521                 if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
00522                     $text = $this->buildRemoveLine( $title );
00523                     $fields['TitlesNs' . $namespace]['options'][$text] = $title->getPrefixedText();
00524                     $count++;
00525                 }
00526             }
00527         }
00528         $this->cleanupWatchlist();
00529 
00530         if ( count( $fields ) > 1 && $count > 30 ) {
00531             $this->toc = Linker::tocIndent();
00532             $tocLength = 0;
00533 
00534             foreach ( $fields as $data ) {
00535                 # strip out the 'ns' prefix from the section name:
00536                 $ns = substr( $data['section'], 2 );
00537 
00538                 $nsText = ( $ns == NS_MAIN )
00539                     ? $this->msg( 'blanknamespace' )->escaped()
00540                     : htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) );
00541                 $this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
00542                     $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
00543             }
00544 
00545             $this->toc = Linker::tocList( $this->toc );
00546         } else {
00547             $this->toc = false;
00548         }
00549 
00550         $context = new DerivativeContext( $this->getContext() );
00551         $context->setTitle( $this->getTitle() ); // Remove subpage
00552         $form = new EditWatchlistNormalHTMLForm( $fields, $context );
00553         $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
00554         # Used message keys: 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
00555         $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
00556         $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
00557         $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
00558         $form->setSubmitCallback( array( $this, 'submitNormal' ) );
00559 
00560         return $form;
00561     }
00562 
00569     private function buildRemoveLine( $title ) {
00570         $link = Linker::link( $title );
00571 
00572         if ( $title->isRedirect() ) {
00573             // Linker already makes class mw-redirect, so this is redundant
00574             $link = '<span class="watchlistredir">' . $link . '</span>';
00575         }
00576 
00577         $tools[] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() );
00578 
00579         if ( $title->exists() ) {
00580             $tools[] = Linker::linkKnown(
00581                 $title,
00582                 $this->msg( 'history_short' )->escaped(),
00583                 array(),
00584                 array( 'action' => 'history' )
00585             );
00586         }
00587 
00588         if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
00589             $tools[] = Linker::linkKnown(
00590                 SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
00591                 $this->msg( 'contributions' )->escaped()
00592             );
00593         }
00594 
00595         wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $title->isRedirect(), $this->getSkin() ) );
00596 
00597         return $link . " (" . $this->getLanguage()->pipeList( $tools ) . ")";
00598     }
00599 
00605     protected function getRawForm() {
00606         $titles = implode( $this->getWatchlist(), "\n" );
00607         $fields = array(
00608             'Titles' => array(
00609                 'type' => 'textarea',
00610                 'label-message' => 'watchlistedit-raw-titles',
00611                 'default' => $titles,
00612             ),
00613         );
00614         $context = new DerivativeContext( $this->getContext() );
00615         $context->setTitle( $this->getTitle( 'raw' ) ); // Reset subpage
00616         $form = new HTMLForm( $fields, $context );
00617         $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
00618         # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
00619         $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
00620         $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
00621         $form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
00622         $form->setSubmitCallback( array( $this, 'submitRaw' ) );
00623 
00624         return $form;
00625     }
00626 
00635     public static function getMode( $request, $par ) {
00636         $mode = strtolower( $request->getVal( 'action', $par ) );
00637 
00638         switch ( $mode ) {
00639             case 'clear':
00640             case self::EDIT_CLEAR:
00641                 return self::EDIT_CLEAR;
00642             case 'raw':
00643             case self::EDIT_RAW:
00644                 return self::EDIT_RAW;
00645             case 'edit':
00646             case self::EDIT_NORMAL:
00647                 return self::EDIT_NORMAL;
00648             default:
00649                 return false;
00650         }
00651     }
00652 
00660     public static function buildTools( $unused ) {
00661         global $wgLang;
00662 
00663         $tools = array();
00664         $modes = array(
00665             'view' => array( 'Watchlist', false ),
00666             'edit' => array( 'EditWatchlist', false ),
00667             'raw' => array( 'EditWatchlist', 'raw' ),
00668         );
00669 
00670         foreach ( $modes as $mode => $arr ) {
00671             // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
00672             $tools[] = Linker::linkKnown(
00673                 SpecialPage::getTitleFor( $arr[0], $arr[1] ),
00674                 wfMessage( "watchlisttools-{$mode}" )->escaped()
00675             );
00676         }
00677 
00678         return Html::rawElement(
00679             'span',
00680             array( 'class' => 'mw-watchlist-toollinks' ),
00681             wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text()
00682         );
00683     }
00684 }
00685 
00686 # B/C since 1.18
00687 class WatchlistEditor extends SpecialEditWatchlist {
00688 }
00689 
00693 class EditWatchlistNormalHTMLForm extends HTMLForm {
00694     public function getLegend( $namespace ) {
00695         $namespace = substr( $namespace, 2 );
00696 
00697         return $namespace == NS_MAIN
00698             ? $this->msg( 'blanknamespace' )->escaped()
00699             : htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
00700     }
00701 
00702     public function getBody() {
00703         return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
00704     }
00705 }
00706 
00707 class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
00719     function validate( $value, $alldata ) {
00720         // Need to call into grandparent to be a good citizen. :)
00721         return HTMLFormField::validate( $value, $alldata );
00722     }
00723 }