MediaWiki
REL1_23
|
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 compatability 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 $out->addHTML( 00207 '<p>' . $pager->getNavigationBar() . '</p>' . 00208 $pager->getBody() . 00209 '<p>' . $pager->getNavigationBar() . '</p>' 00210 ); 00211 } 00212 $out->preventClickjacking( $pager->getPreventClickjacking() ); 00213 00214 # Show the appropriate "footer" message - WHOIS tools, etc. 00215 if ( $this->opts['contribs'] == 'newbie' ) { 00216 $message = 'sp-contributions-footer-newbies'; 00217 } elseif ( IP::isIPAddress( $target ) ) { 00218 $message = 'sp-contributions-footer-anon'; 00219 } elseif ( $userObj->isAnon() ) { 00220 // No message for non-existing users 00221 $message = ''; 00222 } else { 00223 $message = 'sp-contributions-footer'; 00224 } 00225 00226 if ( $message ) { 00227 if ( !$this->including() ) { 00228 if ( !$this->msg( $message, $target )->isDisabled() ) { 00229 $out->wrapWikiMsg( 00230 "<div class='mw-contributions-footer'>\n$1\n</div>", 00231 array( $message, $target ) ); 00232 } 00233 } 00234 } 00235 } 00236 } 00237 00245 protected function contributionsSub( $userObj ) { 00246 if ( $userObj->isAnon() ) { 00247 // Show a warning message that the user being searched for doesn't exists 00248 if ( !User::isIP( $userObj ) ) { 00249 $this->getOutput()->wrapWikiMsg( 00250 "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", 00251 array( 00252 'contributions-userdoesnotexist', 00253 wfEscapeWikiText( $userObj->getName() ), 00254 ) 00255 ); 00256 } 00257 $user = htmlspecialchars( $userObj->getName() ); 00258 } else { 00259 $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) ); 00260 } 00261 $nt = $userObj->getUserPage(); 00262 $talk = $userObj->getTalkPage(); 00263 $links = ''; 00264 if ( $talk ) { 00265 $tools = $this->getUserLinks( $nt, $talk, $userObj ); 00266 $links = $this->getLanguage()->pipeList( $tools ); 00267 00268 // Show a note if the user is blocked and display the last block log entry. 00269 // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, 00270 // and also this will display a totally irrelevant log entry as a current block. 00271 if ( $userObj->isBlocked() && $userObj->getBlock()->getType() != Block::TYPE_AUTO ) { 00272 $out = $this->getOutput(); // showLogExtract() wants first parameter by reference 00273 LogEventsList::showLogExtract( 00274 $out, 00275 'block', 00276 $nt, 00277 '', 00278 array( 00279 'lim' => 1, 00280 'showIfEmpty' => false, 00281 'msgKey' => array( 00282 $userObj->isAnon() ? 00283 'sp-contributions-blocked-notice-anon' : 00284 'sp-contributions-blocked-notice', 00285 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' 00286 ), 00287 'offset' => '' # don't use WebRequest parameter offset 00288 ) 00289 ); 00290 } 00291 } 00292 00293 return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() ); 00294 } 00295 00303 public function getUserLinks( Title $userpage, Title $talkpage, User $target ) { 00304 00305 $id = $target->getId(); 00306 $username = $target->getName(); 00307 00308 $tools[] = Linker::link( $talkpage, $this->msg( 'sp-contributions-talk' )->escaped() ); 00309 00310 if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) { 00311 if ( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links 00312 if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) { 00313 $tools[] = Linker::linkKnown( # Change block link 00314 SpecialPage::getTitleFor( 'Block', $username ), 00315 $this->msg( 'change-blocklink' )->escaped() 00316 ); 00317 $tools[] = Linker::linkKnown( # Unblock link 00318 SpecialPage::getTitleFor( 'Unblock', $username ), 00319 $this->msg( 'unblocklink' )->escaped() 00320 ); 00321 } else { # User is not blocked 00322 $tools[] = Linker::linkKnown( # Block link 00323 SpecialPage::getTitleFor( 'Block', $username ), 00324 $this->msg( 'blocklink' )->escaped() 00325 ); 00326 } 00327 } 00328 00329 # Block log link 00330 $tools[] = Linker::linkKnown( 00331 SpecialPage::getTitleFor( 'Log', 'block' ), 00332 $this->msg( 'sp-contributions-blocklog' )->escaped(), 00333 array(), 00334 array( 'page' => $userpage->getPrefixedText() ) 00335 ); 00336 00337 # Suppression log link (bug 59120) 00338 if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) { 00339 $tools[] = Linker::linkKnown( 00340 SpecialPage::getTitleFor( 'Log', 'suppress' ), 00341 $this->msg( 'sp-contributions-suppresslog' )->escaped(), 00342 array(), 00343 array( 'offender' => $username ) 00344 ); 00345 } 00346 } 00347 # Uploads 00348 $tools[] = Linker::linkKnown( 00349 SpecialPage::getTitleFor( 'Listfiles', $username ), 00350 $this->msg( 'sp-contributions-uploads' )->escaped() 00351 ); 00352 00353 # Other logs link 00354 $tools[] = Linker::linkKnown( 00355 SpecialPage::getTitleFor( 'Log', $username ), 00356 $this->msg( 'sp-contributions-logs' )->escaped() 00357 ); 00358 00359 # Add link to deleted user contributions for priviledged users 00360 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00361 $tools[] = Linker::linkKnown( 00362 SpecialPage::getTitleFor( 'DeletedContributions', $username ), 00363 $this->msg( 'sp-contributions-deleted' )->escaped() 00364 ); 00365 } 00366 00367 # Add a link to change user rights for privileged users 00368 $userrightsPage = new UserrightsPage(); 00369 $userrightsPage->setContext( $this->getContext() ); 00370 if ( $userrightsPage->userCanChangeRights( $target ) ) { 00371 $tools[] = Linker::linkKnown( 00372 SpecialPage::getTitleFor( 'Userrights', $username ), 00373 $this->msg( 'sp-contributions-userrights' )->escaped() 00374 ); 00375 } 00376 00377 wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) ); 00378 00379 return $tools; 00380 } 00381 00386 protected function getForm() { 00387 global $wgScript; 00388 00389 $this->opts['title'] = $this->getPageTitle()->getPrefixedText(); 00390 if ( !isset( $this->opts['target'] ) ) { 00391 $this->opts['target'] = ''; 00392 } else { 00393 $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] ); 00394 } 00395 00396 if ( !isset( $this->opts['namespace'] ) ) { 00397 $this->opts['namespace'] = ''; 00398 } 00399 00400 if ( !isset( $this->opts['nsInvert'] ) ) { 00401 $this->opts['nsInvert'] = ''; 00402 } 00403 00404 if ( !isset( $this->opts['associated'] ) ) { 00405 $this->opts['associated'] = false; 00406 } 00407 00408 if ( !isset( $this->opts['contribs'] ) ) { 00409 $this->opts['contribs'] = 'user'; 00410 } 00411 00412 if ( !isset( $this->opts['year'] ) ) { 00413 $this->opts['year'] = ''; 00414 } 00415 00416 if ( !isset( $this->opts['month'] ) ) { 00417 $this->opts['month'] = ''; 00418 } 00419 00420 if ( $this->opts['contribs'] == 'newbie' ) { 00421 $this->opts['target'] = ''; 00422 } 00423 00424 if ( !isset( $this->opts['tagfilter'] ) ) { 00425 $this->opts['tagfilter'] = ''; 00426 } 00427 00428 if ( !isset( $this->opts['topOnly'] ) ) { 00429 $this->opts['topOnly'] = false; 00430 } 00431 00432 if ( !isset( $this->opts['newOnly'] ) ) { 00433 $this->opts['newOnly'] = false; 00434 } 00435 00436 $form = Html::openElement( 00437 'form', 00438 array( 00439 'method' => 'get', 00440 'action' => $wgScript, 00441 'class' => 'mw-contributions-form' 00442 ) 00443 ); 00444 00445 # Add hidden params for tracking except for parameters in $skipParameters 00446 $skipParameters = array( 00447 'namespace', 00448 'nsInvert', 00449 'deletedOnly', 00450 'target', 00451 'contribs', 00452 'year', 00453 'month', 00454 'topOnly', 00455 'newOnly', 00456 'associated' 00457 ); 00458 00459 foreach ( $this->opts as $name => $value ) { 00460 if ( in_array( $name, $skipParameters ) ) { 00461 continue; 00462 } 00463 $form .= "\t" . Html::hidden( $name, $value ) . "\n"; 00464 } 00465 00466 $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ); 00467 00468 if ( $tagFilter ) { 00469 $filterSelection = Html::rawElement( 00470 'td', 00471 array( 'class' => 'mw-label' ), 00472 array_shift( $tagFilter ) 00473 ); 00474 $filterSelection .= Html::rawElement( 00475 'td', 00476 array( 'class' => 'mw-input' ), 00477 implode( ' ', $tagFilter ) 00478 ); 00479 } else { 00480 $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' ); 00481 } 00482 00483 $labelNewbies = Xml::radioLabel( 00484 $this->msg( 'sp-contributions-newbies' )->text(), 00485 'contribs', 00486 'newbie', 00487 'newbie', 00488 $this->opts['contribs'] == 'newbie', 00489 array( 'class' => 'mw-input' ) 00490 ); 00491 $labelUsername = Xml::radioLabel( 00492 $this->msg( 'sp-contributions-username' )->text(), 00493 'contribs', 00494 'user', 00495 'user', 00496 $this->opts['contribs'] == 'user', 00497 array( 'class' => 'mw-input' ) 00498 ); 00499 $input = Html::input( 00500 'target', 00501 $this->opts['target'], 00502 'text', 00503 array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) + 00504 ( $this->opts['target'] ? array() : array( 'autofocus' ) 00505 ) 00506 ); 00507 $targetSelection = Html::rawElement( 00508 'td', 00509 array( 'colspan' => 2 ), 00510 $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' ' 00511 ); 00512 00513 $namespaceSelection = Xml::tags( 00514 'td', 00515 array( 'class' => 'mw-label' ), 00516 Xml::label( 00517 $this->msg( 'namespace' )->text(), 00518 'namespace', 00519 '' 00520 ) 00521 ); 00522 $namespaceSelection .= Html::rawElement( 00523 'td', 00524 null, 00525 Html::namespaceSelector( 00526 array( 'selected' => $this->opts['namespace'], 'all' => '' ), 00527 array( 00528 'name' => 'namespace', 00529 'id' => 'namespace', 00530 'class' => 'namespaceselector', 00531 ) 00532 ) . ' ' . 00533 Html::rawElement( 00534 'span', 00535 array( 'style' => 'white-space: nowrap' ), 00536 Xml::checkLabel( 00537 $this->msg( 'invert' )->text(), 00538 'nsInvert', 00539 'nsInvert', 00540 $this->opts['nsInvert'], 00541 array( 00542 'title' => $this->msg( 'tooltip-invert' )->text(), 00543 'class' => 'mw-input' 00544 ) 00545 ) . ' ' 00546 ) . 00547 Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ), 00548 Xml::checkLabel( 00549 $this->msg( 'namespace_association' )->text(), 00550 'associated', 00551 'associated', 00552 $this->opts['associated'], 00553 array( 00554 'title' => $this->msg( 'tooltip-namespace_association' )->text(), 00555 'class' => 'mw-input' 00556 ) 00557 ) . ' ' 00558 ) 00559 ); 00560 00561 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00562 $deletedOnlyCheck = Html::rawElement( 00563 'span', 00564 array( 'style' => 'white-space: nowrap' ), 00565 Xml::checkLabel( 00566 $this->msg( 'history-show-deleted' )->text(), 00567 'deletedOnly', 00568 'mw-show-deleted-only', 00569 $this->opts['deletedOnly'], 00570 array( 'class' => 'mw-input' ) 00571 ) 00572 ); 00573 } else { 00574 $deletedOnlyCheck = ''; 00575 } 00576 00577 $checkLabelTopOnly = Html::rawElement( 00578 'span', 00579 array( 'style' => 'white-space: nowrap' ), 00580 Xml::checkLabel( 00581 $this->msg( 'sp-contributions-toponly' )->text(), 00582 'topOnly', 00583 'mw-show-top-only', 00584 $this->opts['topOnly'], 00585 array( 'class' => 'mw-input' ) 00586 ) 00587 ); 00588 $checkLabelNewOnly = Html::rawElement( 00589 'span', 00590 array( 'style' => 'white-space: nowrap' ), 00591 Xml::checkLabel( 00592 $this->msg( 'sp-contributions-newonly' )->text(), 00593 'newOnly', 00594 'mw-show-new-only', 00595 $this->opts['newOnly'], 00596 array( 'class' => 'mw-input' ) 00597 ) 00598 ); 00599 $extraOptions = Html::rawElement( 00600 'td', 00601 array( 'colspan' => 2 ), 00602 $deletedOnlyCheck . $checkLabelTopOnly . $checkLabelNewOnly 00603 ); 00604 00605 $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ), 00606 Xml::dateMenu( 00607 $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'], 00608 $this->opts['month'] 00609 ) . ' ' . 00610 Xml::submitButton( 00611 $this->msg( 'sp-contributions-submit' )->text(), 00612 array( 'class' => 'mw-submit' ) 00613 ) 00614 ); 00615 00616 $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() ); 00617 $form .= Html::rawElement( 'table', array( 'class' => 'mw-contributions-table' ), "\n" . 00618 Html::rawElement( 'tr', array(), $targetSelection ) . "\n" . 00619 Html::rawElement( 'tr', array(), $namespaceSelection ) . "\n" . 00620 Html::rawElement( 'tr', array(), $filterSelection ) . "\n" . 00621 Html::rawElement( 'tr', array(), $extraOptions ) . "\n" . 00622 Html::rawElement( 'tr', array(), $dateSelectionAndSubmit ) . "\n" 00623 ); 00624 00625 $explain = $this->msg( 'sp-contributions-explain' ); 00626 if ( !$explain->isBlank() ) { 00627 $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>"; 00628 } 00629 00630 $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); 00631 00632 return $form; 00633 } 00634 00635 protected function getGroupName() { 00636 return 'users'; 00637 } 00638 } 00639 00644 class ContribsPager extends ReverseChronologicalPager { 00645 public $mDefaultDirection = true; 00646 public $messages; 00647 public $target; 00648 public $namespace = ''; 00649 public $mDb; 00650 public $preventClickjacking = false; 00651 00653 public $mDbSecondary; 00654 00658 protected $mParentLens; 00659 00660 function __construct( IContextSource $context, array $options ) { 00661 parent::__construct( $context ); 00662 00663 $msgs = array( 00664 'diff', 00665 'hist', 00666 'newarticle', 00667 'pipe-separator', 00668 'rev-delundel', 00669 'rollbacklink', 00670 'uctop' 00671 ); 00672 00673 foreach ( $msgs as $msg ) { 00674 $this->messages[$msg] = $this->msg( $msg )->escaped(); 00675 } 00676 00677 $this->target = isset( $options['target'] ) ? $options['target'] : ''; 00678 $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users'; 00679 $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : ''; 00680 $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false; 00681 $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false; 00682 $this->associated = isset( $options['associated'] ) ? $options['associated'] : false; 00683 00684 $this->deletedOnly = !empty( $options['deletedOnly'] ); 00685 $this->topOnly = !empty( $options['topOnly'] ); 00686 $this->newOnly = !empty( $options['newOnly'] ); 00687 00688 $year = isset( $options['year'] ) ? $options['year'] : false; 00689 $month = isset( $options['month'] ) ? $options['month'] : false; 00690 $this->getDateCond( $year, $month ); 00691 00692 // Most of this code will use the 'contributions' group DB, which can map to slaves 00693 // with extra user based indexes or partioning by user. The additional metadata 00694 // queries should use a regular slave since the lookup pattern is not all by user. 00695 $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave 00696 $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); 00697 } 00698 00699 function getDefaultQuery() { 00700 $query = parent::getDefaultQuery(); 00701 $query['target'] = $this->target; 00702 00703 return $query; 00704 } 00705 00715 function reallyDoQuery( $offset, $limit, $descending ) { 00716 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( 00717 $offset, 00718 $limit, 00719 $descending 00720 ); 00721 $pager = $this; 00722 00723 /* 00724 * This hook will allow extensions to add in additional queries, so they can get their data 00725 * in My Contributions as well. Extensions should append their results to the $data array. 00726 * 00727 * Extension queries have to implement the navbar requirement as well. They should 00728 * - have a column aliased as $pager->getIndexField() 00729 * - have LIMIT set 00730 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset 00731 * - have the ORDER BY specified based upon the details provided by the navbar 00732 * 00733 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY 00734 * 00735 * &$data: an array of results of all contribs queries 00736 * $pager: the ContribsPager object hooked into 00737 * $offset: see phpdoc above 00738 * $limit: see phpdoc above 00739 * $descending: see phpdoc above 00740 */ 00741 $data = array( $this->mDb->select( 00742 $tables, $fields, $conds, $fname, $options, $join_conds 00743 ) ); 00744 wfRunHooks( 00745 'ContribsPager::reallyDoQuery', 00746 array( &$data, $pager, $offset, $limit, $descending ) 00747 ); 00748 00749 $result = array(); 00750 00751 // loop all results and collect them in an array 00752 foreach ( $data as $query ) { 00753 foreach ( $query as $i => $row ) { 00754 // use index column as key, allowing us to easily sort in PHP 00755 $result[$row->{$this->getIndexField()} . "-$i"] = $row; 00756 } 00757 } 00758 00759 // sort results 00760 if ( $descending ) { 00761 ksort( $result ); 00762 } else { 00763 krsort( $result ); 00764 } 00765 00766 // enforce limit 00767 $result = array_slice( $result, 0, $limit ); 00768 00769 // get rid of array keys 00770 $result = array_values( $result ); 00771 00772 return new FakeResultWrapper( $result ); 00773 } 00774 00775 function getQueryInfo() { 00776 list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); 00777 00778 $user = $this->getUser(); 00779 $conds = array_merge( $userCond, $this->getNamespaceCond() ); 00780 00781 // Paranoia: avoid brute force searches (bug 17342) 00782 if ( !$user->isAllowed( 'deletedhistory' ) ) { 00783 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; 00784 } elseif ( !$user->isAllowed( 'suppressrevision' ) ) { 00785 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . 00786 ' != ' . Revision::SUPPRESSED_USER; 00787 } 00788 00789 # Don't include orphaned revisions 00790 $join_cond['page'] = Revision::pageJoinCond(); 00791 # Get the current user name for accounts 00792 $join_cond['user'] = Revision::userJoinCond(); 00793 00794 $options = array(); 00795 if ( $index ) { 00796 $options['USE INDEX'] = array( 'revision' => $index ); 00797 } 00798 00799 $queryInfo = array( 00800 'tables' => $tables, 00801 'fields' => array_merge( 00802 Revision::selectFields(), 00803 Revision::selectUserFields(), 00804 array( 'page_namespace', 'page_title', 'page_is_new', 00805 'page_latest', 'page_is_redirect', 'page_len' ) 00806 ), 00807 'conds' => $conds, 00808 'options' => $options, 00809 'join_conds' => $join_cond 00810 ); 00811 00812 ChangeTags::modifyDisplayQuery( 00813 $queryInfo['tables'], 00814 $queryInfo['fields'], 00815 $queryInfo['conds'], 00816 $queryInfo['join_conds'], 00817 $queryInfo['options'], 00818 $this->tagFilter 00819 ); 00820 00821 wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); 00822 00823 return $queryInfo; 00824 } 00825 00826 function getUserCond() { 00827 $condition = array(); 00828 $join_conds = array(); 00829 $tables = array( 'revision', 'page', 'user' ); 00830 $index = false; 00831 if ( $this->contribs == 'newbie' ) { 00832 $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); 00833 $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); 00834 # ignore local groups with the bot right 00835 # @todo FIXME: Global groups may have 'bot' rights 00836 $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); 00837 if ( count( $groupsWithBotPermission ) ) { 00838 $tables[] = 'user_groups'; 00839 $condition[] = 'ug_group IS NULL'; 00840 $join_conds['user_groups'] = array( 00841 'LEFT JOIN', array( 00842 'ug_user = rev_user', 00843 'ug_group' => $groupsWithBotPermission 00844 ) 00845 ); 00846 } 00847 } else { 00848 $uid = User::idFromName( $this->target ); 00849 if ( $uid ) { 00850 $condition['rev_user'] = $uid; 00851 $index = 'user_timestamp'; 00852 } else { 00853 $condition['rev_user_text'] = $this->target; 00854 $index = 'usertext_timestamp'; 00855 } 00856 } 00857 00858 if ( $this->deletedOnly ) { 00859 $condition[] = 'rev_deleted != 0'; 00860 } 00861 00862 if ( $this->topOnly ) { 00863 $condition[] = 'rev_id = page_latest'; 00864 } 00865 00866 if ( $this->newOnly ) { 00867 $condition[] = 'rev_parent_id = 0'; 00868 } 00869 00870 return array( $tables, $index, $condition, $join_conds ); 00871 } 00872 00873 function getNamespaceCond() { 00874 if ( $this->namespace !== '' ) { 00875 $selectedNS = $this->mDb->addQuotes( $this->namespace ); 00876 $eq_op = $this->nsInvert ? '!=' : '='; 00877 $bool_op = $this->nsInvert ? 'AND' : 'OR'; 00878 00879 if ( !$this->associated ) { 00880 return array( "page_namespace $eq_op $selectedNS" ); 00881 } 00882 00883 $associatedNS = $this->mDb->addQuotes( 00884 MWNamespace::getAssociated( $this->namespace ) 00885 ); 00886 00887 return array( 00888 "page_namespace $eq_op $selectedNS " . 00889 $bool_op . 00890 " page_namespace $eq_op $associatedNS" 00891 ); 00892 } 00893 00894 return array(); 00895 } 00896 00897 function getIndexField() { 00898 return 'rev_timestamp'; 00899 } 00900 00901 function doBatchLookups() { 00902 # Do a link batch query 00903 $this->mResult->seek( 0 ); 00904 $revIds = array(); 00905 $batch = new LinkBatch(); 00906 # Give some pointers to make (last) links 00907 foreach ( $this->mResult as $row ) { 00908 if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { 00909 $revIds[] = $row->rev_parent_id; 00910 } 00911 if ( isset( $row->rev_id ) ) { 00912 if ( $this->contribs === 'newbie' ) { // multiple users 00913 $batch->add( NS_USER, $row->user_name ); 00914 $batch->add( NS_USER_TALK, $row->user_name ); 00915 } 00916 $batch->add( $row->page_namespace, $row->page_title ); 00917 } 00918 } 00919 $this->mParentLens = Revision::getParentLengths( $this->mDbSecondary, $revIds ); 00920 $batch->execute(); 00921 $this->mResult->seek( 0 ); 00922 } 00923 00927 function getStartBody() { 00928 return "<ul>\n"; 00929 } 00930 00934 function getEndBody() { 00935 return "</ul>\n"; 00936 } 00937 00950 function formatRow( $row ) { 00951 wfProfileIn( __METHOD__ ); 00952 00953 $ret = ''; 00954 $classes = array(); 00955 00956 /* 00957 * There may be more than just revision rows. To make sure that we'll only be processing 00958 * revisions here, let's _try_ to build a revision out of our row (without displaying 00959 * notices though) and then trying to grab data from the built object. If we succeed, 00960 * we're definitely dealing with revision data and we may proceed, if not, we'll leave it 00961 * to extensions to subscribe to the hook to parse the row. 00962 */ 00963 wfSuppressWarnings(); 00964 try { 00965 $rev = new Revision( $row ); 00966 $validRevision = (bool)$rev->getId(); 00967 } catch ( MWException $e ) { 00968 $validRevision = false; 00969 } 00970 wfRestoreWarnings(); 00971 00972 if ( $validRevision ) { 00973 $classes = array(); 00974 00975 $page = Title::newFromRow( $row ); 00976 $link = Linker::link( 00977 $page, 00978 htmlspecialchars( $page->getPrefixedText() ), 00979 array( 'class' => 'mw-contributions-title' ), 00980 $page->isRedirect() ? array( 'redirect' => 'no' ) : array() 00981 ); 00982 # Mark current revisions 00983 $topmarktext = ''; 00984 $user = $this->getUser(); 00985 if ( $row->rev_id == $row->page_latest ) { 00986 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; 00987 # Add rollback link 00988 if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) 00989 && $page->quickUserCan( 'edit', $user ) 00990 ) { 00991 $this->preventClickjacking(); 00992 $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); 00993 } 00994 } 00995 # Is there a visible previous revision? 00996 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { 00997 $difftext = Linker::linkKnown( 00998 $page, 00999 $this->messages['diff'], 01000 array(), 01001 array( 01002 'diff' => 'prev', 01003 'oldid' => $row->rev_id 01004 ) 01005 ); 01006 } else { 01007 $difftext = $this->messages['diff']; 01008 } 01009 $histlink = Linker::linkKnown( 01010 $page, 01011 $this->messages['hist'], 01012 array(), 01013 array( 'action' => 'history' ) 01014 ); 01015 01016 if ( $row->rev_parent_id === null ) { 01017 // For some reason rev_parent_id isn't populated for this row. 01018 // Its rumoured this is true on wikipedia for some revisions (bug 34922). 01019 // Next best thing is to have the total number of bytes. 01020 $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; 01021 $chardiff .= Linker::formatRevisionSize( $row->rev_len ); 01022 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; 01023 } else { 01024 $parentLen = 0; 01025 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) { 01026 $parentLen = $this->mParentLens[$row->rev_parent_id]; 01027 } 01028 01029 $chardiff = ' <span class="mw-changeslist-separator">. .</span> '; 01030 $chardiff .= ChangesList::showCharacterDifference( 01031 $parentLen, 01032 $row->rev_len, 01033 $this->getContext() 01034 ); 01035 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> '; 01036 } 01037 01038 $lang = $this->getLanguage(); 01039 $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); 01040 $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); 01041 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 01042 $d = Linker::linkKnown( 01043 $page, 01044 htmlspecialchars( $date ), 01045 array( 'class' => 'mw-changeslist-date' ), 01046 array( 'oldid' => intval( $row->rev_id ) ) 01047 ); 01048 } else { 01049 $d = htmlspecialchars( $date ); 01050 } 01051 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 01052 $d = '<span class="history-deleted">' . $d . '</span>'; 01053 } 01054 01055 # Show user names for /newbies as there may be different users. 01056 # Note that we already excluded rows with hidden user names. 01057 if ( $this->contribs == 'newbie' ) { 01058 $userlink = ' . . ' . $lang->getDirMark() . Linker::userLink( $rev->getUser(), $rev->getUserText() ); 01059 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( 01060 Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; 01061 } else { 01062 $userlink = ''; 01063 } 01064 01065 if ( $rev->getParentId() === 0 ) { 01066 $nflag = ChangesList::flag( 'newpage' ); 01067 } else { 01068 $nflag = ''; 01069 } 01070 01071 if ( $rev->isMinor() ) { 01072 $mflag = ChangesList::flag( 'minor' ); 01073 } else { 01074 $mflag = ''; 01075 } 01076 01077 $del = Linker::getRevDeleteLink( $user, $rev, $page ); 01078 if ( $del !== '' ) { 01079 $del .= ' '; 01080 } 01081 01082 $diffHistLinks = $this->msg( 'parentheses' ) 01083 ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink ) 01084 ->escaped(); 01085 $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} "; 01086 $ret .= "{$link}{$userlink} {$comment} {$topmarktext}"; 01087 01088 # Denote if username is redacted for this edit 01089 if ( $rev->isDeleted( Revision::DELETED_USER ) ) { 01090 $ret .= " <strong>" . 01091 $this->msg( 'rev-deleted-user-contribs' )->escaped() . 01092 "</strong>"; 01093 } 01094 01095 # Tags, if any. 01096 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( 01097 $row->ts_tags, 01098 'contributions' 01099 ); 01100 $classes = array_merge( $classes, $newClasses ); 01101 $ret .= " $tagSummary"; 01102 } 01103 01104 // Let extensions add data 01105 wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); 01106 01107 if ( $classes === array() && $ret === '' ) { 01108 wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); 01109 $ret = "<!-- Could not format Special:Contribution row. -->\n"; 01110 } else { 01111 $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n"; 01112 } 01113 01114 wfProfileOut( __METHOD__ ); 01115 01116 return $ret; 01117 } 01118 01123 function getSqlComment() { 01124 if ( $this->namespace || $this->deletedOnly ) { 01125 // potentially slow, see CR r58153 01126 return 'contributions page filtered for namespace or RevisionDeleted edits'; 01127 } else { 01128 return 'contributions page unfiltered'; 01129 } 01130 } 01131 01132 protected function preventClickjacking() { 01133 $this->preventClickjacking = true; 01134 } 01135 01139 public function getPreventClickjacking() { 01140 return $this->preventClickjacking; 01141 } 01142 }