MediaWiki
REL1_23
|
00001 <?php 00032 class ApiQueryContributions extends ApiQueryBase { 00033 00034 public function __construct( $query, $moduleName ) { 00035 parent::__construct( $query, $moduleName, 'uc' ); 00036 } 00037 00038 private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens; 00039 private $fld_ids = false, $fld_title = false, $fld_timestamp = false, 00040 $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, 00041 $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; 00042 00043 public function execute() { 00044 // Parse some parameters 00045 $this->params = $this->extractRequestParams(); 00046 00047 $prop = array_flip( $this->params['prop'] ); 00048 $this->fld_ids = isset( $prop['ids'] ); 00049 $this->fld_title = isset( $prop['title'] ); 00050 $this->fld_comment = isset( $prop['comment'] ); 00051 $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); 00052 $this->fld_size = isset( $prop['size'] ); 00053 $this->fld_sizediff = isset( $prop['sizediff'] ); 00054 $this->fld_flags = isset( $prop['flags'] ); 00055 $this->fld_timestamp = isset( $prop['timestamp'] ); 00056 $this->fld_patrolled = isset( $prop['patrolled'] ); 00057 $this->fld_tags = isset( $prop['tags'] ); 00058 00059 // Most of this code will use the 'contributions' group DB, which can map to slaves 00060 // with extra user based indexes or partioning by user. The additional metadata 00061 // queries should use a regular slave since the lookup pattern is not all by user. 00062 $dbSecondary = $this->getDB(); // any random slave 00063 00064 // TODO: if the query is going only against the revision table, should this be done? 00065 $this->selectNamedDB( 'contributions', DB_SLAVE, 'contributions' ); 00066 00067 if ( isset( $this->params['userprefix'] ) ) { 00068 $this->prefixMode = true; 00069 $this->multiUserMode = true; 00070 $this->userprefix = $this->params['userprefix']; 00071 } else { 00072 $this->usernames = array(); 00073 if ( !is_array( $this->params['user'] ) ) { 00074 $this->params['user'] = array( $this->params['user'] ); 00075 } 00076 if ( !count( $this->params['user'] ) ) { 00077 $this->dieUsage( 'User parameter may not be empty.', 'param_user' ); 00078 } 00079 foreach ( $this->params['user'] as $u ) { 00080 $this->prepareUsername( $u ); 00081 } 00082 $this->prefixMode = false; 00083 $this->multiUserMode = ( count( $this->params['user'] ) > 1 ); 00084 } 00085 00086 $this->prepareQuery(); 00087 00088 // Do the actual query. 00089 $res = $this->select( __METHOD__ ); 00090 00091 if ( $this->fld_sizediff ) { 00092 $revIds = array(); 00093 foreach ( $res as $row ) { 00094 if ( $row->rev_parent_id ) { 00095 $revIds[] = $row->rev_parent_id; 00096 } 00097 } 00098 $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds ); 00099 $res->rewind(); // reset 00100 } 00101 00102 // Initialise some variables 00103 $count = 0; 00104 $limit = $this->params['limit']; 00105 00106 // Fetch each row 00107 foreach ( $res as $row ) { 00108 if ( ++$count > $limit ) { 00109 // We've reached the one extra which shows that there are 00110 // additional pages to be had. Stop here... 00111 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); 00112 break; 00113 } 00114 00115 $vals = $this->extractRowInfo( $row ); 00116 $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals ); 00117 if ( !$fit ) { 00118 $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); 00119 break; 00120 } 00121 } 00122 00123 $this->getResult()->setIndexedTagName_internal( 00124 array( 'query', $this->getModuleName() ), 00125 'item' 00126 ); 00127 } 00128 00135 private function prepareUsername( $user ) { 00136 if ( !is_null( $user ) && $user !== '' ) { 00137 $name = User::isIP( $user ) 00138 ? $user 00139 : User::getCanonicalName( $user, 'valid' ); 00140 if ( $name === false ) { 00141 $this->dieUsage( "User name {$user} is not valid", 'param_user' ); 00142 } else { 00143 $this->usernames[] = $name; 00144 } 00145 } else { 00146 $this->dieUsage( 'User parameter may not be empty', 'param_user' ); 00147 } 00148 } 00149 00153 private function prepareQuery() { 00154 // We're after the revision table, and the corresponding page 00155 // row for anything we retrieve. We may also need the 00156 // recentchanges row and/or tag summary row. 00157 $user = $this->getUser(); 00158 $tables = array( 'page', 'revision' ); // Order may change 00159 $this->addWhere( 'page_id=rev_page' ); 00160 00161 // Handle continue parameter 00162 if ( !is_null( $this->params['continue'] ) ) { 00163 $continue = explode( '|', $this->params['continue'] ); 00164 $db = $this->getDB(); 00165 if ( $this->multiUserMode ) { 00166 $this->dieContinueUsageIf( count( $continue ) != 3 ); 00167 $encUser = $db->addQuotes( array_shift( $continue ) ); 00168 } else { 00169 $this->dieContinueUsageIf( count( $continue ) != 2 ); 00170 } 00171 $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) ); 00172 $encId = (int)$continue[1]; 00173 $this->dieContinueUsageIf( $encId != $continue[1] ); 00174 $op = ( $this->params['dir'] == 'older' ? '<' : '>' ); 00175 if ( $this->multiUserMode ) { 00176 $this->addWhere( 00177 "rev_user_text $op $encUser OR " . 00178 "(rev_user_text = $encUser AND " . 00179 "(rev_timestamp $op $encTS OR " . 00180 "(rev_timestamp = $encTS AND " . 00181 "rev_id $op= $encId)))" 00182 ); 00183 } else { 00184 $this->addWhere( 00185 "rev_timestamp $op $encTS OR " . 00186 "(rev_timestamp = $encTS AND " . 00187 "rev_id $op= $encId)" 00188 ); 00189 } 00190 } 00191 00192 // Don't include any revisions where we're not supposed to be able to 00193 // see the username. 00194 if ( !$user->isAllowed( 'deletedhistory' ) ) { 00195 $bitmask = Revision::DELETED_USER; 00196 } elseif ( !$user->isAllowed( 'suppressrevision' ) ) { 00197 $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; 00198 } else { 00199 $bitmask = 0; 00200 } 00201 if ( $bitmask ) { 00202 $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); 00203 } 00204 00205 // We only want pages by the specified users. 00206 if ( $this->prefixMode ) { 00207 $this->addWhere( 'rev_user_text' . 00208 $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) ); 00209 } else { 00210 $this->addWhereFld( 'rev_user_text', $this->usernames ); 00211 } 00212 // ... and in the specified timeframe. 00213 // Ensure the same sort order for rev_user_text and rev_timestamp 00214 // so our query is indexed 00215 if ( $this->multiUserMode ) { 00216 $this->addWhereRange( 'rev_user_text', $this->params['dir'], null, null ); 00217 } 00218 $this->addTimestampWhereRange( 'rev_timestamp', 00219 $this->params['dir'], $this->params['start'], $this->params['end'] ); 00220 // Include in ORDER BY for uniqueness 00221 $this->addWhereRange( 'rev_id', $this->params['dir'], null, null ); 00222 00223 $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); 00224 00225 $show = $this->params['show']; 00226 if ( $this->params['toponly'] ) { // deprecated/old param 00227 $show[] = 'top'; 00228 } 00229 if ( !is_null( $show ) ) { 00230 $show = array_flip( $show ); 00231 00232 if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) 00233 || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) 00234 || ( isset( $show['top'] ) && isset( $show['!top'] ) ) 00235 || ( isset( $show['new'] ) && isset( $show['!new'] ) ) 00236 ) { 00237 $this->dieUsageMsg( 'show' ); 00238 } 00239 00240 $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) ); 00241 $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) ); 00242 $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) ); 00243 $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); 00244 $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) ); 00245 $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) ); 00246 $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) ); 00247 $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) ); 00248 } 00249 $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); 00250 $index = array( 'revision' => 'usertext_timestamp' ); 00251 00252 // Mandatory fields: timestamp allows request continuation 00253 // ns+title checks if the user has access rights for this page 00254 // user_text is necessary if multiple users were specified 00255 $this->addFields( array( 00256 'rev_id', 00257 'rev_timestamp', 00258 'page_namespace', 00259 'page_title', 00260 'rev_user', 00261 'rev_user_text', 00262 'rev_deleted' 00263 ) ); 00264 00265 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) || 00266 $this->fld_patrolled 00267 ) { 00268 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { 00269 $this->dieUsage( 00270 'You need the patrol right to request the patrolled flag', 00271 'permissiondenied' 00272 ); 00273 } 00274 00275 // Use a redundant join condition on both 00276 // timestamp and ID so we can use the timestamp 00277 // index 00278 $index['recentchanges'] = 'rc_user_text'; 00279 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) { 00280 // Put the tables in the right order for 00281 // STRAIGHT_JOIN 00282 $tables = array( 'revision', 'recentchanges', 'page' ); 00283 $this->addOption( 'STRAIGHT_JOIN' ); 00284 $this->addWhere( 'rc_user_text=rev_user_text' ); 00285 $this->addWhere( 'rc_timestamp=rev_timestamp' ); 00286 $this->addWhere( 'rc_this_oldid=rev_id' ); 00287 } else { 00288 $tables[] = 'recentchanges'; 00289 $this->addJoinConds( array( 'recentchanges' => array( 00290 'LEFT JOIN', array( 00291 'rc_user_text=rev_user_text', 00292 'rc_timestamp=rev_timestamp', 00293 'rc_this_oldid=rev_id' ) ) ) ); 00294 } 00295 } 00296 00297 $this->addTables( $tables ); 00298 $this->addFieldsIf( 'rev_page', $this->fld_ids ); 00299 $this->addFieldsIf( 'page_latest', $this->fld_flags ); 00300 // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed? 00301 $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment ); 00302 $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff ); 00303 $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags ); 00304 $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids ); 00305 $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); 00306 00307 if ( $this->fld_tags ) { 00308 $this->addTables( 'tag_summary' ); 00309 $this->addJoinConds( 00310 array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) 00311 ); 00312 $this->addFields( 'ts_tags' ); 00313 } 00314 00315 if ( isset( $this->params['tag'] ) ) { 00316 $this->addTables( 'change_tag' ); 00317 $this->addJoinConds( 00318 array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) 00319 ); 00320 $this->addWhereFld( 'ct_tag', $this->params['tag'] ); 00321 } 00322 00323 $this->addOption( 'USE INDEX', $index ); 00324 } 00325 00332 private function extractRowInfo( $row ) { 00333 $vals = array(); 00334 $anyHidden = false; 00335 00336 if ( $row->rev_deleted & Revision::DELETED_TEXT ) { 00337 $vals['texthidden'] = ''; 00338 $anyHidden = true; 00339 } 00340 00341 // Any rows where we can't view the user were filtered out in the query. 00342 $vals['userid'] = $row->rev_user; 00343 $vals['user'] = $row->rev_user_text; 00344 if ( $row->rev_deleted & Revision::DELETED_USER ) { 00345 $vals['userhidden'] = ''; 00346 $anyHidden = true; 00347 } 00348 if ( $this->fld_ids ) { 00349 $vals['pageid'] = intval( $row->rev_page ); 00350 $vals['revid'] = intval( $row->rev_id ); 00351 // $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed? 00352 00353 if ( !is_null( $row->rev_parent_id ) ) { 00354 $vals['parentid'] = intval( $row->rev_parent_id ); 00355 } 00356 } 00357 00358 $title = Title::makeTitle( $row->page_namespace, $row->page_title ); 00359 00360 if ( $this->fld_title ) { 00361 ApiQueryBase::addTitleInfo( $vals, $title ); 00362 } 00363 00364 if ( $this->fld_timestamp ) { 00365 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); 00366 } 00367 00368 if ( $this->fld_flags ) { 00369 if ( $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id ) ) { 00370 $vals['new'] = ''; 00371 } 00372 if ( $row->rev_minor_edit ) { 00373 $vals['minor'] = ''; 00374 } 00375 if ( $row->page_latest == $row->rev_id ) { 00376 $vals['top'] = ''; 00377 } 00378 } 00379 00380 if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) { 00381 if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { 00382 $vals['commenthidden'] = ''; 00383 $anyHidden = true; 00384 } 00385 00386 $userCanView = Revision::userCanBitfield( 00387 $row->rev_deleted, 00388 Revision::DELETED_COMMENT, $this->getUser() 00389 ); 00390 00391 if ( $userCanView ) { 00392 if ( $this->fld_comment ) { 00393 $vals['comment'] = $row->rev_comment; 00394 } 00395 00396 if ( $this->fld_parsedcomment ) { 00397 $vals['parsedcomment'] = Linker::formatComment( $row->rev_comment, $title ); 00398 } 00399 } 00400 } 00401 00402 if ( $this->fld_patrolled && $row->rc_patrolled ) { 00403 $vals['patrolled'] = ''; 00404 } 00405 00406 if ( $this->fld_size && !is_null( $row->rev_len ) ) { 00407 $vals['size'] = intval( $row->rev_len ); 00408 } 00409 00410 if ( $this->fld_sizediff 00411 && !is_null( $row->rev_len ) 00412 && !is_null( $row->rev_parent_id ) 00413 ) { 00414 $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) 00415 ? $this->parentLens[$row->rev_parent_id] 00416 : 0; 00417 $vals['sizediff'] = intval( $row->rev_len - $parentLen ); 00418 } 00419 00420 if ( $this->fld_tags ) { 00421 if ( $row->ts_tags ) { 00422 $tags = explode( ',', $row->ts_tags ); 00423 $this->getResult()->setIndexedTagName( $tags, 'tag' ); 00424 $vals['tags'] = $tags; 00425 } else { 00426 $vals['tags'] = array(); 00427 } 00428 } 00429 00430 if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) { 00431 $vals['suppressed'] = ''; 00432 } 00433 00434 return $vals; 00435 } 00436 00437 private function continueStr( $row ) { 00438 if ( $this->multiUserMode ) { 00439 return "$row->rev_user_text|$row->rev_timestamp|$row->rev_id"; 00440 } else { 00441 return "$row->rev_timestamp|$row->rev_id"; 00442 } 00443 } 00444 00445 public function getCacheMode( $params ) { 00446 // This module provides access to deleted revisions and patrol flags if 00447 // the requester is logged in 00448 return 'anon-public-user-private'; 00449 } 00450 00451 public function getAllowedParams() { 00452 return array( 00453 'limit' => array( 00454 ApiBase::PARAM_DFLT => 10, 00455 ApiBase::PARAM_TYPE => 'limit', 00456 ApiBase::PARAM_MIN => 1, 00457 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, 00458 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 00459 ), 00460 'start' => array( 00461 ApiBase::PARAM_TYPE => 'timestamp' 00462 ), 00463 'end' => array( 00464 ApiBase::PARAM_TYPE => 'timestamp' 00465 ), 00466 'continue' => null, 00467 'user' => array( 00468 ApiBase::PARAM_ISMULTI => true 00469 ), 00470 'userprefix' => null, 00471 'dir' => array( 00472 ApiBase::PARAM_DFLT => 'older', 00473 ApiBase::PARAM_TYPE => array( 00474 'newer', 00475 'older' 00476 ) 00477 ), 00478 'namespace' => array( 00479 ApiBase::PARAM_ISMULTI => true, 00480 ApiBase::PARAM_TYPE => 'namespace' 00481 ), 00482 'prop' => array( 00483 ApiBase::PARAM_ISMULTI => true, 00484 ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags', 00485 ApiBase::PARAM_TYPE => array( 00486 'ids', 00487 'title', 00488 'timestamp', 00489 'comment', 00490 'parsedcomment', 00491 'size', 00492 'sizediff', 00493 'flags', 00494 'patrolled', 00495 'tags' 00496 ) 00497 ), 00498 'show' => array( 00499 ApiBase::PARAM_ISMULTI => true, 00500 ApiBase::PARAM_TYPE => array( 00501 'minor', 00502 '!minor', 00503 'patrolled', 00504 '!patrolled', 00505 'top', 00506 '!top', 00507 'new', 00508 '!new', 00509 ) 00510 ), 00511 'tag' => null, 00512 'toponly' => array( 00513 ApiBase::PARAM_DFLT => false, 00514 ApiBase::PARAM_DEPRECATED => true, 00515 ), 00516 ); 00517 } 00518 00519 public function getParamDescription() { 00520 global $wgRCMaxAge; 00521 $p = $this->getModulePrefix(); 00522 00523 return array( 00524 'limit' => 'The maximum number of contributions to return', 00525 'start' => 'The start timestamp to return from', 00526 'end' => 'The end timestamp to return to', 00527 'continue' => 'When more results are available, use this to continue', 00528 'user' => 'The users to retrieve contributions for', 00529 'userprefix' => array( 00530 "Retrieve contributions for all users whose names begin with this value.", 00531 "Overrides {$p}user", 00532 ), 00533 'dir' => $this->getDirectionDescription( $p ), 00534 'namespace' => 'Only list contributions in these namespaces', 00535 'prop' => array( 00536 'Include additional pieces of information', 00537 ' ids - Adds the page ID and revision ID', 00538 ' title - Adds the title and namespace ID of the page', 00539 ' timestamp - Adds the timestamp of the edit', 00540 ' comment - Adds the comment of the edit', 00541 ' parsedcomment - Adds the parsed comment of the edit', 00542 ' size - Adds the new size of the edit', 00543 ' sizediff - Adds the size delta of the edit against its parent', 00544 ' flags - Adds flags of the edit', 00545 ' patrolled - Tags patrolled edits', 00546 ' tags - Lists tags for the edit', 00547 ), 00548 'show' => array( 00549 "Show only items that meet thse criteria, e.g. non minor edits only: {$p}show=!minor", 00550 "NOTE: If {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than", 00551 "\$wgRCMaxAge ($wgRCMaxAge) won't be shown", 00552 ), 00553 'tag' => 'Only list revisions tagged with this tag', 00554 'toponly' => 'Only list changes which are the latest revision', 00555 ); 00556 } 00557 00558 public function getResultProperties() { 00559 return array( 00560 '' => array( 00561 'userid' => 'integer', 00562 'user' => 'string', 00563 'userhidden' => 'boolean' 00564 ), 00565 'ids' => array( 00566 'pageid' => 'integer', 00567 'revid' => 'integer', 00568 'parentid' => array( 00569 ApiBase::PROP_TYPE => 'integer', 00570 ApiBase::PROP_NULLABLE => true 00571 ) 00572 ), 00573 'title' => array( 00574 'ns' => 'namespace', 00575 'title' => 'string' 00576 ), 00577 'timestamp' => array( 00578 'timestamp' => 'timestamp' 00579 ), 00580 'flags' => array( 00581 'new' => 'boolean', 00582 'minor' => 'boolean', 00583 'top' => 'boolean' 00584 ), 00585 'comment' => array( 00586 'commenthidden' => 'boolean', 00587 'comment' => array( 00588 ApiBase::PROP_TYPE => 'string', 00589 ApiBase::PROP_NULLABLE => true 00590 ) 00591 ), 00592 'parsedcomment' => array( 00593 'commenthidden' => 'boolean', 00594 'parsedcomment' => array( 00595 ApiBase::PROP_TYPE => 'string', 00596 ApiBase::PROP_NULLABLE => true 00597 ) 00598 ), 00599 'patrolled' => array( 00600 'patrolled' => 'boolean' 00601 ), 00602 'size' => array( 00603 'size' => array( 00604 ApiBase::PROP_TYPE => 'integer', 00605 ApiBase::PROP_NULLABLE => true 00606 ) 00607 ), 00608 'sizediff' => array( 00609 'sizediff' => array( 00610 ApiBase::PROP_TYPE => 'integer', 00611 ApiBase::PROP_NULLABLE => true 00612 ) 00613 ) 00614 ); 00615 } 00616 00617 public function getDescription() { 00618 return 'Get all edits by a user.'; 00619 } 00620 00621 public function getPossibleErrors() { 00622 return array_merge( parent::getPossibleErrors(), array( 00623 array( 'code' => 'param_user', 'info' => 'User parameter may not be empty.' ), 00624 array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), 00625 array( 'show' ), 00626 array( 00627 'code' => 'permissiondenied', 00628 'info' => 'You need the patrol right to request the patrolled flag' 00629 ), 00630 ) ); 00631 } 00632 00633 public function getExamples() { 00634 return array( 00635 'api.php?action=query&list=usercontribs&ucuser=YurikBot', 00636 'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.', 00637 ); 00638 } 00639 00640 public function getHelpUrls() { 00641 return 'https://www.mediawiki.org/wiki/API:Usercontribs'; 00642 } 00643 }