[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/api/ -> ApiQueryUserContributions.php (source)

   1  <?php
   2  /**
   3   *
   4   *
   5   * Created on Oct 16, 2006
   6   *
   7   * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
   8   *
   9   * This program is free software; you can redistribute it and/or modify
  10   * it under the terms of the GNU General Public License as published by
  11   * the Free Software Foundation; either version 2 of the License, or
  12   * (at your option) any later version.
  13   *
  14   * This program is distributed in the hope that it will be useful,
  15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17   * GNU General Public License for more details.
  18   *
  19   * You should have received a copy of the GNU General Public License along
  20   * with this program; if not, write to the Free Software Foundation, Inc.,
  21   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22   * http://www.gnu.org/copyleft/gpl.html
  23   *
  24   * @file
  25   */
  26  
  27  /**
  28   * This query action adds a list of a specified user's contributions to the output.
  29   *
  30   * @ingroup API
  31   */
  32  class ApiQueryContributions extends ApiQueryBase {
  33  
  34  	public function __construct( ApiQuery $query, $moduleName ) {
  35          parent::__construct( $query, $moduleName, 'uc' );
  36      }
  37  
  38      private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens;
  39      private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
  40          $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
  41          $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
  42  
  43  	public function execute() {
  44          // Parse some parameters
  45          $this->params = $this->extractRequestParams();
  46  
  47          $prop = array_flip( $this->params['prop'] );
  48          $this->fld_ids = isset( $prop['ids'] );
  49          $this->fld_title = isset( $prop['title'] );
  50          $this->fld_comment = isset( $prop['comment'] );
  51          $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
  52          $this->fld_size = isset( $prop['size'] );
  53          $this->fld_sizediff = isset( $prop['sizediff'] );
  54          $this->fld_flags = isset( $prop['flags'] );
  55          $this->fld_timestamp = isset( $prop['timestamp'] );
  56          $this->fld_patrolled = isset( $prop['patrolled'] );
  57          $this->fld_tags = isset( $prop['tags'] );
  58  
  59          // Most of this code will use the 'contributions' group DB, which can map to slaves
  60          // with extra user based indexes or partioning by user. The additional metadata
  61          // queries should use a regular slave since the lookup pattern is not all by user.
  62          $dbSecondary = $this->getDB(); // any random slave
  63  
  64          // TODO: if the query is going only against the revision table, should this be done?
  65          $this->selectNamedDB( 'contributions', DB_SLAVE, 'contributions' );
  66  
  67          if ( isset( $this->params['userprefix'] ) ) {
  68              $this->prefixMode = true;
  69              $this->multiUserMode = true;
  70              $this->userprefix = $this->params['userprefix'];
  71          } else {
  72              $this->usernames = array();
  73              if ( !is_array( $this->params['user'] ) ) {
  74                  $this->params['user'] = array( $this->params['user'] );
  75              }
  76              if ( !count( $this->params['user'] ) ) {
  77                  $this->dieUsage( 'User parameter may not be empty.', 'param_user' );
  78              }
  79              foreach ( $this->params['user'] as $u ) {
  80                  $this->prepareUsername( $u );
  81              }
  82              $this->prefixMode = false;
  83              $this->multiUserMode = ( count( $this->params['user'] ) > 1 );
  84          }
  85  
  86          $this->prepareQuery();
  87  
  88          // Do the actual query.
  89          $res = $this->select( __METHOD__ );
  90  
  91          if ( $this->fld_sizediff ) {
  92              $revIds = array();
  93              foreach ( $res as $row ) {
  94                  if ( $row->rev_parent_id ) {
  95                      $revIds[] = $row->rev_parent_id;
  96                  }
  97              }
  98              $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
  99              $res->rewind(); // reset
 100          }
 101  
 102          // Initialise some variables
 103          $count = 0;
 104          $limit = $this->params['limit'];
 105  
 106          // Fetch each row
 107          foreach ( $res as $row ) {
 108              if ( ++$count > $limit ) {
 109                  // We've reached the one extra which shows that there are
 110                  // additional pages to be had. Stop here...
 111                  $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
 112                  break;
 113              }
 114  
 115              $vals = $this->extractRowInfo( $row );
 116              $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
 117              if ( !$fit ) {
 118                  $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
 119                  break;
 120              }
 121          }
 122  
 123          $this->getResult()->setIndexedTagName_internal(
 124              array( 'query', $this->getModuleName() ),
 125              'item'
 126          );
 127      }
 128  
 129      /**
 130       * Validate the 'user' parameter and set the value to compare
 131       * against `revision`.`rev_user_text`
 132       *
 133       * @param string $user
 134       */
 135  	private function prepareUsername( $user ) {
 136          if ( !is_null( $user ) && $user !== '' ) {
 137              $name = User::isIP( $user )
 138                  ? $user
 139                  : User::getCanonicalName( $user, 'valid' );
 140              if ( $name === false ) {
 141                  $this->dieUsage( "User name {$user} is not valid", 'param_user' );
 142              } else {
 143                  $this->usernames[] = $name;
 144              }
 145          } else {
 146              $this->dieUsage( 'User parameter may not be empty', 'param_user' );
 147          }
 148      }
 149  
 150      /**
 151       * Prepares the query and returns the limit of rows requested
 152       */
 153  	private function prepareQuery() {
 154          // We're after the revision table, and the corresponding page
 155          // row for anything we retrieve. We may also need the
 156          // recentchanges row and/or tag summary row.
 157          $user = $this->getUser();
 158          $tables = array( 'page', 'revision' ); // Order may change
 159          $this->addWhere( 'page_id=rev_page' );
 160  
 161          // Handle continue parameter
 162          if ( !is_null( $this->params['continue'] ) ) {
 163              $continue = explode( '|', $this->params['continue'] );
 164              $db = $this->getDB();
 165              if ( $this->multiUserMode ) {
 166                  $this->dieContinueUsageIf( count( $continue ) != 3 );
 167                  $encUser = $db->addQuotes( array_shift( $continue ) );
 168              } else {
 169                  $this->dieContinueUsageIf( count( $continue ) != 2 );
 170              }
 171              $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) );
 172              $encId = (int)$continue[1];
 173              $this->dieContinueUsageIf( $encId != $continue[1] );
 174              $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
 175              if ( $this->multiUserMode ) {
 176                  $this->addWhere(
 177                      "rev_user_text $op $encUser OR " .
 178                      "(rev_user_text = $encUser AND " .
 179                      "(rev_timestamp $op $encTS OR " .
 180                      "(rev_timestamp = $encTS AND " .
 181                      "rev_id $op= $encId)))"
 182                  );
 183              } else {
 184                  $this->addWhere(
 185                      "rev_timestamp $op $encTS OR " .
 186                      "(rev_timestamp = $encTS AND " .
 187                      "rev_id $op= $encId)"
 188                  );
 189              }
 190          }
 191  
 192          // Don't include any revisions where we're not supposed to be able to
 193          // see the username.
 194          if ( !$user->isAllowed( 'deletedhistory' ) ) {
 195              $bitmask = Revision::DELETED_USER;
 196          } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
 197              $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
 198          } else {
 199              $bitmask = 0;
 200          }
 201          if ( $bitmask ) {
 202              $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
 203          }
 204  
 205          // We only want pages by the specified users.
 206          if ( $this->prefixMode ) {
 207              $this->addWhere( 'rev_user_text' .
 208                  $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
 209          } else {
 210              $this->addWhereFld( 'rev_user_text', $this->usernames );
 211          }
 212          // ... and in the specified timeframe.
 213          // Ensure the same sort order for rev_user_text and rev_timestamp
 214          // so our query is indexed
 215          if ( $this->multiUserMode ) {
 216              $this->addWhereRange( 'rev_user_text', $this->params['dir'], null, null );
 217          }
 218          $this->addTimestampWhereRange( 'rev_timestamp',
 219              $this->params['dir'], $this->params['start'], $this->params['end'] );
 220          // Include in ORDER BY for uniqueness
 221          $this->addWhereRange( 'rev_id', $this->params['dir'], null, null );
 222  
 223          $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
 224  
 225          $show = $this->params['show'];
 226          if ( $this->params['toponly'] ) { // deprecated/old param
 227              $this->logFeatureUsage( 'list=usercontribs&uctoponly' );
 228              $show[] = 'top';
 229          }
 230          if ( !is_null( $show ) ) {
 231              $show = array_flip( $show );
 232  
 233              if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
 234                  || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
 235                  || ( isset( $show['top'] ) && isset( $show['!top'] ) )
 236                  || ( isset( $show['new'] ) && isset( $show['!new'] ) )
 237              ) {
 238                  $this->dieUsageMsg( 'show' );
 239              }
 240  
 241              $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
 242              $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
 243              $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
 244              $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
 245              $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) );
 246              $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) );
 247              $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
 248              $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
 249          }
 250          $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
 251          $index = array( 'revision' => 'usertext_timestamp' );
 252  
 253          // Mandatory fields: timestamp allows request continuation
 254          // ns+title checks if the user has access rights for this page
 255          // user_text is necessary if multiple users were specified
 256          $this->addFields( array(
 257              'rev_id',
 258              'rev_timestamp',
 259              'page_namespace',
 260              'page_title',
 261              'rev_user',
 262              'rev_user_text',
 263              'rev_deleted'
 264          ) );
 265  
 266          if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
 267              $this->fld_patrolled
 268          ) {
 269              if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
 270                  $this->dieUsage(
 271                      'You need the patrol right to request the patrolled flag',
 272                      'permissiondenied'
 273                  );
 274              }
 275  
 276              // Use a redundant join condition on both
 277              // timestamp and ID so we can use the timestamp
 278              // index
 279              $index['recentchanges'] = 'rc_user_text';
 280              if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
 281                  // Put the tables in the right order for
 282                  // STRAIGHT_JOIN
 283                  $tables = array( 'revision', 'recentchanges', 'page' );
 284                  $this->addOption( 'STRAIGHT_JOIN' );
 285                  $this->addWhere( 'rc_user_text=rev_user_text' );
 286                  $this->addWhere( 'rc_timestamp=rev_timestamp' );
 287                  $this->addWhere( 'rc_this_oldid=rev_id' );
 288              } else {
 289                  $tables[] = 'recentchanges';
 290                  $this->addJoinConds( array( 'recentchanges' => array(
 291                      'LEFT JOIN', array(
 292                          'rc_user_text=rev_user_text',
 293                          'rc_timestamp=rev_timestamp',
 294                          'rc_this_oldid=rev_id' ) ) ) );
 295              }
 296          }
 297  
 298          $this->addTables( $tables );
 299          $this->addFieldsIf( 'rev_page', $this->fld_ids );
 300          $this->addFieldsIf( 'page_latest', $this->fld_flags );
 301          // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
 302          $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
 303          $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
 304          $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
 305          $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
 306          $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
 307  
 308          if ( $this->fld_tags ) {
 309              $this->addTables( 'tag_summary' );
 310              $this->addJoinConds(
 311                  array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) )
 312              );
 313              $this->addFields( 'ts_tags' );
 314          }
 315  
 316          if ( isset( $this->params['tag'] ) ) {
 317              $this->addTables( 'change_tag' );
 318              $this->addJoinConds(
 319                  array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) )
 320              );
 321              $this->addWhereFld( 'ct_tag', $this->params['tag'] );
 322          }
 323  
 324          $this->addOption( 'USE INDEX', $index );
 325      }
 326  
 327      /**
 328       * Extract fields from the database row and append them to a result array
 329       *
 330       * @param stdClass $row
 331       * @return array
 332       */
 333  	private function extractRowInfo( $row ) {
 334          $vals = array();
 335          $anyHidden = false;
 336  
 337          if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
 338              $vals['texthidden'] = '';
 339              $anyHidden = true;
 340          }
 341  
 342          // Any rows where we can't view the user were filtered out in the query.
 343          $vals['userid'] = $row->rev_user;
 344          $vals['user'] = $row->rev_user_text;
 345          if ( $row->rev_deleted & Revision::DELETED_USER ) {
 346              $vals['userhidden'] = '';
 347              $anyHidden = true;
 348          }
 349          if ( $this->fld_ids ) {
 350              $vals['pageid'] = intval( $row->rev_page );
 351              $vals['revid'] = intval( $row->rev_id );
 352              // $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed?
 353  
 354              if ( !is_null( $row->rev_parent_id ) ) {
 355                  $vals['parentid'] = intval( $row->rev_parent_id );
 356              }
 357          }
 358  
 359          $title = Title::makeTitle( $row->page_namespace, $row->page_title );
 360  
 361          if ( $this->fld_title ) {
 362              ApiQueryBase::addTitleInfo( $vals, $title );
 363          }
 364  
 365          if ( $this->fld_timestamp ) {
 366              $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
 367          }
 368  
 369          if ( $this->fld_flags ) {
 370              if ( $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id ) ) {
 371                  $vals['new'] = '';
 372              }
 373              if ( $row->rev_minor_edit ) {
 374                  $vals['minor'] = '';
 375              }
 376              if ( $row->page_latest == $row->rev_id ) {
 377                  $vals['top'] = '';
 378              }
 379          }
 380  
 381          if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) {
 382              if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
 383                  $vals['commenthidden'] = '';
 384                  $anyHidden = true;
 385              }
 386  
 387              $userCanView = Revision::userCanBitfield(
 388                  $row->rev_deleted,
 389                  Revision::DELETED_COMMENT, $this->getUser()
 390              );
 391  
 392              if ( $userCanView ) {
 393                  if ( $this->fld_comment ) {
 394                      $vals['comment'] = $row->rev_comment;
 395                  }
 396  
 397                  if ( $this->fld_parsedcomment ) {
 398                      $vals['parsedcomment'] = Linker::formatComment( $row->rev_comment, $title );
 399                  }
 400              }
 401          }
 402  
 403          if ( $this->fld_patrolled && $row->rc_patrolled ) {
 404              $vals['patrolled'] = '';
 405          }
 406  
 407          if ( $this->fld_size && !is_null( $row->rev_len ) ) {
 408              $vals['size'] = intval( $row->rev_len );
 409          }
 410  
 411          if ( $this->fld_sizediff
 412              && !is_null( $row->rev_len )
 413              && !is_null( $row->rev_parent_id )
 414          ) {
 415              $parentLen = isset( $this->parentLens[$row->rev_parent_id] )
 416                  ? $this->parentLens[$row->rev_parent_id]
 417                  : 0;
 418              $vals['sizediff'] = intval( $row->rev_len - $parentLen );
 419          }
 420  
 421          if ( $this->fld_tags ) {
 422              if ( $row->ts_tags ) {
 423                  $tags = explode( ',', $row->ts_tags );
 424                  $this->getResult()->setIndexedTagName( $tags, 'tag' );
 425                  $vals['tags'] = $tags;
 426              } else {
 427                  $vals['tags'] = array();
 428              }
 429          }
 430  
 431          if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) {
 432              $vals['suppressed'] = '';
 433          }
 434  
 435          return $vals;
 436      }
 437  
 438  	private function continueStr( $row ) {
 439          if ( $this->multiUserMode ) {
 440              return "$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
 441          } else {
 442              return "$row->rev_timestamp|$row->rev_id";
 443          }
 444      }
 445  
 446  	public function getCacheMode( $params ) {
 447          // This module provides access to deleted revisions and patrol flags if
 448          // the requester is logged in
 449          return 'anon-public-user-private';
 450      }
 451  
 452  	public function getAllowedParams() {
 453          return array(
 454              'limit' => array(
 455                  ApiBase::PARAM_DFLT => 10,
 456                  ApiBase::PARAM_TYPE => 'limit',
 457                  ApiBase::PARAM_MIN => 1,
 458                  ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
 459                  ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
 460              ),
 461              'start' => array(
 462                  ApiBase::PARAM_TYPE => 'timestamp'
 463              ),
 464              'end' => array(
 465                  ApiBase::PARAM_TYPE => 'timestamp'
 466              ),
 467              'continue' => null,
 468              'user' => array(
 469                  ApiBase::PARAM_ISMULTI => true
 470              ),
 471              'userprefix' => null,
 472              'dir' => array(
 473                  ApiBase::PARAM_DFLT => 'older',
 474                  ApiBase::PARAM_TYPE => array(
 475                      'newer',
 476                      'older'
 477                  )
 478              ),
 479              'namespace' => array(
 480                  ApiBase::PARAM_ISMULTI => true,
 481                  ApiBase::PARAM_TYPE => 'namespace'
 482              ),
 483              'prop' => array(
 484                  ApiBase::PARAM_ISMULTI => true,
 485                  ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
 486                  ApiBase::PARAM_TYPE => array(
 487                      'ids',
 488                      'title',
 489                      'timestamp',
 490                      'comment',
 491                      'parsedcomment',
 492                      'size',
 493                      'sizediff',
 494                      'flags',
 495                      'patrolled',
 496                      'tags'
 497                  )
 498              ),
 499              'show' => array(
 500                  ApiBase::PARAM_ISMULTI => true,
 501                  ApiBase::PARAM_TYPE => array(
 502                      'minor',
 503                      '!minor',
 504                      'patrolled',
 505                      '!patrolled',
 506                      'top',
 507                      '!top',
 508                      'new',
 509                      '!new',
 510                  )
 511              ),
 512              'tag' => null,
 513              'toponly' => array(
 514                  ApiBase::PARAM_DFLT => false,
 515                  ApiBase::PARAM_DEPRECATED => true,
 516              ),
 517          );
 518      }
 519  
 520  	public function getParamDescription() {
 521          $p = $this->getModulePrefix();
 522          $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
 523  
 524          return array(
 525              'limit' => 'The maximum number of contributions to return',
 526              'start' => 'The start timestamp to return from',
 527              'end' => 'The end timestamp to return to',
 528              'continue' => 'When more results are available, use this to continue',
 529              'user' => 'The users to retrieve contributions for',
 530              'userprefix' => array(
 531                  "Retrieve contributions for all users whose names begin with this value.",
 532                  "Overrides {$p}user",
 533              ),
 534              'dir' => $this->getDirectionDescription( $p ),
 535              'namespace' => 'Only list contributions in these namespaces',
 536              'prop' => array(
 537                  'Include additional pieces of information',
 538                  ' ids            - Adds the page ID and revision ID',
 539                  ' title          - Adds the title and namespace ID of the page',
 540                  ' timestamp      - Adds the timestamp of the edit',
 541                  ' comment        - Adds the comment of the edit',
 542                  ' parsedcomment  - Adds the parsed comment of the edit',
 543                  ' size           - Adds the new size of the edit',
 544                  ' sizediff       - Adds the size delta of the edit against its parent',
 545                  ' flags          - Adds flags of the edit',
 546                  ' patrolled      - Tags patrolled edits',
 547                  ' tags           - Lists tags for the edit',
 548              ),
 549              'show' => array(
 550                  "Show only items that meet thse criteria, e.g. non minor edits only: {$p}show=!minor",
 551                  "NOTE: If {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than",
 552                  "\$wgRCMaxAge ($RCMaxAge) won't be shown",
 553              ),
 554              'tag' => 'Only list revisions tagged with this tag',
 555              'toponly' => 'Only list changes which are the latest revision',
 556          );
 557      }
 558  
 559  	public function getDescription() {
 560          return 'Get all edits by a user.';
 561      }
 562  
 563  	public function getExamples() {
 564          return array(
 565              'api.php?action=query&list=usercontribs&ucuser=YurikBot',
 566              'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.',
 567          );
 568      }
 569  
 570  	public function getHelpUrls() {
 571          return 'https://www.mediawiki.org/wiki/API:Usercontribs';
 572      }
 573  }


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