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