MediaWiki
REL1_24
|
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 $dbr = $this->getDB(); 00196 $user = $this->getUser(); 00197 00198 $tables = array( 'recentchanges' ); 00199 $fields = RecentChange::selectFields(); 00200 $query_options = array(); 00201 $join_conds = array(); 00202 00203 // JOIN on watchlist for users 00204 if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) { 00205 $tables[] = 'watchlist'; 00206 $fields[] = 'wl_user'; 00207 $fields[] = 'wl_notificationtimestamp'; 00208 $join_conds['watchlist'] = array( 'LEFT JOIN', array( 00209 'wl_user' => $user->getId(), 00210 'wl_title=rc_title', 00211 'wl_namespace=rc_namespace' 00212 ) ); 00213 } 00214 00215 if ( $user->isAllowed( 'rollback' ) ) { 00216 $tables[] = 'page'; 00217 $fields[] = 'page_latest'; 00218 $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' ); 00219 } 00220 00221 ChangeTags::modifyDisplayQuery( 00222 $tables, 00223 $fields, 00224 $conds, 00225 $join_conds, 00226 $query_options, 00227 $opts['tagfilter'] 00228 ); 00229 00230 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, 00231 $opts ) 00232 ) { 00233 return false; 00234 } 00235 00236 // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough 00237 // knowledge to use an index merge if it wants (it may use some other index though). 00238 $rows = $dbr->select( 00239 $tables, 00240 $fields, 00241 $conds + array( 'rc_new' => array( 0, 1 ) ), 00242 __METHOD__, 00243 array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $opts['limit'] ) + $query_options, 00244 $join_conds 00245 ); 00246 00247 // Build the final data 00248 if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) { 00249 $this->filterByCategories( $rows, $opts ); 00250 } 00251 00252 return $rows; 00253 } 00254 00255 protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) { 00256 return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) 00257 && wfRunHooks( 00258 'SpecialRecentChangesQuery', 00259 array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ), 00260 '1.23' 00261 ); 00262 } 00263 00264 public function outputFeedLinks() { 00265 $this->addFeedLinks( $this->getFeedQuery() ); 00266 } 00267 00273 private function getFeedQuery() { 00274 $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) { 00275 // API handles empty parameters in a different way 00276 return $value !== ''; 00277 } ); 00278 $query['action'] = 'feedrecentchanges'; 00279 $feedLimit = $this->getConfig()->get( 'FeedLimit' ); 00280 if ( $query['limit'] > $feedLimit ) { 00281 $query['limit'] = $feedLimit; 00282 } 00283 00284 return $query; 00285 } 00286 00293 public function outputChangesList( $rows, $opts ) { 00294 $limit = $opts['limit']; 00295 00296 $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' ) 00297 && $this->getUser()->getOption( 'shownumberswatching' ); 00298 $watcherCache = array(); 00299 00300 $dbr = $this->getDB(); 00301 00302 $counter = 1; 00303 $list = ChangesList::newFromContext( $this->getContext() ); 00304 $list->initChangesListRows( $rows ); 00305 00306 $rclistOutput = $list->beginRecentChangesList(); 00307 foreach ( $rows as $obj ) { 00308 if ( $limit == 0 ) { 00309 break; 00310 } 00311 $rc = RecentChange::newFromRow( $obj ); 00312 $rc->counter = $counter++; 00313 # Check if the page has been updated since the last visit 00314 if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) && !empty( $obj->wl_notificationtimestamp ) ) { 00315 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp ); 00316 } else { 00317 $rc->notificationtimestamp = false; // Default 00318 } 00319 # Check the number of users watching the page 00320 $rc->numberofWatchingusers = 0; // Default 00321 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) { 00322 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) { 00323 $watcherCache[$obj->rc_namespace][$obj->rc_title] = 00324 $dbr->selectField( 00325 'watchlist', 00326 'COUNT(*)', 00327 array( 00328 'wl_namespace' => $obj->rc_namespace, 00329 'wl_title' => $obj->rc_title, 00330 ), 00331 __METHOD__ . '-watchers' 00332 ); 00333 } 00334 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title]; 00335 } 00336 00337 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter ); 00338 if ( $changeLine !== false ) { 00339 $rclistOutput .= $changeLine; 00340 --$limit; 00341 } 00342 } 00343 $rclistOutput .= $list->endRecentChangesList(); 00344 00345 if ( $rows->numRows() === 0 ) { 00346 $this->getOutput()->addHtml( 00347 '<div class="mw-changeslist-empty">' . 00348 $this->msg( 'recentchanges-noresult' )->parse() . 00349 '</div>' 00350 ); 00351 if ( !$this->including() ) { 00352 $this->getOutput()->setStatusCode( 404 ); 00353 } 00354 } else { 00355 $this->getOutput()->addHTML( $rclistOutput ); 00356 } 00357 } 00358 00365 public function doHeader( $opts, $numRows ) { 00366 $this->setTopText( $opts ); 00367 00368 $defaults = $opts->getAllValues(); 00369 $nondefaults = $opts->getChangedValues(); 00370 00371 $panel = array(); 00372 $panel[] = self::makeLegend( $this->getContext() ); 00373 $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows ); 00374 $panel[] = '<hr />'; 00375 00376 $extraOpts = $this->getExtraOptions( $opts ); 00377 $extraOptsCount = count( $extraOpts ); 00378 $count = 0; 00379 $submit = ' ' . Xml::submitbutton( $this->msg( 'allpagessubmit' )->text() ); 00380 00381 $out = Xml::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) ); 00382 foreach ( $extraOpts as $name => $optionRow ) { 00383 # Add submit button to the last row only 00384 ++$count; 00385 $addSubmit = ( $count === $extraOptsCount ) ? $submit : ''; 00386 00387 $out .= Xml::openElement( 'tr' ); 00388 if ( is_array( $optionRow ) ) { 00389 $out .= Xml::tags( 00390 'td', 00391 array( 'class' => 'mw-label mw-' . $name . '-label' ), 00392 $optionRow[0] 00393 ); 00394 $out .= Xml::tags( 00395 'td', 00396 array( 'class' => 'mw-input' ), 00397 $optionRow[1] . $addSubmit 00398 ); 00399 } else { 00400 $out .= Xml::tags( 00401 'td', 00402 array( 'class' => 'mw-input', 'colspan' => 2 ), 00403 $optionRow . $addSubmit 00404 ); 00405 } 00406 $out .= Xml::closeElement( 'tr' ); 00407 } 00408 $out .= Xml::closeElement( 'table' ); 00409 00410 $unconsumed = $opts->getUnconsumedValues(); 00411 foreach ( $unconsumed as $key => $value ) { 00412 $out .= Html::hidden( $key, $value ); 00413 } 00414 00415 $t = $this->getPageTitle(); 00416 $out .= Html::hidden( 'title', $t->getPrefixedText() ); 00417 $form = Xml::tags( 'form', array( 'action' => wfScript() ), $out ); 00418 $panel[] = $form; 00419 $panelString = implode( "\n", $panel ); 00420 00421 $this->getOutput()->addHTML( 00422 Xml::fieldset( 00423 $this->msg( 'recentchanges-legend' )->text(), 00424 $panelString, 00425 array( 'class' => 'rcoptions' ) 00426 ) 00427 ); 00428 00429 $this->setBottomText( $opts ); 00430 } 00431 00437 function setTopText( FormOptions $opts ) { 00438 global $wgContLang; 00439 00440 $message = $this->msg( 'recentchangestext' )->inContentLanguage(); 00441 if ( !$message->isDisabled() ) { 00442 $this->getOutput()->addWikiText( 00443 Html::rawElement( 'p', 00444 array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), 00445 "\n" . $message->plain() . "\n" 00446 ), 00447 /* $lineStart */ false, 00448 /* $interface */ false 00449 ); 00450 } 00451 } 00452 00459 function getExtraOptions( $opts ) { 00460 $opts->consumeValues( array( 00461 'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any' 00462 ) ); 00463 00464 $extraOpts = array(); 00465 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts ); 00466 00467 if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) { 00468 $extraOpts['category'] = $this->categoryFilterForm( $opts ); 00469 } 00470 00471 $tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] ); 00472 if ( count( $tagFilter ) ) { 00473 $extraOpts['tagfilter'] = $tagFilter; 00474 } 00475 00476 // Don't fire the hook for subclasses. (Or should we?) 00477 if ( $this->getName() === 'Recentchanges' ) { 00478 wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) ); 00479 } 00480 00481 return $extraOpts; 00482 } 00483 00487 protected function addModules() { 00488 parent::addModules(); 00489 $out = $this->getOutput(); 00490 $out->addModules( 'mediawiki.special.recentchanges' ); 00491 } 00492 00500 public function checkLastModified() { 00501 $dbr = $this->getDB(); 00502 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ ); 00503 00504 return $lastmod; 00505 } 00506 00513 protected function namespaceFilterForm( FormOptions $opts ) { 00514 $nsSelect = Html::namespaceSelector( 00515 array( 'selected' => $opts['namespace'], 'all' => '' ), 00516 array( 'name' => 'namespace', 'id' => 'namespace' ) 00517 ); 00518 $nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ); 00519 $invert = Xml::checkLabel( 00520 $this->msg( 'invert' )->text(), 'invert', 'nsinvert', 00521 $opts['invert'], 00522 array( 'title' => $this->msg( 'tooltip-invert' )->text() ) 00523 ); 00524 $associated = Xml::checkLabel( 00525 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated', 00526 $opts['associated'], 00527 array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() ) 00528 ); 00529 00530 return array( $nsLabel, "$nsSelect $invert $associated" ); 00531 } 00532 00539 protected function categoryFilterForm( FormOptions $opts ) { 00540 list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(), 00541 'categories', 'mw-categories', false, $opts['categories'] ); 00542 00543 $input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(), 00544 'categories_any', 'mw-categories_any', $opts['categories_any'] ); 00545 00546 return array( $label, $input ); 00547 } 00548 00555 function filterByCategories( &$rows, FormOptions $opts ) { 00556 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) ); 00557 00558 if ( !count( $categories ) ) { 00559 return; 00560 } 00561 00562 # Filter categories 00563 $cats = array(); 00564 foreach ( $categories as $cat ) { 00565 $cat = trim( $cat ); 00566 if ( $cat == '' ) { 00567 continue; 00568 } 00569 $cats[] = $cat; 00570 } 00571 00572 # Filter articles 00573 $articles = array(); 00574 $a2r = array(); 00575 $rowsarr = array(); 00576 foreach ( $rows as $k => $r ) { 00577 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title ); 00578 $id = $nt->getArticleID(); 00579 if ( $id == 0 ) { 00580 continue; # Page might have been deleted... 00581 } 00582 if ( !in_array( $id, $articles ) ) { 00583 $articles[] = $id; 00584 } 00585 if ( !isset( $a2r[$id] ) ) { 00586 $a2r[$id] = array(); 00587 } 00588 $a2r[$id][] = $k; 00589 $rowsarr[$k] = $r; 00590 } 00591 00592 # Shortcut? 00593 if ( !count( $articles ) || !count( $cats ) ) { 00594 return; 00595 } 00596 00597 # Look up 00598 $catFind = new CategoryFinder; 00599 $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' ); 00600 $match = $catFind->run(); 00601 00602 # Filter 00603 $newrows = array(); 00604 foreach ( $match as $id ) { 00605 foreach ( $a2r[$id] as $rev ) { 00606 $k = $rev; 00607 $newrows[$k] = $rowsarr[$k]; 00608 } 00609 } 00610 $rows = $newrows; 00611 } 00612 00622 function makeOptionsLink( $title, $override, $options, $active = false ) { 00623 $params = $override + $options; 00624 00625 // Bug 36524: false values have be converted to "0" otherwise 00626 // wfArrayToCgi() will omit it them. 00627 foreach ( $params as &$value ) { 00628 if ( $value === false ) { 00629 $value = '0'; 00630 } 00631 } 00632 unset( $value ); 00633 00634 $text = htmlspecialchars( $title ); 00635 if ( $active ) { 00636 $text = '<strong>' . $text . '</strong>'; 00637 } 00638 00639 return Linker::linkKnown( $this->getPageTitle(), $text, array(), $params ); 00640 } 00641 00650 function optionsPanel( $defaults, $nondefaults, $numRows ) { 00651 $options = $nondefaults + $defaults; 00652 00653 $note = ''; 00654 $msg = $this->msg( 'rclegend' ); 00655 if ( !$msg->isDisabled() ) { 00656 $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n"; 00657 } 00658 00659 $lang = $this->getLanguage(); 00660 $user = $this->getUser(); 00661 if ( $options['from'] ) { 00662 $note .= $this->msg( 'rcnotefrom' ) 00663 ->numParams( $options['limit'] ) 00664 ->params( 00665 $lang->userTimeAndDate( $options['from'], $user ), 00666 $lang->userDate( $options['from'], $user ), 00667 $lang->userTime( $options['from'], $user ) 00668 ) 00669 ->numParams( $numRows ) 00670 ->parse() . '<br />'; 00671 } 00672 00673 # Sort data for display and make sure it's unique after we've added user data. 00674 $linkLimits = $this->getConfig()->get( 'RCLinkLimits' ); 00675 $linkLimits[] = $options['limit']; 00676 sort( $linkLimits ); 00677 $linkLimits = array_unique( $linkLimits ); 00678 00679 $linkDays = $this->getConfig()->get( 'RCLinkDays' ); 00680 $linkDays[] = $options['days']; 00681 sort( $linkDays ); 00682 $linkDays = array_unique( $linkDays ); 00683 00684 // limit links 00685 $cl = array(); 00686 foreach ( $linkLimits as $value ) { 00687 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ), 00688 array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ); 00689 } 00690 $cl = $lang->pipeList( $cl ); 00691 00692 // day links, reset 'from' to none 00693 $dl = array(); 00694 foreach ( $linkDays as $value ) { 00695 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ), 00696 array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ); 00697 } 00698 $dl = $lang->pipeList( $dl ); 00699 00700 // show/hide links 00701 $filters = array( 00702 'hideminor' => 'rcshowhideminor', 00703 'hidebots' => 'rcshowhidebots', 00704 'hideanons' => 'rcshowhideanons', 00705 'hideliu' => 'rcshowhideliu', 00706 'hidepatrolled' => 'rcshowhidepatr', 00707 'hidemyself' => 'rcshowhidemine' 00708 ); 00709 00710 $showhide = array( 'show', 'hide' ); 00711 00712 foreach ( $this->getCustomFilters() as $key => $params ) { 00713 $filters[$key] = $params['msg']; 00714 } 00715 // Disable some if needed 00716 if ( !$user->useRCPatrol() ) { 00717 unset( $filters['hidepatrolled'] ); 00718 } 00719 00720 $links = array(); 00721 foreach ( $filters as $key => $msg ) { 00722 // The following messages are used here: 00723 // rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide, 00724 // rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide, 00725 // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide. 00726 $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] ); 00727 // Extensions can define additional filters, but don't need to define the corresponding 00728 // messages. If they don't exist, just fall back to 'show' and 'hide'. 00729 if ( !$linkMessage->exists() ) { 00730 $linkMessage = $this->msg( $showhide[1 - $options[$key]] ); 00731 } 00732 00733 $link = $this->makeOptionsLink( $linkMessage->text(), 00734 array( $key => 1 - $options[$key] ), $nondefaults ); 00735 $links[] = "<span class=\"$msg rcshowhideoption\">" . $this->msg( $msg )->rawParams( $link )->escaped() . '</span>'; 00736 } 00737 00738 // show from this onward link 00739 $timestamp = wfTimestampNow(); 00740 $now = $lang->userTimeAndDate( $timestamp, $user ); 00741 $timenow = $lang->userTime( $timestamp, $user ); 00742 $datenow = $lang->userDate( $timestamp, $user ); 00743 $pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>'; 00744 00745 $rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, $pipedLinks ) 00746 ->parse() . '</span>'; 00747 00748 $rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink( 00749 $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(), 00750 array( 'from' => $timestamp ), 00751 $nondefaults 00752 ) . '</span>'; 00753 00754 return "{$note}$rclinks<br />$rclistfrom"; 00755 } 00756 00757 public function isIncludable() { 00758 return true; 00759 } 00760 }