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