MediaWiki
REL1_24
|
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 }