MediaWiki  REL1_23
SpecialRecentchanges.php
Go to the documentation of this file.
00001 <?php
00029 class SpecialRecentChanges extends ChangesListSpecialPage {
00030     // @codingStandardsIgnoreStart Needed "useless" override to change parameters.
00031     public function __construct( $name = 'Recentchanges', $restriction = '' ) {
00032         parent::__construct( $name, $restriction );
00033     }
00034     // @codingStandardsIgnoreEnd
00035 
00041     public function execute( $subpage ) {
00042         // Backwards-compatibility: redirect to new feed URLs
00043         $feedFormat = $this->getRequest()->getVal( 'feed' );
00044         if ( !$this->including() && $feedFormat ) {
00045             $query = $this->getFeedQuery();
00046             $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss';
00047             $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) );
00048 
00049             return;
00050         }
00051 
00052         // 10 seconds server-side caching max
00053         $this->getOutput()->setSquidMaxage( 10 );
00054         // Check if the client has a cached version
00055         $lastmod = $this->checkLastModified();
00056         if ( $lastmod === false ) {
00057             return;
00058         }
00059 
00060         parent::execute( $subpage );
00061     }
00062 
00068     public function getDefaultOptions() {
00069         $opts = parent::getDefaultOptions();
00070         $user = $this->getUser();
00071 
00072         $opts->add( 'days', $user->getIntOption( 'rcdays' ) );
00073         $opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
00074         $opts->add( 'from', '' );
00075 
00076         $opts->add( 'hideminor', $user->getBoolOption( 'hideminor' ) );
00077         $opts->add( 'hidebots', true );
00078         $opts->add( 'hideanons', false );
00079         $opts->add( 'hideliu', false );
00080         $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) );
00081         $opts->add( 'hidemyself', false );
00082 
00083         $opts->add( 'categories', '' );
00084         $opts->add( 'categories_any', false );
00085         $opts->add( 'tagfilter', '' );
00086 
00087         return $opts;
00088     }
00089 
00095     protected function getCustomFilters() {
00096         if ( $this->customFilters === null ) {
00097             $this->customFilters = parent::getCustomFilters();
00098             wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ), '1.23' );
00099         }
00100 
00101         return $this->customFilters;
00102     }
00103 
00110     public function parseParameters( $par, FormOptions $opts ) {
00111         $bits = preg_split( '/\s*,\s*/', trim( $par ) );
00112         foreach ( $bits as $bit ) {
00113             if ( 'hidebots' === $bit ) {
00114                 $opts['hidebots'] = true;
00115             }
00116             if ( 'bots' === $bit ) {
00117                 $opts['hidebots'] = false;
00118             }
00119             if ( 'hideminor' === $bit ) {
00120                 $opts['hideminor'] = true;
00121             }
00122             if ( 'minor' === $bit ) {
00123                 $opts['hideminor'] = false;
00124             }
00125             if ( 'hideliu' === $bit ) {
00126                 $opts['hideliu'] = true;
00127             }
00128             if ( 'hidepatrolled' === $bit ) {
00129                 $opts['hidepatrolled'] = true;
00130             }
00131             if ( 'hideanons' === $bit ) {
00132                 $opts['hideanons'] = true;
00133             }
00134             if ( 'hidemyself' === $bit ) {
00135                 $opts['hidemyself'] = true;
00136             }
00137 
00138             if ( is_numeric( $bit ) ) {
00139                 $opts['limit'] = $bit;
00140             }
00141 
00142             $m = array();
00143             if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
00144                 $opts['limit'] = $m[1];
00145             }
00146             if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
00147                 $opts['days'] = $m[1];
00148             }
00149             if ( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
00150                 $opts['namespace'] = $m[1];
00151             }
00152         }
00153     }
00154 
00155     public function validateOptions( FormOptions $opts ) {
00156         $opts->validateIntBounds( 'limit', 0, 5000 );
00157         parent::validateOptions( $opts );
00158     }
00159 
00166     public function buildMainQueryConds( FormOptions $opts ) {
00167         $dbr = $this->getDB();
00168         $conds = parent::buildMainQueryConds( $opts );
00169 
00170         // Calculate cutoff
00171         $cutoff_unixtime = time() - ( $opts['days'] * 86400 );
00172         $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
00173         $cutoff = $dbr->timestamp( $cutoff_unixtime );
00174 
00175         $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
00176         if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
00177             $cutoff = $dbr->timestamp( $opts['from'] );
00178         } else {
00179             $opts->reset( 'from' );
00180         }
00181 
00182         $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
00183 
00184         return $conds;
00185     }
00186 
00194     public function doMainQuery( $conds, $opts ) {
00195         global $wgAllowCategorizedRecentChanges;
00196 
00197         $dbr = $this->getDB();
00198         $user = $this->getUser();
00199 
00200         $tables = array( 'recentchanges' );
00201         $fields = RecentChange::selectFields();
00202         $query_options = array();
00203         $join_conds = array();
00204 
00205         // JOIN on watchlist for users
00206         if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) {
00207             $tables[] = 'watchlist';
00208             $fields[] = 'wl_user';
00209             $fields[] = 'wl_notificationtimestamp';
00210             $join_conds['watchlist'] = array( 'LEFT JOIN', array(
00211                 'wl_user' => $user->getId(),
00212                 'wl_title=rc_title',
00213                 'wl_namespace=rc_namespace'
00214             ) );
00215         }
00216 
00217         if ( $user->isAllowed( 'rollback' ) ) {
00218             $tables[] = 'page';
00219             $fields[] = 'page_latest';
00220             $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
00221         }
00222 
00223         ChangeTags::modifyDisplayQuery(
00224             $tables,
00225             $fields,
00226             $conds,
00227             $join_conds,
00228             $query_options,
00229             $opts['tagfilter']
00230         );
00231 
00232         if ( !wfRunHooks( 'SpecialRecentChangesQuery',
00233             array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ),
00234             '1.23' )
00235         ) {
00236             return false;
00237         }
00238 
00239         // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
00240         // knowledge to use an index merge if it wants (it may use some other index though).
00241         $rows = $dbr->select(
00242             $tables,
00243             $fields,
00244             $conds + array( 'rc_new' => array( 0, 1 ) ),
00245             __METHOD__,
00246             array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $opts['limit'] ) + $query_options,
00247             $join_conds
00248         );
00249 
00250         // Build the final data
00251         if ( $wgAllowCategorizedRecentChanges ) {
00252             $this->filterByCategories( $rows, $opts );
00253         }
00254 
00255         return $rows;
00256     }
00257 
00258     public function outputFeedLinks() {
00259         $this->addFeedLinks( $this->getFeedQuery() );
00260     }
00261 
00267     private function getFeedQuery() {
00268         global $wgFeedLimit;
00269         $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
00270             // API handles empty parameters in a different way
00271             return $value !== '';
00272         } );
00273         $query['action'] = 'feedrecentchanges';
00274         if ( $query['limit'] > $wgFeedLimit ) {
00275             $query['limit'] = $wgFeedLimit;
00276         }
00277 
00278         return $query;
00279     }
00280 
00287     public function outputChangesList( $rows, $opts ) {
00288         global $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
00289 
00290         $limit = $opts['limit'];
00291 
00292         $showWatcherCount = $wgRCShowWatchingUsers
00293             && $this->getUser()->getOption( 'shownumberswatching' );
00294         $watcherCache = array();
00295 
00296         $dbr = $this->getDB();
00297 
00298         $counter = 1;
00299         $list = ChangesList::newFromContext( $this->getContext() );
00300         $list->initChangesListRows( $rows );
00301 
00302         $rclistOutput = $list->beginRecentChangesList();
00303         foreach ( $rows as $obj ) {
00304             if ( $limit == 0 ) {
00305                 break;
00306             }
00307             $rc = RecentChange::newFromRow( $obj );
00308             $rc->counter = $counter++;
00309             # Check if the page has been updated since the last visit
00310             if ( $wgShowUpdatedMarker && !empty( $obj->wl_notificationtimestamp ) ) {
00311                 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
00312             } else {
00313                 $rc->notificationtimestamp = false; // Default
00314             }
00315             # Check the number of users watching the page
00316             $rc->numberofWatchingusers = 0; // Default
00317             if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
00318                 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
00319                     $watcherCache[$obj->rc_namespace][$obj->rc_title] =
00320                         $dbr->selectField(
00321                             'watchlist',
00322                             'COUNT(*)',
00323                             array(
00324                                 'wl_namespace' => $obj->rc_namespace,
00325                                 'wl_title' => $obj->rc_title,
00326                             ),
00327                             __METHOD__ . '-watchers'
00328                         );
00329                 }
00330                 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
00331             }
00332 
00333             $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
00334             if ( $changeLine !== false ) {
00335                 $rclistOutput .= $changeLine;
00336                 --$limit;
00337             }
00338         }
00339         $rclistOutput .= $list->endRecentChangesList();
00340 
00341         if ( $rows->numRows() === 0 ) {
00342             $this->getOutput()->addHtml(
00343                 '<div class="mw-changeslist-empty">' .
00344                 $this->msg( 'recentchanges-noresult' )->parse() .
00345                 '</div>'
00346             );
00347         } else {
00348             $this->getOutput()->addHTML( $rclistOutput );
00349         }
00350     }
00351 
00358     public function doHeader( $opts, $numRows ) {
00359         global $wgScript;
00360 
00361         $this->setTopText( $opts );
00362 
00363         $defaults = $opts->getAllValues();
00364         $nondefaults = $opts->getChangedValues();
00365 
00366         $panel = array();
00367         $panel[] = self::makeLegend( $this->getContext() );
00368         $panel[] = $this->optionsPanel( $defaults, $nondefaults );
00369         $panel[] = '<hr />';
00370 
00371         $extraOpts = $this->getExtraOptions( $opts );
00372         $extraOptsCount = count( $extraOpts );
00373         $count = 0;
00374         $submit = ' ' . Xml::submitbutton( $this->msg( 'allpagessubmit' )->text() );
00375 
00376         $out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) );
00377         foreach ( $extraOpts as $name => $optionRow ) {
00378             # Add submit button to the last row only
00379             ++$count;
00380             $addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
00381 
00382             $out .= Xml::openElement( 'tr' );
00383             if ( is_array( $optionRow ) ) {
00384                 $out .= Xml::tags(
00385                     'td',
00386                     array( 'class' => 'mw-label mw-' . $name . '-label' ),
00387                     $optionRow[0]
00388                 );
00389                 $out .= Xml::tags(
00390                     'td',
00391                     array( 'class' => 'mw-input' ),
00392                     $optionRow[1] . $addSubmit
00393                 );
00394             } else {
00395                 $out .= Xml::tags(
00396                     'td',
00397                     array( 'class' => 'mw-input', 'colspan' => 2 ),
00398                     $optionRow . $addSubmit
00399                 );
00400             }
00401             $out .= Xml::closeElement( 'tr' );
00402         }
00403         $out .= Xml::closeElement( 'table' );
00404 
00405         $unconsumed = $opts->getUnconsumedValues();
00406         foreach ( $unconsumed as $key => $value ) {
00407             $out .= Html::hidden( $key, $value );
00408         }
00409 
00410         $t = $this->getPageTitle();
00411         $out .= Html::hidden( 'title', $t->getPrefixedText() );
00412         $form = Xml::tags( 'form', array( 'action' => $wgScript ), $out );
00413         $panel[] = $form;
00414         $panelString = implode( "\n", $panel );
00415 
00416         $this->getOutput()->addHTML(
00417             Xml::fieldset(
00418                 $this->msg( 'recentchanges-legend' )->text(),
00419                 $panelString,
00420                 array( 'class' => 'rcoptions' )
00421             )
00422         );
00423 
00424         $this->setBottomText( $opts );
00425     }
00426 
00432     function setTopText( FormOptions $opts ) {
00433         global $wgContLang;
00434 
00435         $message = $this->msg( 'recentchangestext' )->inContentLanguage();
00436         if ( !$message->isDisabled() ) {
00437             $this->getOutput()->addWikiText(
00438                 Html::rawElement( 'p',
00439                     array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ),
00440                     "\n" . $message->plain() . "\n"
00441                 ),
00442                 /* $lineStart */ false,
00443                 /* $interface */ false
00444             );
00445         }
00446     }
00447 
00454     function getExtraOptions( $opts ) {
00455         $opts->consumeValues( array(
00456             'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
00457         ) );
00458 
00459         $extraOpts = array();
00460         $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
00461 
00462         global $wgAllowCategorizedRecentChanges;
00463         if ( $wgAllowCategorizedRecentChanges ) {
00464             $extraOpts['category'] = $this->categoryFilterForm( $opts );
00465         }
00466 
00467         $tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
00468         if ( count( $tagFilter ) ) {
00469             $extraOpts['tagfilter'] = $tagFilter;
00470         }
00471 
00472         // Don't fire the hook for subclasses. (Or should we?)
00473         if ( $this->getName() === 'Recentchanges' ) {
00474             wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
00475         }
00476 
00477         return $extraOpts;
00478     }
00479 
00483     protected function addModules() {
00484         parent::addModules();
00485         $out = $this->getOutput();
00486         $out->addModules( 'mediawiki.special.recentchanges' );
00487     }
00488 
00496     public function checkLastModified() {
00497         $dbr = $this->getDB();
00498         $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
00499 
00500         return $lastmod;
00501     }
00502 
00509     protected function namespaceFilterForm( FormOptions $opts ) {
00510         $nsSelect = Html::namespaceSelector(
00511             array( 'selected' => $opts['namespace'], 'all' => '' ),
00512             array( 'name' => 'namespace', 'id' => 'namespace' )
00513         );
00514         $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
00515         $invert = Xml::checkLabel(
00516             $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
00517             $opts['invert'],
00518             array( 'title' => $this->msg( 'tooltip-invert' )->text() )
00519         );
00520         $associated = Xml::checkLabel(
00521             $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
00522             $opts['associated'],
00523             array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
00524         );
00525 
00526         return array( $nsLabel, "$nsSelect $invert $associated" );
00527     }
00528 
00535     protected function categoryFilterForm( FormOptions $opts ) {
00536         list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
00537             'categories', 'mw-categories', false, $opts['categories'] );
00538 
00539         $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
00540             'categories_any', 'mw-categories_any', $opts['categories_any'] );
00541 
00542         return array( $label, $input );
00543     }
00544 
00551     function filterByCategories( &$rows, FormOptions $opts ) {
00552         $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
00553 
00554         if ( !count( $categories ) ) {
00555             return;
00556         }
00557 
00558         # Filter categories
00559         $cats = array();
00560         foreach ( $categories as $cat ) {
00561             $cat = trim( $cat );
00562             if ( $cat == '' ) {
00563                 continue;
00564             }
00565             $cats[] = $cat;
00566         }
00567 
00568         # Filter articles
00569         $articles = array();
00570         $a2r = array();
00571         $rowsarr = array();
00572         foreach ( $rows as $k => $r ) {
00573             $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
00574             $id = $nt->getArticleID();
00575             if ( $id == 0 ) {
00576                 continue; # Page might have been deleted...
00577             }
00578             if ( !in_array( $id, $articles ) ) {
00579                 $articles[] = $id;
00580             }
00581             if ( !isset( $a2r[$id] ) ) {
00582                 $a2r[$id] = array();
00583             }
00584             $a2r[$id][] = $k;
00585             $rowsarr[$k] = $r;
00586         }
00587 
00588         # Shortcut?
00589         if ( !count( $articles ) || !count( $cats ) ) {
00590             return;
00591         }
00592 
00593         # Look up
00594         $c = new Categoryfinder;
00595         $c->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
00596         $match = $c->run();
00597 
00598         # Filter
00599         $newrows = array();
00600         foreach ( $match as $id ) {
00601             foreach ( $a2r[$id] as $rev ) {
00602                 $k = $rev;
00603                 $newrows[$k] = $rowsarr[$k];
00604             }
00605         }
00606         $rows = $newrows;
00607     }
00608 
00618     function makeOptionsLink( $title, $override, $options, $active = false ) {
00619         $params = $override + $options;
00620 
00621         // Bug 36524: false values have be converted to "0" otherwise
00622         // wfArrayToCgi() will omit it them.
00623         foreach ( $params as &$value ) {
00624             if ( $value === false ) {
00625                 $value = '0';
00626             }
00627         }
00628         unset( $value );
00629 
00630         $text = htmlspecialchars( $title );
00631         if ( $active ) {
00632             $text = '<strong>' . $text . '</strong>';
00633         }
00634 
00635         return Linker::linkKnown( $this->getPageTitle(), $text, array(), $params );
00636     }
00637 
00645     function optionsPanel( $defaults, $nondefaults ) {
00646         global $wgRCLinkLimits, $wgRCLinkDays;
00647 
00648         $options = $nondefaults + $defaults;
00649 
00650         $note = '';
00651         $msg = $this->msg( 'rclegend' );
00652         if ( !$msg->isDisabled() ) {
00653             $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
00654         }
00655 
00656         $lang = $this->getLanguage();
00657         $user = $this->getUser();
00658         if ( $options['from'] ) {
00659             $note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params(
00660                 $lang->userTimeAndDate( $options['from'], $user ),
00661                 $lang->userDate( $options['from'], $user ),
00662                 $lang->userTime( $options['from'], $user ) )->parse() . '<br />';
00663         }
00664 
00665         # Sort data for display and make sure it's unique after we've added user data.
00666         $linkLimits = $wgRCLinkLimits;
00667         $linkLimits[] = $options['limit'];
00668         sort( $linkLimits );
00669         $linkLimits = array_unique( $linkLimits );
00670 
00671         $linkDays = $wgRCLinkDays;
00672         $linkDays[] = $options['days'];
00673         sort( $linkDays );
00674         $linkDays = array_unique( $linkDays );
00675 
00676         // limit links
00677         $cl = array();
00678         foreach ( $linkLimits as $value ) {
00679             $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
00680                 array( 'limit' => $value ), $nondefaults, $value == $options['limit'] );
00681         }
00682         $cl = $lang->pipeList( $cl );
00683 
00684         // day links, reset 'from' to none
00685         $dl = array();
00686         foreach ( $linkDays as $value ) {
00687             $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
00688                 array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] );
00689         }
00690         $dl = $lang->pipeList( $dl );
00691 
00692         // show/hide links
00693         $filters = array(
00694             'hideminor' => 'rcshowhideminor',
00695             'hidebots' => 'rcshowhidebots',
00696             'hideanons' => 'rcshowhideanons',
00697             'hideliu' => 'rcshowhideliu',
00698             'hidepatrolled' => 'rcshowhidepatr',
00699             'hidemyself' => 'rcshowhidemine'
00700         );
00701 
00702         $showhide = array( 'show', 'hide' );
00703 
00704         foreach ( $this->getCustomFilters() as $key => $params ) {
00705             $filters[$key] = $params['msg'];
00706         }
00707         // Disable some if needed
00708         if ( !$user->useRCPatrol() ) {
00709             unset( $filters['hidepatrolled'] );
00710         }
00711 
00712         $links = array();
00713         foreach ( $filters as $key => $msg ) {
00714             // The following messages are used here:
00715             // rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide,
00716             // rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide,
00717             // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide.
00718             $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
00719             // Extensions can define additional filters, but don't need to define the corresponding
00720             // messages. If they don't exist, just fall back to 'show' and 'hide'.
00721             if ( !$linkMessage->exists() ) {
00722                 $linkMessage = $this->msg( $showhide[1 - $options[$key]] );
00723             }
00724 
00725             $link = $this->makeOptionsLink( $linkMessage->text(),
00726                 array( $key => 1 - $options[$key] ), $nondefaults );
00727             $links[] = $this->msg( $msg )->rawParams( $link )->escaped();
00728         }
00729 
00730         // show from this onward link
00731         $timestamp = wfTimestampNow();
00732         $now = $lang->userTimeAndDate( $timestamp, $user );
00733         $timenow = $lang->userTime( $timestamp, $user );
00734         $datenow = $lang->userDate( $timestamp, $user );
00735         $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )
00736             ->parse();
00737         $rclistfrom = $this->makeOptionsLink(
00738             $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
00739             array( 'from' => $timestamp ),
00740             $nondefaults
00741         );
00742 
00743         return "{$note}$rclinks<br />$rclistfrom";
00744     }
00745 
00746     public function isIncludable() {
00747         return true;
00748     }
00749 }