MediaWiki  master
PageHistoryHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest\Handler;
4 
15 use RequestContext;
16 use Title;
17 use User;
24 
29  private const REVISIONS_RETURN_LIMIT = 20;
30  private const REVERTED_TAG_NAMES = [ 'mw-undo', 'mw-rollback' ];
31  private const ALLOWED_FILTER_TYPES = [ 'anonymous', 'bot', 'reverted', 'minor' ];
32 
34  private $revisionStore;
35 
38 
41 
43  private $loadBalancer;
44 
46  private $user;
47 
51  private $title = null;
52 
61  public function __construct(
63  NameTableStoreFactory $nameTableStoreFactory,
66  ) {
67  $this->revisionStore = $revisionStore;
68  $this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
69  $this->permissionManager = $permissionManager;
70  $this->loadBalancer = $loadBalancer;
71 
72  // @todo Inject this, when there is a good way to do that
73  $this->user = RequestContext::getMain()->getUser();
74  }
75 
79  private function getTitle() {
80  if ( $this->title === null ) {
81  $this->title = Title::newFromText( $this->getValidatedParams()['title'] ) ?? false;
82  }
83  return $this->title;
84  }
85 
95  public function run( $title ) {
96  $params = $this->getValidatedParams();
97  if ( $params['older_than'] !== null && $params['newer_than'] !== null ) {
98  throw new LocalizedHttpException(
99  new MessageValue( 'rest-pagehistory-incompatible-params' ), 400 );
100  }
101 
102  if ( ( $params['older_than'] !== null && $params['older_than'] < 1 ) ||
103  ( $params['newer_than'] !== null && $params['newer_than'] < 1 )
104  ) {
105  throw new LocalizedHttpException(
106  new MessageValue( 'rest-pagehistory-param-range-error' ), 400 );
107  }
108 
109  $tagIds = [];
110  if ( $params['filter'] === 'reverted' ) {
111  foreach ( self::REVERTED_TAG_NAMES as $tagName ) {
112  try {
113  $tagIds[] = $this->changeTagDefStore->getId( $tagName );
114  } catch ( NameTableAccessException $exception ) {
115  // If no revisions are tagged with a name, no tag id will be present
116  }
117  }
118  }
119 
120  $titleObj = Title::newFromText( $title );
121  if ( !$titleObj || !$titleObj->getArticleID() ) {
122  throw new LocalizedHttpException(
123  new MessageValue( 'rest-nonexistent-title',
124  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ]
125  ),
126  404
127  );
128  }
129  if ( !$this->permissionManager->userCan( 'read', $this->user, $titleObj ) ) {
130  throw new LocalizedHttpException(
131  new MessageValue( 'rest-permission-denied-title',
132  [ new ScalarParam( ParamType::PLAINTEXT, $title ) ] ),
133  403
134  );
135  }
136 
137  $relativeRevId = $params['older_than'] ?? $params['newer_than'] ?? 0;
138  if ( $relativeRevId ) {
139  // Confirm the relative revision exists for this page. If so, get its timestamp.
140  $rev = $this->revisionStore->getRevisionByPageId(
141  $titleObj->getArticleID(),
142  $relativeRevId
143  );
144  if ( !$rev ) {
145  throw new LocalizedHttpException(
146  new MessageValue( 'rest-nonexistent-title-revision',
147  [ $relativeRevId, new ScalarParam( ParamType::PLAINTEXT, $title ) ]
148  ),
149  404
150  );
151  }
152  $ts = $rev->getTimestamp();
153  if ( $ts === null ) {
154  throw new LocalizedHttpException(
155  new MessageValue( 'rest-pagehistory-timestamp-error',
156  [ $relativeRevId ]
157  ),
158  500
159  );
160  }
161  } else {
162  $ts = 0;
163  }
164 
165  $res = $this->getDbResults( $titleObj, $params, $relativeRevId, $ts, $tagIds );
166  $response = $this->processDbResults( $res, $titleObj, $params );
167  return $this->getResponseFactory()->createJson( $response );
168  }
169 
178  private function getDbResults( Title $titleObj, array $params, $relativeRevId, $ts, $tagIds ) {
179  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
180  $revQuery = $this->revisionStore->getQueryInfo();
181  $cond = [
182  'rev_page' => $titleObj->getArticleID()
183  ];
184 
185  if ( $params['filter'] ) {
186  // This redundant join condition tells MySQL that rev_page and revactor_page are the
187  // same, so it can propagate the condition
188  $revQuery['joins']['temp_rev_user'][1] =
189  "temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
190 
191  // The validator ensures this value, if present, is one of the expected values
192  switch ( $params['filter'] ) {
193  case 'bot':
194  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
195  'user_groups',
196  '1',
197  [
198  'actor_rev_user.actor_user = ug_user',
199  'ug_group' => $this->permissionManager->getGroupsWithPermission( 'bot' ),
200  'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
201  ],
202  __METHOD__
203  ) . ')';
204  $bitmask = $this->getBitmask();
205  if ( $bitmask ) {
206  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
207  }
208  break;
209 
210  case 'anonymous':
211  $cond[] = "actor_user IS NULL";
212  $bitmask = $this->getBitmask();
213  if ( $bitmask ) {
214  $cond[] = $dbr->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask";
215  }
216  break;
217 
218  case 'reverted':
219  if ( !$tagIds ) {
220  return false;
221  }
222  $cond[] = 'EXISTS(' . $dbr->selectSQLText(
223  'change_tag',
224  '1',
225  [ 'ct_rev_id = rev_id', 'ct_tag_id' => $tagIds ],
226  __METHOD__
227  ) . ')';
228  break;
229 
230  case 'minor':
231  $cond[] = 'rev_minor_edit != 0';
232  break;
233  }
234  }
235 
236  if ( $relativeRevId ) {
237  $op = $params['older_than'] ? '<' : '>';
238  $sort = $params['older_than'] ? 'DESC' : 'ASC';
239  $ts = $dbr->addQuotes( $dbr->timestamp( $ts ) );
240  $cond[] = "rev_timestamp $op $ts OR " .
241  "(rev_timestamp = $ts AND rev_id $op $relativeRevId)";
242  $orderBy = "rev_timestamp $sort, rev_id $sort";
243  } else {
244  $orderBy = "rev_timestamp DESC, rev_id DESC";
245  }
246 
247  // Select one more than the return limit, to learn if there are additional revisions.
248  $limit = self::REVISIONS_RETURN_LIMIT + 1;
249 
250  $res = $dbr->select(
251  $revQuery['tables'],
252  $revQuery['fields'],
253  $cond,
254  __METHOD__,
255  [
256  'ORDER BY' => $orderBy,
257  'LIMIT' => $limit,
258  ],
259  $revQuery['joins']
260  );
261 
262  return $res;
263  }
264 
272  private function getBitmask() {
273  if ( !$this->permissionManager->userHasRight( $this->user, 'deletedhistory' ) ) {
274  $bitmask = RevisionRecord::DELETED_USER;
275  } elseif ( !$this->permissionManager
276  ->userHasAnyRight( $this->user, 'suppressrevision', 'viewsuppressed' )
277  ) {
278  $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
279  } else {
280  $bitmask = 0;
281  }
282  return $bitmask;
283  }
284 
291  private function processDbResults( $res, $titleObj, $params ) {
292  $revisions = [];
293 
294  if ( $res ) {
295  $sizes = [];
296  foreach ( $res as $row ) {
297  $rev = $this->revisionStore->newRevisionFromRow(
298  $row,
299  IDBAccessObject::READ_NORMAL,
300  $titleObj
301  );
302  if ( !$revisions ) {
303  $firstRevId = $row->rev_id;
304  }
305  $lastRevId = $row->rev_id;
306 
307  $revision = [
308  'id' => $rev->getId(),
309  'timestamp' => wfTimestamp( TS_ISO_8601, $rev->getTimestamp() ),
310  'minor' => $rev->isMinor(),
311  'size' => $rev->getSize()
312  ];
313 
314  // Remember revision sizes and parent ids for calculating deltas. If a revision's
315  // parent id is unknown, we will be unable to supply the delta for that revision.
316  $sizes[$rev->getId()] = $rev->getSize();
317  $parentId = $rev->getParentId();
318  if ( $parentId ) {
319  $revision['parent_id'] = $parentId;
320  }
321 
322  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->user );
323  $revision['comment'] = $comment ? $comment->text : null;
324 
325  $revUser = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->user );
326  if ( $revUser ) {
327  $revision['user'] = [
328  'id' => $revUser->isRegistered() ? $revUser->getId() : null,
329  'name' => $revUser->getName()
330  ];
331  } else {
332  $revision['user'] = null;
333  }
334 
335  $revisions[] = $revision;
336 
337  // Break manually at the return limit. We may have more results than we can return.
338  if ( count( $revisions ) == self::REVISIONS_RETURN_LIMIT ) {
339  break;
340  }
341  }
342 
343  // Request any parent sizes that we do not already know, then calculate deltas
344  $unknownSizes = [];
345  foreach ( $revisions as $revision ) {
346  if ( isset( $revision['parent_id'] ) && !isset( $sizes[$revision['parent_id']] ) ) {
347  $unknownSizes[] = $revision['parent_id'];
348  }
349  }
350  if ( $unknownSizes ) {
351  $sizes += $this->revisionStore->getRevisionSizes( $unknownSizes );
352  }
353  foreach ( $revisions as &$revision ) {
354  $revision['delta'] = null;
355  if ( isset( $revision['parent_id'] ) ) {
356  if ( isset( $sizes[$revision['parent_id']] ) ) {
357  $revision['delta'] = $revision['size'] - $sizes[$revision['parent_id']];
358  }
359 
360  // We only remembered this for delta calculations. We do not want to return it.
361  unset( $revision['parent_id'] );
362  }
363  }
364 
365  if ( $revisions && $params['newer_than'] ) {
366  $revisions = array_reverse( $revisions );
367  $temp = $lastRevId;
368  $lastRevId = $firstRevId;
369  $firstRevId = $temp;
370  }
371  }
372 
373  $response = [
374  'revisions' => $revisions
375  ];
376 
377  // Omit newer/older if there are no additional corresponding revisions.
378  // This facilitates clients doing "paging" style api operations.
379  if ( $revisions ) {
380  if ( $params['newer_than'] || $res->numRows() > self::REVISIONS_RETURN_LIMIT ) {
381  $older = $lastRevId;
382  }
383  if ( $params['older_than'] ||
384  ( $params['newer_than'] && $res->numRows() > self::REVISIONS_RETURN_LIMIT )
385  ) {
386  $newer = $firstRevId;
387  }
388  }
389 
390  $queryParts = [];
391 
392  if ( isset( $params['filter'] ) ) {
393  $queryParts['filter'] = $params['filter'];
394  }
395 
396  $pathParams = [ 'title' => $titleObj->getPrefixedDBkey() ];
397 
398  $response['latest'] = $this->getRouteUrl( $pathParams, $queryParts );
399 
400  if ( isset( $older ) ) {
401  $response['older'] =
402  $this->getRouteUrl( $pathParams, $queryParts + [ 'older_than' => $older ] );
403  }
404  if ( isset( $newer ) ) {
405  $response['newer'] =
406  $this->getRouteUrl( $pathParams, $queryParts + [ 'newer_than' => $newer ] );
407  }
408 
409  return $response;
410  }
411 
412  public function needsWriteAccess() {
413  return false;
414  }
415 
416  public function getParamSettings() {
417  return [
418  'title' => [
419  self::PARAM_SOURCE => 'path',
420  ParamValidator::PARAM_TYPE => 'string',
421  ParamValidator::PARAM_REQUIRED => true,
422  ],
423  'older_than' => [
424  self::PARAM_SOURCE => 'query',
425  ParamValidator::PARAM_TYPE => 'integer',
426  ParamValidator::PARAM_REQUIRED => false,
427  ],
428  'newer_than' => [
429  self::PARAM_SOURCE => 'query',
430  ParamValidator::PARAM_TYPE => 'integer',
431  ParamValidator::PARAM_REQUIRED => false,
432  ],
433  'filter' => [
434  self::PARAM_SOURCE => 'query',
435  ParamValidator::PARAM_TYPE => self::ALLOWED_FILTER_TYPES,
436  ParamValidator::PARAM_REQUIRED => false,
437  ],
438  ];
439  }
440 
446  protected function getETag(): ?string {
447  $title = $this->getTitle();
448  if ( !$title || !$title->getArticleID() ) {
449  return null;
450  }
451 
452  return '"' . $title->getLatestRevID() . '"';
453  }
454 
460  protected function getLastModified(): ?string {
461  $title = $this->getTitle();
462  if ( !$title || !$title->getArticleID() ) {
463  return null;
464  }
465 
466  $rev = $this->revisionStore->getKnownCurrentRevision( $title );
467  return $rev->getTimestamp();
468  }
469 
473  protected function hasRepresentation() {
474  $title = $this->getTitle();
475  return $title ? $title->exists() : false;
476  }
477 }
MediaWiki\Rest\Handler\PageHistoryHandler\__construct
__construct(RevisionStore $revisionStore, NameTableStoreFactory $nameTableStoreFactory, PermissionManager $permissionManager, ILoadBalancer $loadBalancer)
RevisionStore $revisionStore.
Definition: PageHistoryHandler.php:61
MediaWiki\Rest\Handler\PageHistoryHandler\$title
Title bool null $title
Definition: PageHistoryHandler.php:51
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:329
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:28
MediaWiki\Rest\Handler\getResponseFactory
getResponseFactory()
Get the ResponseFactory which can be used to generate Response objects.
Definition: Handler.php:120
MediaWiki\Rest\Handler\PageHistoryHandler\getLastModified
getLastModified()
Returns the time of the last change to the page.
Definition: PageHistoryHandler.php:460
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:80
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1811
MediaWiki\Rest\Handler\PageHistoryHandler\processDbResults
processDbResults( $res, $titleObj, $params)
Definition: PageHistoryHandler.php:291
Title\getArticleID
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3216
MediaWiki\Rest\Handler\PageHistoryHandler\REVISIONS_RETURN_LIMIT
const REVISIONS_RETURN_LIMIT
Definition: PageHistoryHandler.php:29
MediaWiki\Rest\Handler\PageHistoryHandler\run
run( $title)
At most one of older_than and newer_than may be specified.
Definition: PageHistoryHandler.php:95
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:40
$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:272
$dbr
$dbr
Definition: testCompression.php:54
MediaWiki\Rest\Handler\PageHistoryHandler\REVERTED_TAG_NAMES
const REVERTED_TAG_NAMES
Definition: PageHistoryHandler.php:30
MediaWiki\Rest\Handler\PageHistoryHandler\$user
User $user
Definition: PageHistoryHandler.php:46
MediaWiki\Rest\Handler\PageHistoryHandler\getDbResults
getDbResults(Title $titleObj, array $params, $relativeRevId, $ts, $tagIds)
Definition: PageHistoryHandler.php:178
MediaWiki\Rest\Handler\PageHistoryHandler\getETag
getETag()
Returns an ETag representing a page's latest revision.
Definition: PageHistoryHandler.php:446
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:37
MediaWiki\Rest\Handler\getRouteUrl
getRouteUrl( $pathParams=[], $queryParams=[])
Get the URL of this handler's endpoint.
Definition: Handler.php:87
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:79
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:216
MediaWiki\Rest\Handler\PageHistoryHandler\$revisionStore
RevisionStore $revisionStore
Definition: PageHistoryHandler.php:34
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:49
Title\getLatestRevID
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3302
MediaWiki\Rest\Handler\PageHistoryHandler\getParamSettings
getParamSettings()
Fetch ParamValidator settings for parameters.
Definition: PageHistoryHandler.php:416
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
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:43
Title\exists
exists( $flags=0)
Check if page exists.
Definition: Title.php:4003
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:54
MediaWiki\Rest\Handler\PageHistoryHandler\ALLOWED_FILTER_TYPES
const ALLOWED_FILTER_TYPES
Definition: PageHistoryHandler.php:31
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:412
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:473