MediaWiki  master
PageHistoryHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest\Handler;
4 
5 use ChangeTags;
18 use TitleFormatter;
25 
30  private const REVISIONS_RETURN_LIMIT = 20;
31  private const ALLOWED_FILTER_TYPES = [ 'anonymous', 'bot', 'reverted', 'minor' ];
32 
34  private $revisionStore;
35 
38 
41 
43  private $loadBalancer;
44 
46  private $pageLookup;
47 
49  private $titleFormatter;
50 
54  private $page = false;
55 
66  public function __construct(
68  NameTableStoreFactory $nameTableStoreFactory,
73  ) {
74  $this->revisionStore = $revisionStore;
75  $this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
76  $this->groupPermissionsLookup = $groupPermissionsLookup;
77  $this->loadBalancer = $loadBalancer;
78  $this->pageLookup = $pageLookup;
79  $this->titleFormatter = $titleFormatter;
80  }
81 
85  private function getPage(): ?ExistingPageRecord {
86  if ( $this->page === false ) {
87  $this->page = $this->pageLookup->getExistingPageByText(
88  $this->getValidatedParams()['title']
89  );
90  }
91  return $this->page;
92  }
93 
103  public function run( $title ) {
104  $params = $this->getValidatedParams();
105  if ( $params['older_than'] !== null && $params['newer_than'] !== null ) {
106  throw new LocalizedHttpException(
107  new MessageValue( 'rest-pagehistory-incompatible-params' ), 400 );
108  }
109 
110  if ( ( $params['older_than'] !== null && $params['older_than'] < 1 ) ||
111  ( $params['newer_than'] !== null && $params['newer_than'] < 1 )
112  ) {
113  throw new LocalizedHttpException(
114  new MessageValue( 'rest-pagehistory-param-range-error' ), 400 );
115  }
116 
117  $tagIds = [];
118  if ( $params['filter'] === 'reverted' ) {
119  foreach ( ChangeTags::REVERT_TAGS as $tagName ) {
120  try {
121  $tagIds[] = $this->changeTagDefStore->getId( $tagName );
122  } catch ( NameTableAccessException $exception ) {
123  // If no revisions are tagged with a name, no tag id will be present
124  }
125  }
126  }
127 
128  $page = $this->getPage();
129  if ( !$page ) {
130  throw new LocalizedHttpException(
131  new MessageValue( 'rest-nonexistent-title',
132  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
133  ),
134  404
135  );
136  }
137  if ( !$this->getAuthority()->authorizeRead( 'read', $page ) ) {
138  throw new LocalizedHttpException(
139  new MessageValue( 'rest-permission-denied-title',
140  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ] ),
141  403
142  );
143  }
144 
145  $relativeRevId = $params['older_than'] ?? $params['newer_than'] ?? 0;
146  if ( $relativeRevId ) {
147  // Confirm the relative revision exists for this page. If so, get its timestamp.
148  $rev = $this->revisionStore->getRevisionByPageId(
149  $page->getId(),
150  $relativeRevId
151  );
152  if ( !$rev ) {
153  throw new LocalizedHttpException(
154  new MessageValue( 'rest-nonexistent-title-revision',
155  [ $relativeRevId, new ScalarParam( ParamType::PLAINTEXT, $title ) ]
156  ),
157  404
158  );
159  }
160  $ts = $rev->getTimestamp();
161  if ( $ts === null ) {
162  throw new LocalizedHttpException(
163  new MessageValue( 'rest-pagehistory-timestamp-error',
164  [ $relativeRevId ]
165  ),
166  500
167  );
168  }
169  } else {
170  $ts = 0;
171  }
172 
173  $res = $this->getDbResults( $page, $params, $relativeRevId, $ts, $tagIds );
174  $response = $this->processDbResults( $res, $page, $params );
175  return $this->getResponseFactory()->createJson( $response );
176  }
177 
186  private function getDbResults( ExistingPageRecord $page, array $params, $relativeRevId, $ts, $tagIds ) {
187  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
188  $revQuery = $this->revisionStore->getQueryInfo();
189  $cond = [
190  'rev_page' => $page->getId()
191  ];
192 
193  if ( $params['filter'] ) {
194  // This redundant join condition tells MySQL that rev_page and revactor_page are the
195  // same, so it can propagate the condition
196  $revQuery['joins']['temp_rev_user'][1] =
197  "temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
198 
199  // The validator ensures this value, if present, is one of the expected values
200  switch ( $params['filter'] ) {
201  case 'bot':
202  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
203  'user_groups',
204  '1',
205  [
206  'actor_rev_user.actor_user = ug_user',
207  'ug_group' => $this->groupPermissionsLookup->getGroupsWithPermission( 'bot' ),
208  'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
209  ],
210  __METHOD__
211  ) . ')';
212  $bitmask = $this->getBitmask();
213  if ( $bitmask ) {
214  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
215  }
216  break;
217 
218  case 'anonymous':
219  $cond[] = "actor_user IS NULL";
220  $bitmask = $this->getBitmask();
221  if ( $bitmask ) {
222  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
223  }
224  break;
225 
226  case 'reverted':
227  if ( !$tagIds ) {
228  return false;
229  }
230  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
231  'change_tag',
232  '1',
233  [ 'ct_rev_id = rev_id', 'ct_tag_id' => $tagIds ],
234  __METHOD__
235  ) . ')';
236  break;
237 
238  case 'minor':
239  $cond[] = 'rev_minor_edit != 0';
240  break;
241  }
242  }
243 
244  if ( $relativeRevId ) {
245  $op = $params['older_than'] ? '<' : '>';
246  $sort = $params['older_than'] ? 'DESC' : 'ASC';
247  $ts = $dbr->addQuotes( $dbr->timestamp( $ts ) );
248  $cond[] = "rev_timestamp $op $ts OR " .
249  "(rev_timestamp = $ts AND rev_id $op $relativeRevId)";
250  $orderBy = "rev_timestamp $sort, rev_id $sort";
251  } else {
252  $orderBy = "rev_timestamp DESC, rev_id DESC";
253  }
254 
255  // Select one more than the return limit, to learn if there are additional revisions.
256  $limit = self::REVISIONS_RETURN_LIMIT + 1;
257 
258  $res = $dbr->select(
259  $revQuery['tables'],
260  $revQuery['fields'],
261  $cond,
262  __METHOD__,
263  [
264  'ORDER BY' => $orderBy,
265  'LIMIT' => $limit,
266  ],
267  $revQuery['joins']
268  );
269 
270  return $res;
271  }
272 
280  private function getBitmask() {
281  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
282  $bitmask = RevisionRecord::DELETED_USER;
283  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
285  } else {
286  $bitmask = 0;
287  }
288  return $bitmask;
289  }
290 
297  private function processDbResults( $res, $page, $params ) {
298  $revisions = [];
299 
300  if ( $res ) {
301  $sizes = [];
302  foreach ( $res as $row ) {
303  $rev = $this->revisionStore->newRevisionFromRow(
304  $row,
305  IDBAccessObject::READ_NORMAL,
306  $page
307  );
308  if ( !$revisions ) {
309  $firstRevId = $row->rev_id;
310  }
311  $lastRevId = $row->rev_id;
312 
313  $revision = [
314  'id' => $rev->getId(),
315  'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ),
316  'minor' => $rev->isMinor(),
317  'size' => $rev->getSize()
318  ];
319 
320  // Remember revision sizes and parent ids for calculating deltas. If a revision's
321  // parent id is unknown, we will be unable to supply the delta for that revision.
322  $sizes[$rev->getId()] = $rev->getSize();
323  $parentId = $rev->getParentId();
324  if ( $parentId ) {
325  $revision['parent_id'] = $parentId;
326  }
327 
328  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
329  $revision['comment'] = $comment ? $comment->text : null;
330 
331  $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
332  if ( $revUser ) {
333  $revision['user'] = [
334  'id' => $revUser->isRegistered() ? $revUser->getId() : null,
335  'name' => $revUser->getName()
336  ];
337  } else {
338  $revision['user'] = null;
339  }
340 
341  $revisions[] = $revision;
342 
343  // Break manually at the return limit. We may have more results than we can return.
344  if ( count( $revisions ) == self::REVISIONS_RETURN_LIMIT ) {
345  break;
346  }
347  }
348 
349  // Request any parent sizes that we do not already know, then calculate deltas
350  $unknownSizes = [];
351  foreach ( $revisions as $revision ) {
352  if ( isset( $revision['parent_id'] ) && !isset( $sizes[$revision['parent_id']] ) ) {
353  $unknownSizes[] = $revision['parent_id'];
354  }
355  }
356  if ( $unknownSizes ) {
357  $sizes += $this->revisionStore->getRevisionSizes( $unknownSizes );
358  }
359  foreach ( $revisions as &$revision ) {
360  $revision['delta'] = null;
361  if ( isset( $revision['parent_id'] ) ) {
362  if ( isset( $sizes[$revision['parent_id']] ) ) {
363  $revision['delta'] = $revision['size'] - $sizes[$revision['parent_id']];
364  }
365 
366  // We only remembered this for delta calculations. We do not want to return it.
367  unset( $revision['parent_id'] );
368  }
369  }
370 
371  if ( $revisions && $params['newer_than'] ) {
372  $revisions = array_reverse( $revisions );
373  $temp = $lastRevId;
374  $lastRevId = $firstRevId;
375  $firstRevId = $temp;
376  }
377  }
378 
379  $response = [
380  'revisions' => $revisions
381  ];
382 
383  // Omit newer/older if there are no additional corresponding revisions.
384  // This facilitates clients doing "paging" style api operations.
385  if ( $revisions ) {
386  if ( $params['newer_than'] || $res->numRows() > self::REVISIONS_RETURN_LIMIT ) {
387  $older = $lastRevId;
388  }
389  if ( $params['older_than'] ||
390  ( $params['newer_than'] && $res->numRows() > self::REVISIONS_RETURN_LIMIT )
391  ) {
392  $newer = $firstRevId;
393  }
394  }
395 
396  $queryParts = [];
397 
398  if ( isset( $params['filter'] ) ) {
399  $queryParts['filter'] = $params['filter'];
400  }
401 
402  $pathParams = [ 'title' => $this->titleFormatter->getPrefixedDBkey( $page ) ];
403 
404  $response['latest'] = $this->getRouteUrl( $pathParams, $queryParts );
405 
406  if ( isset( $older ) ) {
407  $response['older'] =
408  $this->getRouteUrl( $pathParams, $queryParts + [ 'older_than' => $older ] );
409  }
410  if ( isset( $newer ) ) {
411  $response['newer'] =
412  $this->getRouteUrl( $pathParams, $queryParts + [ 'newer_than' => $newer ] );
413  }
414 
415  return $response;
416  }
417 
418  public function needsWriteAccess() {
419  return false;
420  }
421 
422  public function getParamSettings() {
423  return [
424  'title' => [
425  self::PARAM_SOURCE => 'path',
426  ParamValidator::PARAM_TYPE => 'string',
427  ParamValidator::PARAM_REQUIRED => true,
428  ],
429  'older_than' => [
430  self::PARAM_SOURCE => 'query',
431  ParamValidator::PARAM_TYPE => 'integer',
432  ParamValidator::PARAM_REQUIRED => false,
433  ],
434  'newer_than' => [
435  self::PARAM_SOURCE => 'query',
436  ParamValidator::PARAM_TYPE => 'integer',
437  ParamValidator::PARAM_REQUIRED => false,
438  ],
439  'filter' => [
440  self::PARAM_SOURCE => 'query',
441  ParamValidator::PARAM_TYPE => self::ALLOWED_FILTER_TYPES,
442  ParamValidator::PARAM_REQUIRED => false,
443  ],
444  ];
445  }
446 
452  protected function getETag(): ?string {
453  $page = $this->getPage();
454  if ( !$page ) {
455  return null;
456  }
457 
458  return '"' . $page->getLatest() . '"';
459  }
460 
466  protected function getLastModified(): ?string {
467  $page = $this->getPage();
468  if ( !$page ) {
469  return null;
470  }
471 
472  $rev = $this->revisionStore->getKnownCurrentRevision( $page );
473  return $rev->getTimestamp();
474  }
475 
479  protected function hasRepresentation() {
480  return (bool)$this->getPage();
481  }
482 }
ChangeTags\REVERT_TAGS
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:102
MediaWiki\Rest\Handler
Definition: AbstractContributionHandler.php:3
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
MediaWiki\Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:55
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:170
MediaWiki\Rest\Handler\PageHistoryHandler\getLastModified
getLastModified()
Returns the time of the last change to the page.
Definition: PageHistoryHandler.php:466
Page\PageRecord\getLatest
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
MediaWiki\Permissions\GroupPermissionsLookup
Definition: GroupPermissionsLookup.php:30
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1692
MediaWiki\Rest\Handler\PageHistoryHandler\getPage
getPage()
Definition: PageHistoryHandler.php:85
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:103
Wikimedia\Message\ScalarParam
Value object representing a message parameter holding a single value.
Definition: ScalarParam.php:12
$res
$res
Definition: testCompression.php:57
IDBAccessObject
Interface for database access objects.
Definition: IDBAccessObject.php:57
MediaWiki\Rest\Handler\PageHistoryHandler\__construct
__construct(RevisionStore $revisionStore, NameTableStoreFactory $nameTableStoreFactory, GroupPermissionsLookup $groupPermissionsLookup, ILoadBalancer $loadBalancer, PageLookup $pageLookup, TitleFormatter $titleFormatter)
RevisionStore $revisionStore.
Definition: PageHistoryHandler.php:66
$revQuery
$revQuery
Definition: testCompression.php:56
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:16
MediaWiki\Rest\Handler\PageHistoryHandler\getBitmask
getBitmask()
Helper function for rev_deleted/user rights query conditions.
Definition: PageHistoryHandler.php:280
$dbr
$dbr
Definition: testCompression.php:54
Page\ProperPageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
MediaWiki\Rest\Handler\PageHistoryHandler\getETag
getETag()
Returns an ETag representing a page's latest revision.
Definition: PageHistoryHandler.php:452
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
ChangeTags
Definition: ChangeTags.php:32
MediaWiki\Rest\Handler\PageHistoryHandler\$pageLookup
PageLookup $pageLookup
Definition: PageHistoryHandler.php:46
MediaWiki\Rest\Handler\PageHistoryHandler\$changeTagDefStore
NameTableStore $changeTagDefStore
Definition: PageHistoryHandler.php:37
MediaWiki\Rest\Handler\PageHistoryHandler\$page
ExistingPageRecord false null $page
Definition: PageHistoryHandler.php:54
MediaWiki\Rest\Handler\getRouteUrl
getRouteUrl( $pathParams=[], $queryParams=[])
Get the URL of this handler's endpoint.
Definition: Handler.php:101
MediaWiki\Rest\Response
Definition: Response.php:8
$title
$title
Definition: testCompression.php:38
MediaWiki\Rest\Handler\PageHistoryHandler\$groupPermissionsLookup
GroupPermissionsLookup $groupPermissionsLookup
Definition: PageHistoryHandler.php:40
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
MediaWiki\Rest\Handler\getValidatedParams
getValidatedParams()
Fetch the validated parameters.
Definition: Handler.php:282
MediaWiki\Rest\Handler\PageHistoryHandler\$revisionStore
RevisionStore $revisionStore
Definition: PageHistoryHandler.php:34
Page\ExistingPageRecord
Data record representing a page that currently exists as an editable page on a wiki.
Definition: ExistingPageRecord.php:15
Page\PageLookup
Service interface for looking up infermation about wiki pages.
Definition: PageLookup.php:14
MediaWiki\Rest\Handler\PageHistoryHandler\getDbResults
getDbResults(ExistingPageRecord $page, array $params, $relativeRevId, $ts, $tagIds)
Definition: PageHistoryHandler.php:186
MediaWiki\Rest\Handler\PageHistoryHandler\getParamSettings
getParamSettings()
Fetch ParamValidator settings for parameters.
Definition: PageHistoryHandler.php:422
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
MediaWiki\Storage\NameTableStoreFactory\getChangeTagDef
getChangeTagDef( $wiki=false)
Get a NameTableStore for the change_tag_def table.
Definition: NameTableStoreFactory.php:127
TitleFormatter
A title formatter service for MediaWiki.
Definition: TitleFormatter.php:35
MediaWiki\Rest\Handler\PageHistoryHandler\processDbResults
processDbResults( $res, $page, $params)
Definition: PageHistoryHandler.php:297
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
MediaWiki\Rest\Handler\PageHistoryHandler\$loadBalancer
ILoadBalancer $loadBalancer
Definition: PageHistoryHandler.php:43
MediaWiki\Revision\RevisionRecord\FOR_THIS_USER
const FOR_THIS_USER
Definition: RevisionRecord.php:63
MediaWiki\Storage\NameTableStoreFactory
Definition: NameTableStoreFactory.php:26
MediaWiki\Rest\Handler\PageHistoryHandler\$titleFormatter
TitleFormatter $titleFormatter
Definition: PageHistoryHandler.php:49
MediaWiki\Revision\RevisionRecord\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: RevisionRecord.php:56
MediaWiki\Rest\Handler\PageHistoryHandler\ALLOWED_FILTER_TYPES
const ALLOWED_FILTER_TYPES
Definition: PageHistoryHandler.php:31
MediaWiki\Rest\Handler\getAuthority
getAuthority()
Get the current acting authority.
Definition: Handler.php:148
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
@newable
Definition: LocalizedHttpException.php:10
MediaWiki\Rest\Handler\PageHistoryHandler\needsWriteAccess
needsWriteAccess()
Indicates whether this route requires write access.
Definition: PageHistoryHandler.php:418
Wikimedia\Message\ParamType
The constants used to specify parameter types.
Definition: ParamType.php:11
MediaWiki\Rest\SimpleHandler
Definition: SimpleHandler.php:15
MediaWiki\Rest\Handler\PageHistoryHandler\hasRepresentation
hasRepresentation()
Definition: PageHistoryHandler.php:479