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