MediaWiki
REL1_21
|
00001 <?php 00037 class SpecialEditWatchlist extends UnlistedSpecialPage { 00038 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' ); 00054 } 00055 00061 public function execute( $mode ) { 00062 $this->setHeaders(); 00063 00064 $out = $this->getOutput(); 00065 00066 # Anons don't get a watchlist 00067 if( $this->getUser()->isAnon() ) { 00068 $out->setPageTitle( $this->msg( 'watchnologin' ) ); 00069 $llink = Linker::linkKnown( 00070 SpecialPage::getTitleFor( 'Userlogin' ), 00071 $this->msg( 'loginreqlink' )->escaped(), 00072 array(), 00073 array( 'returnto' => $this->getTitle()->getPrefixedText() ) 00074 ); 00075 $out->addHTML( $this->msg( 'watchlistanontext' )->rawParams( $llink )->parse() ); 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 $titles = array(); 00138 foreach( $list as $text ) { 00139 $text = trim( $text ); 00140 if( strlen( $text ) > 0 ) { 00141 $title = Title::newFromText( $text ); 00142 if( $title instanceof Title && $title->isWatchable() ) { 00143 $titles[] = $title; 00144 } 00145 } 00146 } 00147 00148 GenderCache::singleton()->doTitlesArray( $titles ); 00149 00150 $list = array(); 00151 foreach( $titles as $title ) { 00152 $list[] = $title->getPrefixedText(); 00153 } 00154 return array_unique( $list ); 00155 } 00156 00157 public function submitRaw( $data ) { 00158 $wanted = $this->extractTitles( $data['Titles'] ); 00159 $current = $this->getWatchlist(); 00160 00161 if( count( $wanted ) > 0 ) { 00162 $toWatch = array_diff( $wanted, $current ); 00163 $toUnwatch = array_diff( $current, $wanted ); 00164 $this->watchTitles( $toWatch ); 00165 $this->unwatchTitles( $toUnwatch ); 00166 $this->getUser()->invalidateCache(); 00167 00168 if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) { 00169 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse(); 00170 } else { 00171 return false; 00172 } 00173 00174 if( count( $toWatch ) > 0 ) { 00175 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' 00176 )->numParams( count( $toWatch ) )->parse(); 00177 $this->showTitles( $toWatch, $this->successMessage ); 00178 } 00179 00180 if( count( $toUnwatch ) > 0 ) { 00181 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' 00182 )->numParams( count( $toUnwatch ) )->parse(); 00183 $this->showTitles( $toUnwatch, $this->successMessage ); 00184 } 00185 } else { 00186 $this->clearWatchlist(); 00187 $this->getUser()->invalidateCache(); 00188 00189 if( count( $current ) > 0 ) { 00190 $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse(); 00191 } else { 00192 return false; 00193 } 00194 00195 $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' 00196 )->numParams( count( $current ) )->parse(); 00197 $this->showTitles( $current, $this->successMessage ); 00198 } 00199 return true; 00200 } 00201 00211 private function showTitles( $titles, &$output ) { 00212 $talk = $this->msg( 'talkpagelinktext' )->escaped(); 00213 // Do a batch existence check 00214 $batch = new LinkBatch(); 00215 foreach( $titles as $title ) { 00216 if( !$title instanceof Title ) { 00217 $title = Title::newFromText( $title ); 00218 } 00219 if( $title instanceof Title ) { 00220 $batch->addObj( $title ); 00221 $batch->addObj( $title->getTalkPage() ); 00222 } 00223 } 00224 $batch->execute(); 00225 // Print out the list 00226 $output .= "<ul>\n"; 00227 foreach( $titles as $title ) { 00228 if( !$title instanceof Title ) { 00229 $title = Title::newFromText( $title ); 00230 } 00231 if( $title instanceof Title ) { 00232 $output .= "<li>" 00233 . Linker::link( $title ) 00234 . ' (' . Linker::link( $title->getTalkPage(), $talk ) 00235 . ")</li>\n"; 00236 } 00237 } 00238 $output .= "</ul>\n"; 00239 } 00240 00247 private function getWatchlist() { 00248 $list = array(); 00249 $dbr = wfGetDB( DB_MASTER ); 00250 $res = $dbr->select( 00251 'watchlist', 00252 array( 00253 'wl_namespace', 'wl_title' 00254 ), array( 00255 'wl_user' => $this->getUser()->getId(), 00256 ), 00257 __METHOD__ 00258 ); 00259 if( $res->numRows() > 0 ) { 00260 $titles = array(); 00261 foreach ( $res as $row ) { 00262 $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ); 00263 if ( $this->checkTitle( $title, $row->wl_namespace, $row->wl_title ) 00264 && !$title->isTalkPage() 00265 ) { 00266 $titles[] = $title; 00267 } 00268 } 00269 $res->free(); 00270 00271 GenderCache::singleton()->doTitlesArray( $titles ); 00272 00273 foreach( $titles as $title ) { 00274 $list[] = $title->getPrefixedText(); 00275 } 00276 } 00277 $this->cleanupWatchlist(); 00278 return $list; 00279 } 00280 00287 private function getWatchlistInfo() { 00288 $titles = array(); 00289 $dbr = wfGetDB( DB_MASTER ); 00290 00291 $res = $dbr->select( 00292 array( 'watchlist' ), 00293 array( 'wl_namespace', 'wl_title' ), 00294 array( 'wl_user' => $this->getUser()->getId() ), 00295 __METHOD__, 00296 array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' ) ) 00297 ); 00298 00299 $lb = new LinkBatch(); 00300 foreach ( $res as $row ) { 00301 $lb->add( $row->wl_namespace, $row->wl_title ); 00302 if ( !MWNamespace::isTalk( $row->wl_namespace ) ) { 00303 $titles[$row->wl_namespace][$row->wl_title] = 1; 00304 } 00305 } 00306 00307 $lb->execute(); 00308 return $titles; 00309 } 00310 00319 private function checkTitle( $title, $namespace, $dbKey ) { 00320 if ( $title 00321 && ( $title->isExternal() 00322 || $title->getNamespace() < 0 00323 ) 00324 ) { 00325 $title = false; // unrecoverable 00326 } 00327 if ( !$title 00328 || $title->getNamespace() != $namespace 00329 || $title->getDBkey() != $dbKey 00330 ) { 00331 $this->badItems[] = array( $title, $namespace, $dbKey ); 00332 } 00333 return (bool)$title; 00334 } 00335 00339 private function cleanupWatchlist() { 00340 if( !count( $this->badItems ) ) { 00341 return; //nothing to do 00342 } 00343 $dbw = wfGetDB( DB_MASTER ); 00344 $user = $this->getUser(); 00345 foreach ( $this->badItems as $row ) { 00346 list( $title, $namespace, $dbKey ) = $row; 00347 wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, " 00348 . ( $title ? 'cleaning up' : 'deleting' ) . ".\n" 00349 ); 00350 00351 $dbw->delete( 'watchlist', 00352 array( 00353 'wl_user' => $user->getId(), 00354 'wl_namespace' => $namespace, 00355 'wl_title' => $dbKey, 00356 ), 00357 __METHOD__ 00358 ); 00359 00360 // Can't just do an UPDATE instead of DELETE/INSERT due to unique index 00361 if ( $title ) { 00362 $user->addWatch( $title ); 00363 } 00364 } 00365 } 00366 00370 private function clearWatchlist() { 00371 $dbw = wfGetDB( DB_MASTER ); 00372 $dbw->delete( 00373 'watchlist', 00374 array( 'wl_user' => $this->getUser()->getId() ), 00375 __METHOD__ 00376 ); 00377 } 00378 00387 private function watchTitles( $titles ) { 00388 $dbw = wfGetDB( DB_MASTER ); 00389 $rows = array(); 00390 foreach( $titles as $title ) { 00391 if( !$title instanceof Title ) { 00392 $title = Title::newFromText( $title ); 00393 } 00394 if( $title instanceof Title ) { 00395 $rows[] = array( 00396 'wl_user' => $this->getUser()->getId(), 00397 'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ), 00398 'wl_title' => $title->getDBkey(), 00399 'wl_notificationtimestamp' => null, 00400 ); 00401 $rows[] = array( 00402 'wl_user' => $this->getUser()->getId(), 00403 'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ), 00404 'wl_title' => $title->getDBkey(), 00405 'wl_notificationtimestamp' => null, 00406 ); 00407 } 00408 } 00409 $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' ); 00410 } 00411 00420 private function unwatchTitles( $titles ) { 00421 $dbw = wfGetDB( DB_MASTER ); 00422 foreach( $titles as $title ) { 00423 if( !$title instanceof Title ) { 00424 $title = Title::newFromText( $title ); 00425 } 00426 if( $title instanceof Title ) { 00427 $dbw->delete( 00428 'watchlist', 00429 array( 00430 'wl_user' => $this->getUser()->getId(), 00431 'wl_namespace' => MWNamespace::getSubject( $title->getNamespace() ), 00432 'wl_title' => $title->getDBkey(), 00433 ), 00434 __METHOD__ 00435 ); 00436 $dbw->delete( 00437 'watchlist', 00438 array( 00439 'wl_user' => $this->getUser()->getId(), 00440 'wl_namespace' => MWNamespace::getTalk( $title->getNamespace() ), 00441 'wl_title' => $title->getDBkey(), 00442 ), 00443 __METHOD__ 00444 ); 00445 $page = WikiPage::factory( $title ); 00446 wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) ); 00447 } 00448 } 00449 } 00450 00451 public function submitNormal( $data ) { 00452 $removed = array(); 00453 00454 foreach( $data as $titles ) { 00455 $this->unwatchTitles( $titles ); 00456 $removed = array_merge( $removed, $titles ); 00457 } 00458 00459 if( count( $removed ) > 0 ) { 00460 $this->successMessage = $this->msg( 'watchlistedit-normal-done' 00461 )->numParams( count( $removed ) )->parse(); 00462 $this->showTitles( $removed, $this->successMessage ); 00463 return true; 00464 } else { 00465 return false; 00466 } 00467 } 00468 00474 protected function getNormalForm() { 00475 global $wgContLang; 00476 00477 $fields = array(); 00478 $count = 0; 00479 00480 foreach( $this->getWatchlistInfo() as $namespace => $pages ) { 00481 if ( $namespace >= 0 ) { 00482 $fields['TitlesNs' . $namespace] = array( 00483 'class' => 'EditWatchlistCheckboxSeriesField', 00484 'options' => array(), 00485 'section' => "ns$namespace", 00486 ); 00487 } 00488 00489 foreach( array_keys( $pages ) as $dbkey ) { 00490 $title = Title::makeTitleSafe( $namespace, $dbkey ); 00491 if ( $this->checkTitle( $title, $namespace, $dbkey ) ) { 00492 $text = $this->buildRemoveLine( $title ); 00493 $fields['TitlesNs' . $namespace]['options'][$text] = $title->getPrefixedText(); 00494 $count++; 00495 } 00496 } 00497 } 00498 $this->cleanupWatchlist(); 00499 00500 if ( count( $fields ) > 1 && $count > 30 ) { 00501 $this->toc = Linker::tocIndent(); 00502 $tocLength = 0; 00503 foreach( $fields as $data ) { 00504 00505 # strip out the 'ns' prefix from the section name: 00506 $ns = substr( $data['section'], 2 ); 00507 00508 $nsText = ($ns == NS_MAIN) 00509 ? $this->msg( 'blanknamespace' )->escaped() 00510 : htmlspecialchars( $wgContLang->getFormattedNsText( $ns ) ); 00511 $this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText, 00512 $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd(); 00513 } 00514 $this->toc = Linker::tocList( $this->toc ); 00515 } else { 00516 $this->toc = false; 00517 } 00518 00519 $form = new EditWatchlistNormalHTMLForm( $fields, $this->getContext() ); 00520 $form->setTitle( $this->getTitle() ); 00521 $form->setSubmitTextMsg( 'watchlistedit-normal-submit' ); 00522 # Used message keys: 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit' 00523 $form->setSubmitTooltip( 'watchlistedit-normal-submit' ); 00524 $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' ); 00525 $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() ); 00526 $form->setSubmitCallback( array( $this, 'submitNormal' ) ); 00527 return $form; 00528 } 00529 00536 private function buildRemoveLine( $title ) { 00537 $link = Linker::link( $title ); 00538 if( $title->isRedirect() ) { 00539 // Linker already makes class mw-redirect, so this is redundant 00540 $link = '<span class="watchlistredir">' . $link . '</span>'; 00541 } 00542 $tools[] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() ); 00543 if( $title->exists() ) { 00544 $tools[] = Linker::linkKnown( 00545 $title, 00546 $this->msg( 'history_short' )->escaped(), 00547 array(), 00548 array( 'action' => 'history' ) 00549 ); 00550 } 00551 if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) { 00552 $tools[] = Linker::linkKnown( 00553 SpecialPage::getTitleFor( 'Contributions', $title->getText() ), 00554 $this->msg( 'contributions' )->escaped() 00555 ); 00556 } 00557 00558 wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $title->isRedirect(), $this->getSkin() ) ); 00559 00560 return $link . " (" . $this->getLanguage()->pipeList( $tools ) . ")"; 00561 } 00562 00568 protected function getRawForm() { 00569 $titles = implode( $this->getWatchlist(), "\n" ); 00570 $fields = array( 00571 'Titles' => array( 00572 'type' => 'textarea', 00573 'label-message' => 'watchlistedit-raw-titles', 00574 'default' => $titles, 00575 ), 00576 ); 00577 $form = new HTMLForm( $fields, $this->getContext() ); 00578 $form->setTitle( $this->getTitle( 'raw' ) ); 00579 $form->setSubmitTextMsg( 'watchlistedit-raw-submit' ); 00580 # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit' 00581 $form->setSubmitTooltip( 'watchlistedit-raw-submit' ); 00582 $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' ); 00583 $form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() ); 00584 $form->setSubmitCallback( array( $this, 'submitRaw' ) ); 00585 return $form; 00586 } 00587 00596 public static function getMode( $request, $par ) { 00597 $mode = strtolower( $request->getVal( 'action', $par ) ); 00598 switch( $mode ) { 00599 case 'clear': 00600 case self::EDIT_CLEAR: 00601 return self::EDIT_CLEAR; 00602 00603 case 'raw': 00604 case self::EDIT_RAW: 00605 return self::EDIT_RAW; 00606 00607 case 'edit': 00608 case self::EDIT_NORMAL: 00609 return self::EDIT_NORMAL; 00610 00611 default: 00612 return false; 00613 } 00614 } 00615 00623 public static function buildTools( $unused ) { 00624 global $wgLang; 00625 00626 $tools = array(); 00627 $modes = array( 00628 'view' => array( 'Watchlist', false ), 00629 'edit' => array( 'EditWatchlist', false ), 00630 'raw' => array( 'EditWatchlist', 'raw' ), 00631 ); 00632 foreach( $modes as $mode => $arr ) { 00633 // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw' 00634 $tools[] = Linker::linkKnown( 00635 SpecialPage::getTitleFor( $arr[0], $arr[1] ), 00636 wfMessage( "watchlisttools-{$mode}" )->escaped() 00637 ); 00638 } 00639 return Html::rawElement( 'span', 00640 array( 'class' => 'mw-watchlist-toollinks' ), 00641 wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text() ); 00642 } 00643 } 00644 00645 # B/C since 1.18 00646 class WatchlistEditor extends SpecialEditWatchlist {} 00647 00651 class EditWatchlistNormalHTMLForm extends HTMLForm { 00652 public function getLegend( $namespace ) { 00653 $namespace = substr( $namespace, 2 ); 00654 return $namespace == NS_MAIN 00655 ? $this->msg( 'blanknamespace' )->escaped() 00656 : htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) ); 00657 } 00658 public function getBody() { 00659 return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' ); 00660 } 00661 } 00662 00663 class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField { 00675 function validate( $value, $alldata ) { 00676 // Need to call into grandparent to be a good citizen. :) 00677 return HTMLFormField::validate( $value, $alldata ); 00678 } 00679 }