MediaWiki
REL1_24
|
00001 <?php 00032 class ApiQueryContributions extends ApiQueryBase { 00033 00034 public function __construct( ApiQuery $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->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { 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 $this->logFeatureUsage( 'list=usercontribs&uctoponly' ); 00228 $show[] = 'top'; 00229 } 00230 if ( !is_null( $show ) ) { 00231 $show = array_flip( $show ); 00232 00233 if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) 00234 || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) 00235 || ( isset( $show['top'] ) && isset( $show['!top'] ) ) 00236 || ( isset( $show['new'] ) && isset( $show['!new'] ) ) 00237 ) { 00238 $this->dieUsageMsg( 'show' ); 00239 } 00240 00241 $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) ); 00242 $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) ); 00243 $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) ); 00244 $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); 00245 $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) ); 00246 $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) ); 00247 $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) ); 00248 $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) ); 00249 } 00250 $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); 00251 $index = array( 'revision' => 'usertext_timestamp' ); 00252 00253 // Mandatory fields: timestamp allows request continuation 00254 // ns+title checks if the user has access rights for this page 00255 // user_text is necessary if multiple users were specified 00256 $this->addFields( array( 00257 'rev_id', 00258 'rev_timestamp', 00259 'page_namespace', 00260 'page_title', 00261 'rev_user', 00262 'rev_user_text', 00263 'rev_deleted' 00264 ) ); 00265 00266 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) || 00267 $this->fld_patrolled 00268 ) { 00269 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { 00270 $this->dieUsage( 00271 'You need the patrol right to request the patrolled flag', 00272 'permissiondenied' 00273 ); 00274 } 00275 00276 // Use a redundant join condition on both 00277 // timestamp and ID so we can use the timestamp 00278 // index 00279 $index['recentchanges'] = 'rc_user_text'; 00280 if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) { 00281 // Put the tables in the right order for 00282 // STRAIGHT_JOIN 00283 $tables = array( 'revision', 'recentchanges', 'page' ); 00284 $this->addOption( 'STRAIGHT_JOIN' ); 00285 $this->addWhere( 'rc_user_text=rev_user_text' ); 00286 $this->addWhere( 'rc_timestamp=rev_timestamp' ); 00287 $this->addWhere( 'rc_this_oldid=rev_id' ); 00288 } else { 00289 $tables[] = 'recentchanges'; 00290 $this->addJoinConds( array( 'recentchanges' => array( 00291 'LEFT JOIN', array( 00292 'rc_user_text=rev_user_text', 00293 'rc_timestamp=rev_timestamp', 00294 'rc_this_oldid=rev_id' ) ) ) ); 00295 } 00296 } 00297 00298 $this->addTables( $tables ); 00299 $this->addFieldsIf( 'rev_page', $this->fld_ids ); 00300 $this->addFieldsIf( 'page_latest', $this->fld_flags ); 00301 // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed? 00302 $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment ); 00303 $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff ); 00304 $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags ); 00305 $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids ); 00306 $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); 00307 00308 if ( $this->fld_tags ) { 00309 $this->addTables( 'tag_summary' ); 00310 $this->addJoinConds( 00311 array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) 00312 ); 00313 $this->addFields( 'ts_tags' ); 00314 } 00315 00316 if ( isset( $this->params['tag'] ) ) { 00317 $this->addTables( 'change_tag' ); 00318 $this->addJoinConds( 00319 array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) 00320 ); 00321 $this->addWhereFld( 'ct_tag', $this->params['tag'] ); 00322 } 00323 00324 $this->addOption( 'USE INDEX', $index ); 00325 } 00326 00333 private function extractRowInfo( $row ) { 00334 $vals = array(); 00335 $anyHidden = false; 00336 00337 if ( $row->rev_deleted & Revision::DELETED_TEXT ) { 00338 $vals['texthidden'] = ''; 00339 $anyHidden = true; 00340 } 00341 00342 // Any rows where we can't view the user were filtered out in the query. 00343 $vals['userid'] = $row->rev_user; 00344 $vals['user'] = $row->rev_user_text; 00345 if ( $row->rev_deleted & Revision::DELETED_USER ) { 00346 $vals['userhidden'] = ''; 00347 $anyHidden = true; 00348 } 00349 if ( $this->fld_ids ) { 00350 $vals['pageid'] = intval( $row->rev_page ); 00351 $vals['revid'] = intval( $row->rev_id ); 00352 // $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed? 00353 00354 if ( !is_null( $row->rev_parent_id ) ) { 00355 $vals['parentid'] = intval( $row->rev_parent_id ); 00356 } 00357 } 00358 00359 $title = Title::makeTitle( $row->page_namespace, $row->page_title ); 00360 00361 if ( $this->fld_title ) { 00362 ApiQueryBase::addTitleInfo( $vals, $title ); 00363 } 00364 00365 if ( $this->fld_timestamp ) { 00366 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); 00367 } 00368 00369 if ( $this->fld_flags ) { 00370 if ( $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id ) ) { 00371 $vals['new'] = ''; 00372 } 00373 if ( $row->rev_minor_edit ) { 00374 $vals['minor'] = ''; 00375 } 00376 if ( $row->page_latest == $row->rev_id ) { 00377 $vals['top'] = ''; 00378 } 00379 } 00380 00381 if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) { 00382 if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { 00383 $vals['commenthidden'] = ''; 00384 $anyHidden = true; 00385 } 00386 00387 $userCanView = Revision::userCanBitfield( 00388 $row->rev_deleted, 00389 Revision::DELETED_COMMENT, $this->getUser() 00390 ); 00391 00392 if ( $userCanView ) { 00393 if ( $this->fld_comment ) { 00394 $vals['comment'] = $row->rev_comment; 00395 } 00396 00397 if ( $this->fld_parsedcomment ) { 00398 $vals['parsedcomment'] = Linker::formatComment( $row->rev_comment, $title ); 00399 } 00400 } 00401 } 00402 00403 if ( $this->fld_patrolled && $row->rc_patrolled ) { 00404 $vals['patrolled'] = ''; 00405 } 00406 00407 if ( $this->fld_size && !is_null( $row->rev_len ) ) { 00408 $vals['size'] = intval( $row->rev_len ); 00409 } 00410 00411 if ( $this->fld_sizediff 00412 && !is_null( $row->rev_len ) 00413 && !is_null( $row->rev_parent_id ) 00414 ) { 00415 $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) 00416 ? $this->parentLens[$row->rev_parent_id] 00417 : 0; 00418 $vals['sizediff'] = intval( $row->rev_len - $parentLen ); 00419 } 00420 00421 if ( $this->fld_tags ) { 00422 if ( $row->ts_tags ) { 00423 $tags = explode( ',', $row->ts_tags ); 00424 $this->getResult()->setIndexedTagName( $tags, 'tag' ); 00425 $vals['tags'] = $tags; 00426 } else { 00427 $vals['tags'] = array(); 00428 } 00429 } 00430 00431 if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) { 00432 $vals['suppressed'] = ''; 00433 } 00434 00435 return $vals; 00436 } 00437 00438 private function continueStr( $row ) { 00439 if ( $this->multiUserMode ) { 00440 return "$row->rev_user_text|$row->rev_timestamp|$row->rev_id"; 00441 } else { 00442 return "$row->rev_timestamp|$row->rev_id"; 00443 } 00444 } 00445 00446 public function getCacheMode( $params ) { 00447 // This module provides access to deleted revisions and patrol flags if 00448 // the requester is logged in 00449 return 'anon-public-user-private'; 00450 } 00451 00452 public function getAllowedParams() { 00453 return array( 00454 'limit' => array( 00455 ApiBase::PARAM_DFLT => 10, 00456 ApiBase::PARAM_TYPE => 'limit', 00457 ApiBase::PARAM_MIN => 1, 00458 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, 00459 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 00460 ), 00461 'start' => array( 00462 ApiBase::PARAM_TYPE => 'timestamp' 00463 ), 00464 'end' => array( 00465 ApiBase::PARAM_TYPE => 'timestamp' 00466 ), 00467 'continue' => null, 00468 'user' => array( 00469 ApiBase::PARAM_ISMULTI => true 00470 ), 00471 'userprefix' => null, 00472 'dir' => array( 00473 ApiBase::PARAM_DFLT => 'older', 00474 ApiBase::PARAM_TYPE => array( 00475 'newer', 00476 'older' 00477 ) 00478 ), 00479 'namespace' => array( 00480 ApiBase::PARAM_ISMULTI => true, 00481 ApiBase::PARAM_TYPE => 'namespace' 00482 ), 00483 'prop' => array( 00484 ApiBase::PARAM_ISMULTI => true, 00485 ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags', 00486 ApiBase::PARAM_TYPE => array( 00487 'ids', 00488 'title', 00489 'timestamp', 00490 'comment', 00491 'parsedcomment', 00492 'size', 00493 'sizediff', 00494 'flags', 00495 'patrolled', 00496 'tags' 00497 ) 00498 ), 00499 'show' => array( 00500 ApiBase::PARAM_ISMULTI => true, 00501 ApiBase::PARAM_TYPE => array( 00502 'minor', 00503 '!minor', 00504 'patrolled', 00505 '!patrolled', 00506 'top', 00507 '!top', 00508 'new', 00509 '!new', 00510 ) 00511 ), 00512 'tag' => null, 00513 'toponly' => array( 00514 ApiBase::PARAM_DFLT => false, 00515 ApiBase::PARAM_DEPRECATED => true, 00516 ), 00517 ); 00518 } 00519 00520 public function getParamDescription() { 00521 $p = $this->getModulePrefix(); 00522 $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); 00523 00524 return array( 00525 'limit' => 'The maximum number of contributions to return', 00526 'start' => 'The start timestamp to return from', 00527 'end' => 'The end timestamp to return to', 00528 'continue' => 'When more results are available, use this to continue', 00529 'user' => 'The users to retrieve contributions for', 00530 'userprefix' => array( 00531 "Retrieve contributions for all users whose names begin with this value.", 00532 "Overrides {$p}user", 00533 ), 00534 'dir' => $this->getDirectionDescription( $p ), 00535 'namespace' => 'Only list contributions in these namespaces', 00536 'prop' => array( 00537 'Include additional pieces of information', 00538 ' ids - Adds the page ID and revision ID', 00539 ' title - Adds the title and namespace ID of the page', 00540 ' timestamp - Adds the timestamp of the edit', 00541 ' comment - Adds the comment of the edit', 00542 ' parsedcomment - Adds the parsed comment of the edit', 00543 ' size - Adds the new size of the edit', 00544 ' sizediff - Adds the size delta of the edit against its parent', 00545 ' flags - Adds flags of the edit', 00546 ' patrolled - Tags patrolled edits', 00547 ' tags - Lists tags for the edit', 00548 ), 00549 'show' => array( 00550 "Show only items that meet thse criteria, e.g. non minor edits only: {$p}show=!minor", 00551 "NOTE: If {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than", 00552 "\$wgRCMaxAge ($RCMaxAge) won't be shown", 00553 ), 00554 'tag' => 'Only list revisions tagged with this tag', 00555 'toponly' => 'Only list changes which are the latest revision', 00556 ); 00557 } 00558 00559 public function getDescription() { 00560 return 'Get all edits by a user.'; 00561 } 00562 00563 public function getExamples() { 00564 return array( 00565 'api.php?action=query&list=usercontribs&ucuser=YurikBot', 00566 'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.', 00567 ); 00568 } 00569 00570 public function getHelpUrls() { 00571 return 'https://www.mediawiki.org/wiki/API:Usercontribs'; 00572 } 00573 }