MediaWiki
REL1_20
|
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 = Xml::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 Xml::tags( 'td', array( 'class' => 'mw-label' ), array_shift( $tagFilter ) ) . 00418 Xml::tags( 'td', array( 'class' => 'mw-input' ), implode( ' ', $tagFilter ) ); 00419 } else { 00420 $filterSelection = Xml::tags( 'td', array( 'colspan' => 2 ), '' ); 00421 } 00422 00423 $targetSelection = Xml::tags( '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 Xml::tags( '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 $extraOptions = Xml::tags( 'td', array( 'colspan' => 2 ), 00489 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 Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ), 00499 Xml::checkLabel( 00500 $this->msg( 'sp-contributions-toponly' )->text(), 00501 'topOnly', 00502 'mw-show-top-only', 00503 $this->opts['topOnly'], 00504 array( 'class' => 'mw-input' ) 00505 ) 00506 ) 00507 ) ; 00508 00509 $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ), 00510 Xml::dateMenu( 00511 $this->opts['year'], 00512 $this->opts['month'] 00513 ) . ' ' . 00514 Xml::submitButton( 00515 $this->msg( 'sp-contributions-submit' )->text(), 00516 array( 'class' => 'mw-submit' ) 00517 ) 00518 ) ; 00519 00520 $form .= 00521 Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() ) . 00522 Xml::openElement( 'table', array( 'class' => 'mw-contributions-table' ) ) . 00523 Xml::openElement( 'tr' ) . 00524 $targetSelection . 00525 Xml::closeElement( 'tr' ) . 00526 Xml::openElement( 'tr' ) . 00527 $namespaceSelection . 00528 Xml::closeElement( 'tr' ) . 00529 Xml::openElement( 'tr' ) . 00530 $filterSelection . 00531 Xml::closeElement( 'tr' ) . 00532 Xml::openElement( 'tr' ) . 00533 $extraOptions . 00534 Xml::closeElement( 'tr' ) . 00535 Xml::openElement( 'tr' ) . 00536 $dateSelectionAndSubmit . 00537 Xml::closeElement( 'tr' ) . 00538 Xml::closeElement( 'table' ); 00539 00540 $explain = $this->msg( 'sp-contributions-explain' ); 00541 if ( $explain->exists() ) { 00542 $form .= "<p id='mw-sp-contributions-explain'>{$explain}</p>"; 00543 } 00544 $form .= Xml::closeElement( 'fieldset' ) . 00545 Xml::closeElement( 'form' ); 00546 return $form; 00547 } 00548 } 00549 00554 class ContribsPager extends ReverseChronologicalPager { 00555 public $mDefaultDirection = true; 00556 var $messages, $target; 00557 var $namespace = '', $mDb; 00558 var $preventClickjacking = false; 00559 00563 protected $mParentLens; 00564 00565 function __construct( IContextSource $context, array $options ) { 00566 parent::__construct( $context ); 00567 00568 $msgs = array( 'uctop', 'diff', 'newarticle', 'rollbacklink', 'diff', 'hist', 'rev-delundel', 'pipe-separator' ); 00569 00570 foreach ( $msgs as $msg ) { 00571 $this->messages[$msg] = $this->msg( $msg )->escaped(); 00572 } 00573 00574 $this->target = isset( $options['target'] ) ? $options['target'] : ''; 00575 $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users'; 00576 $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : ''; 00577 $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false; 00578 $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false; 00579 $this->associated = isset( $options['associated'] ) ? $options['associated'] : false; 00580 00581 $this->deletedOnly = !empty( $options['deletedOnly'] ); 00582 $this->topOnly = !empty( $options['topOnly'] ); 00583 00584 $year = isset( $options['year'] ) ? $options['year'] : false; 00585 $month = isset( $options['month'] ) ? $options['month'] : false; 00586 $this->getDateCond( $year, $month ); 00587 00588 $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); 00589 } 00590 00591 function getDefaultQuery() { 00592 $query = parent::getDefaultQuery(); 00593 $query['target'] = $this->target; 00594 return $query; 00595 } 00596 00606 function reallyDoQuery( $offset, $limit, $descending ) { 00607 list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $descending ); 00608 $pager = $this; 00609 00610 /* 00611 * This hook will allow extensions to add in additional queries, so they can get their data 00612 * in My Contributions as well. Extensions should append their results to the $data array. 00613 * 00614 * Extension queries have to implement the navbar requirement as well. They should 00615 * - have a column aliased as $pager->getIndexField() 00616 * - have LIMIT set 00617 * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset 00618 * - have the ORDER BY specified based upon the details provided by the navbar 00619 * 00620 * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY 00621 * 00622 * &$data: an array of results of all contribs queries 00623 * $pager: the ContribsPager object hooked into 00624 * $offset: see phpdoc above 00625 * $limit: see phpdoc above 00626 * $descending: see phpdoc above 00627 */ 00628 $data = array( $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ) ); 00629 wfRunHooks( 'ContribsPager::reallyDoQuery', array( &$data, $pager, $offset, $limit, $descending ) ); 00630 00631 $result = array(); 00632 00633 // loop all results and collect them in an array 00634 foreach ( $data as $j => $query ) { 00635 foreach ( $query as $i => $row ) { 00636 // use index column as key, allowing us to easily sort in PHP 00637 $result[$row->{$this->getIndexField()} . "-$i"] = $row; 00638 } 00639 } 00640 00641 // sort results 00642 if ( $descending ) { 00643 ksort( $result ); 00644 } else { 00645 krsort( $result ); 00646 } 00647 00648 // enforce limit 00649 $result = array_slice( $result, 0, $limit ); 00650 00651 // get rid of array keys 00652 $result = array_values( $result ); 00653 00654 return new FakeResultWrapper( $result ); 00655 } 00656 00657 function getQueryInfo() { 00658 list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond(); 00659 00660 $user = $this->getUser(); 00661 $conds = array_merge( $userCond, $this->getNamespaceCond() ); 00662 00663 // Paranoia: avoid brute force searches (bug 17342) 00664 if ( !$user->isAllowed( 'deletedhistory' ) ) { 00665 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; 00666 } elseif ( !$user->isAllowed( 'suppressrevision' ) ) { 00667 $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . 00668 ' != ' . Revision::SUPPRESSED_USER; 00669 } 00670 00671 # Don't include orphaned revisions 00672 $join_cond['page'] = Revision::pageJoinCond(); 00673 # Get the current user name for accounts 00674 $join_cond['user'] = Revision::userJoinCond(); 00675 00676 $queryInfo = array( 00677 'tables' => $tables, 00678 'fields' => array_merge( 00679 Revision::selectFields(), 00680 Revision::selectUserFields(), 00681 array( 'page_namespace', 'page_title', 'page_is_new', 00682 'page_latest', 'page_is_redirect', 'page_len' ) 00683 ), 00684 'conds' => $conds, 00685 'options' => array( 'USE INDEX' => array( 'revision' => $index ) ), 00686 'join_conds' => $join_cond 00687 ); 00688 00689 ChangeTags::modifyDisplayQuery( 00690 $queryInfo['tables'], 00691 $queryInfo['fields'], 00692 $queryInfo['conds'], 00693 $queryInfo['join_conds'], 00694 $queryInfo['options'], 00695 $this->tagFilter 00696 ); 00697 00698 wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); 00699 return $queryInfo; 00700 } 00701 00702 function getUserCond() { 00703 $condition = array(); 00704 $join_conds = array(); 00705 $tables = array( 'revision', 'page', 'user' ); 00706 if ( $this->contribs == 'newbie' ) { 00707 $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); 00708 $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); 00709 $index = 'user_timestamp'; 00710 # ignore local groups with the bot right 00711 # @todo FIXME: Global groups may have 'bot' rights 00712 $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); 00713 if( count( $groupsWithBotPermission ) ) { 00714 $tables[] = 'user_groups'; 00715 $condition[] = 'ug_group IS NULL'; 00716 $join_conds['user_groups'] = array( 00717 'LEFT JOIN', array( 00718 'ug_user = rev_user', 00719 'ug_group' => $groupsWithBotPermission 00720 ) 00721 ); 00722 } 00723 } else { 00724 $uid = User::idFromName( $this->target ); 00725 if ( $uid ) { 00726 $condition['rev_user'] = $uid; 00727 $index = 'user_timestamp'; 00728 } else { 00729 $condition['rev_user_text'] = $this->target; 00730 $index = 'usertext_timestamp'; 00731 } 00732 } 00733 if ( $this->deletedOnly ) { 00734 $condition[] = "rev_deleted != '0'"; 00735 } 00736 if ( $this->topOnly ) { 00737 $condition[] = "rev_id = page_latest"; 00738 } 00739 return array( $tables, $index, $condition, $join_conds ); 00740 } 00741 00742 function getNamespaceCond() { 00743 if ( $this->namespace !== '' ) { 00744 $selectedNS = $this->mDb->addQuotes( $this->namespace ); 00745 $eq_op = $this->nsInvert ? '!=' : '='; 00746 $bool_op = $this->nsInvert ? 'AND' : 'OR'; 00747 00748 if ( !$this->associated ) { 00749 return array( "page_namespace $eq_op $selectedNS" ); 00750 } else { 00751 $associatedNS = $this->mDb->addQuotes ( 00752 MWNamespace::getAssociated( $this->namespace ) 00753 ); 00754 return array( 00755 "page_namespace $eq_op $selectedNS " . 00756 $bool_op . 00757 " page_namespace $eq_op $associatedNS" 00758 ); 00759 } 00760 00761 } else { 00762 return array(); 00763 } 00764 } 00765 00766 function getIndexField() { 00767 return 'rev_timestamp'; 00768 } 00769 00770 function doBatchLookups() { 00771 # Do a link batch query 00772 $this->mResult->seek( 0 ); 00773 $revIds = array(); 00774 $batch = new LinkBatch(); 00775 # Give some pointers to make (last) links 00776 foreach ( $this->mResult as $row ) { 00777 if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { 00778 $revIds[] = $row->rev_parent_id; 00779 } 00780 if ( isset( $row->rev_id ) ) { 00781 if ( $this->contribs === 'newbie' ) { // multiple users 00782 $batch->add( NS_USER, $row->user_name ); 00783 $batch->add( NS_USER_TALK, $row->user_name ); 00784 } 00785 $batch->add( $row->page_namespace, $row->page_title ); 00786 } 00787 } 00788 $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds ); 00789 $batch->execute(); 00790 $this->mResult->seek( 0 ); 00791 } 00792 00796 function getStartBody() { 00797 return "<ul>\n"; 00798 } 00799 00803 function getEndBody() { 00804 return "</ul>\n"; 00805 } 00806 00819 function formatRow( $row ) { 00820 wfProfileIn( __METHOD__ ); 00821 00822 $ret = ''; 00823 $classes = array(); 00824 00825 /* 00826 * There may be more than just revision rows. To make sure that we'll only be processing 00827 * revisions here, let's _try_ to build a revision out of our row (without displaying 00828 * notices though) and then trying to grab data from the built object. If we succeed, 00829 * we're definitely dealing with revision data and we may proceed, if not, we'll leave it 00830 * to extensions to subscribe to the hook to parse the row. 00831 */ 00832 wfSuppressWarnings(); 00833 $rev = new Revision( $row ); 00834 $validRevision = $rev->getParentId() !== null; 00835 wfRestoreWarnings(); 00836 00837 if ( $validRevision ) { 00838 $classes = array(); 00839 00840 $page = Title::newFromRow( $row ); 00841 $link = Linker::link( 00842 $page, 00843 htmlspecialchars( $page->getPrefixedText() ), 00844 array( 'class' => 'mw-contributions-title' ), 00845 $page->isRedirect() ? array( 'redirect' => 'no' ) : array() 00846 ); 00847 # Mark current revisions 00848 $topmarktext = ''; 00849 $user = $this->getUser(); 00850 if ( $row->rev_id == $row->page_latest ) { 00851 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; 00852 # Add rollback link 00853 if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user ) 00854 && $page->quickUserCan( 'edit', $user ) ) 00855 { 00856 $this->preventClickjacking(); 00857 $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() ); 00858 } 00859 } 00860 # Is there a visible previous revision? 00861 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) { 00862 $difftext = Linker::linkKnown( 00863 $page, 00864 $this->messages['diff'], 00865 array(), 00866 array( 00867 'diff' => 'prev', 00868 'oldid' => $row->rev_id 00869 ) 00870 ); 00871 } else { 00872 $difftext = $this->messages['diff']; 00873 } 00874 $histlink = Linker::linkKnown( 00875 $page, 00876 $this->messages['hist'], 00877 array(), 00878 array( 'action' => 'history' ) 00879 ); 00880 00881 if ( $row->rev_parent_id === null ) { 00882 // For some reason rev_parent_id isn't populated for this row. 00883 // Its rumoured this is true on wikipedia for some revisions (bug 34922). 00884 // Next best thing is to have the total number of bytes. 00885 $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . Linker::formatRevisionSize( $row->rev_len ) . ' <span class="mw-changeslist-separator">. .</span> '; 00886 } else { 00887 $parentLen = isset( $this->mParentLens[$row->rev_parent_id] ) ? $this->mParentLens[$row->rev_parent_id] : 0; 00888 $chardiff = ' <span class="mw-changeslist-separator">. .</span> ' . ChangesList::showCharacterDifference( 00889 $parentLen, $row->rev_len, $this->getContext() ) . ' <span class="mw-changeslist-separator">. .</span> '; 00890 } 00891 00892 $lang = $this->getLanguage(); 00893 $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true ); 00894 $date = $lang->userTimeAndDate( $row->rev_timestamp, $user ); 00895 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 00896 $d = Linker::linkKnown( 00897 $page, 00898 htmlspecialchars( $date ), 00899 array( 'class' => 'mw-changeslist-date' ), 00900 array( 'oldid' => intval( $row->rev_id ) ) 00901 ); 00902 } else { 00903 $d = htmlspecialchars( $date ); 00904 } 00905 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 00906 $d = '<span class="history-deleted">' . $d . '</span>'; 00907 } 00908 00909 # Show user names for /newbies as there may be different users. 00910 # Note that we already excluded rows with hidden user names. 00911 if ( $this->contribs == 'newbie' ) { 00912 $userlink = ' . . ' . Linker::userLink( $rev->getUser(), $rev->getUserText() ); 00913 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( 00914 Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; 00915 } else { 00916 $userlink = ''; 00917 } 00918 00919 if ( $rev->getParentId() === 0 ) { 00920 $nflag = ChangesList::flag( 'newpage' ); 00921 } else { 00922 $nflag = ''; 00923 } 00924 00925 if ( $rev->isMinor() ) { 00926 $mflag = ChangesList::flag( 'minor' ); 00927 } else { 00928 $mflag = ''; 00929 } 00930 00931 $del = Linker::getRevDeleteLink( $user, $rev, $page ); 00932 if ( $del !== '' ) { 00933 $del .= ' '; 00934 } 00935 00936 $diffHistLinks = $this->msg( 'parentheses' )->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )->escaped(); 00937 $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; 00938 00939 # Denote if username is redacted for this edit 00940 if ( $rev->isDeleted( Revision::DELETED_USER ) ) { 00941 $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>"; 00942 } 00943 00944 # Tags, if any. 00945 list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' ); 00946 $classes = array_merge( $classes, $newClasses ); 00947 $ret .= " $tagSummary"; 00948 } 00949 00950 // Let extensions add data 00951 wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); 00952 00953 $classes = implode( ' ', $classes ); 00954 $ret = "<li class=\"$classes\">$ret</li>\n"; 00955 00956 wfProfileOut( __METHOD__ ); 00957 return $ret; 00958 } 00959 00964 function getSqlComment() { 00965 if ( $this->namespace || $this->deletedOnly ) { 00966 return 'contributions page filtered for namespace or RevisionDeleted edits'; // potentially slow, see CR r58153 00967 } else { 00968 return 'contributions page unfiltered'; 00969 } 00970 } 00971 00972 protected function preventClickjacking() { 00973 $this->preventClickjacking = true; 00974 } 00975 00979 public function getPreventClickjacking() { 00980 return $this->preventClickjacking; 00981 } 00982 }