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  if ( isset( $revQuery['tables']['temp_rev_user'] ) /* SCHEMA_COMPAT_READ_TEMP */ ) {
197  $revQuery['joins']['temp_rev_user'][1] =
198  "temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
199  }
200 
201  // The validator ensures this value, if present, is one of the expected values
202  switch ( $params['filter'] ) {
203  case 'bot':
204  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
205  'user_groups',
206  '1',
207  [
208  'actor_rev_user.actor_user = ug_user',
209  'ug_group' => $this->groupPermissionsLookup->getGroupsWithPermission( 'bot' ),
210  'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
211  ],
212  __METHOD__
213  ) . ')';
214  $bitmask = $this->getBitmask();
215  if ( $bitmask ) {
216  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
217  }
218  break;
219 
220  case 'anonymous':
221  $cond[] = "actor_user IS NULL";
222  $bitmask = $this->getBitmask();
223  if ( $bitmask ) {
224  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
225  }
226  break;
227 
228  case 'reverted':
229  if ( !$tagIds ) {
230  return false;
231  }
232  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
233  'change_tag',
234  '1',
235  [ 'ct_rev_id = rev_id', 'ct_tag_id' => $tagIds ],
236  __METHOD__
237  ) . ')';
238  break;
239 
240  case 'minor':
241  $cond[] = 'rev_minor_edit != 0';
242  break;
243  }
244  }
245 
246  if ( $relativeRevId ) {
247  $op = $params['older_than'] ? '<' : '>';
248  $sort = $params['older_than'] ? 'DESC' : 'ASC';
249  $ts = $dbr->addQuotes( $dbr->timestamp( $ts ) );
250  $cond[] = "rev_timestamp $op $ts OR " .
251  "(rev_timestamp = $ts AND rev_id $op $relativeRevId)";
252  $orderBy = "rev_timestamp $sort, rev_id $sort";
253  } else {
254  $orderBy = "rev_timestamp DESC, rev_id DESC";
255  }
256 
257  // Select one more than the return limit, to learn if there are additional revisions.
258  $limit = self::REVISIONS_RETURN_LIMIT + 1;
259 
260  $res = $dbr->select(
261  $revQuery['tables'],
262  $revQuery['fields'],
263  $cond,
264  __METHOD__,
265  [
266  'ORDER BY' => $orderBy,
267  'LIMIT' => $limit,
268  ],
269  $revQuery['joins']
270  );
271 
272  return $res;
273  }
274 
282  private function getBitmask() {
283  if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
284  $bitmask = RevisionRecord::DELETED_USER;
285  } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
287  } else {
288  $bitmask = 0;
289  }
290  return $bitmask;
291  }
292 
299  private function processDbResults( $res, $page, $params ) {
300  $revisions = [];
301 
302  if ( $res ) {
303  $sizes = [];
304  foreach ( $res as $row ) {
305  $rev = $this->revisionStore->newRevisionFromRow(
306  $row,
307  IDBAccessObject::READ_NORMAL,
308  $page
309  );
310  if ( !$revisions ) {
311  $firstRevId = $row->rev_id;
312  }
313  $lastRevId = $row->rev_id;
314 
315  $revision = [
316  'id' => $rev->getId(),
317  'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ),
318  'minor' => $rev->isMinor(),
319  'size' => $rev->getSize()
320  ];
321 
322  // Remember revision sizes and parent ids for calculating deltas. If a revision's
323  // parent id is unknown, we will be unable to supply the delta for that revision.
324  $sizes[$rev->getId()] = $rev->getSize();
325  $parentId = $rev->getParentId();
326  if ( $parentId ) {
327  $revision['parent_id'] = $parentId;
328  }
329 
330  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
331  $revision['comment'] = $comment ? $comment->text : null;
332 
333  $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
334  if ( $revUser ) {
335  $revision['user'] = [
336  'id' => $revUser->isRegistered() ? $revUser->getId() : null,
337  'name' => $revUser->getName()
338  ];
339  } else {
340  $revision['user'] = null;
341  }
342 
343  $revisions[] = $revision;
344 
345  // Break manually at the return limit. We may have more results than we can return.
346  if ( count( $revisions ) == self::REVISIONS_RETURN_LIMIT ) {
347  break;
348  }
349  }
350 
351  // Request any parent sizes that we do not already know, then calculate deltas
352  $unknownSizes = [];
353  foreach ( $revisions as $revision ) {
354  if ( isset( $revision['parent_id'] ) && !isset( $sizes[$revision['parent_id']] ) ) {
355  $unknownSizes[] = $revision['parent_id'];
356  }
357  }
358  if ( $unknownSizes ) {
359  $sizes += $this->revisionStore->getRevisionSizes( $unknownSizes );
360  }
361  foreach ( $revisions as &$revision ) {
362  $revision['delta'] = null;
363  if ( isset( $revision['parent_id'] ) ) {
364  if ( isset( $sizes[$revision['parent_id']] ) ) {
365  $revision['delta'] = $revision['size'] - $sizes[$revision['parent_id']];
366  }
367 
368  // We only remembered this for delta calculations. We do not want to return it.
369  unset( $revision['parent_id'] );
370  }
371  }
372 
373  if ( $revisions && $params['newer_than'] ) {
374  $revisions = array_reverse( $revisions );
375  $temp = $lastRevId;
376  $lastRevId = $firstRevId;
377  $firstRevId = $temp;
378  }
379  }
380 
381  $response = [
382  'revisions' => $revisions
383  ];
384 
385  // Omit newer/older if there are no additional corresponding revisions.
386  // This facilitates clients doing "paging" style api operations.
387  if ( $revisions ) {
388  if ( $params['newer_than'] || $res->numRows() > self::REVISIONS_RETURN_LIMIT ) {
389  $older = $lastRevId;
390  }
391  if ( $params['older_than'] ||
392  ( $params['newer_than'] && $res->numRows() > self::REVISIONS_RETURN_LIMIT )
393  ) {
394  $newer = $firstRevId;
395  }
396  }
397 
398  $queryParts = [];
399 
400  if ( isset( $params['filter'] ) ) {
401  $queryParts['filter'] = $params['filter'];
402  }
403 
404  $pathParams = [ 'title' => $this->titleFormatter->getPrefixedDBkey( $page ) ];
405 
406  $response['latest'] = $this->getRouteUrl( $pathParams, $queryParts );
407 
408  if ( isset( $older ) ) {
409  $response['older'] =
410  $this->getRouteUrl( $pathParams, $queryParts + [ 'older_than' => $older ] );
411  }
412  if ( isset( $newer ) ) {
413  $response['newer'] =
414  $this->getRouteUrl( $pathParams, $queryParts + [ 'newer_than' => $newer ] );
415  }
416 
417  return $response;
418  }
419 
420  public function needsWriteAccess() {
421  return false;
422  }
423 
424  public function getParamSettings() {
425  return [
426  'title' => [
427  self::PARAM_SOURCE => 'path',
428  ParamValidator::PARAM_TYPE => 'string',
429  ParamValidator::PARAM_REQUIRED => true,
430  ],
431  'older_than' => [
432  self::PARAM_SOURCE => 'query',
433  ParamValidator::PARAM_TYPE => 'integer',
434  ParamValidator::PARAM_REQUIRED => false,
435  ],
436  'newer_than' => [
437  self::PARAM_SOURCE => 'query',
438  ParamValidator::PARAM_TYPE => 'integer',
439  ParamValidator::PARAM_REQUIRED => false,
440  ],
441  'filter' => [
442  self::PARAM_SOURCE => 'query',
443  ParamValidator::PARAM_TYPE => self::ALLOWED_FILTER_TYPES,
444  ParamValidator::PARAM_REQUIRED => false,
445  ],
446  ];
447  }
448 
454  protected function getETag(): ?string {
455  $page = $this->getPage();
456  if ( !$page ) {
457  return null;
458  }
459 
460  return '"' . $page->getLatest() . '"';
461  }
462 
468  protected function getLastModified(): ?string {
469  $page = $this->getPage();
470  if ( !$page ) {
471  return null;
472  }
473 
474  $rev = $this->revisionStore->getKnownCurrentRevision( $page );
475  return $rev->getTimestamp();
476  }
477 
481  protected function hasRepresentation() {
482  return (bool)$this->getPage();
483  }
484 }
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:468
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:1665
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:14
$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:18
MediaWiki\Rest\Handler\PageHistoryHandler\getBitmask
getBitmask()
Helper function for rev_deleted/user rights query conditions.
Definition: PageHistoryHandler.php:282
$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:454
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:26
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:424
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:299
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:420
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:481