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