40 private $revisionStore;
43 private $slotRoleRegistry;
46 private $guessedTitle =
false;
50 private $contentHandlerFactory;
53 private $contentTransformer;
56 private $commentFormatter;
58 private bool $inlineSupported;
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';
93 $params,
'fromtitle',
'fromid',
'fromrev',
'fromtext',
'fromslots'
96 $params,
'totitle',
'toid',
'torev',
'totext',
'torelative',
'toslots'
99 $this->props = array_fill_keys( $params[
'prop'],
true );
102 $this->
getMain()->setCacheMode(
'public' );
105 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision(
'from', $params );
108 if ( $params[
'torelative'] !==
null ) {
109 if ( !$fromRelRev ) {
110 $this->
dieWithError(
'apierror-compare-relative-to-nothing' );
115 $this->
dieWithError( [
'apierror-compare-relative-to-deleted', $params[
'torelative'] ] );
117 switch ( $params[
'torelative'] ) {
120 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
121 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
122 $fromRelRev = $fromRev;
123 $fromValsRev = $fromRev;
125 $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
127 'apiwarn-compare-no-prev',
135 $title ?: $toRev->getPage()
139 $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
140 ->getContentHandler()
147 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
151 $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
153 'apiwarn-compare-no-next',
160 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
165 $title = $fromRelRev->getPageAsLinkTarget();
166 $toRev = $this->revisionStore->getRevisionByTitle(
$title );
179 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision(
'to', $params );
185 if ( !$fromRev || !$toRev ) {
191 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
192 $this->
dieWithError( [
'apierror-missingcontent-revid', $fromRev->getId() ],
'missingcontent' );
194 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
195 $this->
dieWithError( [
'apierror-missingcontent-revid', $toRev->getId() ],
'missingcontent' );
200 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
201 $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
203 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
204 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
206 $guessedTitle = $this->guessTitle();
207 if ( $guessedTitle ) {
208 $context->setTitle( $guessedTitle );
213 if ( $this->inlineSupported ) {
214 $de->setSlotDiffOptions( [
'diff-type' => $params[
'difftype'] ] );
216 $de->setRevisions( $fromRev, $toRev );
217 if ( $params[
'slots'] ===
null ) {
218 $difftext = $de->getDiffBody();
219 if ( $difftext ===
false ) {
224 foreach ( $params[
'slots'] as $role ) {
225 $difftext[$role] = $de->getDiffBodyForRole( $role );
228 foreach ( $de->getRevisionLoadErrors() as $msg ) {
234 $this->setVals( $vals,
'from', $fromValsRev );
236 $this->setVals( $vals,
'to', $toValsRev );
238 if ( isset( $this->props[
'rel'] ) ) {
240 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
242 $vals[
'prev'] = $rev->getId();
246 $rev = $this->revisionStore->getNextRevision( $toRev );
248 $vals[
'next'] = $rev->getId();
253 if ( isset( $this->props[
'diffsize'] ) ) {
254 $vals[
'diffsize'] = 0;
255 foreach ( (array)$difftext as $text ) {
256 $vals[
'diffsize'] += strlen( $text );
259 if ( isset( $this->props[
'diff'] ) ) {
260 if ( is_array( $difftext ) ) {
261 ApiResult::setArrayType( $difftext,
'kvp',
'diff' );
262 $vals[
'bodies'] = $difftext;
264 ApiResult::setContentValue( $vals,
'body', $difftext );
282 private function getRevisionById( $id ) {
283 $rev = $this->revisionStore->getRevisionById( $id );
284 if ( !$rev && $this->
getAuthority()->isAllowedAny(
'deletedtext',
'undelete' ) ) {
286 $arQuery = $this->revisionStore->getArchiveQueryInfo();
287 $row = $this->
getDB()->selectRow(
291 [
'ar_namespace',
'ar_title' ]
293 [
'ar_rev_id' => $id ],
299 $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
310 private function guessTitle() {
311 if ( $this->guessedTitle !==
false ) {
312 return $this->guessedTitle;
315 $this->guessedTitle =
null;
318 foreach ( [
'from',
'to' ] as $prefix ) {
319 if ( $params[
"{$prefix}rev"] !==
null ) {
320 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
322 $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
327 if ( $params[
"{$prefix}title"] !==
null ) {
328 $title = Title::newFromText( $params[
"{$prefix}title"] );
330 $this->guessedTitle =
$title;
335 if ( $params[
"{$prefix}id"] !==
null ) {
336 $title = Title::newFromID( $params[
"{$prefix}id"] );
338 $this->guessedTitle =
$title;
344 return $this->guessedTitle;
352 private function guessModel( $role ) {
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();
364 $guessedTitle = $this->guessTitle();
365 if ( $guessedTitle ) {
366 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
369 if ( isset( $params[
"fromcontentmodel-$role"] ) ) {
370 return $params[
"fromcontentmodel-$role"];
372 if ( isset( $params[
"tocontentmodel-$role"] ) ) {
373 return $params[
"tocontentmodel-$role"];
376 if ( $role === SlotRecord::MAIN ) {
377 if ( isset( $params[
'fromcontentmodel'] ) ) {
378 return $params[
'fromcontentmodel'];
380 if ( isset( $params[
'tocontentmodel'] ) ) {
381 return $params[
'tocontentmodel'];
403 private function getDiffRevision( $prefix, array $params ) {
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"];
417 $suppliedContent = $params[
"{$prefix}slots"] !==
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"] );
432 $title = Title::newFromID( $params[
"{$prefix}id"] );
434 $this->
dieWithError( [
'apierror-nosuchpageid', $params[
"{$prefix}id"] ] );
437 $revId =
$title->getLatestRevID();
441 if ( !$suppliedContent ) {
456 if ( $revId !==
null ) {
457 $rev = $this->getRevisionById( $revId );
459 $this->
dieWithError( [
'apierror-nosuchrevid', $revId ] );
461 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
465 if ( !$suppliedContent ) {
469 if ( isset( $params[
"{$prefix}section"] ) ) {
470 $section = $params[
"{$prefix}section"];
472 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
473 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
477 [
'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ],
'missingcontent'
483 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
484 "nosuch{$prefix}section"
488 $newRev->setContent( SlotRecord::MAIN,
$content );
491 return [ $newRev, $rev, $rev ];
497 $title = $this->guessTitle();
500 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
504 foreach ( $params[
"{$prefix}slots"] as $role ) {
505 $text = $params[
"{$prefix}text-{$role}"];
506 if ( $text ===
null ) {
508 if ( $role === SlotRecord::MAIN ) {
509 $this->
dieWithError( [
'apierror-compare-maintextrequired', $prefix ] );
514 foreach ( [
'section',
'contentmodel',
'contentformat' ] as $param ) {
515 if ( isset( $params[
"{$prefix}{$param}-{$role}"] ) ) {
517 'apierror-compare-notext',
524 $newRev->removeSlot( $role );
528 $model = $params[
"{$prefix}contentmodel-{$role}"];
529 $format = $params[
"{$prefix}contentformat-{$role}"];
531 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
532 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
534 if ( !$model &&
$title && $role === SlotRecord::MAIN ) {
536 $model =
$title->getContentModel();
539 $model = $this->guessModel( $role );
543 $this->
addWarning( [
'apiwarn-compare-nocontentmodel', $model ] );
547 $content = $this->contentHandlerFactory
548 ->getContentHandler( $model )
549 ->unserializeContent( $text, $format );
552 'wrap' => ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
556 if ( $params[
"{$prefix}pst"] ) {
561 $content = $this->contentTransformer->preSaveTransform(
570 $section = $params[
"{$prefix}section-{$role}"];
571 if ( $section !==
null && $section !==
'' ) {
573 $this->
dieWithError(
"apierror-compare-no{$prefix}revision" );
575 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
576 if ( !$oldContent ) {
578 [
'apierror-missingcontent-revid-role', $rev->getId(),
wfEscapeWikiText( $role ) ],
582 if ( !$oldContent->getContentHandler()->supportsSections() ) {
588 }
catch ( TimeoutException $e ) {
590 }
catch ( Exception $ex ) {
595 $this->
dieWithError( [
'apierror-sectionreplacefailed' ] );
600 if ( $role === SlotRecord::MAIN && isset( $params[
"{$prefix}section"] ) ) {
601 $section = $params[
"{$prefix}section"];
605 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
606 "nosuch{$prefix}section"
612 $newRev->setContent( $role,
$content );
614 return [ $newRev, $rev, null ];
624 private function setVals( &$vals, $prefix, $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();
631 if ( isset( $this->props[
'title'] ) ) {
634 if ( isset( $this->props[
'size'] ) ) {
635 $vals[
"{$prefix}size"] = $rev->getSize();
637 if ( isset( $this->props[
'timestamp'] ) ) {
638 $revTimestamp = $rev->getTimestamp();
639 if ( $revTimestamp ) {
640 $vals[
"{$prefix}timestamp"] =
wfTimestamp( TS_ISO_8601, $revTimestamp );
645 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
646 $vals[
"{$prefix}texthidden"] =
true;
650 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
651 $vals[
"{$prefix}userhidden"] =
true;
654 if ( isset( $this->props[
'user'] ) ) {
655 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
657 $vals[
"{$prefix}user"] = $user->getName();
658 $vals[
"{$prefix}userid"] = $user->getId();
662 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
663 $vals[
"{$prefix}commenthidden"] =
true;
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;
672 $vals[
"{$prefix}parsedcomment"] = $this->commentFormatter->format(
679 $this->
getMain()->setCacheMode(
'private' );
680 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
681 $vals[
"{$prefix}suppressed"] =
true;
686 $this->
getMain()->setCacheMode(
'private' );
687 $vals[
"{$prefix}archive"] =
true;
693 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
694 sort( $slotRoles, SORT_STRING );
700 ParamValidator::PARAM_TYPE =>
'integer'
703 ParamValidator::PARAM_TYPE =>
'integer'
707 ParamValidator::PARAM_TYPE => $slotRoles,
708 ParamValidator::PARAM_ISMULTI =>
true,
712 ParamValidator::PARAM_TYPE =>
'text',
714 'section-{slot}' => [
716 ParamValidator::PARAM_TYPE =>
'string',
718 'contentformat-{slot}' => [
720 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
722 'contentmodel-{slot}' => [
724 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
729 ParamValidator::PARAM_TYPE =>
'text',
730 ParamValidator::PARAM_DEPRECATED =>
true,
733 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
734 ParamValidator::PARAM_DEPRECATED =>
true,
737 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
738 ParamValidator::PARAM_DEPRECATED =>
true,
741 ParamValidator::PARAM_DEFAULT =>
null,
742 ParamValidator::PARAM_DEPRECATED =>
true,
747 foreach ( $fromToParams as $k => $v ) {
753 foreach ( $fromToParams as $k => $v ) {
762 [
'torelative' => [ ParamValidator::PARAM_TYPE => [
'prev',
'next',
'cur' ], ] ],
767 ParamValidator::PARAM_DEFAULT =>
'diff|ids|title',
768 ParamValidator::PARAM_TYPE => [
780 ParamValidator::PARAM_ISMULTI =>
true,
785 ParamValidator::PARAM_TYPE => $slotRoles,
786 ParamValidator::PARAM_ISMULTI =>
true,
787 ParamValidator::PARAM_ALL =>
true,
791 if ( $this->inlineSupported ) {
793 ParamValidator::PARAM_TYPE => [
'table',
'inline' ],
794 ParamValidator::PARAM_DEFAULT =>
'table',
803 'action=compare&fromrev=1&torev=2'
804 =>
'apihelp-compare-example-1',
809 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
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.
getContext()
Get the base IContextSource object.
An IContextSource implementation which will inherit context from another source but allow individual ...
Exception representing a failure to serialize or unserialize a content object.