MediaWiki  REL1_21
SpecialWatchlist.php
Go to the documentation of this file.
00001 <?php
00023 class SpecialWatchlist extends SpecialPage {
00024         protected $customFilters;
00025 
00029         public function __construct( $page = 'Watchlist' ) {
00030                 parent::__construct( $page );
00031         }
00032 
00037         function execute( $par ) {
00038                 global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
00039 
00040                 $user = $this->getUser();
00041                 $output = $this->getOutput();
00042 
00043                 # Anons don't get a watchlist
00044                 if( $user->isAnon() ) {
00045                         $output->setPageTitle( $this->msg( 'watchnologin' ) );
00046                         $output->setRobotPolicy( 'noindex,nofollow' );
00047                         $llink = Linker::linkKnown(
00048                                 SpecialPage::getTitleFor( 'Userlogin' ),
00049                                 $this->msg( 'loginreqlink' )->escaped(),
00050                                 array(),
00051                                 array( 'returnto' => $this->getTitle()->getPrefixedText() )
00052                         );
00053                         $output->addHTML( $this->msg( 'watchlistanontext' )->rawParams( $llink )->parse() );
00054                         return;
00055                 }
00056 
00057                 // Add feed links
00058                 $wlToken = $user->getOption( 'watchlisttoken' );
00059                 if ( !$wlToken ) {
00060                         $wlToken = MWCryptRand::generateHex( 40 );
00061                         $user->setOption( 'watchlisttoken', $wlToken );
00062                         $user->saveSettings();
00063                 }
00064 
00065                 $this->addFeedLinks( array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
00066                                                         'wlowner' => $user->getName(), 'wltoken' => $wlToken ) );
00067 
00068                 $this->setHeaders();
00069                 $this->outputHeader();
00070 
00071                 $output->addSubtitle( $this->msg( 'watchlistfor2', $user->getName()
00072                         )->rawParams( SpecialEditWatchlist::buildTools( null ) ) );
00073 
00074                 $request = $this->getRequest();
00075 
00076                 $mode = SpecialEditWatchlist::getMode( $request, $par );
00077                 if( $mode !== false ) {
00078                         # TODO: localise?
00079                         switch( $mode ) {
00080                                 case SpecialEditWatchlist::EDIT_CLEAR:
00081                                         $mode = 'clear';
00082                                         break;
00083                                 case SpecialEditWatchlist::EDIT_RAW:
00084                                         $mode = 'raw';
00085                                         break;
00086                                 default:
00087                                         $mode = null;
00088                         }
00089                         $title = SpecialPage::getTitleFor( 'EditWatchlist', $mode );
00090                         $output->redirect( $title->getLocalUrl() );
00091                         return;
00092                 }
00093 
00094                 $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
00095 
00096                 $nitems = $this->countItems( $dbr );
00097                 if ( $nitems == 0 ) {
00098                         $output->addWikiMsg( 'nowatchlist' );
00099                         return;
00100                 }
00101 
00102                 // @TODO: use FormOptions!
00103                 $defaults = array(
00104                 /* float */ 'days'      => floatval( $user->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
00105                 /* bool  */ 'hideMinor' => (int)$user->getBoolOption( 'watchlisthideminor' ),
00106                 /* bool  */ 'hideBots'  => (int)$user->getBoolOption( 'watchlisthidebots' ),
00107                 /* bool  */ 'hideAnons' => (int)$user->getBoolOption( 'watchlisthideanons' ),
00108                 /* bool  */ 'hideLiu'   => (int)$user->getBoolOption( 'watchlisthideliu' ),
00109                 /* bool  */ 'hidePatrolled' => (int)$user->getBoolOption( 'watchlisthidepatrolled' ),
00110                 /* bool  */ 'hideOwn'   => (int)$user->getBoolOption( 'watchlisthideown' ),
00111                 /* bool  */ 'extended'   => (int)$user->getBoolOption( 'extendwatchlist' ),
00112                 /* ?     */ 'namespace' => '', //means all
00113                 /* ?     */ 'invert'    => false,
00114                 /* bool  */ 'associated' => false,
00115                 );
00116                 $this->customFilters = array();
00117                 wfRunHooks( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ) );
00118                 foreach( $this->customFilters as $key => $params ) {
00119                         $defaults[$key] = $params['default'];
00120                 }
00121 
00122                 # Extract variables from the request, falling back to user preferences or
00123                 # other default values if these don't exist
00124                 $values = array();
00125                 $values['days'] = $request->getVal( 'days', $defaults['days'] );
00126                 $values['hideMinor'] = (int)$request->getBool( 'hideMinor', $defaults['hideMinor'] );
00127                 $values['hideBots'] = (int)$request->getBool( 'hideBots', $defaults['hideBots'] );
00128                 $values['hideAnons'] = (int)$request->getBool( 'hideAnons', $defaults['hideAnons'] );
00129                 $values['hideLiu'] = (int)$request->getBool( 'hideLiu', $defaults['hideLiu'] );
00130                 $values['hideOwn'] = (int)$request->getBool( 'hideOwn', $defaults['hideOwn'] );
00131                 $values['hidePatrolled'] = (int)$request->getBool( 'hidePatrolled', $defaults['hidePatrolled'] );
00132                 $values['extended'] = (int)$request->getBool( 'extended', $defaults['extended'] );
00133                 foreach( $this->customFilters as $key => $params ) {
00134                         $values[$key] = (int)$request->getBool( $key, $defaults[$key] );
00135                 }
00136 
00137                 # Get namespace value, if supplied, and prepare a WHERE fragment
00138                 $nameSpace = $request->getIntOrNull( 'namespace' );
00139                 $invert = $request->getBool( 'invert' );
00140                 $associated = $request->getBool( 'associated' );
00141                 if ( !is_null( $nameSpace ) ) {
00142                         $eq_op = $invert ? '!=' : '=';
00143                         $bool_op = $invert ? 'AND' : 'OR';
00144                         $nameSpace = intval( $nameSpace ); // paranioa
00145                         if ( !$associated ) {
00146                                 $nameSpaceClause = "rc_namespace $eq_op $nameSpace";
00147                         } else {
00148                                 $associatedNS = MWNamespace::getAssociated( $nameSpace );
00149                                 $nameSpaceClause =
00150                                         "rc_namespace $eq_op $nameSpace " .
00151                                         $bool_op .
00152                                         " rc_namespace $eq_op $associatedNS";
00153                         }
00154                 } else {
00155                         $nameSpace = '';
00156                         $nameSpaceClause = '';
00157                 }
00158                 $values['namespace'] = $nameSpace;
00159                 $values['invert'] = $invert;
00160                 $values['associated'] = $associated;
00161 
00162                 if( is_null( $values['days'] ) || !is_numeric( $values['days'] ) ) {
00163                         $big = 1000; /* The magical big */
00164                         if( $nitems > $big ) {
00165                                 # Set default cutoff shorter
00166                                 $values['days'] = $defaults['days'] = (12.0 / 24.0); # 12 hours...
00167                         } else {
00168                                 $values['days'] = $defaults['days']; # default cutoff for shortlisters
00169                         }
00170                 } else {
00171                         $values['days'] = floatval( $values['days'] );
00172                 }
00173 
00174                 // Dump everything here
00175                 $nondefaults = array();
00176                 foreach ( $defaults as $name => $defValue ) {
00177                         wfAppendToArrayIfNotDefault( $name, $values[$name], $defaults, $nondefaults );
00178                 }
00179 
00180                 if( ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) && $request->getVal( 'reset' ) &&
00181                         $request->wasPosted() )
00182                 {
00183                         $user->clearAllNotifications();
00184                         $output->redirect( $this->getTitle()->getFullUrl( $nondefaults ) );
00185                         return;
00186                 }
00187 
00188                 # Possible where conditions
00189                 $conds = array();
00190 
00191                 if( $values['days'] > 0 ) {
00192                         $conds[] = 'rc_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( time() - intval( $values['days'] * 86400 ) ) );
00193                 }
00194 
00195                 # If the watchlist is relatively short, it's simplest to zip
00196                 # down its entirety and then sort the results.
00197 
00198                 # If it's relatively long, it may be worth our while to zip
00199                 # through the time-sorted page list checking for watched items.
00200 
00201                 # Up estimate of watched items by 15% to compensate for talk pages...
00202 
00203                 # Toggles
00204                 if( $values['hideOwn'] ) {
00205                         $conds[] = 'rc_user != ' . $user->getId();
00206                 }
00207                 if( $values['hideBots'] ) {
00208                         $conds[] = 'rc_bot = 0';
00209                 }
00210                 if( $values['hideMinor'] ) {
00211                         $conds[] = 'rc_minor = 0';
00212                 }
00213                 if( $values['hideLiu'] ) {
00214                         $conds[] = 'rc_user = 0';
00215                 }
00216                 if( $values['hideAnons'] ) {
00217                         $conds[] = 'rc_user != 0';
00218                 }
00219                 if ( $user->useRCPatrol() && $values['hidePatrolled'] ) {
00220                         $conds[] = 'rc_patrolled != 1';
00221                 }
00222                 if ( $nameSpaceClause ) {
00223                         $conds[] = $nameSpaceClause;
00224                 }
00225 
00226                 # Toggle watchlist content (all recent edits or just the latest)
00227                 if( $values['extended'] ) {
00228                         $limitWatchlist = $user->getIntOption( 'wllimit' );
00229                         $usePage = false;
00230                 } else {
00231                         # Top log Ids for a page are not stored
00232                         $conds[] = 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG;
00233                         $limitWatchlist = 0;
00234                         $usePage = true;
00235                 }
00236 
00237                 # Show a message about slave lag, if applicable
00238                 $lag = wfGetLB()->safeGetLag( $dbr );
00239                 if( $lag > 0 ) {
00240                         $output->showLagWarning( $lag );
00241                 }
00242 
00243                 # Create output form
00244                 $form = Xml::fieldset( $this->msg( 'watchlist-options' )->text(), false, array( 'id' => 'mw-watchlist-options' ) );
00245 
00246                 # Show watchlist header
00247                 $form .= $this->msg( 'watchlist-details' )->numParams( $nitems )->parse() . "\n";
00248 
00249                 if( $user->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
00250                         $form .= $this->msg( 'wlheader-enotif' )->parseAsBlock() . "\n";
00251                 }
00252                 if( $wgShowUpdatedMarker ) {
00253                         $form .= Xml::openElement( 'form', array( 'method' => 'post',
00254                                                 'action' => $this->getTitle()->getLocalUrl(),
00255                                                 'id' => 'mw-watchlist-resetbutton' ) ) . "\n" .
00256                                         $this->msg( 'wlheader-showupdated' )->parse() .
00257                                         Xml::submitButton( $this->msg( 'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) . "\n" .
00258                                         Html::hidden( 'reset', 'all' ) . "\n";
00259                                         foreach ( $nondefaults as $key => $value ) {
00260                                                 $form .= Html::hidden( $key, $value ) . "\n";
00261                                         }
00262                                         $form .= Xml::closeElement( 'form' ) . "\n";
00263                 }
00264                 $form .= "<hr />\n";
00265 
00266                 $tables = array( 'recentchanges', 'watchlist' );
00267                 $fields = RecentChange::selectFields();
00268                 $join_conds = array(
00269                         'watchlist' => array(
00270                                 'INNER JOIN',
00271                                 array(
00272                                         'wl_user' => $user->getId(),
00273                                         'wl_namespace=rc_namespace',
00274                                         'wl_title=rc_title'
00275                                 ),
00276                         ),
00277                 );
00278                 $options = array( 'ORDER BY' => 'rc_timestamp DESC' );
00279                 if( $wgShowUpdatedMarker ) {
00280                         $fields[] = 'wl_notificationtimestamp';
00281                 }
00282                 if( $limitWatchlist ) {
00283                         $options['LIMIT'] = $limitWatchlist;
00284                 }
00285 
00286                 $rollbacker = $user->isAllowed( 'rollback' );
00287                 if ( $usePage || $rollbacker ) {
00288                         $tables[] = 'page';
00289                         $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
00290                         if ( $rollbacker ) {
00291                                 $fields[] = 'page_latest';
00292                         }
00293                 }
00294                 
00295                 // Log entries with DELETED_ACTION must not show up unless the user has
00296                 // the necessary rights.
00297                 if ( !$user->isAllowed( 'deletedhistory' ) ) {
00298                         $bitmask = LogPage::DELETED_ACTION;
00299                 } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
00300                         $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
00301                 } else {
00302                         $bitmask = 0;
00303                 }
00304                 if ( $bitmask ) {
00305                         $conds[] = $dbr->makeList( array(
00306                                 'rc_type != ' . RC_LOG,
00307                                 $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
00308                         ), LIST_OR );
00309                 }
00310 
00311 
00312                 ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
00313                 wfRunHooks( 'SpecialWatchlistQuery', array( &$conds, &$tables, &$join_conds, &$fields, $values ) );
00314 
00315                 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
00316                 $numRows = $res->numRows();
00317 
00318                 /* Start bottom header */
00319 
00320                 $lang = $this->getLanguage();
00321                 $wlInfo = '';
00322                 if( $values['days'] > 0 ) {
00323                         $timestamp = wfTimestampNow();
00324                         $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $values['days'] * 24 ) )->params(
00325                                 $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user ) )->parse() . "<br />\n";
00326                 }
00327 
00328                 $cutofflinks = $this->cutoffLinks( $values['days'], $nondefaults ) . "<br />\n";
00329 
00330                 # Spit out some control panel links
00331                 $filters = array(
00332                         'hideMinor'     => 'rcshowhideminor',
00333                         'hideBots'              => 'rcshowhidebots',
00334                         'hideAnons'     => 'rcshowhideanons',
00335                         'hideLiu'               => 'rcshowhideliu',
00336                         'hideOwn'               => 'rcshowhidemine',
00337                         'hidePatrolled' => 'rcshowhidepatr'
00338                 );
00339                 foreach ( $this->customFilters as $key => $params ) {
00340                         $filters[$key] = $params['msg'];
00341                 }
00342                 // Disable some if needed
00343                 if ( !$user->useNPPatrol() ) {
00344                         unset( $filters['hidePatrolled'] );
00345                 }
00346 
00347                 $links = array();
00348                 foreach( $filters as $name => $msg ) {
00349                         $links[] = $this->showHideLink( $nondefaults, $msg, $name, $values[$name] );
00350                 }
00351 
00352                 $hiddenFields = $nondefaults;
00353                 unset( $hiddenFields['namespace'] );
00354                 unset( $hiddenFields['invert'] );
00355                 unset( $hiddenFields['associated'] );
00356 
00357                 # Namespace filter and put the whole form together.
00358                 $form .= $wlInfo;
00359                 $form .= $cutofflinks;
00360                 $form .= $lang->pipeList( $links ) . "\n";
00361                 $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl(), 'id' => 'mw-watchlist-form-namespaceselector' ) ) . "\n";
00362                 $form .= "<hr />\n<p>";
00363                 $form .= Html::namespaceSelector(
00364                         array(
00365                                 'selected' => $nameSpace,
00366                                 'all' => '',
00367                                 'label' => $this->msg( 'namespace' )->text()
00368                         ), array(
00369                                 'name'  => 'namespace',
00370                                 'id'    => 'namespace',
00371                                 'class' => 'namespaceselector',
00372                         )
00373                 ) . '&#160;';
00374                 $form .= Xml::checkLabel(
00375                         $this->msg( 'invert' )->text(),
00376                         'invert',
00377                         'nsinvert',
00378                         $invert,
00379                         array( 'title' => $this->msg( 'tooltip-invert' )->text() )
00380                 ) . '&#160;';
00381                 $form .= Xml::checkLabel(
00382                         $this->msg( 'namespace_association' )->text(),
00383                         'associated',
00384                         'associated',
00385                         $associated,
00386                         array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
00387                 ) . '&#160;';
00388                 $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "</p>\n";
00389                 foreach ( $hiddenFields as $key => $value ) {
00390                         $form .= Html::hidden( $key, $value ) . "\n";
00391                 }
00392                 $form .= Xml::closeElement( 'form' ) . "\n";
00393                 $form .= Xml::closeElement( 'fieldset' ) . "\n";
00394                 $output->addHTML( $form );
00395 
00396                 # If there's nothing to show, stop here
00397                 if( $numRows == 0 ) {
00398                         $output->addWikiMsg( 'watchnochange' );
00399                         return;
00400                 }
00401 
00402                 /* End bottom header */
00403 
00404                 /* Do link batch query */
00405                 $linkBatch = new LinkBatch;
00406                 foreach ( $res as $row ) {
00407                         $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
00408                         if ( $row->rc_user != 0 ) {
00409                                 $linkBatch->add( NS_USER, $userNameUnderscored );
00410                         }
00411                         $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
00412 
00413                         $linkBatch->add( $row->rc_namespace, $row->rc_title );
00414                 }
00415                 $linkBatch->execute();
00416                 $dbr->dataSeek( $res, 0 );
00417 
00418                 $list = ChangesList::newFromContext( $this->getContext() );
00419                 $list->setWatchlistDivs();
00420 
00421                 $s = $list->beginRecentChangesList();
00422                 $counter = 1;
00423                 foreach ( $res as $obj ) {
00424                         # Make RC entry
00425                         $rc = RecentChange::newFromRow( $obj );
00426                         $rc->counter = $counter++;
00427 
00428                         if ( $wgShowUpdatedMarker ) {
00429                                 $updated = $obj->wl_notificationtimestamp;
00430                         } else {
00431                                 $updated = false;
00432                         }
00433 
00434                         if ( $wgRCShowWatchingUsers && $user->getOption( 'shownumberswatching' ) ) {
00435                                 $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
00436                                         'COUNT(*)',
00437                                         array(
00438                                                 'wl_namespace' => $obj->rc_namespace,
00439                                                 'wl_title' => $obj->rc_title,
00440                                         ),
00441                                         __METHOD__ );
00442                         } else {
00443                                 $rc->numberofWatchingusers = 0;
00444                         }
00445 
00446                         $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
00447                         if ( $changeLine !== false ) {
00448                                 $s .= $changeLine;
00449                         }
00450                 }
00451                 $s .= $list->endRecentChangesList();
00452 
00453                 $output->addHTML( $s );
00454         }
00455 
00456         protected function showHideLink( $options, $message, $name, $value ) {
00457                 $label = $this->msg( $value ? 'show' : 'hide' )->escaped();
00458                 $options[$name] = 1 - (int) $value;
00459 
00460                 return $this->msg( $message )->rawParams( Linker::linkKnown( $this->getTitle(), $label, array(), $options ) )->escaped();
00461         }
00462 
00463         protected function hoursLink( $h, $options = array() ) {
00464                 $options['days'] = ( $h / 24.0 );
00465 
00466                 return Linker::linkKnown(
00467                         $this->getTitle(),
00468                         $this->getLanguage()->formatNum( $h ),
00469                         array(),
00470                         $options
00471                 );
00472         }
00473 
00474         protected function daysLink( $d, $options = array() ) {
00475                 $options['days'] = $d;
00476                 $message = ( $d ? $this->getLanguage()->formatNum( $d ) : $this->msg( 'watchlistall2' )->escaped() );
00477 
00478                 return Linker::linkKnown(
00479                         $this->getTitle(),
00480                         $message,
00481                         array(),
00482                         $options
00483                 );
00484         }
00485 
00491         protected function cutoffLinks( $days, $options = array() ) {
00492                 $hours = array( 1, 2, 6, 12 );
00493                 $days = array( 1, 3, 7 );
00494                 $i = 0;
00495                 foreach( $hours as $h ) {
00496                         $hours[$i++] = $this->hoursLink( $h, $options );
00497                 }
00498                 $i = 0;
00499                 foreach( $days as $d ) {
00500                         $days[$i++] = $this->daysLink( $d, $options );
00501                 }
00502                 return $this->msg( 'wlshowlast' )->rawParams(
00503                         $this->getLanguage()->pipeList( $hours ),
00504                         $this->getLanguage()->pipeList( $days ),
00505                         $this->daysLink( 0, $options ) )->parse();
00506         }
00507 
00514         protected function countItems( $dbr ) {
00515                 # Fetch the raw count
00516                 $res = $dbr->select( 'watchlist', array( 'count' => 'COUNT(*)' ),
00517                         array( 'wl_user' => $this->getUser()->getId() ), __METHOD__ );
00518                 $row = $dbr->fetchObject( $res );
00519                 $count = $row->count;
00520 
00521                 return floor( $count / 2 );
00522         }
00523 
00524         protected function getGroupName() {
00525                 return 'changes';
00526         }
00527 }