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