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 
52  private $title = null;
53 
62  public function __construct(
64  NameTableStoreFactory $nameTableStoreFactory,
67  ) {
68  $this->revisionStore = $revisionStore;
69  $this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
70  $this->permissionManager = $permissionManager;
71  $this->loadBalancer = $loadBalancer;
72 
73  // @todo Inject this, when there is a good way to do that
74  $this->user = RequestContext::getMain()->getUser();
75  }
76 
80  private function getTitle() {
81  if ( $this->title === null ) {
82  $this->title = Title::newFromText( $this->getValidatedParams()['title'] ) ?? false;
83  }
84  return $this->title;
85  }
86 
96  public function run( $title ) {
97  $params = $this->getValidatedParams();
98  if ( $params['older_than'] !== null && $params['newer_than'] !== null ) {
99  throw new LocalizedHttpException(
100  new MessageValue( 'rest-pagehistory-incompatible-params' ), 400 );
101  }
102 
103  if ( ( $params['older_than'] !== null && $params['older_than'] < 1 ) ||
104  ( $params['newer_than'] !== null && $params['newer_than'] < 1 )
105  ) {
106  throw new LocalizedHttpException(
107  new MessageValue( 'rest-pagehistory-param-range-error' ), 400 );
108  }
109 
110  $tagIds = [];
111  if ( $params['filter'] === 'reverted' ) {
112  foreach ( self::REVERTED_TAG_NAMES as $tagName ) {
113  try {
114  $tagIds[] = $this->changeTagDefStore->getId( $tagName );
115  } catch ( NameTableAccessException $exception ) {
116  // If no revisions are tagged with a name, no tag id will be present
117  }
118  }
119  }
120 
121  $titleObj = Title::newFromText( $title );
122  if ( !$titleObj || !$titleObj->getArticleID() ) {
123  throw new LocalizedHttpException(
124  new MessageValue( 'rest-nonexistent-title',
125  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
126  ),
127  404
128  );
129  }
130  if ( !$this->permissionManager->userCan( 'read', $this->user, $titleObj ) ) {
131  throw new LocalizedHttpException(
132  new MessageValue( 'rest-permission-denied-title',
133  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ] ),
134  403
135  );
136  }
137 
138  $relativeRevId = $params['older_than'] ?? $params['newer_than'] ?? 0;
139  if ( $relativeRevId ) {
140  // Confirm the relative revision exists for this page. If so, get its timestamp.
141  $rev = $this->revisionStore->getRevisionByPageId(
142  $titleObj->getArticleID(),
143  $relativeRevId
144  );
145  if ( !$rev ) {
146  throw new LocalizedHttpException(
147  new MessageValue( 'rest-nonexistent-title-revision',
148  [ $relativeRevId, new ScalarParam( ParamType::PLAINTEXT, $title ) ]
149  ),
150  404
151  );
152  }
153  $ts = $rev->getTimestamp();
154  if ( $ts === null ) {
155  throw new LocalizedHttpException(
156  new MessageValue( 'rest-pagehistory-timestamp-error',
157  [ $relativeRevId ]
158  ),
159  500
160  );
161  }
162  } else {
163  $ts = 0;
164  }
165 
166  $res = $this->getDbResults( $titleObj, $params, $relativeRevId, $ts, $tagIds );
167  $response = $this->processDbResults( $res, $titleObj, $params );
168  return $this->getResponseFactory()->createJson( $response );
169  }
170 
179  private function getDbResults( Title $titleObj, array $params, $relativeRevId, $ts, $tagIds ) {
180  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
181  $revQuery = $this->revisionStore->getQueryInfo();
182  $cond = [
183  'rev_page' => $titleObj->getArticleID()
184  ];
185 
186  if ( $params['filter'] ) {
187  // This redundant join condition tells MySQL that rev_page and revactor_page are the
188  // same, so it can propagate the condition
189  $revQuery['joins']['temp_rev_user'][1] =
190  "temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
191 
192  // The validator ensures this value, if present, is one of the expected values
193  switch ( $params['filter'] ) {
194  case 'bot':
195  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
196  'user_groups',
197  '1',
198  [
199  'actor_rev_user.actor_user = ug_user',
200  'ug_group' => $this->permissionManager->getGroupsWithPermission( 'bot' ),
201  'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
202  ],
203  __METHOD__
204  ) . ')';
205  $bitmask = $this->getBitmask();
206  if ( $bitmask ) {
207  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
208  }
209  break;
210 
211  case 'anonymous':
212  $cond[] = "actor_user IS NULL";
213  $bitmask = $this->getBitmask();
214  if ( $bitmask ) {
215  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
216  }
217  break;
218 
219  case 'reverted':
220  if ( !$tagIds ) {
221  return false;
222  }
223  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
224  'change_tag',
225  '1',
226  [ 'ct_rev_id = rev_id', 'ct_tag_id' => $tagIds ],
227  __METHOD__
228  ) . ')';
229  break;
230 
231  case 'minor':
232  $cond[] = 'rev_minor_edit != 0';
233  break;
234  }
235  }
236 
237  if ( $relativeRevId ) {
238  $op = $params['older_than'] ? '<' : '>';
239  $sort = $params['older_than'] ? 'DESC' : 'ASC';
240  $ts = $dbr->addQuotes( $dbr->timestamp( $ts ) );
241  $cond[] = "rev_timestamp $op $ts OR " .
242  "(rev_timestamp = $ts AND rev_id $op $relativeRevId)";
243  $orderBy = "rev_timestamp $sort, rev_id $sort";
244  } else {
245  $orderBy = "rev_timestamp DESC, rev_id DESC";
246  }
247 
248  // Select one more than the return limit, to learn if there are additional revisions.
249  $limit = self::REVISIONS_RETURN_LIMIT + 1;
250 
251  $res = $dbr->select(
252  $revQuery['tables'],
253  $revQuery['fields'],
254  $cond,
255  __METHOD__,
256  [
257  'ORDER BY' => $orderBy,
258  'LIMIT' => $limit,
259  ],
260  $revQuery['joins']
261  );
262 
263  return $res;
264  }
265 
273  private function getBitmask() {
274  if ( !$this->permissionManager->userHasRight( $this->user, 'deletedhistory' ) ) {
275  $bitmask = RevisionRecord::DELETED_USER;
276  } elseif ( !$this->permissionManager
277  ->userHasAnyRight( $this->user, 'suppressrevision', 'viewsuppressed' )
278  ) {
279  $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
280  } else {
281  $bitmask = 0;
282  }
283  return $bitmask;
284  }
285 
292  private function processDbResults( $res, $titleObj, $params ) {
293  $revisions = [];
294 
295  if ( $res ) {
296  $sizes = [];
297  foreach ( $res as $row ) {
298  $rev = $this->revisionStore->newRevisionFromRow(
299  $row,
300  IDBAccessObject::READ_NORMAL,
301  $titleObj
302  );
303  if ( !$revisions ) {
304  $firstRevId = $row->rev_id;
305  }
306  $lastRevId = $row->rev_id;
307 
308  $revision = [
309  'id' => $rev->getId(),
310  'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ),
311  'minor' => $rev->isMinor(),
312  'size' => $rev->getSize()
313  ];
314 
315  // Remember revision sizes and parent ids for calculating deltas. If a revision's
316  // parent id is unknown, we will be unable to supply the delta for that revision.
317  $sizes[$rev->getId()] = $rev->getSize();
318  $parentId = $rev->getParentId();
319  if ( $parentId ) {
320  $revision['parent_id'] = $parentId;
321  }
322 
323  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->user );
324  $revision['comment'] = $comment ? $comment->text : null;
325 
326  $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->user );
327  if ( $revUser ) {
328  $revision['user'] = [
329  'id' => $revUser->isRegistered() ? $revUser->getId() : null,
330  'name' => $revUser->getName()
331  ];
332  } else {
333  $revision['user'] = null;
334  }
335 
336  $revisions[] = $revision;
337 
338  // Break manually at the return limit. We may have more results than we can return.
339  if ( count( $revisions ) == self::REVISIONS_RETURN_LIMIT ) {
340  break;
341  }
342  }
343 
344  // Request any parent sizes that we do not already know, then calculate deltas
345  $unknownSizes = [];
346  foreach ( $revisions as $revision ) {
347  if ( isset( $revision['parent_id'] ) && !isset( $sizes[$revision['parent_id']] ) ) {
348  $unknownSizes[] = $revision['parent_id'];
349  }
350  }
351  if ( $unknownSizes ) {
352  $sizes += $this->revisionStore->getRevisionSizes( $unknownSizes );
353  }
354  foreach ( $revisions as &$revision ) {
355  $revision['delta'] = null;
356  if ( isset( $revision['parent_id'] ) ) {
357  if ( isset( $sizes[$revision['parent_id']] ) ) {
358  $revision['delta'] = $revision['size'] - $sizes[$revision['parent_id']];
359  }
360 
361  // We only remembered this for delta calculations. We do not want to return it.
362  unset( $revision['parent_id'] );
363  }
364  }
365 
366  if ( $revisions && $params['newer_than'] ) {
367  $revisions = array_reverse( $revisions );
368  $temp = $lastRevId;
369  $lastRevId = $firstRevId;
370  $firstRevId = $temp;
371  }
372  }
373 
374  $response = [
375  'revisions' => $revisions
376  ];
377 
378  // Omit newer/older if there are no additional corresponding revisions.
379  // This facilitates clients doing "paging" style api operations.
380  if ( $revisions ) {
381  if ( $params['newer_than'] || $res->numRows() > self::REVISIONS_RETURN_LIMIT ) {
382  $older = $lastRevId;
383  }
384  if ( $params['older_than'] ||
385  ( $params['newer_than'] && $res->numRows() > self::REVISIONS_RETURN_LIMIT )
386  ) {
387  $newer = $firstRevId;
388  }
389  }
390 
391  $queryParts = [];
392 
393  if ( isset( $params['filter'] ) ) {
394  $queryParts['filter'] = $params['filter'];
395  }
396 
397  $path = '/page/' . urlencode( $titleObj->getPrefixedDBkey() ) . '/history';
398  $url = $this->getRouter()->getRouteUrl( $path, $queryParts );
399  $uri = new Uri( $url );
400 
401  $response['latest'] = $url;
402  if ( isset( $older ) ) {
403  $response['older'] = '' . Uri::withQueryValues(
404  $uri,
405  $queryParts + [ 'older_than' => $older ]
406  );
407  }
408  if ( isset( $newer ) ) {
409  $response['newer'] = '' . Uri::withQueryValues(
410  $uri,
411  $queryParts + [ 'newer_than' => $newer ]
412  );
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  $title = $this->getTitle();
454  if ( !$title || !$title->getArticleID() ) {
455  return null;
456  }
457 
458  return '"' . $title->getLatestRevID() . '"';
459  }
460 
466  protected function getLastModified(): ?string {
467  $title = $this->getTitle();
468  if ( !$title || !$title->getArticleID() ) {
469  return null;
470  }
471 
472  $rev = $this->revisionStore->getKnownCurrentRevision( $title );
473  return $rev->getTimestamp();
474  }
475 
479  protected function hasRepresentation() {
480  $title = $this->getTitle();
481  return $title ? $title->exists() : false;
482  }
483 }
MediaWiki\Rest\Handler\PageHistoryHandler\__construct
__construct(RevisionStore $revisionStore, NameTableStoreFactory $nameTableStoreFactory, PermissionManager $permissionManager, ILoadBalancer $loadBalancer)
RevisionStore $revisionStore.
Definition: PageHistoryHandler.php:62
MediaWiki\Rest\Handler\PageHistoryHandler\$title
Title bool null $title
Definition: PageHistoryHandler.php:52
MediaWiki\Rest\Handler
Definition: ActionModuleBasedHandler.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:332
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
MediaWiki\Rest\Handler\PageHistoryHandler\getLastModified
getLastModified()
Returns the time of the last change to the page.
Definition: PageHistoryHandler.php:466
$response
$response
Definition: opensearch_desc.php:44
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:77
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1809
MediaWiki\Rest\Handler\PageHistoryHandler\processDbResults
processDbResults( $res, $titleObj, $params)
Definition: PageHistoryHandler.php:292
Title\getArticleID
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3169
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:96
Wikimedia\Message\ScalarParam
Value object representing a message parameter holding a single value.
Definition: ScalarParam.php:10
$res
$res
Definition: testCompression.php:57
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:56
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:273
$dbr
$dbr
Definition: testCompression.php:54
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
MediaWiki\Rest\Handler\PageHistoryHandler\getDbResults
getDbResults(Title $titleObj, array $params, $relativeRevId, $ts, $tagIds)
Definition: PageHistoryHandler.php:179
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
MediaWiki\Rest\Handler\PageHistoryHandler\$changeTagDefStore
NameTableStore $changeTagDefStore
Definition: PageHistoryHandler.php:38
MediaWiki\Rest\Response
Definition: Response.php:8
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
MediaWiki\Rest\Handler\PageHistoryHandler\getTitle
getTitle()
Definition: PageHistoryHandler.php:80
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
Title\getLatestRevID
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3250
MediaWiki\Rest\Handler\PageHistoryHandler\getParamSettings
getParamSettings()
Fetch ParamValidator settings for parameters.
Definition: PageHistoryHandler.php:422
MediaWiki\Storage\NameTableStore
Definition: NameTableStore.php:36
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:451
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
$path
$path
Definition: NoLocalSettings.php:25
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
Title\exists
exists( $flags=0)
Check if page exists.
Definition: Title.php:3966
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:53
MediaWiki\Rest\Handler\PageHistoryHandler\ALLOWED_FILTER_TYPES
const ALLOWED_FILTER_TYPES
Definition: PageHistoryHandler.php:32
MediaWiki\Rest\Handler\getRouter
getRouter()
Get the Router.
Definition: Handler.php:60
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:418
Wikimedia\Message\ParamType
The constants used to specify parameter types.
Definition: ParamType.php:11
MediaWiki\Rest\SimpleHandler
Definition: SimpleHandler.php:14
MediaWiki\Rest\Handler\PageHistoryHandler\hasRepresentation
hasRepresentation()
Definition: PageHistoryHandler.php:479