[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/specials/ -> SpecialContributions.php (source)

   1  <?php
   2  /**
   3   * Implements Special:Contributions
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup SpecialPage
  22   */
  23  
  24  /**
  25   * Special:Contributions, show user contributions in a paged list
  26   *
  27   * @ingroup SpecialPage
  28   */
  29  class SpecialContributions extends IncludableSpecialPage {
  30      protected $opts;
  31  
  32  	public function __construct() {
  33          parent::__construct( 'Contributions' );
  34      }
  35  
  36  	public function execute( $par ) {
  37          $this->setHeaders();
  38          $this->outputHeader();
  39          $out = $this->getOutput();
  40          $out->addModuleStyles( 'mediawiki.special' );
  41  
  42          $this->opts = array();
  43          $request = $this->getRequest();
  44  
  45          if ( $par !== null ) {
  46              $target = $par;
  47          } else {
  48              $target = $request->getVal( 'target' );
  49          }
  50  
  51          // check for radiobox
  52          if ( $request->getVal( 'contribs' ) == 'newbie' ) {
  53              $target = 'newbies';
  54              $this->opts['contribs'] = 'newbie';
  55          } elseif ( $par === 'newbies' ) { // b/c for WMF
  56              $target = 'newbies';
  57              $this->opts['contribs'] = 'newbie';
  58          } else {
  59              $this->opts['contribs'] = 'user';
  60          }
  61  
  62          $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
  63  
  64          if ( !strlen( $target ) ) {
  65              if ( !$this->including() ) {
  66                  $out->addHTML( $this->getForm() );
  67              }
  68  
  69              return;
  70          }
  71  
  72          $user = $this->getUser();
  73  
  74          $this->opts['limit'] = $request->getInt( 'limit', $user->getOption( 'rclimit' ) );
  75          $this->opts['target'] = $target;
  76          $this->opts['topOnly'] = $request->getBool( 'topOnly' );
  77          $this->opts['newOnly'] = $request->getBool( 'newOnly' );
  78  
  79          $nt = Title::makeTitleSafe( NS_USER, $target );
  80          if ( !$nt ) {
  81              $out->addHTML( $this->getForm() );
  82  
  83              return;
  84          }
  85          $userObj = User::newFromName( $nt->getText(), false );
  86          if ( !$userObj ) {
  87              $out->addHTML( $this->getForm() );
  88  
  89              return;
  90          }
  91          $id = $userObj->getID();
  92  
  93          if ( $this->opts['contribs'] != 'newbie' ) {
  94              $target = $nt->getText();
  95              $out->addSubtitle( $this->contributionsSub( $userObj ) );
  96              $out->setHTMLTitle( $this->msg(
  97                  'pagetitle',
  98                  $this->msg( 'contributions-title', $target )->plain()
  99              )->inContentLanguage() );
 100              $this->getSkin()->setRelevantUser( $userObj );
 101          } else {
 102              $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
 103              $out->setHTMLTitle( $this->msg(
 104                  'pagetitle',
 105                  $this->msg( 'sp-contributions-newbies-title' )->plain()
 106              )->inContentLanguage() );
 107          }
 108  
 109          if ( ( $ns = $request->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
 110              $this->opts['namespace'] = intval( $ns );
 111          } else {
 112              $this->opts['namespace'] = '';
 113          }
 114  
 115          $this->opts['associated'] = $request->getBool( 'associated' );
 116          $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
 117          $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
 118  
 119          // Allows reverts to have the bot flag in recent changes. It is just here to
 120          // be passed in the form at the top of the page
 121          if ( $user->isAllowed( 'markbotedits' ) && $request->getBool( 'bot' ) ) {
 122              $this->opts['bot'] = '1';
 123          }
 124  
 125          $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
 126          # Offset overrides year/month selection
 127          if ( $skip ) {
 128              $this->opts['year'] = '';
 129              $this->opts['month'] = '';
 130          } else {
 131              $this->opts['year'] = $request->getIntOrNull( 'year' );
 132              $this->opts['month'] = $request->getIntOrNull( 'month' );
 133          }
 134  
 135          $feedType = $request->getVal( 'feed' );
 136  
 137          $feedParams = array(
 138              'action' => 'feedcontributions',
 139              'user' => $target,
 140          );
 141          if ( $this->opts['topOnly'] ) {
 142              $feedParams['toponly'] = true;
 143          }
 144          if ( $this->opts['newOnly'] ) {
 145              $feedParams['newonly'] = true;
 146          }
 147          if ( $this->opts['deletedOnly'] ) {
 148              $feedParams['deletedonly'] = true;
 149          }
 150          if ( $this->opts['tagfilter'] !== '' ) {
 151              $feedParams['tagfilter'] = $this->opts['tagfilter'];
 152          }
 153          if ( $this->opts['namespace'] !== '' ) {
 154              $feedParams['namespace'] = $this->opts['namespace'];
 155          }
 156          // Don't use year and month for the feed URL, but pass them on if
 157          // we redirect to API (if $feedType is specified)
 158          if ( $feedType && $this->opts['year'] !== null ) {
 159              $feedParams['year'] = $this->opts['year'];
 160          }
 161          if ( $feedType && $this->opts['month'] !== null ) {
 162              $feedParams['month'] = $this->opts['month'];
 163          }
 164  
 165          if ( $feedType ) {
 166              // Maintain some level of backwards compatibility
 167              // If people request feeds using the old parameters, redirect to API
 168              $feedParams['feedformat'] = $feedType;
 169              $url = wfAppendQuery( wfScript( 'api' ), $feedParams );
 170  
 171              $out->redirect( $url, '301' );
 172  
 173              return;
 174          }
 175  
 176          // Add RSS/atom links
 177          $this->addFeedLinks( $feedParams );
 178  
 179          if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id, $userObj, $this ) ) ) {
 180              if ( !$this->including() ) {
 181                  $out->addHTML( $this->getForm() );
 182              }
 183              $pager = new ContribsPager( $this->getContext(), array(
 184                  'target' => $target,
 185                  'contribs' => $this->opts['contribs'],
 186                  'namespace' => $this->opts['namespace'],
 187                  'tagfilter' => $this->opts['tagfilter'],
 188                  'year' => $this->opts['year'],
 189                  'month' => $this->opts['month'],
 190                  'deletedOnly' => $this->opts['deletedOnly'],
 191                  'topOnly' => $this->opts['topOnly'],
 192                  'newOnly' => $this->opts['newOnly'],
 193                  'nsInvert' => $this->opts['nsInvert'],
 194                  'associated' => $this->opts['associated'],
 195              ) );
 196  
 197              if ( !$pager->getNumRows() ) {
 198                  $out->addWikiMsg( 'nocontribs', $target );
 199              } else {
 200                  # Show a message about slave lag, if applicable
 201                  $lag = wfGetLB()->safeGetLag( $pager->getDatabase() );
 202                  if ( $lag > 0 ) {
 203                      $out->showLagWarning( $lag );
 204                  }
 205  
 206                  $output = $pager->getBody();
 207                  if ( !$this->including() ) {
 208                      $output = '<p>' . $pager->getNavigationBar() . '</p>' .
 209                          $output .
 210                          '<p>' . $pager->getNavigationBar() . '</p>';
 211                  }
 212                  $out->addHTML( $output );
 213              }
 214              $out->preventClickjacking( $pager->getPreventClickjacking() );
 215  
 216              # Show the appropriate "footer" message - WHOIS tools, etc.
 217              if ( $this->opts['contribs'] == 'newbie' ) {
 218                  $message = 'sp-contributions-footer-newbies';
 219              } elseif ( IP::isIPAddress( $target ) ) {
 220                  $message = 'sp-contributions-footer-anon';
 221              } elseif ( $userObj->isAnon() ) {
 222                  // No message for non-existing users
 223                  $message = '';
 224              } else {
 225                  $message = 'sp-contributions-footer';
 226              }
 227  
 228              if ( $message ) {
 229                  if ( !$this->including() ) {
 230                      if ( !$this->msg( $message, $target )->isDisabled() ) {
 231                          $out->wrapWikiMsg(
 232                              "<div class='mw-contributions-footer'>\n$1\n</div>",
 233                              array( $message, $target ) );
 234                      }
 235                  }
 236              }
 237          }
 238      }
 239  
 240      /**
 241       * Generates the subheading with links
 242       * @param User $userObj User object for the target
 243       * @return string Appropriately-escaped HTML to be output literally
 244       * @todo FIXME: Almost the same as getSubTitle in SpecialDeletedContributions.php.
 245       * Could be combined.
 246       */
 247  	protected function contributionsSub( $userObj ) {
 248          if ( $userObj->isAnon() ) {
 249              // Show a warning message that the user being searched for doesn't exists
 250              if ( !User::isIP( $userObj->getName() ) ) {
 251                  $this->getOutput()->wrapWikiMsg(
 252                      "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
 253                      array(
 254                          'contributions-userdoesnotexist',
 255                          wfEscapeWikiText( $userObj->getName() ),
 256                      )
 257                  );
 258                  if ( !$this->including() ) {
 259                      $this->getOutput()->setStatusCode( 404 );
 260                  }
 261              }
 262              $user = htmlspecialchars( $userObj->getName() );
 263          } else {
 264              $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) );
 265          }
 266          $nt = $userObj->getUserPage();
 267          $talk = $userObj->getTalkPage();
 268          $links = '';
 269          if ( $talk ) {
 270              $tools = $this->getUserLinks( $nt, $talk, $userObj );
 271              $links = $this->getLanguage()->pipeList( $tools );
 272  
 273              // Show a note if the user is blocked and display the last block log entry.
 274              // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
 275              // and also this will display a totally irrelevant log entry as a current block.
 276              if ( !$this->including() ) {
 277                  $block = Block::newFromTarget( $userObj, $userObj );
 278                  if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
 279                      if ( $block->getType() == Block::TYPE_RANGE ) {
 280                          $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
 281                      }
 282  
 283                      $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
 284                      LogEventsList::showLogExtract(
 285                          $out,
 286                          'block',
 287                          $nt,
 288                          '',
 289                          array(
 290                              'lim' => 1,
 291                              'showIfEmpty' => false,
 292                              'msgKey' => array(
 293                                  $userObj->isAnon() ?
 294                                      'sp-contributions-blocked-notice-anon' :
 295                                      'sp-contributions-blocked-notice',
 296                                  $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
 297                              ),
 298                              'offset' => '' # don't use WebRequest parameter offset
 299                          )
 300                      );
 301                  }
 302              }
 303          }
 304  
 305          return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
 306      }
 307  
 308      /**
 309       * Links to different places.
 310       * @param Title $userpage Target user page
 311       * @param Title $talkpage Talk page
 312       * @param User $target Target user object
 313       * @return array
 314       */
 315  	public function getUserLinks( Title $userpage, Title $talkpage, User $target ) {
 316  
 317          $id = $target->getId();
 318          $username = $target->getName();
 319  
 320          $tools[] = Linker::link( $talkpage, $this->msg( 'sp-contributions-talk' )->escaped() );
 321  
 322          if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
 323              if ( $this->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
 324                  if ( $target->isBlocked() && $target->getBlock()->getType() != Block::TYPE_AUTO ) {
 325                      $tools[] = Linker::linkKnown( # Change block link
 326                          SpecialPage::getTitleFor( 'Block', $username ),
 327                          $this->msg( 'change-blocklink' )->escaped()
 328                      );
 329                      $tools[] = Linker::linkKnown( # Unblock link
 330                          SpecialPage::getTitleFor( 'Unblock', $username ),
 331                          $this->msg( 'unblocklink' )->escaped()
 332                      );
 333                  } else { # User is not blocked
 334                      $tools[] = Linker::linkKnown( # Block link
 335                          SpecialPage::getTitleFor( 'Block', $username ),
 336                          $this->msg( 'blocklink' )->escaped()
 337                      );
 338                  }
 339              }
 340  
 341              # Block log link
 342              $tools[] = Linker::linkKnown(
 343                  SpecialPage::getTitleFor( 'Log', 'block' ),
 344                  $this->msg( 'sp-contributions-blocklog' )->escaped(),
 345                  array(),
 346                  array( 'page' => $userpage->getPrefixedText() )
 347              );
 348  
 349              # Suppression log link (bug 59120)
 350              if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) {
 351                  $tools[] = Linker::linkKnown(
 352                      SpecialPage::getTitleFor( 'Log', 'suppress' ),
 353                      $this->msg( 'sp-contributions-suppresslog' )->escaped(),
 354                      array(),
 355                      array( 'offender' => $username )
 356                  );
 357              }
 358          }
 359          # Uploads
 360          $tools[] = Linker::linkKnown(
 361              SpecialPage::getTitleFor( 'Listfiles', $username ),
 362              $this->msg( 'sp-contributions-uploads' )->escaped()
 363          );
 364  
 365          # Other logs link
 366          $tools[] = Linker::linkKnown(
 367              SpecialPage::getTitleFor( 'Log', $username ),
 368              $this->msg( 'sp-contributions-logs' )->escaped()
 369          );
 370  
 371          # Add link to deleted user contributions for priviledged users
 372          if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
 373              $tools[] = Linker::linkKnown(
 374                  SpecialPage::getTitleFor( 'DeletedContributions', $username ),
 375                  $this->msg( 'sp-contributions-deleted' )->escaped()
 376              );
 377          }
 378  
 379          # Add a link to change user rights for privileged users
 380          $userrightsPage = new UserrightsPage();
 381          $userrightsPage->setContext( $this->getContext() );
 382          if ( $userrightsPage->userCanChangeRights( $target ) ) {
 383              $tools[] = Linker::linkKnown(
 384                  SpecialPage::getTitleFor( 'Userrights', $username ),
 385                  $this->msg( 'sp-contributions-userrights' )->escaped()
 386              );
 387          }
 388  
 389          wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) );
 390  
 391          return $tools;
 392      }
 393  
 394      /**
 395       * Generates the namespace selector form with hidden attributes.
 396       * @return string HTML fragment
 397       */
 398  	protected function getForm() {
 399          $this->opts['title'] = $this->getPageTitle()->getPrefixedText();
 400          if ( !isset( $this->opts['target'] ) ) {
 401              $this->opts['target'] = '';
 402          } else {
 403              $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
 404          }
 405  
 406          if ( !isset( $this->opts['namespace'] ) ) {
 407              $this->opts['namespace'] = '';
 408          }
 409  
 410          if ( !isset( $this->opts['nsInvert'] ) ) {
 411              $this->opts['nsInvert'] = '';
 412          }
 413  
 414          if ( !isset( $this->opts['associated'] ) ) {
 415              $this->opts['associated'] = false;
 416          }
 417  
 418          if ( !isset( $this->opts['contribs'] ) ) {
 419              $this->opts['contribs'] = 'user';
 420          }
 421  
 422          if ( !isset( $this->opts['year'] ) ) {
 423              $this->opts['year'] = '';
 424          }
 425  
 426          if ( !isset( $this->opts['month'] ) ) {
 427              $this->opts['month'] = '';
 428          }
 429  
 430          if ( $this->opts['contribs'] == 'newbie' ) {
 431              $this->opts['target'] = '';
 432          }
 433  
 434          if ( !isset( $this->opts['tagfilter'] ) ) {
 435              $this->opts['tagfilter'] = '';
 436          }
 437  
 438          if ( !isset( $this->opts['topOnly'] ) ) {
 439              $this->opts['topOnly'] = false;
 440          }
 441  
 442          if ( !isset( $this->opts['newOnly'] ) ) {
 443              $this->opts['newOnly'] = false;
 444          }
 445  
 446          $form = Html::openElement(
 447              'form',
 448              array(
 449                  'method' => 'get',
 450                  'action' => wfScript(),
 451                  'class' => 'mw-contributions-form'
 452              )
 453          );
 454  
 455          # Add hidden params for tracking except for parameters in $skipParameters
 456          $skipParameters = array(
 457              'namespace',
 458              'nsInvert',
 459              'deletedOnly',
 460              'target',
 461              'contribs',
 462              'year',
 463              'month',
 464              'topOnly',
 465              'newOnly',
 466              'associated'
 467          );
 468  
 469          foreach ( $this->opts as $name => $value ) {
 470              if ( in_array( $name, $skipParameters ) ) {
 471                  continue;
 472              }
 473              $form .= "\t" . Html::hidden( $name, $value ) . "\n";
 474          }
 475  
 476          $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
 477  
 478          if ( $tagFilter ) {
 479              $filterSelection = Html::rawElement(
 480                  'td',
 481                  array( 'class' => 'mw-label' ),
 482                  array_shift( $tagFilter )
 483              );
 484              $filterSelection .= Html::rawElement(
 485                  'td',
 486                  array( 'class' => 'mw-input' ),
 487                  implode( '&#160', $tagFilter )
 488              );
 489          } else {
 490              $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' );
 491          }
 492  
 493          $labelNewbies = Xml::radioLabel(
 494              $this->msg( 'sp-contributions-newbies' )->text(),
 495              'contribs',
 496              'newbie',
 497              'newbie',
 498              $this->opts['contribs'] == 'newbie',
 499              array( 'class' => 'mw-input' )
 500          );
 501          $labelUsername = Xml::radioLabel(
 502              $this->msg( 'sp-contributions-username' )->text(),
 503              'contribs',
 504              'user',
 505              'user',
 506              $this->opts['contribs'] == 'user',
 507              array( 'class' => 'mw-input' )
 508          );
 509          $input = Html::input(
 510              'target',
 511              $this->opts['target'],
 512              'text',
 513              array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) +
 514                  ( $this->opts['target'] ? array() : array( 'autofocus' )
 515                  )
 516          );
 517          $targetSelection = Html::rawElement(
 518              'td',
 519              array( 'colspan' => 2 ),
 520              $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' '
 521          );
 522  
 523          $namespaceSelection = Xml::tags(
 524              'td',
 525              array( 'class' => 'mw-label' ),
 526              Xml::label(
 527                  $this->msg( 'namespace' )->text(),
 528                  'namespace',
 529                  ''
 530              )
 531          );
 532          $namespaceSelection .= Html::rawElement(
 533              'td',
 534              null,
 535              Html::namespaceSelector(
 536                  array( 'selected' => $this->opts['namespace'], 'all' => '' ),
 537                  array(
 538                      'name' => 'namespace',
 539                      'id' => 'namespace',
 540                      'class' => 'namespaceselector',
 541                  )
 542              ) . '&#160;' .
 543                  Html::rawElement(
 544                      'span',
 545                      array( 'style' => 'white-space: nowrap' ),
 546                      Xml::checkLabel(
 547                          $this->msg( 'invert' )->text(),
 548                          'nsInvert',
 549                          'nsInvert',
 550                          $this->opts['nsInvert'],
 551                          array(
 552                              'title' => $this->msg( 'tooltip-invert' )->text(),
 553                              'class' => 'mw-input'
 554                          )
 555                      ) . '&#160;'
 556                  ) .
 557                  Html::rawElement( 'span', array( 'style' => 'white-space: nowrap' ),
 558                      Xml::checkLabel(
 559                          $this->msg( 'namespace_association' )->text(),
 560                          'associated',
 561                          'associated',
 562                          $this->opts['associated'],
 563                          array(
 564                              'title' => $this->msg( 'tooltip-namespace_association' )->text(),
 565                              'class' => 'mw-input'
 566                          )
 567                      ) . '&#160;'
 568                  )
 569          );
 570  
 571          if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
 572              $deletedOnlyCheck = Html::rawElement(
 573                  'span',
 574                  array( 'style' => 'white-space: nowrap' ),
 575                  Xml::checkLabel(
 576                      $this->msg( 'history-show-deleted' )->text(),
 577                      'deletedOnly',
 578                      'mw-show-deleted-only',
 579                      $this->opts['deletedOnly'],
 580                      array( 'class' => 'mw-input' )
 581                  )
 582              );
 583          } else {
 584              $deletedOnlyCheck = '';
 585          }
 586  
 587          $checkLabelTopOnly = Html::rawElement(
 588              'span',
 589              array( 'style' => 'white-space: nowrap' ),
 590              Xml::checkLabel(
 591                  $this->msg( 'sp-contributions-toponly' )->text(),
 592                  'topOnly',
 593                  'mw-show-top-only',
 594                  $this->opts['topOnly'],
 595                  array( 'class' => 'mw-input' )
 596              )
 597          );
 598          $checkLabelNewOnly = Html::rawElement(
 599              'span',
 600              array( 'style' => 'white-space: nowrap' ),
 601              Xml::checkLabel(
 602                  $this->msg( 'sp-contributions-newonly' )->text(),
 603                  'newOnly',
 604                  'mw-show-new-only',
 605                  $this->opts['newOnly'],
 606                  array( 'class' => 'mw-input' )
 607              )
 608          );
 609          $extraOptions = Html::rawElement(
 610              'td',
 611              array( 'colspan' => 2 ),
 612              $deletedOnlyCheck . $checkLabelTopOnly . $checkLabelNewOnly
 613          );
 614  
 615          $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ),
 616              Xml::dateMenu(
 617                  $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'],
 618                  $this->opts['month']
 619              ) . ' ' .
 620                  Xml::submitButton(
 621                      $this->msg( 'sp-contributions-submit' )->text(),
 622                      array( 'class' => 'mw-submit' )
 623                  )
 624          );
 625  
 626          $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() );
 627          $form .= Html::rawElement( 'table', array( 'class' => 'mw-contributions-table' ), "\n" .
 628              Html::rawElement( 'tr', array(), $targetSelection ) . "\n" .
 629              Html::rawElement( 'tr', array(), $namespaceSelection ) . "\n" .
 630              Html::rawElement( 'tr', array(), $filterSelection ) . "\n" .
 631              Html::rawElement( 'tr', array(), $extraOptions ) . "\n" .
 632              Html::rawElement( 'tr', array(), $dateSelectionAndSubmit ) . "\n"
 633          );
 634  
 635          $explain = $this->msg( 'sp-contributions-explain' );
 636          if ( !$explain->isBlank() ) {
 637              $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
 638          }
 639  
 640          $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' );
 641  
 642          return $form;
 643      }
 644  
 645  	protected function getGroupName() {
 646          return 'users';
 647      }
 648  }
 649  
 650  /**
 651   * Pager for Special:Contributions
 652   * @ingroup SpecialPage Pager
 653   */
 654  class ContribsPager extends ReverseChronologicalPager {
 655      public $mDefaultDirection = IndexPager::DIR_DESCENDING;
 656      public $messages;
 657      public $target;
 658      public $namespace = '';
 659      public $mDb;
 660      public $preventClickjacking = false;
 661  
 662      /** @var DatabaseBase */
 663      public $mDbSecondary;
 664  
 665      /**
 666       * @var array
 667       */
 668      protected $mParentLens;
 669  
 670  	function __construct( IContextSource $context, array $options ) {
 671          parent::__construct( $context );
 672  
 673          $msgs = array(
 674              'diff',
 675              'hist',
 676              'newarticle',
 677              'pipe-separator',
 678              'rev-delundel',
 679              'rollbacklink',
 680              'uctop'
 681          );
 682  
 683          foreach ( $msgs as $msg ) {
 684              $this->messages[$msg] = $this->msg( $msg )->escaped();
 685          }
 686  
 687          $this->target = isset( $options['target'] ) ? $options['target'] : '';
 688          $this->contribs = isset( $options['contribs'] ) ? $options['contribs'] : 'users';
 689          $this->namespace = isset( $options['namespace'] ) ? $options['namespace'] : '';
 690          $this->tagFilter = isset( $options['tagfilter'] ) ? $options['tagfilter'] : false;
 691          $this->nsInvert = isset( $options['nsInvert'] ) ? $options['nsInvert'] : false;
 692          $this->associated = isset( $options['associated'] ) ? $options['associated'] : false;
 693  
 694          $this->deletedOnly = !empty( $options['deletedOnly'] );
 695          $this->topOnly = !empty( $options['topOnly'] );
 696          $this->newOnly = !empty( $options['newOnly'] );
 697  
 698          $year = isset( $options['year'] ) ? $options['year'] : false;
 699          $month = isset( $options['month'] ) ? $options['month'] : false;
 700          $this->getDateCond( $year, $month );
 701  
 702          // Most of this code will use the 'contributions' group DB, which can map to slaves
 703          // with extra user based indexes or partioning by user. The additional metadata
 704          // queries should use a regular slave since the lookup pattern is not all by user.
 705          $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave
 706          $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
 707      }
 708  
 709  	function getDefaultQuery() {
 710          $query = parent::getDefaultQuery();
 711          $query['target'] = $this->target;
 712  
 713          return $query;
 714      }
 715  
 716      /**
 717       * This method basically executes the exact same code as the parent class, though with
 718       * a hook added, to allow extentions to add additional queries.
 719       *
 720       * @param string $offset Index offset, inclusive
 721       * @param int $limit Exact query limit
 722       * @param bool $descending Query direction, false for ascending, true for descending
 723       * @return ResultWrapper
 724       */
 725  	function reallyDoQuery( $offset, $limit, $descending ) {
 726          list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo(
 727              $offset,
 728              $limit,
 729              $descending
 730          );
 731          $pager = $this;
 732  
 733          /*
 734           * This hook will allow extensions to add in additional queries, so they can get their data
 735           * in My Contributions as well. Extensions should append their results to the $data array.
 736           *
 737           * Extension queries have to implement the navbar requirement as well. They should
 738           * - have a column aliased as $pager->getIndexField()
 739           * - have LIMIT set
 740           * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset
 741           * - have the ORDER BY specified based upon the details provided by the navbar
 742           *
 743           * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY
 744           *
 745           * &$data: an array of results of all contribs queries
 746           * $pager: the ContribsPager object hooked into
 747           * $offset: see phpdoc above
 748           * $limit: see phpdoc above
 749           * $descending: see phpdoc above
 750           */
 751          $data = array( $this->mDb->select(
 752              $tables, $fields, $conds, $fname, $options, $join_conds
 753          ) );
 754          wfRunHooks(
 755              'ContribsPager::reallyDoQuery',
 756              array( &$data, $pager, $offset, $limit, $descending )
 757          );
 758  
 759          $result = array();
 760  
 761          // loop all results and collect them in an array
 762          foreach ( $data as $query ) {
 763              foreach ( $query as $i => $row ) {
 764                  // use index column as key, allowing us to easily sort in PHP
 765                  $result[$row->{$this->getIndexField()} . "-$i"] = $row;
 766              }
 767          }
 768  
 769          // sort results
 770          if ( $descending ) {
 771              ksort( $result );
 772          } else {
 773              krsort( $result );
 774          }
 775  
 776          // enforce limit
 777          $result = array_slice( $result, 0, $limit );
 778  
 779          // get rid of array keys
 780          $result = array_values( $result );
 781  
 782          return new FakeResultWrapper( $result );
 783      }
 784  
 785  	function getQueryInfo() {
 786          list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
 787  
 788          $user = $this->getUser();
 789          $conds = array_merge( $userCond, $this->getNamespaceCond() );
 790  
 791          // Paranoia: avoid brute force searches (bug 17342)
 792          if ( !$user->isAllowed( 'deletedhistory' ) ) {
 793              $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0';
 794          } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
 795              $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) .
 796                  ' != ' . Revision::SUPPRESSED_USER;
 797          }
 798  
 799          # Don't include orphaned revisions
 800          $join_cond['page'] = Revision::pageJoinCond();
 801          # Get the current user name for accounts
 802          $join_cond['user'] = Revision::userJoinCond();
 803  
 804          $options = array();
 805          if ( $index ) {
 806              $options['USE INDEX'] = array( 'revision' => $index );
 807          }
 808  
 809          $queryInfo = array(
 810              'tables' => $tables,
 811              'fields' => array_merge(
 812                  Revision::selectFields(),
 813                  Revision::selectUserFields(),
 814                  array( 'page_namespace', 'page_title', 'page_is_new',
 815                      'page_latest', 'page_is_redirect', 'page_len' )
 816              ),
 817              'conds' => $conds,
 818              'options' => $options,
 819              'join_conds' => $join_cond
 820          );
 821  
 822          ChangeTags::modifyDisplayQuery(
 823              $queryInfo['tables'],
 824              $queryInfo['fields'],
 825              $queryInfo['conds'],
 826              $queryInfo['join_conds'],
 827              $queryInfo['options'],
 828              $this->tagFilter
 829          );
 830  
 831          wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
 832  
 833          return $queryInfo;
 834      }
 835  
 836  	function getUserCond() {
 837          $condition = array();
 838          $join_conds = array();
 839          $tables = array( 'revision', 'page', 'user' );
 840          $index = false;
 841          if ( $this->contribs == 'newbie' ) {
 842              $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
 843              $condition[] = 'rev_user >' . (int)( $max - $max / 100 );
 844              # ignore local groups with the bot right
 845              # @todo FIXME: Global groups may have 'bot' rights
 846              $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
 847              if ( count( $groupsWithBotPermission ) ) {
 848                  $tables[] = 'user_groups';
 849                  $condition[] = 'ug_group IS NULL';
 850                  $join_conds['user_groups'] = array(
 851                      'LEFT JOIN', array(
 852                          'ug_user = rev_user',
 853                          'ug_group' => $groupsWithBotPermission
 854                      )
 855                  );
 856              }
 857          } else {
 858              $uid = User::idFromName( $this->target );
 859              if ( $uid ) {
 860                  $condition['rev_user'] = $uid;
 861                  $index = 'user_timestamp';
 862              } else {
 863                  $condition['rev_user_text'] = $this->target;
 864                  $index = 'usertext_timestamp';
 865              }
 866          }
 867  
 868          if ( $this->deletedOnly ) {
 869              $condition[] = 'rev_deleted != 0';
 870          }
 871  
 872          if ( $this->topOnly ) {
 873              $condition[] = 'rev_id = page_latest';
 874          }
 875  
 876          if ( $this->newOnly ) {
 877              $condition[] = 'rev_parent_id = 0';
 878          }
 879  
 880          return array( $tables, $index, $condition, $join_conds );
 881      }
 882  
 883  	function getNamespaceCond() {
 884          if ( $this->namespace !== '' ) {
 885              $selectedNS = $this->mDb->addQuotes( $this->namespace );
 886              $eq_op = $this->nsInvert ? '!=' : '=';
 887              $bool_op = $this->nsInvert ? 'AND' : 'OR';
 888  
 889              if ( !$this->associated ) {
 890                  return array( "page_namespace $eq_op $selectedNS" );
 891              }
 892  
 893              $associatedNS = $this->mDb->addQuotes(
 894                  MWNamespace::getAssociated( $this->namespace )
 895              );
 896  
 897              return array(
 898                  "page_namespace $eq_op $selectedNS " .
 899                      $bool_op .
 900                      " page_namespace $eq_op $associatedNS"
 901              );
 902          }
 903  
 904          return array();
 905      }
 906  
 907  	function getIndexField() {
 908          return 'rev_timestamp';
 909      }
 910  
 911  	function doBatchLookups() {
 912          # Do a link batch query
 913          $this->mResult->seek( 0 );
 914          $revIds = array();
 915          $batch = new LinkBatch();
 916          # Give some pointers to make (last) links
 917          foreach ( $this->mResult as $row ) {
 918              if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
 919                  $revIds[] = $row->rev_parent_id;
 920              }
 921              if ( isset( $row->rev_id ) ) {
 922                  if ( $this->contribs === 'newbie' ) { // multiple users
 923                      $batch->add( NS_USER, $row->user_name );
 924                      $batch->add( NS_USER_TALK, $row->user_name );
 925                  }
 926                  $batch->add( $row->page_namespace, $row->page_title );
 927              }
 928          }
 929          $this->mParentLens = Revision::getParentLengths( $this->mDbSecondary, $revIds );
 930          $batch->execute();
 931          $this->mResult->seek( 0 );
 932      }
 933  
 934      /**
 935       * @return string
 936       */
 937  	function getStartBody() {
 938          return "<ul>\n";
 939      }
 940  
 941      /**
 942       * @return string
 943       */
 944  	function getEndBody() {
 945          return "</ul>\n";
 946      }
 947  
 948      /**
 949       * Generates each row in the contributions list.
 950       *
 951       * Contributions which are marked "top" are currently on top of the history.
 952       * For these contributions, a [rollback] link is shown for users with roll-
 953       * back privileges. The rollback link restores the most recent version that
 954       * was not written by the target user.
 955       *
 956       * @todo This would probably look a lot nicer in a table.
 957       * @param object $row
 958       * @return string
 959       */
 960  	function formatRow( $row ) {
 961          wfProfileIn( __METHOD__ );
 962  
 963          $ret = '';
 964          $classes = array();
 965  
 966          /*
 967           * There may be more than just revision rows. To make sure that we'll only be processing
 968           * revisions here, let's _try_ to build a revision out of our row (without displaying
 969           * notices though) and then trying to grab data from the built object. If we succeed,
 970           * we're definitely dealing with revision data and we may proceed, if not, we'll leave it
 971           * to extensions to subscribe to the hook to parse the row.
 972           */
 973          wfSuppressWarnings();
 974          try {
 975              $rev = new Revision( $row );
 976              $validRevision = (bool)$rev->getId();
 977          } catch ( MWException $e ) {
 978              $validRevision = false;
 979          }
 980          wfRestoreWarnings();
 981  
 982          if ( $validRevision ) {
 983              $classes = array();
 984  
 985              $page = Title::newFromRow( $row );
 986              $link = Linker::link(
 987                  $page,
 988                  htmlspecialchars( $page->getPrefixedText() ),
 989                  array( 'class' => 'mw-contributions-title' ),
 990                  $page->isRedirect() ? array( 'redirect' => 'no' ) : array()
 991              );
 992              # Mark current revisions
 993              $topmarktext = '';
 994              $user = $this->getUser();
 995              if ( $row->rev_id == $row->page_latest ) {
 996                  $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
 997                  # Add rollback link
 998                  if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user )
 999                      && $page->quickUserCan( 'edit', $user )
1000                  ) {
1001                      $this->preventClickjacking();
1002                      $topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
1003                  }
1004              }
1005              # Is there a visible previous revision?
1006              if ( $rev->userCan( Revision::DELETED_TEXT, $user ) && $rev->getParentId() !== 0 ) {
1007                  $difftext = Linker::linkKnown(
1008                      $page,
1009                      $this->messages['diff'],
1010                      array(),
1011                      array(
1012                          'diff' => 'prev',
1013                          'oldid' => $row->rev_id
1014                      )
1015                  );
1016              } else {
1017                  $difftext = $this->messages['diff'];
1018              }
1019              $histlink = Linker::linkKnown(
1020                  $page,
1021                  $this->messages['hist'],
1022                  array(),
1023                  array( 'action' => 'history' )
1024              );
1025  
1026              if ( $row->rev_parent_id === null ) {
1027                  // For some reason rev_parent_id isn't populated for this row.
1028                  // Its rumoured this is true on wikipedia for some revisions (bug 34922).
1029                  // Next best thing is to have the total number of bytes.
1030                  $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
1031                  $chardiff .= Linker::formatRevisionSize( $row->rev_len );
1032                  $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
1033              } else {
1034                  $parentLen = 0;
1035                  if ( isset( $this->mParentLens[$row->rev_parent_id] ) ) {
1036                      $parentLen = $this->mParentLens[$row->rev_parent_id];
1037                  }
1038  
1039                  $chardiff = ' <span class="mw-changeslist-separator">. .</span> ';
1040                  $chardiff .= ChangesList::showCharacterDifference(
1041                      $parentLen,
1042                      $row->rev_len,
1043                      $this->getContext()
1044                  );
1045                  $chardiff .= ' <span class="mw-changeslist-separator">. .</span> ';
1046              }
1047  
1048              $lang = $this->getLanguage();
1049              $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true );
1050              $date = $lang->userTimeAndDate( $row->rev_timestamp, $user );
1051              if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
1052                  $d = Linker::linkKnown(
1053                      $page,
1054                      htmlspecialchars( $date ),
1055                      array( 'class' => 'mw-changeslist-date' ),
1056                      array( 'oldid' => intval( $row->rev_id ) )
1057                  );
1058              } else {
1059                  $d = htmlspecialchars( $date );
1060              }
1061              if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
1062                  $d = '<span class="history-deleted">' . $d . '</span>';
1063              }
1064  
1065              # Show user names for /newbies as there may be different users.
1066              # Note that we already excluded rows with hidden user names.
1067              if ( $this->contribs == 'newbie' ) {
1068                  $userlink = ' . . ' . $lang->getDirMark()
1069                      . Linker::userLink( $rev->getUser(), $rev->getUserText() );
1070                  $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(
1071                      Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' ';
1072              } else {
1073                  $userlink = '';
1074              }
1075  
1076              if ( $rev->getParentId() === 0 ) {
1077                  $nflag = ChangesList::flag( 'newpage' );
1078              } else {
1079                  $nflag = '';
1080              }
1081  
1082              if ( $rev->isMinor() ) {
1083                  $mflag = ChangesList::flag( 'minor' );
1084              } else {
1085                  $mflag = '';
1086              }
1087  
1088              $del = Linker::getRevDeleteLink( $user, $rev, $page );
1089              if ( $del !== '' ) {
1090                  $del .= ' ';
1091              }
1092  
1093              $diffHistLinks = $this->msg( 'parentheses' )
1094                  ->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )
1095                  ->escaped();
1096              $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} ";
1097              $ret .= "{$link}{$userlink} {$comment} {$topmarktext}";
1098  
1099              # Denote if username is redacted for this edit
1100              if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1101                  $ret .= " <strong>" .
1102                      $this->msg( 'rev-deleted-user-contribs' )->escaped() .
1103                      "</strong>";
1104              }
1105  
1106              # Tags, if any.
1107              list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
1108                  $row->ts_tags,
1109                  'contributions'
1110              );
1111              $classes = array_merge( $classes, $newClasses );
1112              $ret .= " $tagSummary";
1113          }
1114  
1115          // Let extensions add data
1116          wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) );
1117  
1118          if ( $classes === array() && $ret === '' ) {
1119              wfDebug( "Dropping Special:Contribution row that could not be formatted\n" );
1120              $ret = "<!-- Could not format Special:Contribution row. -->\n";
1121          } else {
1122              $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n";
1123          }
1124  
1125          wfProfileOut( __METHOD__ );
1126  
1127          return $ret;
1128      }
1129  
1130      /**
1131       * Overwrite Pager function and return a helpful comment
1132       * @return string
1133       */
1134  	function getSqlComment() {
1135          if ( $this->namespace || $this->deletedOnly ) {
1136              // potentially slow, see CR r58153
1137              return 'contributions page filtered for namespace or RevisionDeleted edits';
1138          } else {
1139              return 'contributions page unfiltered';
1140          }
1141      }
1142  
1143  	protected function preventClickjacking() {
1144          $this->preventClickjacking = true;
1145      }
1146  
1147      /**
1148       * @return bool
1149       */
1150  	public function getPreventClickjacking() {
1151          return $this->preventClickjacking;
1152      }
1153  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1