MediaWiki  master
ApiComparePages.php
Go to the documentation of this file.
1 <?php
35 use Wikimedia\RequestTimeout\TimeoutException;
36 
40 class ApiComparePages extends ApiBase {
41 
42  private RevisionStore $revisionStore;
43  private ArchivedRevisionLookup $archivedRevisionLookup;
44  private SlotRoleRegistry $slotRoleRegistry;
45 
47  private $guessedTitle = false;
48  private $props;
49 
50  private IContentHandlerFactory $contentHandlerFactory;
51  private ContentTransformer $contentTransformer;
52  private CommentFormatter $commentFormatter;
53  private TempUserCreator $tempUserCreator;
54  private UserFactory $userFactory;
55  private DifferenceEngine $differenceEngine;
56 
69  public function __construct(
70  ApiMain $mainModule,
71  $moduleName,
72  RevisionStore $revisionStore,
73  ArchivedRevisionLookup $archivedRevisionLookup,
74  SlotRoleRegistry $slotRoleRegistry,
75  IContentHandlerFactory $contentHandlerFactory,
76  ContentTransformer $contentTransformer,
77  CommentFormatter $commentFormatter,
78  TempUserCreator $tempUserCreator,
79  UserFactory $userFactory
80  ) {
81  parent::__construct( $mainModule, $moduleName );
82  $this->revisionStore = $revisionStore;
83  $this->archivedRevisionLookup = $archivedRevisionLookup;
84  $this->slotRoleRegistry = $slotRoleRegistry;
85  $this->contentHandlerFactory = $contentHandlerFactory;
86  $this->contentTransformer = $contentTransformer;
87  $this->commentFormatter = $commentFormatter;
88  $this->tempUserCreator = $tempUserCreator;
89  $this->userFactory = $userFactory;
90  $this->differenceEngine = new DifferenceEngine;
91  }
92 
93  public function execute() {
94  $params = $this->extractRequestParams();
95 
96  // Parameter validation
98  $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
99  );
101  $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
102  );
103 
104  $this->props = array_fill_keys( $params['prop'], true );
105 
106  // Cache responses publicly by default. This may be overridden later.
107  $this->getMain()->setCacheMode( 'public' );
108 
109  // Get the 'from' RevisionRecord
110  [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision( 'from', $params );
111 
112  // Get the 'to' RevisionRecord
113  if ( $params['torelative'] !== null ) {
114  if ( !$fromRelRev ) {
115  $this->dieWithError( 'apierror-compare-relative-to-nothing' );
116  }
117  if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) {
118  // RevisionStore's getPreviousRevision/getNextRevision blow up
119  // when passed an RevisionArchiveRecord for a deleted page
120  $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] );
121  }
122  switch ( $params['torelative'] ) {
123  case 'prev':
124  // Swap 'from' and 'to'
125  [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
126  $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
127  $fromRelRev = $fromRev;
128  $fromValsRev = $fromRev;
129  if ( !$fromRev ) {
130  $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
131  $this->addWarning( [
132  'apiwarn-compare-no-prev',
133  wfEscapeWikiText( $title->getPrefixedText() ),
134  $toRelRev->getId()
135  ] );
136 
137  // (T203433) Create an empty dummy revision as the "previous".
138  // The main slot has to exist, the rest will be handled by DifferenceEngine.
139  $fromRev = new MutableRevisionRecord(
140  $title ?: $toRev->getPage()
141  );
142  $fromRev->setContent(
143  SlotRecord::MAIN,
144  $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
145  ->getContentHandler()
146  ->makeEmptyContent()
147  );
148  }
149  break;
150 
151  case 'next':
152  $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
153  $toRelRev = $toRev;
154  $toValsRev = $toRev;
155  if ( !$toRev ) {
156  $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
157  $this->addWarning( [
158  'apiwarn-compare-no-next',
159  wfEscapeWikiText( $title->getPrefixedText() ),
160  $fromRelRev->getId()
161  ] );
162 
163  // (T203433) The web UI treats "next" as "cur" in this case.
164  // Avoid repeating metadata by making a MutableRevisionRecord with no changes.
165  $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
166  }
167  break;
168 
169  case 'cur':
170  $title = $fromRelRev->getPageAsLinkTarget();
171  $toRev = $this->revisionStore->getRevisionByTitle( $title );
172  if ( !$toRev ) {
173  $title = Title::newFromLinkTarget( $title );
174  $this->dieWithError(
175  [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
176  'nosuchrevid'
177  );
178  }
179  $toRelRev = $toRev;
180  $toValsRev = $toRev;
181  break;
182  }
183  } else {
184  [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision( 'to', $params );
185  }
186 
187  // Handle missing from or to revisions (should never happen)
188  // @codeCoverageIgnoreStart
189  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
190  if ( !$fromRev || !$toRev ) {
191  $this->dieWithError( 'apierror-baddiff' );
192  }
193  // @codeCoverageIgnoreEnd
194 
195  // Handle revdel
196  if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
197  $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
198  }
199  if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
200  $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
201  }
202 
203  // Get the diff
204  $context = new DerivativeContext( $this->getContext() );
205  if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
206  $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
207  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
208  } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
209  $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
210  } else {
211  $guessedTitle = $this->guessTitle();
212  if ( $guessedTitle ) {
213  $context->setTitle( $guessedTitle );
214  }
215  }
216  $this->differenceEngine->setContext( $context );
217  $this->differenceEngine->setSlotDiffOptions( [ 'diff-type' => $params['difftype'] ] );
218  $this->differenceEngine->setRevisions( $fromRev, $toRev );
219  if ( $params['slots'] === null ) {
220  $difftext = $this->differenceEngine->getDiffBody();
221  if ( $difftext === false ) {
222  $this->dieWithError( 'apierror-baddiff' );
223  }
224  } else {
225  $difftext = [];
226  foreach ( $params['slots'] as $role ) {
227  $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
228  }
229  }
230  foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
231  $this->addWarning( $msg );
232  }
233 
234  // Fill in the response
235  $vals = [];
236  $this->setVals( $vals, 'from', $fromValsRev );
237  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
238  $this->setVals( $vals, 'to', $toValsRev );
239 
240  if ( isset( $this->props['rel'] ) ) {
241  if ( !$fromRev instanceof MutableRevisionRecord ) {
242  $rev = $this->revisionStore->getPreviousRevision( $fromRev );
243  if ( $rev ) {
244  $vals['prev'] = $rev->getId();
245  }
246  }
247  if ( !$toRev instanceof MutableRevisionRecord ) {
248  $rev = $this->revisionStore->getNextRevision( $toRev );
249  if ( $rev ) {
250  $vals['next'] = $rev->getId();
251  }
252  }
253  }
254 
255  if ( isset( $this->props['diffsize'] ) ) {
256  $vals['diffsize'] = 0;
257  foreach ( (array)$difftext as $text ) {
258  $vals['diffsize'] += strlen( $text );
259  }
260  }
261  if ( isset( $this->props['diff'] ) ) {
262  if ( is_array( $difftext ) ) {
263  ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
264  $vals['bodies'] = $difftext;
265  } else {
266  ApiResult::setContentValue( $vals, 'body', $difftext );
267  }
268  }
269 
270  // Diffs can be really big and there's little point in having
271  // ApiResult truncate it to an empty response since the diff is the
272  // whole reason this module exists. So pass NO_SIZE_CHECK here.
273  $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
274  }
275 
284  private function getRevisionById( $id ) {
285  $rev = $this->revisionStore->getRevisionById( $id );
286  if ( !$rev && $this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
287  // Try the 'archive' table
288  $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
289  }
290  return $rev;
291  }
292 
298  private function guessTitle() {
299  if ( $this->guessedTitle !== false ) {
300  return $this->guessedTitle;
301  }
302 
303  $this->guessedTitle = null;
304  $params = $this->extractRequestParams();
305 
306  foreach ( [ 'from', 'to' ] as $prefix ) {
307  if ( $params["{$prefix}rev"] !== null ) {
308  $rev = $this->getRevisionById( $params["{$prefix}rev"] );
309  if ( $rev ) {
310  $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
311  break;
312  }
313  }
314 
315  if ( $params["{$prefix}title"] !== null ) {
316  $title = Title::newFromText( $params["{$prefix}title"] );
317  if ( $title && !$title->isExternal() ) {
318  $this->guessedTitle = $title;
319  break;
320  }
321  }
322 
323  if ( $params["{$prefix}id"] !== null ) {
324  $title = Title::newFromID( $params["{$prefix}id"] );
325  if ( $title ) {
326  $this->guessedTitle = $title;
327  break;
328  }
329  }
330  }
331 
332  return $this->guessedTitle;
333  }
334 
340  private function guessModel( $role ) {
341  $params = $this->extractRequestParams();
342 
343  foreach ( [ 'from', 'to' ] as $prefix ) {
344  if ( $params["{$prefix}rev"] !== null ) {
345  $rev = $this->getRevisionById( $params["{$prefix}rev"] );
346  if ( $rev && $rev->hasSlot( $role ) ) {
347  return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
348  }
349  }
350  }
351 
352  $guessedTitle = $this->guessTitle();
353  if ( $guessedTitle ) {
354  return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
355  }
356 
357  if ( isset( $params["fromcontentmodel-$role"] ) ) {
358  return $params["fromcontentmodel-$role"];
359  }
360  if ( isset( $params["tocontentmodel-$role"] ) ) {
361  return $params["tocontentmodel-$role"];
362  }
363 
364  if ( $role === SlotRecord::MAIN ) {
365  if ( isset( $params['fromcontentmodel'] ) ) {
366  return $params['fromcontentmodel'];
367  }
368  if ( isset( $params['tocontentmodel'] ) ) {
369  return $params['tocontentmodel'];
370  }
371  }
372 
373  return null;
374  }
375 
391  private function getDiffRevision( $prefix, array $params ) {
392  // Back compat params
393  $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
394  $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
395  if ( $params["{$prefix}text"] !== null ) {
396  $params["{$prefix}slots"] = [ SlotRecord::MAIN ];
397  $params["{$prefix}text-main"] = $params["{$prefix}text"];
398  $params["{$prefix}section-main"] = null;
399  $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
400  $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
401  }
402 
403  $title = null;
404  $rev = null;
405  $suppliedContent = $params["{$prefix}slots"] !== null;
406 
407  // Get the revision and title, if applicable
408  $revId = null;
409  if ( $params["{$prefix}rev"] !== null ) {
410  $revId = $params["{$prefix}rev"];
411  } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
412  if ( $params["{$prefix}title"] !== null ) {
413  $title = Title::newFromText( $params["{$prefix}title"] );
414  if ( !$title || $title->isExternal() ) {
415  $this->dieWithError(
416  [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
417  );
418  }
419  } else {
420  $title = Title::newFromID( $params["{$prefix}id"] );
421  if ( !$title ) {
422  $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
423  }
424  }
425  $revId = $title->getLatestRevID();
426  if ( !$revId ) {
427  $revId = null;
428  // Only die here if we're not using supplied text
429  if ( !$suppliedContent ) {
430  if ( $title->exists() ) {
431  $this->dieWithError(
432  [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
433  'nosuchrevid'
434  );
435  } else {
436  $this->dieWithError(
437  [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
438  'missingtitle'
439  );
440  }
441  }
442  }
443  }
444  if ( $revId !== null ) {
445  $rev = $this->getRevisionById( $revId );
446  if ( !$rev ) {
447  $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
448  }
449  $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
450 
451  // If we don't have supplied content, return here. Otherwise,
452  // continue on below with the supplied content.
453  if ( !$suppliedContent ) {
454  $newRev = $rev;
455 
456  // Deprecated 'fromsection'/'tosection'
457  if ( isset( $params["{$prefix}section"] ) ) {
458  $section = $params["{$prefix}section"];
459  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
460  $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
461  $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
462  $this->getAuthority() );
463  if ( !$content ) {
464  $this->dieWithError(
465  [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
466  );
467  }
468  $content = $content->getSection( $section );
469  if ( !$content ) {
470  $this->dieWithError(
471  [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
472  "nosuch{$prefix}section"
473  );
474  }
475  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
476  $newRev->setContent( SlotRecord::MAIN, $content );
477  }
478 
479  return [ $newRev, $rev, $rev ];
480  }
481  }
482 
483  // Override $content based on supplied text
484  if ( !$title ) {
485  $title = $this->guessTitle();
486  }
487  if ( $rev ) {
488  $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
489  } else {
490  $newRev = new MutableRevisionRecord( $title ?: Title::newMainPage() );
491  }
492  foreach ( $params["{$prefix}slots"] as $role ) {
493  $text = $params["{$prefix}text-{$role}"];
494  if ( $text === null ) {
495  // The SlotRecord::MAIN role can't be deleted
496  if ( $role === SlotRecord::MAIN ) {
497  $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
498  }
499 
500  // These parameters make no sense without text. Reject them to avoid
501  // confusion.
502  foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
503  if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
504  $this->dieWithError( [
505  'apierror-compare-notext',
506  wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
507  wfEscapeWikiText( "{$prefix}text-{$role}" ),
508  ] );
509  }
510  }
511 
512  $newRev->removeSlot( $role );
513  continue;
514  }
515 
516  $model = $params["{$prefix}contentmodel-{$role}"];
517  $format = $params["{$prefix}contentformat-{$role}"];
518 
519  if ( !$model && $rev && $rev->hasSlot( $role ) ) {
520  $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
521  }
522  if ( !$model && $title && $role === SlotRecord::MAIN ) {
523  // @todo: Use SlotRoleRegistry and do this for all slots
524  $model = $title->getContentModel();
525  }
526  if ( !$model ) {
527  $model = $this->guessModel( $role );
528  }
529  if ( !$model ) {
530  $model = CONTENT_MODEL_WIKITEXT;
531  $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
532  }
533 
534  try {
535  $content = $this->contentHandlerFactory
536  ->getContentHandler( $model )
537  ->unserializeContent( $text, $format );
538  } catch ( MWContentSerializationException $ex ) {
539  $this->dieWithException( $ex, [
540  'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
541  ] );
542  }
543 
544  if ( $params["{$prefix}pst"] ) {
545  if ( !$title ) {
546  $this->dieWithError( 'apierror-compare-no-title' );
547  }
548  $popts = ParserOptions::newFromContext( $this->getContext() );
549  $content = $this->contentTransformer->preSaveTransform(
550  $content,
551  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
552  $title,
553  $this->getUserForPreview(),
554  $popts
555  );
556  }
557 
558  $section = $params["{$prefix}section-{$role}"];
559  if ( $section !== null && $section !== '' ) {
560  if ( !$rev ) {
561  $this->dieWithError( "apierror-compare-no{$prefix}revision" );
562  }
563  $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
564  if ( !$oldContent ) {
565  $this->dieWithError(
566  [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
567  'missingcontent'
568  );
569  }
570  if ( !$oldContent->getContentHandler()->supportsSections() ) {
571  $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
572  }
573  try {
574  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
575  $content = $oldContent->replaceSection( $section, $content, '' );
576  } catch ( TimeoutException $e ) {
577  throw $e;
578  } catch ( Exception $ex ) {
579  // Probably a content model mismatch.
580  $content = null;
581  }
582  if ( !$content ) {
583  $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
584  }
585  }
586 
587  // Deprecated 'fromsection'/'tosection'
588  if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
589  $section = $params["{$prefix}section"];
590  $content = $content->getSection( $section );
591  if ( !$content ) {
592  $this->dieWithError(
593  [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
594  "nosuch{$prefix}section"
595  );
596  }
597  }
598 
599  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
600  $newRev->setContent( $role, $content );
601  }
602  return [ $newRev, $rev, null ];
603  }
604 
612  private function setVals( &$vals, $prefix, $rev ) {
613  if ( $rev ) {
614  $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
615  if ( isset( $this->props['ids'] ) ) {
616  $vals["{$prefix}id"] = $title->getArticleID();
617  $vals["{$prefix}revid"] = $rev->getId();
618  }
619  if ( isset( $this->props['title'] ) ) {
620  ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
621  }
622  if ( isset( $this->props['size'] ) ) {
623  $vals["{$prefix}size"] = $rev->getSize();
624  }
625  if ( isset( $this->props['timestamp'] ) ) {
626  $revTimestamp = $rev->getTimestamp();
627  if ( $revTimestamp ) {
628  $vals["{$prefix}timestamp"] = wfTimestamp( TS_ISO_8601, $revTimestamp );
629  }
630  }
631 
632  $anyHidden = false;
633  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
634  $vals["{$prefix}texthidden"] = true;
635  $anyHidden = true;
636  }
637 
638  if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
639  $vals["{$prefix}userhidden"] = true;
640  $anyHidden = true;
641  }
642  if ( isset( $this->props['user'] ) ) {
643  $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
644  if ( $user ) {
645  $vals["{$prefix}user"] = $user->getName();
646  $vals["{$prefix}userid"] = $user->getId();
647  }
648  }
649 
650  if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
651  $vals["{$prefix}commenthidden"] = true;
652  $anyHidden = true;
653  }
654  if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
655  $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
656  if ( $comment !== null ) {
657  if ( isset( $this->props['comment'] ) ) {
658  $vals["{$prefix}comment"] = $comment->text;
659  }
660  $vals["{$prefix}parsedcomment"] = $this->commentFormatter->format(
661  $comment->text, $title
662  );
663  }
664  }
665 
666  if ( $anyHidden ) {
667  $this->getMain()->setCacheMode( 'private' );
668  if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
669  $vals["{$prefix}suppressed"] = true;
670  }
671  }
672 
673  if ( $rev instanceof RevisionArchiveRecord ) {
674  $this->getMain()->setCacheMode( 'private' );
675  $vals["{$prefix}archive"] = true;
676  }
677  }
678  }
679 
680  private function getUserForPreview() {
681  $user = $this->getUser();
682  if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
683  return $this->userFactory->newUnsavedTempUser(
684  $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
685  );
686  }
687  return $user;
688  }
689 
690  public function getAllowedParams() {
691  $slotRoles = $this->slotRoleRegistry->getKnownRoles();
692  sort( $slotRoles, SORT_STRING );
693 
694  // Parameters for the 'from' and 'to' content
695  $fromToParams = [
696  'title' => null,
697  'id' => [
698  ParamValidator::PARAM_TYPE => 'integer'
699  ],
700  'rev' => [
701  ParamValidator::PARAM_TYPE => 'integer'
702  ],
703 
704  'slots' => [
705  ParamValidator::PARAM_TYPE => $slotRoles,
706  ParamValidator::PARAM_ISMULTI => true,
707  ],
708  'text-{slot}' => [
709  ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
710  ParamValidator::PARAM_TYPE => 'text',
711  ],
712  'section-{slot}' => [
713  ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
714  ParamValidator::PARAM_TYPE => 'string',
715  ],
716  'contentformat-{slot}' => [
717  ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
718  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
719  ],
720  'contentmodel-{slot}' => [
721  ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
722  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
723  ],
724  'pst' => false,
725 
726  'text' => [
727  ParamValidator::PARAM_TYPE => 'text',
728  ParamValidator::PARAM_DEPRECATED => true,
729  ],
730  'contentformat' => [
731  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
732  ParamValidator::PARAM_DEPRECATED => true,
733  ],
734  'contentmodel' => [
735  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
736  ParamValidator::PARAM_DEPRECATED => true,
737  ],
738  'section' => [
739  ParamValidator::PARAM_DEFAULT => null,
740  ParamValidator::PARAM_DEPRECATED => true,
741  ],
742  ];
743 
744  $ret = [];
745  foreach ( $fromToParams as $k => $v ) {
746  if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
747  $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
748  }
749  $ret["from$k"] = $v;
750  }
751  foreach ( $fromToParams as $k => $v ) {
752  if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
753  $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
754  }
755  $ret["to$k"] = $v;
756  }
757 
758  $ret = wfArrayInsertAfter(
759  $ret,
760  [ 'torelative' => [ ParamValidator::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
761  'torev'
762  );
763 
764  $ret['prop'] = [
765  ParamValidator::PARAM_DEFAULT => 'diff|ids|title',
766  ParamValidator::PARAM_TYPE => [
767  'diff',
768  'diffsize',
769  'rel',
770  'ids',
771  'title',
772  'user',
773  'comment',
774  'parsedcomment',
775  'size',
776  'timestamp',
777  ],
778  ParamValidator::PARAM_ISMULTI => true,
780  ];
781 
782  $ret['slots'] = [
783  ParamValidator::PARAM_TYPE => $slotRoles,
784  ParamValidator::PARAM_ISMULTI => true,
785  ParamValidator::PARAM_ALL => true,
786  ];
787 
788  $ret['difftype'] = [
789  ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
790  ParamValidator::PARAM_DEFAULT => 'table',
791  ];
792 
793  return $ret;
794  }
795 
796  protected function getExamplesMessages() {
797  return [
798  'action=compare&fromrev=1&torev=2'
799  => 'apihelp-compare-example-1',
800  ];
801  }
802 
803  public function getHelpUrls() {
804  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
805  }
806 }
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:209
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfArrayInsertAfter(array $array, array $insert, $after)
Insert an array into another array after the specified key.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:63
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1516
getMain()
Get the main module.
Definition: ApiBase.php:547
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition: ApiBase.php:210
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition: ApiBase.php:1007
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition: ApiBase.php:982
const PARAM_TEMPLATE_VARS
(array) Indicate that this is a templated parameter, and specify replacements.
Definition: ApiBase.php:228
getResult()
Get the result object.
Definition: ApiBase.php:668
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:808
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1434
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:529
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1529
getHelpUrls()
Return links to more detailed help pages about the module.
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiMain $mainModule, $moduleName, RevisionStore $revisionStore, ArchivedRevisionLookup $archivedRevisionLookup, SlotRoleRegistry $slotRoleRegistry, IContentHandlerFactory $contentHandlerFactory, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter, TempUserCreator $tempUserCreator, UserFactory $userFactory)
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:64
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:45
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:716
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don't use this unless yo...
Definition: ApiResult.php:58
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
Definition: ApiResult.php:467
getContext()
Get the base IContextSource object.
An IContextSource implementation which will inherit context from another source but allow individual ...
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Exception representing a failure to serialize or unserialize a content object.
This is the main service interface for converting single-line comments from various DB comment fields...
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Represents a title within MediaWiki.
Definition: Title.php:76
Service for temporary user creation.
Creates User objects.
Definition: UserFactory.php:41
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Service for formatting and validating API parameters.
$content
Definition: router.php:76