MediaWiki
REL1_24
|
00001 <?php 00029 class SpecialContributions extends IncludableSpecialPage { 00030 protected $opts; 00031 00032 public function __construct() { 00033 parent::__construct( 'Contributions' ); 00034 } 00035 00036 public function execute( $par ) { 00037 $this->setHeaders(); 00038 $this->outputHeader(); 00039 $out = $this->getOutput(); 00040 $out->addModuleStyles( 'mediawiki.special' ); 00041 00042 $this->opts = array(); 00043 $request = $this->getRequest(); 00044 00045 if ( $par !== null ) { 00046 $target = $par; 00047 } else { 00048 $target = $request->getVal( 'target' ); 00049 } 00050 00051 // check for radiobox 00052 if ( $request->getVal( 'contribs' ) == 'newbie' ) { 00053 $target = 'newbies'; 00054 $this->opts['contribs'] = 'newbie'; 00055 } elseif ( $par === 'newbies' ) { // b/c for WMF 00056 $target = 'newbies'; 00057 $this->opts['contribs'] = 'newbie'; 00058 } else { 00059 $this->opts['contribs'] = 'user'; 00060 } 00061 00062 $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' ); 00063 00064 if ( !strlen( $target ) ) { 00065 if ( !$this->including() ) { 00066 $out->addHTML( $this->getForm() ); 00067 } 00068 00069 return; 00070 } 00071 00072 $user = $this->getUser(); 00073 00074 $this->opts['limit'] = $request->getInt( 'limit', $user->getOption( 'rclimit' ) ); 00075 $this->opts['target'] = $target; 00076 $this->opts['topOnly'] = $request->getBool( 'topOnly' ); 00077 $this->opts['newOnly'] = $request->getBool( 'newOnly' ); 00078 00079 $nt = Title::makeTitleSafe( NS_USER, $target ); 00080 if ( !$nt ) { 00081 $out->addHTML( $this->getForm() ); 00082 00083 return; 00084 } 00085 $userObj = User::newFromName( $nt->getText(), false ); 00086 if ( !$userObj ) { 00087 $out->addHTML( $this->getForm() ); 00088 00089 return; 00090 } 00091 $id = $userObj->getID(); 00092 00093 if ( $this->opts['contribs'] != 'newbie' ) { 00094 $target = $nt->getText(); 00095 $out->addSubtitle( $this->contributionsSub( $userObj ) ); 00096 $out->setHTMLTitle( $this->msg( 00097 'pagetitle', 00098 $this->msg( 'contributions-title', $target )->plain() 00099 )->inContentLanguage() ); 00100 $this->getSkin()->setRelevantUser( $userObj ); 00101 } else { 00102 $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) ); 00103 $out->setHTMLTitle( $this->msg( 00104 'pagetitle', 00105 $this->msg( 'sp-contributions-newbies-title' )->plain() 00106 )->inContentLanguage() ); 00107 } 00108 00109 if ( ( $ns = $request->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { 00110 $this->opts['namespace'] = intval( $ns ); 00111 } else { 00112 $this->opts['namespace'] = ''; 00113 } 00114 00115 $this->opts['associated'] = $request->getBool( 'associated' ); 00116 $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' ); 00117 $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' ); 00118 00119 // Allows reverts to have the bot flag in recent changes. It is just here to 00120 // be passed in the form at the top of the page 00121 if ( $user->isAllowed( 'markbotedits' ) && $request->getBool( 'bot' ) ) { 00122 $this->opts['bot'] = '1'; 00123 } 00124 00125 $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev'; 00126 # Offset overrides year/month selection 00127 if ( $skip ) { 00128 $this->opts['year'] = ''; 00129 $this->opts['month'] = ''; 00130 } else { 00131 $this->opts['year'] = $request->getIntOrNull( 'year' ); 00132 $this->opts['month'] = $request->getIntOrNull( 'month' ); 00133 } 00134 00135 $feedType = $request->getVal( 'feed' ); 00136 00137 $feedParams = array( 00138 'action' => 'feedcontributions', 00139 'user' => $target, 00140 ); 00141 if ( $this->opts['topOnly'] ) { 00142 $feedParams['toponly'] = true; 00143 } 00144 if ( $this->opts['newOnly'] ) { 00145 $feedParams['newonly'] = true; 00146 } 00147 if ( $this->opts['deletedOnly'] ) { 00148 $feedParams['deletedonly'] = true; 00149 } 00150 if ( $this->opts['tagfilter'] !== '' ) { 00151 $feedParams['tagfilter'] = $this->opts['tagfilter']; 00152 } 00153 if ( $this->opts['namespace'] !== '' ) { 00154 $feedParams['namespace'] = $this->opts['namespace']; 00155 } 00156 // Don't use year and month for the feed URL, but pass them on if 00157 // we redirect to API (if $feedType is specified) 00158 if ( $feedType && $this->opts['year'] !== null ) { 00159 $feedParams['year'] = $this->opts['year']; 00160 } 00161 if ( $feedType && $this->opts['month'] !== null ) { 00162 $feedParams['month'] = $this->opts['month']; 00163 } 00164 00165 if ( $feedType ) { 00166 // Maintain some level of backwards compatibility 00167 // If people request feeds using the old parameters, redirect to API 00168 $feedParams['feedformat'] = $feedType; 00169 $url = wfAppendQuery( wfScript( 'api' ), $feedParams ); 00170 00171 $out->redirect( $url, '301' ); 00172 00173 return; 00174 } 00175 00176 // Add RSS/atom links 00177 $this->addFeedLinks( $feedParams ); 00178 00179 if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id, $userObj, $this ) ) ) { 00180 if ( !$this->including() ) { 00181 $out->addHTML( $this->getForm() ); 00182 } 00183 $pager = new ContribsPager( $this->getContext(), array( 00184 'target' => $target, 00185 'contribs' => $this->opts['contribs'], 00186 'namespace' => $this->opts['namespace'], 00187 'tagfilter' => $this->opts['tagfilter'], 00188 'year' => $this->opts['year'], 00189 'month' => $this->opts['month'], 00190 'deletedOnly' => $this->opts['deletedOnly'], 00191 'topOnly' => $this->opts['topOnly'], 00192 'newOnly' => $this->opts['newOnly'], 00193 'nsInvert' => $this->opts['nsInvert'], 00194 'associated' => $this->opts['associated'], 00195 ) ); 00196 00197 if ( !$pager->getNumRows() ) { 00198 $out->addWikiMsg( 'nocontribs', $target ); 00199 } else { 00200 # Show a message about slave lag, if applicable 00201 $lag = wfGetLB()->safeGetLag( $pager->getDatabase() ); 00202 if ( $lag > 0 ) { 00203 $out->showLagWarning( $lag ); 00204 } 00205 00206 $output = $pager->getBody(); 00207 if ( !$this->including() ) { 00208 $output = '<p>' . $pager->getNavigationBar() . '</p>' . 00209 $output . 00210 '<p>' . $pager->getNavigationBar() . '</p>'; 00211 } 00212 $out->addHTML( $output ); 00213 } 00214 $out->preventClickjacking( $pager->getPreventClickjacking() ); 00215 00216 # Show the appropriate "footer" message - WHOIS tools, etc. 00217 if ( $this->opts['contribs'] == 'newbie' ) { 00218 $message = 'sp-contributions-footer-newbies'; 00219 } elseif ( IP::isIPAddress( $target ) ) { 00220 $message = 'sp-contributions-footer-anon'; 00221 } elseif ( $userObj->isAnon() ) { 00222 // No message for non-existing users 00223 $message = ''; 00224 } else { 00225 $message = 'sp-contributions-footer'; 00226 } 00227 00228 if ( $message ) { 00229 if ( !$this->including() ) { 00230 if ( !$this->msg( $message, $target )->isDisabled() ) { 00231 $out->wrapWikiMsg( 00232 "<div class='mw-contributions-footer'>\n$1\n</div>", 00233 array( $message, $target ) ); 00234 } 00235 } 00236 } 00237 } 00238 } 00239 00247 protected function contributionsSub( $userObj ) { 00248 if ( $userObj->isAnon() ) { 00249 // Show a warning message that the user being searched for doesn't exists 00250 if ( !User::isIP( $userObj->getName() ) ) { 00251 $this->getOutput()->wrapWikiMsg( 00252 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", 00253 array( 00254 'contributions-userdoesnotexist', 00255 wfEscapeWikiText( $userObj->getName() ), 00256 ) 00257 ); 00258 if ( !$this->including() ) { 00259 $this->getOutput()->setStatusCode( 404 ); 00260 } 00261 } 00262 $user = htmlspecialchars( $userObj->getName() ); 00263 } else { 00264 $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) ); 00265 } 00266 $nt = $userObj->getUserPage(); 00267 $talk = $userObj->getTalkPage(); 00268 $links = ''; 00269 if ( $talk ) { 00270 $tools = $this->getUserLinks( $nt, $talk, $userObj ); 00271 $links = $this->getLanguage()->pipeList( $tools ); 00272 00273 // Show a note if the user is blocked and display the last block log entry. 00274 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, 00275 // and also this will display a totally irrelevant log entry as a current block. 00276 if ( !$this->including() ) { 00277 $block = Block::newFromTarget( $userObj, $userObj ); 00278 if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { 00279 if ( $block->getType() == Block::TYPE_RANGE ) { 00280 $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(); 00281 } 00282 00283 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference 00284 LogEventsList::showLogExtract( 00285 $out, 00286 'block', 00287 $nt, 00288 '', 00289 array( 00290 'lim' => 1, 00291 'showIfEmpty' => false, 00292 'msgKey' => array( 00293 $userObj->isAnon() ? 00294 'sp-contributions-blocked-notice-anon' : 00295 'sp-contributions-blocked-notice', 00296 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' 00297 ), 00298 'offset' => '' # don't use WebRequest parameter offset 00299 ) 00300 ); 00301 } 00302 } 00303 } 00304 00305 return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() ); 00306 } 00307 00315 public function getUserLinks( Title $userpage, Title $talkpage, User $target ) { 00316 00317 $id = $target->getId(); 00318 $username = $target->getName(); 00319 00320 $tools[] = Linker::link( $talkpage, $this->msg( 'sp-contributions-talk' )->escaped() ); 00321 00322 if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) { 00323 if ( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links 00324 if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) { 00325 $tools[] = Linker::linkKnown( # Change block link 00326 SpecialPage::getTitleFor( 'Block', $username ), 00327 $this->msg( 'change-blocklink' )->escaped() 00328 ); 00329 $tools[] = Linker::linkKnown( # Unblock link 00330 SpecialPage::getTitleFor( 'Unblock', $username ), 00331 $this->msg( 'unblocklink' )->escaped() 00332 ); 00333 } else { # User is not blocked 00334 $tools[] = Linker::linkKnown( # Block link 00335 SpecialPage::getTitleFor( 'Block', $username ), 00336 $this->msg( 'blocklink' )->escaped() 00337 ); 00338 } 00339 } 00340 00341 # Block log link 00342 $tools[] = Linker::linkKnown( 00343 SpecialPage::getTitleFor( 'Log', 'block' ), 00344 $this->msg( 'sp-contributions-blocklog' )->escaped(), 00345 array(), 00346 array( 'page' => $userpage->getPrefixedText() ) 00347 ); 00348 00349 # Suppression log link (bug 59120) 00350 if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) { 00351 $tools[] = Linker::linkKnown( 00352 SpecialPage::getTitleFor( 'Log', 'suppress' ), 00353 $this->msg( 'sp-contributions-suppresslog' )->escaped(), 00354 array(), 00355 array( 'offender' => $username ) 00356 ); 00357 } 00358 } 00359 # Uploads 00360 $tools[] = Linker::linkKnown( 00361 SpecialPage::getTitleFor( 'Listfiles', $username ), 00362 $this->msg( 'sp-contributions-uploads' )->escaped() 00363 ); 00364 00365 # Other logs link 00366 $tools[] = Linker::linkKnown( 00367 SpecialPage::getTitleFor( 'Log', $username ), 00368 $this->msg( 'sp-contributions-logs' )->escaped() 00369 ); 00370 00371 # Add link to deleted user contributions for priviledged users 00372 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00373 $tools[] = Linker::linkKnown( 00374 SpecialPage::getTitleFor( 'DeletedContributions', $username ), 00375 $this->msg( 'sp-contributions-deleted' )->escaped() 00376 ); 00377 } 00378 00379 # Add a link to change user rights for privileged users 00380 $userrightsPage = new UserrightsPage(); 00381 $userrightsPage->setContext( $this->getContext() ); 00382 if ( $userrightsPage->userCanChangeRights( $target ) ) { 00383 $tools[] = Linker::linkKnown( 00384 SpecialPage::getTitleFor( 'Userrights', $username ), 00385 $this->msg( 'sp-contributions-userrights' )->escaped() 00386 ); 00387 } 00388 00389 wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) ); 00390 00391 return $tools; 00392 } 00393 00398 protected function getForm() { 00399 $this->opts['title'] = $this->getPageTitle()->getPrefixedText(); 00400 if ( !isset( $this->opts['target'] ) ) { 00401 $this->opts['target'] = ''; 00402 } else { 00403 $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] ); 00404 } 00405 00406 if ( !isset( $this->opts['namespace'] ) ) { 00407 $this->opts['namespace'] = ''; 00408 } 00409 00410 if ( !isset( $this->opts['nsInvert'] ) ) { 00411 $this->opts['nsInvert'] = ''; 00412 } 00413 00414 if ( !isset( $this->opts['associated'] ) ) { 00415 $this->opts['associated'] = false; 00416 } 00417 00418 if ( !isset( $this->opts['contribs'] ) ) { 00419 $this->opts['contribs'] = 'user'; 00420 } 00421 00422 if ( !isset( $this->opts['year'] ) ) { 00423 $this->opts['year'] = ''; 00424 } 00425 00426 if ( !isset( $this->opts['month'] ) ) { 00427 $this->opts['month'] = ''; 00428 } 00429 00430 if ( $this->opts['contribs'] == 'newbie' ) { 00431 $this->opts['target'] = ''; 00432 } 00433 00434 if ( !isset( $this->opts['tagfilter'] ) ) { 00435 $this->opts['tagfilter'] = ''; 00436 } 00437 00438 if ( !isset( $this->opts['topOnly'] ) ) { 00439 $this->opts['topOnly'] = false; 00440 } 00441 00442 if ( !isset( $this->opts['newOnly'] ) ) { 00443 $this->opts['newOnly'] = false; 00444 } 00445 00446 $form = Html::openElement( 00447 'form', 00448 array( 00449 'method' => 'get', 00450 'action' => wfScript(), 00451 'class' => 'mw-contributions-form' 00452 ) 00453 ); 00454 00455 # Add hidden params for tracking except for parameters in $skipParameters 00456 $skipParameters = array( 00457 'namespace', 00458 'nsInvert', 00459 'deletedOnly', 00460 'target', 00461 'contribs', 00462 'year', 00463 'month', 00464 'topOnly', 00465 'newOnly', 00466 'associated' 00467 ); 00468 00469 foreach ( $this->opts as $name => $value ) { 00470 if ( in_array( $name, $skipParameters ) ) { 00471 continue; 00472 } 00473 $form .= "\t" . Html::hidden( $name, $value ) . "\n"; 00474 } 00475 00476 $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ); 00477 00478 if ( $tagFilter ) { 00479 $filterSelection = Html::rawElement( 00480 'td', 00481 array( 'class' => 'mw-label' ), 00482 array_shift( $tagFilter ) 00483 ); 00484 $filterSelection .= Html::rawElement( 00485 'td', 00486 array( 'class' => 'mw-input' ), 00487 implode( ' ', $tagFilter ) 00488 ); 00489 } else { 00490 $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' ); 00491 } 00492 00493 $labelNewbies = Xml::radioLabel( 00494 $this->msg( 'sp-contributions-newbies' )->text(), 00495 'contribs', 00496 'newbie', 00497 'newbie', 00498 $this->opts['contribs'] == 'newbie', 00499 array( 'class' => 'mw-input' ) 00500 ); 00501 $labelUsername = Xml::radioLabel( 00502 $this->msg( 'sp-contributions-username' )->text(), 00503 'contribs', 00504 'user', 00505 'user', 00506 $this->opts['contribs'] == 'user', 00507 array( 'class' => 'mw-input' ) 00508 ); 00509 $input = Html::input( 00510 'target', 00511 $this->opts['target'], 00512 'text', 00513 array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) + 00514 ( $this->opts['target'] ? array() : array( 'autofocus' ) 00515 ) 00516 ); 00517 $targetSelection = Html::rawElement( 00518 'td', 00519 array( 'colspan' => 2 ), 00520 $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' ' 00521 ); 00522 00523 $namespaceSelection = Xml::tags( 00524 'td', 00525 array( 'class' => 'mw-label' ), 00526 Xml::label( 00527 $this->msg( 'namespace' )->text(), 00528 'namespace', 00529 '' 00530 ) 00531 ); 00532 $namespaceSelection .= Html::rawElement( 00533 'td', 00534 null, 00535 Html::namespaceSelector( 00536 array( 'selected' => $this->opts['namespace'], 'all' => '' ), 00537 array( 00538 'name' => 'namespace', 00539 'id' => 'namespace', 00540 'class' => 'namespaceselector', 00541 ) 00542 ) . ' ' . 00543 Html::rawElement( 00544 'span', 00545 array( 'style' => 'white-space: nowrap' ), 00546 Xml::checkLabel( 00547 $this->msg( 'invert' )->text(), 00548 'nsInvert', 00549 'nsInvert', 00550 $this->opts['nsInvert'], 00551 array( 00552 'title' => $this->msg( 'tooltip-invert' )->text(), 00553 'class' => 'mw-input' 00554 ) 00555 ) . ' ' 00556 ) . 00557 Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ), 00558 Xml::checkLabel( 00559 $this->msg( 'namespace_association' )->text(), 00560 'associated', 00561 'associated', 00562 $this->opts['associated'], 00563 array( 00564 'title' => $this->msg( 'tooltip-namespace_association' )->text(), 00565 'class' => 'mw-input' 00566 ) 00567 ) . ' ' 00568 ) 00569 ); 00570 00571 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00572 $deletedOnlyCheck = Html::rawElement( 00573 'span', 00574 array( 'style' => 'white-space: nowrap' ), 00575 Xml::checkLabel( 00576 $this->msg( 'history-show-deleted' )->text(), 00577 'deletedOnly', 00578 'mw-show-deleted-only', 00579 $this->opts['deletedOnly'], 00580 array( 'class' => 'mw-input' ) 00581 ) 00582 ); 00583 } else { 00584 $deletedOnlyCheck = ''; 00585 } 00586 00587 $checkLabelTopOnly = Html::rawElement( 00588 'span', 00589 array( 'style' => 'white-space: nowrap' ), 00590 Xml::checkLabel( 00591 $this->msg( 'sp-contributions-toponly' )->text(), 00592 'topOnly', 00593 'mw-show-top-only', 00594 $this->opts['topOnly'], 00595 array( 'class' => 'mw-input' ) 00596 ) 00597 ); 00598 $checkLabelNewOnly = Html::rawElement( 00599 'span', 00600 array( 'style' => 'white-space: nowrap' ), 00601 Xml::checkLabel( 00602 $this->msg( 'sp-contributions-newonly' )->text(), 00603 'newOnly', 00604 'mw-show-new-only', 00605 $this->opts['newOnly'], 00606 array( 'class' => 'mw-input' ) 00607 ) 00608 ); 00609 $extraOptions = Html::rawElement( 00610 'td', 00611 array( 'colspan' => 2 ), 00612 $deletedOnlyCheck . $checkLabelTopOnly . $checkLabelNewOnly 00613 ); 00614 00615 $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ), 00616 Xml::dateMenu( 00617 $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'], 00618 $this->opts['month'] 00619 ) . ' ' . 00620 Xml::submitButton( 00621 $this->msg( 'sp-contributions-submit' )->text(), 00622 array( 'class' => 'mw-submit' ) 00623 ) 00624 ); 00625 00626 $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() ); 00627 $form .= Html::rawElement( 'table', array( 'class' => 'mw-contributions-table' ), "\n" . 00628 Html::rawElement( 'tr', array(), $targetSelection ) . "\n" . 00629 Html::rawElement( 'tr', array(), $namespaceSelection ) . "\n" . 00630 Html::rawElement( 'tr', array(), $filterSelection ) . "\n" . 00631 Html::rawElement( 'tr', array(), $extraOptions ) . "\n" . 00632 Html::rawElement( 'tr', array(), $dateSelectionAndSubmit ) . "\n" 00633 ); 00634 00635 $explain = $this->msg( 'sp-contributions-explain' ); 00636 if ( !$explain->isBlank() ) { 00637 $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>"; 00638 } 00639 00640 $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); 00641 00642 return $form; 00643 } 00644 00645 protected function getGroupName() { 00646 return 'users'; 00647 } 00648 } 00649 00654 class ContribsPager extends ReverseChronologicalPager { 00655 public $mDefaultDirection = IndexPager::DIR_DESCENDING; 00656 public $messages; 00657 public $target; 00658 public $namespace = ''; 00659 public $mDb; 00660 public $preventClickjacking = false; 00661 00663 public $mDbSecondary; 00664 00668 protected $mParentLens; 00669 00670 function __construct( IContextSource $context, array $options ) { 00671 parent::__construct( $context ); 00672 00673 $msgs = array( 00674 'diff', 00675 'hist', 00676 'newarticle', 00677 'pipe-separator', 00678 'rev-delundel', 00679 'rollbacklink', 00680 'uctop' 00681 ); 00682 00683 foreach ( $msgs as $msg ) { 00684 $this->messages[$msg] = $this->msg( $msg )->escaped(); 00685 } 00686 00687 $this->target = isset( $options['target'] ) ? $options['target'] : ''; 00688 $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users'; 00689 $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : ''; 00690 $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false; 00691 $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false; 00692 $this->associated = isset( $options['associated'] ) ? $options['associated'] : false; 00693 00694 $this->deletedOnly = !empty( $options['deletedOnly'] ); 00695 $this->topOnly = !empty( $options['topOnly'] ); 00696 $this->newOnly = !empty( $options['newOnly'] ); 00697 00698 $year = isset( $options['year'] ) ? $options['year'] : false; 00699 $month = isset( $options['month'] ) ? $options['month'] : false; 00700 $this->getDateCond( $year, $month ); 00701 00702 // Most of this code will use the 'contributions' group DB, which can map to slaves 00703 // with extra user based indexes or partioning by user. The additional metadata 00704 // queries should use a regular slave since the lookup pattern is not all by user. 00705 $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave 00706 $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); 00707 } 00708 00709 function getDefaultQuery() { 00710 $query = parent::getDefaultQuery(); 00711 $query['target'] = $this->target; 00712 00713 return $query; 00714 } 00715 00725 function reallyDoQuery( $offset, $limit, $descending ) { 00726 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( 00727 $offset, 00728 $limit, 00729 $descending 00730 ); 00731 $pager = $this; 00732 00733 /* 00734 * This hook will allow extensions to add in additional queries, so they can get their data 00735 * in My Contributions as well. Extensions should append their results to the $data array. 00736 * 00737 * Extension queries have to implement the navbar requirement as well. They should 00738 * - have a column aliased as $pager->getIndexField() 00739 * - have LIMIT set 00740 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset 00741 * - have the ORDER BY specified based upon the details provided by the navbar 00742 * 00743 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY 00744 * 00745 * &$data: an array of results of all contribs queries 00746 * $pager: the ContribsPager object hooked into 00747 * $offset: see phpdoc above 00748 * $limit: see phpdoc above 00749 * $descending: see phpdoc above 00750 */ 00751 $data = array( $this->mDb->select( 00752 $tables, $fields, $conds, $fname, $options, $join_conds 00753 ) ); 00754 wfRunHooks( 00755 'ContribsPager::reallyDoQuery', 00756 array( &$data, $pager, $offset, $limit, $descending ) 00757 ); 00758 00759 $result = array(); 00760 00761 // loop all results and collect them in an array 00762 foreach ( $data as $query ) { 00763 foreach ( $query as $i => $row ) { 00764 // use index column as key, allowing us to easily sort in PHP 00765 $result[$row->{$this->getIndexField()} . "-$i"] = $row; 00766 } 00767 } 00768 00769 // sort results 00770 if ( $descending ) { 00771 ksort( $result ); 00772 } else { 00773 krsort( $result ); 00774 } 00775 00776 // enforce limit 00777 $result = array_slice( $result, 0, $limit ); 00778 00779 // get rid of array keys 00780 $result = array_values( $result ); 00781 00782 return new FakeResultWrapper( $result ); 00783 } 00784 00785 function getQueryInfo() { 00786 list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); 00787 00788 $user = $this->getUser(); 00789 $conds = array_merge( $userCond, $this->getNamespaceCond() ); 00790 00791 // Paranoia: avoid brute force searches (bug 17342) 00792 if ( !$user->isAllowed( 'deletedhistory' ) ) { 00793 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; 00794 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { 00795 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . 00796 ' != ' . Revision::SUPPRESSED_USER; 00797 } 00798 00799 # Don't include orphaned revisions 00800 $join_cond['page'] = Revision::pageJoinCond(); 00801 # Get the current user name for accounts 00802 $join_cond['user'] = Revision::userJoinCond(); 00803 00804 $options = array(); 00805 if ( $index ) { 00806 $options['USE INDEX'] = array( 'revision' => $index ); 00807 } 00808 00809 $queryInfo = array( 00810 'tables' => $tables, 00811 'fields' => array_merge( 00812 Revision::selectFields(), 00813 Revision::selectUserFields(), 00814 array( 'page_namespace', 'page_title', 'page_is_new', 00815 'page_latest', 'page_is_redirect', 'page_len' ) 00816 ), 00817 'conds' => $conds, 00818 'options' => $options, 00819 'join_conds' => $join_cond 00820 ); 00821 00822 ChangeTags::modifyDisplayQuery( 00823 $queryInfo['tables'], 00824 $queryInfo['fields'], 00825 $queryInfo['conds'], 00826 $queryInfo['join_conds'], 00827 $queryInfo['options'], 00828 $this->tagFilter 00829 ); 00830 00831 wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); 00832 00833 return $queryInfo; 00834 } 00835 00836 function getUserCond() { 00837 $condition = array(); 00838 $join_conds = array(); 00839 $tables = array( 'revision', 'page', 'user' ); 00840 $index = false; 00841 if ( $this->contribs == 'newbie' ) { 00842 $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); 00843 $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); 00844 # ignore local groups with the bot right 00845 # @todo FIXME: Global groups may have 'bot' rights 00846 $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); 00847 if ( count( $groupsWithBotPermission ) ) { 00848 $tables[] = 'user_groups'; 00849 $condition[] = 'ug_group IS NULL'; 00850 $join_conds['user_groups'] = array( 00851 'LEFT JOIN', array( 00852 'ug_user = rev_user', 00853 'ug_group' => $groupsWithBotPermission 00854 ) 00855 ); 00856 } 00857 } else { 00858 $uid = User::idFromName( $this->target ); 00859 if ( $uid ) { 00860 $condition['rev_user'] = $uid; 00861 $index = 'user_timestamp'; 00862 } else { 00863 $condition['rev_user_text'] = $this->target; 00864 $index = 'usertext_timestamp'; 00865 } 00866 } 00867 00868 if ( $this->deletedOnly ) { 00869 $condition[] = 'rev_deleted != 0'; 00870 } 00871 00872 if ( $this->topOnly ) { 00873 $condition[] = 'rev_id = page_latest'; 00874 } 00875 00876 if ( $this->newOnly ) { 00877 $condition[] = 'rev_parent_id = 0'; 00878 } 00879 00880 return array( $tables, $index, $condition, $join_conds ); 00881 } 00882 00883 function getNamespaceCond() { 00884 if ( $this->namespace !== '' ) { 00885 $selectedNS = $this->mDb->addQuotes( $this->namespace ); 00886 $eq_op = $this->nsInvert ? '!=' : '='; 00887 $bool_op = $this->nsInvert ? 'AND' : 'OR'; 00888 00889 if ( !$this->associated ) { 00890 return array( "page_namespace $eq_op $selectedNS" ); 00891 } 00892 00893 $associatedNS = $this->mDb->addQuotes( 00894 MWNamespace::getAssociated( $this->namespace ) 00895 ); 00896 00897 return array( 00898 "page_namespace $eq_op $selectedNS " . 00899 $bool_op . 00900 " page_namespace $eq_op $associatedNS" 00901 ); 00902 } 00903 00904 return array(); 00905 } 00906 00907 function getIndexField() { 00908 return 'rev_timestamp'; 00909 } 00910 00911 function doBatchLookups() { 00912 # Do a link batch query 00913 $this->mResult->seek( 0 ); 00914 $revIds = array(); 00915 $batch = new LinkBatch(); 00916 # Give some pointers to make (last) links 00917 foreach ( $this->mResult as $row ) { 00918 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { 00919 $revIds[] = $row->rev_parent_id; 00920 } 00921 if ( isset( $row->rev_id ) ) { 00922 if ( $this->contribs === 'newbie' ) { // multiple users 00923 $batch->add( NS_USER, $row->user_name ); 00924 $batch->add( NS_USER_TALK, $row->user_name ); 00925 } 00926 $batch->add( $row->page_namespace, $row->page_title ); 00927 } 00928 } 00929 $this->mParentLens = Revision::getParentLengths( $this->mDbSecondary, $revIds ); 00930 $batch->execute(); 00931 $this->mResult->seek( 0 ); 00932 } 00933 00937 function getStartBody() { 00938 return "<ul>\n"; 00939 } 00940 00944 function getEndBody() { 00945 return "</ul>\n"; 00946 } 00947 00960 function formatRow( $row ) { 00961 wfProfileIn( __METHOD__ ); 00962 00963 $ret = ''; 00964 $classes = array(); 00965 00966 /* 00967 * There may be more than just revision rows. To make sure that we'll only be processing 00968 * revisions here, let's _try_ to build a revision out of our row (without displaying 00969 * notices though) and then trying to grab data from the built object. If we succeed, 00970 * we're definitely dealing with revision data and we may proceed, if not, we'll leave it 00971 * to extensions to subscribe to the hook to parse the row. 00972 */ 00973 wfSuppressWarnings(); 00974 try { 00975 $rev = new Revision( $row ); 00976 $validRevision = (bool)$rev->getId(); 00977 } catch ( MWException $e ) { 00978 $validRevision = false; 00979 } 00980 wfRestoreWarnings(); 00981 00982 if ( $validRevision ) { 00983 $classes = array(); 00984 00985 $page = Title::newFromRow( $row ); 00986 $link = Linker::link( 00987 $page, 00988 htmlspecialchars( $page->getPrefixedText() ), 00989 array( 'class' => 'mw-contributions-title' ), 00990 $page->isRedirect() ? array( 'redirect' => 'no' ) : array() 00991 ); 00992 # Mark current revisions 00993 $topmarktext = ''; 00994 $user = $this->getUser(); 00995 if ( $row->rev_id == $row->page_latest ) { 00996 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; 00997 # Add rollback link 00998 if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) 00999 && $page->quickUserCan( 'edit', $user ) 01000 ) { 01001 $this->preventClickjacking(); 01002 $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); 01003 } 01004 } 01005 # Is there a visible previous revision? 01006 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { 01007 $difftext = Linker::linkKnown( 01008 $page, 01009 $this->messages['diff'], 01010 array(), 01011 array( 01012 'diff' => 'prev', 01013 'oldid' => $row->rev_id 01014 ) 01015 ); 01016 } else { 01017 $difftext = $this->messages['diff']; 01018 } 01019 $histlink = Linker::linkKnown( 01020 $page, 01021 $this->messages['hist'], 01022 array(), 01023 array( 'action' => 'history' ) 01024 ); 01025 01026 if ( $row->rev_parent_id === null ) { 01027 // For some reason rev_parent_id isn't populated for this row. 01028 // Its rumoured this is true on wikipedia for some revisions (bug 34922). 01029 // Next best thing is to have the total number of bytes. 01030 $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; 01031 $chardiff .= Linker::formatRevisionSize( $row->rev_len ); 01032 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; 01033 } else { 01034 $parentLen = 0; 01035 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) { 01036 $parentLen = $this->mParentLens[$row->rev_parent_id]; 01037 } 01038 01039 $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; 01040 $chardiff .= ChangesList::showCharacterDifference( 01041 $parentLen, 01042 $row->rev_len, 01043 $this->getContext() 01044 ); 01045 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; 01046 } 01047 01048 $lang = $this->getLanguage(); 01049 $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); 01050 $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); 01051 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 01052 $d = Linker::linkKnown( 01053 $page, 01054 htmlspecialchars( $date ), 01055 array( 'class' => 'mw-changeslist-date' ), 01056 array( 'oldid' => intval( $row->rev_id ) ) 01057 ); 01058 } else { 01059 $d = htmlspecialchars( $date ); 01060 } 01061 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 01062 $d = '<span class="history-deleted">' . $d . '</span>'; 01063 } 01064 01065 # Show user names for /newbies as there may be different users. 01066 # Note that we already excluded rows with hidden user names. 01067 if ( $this->contribs == 'newbie' ) { 01068 $userlink = ' . . ' . $lang->getDirMark() 01069 . Linker::userLink( $rev->getUser(), $rev->getUserText() ); 01070 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( 01071 Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; 01072 } else { 01073 $userlink = ''; 01074 } 01075 01076 if ( $rev->getParentId() === 0 ) { 01077 $nflag = ChangesList::flag( 'newpage' ); 01078 } else { 01079 $nflag = ''; 01080 } 01081 01082 if ( $rev->isMinor() ) { 01083 $mflag = ChangesList::flag( 'minor' ); 01084 } else { 01085 $mflag = ''; 01086 } 01087 01088 $del = Linker::getRevDeleteLink( $user, $rev, $page ); 01089 if ( $del !== '' ) { 01090 $del .= ' '; 01091 } 01092 01093 $diffHistLinks = $this->msg( 'parentheses' ) 01094 ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink ) 01095 ->escaped(); 01096 $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} "; 01097 $ret .= "{$link}{$userlink} {$comment} {$topmarktext}"; 01098 01099 # Denote if username is redacted for this edit 01100 if ( $rev->isDeleted( Revision::DELETED_USER ) ) { 01101 $ret .= " <strong>" . 01102 $this->msg( 'rev-deleted-user-contribs' )->escaped() . 01103 "</strong>"; 01104 } 01105 01106 # Tags, if any. 01107 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( 01108 $row->ts_tags, 01109 'contributions' 01110 ); 01111 $classes = array_merge( $classes, $newClasses ); 01112 $ret .= " $tagSummary"; 01113 } 01114 01115 // Let extensions add data 01116 wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); 01117 01118 if ( $classes === array() && $ret === '' ) { 01119 wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); 01120 $ret = "<!-- Could not format Special:Contribution row. -->\n"; 01121 } else { 01122 $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n"; 01123 } 01124 01125 wfProfileOut( __METHOD__ ); 01126 01127 return $ret; 01128 } 01129 01134 function getSqlComment() { 01135 if ( $this->namespace || $this->deletedOnly ) { 01136 // potentially slow, see CR r58153 01137 return 'contributions page filtered for namespace or RevisionDeleted edits'; 01138 } else { 01139 return 'contributions page unfiltered'; 01140 } 01141 } 01142 01143 protected function preventClickjacking() { 01144 $this->preventClickjacking = true; 01145 } 01146 01150 public function getPreventClickjacking() { 01151 return $this->preventClickjacking; 01152 } 01153 }