MediaWiki  master
PageHistoryHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest\Handler;
4 
5 use GuzzleHttp\Psr7\Uri;
16 use RequestContext;
17 use Title;
18 use User;
25 
31  const REVERTED_TAG_NAMES = [ 'mw-undo', 'mw-rollback' ];
32  const ALLOWED_FILTER_TYPES = [ 'anonymous', 'bot', 'reverted', 'minor' ];
33 
35  private $revisionStore;
36 
39 
42 
44  private $loadBalancer;
45 
47  private $user;
48 
55  public function __construct(
57  NameTableStoreFactory $nameTableStoreFactory,
60  ) {
61  $this->revisionStore = $revisionStore;
62  $this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
63  $this->permissionManager = $permissionManager;
64  $this->loadBalancer = $loadBalancer;
65 
66  // @todo Inject this, when there is a good way to do that
67  $this->user = RequestContext::getMain()->getUser();
68  }
69 
79  public function run( $title ) {
80  $params = $this->getValidatedParams();
81  if ( $params['older_than'] !== null && $params['newer_than'] !== null ) {
82  throw new LocalizedHttpException(
83  new MessageValue( 'rest-pagehistory-incompatible-params' ), 400 );
84  }
85 
86  if ( ( $params['older_than'] !== null && $params['older_than'] < 1 ) ||
87  ( $params['newer_than'] !== null && $params['newer_than'] < 1 )
88  ) {
89  throw new LocalizedHttpException(
90  new MessageValue( 'rest-pagehistory-param-range-error' ), 400 );
91  }
92 
93  $tagIds = [];
94  if ( $params['filter'] === 'reverted' ) {
95  foreach ( self::REVERTED_TAG_NAMES as $tagName ) {
96  try {
97  $tagIds[] = $this->changeTagDefStore->getId( $tagName );
98  } catch ( NameTableAccessException $exception ) {
99  // If no revisions are tagged with a name, no tag id will be present
100  }
101  }
102  }
103 
104  $titleObj = Title::newFromText( $title );
105  if ( !$titleObj || !$titleObj->getArticleID() ) {
106  throw new LocalizedHttpException(
107  new MessageValue( 'rest-nonexistent-title',
108  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
109  ),
110  404
111  );
112  }
113  if ( !$this->permissionManager->userCan( 'read', $this->user, $titleObj ) ) {
114  throw new LocalizedHttpException(
115  new MessageValue( 'rest-permission-denied-title',
116  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ] ),
117  403
118  );
119  }
120 
121  $relativeRevId = $params['older_than'] ?? $params['newer_than'] ?? 0;
122  if ( $relativeRevId ) {
123  // Confirm the relative revision exists for this page. If so, get its timestamp.
124  $rev = $this->revisionStore->getRevisionByPageId(
125  $titleObj->getArticleID(),
126  $relativeRevId
127  );
128  if ( !$rev ) {
129  throw new LocalizedHttpException(
130  new MessageValue( 'rest-nonexistent-title-revision',
131  [ $relativeRevId, new ScalarParam( ParamType::PLAINTEXT, $title ) ]
132  ),
133  404
134  );
135  }
136  $ts = $rev->getTimestamp();
137  if ( $ts === null ) {
138  throw new LocalizedHttpException(
139  new MessageValue( 'rest-pagehistory-timestamp-error',
140  [ $relativeRevId ]
141  ),
142  500
143  );
144  }
145  } else {
146  $ts = 0;
147  }
148 
149  $res = $this->getDbResults( $titleObj, $params, $relativeRevId, $ts, $tagIds );
150  $response = $this->processDbResults( $res, $titleObj, $params );
151  return $this->getResponseFactory()->createJson( $response );
152  }
153 
162  private function getDbResults( Title $titleObj, array $params, $relativeRevId, $ts, $tagIds ) {
163  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
164  $revQuery = $this->revisionStore->getQueryInfo();
165  $cond = [
166  'rev_page' => $titleObj->getArticleID()
167  ];
168 
169  if ( $params['filter'] ) {
170  // This redundant join condition tells MySQL that rev_page and revactor_page are the
171  // same, so it can propagate the condition
172  $revQuery['joins']['temp_rev_user'][1] =
173  "temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
174 
175  // The validator ensures this value, if present, is one of the expected values
176  switch ( $params['filter'] ) {
177  case 'bot':
178  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
179  'user_groups',
180  '1',
181  [
182  'actor_rev_user.actor_user = ug_user',
183  'ug_group' => $this->permissionManager->getGroupsWithPermission( 'bot' ),
184  'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
185  ],
186  __METHOD__
187  ) . ')';
188  $bitmask = $this->getBitmask();
189  if ( $bitmask ) {
190  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
191  }
192  break;
193 
194  case 'anonymous':
195  $cond[] = "actor_user IS NULL";
196  $bitmask = $this->getBitmask();
197  if ( $bitmask ) {
198  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
199  }
200  break;
201 
202  case 'reverted':
203  if ( !$tagIds ) {
204  return false;
205  }
206  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
207  'change_tag',
208  '1',
209  [ 'ct_rev_id = rev_id', 'ct_tag_id' => $tagIds ],
210  __METHOD__
211  ) . ')';
212  break;
213 
214  case 'minor':
215  $cond[] = 'rev_minor_edit != 0';
216  break;
217  }
218  }
219 
220  if ( $relativeRevId ) {
221  $op = $params['older_than'] ? '<' : '>';
222  $sort = $params['older_than'] ? 'DESC' : 'ASC';
223  $ts = $dbr->addQuotes( $dbr->timestamp( $ts ) );
224  $cond[] = "rev_timestamp $op $ts OR " .
225  "(rev_timestamp = $ts AND rev_id $op $relativeRevId)";
226  $orderBy = "rev_timestamp $sort, rev_id $sort";
227  } else {
228  $orderBy = "rev_timestamp DESC, rev_id DESC";
229  }
230 
231  // Select one more than the return limit, to learn if there are additional revisions.
232  $limit = self::REVISIONS_RETURN_LIMIT + 1;
233 
234  $res = $dbr->select(
235  $revQuery['tables'],
236  $revQuery['fields'],
237  $cond,
238  __METHOD__,
239  [
240  'ORDER BY' => $orderBy,
241  'LIMIT' => $limit,
242  ],
243  $revQuery['joins']
244  );
245 
246  return $res;
247  }
248 
256  private function getBitmask() {
257  if ( !$this->permissionManager->userHasRight( $this->user, 'deletedhistory' ) ) {
258  $bitmask = RevisionRecord::DELETED_USER;
259  } elseif ( !$this->permissionManager
260  ->userHasAnyRight( $this->user, 'suppressrevision', 'viewsuppressed' )
261  ) {
262  $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
263  } else {
264  $bitmask = 0;
265  }
266  return $bitmask;
267  }
268 
275  private function processDbResults( $res, $titleObj, $params ) {
276  $revisions = [];
277 
278  if ( $res ) {
279  $sizes = [];
280  foreach ( $res as $row ) {
281  $rev = $this->revisionStore->newRevisionFromRow(
282  $row,
283  IDBAccessObject::READ_NORMAL,
284  $titleObj
285  );
286  if ( !$revisions ) {
287  $firstRevId = $row->rev_id;
288  }
289  $lastRevId = $row->rev_id;
290 
291  $revision = [
292  'id' => $rev->getId(),
293  'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ),
294  'minor' => $rev->isMinor(),
295  'size' => $rev->getSize()
296  ];
297 
298  // Remember revision sizes and parent ids for calculating deltas. If a revision's
299  // parent id is unknown, we will be unable to supply the delta for that revision.
300  $sizes[$rev->getId()] = $rev->getSize();
301  $parentId = $rev->getParentId();
302  if ( $parentId ) {
303  $revision['parent_id'] = $parentId;
304  }
305 
306  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->user );
307  $revision['comment'] = $comment ? $comment->text : null;
308 
309  $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->user );
310  if ( $revUser ) {
311  $revision['user'] = [
312  'id' => $revUser->isRegistered() ? $revUser->getId() : null,
313  'name' => $revUser->getName()
314  ];
315  } else {
316  $revision['user'] = null;
317  }
318 
319  $revisions[] = $revision;
320 
321  // Break manually at the return limit. We may have more results than we can return.
322  if ( count( $revisions ) == self::REVISIONS_RETURN_LIMIT ) {
323  break;
324  }
325  }
326 
327  // Request any parent sizes that we do not already know, then calculate deltas
328  $unknownSizes = [];
329  foreach ( $revisions as $revision ) {
330  if ( isset( $revision['parent_id'] ) && !isset( $sizes[$revision['parent_id']] ) ) {
331  $unknownSizes[] = $revision['parent_id'];
332  }
333  }
334  if ( $unknownSizes ) {
335  $sizes += $this->revisionStore->getRevisionSizes( $unknownSizes );
336  }
337  foreach ( $revisions as &$revision ) {
338  $revision['delta'] = null;
339  if ( isset( $revision['parent_id'] ) ) {
340  if ( isset( $sizes[$revision['parent_id']] ) ) {
341  $revision['delta'] = $revision['size'] - $sizes[$revision['parent_id']];
342  }
343 
344  // We only remembered this for delta calculations. We do not want to return it.
345  unset( $revision['parent_id'] );
346  }
347  }
348 
349  if ( $revisions && $params['newer_than'] ) {
350  $revisions = array_reverse( $revisions );
351  $temp = $lastRevId;
352  $lastRevId = $firstRevId;
353  $firstRevId = $temp;
354  }
355  }
356 
357  $response = [
358  'revisions' => $revisions
359  ];
360 
361  // Omit newer/older if there are no additional corresponding revisions.
362  // This facilitates clients doing "paging" style api operations.
363  if ( $revisions ) {
364  if ( $params['newer_than'] || $res->numRows() > self::REVISIONS_RETURN_LIMIT ) {
365  $older = $lastRevId;
366  }
367  if ( $params['older_than'] ||
368  ( $params['newer_than'] && $res->numRows() > self::REVISIONS_RETURN_LIMIT )
369  ) {
370  $newer = $firstRevId;
371  }
372  }
373 
374  $wr = new \WebRequest();
375  $urlParts = wfParseUrl( $wr->getFullRequestURL() );
376  if ( $urlParts ) {
377  $queryParts = wfCgiToArray( $urlParts['query'] );
378  unset( $urlParts['query'] );
379  unset( $queryParts['older_than'] );
380  unset( $queryParts['newer_than'] );
381 
382  $uri = Uri::fromParts( $urlParts );
383  $response['latest'] = Uri::withQueryValues( $uri, $queryParts )->__toString();
384  if ( isset( $older ) ) {
385  $response['older'] = Uri::withQueryValues(
386  $uri,
387  $queryParts + [ 'older_than' => $older ]
388  )->__toString();
389  }
390  if ( isset( $newer ) ) {
391  $response['newer'] = Uri::withQueryValues(
392  $uri,
393  $queryParts + [ 'newer_than' => $newer ]
394  )->__toString();
395  }
396  }
397 
398  return $response;
399  }
400 
401  public function needsWriteAccess() {
402  return false;
403  }
404 
405  public function getParamSettings() {
406  return [
407  'title' => [
408  self::PARAM_SOURCE => 'path',
409  ParamValidator::PARAM_TYPE => 'string',
410  ParamValidator::PARAM_REQUIRED => true,
411  ],
412  'older_than' => [
413  self::PARAM_SOURCE => 'query',
414  ParamValidator::PARAM_TYPE => 'integer',
415  ParamValidator::PARAM_REQUIRED => false,
416  ],
417  'newer_than' => [
418  self::PARAM_SOURCE => 'query',
419  ParamValidator::PARAM_TYPE => 'integer',
420  ParamValidator::PARAM_REQUIRED => false,
421  ],
422  'filter' => [
423  self::PARAM_SOURCE => 'query',
424  ParamValidator::PARAM_TYPE => self::ALLOWED_FILTER_TYPES,
425  ParamValidator::PARAM_REQUIRED => false,
426  ],
427  ];
428  }
429 }
MediaWiki\Rest\Handler\PageHistoryHandler\__construct
__construct(RevisionStore $revisionStore, NameTableStoreFactory $nameTableStoreFactory, PermissionManager $permissionManager, ILoadBalancer $loadBalancer)
Definition: PageHistoryHandler.php:55
MediaWiki\Rest\Handler
Definition: CompareHandler.php:3
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:317
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
MediaWiki\Rest\Handler\PageHistoryHandler
Handler class for Core REST API endpoints that perform operations on revisions.
Definition: PageHistoryHandler.php:29
MediaWiki\Rest\Handler\getResponseFactory
getResponseFactory()
Get the ResponseFactory which can be used to generate Response objects.
Definition: Handler.php:92
$response
$response
Definition: opensearch_desc.php:38
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:79
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1871
MediaWiki\Rest\Handler\PageHistoryHandler\processDbResults
processDbResults( $res, $titleObj, $params)
Definition: PageHistoryHandler.php:275
Title\getArticleID
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3161
MediaWiki\Rest\Handler\PageHistoryHandler\REVISIONS_RETURN_LIMIT
const REVISIONS_RETURN_LIMIT
Definition: PageHistoryHandler.php:30
MediaWiki\Rest\Handler\PageHistoryHandler\run
run( $title)
At most one of older_than and newer_than may be specified.
Definition: PageHistoryHandler.php:79
Wikimedia\Message\ScalarParam
Value object representing a message parameter holding a single value.
Definition: ScalarParam.php:10
$res
$res
Definition: testCompression.php:54
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:55
MediaWiki\Rest\Handler\PageHistoryHandler\$permissionManager
PermissionManager $permissionManager
Definition: PageHistoryHandler.php:41
$revQuery
$revQuery
Definition: testCompression.php:53
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:14
MediaWiki\Rest\Handler\PageHistoryHandler\getBitmask
getBitmask()
Helper function for rev_deleted/user rights query conditions.
Definition: PageHistoryHandler.php:256
$dbr
$dbr
Definition: testCompression.php:52
MediaWiki\Rest\Handler\PageHistoryHandler\REVERTED_TAG_NAMES
const REVERTED_TAG_NAMES
Definition: PageHistoryHandler.php:31
MediaWiki\Rest\Handler\PageHistoryHandler\$user
User $user
Definition: PageHistoryHandler.php:47
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:793
MediaWiki\Rest\Handler\PageHistoryHandler\getDbResults
getDbResults(Title $titleObj, array $params, $relativeRevId, $ts, $tagIds)
Definition: PageHistoryHandler.php:162
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
MediaWiki\Rest\Handler\PageHistoryHandler\$changeTagDefStore
NameTableStore $changeTagDefStore
Definition: PageHistoryHandler.php:38
wfCgiToArray
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Definition: GlobalFunctions.php:392
MediaWiki\Rest\Response
Definition: Response.php:8
$title
$title
Definition: testCompression.php:36
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
RequestContext
Group all the pieces relevant to the context of a request into one instance.
Definition: RequestContext.php:34
MediaWiki\Rest\Handler\getValidatedParams
getValidatedParams()
Fetch the validated parameters.
Definition: Handler.php:188
MediaWiki\Rest\Handler\PageHistoryHandler\$revisionStore
RevisionStore $revisionStore
Definition: PageHistoryHandler.php:35
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:48
MediaWiki\Rest\Handler\PageHistoryHandler\getParamSettings
getParamSettings()
Fetch ParamValidator settings for parameters.
Definition: PageHistoryHandler.php:405
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:447
MediaWiki\Storage\NameTableStoreFactory\getChangeTagDef
getChangeTagDef( $wiki=false)
Get a NameTableStore for the change_tag_def table.
Definition: NameTableStoreFactory.php:127
Title
Represents a title within MediaWiki.
Definition: Title.php:42
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
MediaWiki\Rest\Handler\PageHistoryHandler\$loadBalancer
ILoadBalancer $loadBalancer
Definition: PageHistoryHandler.php:44
MediaWiki\Storage\NameTableStoreFactory
Definition: NameTableStoreFactory.php:26
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:52
MediaWiki\Rest\Handler\PageHistoryHandler\ALLOWED_FILTER_TYPES
const ALLOWED_FILTER_TYPES
Definition: PageHistoryHandler.php:32
Wikimedia\ParamValidator\ParamValidator
Service for formatting and validating API parameters.
Definition: ParamValidator.php:42
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Rest\LocalizedHttpException
Definition: LocalizedHttpException.php:7
MediaWiki\Rest\Handler\PageHistoryHandler\needsWriteAccess
needsWriteAccess()
Indicates whether this route requires write access.
Definition: PageHistoryHandler.php:401
Wikimedia\Message\ParamType
The constants used to specify parameter types.
Definition: ParamType.php:11
MediaWiki\Rest\SimpleHandler
Definition: SimpleHandler.php:14