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