MediaWiki  REL1_24
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 compatibility
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                 $output = $pager->getBody();
00207                 if ( !$this->including() ) {
00208                     $output = '<p>' . $pager->getNavigationBar() . '</p>' .
00209                         $output .
00210                         '<p>' . $pager->getNavigationBar() . '</p>';
00211                 }
00212                 $out->addHTML( $output );
00213             }
00214             $out->preventClickjacking( $pager->getPreventClickjacking() );
00215 
00216             # Show the appropriate "footer" message - WHOIS tools, etc.
00217             if ( $this->opts['contribs'] == 'newbie' ) {
00218                 $message = 'sp-contributions-footer-newbies';
00219             } elseif ( IP::isIPAddress( $target ) ) {
00220                 $message = 'sp-contributions-footer-anon';
00221             } elseif ( $userObj->isAnon() ) {
00222                 // No message for non-existing users
00223                 $message = '';
00224             } else {
00225                 $message = 'sp-contributions-footer';
00226             }
00227 
00228             if ( $message ) {
00229                 if ( !$this->including() ) {
00230                     if ( !$this->msg( $message, $target )->isDisabled() ) {
00231                         $out->wrapWikiMsg(
00232                             "<div class='mw-contributions-footer'>\n$1\n</div>",
00233                             array( $message, $target ) );
00234                     }
00235                 }
00236             }
00237         }
00238     }
00239 
00247     protected function contributionsSub( $userObj ) {
00248         if ( $userObj->isAnon() ) {
00249             // Show a warning message that the user being searched for doesn't exists
00250             if ( !User::isIP( $userObj->getName() ) ) {
00251                 $this->getOutput()->wrapWikiMsg(
00252                     "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
00253                     array(
00254                         'contributions-userdoesnotexist',
00255                         wfEscapeWikiText( $userObj->getName() ),
00256                     )
00257                 );
00258                 if ( !$this->including() ) {
00259                     $this->getOutput()->setStatusCode( 404 );
00260                 }
00261             }
00262             $user = htmlspecialchars( $userObj->getName() );
00263         } else {
00264             $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) );
00265         }
00266         $nt = $userObj->getUserPage();
00267         $talk = $userObj->getTalkPage();
00268         $links = '';
00269         if ( $talk ) {
00270             $tools = $this->getUserLinks( $nt, $talk, $userObj );
00271             $links = $this->getLanguage()->pipeList( $tools );
00272 
00273             // Show a note if the user is blocked and display the last block log entry.
00274             // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
00275             // and also this will display a totally irrelevant log entry as a current block.
00276             if ( !$this->including() ) {
00277                 $block = Block::newFromTarget( $userObj, $userObj );
00278                 if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
00279                     if ( $block->getType() == Block::TYPE_RANGE ) {
00280                         $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
00281                     }
00282 
00283                     $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
00284                     LogEventsList::showLogExtract(
00285                         $out,
00286                         'block',
00287                         $nt,
00288                         '',
00289                         array(
00290                             'lim' => 1,
00291                             'showIfEmpty' => false,
00292                             'msgKey' => array(
00293                                 $userObj->isAnon() ?
00294                                     'sp-contributions-blocked-notice-anon' :
00295                                     'sp-contributions-blocked-notice',
00296                                 $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
00297                             ),
00298                             'offset' => '' # don't use WebRequest parameter offset
00299                         )
00300                     );
00301                 }
00302             }
00303         }
00304 
00305         return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
00306     }
00307 
00315     public function getUserLinks( Title $userpage, Title $talkpage, User $target ) {
00316 
00317         $id = $target->getId();
00318         $username = $target->getName();
00319 
00320         $tools[] = Linker::link( $talkpage, $this->msg( 'sp-contributions-talk' )->escaped() );
00321 
00322         if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
00323             if ( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
00324                 if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
00325                     $tools[] = Linker::linkKnown( # Change block link
00326                         SpecialPage::getTitleFor( 'Block', $username ),
00327                         $this->msg( 'change-blocklink' )->escaped()
00328                     );
00329                     $tools[] = Linker::linkKnown( # Unblock link
00330                         SpecialPage::getTitleFor( 'Unblock', $username ),
00331                         $this->msg( 'unblocklink' )->escaped()
00332                     );
00333                 } else { # User is not blocked
00334                     $tools[] = Linker::linkKnown( # Block link
00335                         SpecialPage::getTitleFor( 'Block', $username ),
00336                         $this->msg( 'blocklink' )->escaped()
00337                     );
00338                 }
00339             }
00340 
00341             # Block log link
00342             $tools[] = Linker::linkKnown(
00343                 SpecialPage::getTitleFor( 'Log', 'block' ),
00344                 $this->msg( 'sp-contributions-blocklog' )->escaped(),
00345                 array(),
00346                 array( 'page' => $userpage->getPrefixedText() )
00347             );
00348 
00349             # Suppression log link (bug 59120)
00350             if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
00351                 $tools[] = Linker::linkKnown(
00352                     SpecialPage::getTitleFor( 'Log', 'suppress' ),
00353                     $this->msg( 'sp-contributions-suppresslog' )->escaped(),
00354                     array(),
00355                     array( 'offender' => $username )
00356                 );
00357             }
00358         }
00359         # Uploads
00360         $tools[] = Linker::linkKnown(
00361             SpecialPage::getTitleFor( 'Listfiles', $username ),
00362             $this->msg( 'sp-contributions-uploads' )->escaped()
00363         );
00364 
00365         # Other logs link
00366         $tools[] = Linker::linkKnown(
00367             SpecialPage::getTitleFor( 'Log', $username ),
00368             $this->msg( 'sp-contributions-logs' )->escaped()
00369         );
00370 
00371         # Add link to deleted user contributions for priviledged users
00372         if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
00373             $tools[] = Linker::linkKnown(
00374                 SpecialPage::getTitleFor( 'DeletedContributions', $username ),
00375                 $this->msg( 'sp-contributions-deleted' )->escaped()
00376             );
00377         }
00378 
00379         # Add a link to change user rights for privileged users
00380         $userrightsPage = new UserrightsPage();
00381         $userrightsPage->setContext( $this->getContext() );
00382         if ( $userrightsPage->userCanChangeRights( $target ) ) {
00383             $tools[] = Linker::linkKnown(
00384                 SpecialPage::getTitleFor( 'Userrights', $username ),
00385                 $this->msg( 'sp-contributions-userrights' )->escaped()
00386             );
00387         }
00388 
00389         wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) );
00390 
00391         return $tools;
00392     }
00393 
00398     protected function getForm() {
00399         $this->opts['title'] = $this->getPageTitle()->getPrefixedText();
00400         if ( !isset( $this->opts['target'] ) ) {
00401             $this->opts['target'] = '';
00402         } else {
00403             $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
00404         }
00405 
00406         if ( !isset( $this->opts['namespace'] ) ) {
00407             $this->opts['namespace'] = '';
00408         }
00409 
00410         if ( !isset( $this->opts['nsInvert'] ) ) {
00411             $this->opts['nsInvert'] = '';
00412         }
00413 
00414         if ( !isset( $this->opts['associated'] ) ) {
00415             $this->opts['associated'] = false;
00416         }
00417 
00418         if ( !isset( $this->opts['contribs'] ) ) {
00419             $this->opts['contribs'] = 'user';
00420         }
00421 
00422         if ( !isset( $this->opts['year'] ) ) {
00423             $this->opts['year'] = '';
00424         }
00425 
00426         if ( !isset( $this->opts['month'] ) ) {
00427             $this->opts['month'] = '';
00428         }
00429 
00430         if ( $this->opts['contribs'] == 'newbie' ) {
00431             $this->opts['target'] = '';
00432         }
00433 
00434         if ( !isset( $this->opts['tagfilter'] ) ) {
00435             $this->opts['tagfilter'] = '';
00436         }
00437 
00438         if ( !isset( $this->opts['topOnly'] ) ) {
00439             $this->opts['topOnly'] = false;
00440         }
00441 
00442         if ( !isset( $this->opts['newOnly'] ) ) {
00443             $this->opts['newOnly'] = false;
00444         }
00445 
00446         $form = Html::openElement(
00447             'form',
00448             array(
00449                 'method' => 'get',
00450                 'action' => wfScript(),
00451                 'class' => 'mw-contributions-form'
00452             )
00453         );
00454 
00455         # Add hidden params for tracking except for parameters in $skipParameters
00456         $skipParameters = array(
00457             'namespace',
00458             'nsInvert',
00459             'deletedOnly',
00460             'target',
00461             'contribs',
00462             'year',
00463             'month',
00464             'topOnly',
00465             'newOnly',
00466             'associated'
00467         );
00468 
00469         foreach ( $this->opts as $name => $value ) {
00470             if ( in_array( $name, $skipParameters ) ) {
00471                 continue;
00472             }
00473             $form .= "\t" . Html::hidden( $name, $value ) . "\n";
00474         }
00475 
00476         $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
00477 
00478         if ( $tagFilter ) {
00479             $filterSelection = Html::rawElement(
00480                 'td',
00481                 array( 'class' => 'mw-label' ),
00482                 array_shift( $tagFilter )
00483             );
00484             $filterSelection .= Html::rawElement(
00485                 'td',
00486                 array( 'class' => 'mw-input' ),
00487                 implode( '&#160', $tagFilter )
00488             );
00489         } else {
00490             $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' );
00491         }
00492 
00493         $labelNewbies = Xml::radioLabel(
00494             $this->msg( 'sp-contributions-newbies' )->text(),
00495             'contribs',
00496             'newbie',
00497             'newbie',
00498             $this->opts['contribs'] == 'newbie',
00499             array( 'class' => 'mw-input' )
00500         );
00501         $labelUsername = Xml::radioLabel(
00502             $this->msg( 'sp-contributions-username' )->text(),
00503             'contribs',
00504             'user',
00505             'user',
00506             $this->opts['contribs'] == 'user',
00507             array( 'class' => 'mw-input' )
00508         );
00509         $input = Html::input(
00510             'target',
00511             $this->opts['target'],
00512             'text',
00513             array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) +
00514                 ( $this->opts['target'] ? array() : array( 'autofocus' )
00515                 )
00516         );
00517         $targetSelection = Html::rawElement(
00518             'td',
00519             array( 'colspan' => 2 ),
00520             $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' '
00521         );
00522 
00523         $namespaceSelection = Xml::tags(
00524             'td',
00525             array( 'class' => 'mw-label' ),
00526             Xml::label(
00527                 $this->msg( 'namespace' )->text(),
00528                 'namespace',
00529                 ''
00530             )
00531         );
00532         $namespaceSelection .= Html::rawElement(
00533             'td',
00534             null,
00535             Html::namespaceSelector(
00536                 array( 'selected' => $this->opts['namespace'], 'all' => '' ),
00537                 array(
00538                     'name' => 'namespace',
00539                     'id' => 'namespace',
00540                     'class' => 'namespaceselector',
00541                 )
00542             ) . '&#160;' .
00543                 Html::rawElement(
00544                     'span',
00545                     array( 'style' => 'white-space: nowrap' ),
00546                     Xml::checkLabel(
00547                         $this->msg( 'invert' )->text(),
00548                         'nsInvert',
00549                         'nsInvert',
00550                         $this->opts['nsInvert'],
00551                         array(
00552                             'title' => $this->msg( 'tooltip-invert' )->text(),
00553                             'class' => 'mw-input'
00554                         )
00555                     ) . '&#160;'
00556                 ) .
00557                 Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
00558                     Xml::checkLabel(
00559                         $this->msg( 'namespace_association' )->text(),
00560                         'associated',
00561                         'associated',
00562                         $this->opts['associated'],
00563                         array(
00564                             'title' => $this->msg( 'tooltip-namespace_association' )->text(),
00565                             'class' => 'mw-input'
00566                         )
00567                     ) . '&#160;'
00568                 )
00569         );
00570 
00571         if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
00572             $deletedOnlyCheck = Html::rawElement(
00573                 'span',
00574                 array( 'style' => 'white-space: nowrap' ),
00575                 Xml::checkLabel(
00576                     $this->msg( 'history-show-deleted' )->text(),
00577                     'deletedOnly',
00578                     'mw-show-deleted-only',
00579                     $this->opts['deletedOnly'],
00580                     array( 'class' => 'mw-input' )
00581                 )
00582             );
00583         } else {
00584             $deletedOnlyCheck = '';
00585         }
00586 
00587         $checkLabelTopOnly = Html::rawElement(
00588             'span',
00589             array( 'style' => 'white-space: nowrap' ),
00590             Xml::checkLabel(
00591                 $this->msg( 'sp-contributions-toponly' )->text(),
00592                 'topOnly',
00593                 'mw-show-top-only',
00594                 $this->opts['topOnly'],
00595                 array( 'class' => 'mw-input' )
00596             )
00597         );
00598         $checkLabelNewOnly = Html::rawElement(
00599             'span',
00600             array( 'style' => 'white-space: nowrap' ),
00601             Xml::checkLabel(
00602                 $this->msg( 'sp-contributions-newonly' )->text(),
00603                 'newOnly',
00604                 'mw-show-new-only',
00605                 $this->opts['newOnly'],
00606                 array( 'class' => 'mw-input' )
00607             )
00608         );
00609         $extraOptions = Html::rawElement(
00610             'td',
00611             array( 'colspan' => 2 ),
00612             $deletedOnlyCheck . $checkLabelTopOnly . $checkLabelNewOnly
00613         );
00614 
00615         $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ),
00616             Xml::dateMenu(
00617                 $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'],
00618                 $this->opts['month']
00619             ) . ' ' .
00620                 Xml::submitButton(
00621                     $this->msg( 'sp-contributions-submit' )->text(),
00622                     array( 'class' => 'mw-submit' )
00623                 )
00624         );
00625 
00626         $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() );
00627         $form .= Html::rawElement( 'table', array( 'class' => 'mw-contributions-table' ), "\n" .
00628             Html::rawElement( 'tr', array(), $targetSelection ) . "\n" .
00629             Html::rawElement( 'tr', array(), $namespaceSelection ) . "\n" .
00630             Html::rawElement( 'tr', array(), $filterSelection ) . "\n" .
00631             Html::rawElement( 'tr', array(), $extraOptions ) . "\n" .
00632             Html::rawElement( 'tr', array(), $dateSelectionAndSubmit ) . "\n"
00633         );
00634 
00635         $explain = $this->msg( 'sp-contributions-explain' );
00636         if ( !$explain->isBlank() ) {
00637             $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
00638         }
00639 
00640         $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' );
00641 
00642         return $form;
00643     }
00644 
00645     protected function getGroupName() {
00646         return 'users';
00647     }
00648 }
00649 
00654 class ContribsPager extends ReverseChronologicalPager {
00655     public $mDefaultDirection = IndexPager::DIR_DESCENDING;
00656     public $messages;
00657     public $target;
00658     public $namespace = '';
00659     public $mDb;
00660     public $preventClickjacking = false;
00661 
00663     public $mDbSecondary;
00664 
00668     protected $mParentLens;
00669 
00670     function __construct( IContextSource $context, array $options ) {
00671         parent::__construct( $context );
00672 
00673         $msgs = array(
00674             'diff',
00675             'hist',
00676             'newarticle',
00677             'pipe-separator',
00678             'rev-delundel',
00679             'rollbacklink',
00680             'uctop'
00681         );
00682 
00683         foreach ( $msgs as $msg ) {
00684             $this->messages[$msg] = $this->msg( $msg )->escaped();
00685         }
00686 
00687         $this->target = isset( $options['target'] ) ? $options['target'] : '';
00688         $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users';
00689         $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : '';
00690         $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false;
00691         $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false;
00692         $this->associated = isset( $options['associated'] ) ? $options['associated'] : false;
00693 
00694         $this->deletedOnly = !empty( $options['deletedOnly'] );
00695         $this->topOnly = !empty( $options['topOnly'] );
00696         $this->newOnly = !empty( $options['newOnly'] );
00697 
00698         $year = isset( $options['year'] ) ? $options['year'] : false;
00699         $month = isset( $options['month'] ) ? $options['month'] : false;
00700         $this->getDateCond( $year, $month );
00701 
00702         // Most of this code will use the 'contributions' group DB, which can map to slaves
00703         // with extra user based indexes or partioning by user. The additional metadata
00704         // queries should use a regular slave since the lookup pattern is not all by user.
00705         $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave
00706         $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
00707     }
00708 
00709     function getDefaultQuery() {
00710         $query = parent::getDefaultQuery();
00711         $query['target'] = $this->target;
00712 
00713         return $query;
00714     }
00715 
00725     function reallyDoQuery( $offset, $limit, $descending ) {
00726         list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
00727             $offset,
00728             $limit,
00729             $descending
00730         );
00731         $pager = $this;
00732 
00733         /*
00734          * This hook will allow extensions to add in additional queries, so they can get their data
00735          * in My Contributions as well. Extensions should append their results to the $data array.
00736          *
00737          * Extension queries have to implement the navbar requirement as well. They should
00738          * - have a column aliased as $pager->getIndexField()
00739          * - have LIMIT set
00740          * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
00741          * - have the ORDER BY specified based upon the details provided by the navbar
00742          *
00743          * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
00744          *
00745          * &$data: an array of results of all contribs queries
00746          * $pager: the ContribsPager object hooked into
00747          * $offset: see phpdoc above
00748          * $limit: see phpdoc above
00749          * $descending: see phpdoc above
00750          */
00751         $data = array( $this->mDb->select(
00752             $tables, $fields, $conds, $fname, $options, $join_conds
00753         ) );
00754         wfRunHooks(
00755             'ContribsPager::reallyDoQuery',
00756             array( &$data, $pager, $offset, $limit, $descending )
00757         );
00758 
00759         $result = array();
00760 
00761         // loop all results and collect them in an array
00762         foreach ( $data as $query ) {
00763             foreach ( $query as $i => $row ) {
00764                 // use index column as key, allowing us to easily sort in PHP
00765                 $result[$row->{$this->getIndexField()} . "-$i"] = $row;
00766             }
00767         }
00768 
00769         // sort results
00770         if ( $descending ) {
00771             ksort( $result );
00772         } else {
00773             krsort( $result );
00774         }
00775 
00776         // enforce limit
00777         $result = array_slice( $result, 0, $limit );
00778 
00779         // get rid of array keys
00780         $result = array_values( $result );
00781 
00782         return new FakeResultWrapper( $result );
00783     }
00784 
00785     function getQueryInfo() {
00786         list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
00787 
00788         $user = $this->getUser();
00789         $conds = array_merge( $userCond, $this->getNamespaceCond() );
00790 
00791         // Paranoia: avoid brute force searches (bug 17342)
00792         if ( !$user->isAllowed( 'deletedhistory' ) ) {
00793             $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0';
00794         } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
00795             $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) .
00796                 ' != ' . Revision::SUPPRESSED_USER;
00797         }
00798 
00799         # Don't include orphaned revisions
00800         $join_cond['page'] = Revision::pageJoinCond();
00801         # Get the current user name for accounts
00802         $join_cond['user'] = Revision::userJoinCond();
00803 
00804         $options = array();
00805         if ( $index ) {
00806             $options['USE INDEX'] = array( 'revision' => $index );
00807         }
00808 
00809         $queryInfo = array(
00810             'tables' => $tables,
00811             'fields' => array_merge(
00812                 Revision::selectFields(),
00813                 Revision::selectUserFields(),
00814                 array( 'page_namespace', 'page_title', 'page_is_new',
00815                     'page_latest', 'page_is_redirect', 'page_len' )
00816             ),
00817             'conds' => $conds,
00818             'options' => $options,
00819             'join_conds' => $join_cond
00820         );
00821 
00822         ChangeTags::modifyDisplayQuery(
00823             $queryInfo['tables'],
00824             $queryInfo['fields'],
00825             $queryInfo['conds'],
00826             $queryInfo['join_conds'],
00827             $queryInfo['options'],
00828             $this->tagFilter
00829         );
00830 
00831         wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
00832 
00833         return $queryInfo;
00834     }
00835 
00836     function getUserCond() {
00837         $condition = array();
00838         $join_conds = array();
00839         $tables = array( 'revision', 'page', 'user' );
00840         $index = false;
00841         if ( $this->contribs == 'newbie' ) {
00842             $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
00843             $condition[] = 'rev_user >' . (int)( $max - $max / 100 );
00844             # ignore local groups with the bot right
00845             # @todo FIXME: Global groups may have 'bot' rights
00846             $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
00847             if ( count( $groupsWithBotPermission ) ) {
00848                 $tables[] = 'user_groups';
00849                 $condition[] = 'ug_group IS NULL';
00850                 $join_conds['user_groups'] = array(
00851                     'LEFT JOIN', array(
00852                         'ug_user = rev_user',
00853                         'ug_group' => $groupsWithBotPermission
00854                     )
00855                 );
00856             }
00857         } else {
00858             $uid = User::idFromName( $this->target );
00859             if ( $uid ) {
00860                 $condition['rev_user'] = $uid;
00861                 $index = 'user_timestamp';
00862             } else {
00863                 $condition['rev_user_text'] = $this->target;
00864                 $index = 'usertext_timestamp';
00865             }
00866         }
00867 
00868         if ( $this->deletedOnly ) {
00869             $condition[] = 'rev_deleted != 0';
00870         }
00871 
00872         if ( $this->topOnly ) {
00873             $condition[] = 'rev_id = page_latest';
00874         }
00875 
00876         if ( $this->newOnly ) {
00877             $condition[] = 'rev_parent_id = 0';
00878         }
00879 
00880         return array( $tables, $index, $condition, $join_conds );
00881     }
00882 
00883     function getNamespaceCond() {
00884         if ( $this->namespace !== '' ) {
00885             $selectedNS = $this->mDb->addQuotes( $this->namespace );
00886             $eq_op = $this->nsInvert ? '!=' : '=';
00887             $bool_op = $this->nsInvert ? 'AND' : 'OR';
00888 
00889             if ( !$this->associated ) {
00890                 return array( "page_namespace $eq_op $selectedNS" );
00891             }
00892 
00893             $associatedNS = $this->mDb->addQuotes(
00894                 MWNamespace::getAssociated( $this->namespace )
00895             );
00896 
00897             return array(
00898                 "page_namespace $eq_op $selectedNS " .
00899                     $bool_op .
00900                     " page_namespace $eq_op $associatedNS"
00901             );
00902         }
00903 
00904         return array();
00905     }
00906 
00907     function getIndexField() {
00908         return 'rev_timestamp';
00909     }
00910 
00911     function doBatchLookups() {
00912         # Do a link batch query
00913         $this->mResult->seek( 0 );
00914         $revIds = array();
00915         $batch = new LinkBatch();
00916         # Give some pointers to make (last) links
00917         foreach ( $this->mResult as $row ) {
00918             if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
00919                 $revIds[] = $row->rev_parent_id;
00920             }
00921             if ( isset( $row->rev_id ) ) {
00922                 if ( $this->contribs === 'newbie' ) { // multiple users
00923                     $batch->add( NS_USER, $row->user_name );
00924                     $batch->add( NS_USER_TALK, $row->user_name );
00925                 }
00926                 $batch->add( $row->page_namespace, $row->page_title );
00927             }
00928         }
00929         $this->mParentLens = Revision::getParentLengths( $this->mDbSecondary, $revIds );
00930         $batch->execute();
00931         $this->mResult->seek( 0 );
00932     }
00933 
00937     function getStartBody() {
00938         return "<ul>\n";
00939     }
00940 
00944     function getEndBody() {
00945         return "</ul>\n";
00946     }
00947 
00960     function formatRow( $row ) {
00961         wfProfileIn( __METHOD__ );
00962 
00963         $ret = '';
00964         $classes = array();
00965 
00966         /*
00967          * There may be more than just revision rows. To make sure that we'll only be processing
00968          * revisions here, let's _try_ to build a revision out of our row (without displaying
00969          * notices though) and then trying to grab data from the built object. If we succeed,
00970          * we're definitely dealing with revision data and we may proceed, if not, we'll leave it
00971          * to extensions to subscribe to the hook to parse the row.
00972          */
00973         wfSuppressWarnings();
00974         try {
00975             $rev = new Revision( $row );
00976             $validRevision = (bool)$rev->getId();
00977         } catch ( MWException $e ) {
00978             $validRevision = false;
00979         }
00980         wfRestoreWarnings();
00981 
00982         if ( $validRevision ) {
00983             $classes = array();
00984 
00985             $page = Title::newFromRow( $row );
00986             $link = Linker::link(
00987                 $page,
00988                 htmlspecialchars( $page->getPrefixedText() ),
00989                 array( 'class' => 'mw-contributions-title' ),
00990                 $page->isRedirect() ? array( 'redirect' => 'no' ) : array()
00991             );
00992             # Mark current revisions
00993             $topmarktext = '';
00994             $user = $this->getUser();
00995             if ( $row->rev_id == $row->page_latest ) {
00996                 $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
00997                 # Add rollback link
00998                 if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user )
00999                     && $page->quickUserCan( 'edit', $user )
01000                 ) {
01001                     $this->preventClickjacking();
01002                     $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
01003                 }
01004             }
01005             # Is there a visible previous revision?
01006             if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) {
01007                 $difftext = Linker::linkKnown(
01008                     $page,
01009                     $this->messages['diff'],
01010                     array(),
01011                     array(
01012                         'diff' => 'prev',
01013                         'oldid' => $row->rev_id
01014                     )
01015                 );
01016             } else {
01017                 $difftext = $this->messages['diff'];
01018             }
01019             $histlink = Linker::linkKnown(
01020                 $page,
01021                 $this->messages['hist'],
01022                 array(),
01023                 array( 'action' => 'history' )
01024             );
01025 
01026             if ( $row->rev_parent_id === null ) {
01027                 // For some reason rev_parent_id isn't populated for this row.
01028                 // Its rumoured this is true on wikipedia for some revisions (bug 34922).
01029                 // Next best thing is to have the total number of bytes.
01030                 $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
01031                 $chardiff .= Linker::formatRevisionSize( $row->rev_len );
01032                 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
01033             } else {
01034                 $parentLen = 0;
01035                 if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
01036                     $parentLen = $this->mParentLens[$row->rev_parent_id];
01037                 }
01038 
01039                 $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
01040                 $chardiff .= ChangesList::showCharacterDifference(
01041                     $parentLen,
01042                     $row->rev_len,
01043                     $this->getContext()
01044                 );
01045                 $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
01046             }
01047 
01048             $lang = $this->getLanguage();
01049             $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true );
01050             $date = $lang->userTimeAndDate( $row->rev_timestamp, $user );
01051             if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
01052                 $d = Linker::linkKnown(
01053                     $page,
01054                     htmlspecialchars( $date ),
01055                     array( 'class' => 'mw-changeslist-date' ),
01056                     array( 'oldid' => intval( $row->rev_id ) )
01057                 );
01058             } else {
01059                 $d = htmlspecialchars( $date );
01060             }
01061             if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
01062                 $d = '<span class="history-deleted">' . $d . '</span>';
01063             }
01064 
01065             # Show user names for /newbies as there may be different users.
01066             # Note that we already excluded rows with hidden user names.
01067             if ( $this->contribs == 'newbie' ) {
01068                 $userlink = ' . . ' . $lang->getDirMark()
01069                     . Linker::userLink( $rev->getUser(), $rev->getUserText() );
01070                 $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
01071                     Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' ';
01072             } else {
01073                 $userlink = '';
01074             }
01075 
01076             if ( $rev->getParentId() === 0 ) {
01077                 $nflag = ChangesList::flag( 'newpage' );
01078             } else {
01079                 $nflag = '';
01080             }
01081 
01082             if ( $rev->isMinor() ) {
01083                 $mflag = ChangesList::flag( 'minor' );
01084             } else {
01085                 $mflag = '';
01086             }
01087 
01088             $del = Linker::getRevDeleteLink( $user, $rev, $page );
01089             if ( $del !== '' ) {
01090                 $del .= ' ';
01091             }
01092 
01093             $diffHistLinks = $this->msg( 'parentheses' )
01094                 ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )
01095                 ->escaped();
01096             $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} ";
01097             $ret .= "{$link}{$userlink} {$comment} {$topmarktext}";
01098 
01099             # Denote if username is redacted for this edit
01100             if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
01101                 $ret .= " <strong>" .
01102                     $this->msg( 'rev-deleted-user-contribs' )->escaped() .
01103                     "</strong>";
01104             }
01105 
01106             # Tags, if any.
01107             list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
01108                 $row->ts_tags,
01109                 'contributions'
01110             );
01111             $classes = array_merge( $classes, $newClasses );
01112             $ret .= " $tagSummary";
01113         }
01114 
01115         // Let extensions add data
01116         wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) );
01117 
01118         if ( $classes === array() && $ret === '' ) {
01119             wfDebug( "Dropping Special:Contribution row that could not be formatted\n" );
01120             $ret = "<!-- Could not format Special:Contribution row. -->\n";
01121         } else {
01122             $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n";
01123         }
01124 
01125         wfProfileOut( __METHOD__ );
01126 
01127         return $ret;
01128     }
01129 
01134     function getSqlComment() {
01135         if ( $this->namespace || $this->deletedOnly ) {
01136             // potentially slow, see CR r58153
01137             return 'contributions page filtered for namespace or RevisionDeleted edits';
01138         } else {
01139             return 'contributions page unfiltered';
01140         }
01141     }
01142 
01143     protected function preventClickjacking() {
01144         $this->preventClickjacking = true;
01145     }
01146 
01150     public function getPreventClickjacking() {
01151         return $this->preventClickjacking;
01152     }
01153 }