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