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