54 private $guessedTitle =
false;
77 parent::__construct( $mainModule, $moduleName );
78 $this->revisionStore = $revisionStore;
79 $this->archivedRevisionLookup = $archivedRevisionLookup;
80 $this->slotRoleRegistry = $slotRoleRegistry;
81 $this->contentHandlerFactory = $contentHandlerFactory;
82 $this->contentTransformer = $contentTransformer;
83 $this->commentFormatter = $commentFormatter;
84 $this->tempUserCreator = $tempUserCreator;
85 $this->userFactory = $userFactory;
94 $params,
'fromtitle',
'fromid',
'fromrev',
'fromtext',
'fromslots'
97 $params,
'totitle',
'toid',
'torev',
'totext',
'torelative',
'toslots'
100 $this->props = array_fill_keys( $params[
'prop'],
true );
103 $this->
getMain()->setCacheMode(
'public' );
106 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision(
'from', $params );
109 if ( $params[
'torelative'] !==
null ) {
110 if ( !$fromRelRev ) {
111 $this->
dieWithError(
'apierror-compare-relative-to-nothing' );
116 $this->
dieWithError( [
'apierror-compare-relative-to-deleted', $params[
'torelative'] ] );
118 switch ( $params[
'torelative'] ) {
121 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
122 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
123 $fromRelRev = $fromRev;
124 $fromValsRev = $fromRev;
126 $title = Title::newFromPageIdentity( $toRelRev->getPage() );
128 'apiwarn-compare-no-prev',
136 $fromRev->setContent(
138 $toRelRev->getMainContentRaw()
139 ->getContentHandler()
146 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
150 $title = Title::newFromPageIdentity( $fromRelRev->getPage() );
152 'apiwarn-compare-no-next',
159 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
164 $title = $fromRelRev->getPageAsLinkTarget();
165 $toRev = $this->revisionStore->getRevisionByTitle( $title );
167 $title = Title::newFromLinkTarget( $title );
169 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
178 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision(
'to', $params );
184 if ( !$fromRev || !$toRev ) {
190 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
191 $this->
dieWithError( [
'apierror-missingcontent-revid', $fromRev->getId() ],
'missingcontent' );
193 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
194 $this->
dieWithError( [
'apierror-missingcontent-revid', $toRev->getId() ],
'missingcontent' );
199 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
200 $context->setTitle( Title::newFromPageIdentity( $fromRelRev->getPage() ) );
202 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
203 $context->setTitle( Title::newFromPageIdentity( $toRelRev->getPage() ) );
205 $guessedTitle = $this->guessTitle();
206 if ( $guessedTitle ) {
207 $context->setTitle( $guessedTitle );
210 $this->differenceEngine->setContext( $context );
211 $this->differenceEngine->setSlotDiffOptions( [
'diff-type' => $params[
'difftype'] ] );
212 $this->differenceEngine->setRevisions( $fromRev, $toRev );
213 if ( $params[
'slots'] ===
null ) {
214 $difftext = $this->differenceEngine->getDiffBody();
215 if ( $difftext ===
false ) {
220 foreach ( $params[
'slots'] as $role ) {
221 $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
224 foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
230 $this->setVals( $vals,
'from', $fromValsRev );
232 $this->setVals( $vals,
'to', $toValsRev );
234 if ( isset( $this->props[
'rel'] ) ) {
236 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
238 $vals[
'prev'] = $rev->getId();
242 $rev = $this->revisionStore->getNextRevision( $toRev );
244 $vals[
'next'] = $rev->getId();
249 if ( isset( $this->props[
'diffsize'] ) ) {
250 $vals[
'diffsize'] = 0;
251 foreach ( (array)$difftext as $text ) {
252 $vals[
'diffsize'] += strlen( $text );
255 if ( isset( $this->props[
'diff'] ) ) {
256 if ( is_array( $difftext ) ) {
258 $vals[
'bodies'] = $difftext;
278 private function getRevisionById( $id ) {
279 $rev = $this->revisionStore->getRevisionById( $id );
280 if ( !$rev && $this->
getAuthority()->isAllowedAny(
'deletedtext',
'undelete' ) ) {
282 $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord(
null, $id );
292 private function guessTitle() {
293 if ( $this->guessedTitle !==
false ) {
294 return $this->guessedTitle;
297 $this->guessedTitle =
null;
300 foreach ( [
'from',
'to' ] as $prefix ) {
301 if ( $params[
"{$prefix}rev"] !==
null ) {
302 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
304 $this->guessedTitle = Title::newFromPageIdentity( $rev->getPage() );
309 if ( $params[
"{$prefix}title"] !==
null ) {
310 $title = Title::newFromText( $params[
"{$prefix}title"] );
311 if ( $title && !$title->isExternal() ) {
312 $this->guessedTitle = $title;
317 if ( $params[
"{$prefix}id"] !==
null ) {
318 $title = Title::newFromID( $params[
"{$prefix}id"] );
320 $this->guessedTitle = $title;
326 return $this->guessedTitle;
334 private function guessModel( $role ) {
337 foreach ( [
'from',
'to' ] as $prefix ) {
338 if ( $params[
"{$prefix}rev"] !==
null ) {
339 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
340 if ( $rev && $rev->hasSlot( $role ) ) {
341 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
346 $guessedTitle = $this->guessTitle();
347 if ( $guessedTitle ) {
348 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
351 if ( isset( $params[
"fromcontentmodel-$role"] ) ) {
352 return $params[
"fromcontentmodel-$role"];
354 if ( isset( $params[
"tocontentmodel-$role"] ) ) {
355 return $params[
"tocontentmodel-$role"];
358 if ( $role === SlotRecord::MAIN ) {
359 if ( isset( $params[
'fromcontentmodel'] ) ) {
360 return $params[
'fromcontentmodel'];
362 if ( isset( $params[
'tocontentmodel'] ) ) {
363 return $params[
'tocontentmodel'];
385 private function getDiffRevision( $prefix, array $params ) {
389 if ( $params[
"{$prefix}text"] !==
null ) {
390 $params[
"{$prefix}slots"] = [ SlotRecord::MAIN ];
392 $params[
"{$prefix}section-main"] =
null;
393 $params[
"{$prefix}contentmodel-main"] =
$params[
"{$prefix}contentmodel"];
394 $params[
"{$prefix}contentformat-main"] =
$params[
"{$prefix}contentformat"];
399 $suppliedContent =
$params[
"{$prefix}slots"] !==
null;
403 if ( $params[
"{$prefix}rev"] !==
null ) {
404 $revId =
$params[
"{$prefix}rev"];
405 } elseif ( $params[
"{$prefix}title"] !==
null || $params[
"{$prefix}id"] !==
null ) {
406 if ( $params[
"{$prefix}title"] !==
null ) {
407 $title = Title::newFromText( $params[
"{$prefix}title"] );
408 if ( !$title || $title->isExternal() ) {
414 $title = Title::newFromID( $params[
"{$prefix}id"] );
416 $this->
dieWithError( [
'apierror-nosuchpageid', $params[
"{$prefix}id"] ] );
419 $revId = $title->getLatestRevID();
423 if ( !$suppliedContent ) {
424 if ( $title->exists() ) {
426 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
431 [
'apierror-missingtitle-byname',
wfEscapeWikiText( $title->getPrefixedText() ) ],
438 if ( $revId !==
null ) {
439 $rev = $this->getRevisionById( $revId );
441 $this->
dieWithError( [
'apierror-nosuchrevid', $revId ] );
443 $title = Title::newFromPageIdentity( $rev->getPage() );
447 if ( !$suppliedContent ) {
451 if ( isset( $params[
"{$prefix}section"] ) ) {
452 $section =
$params[
"{$prefix}section"];
454 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
455 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
459 [
'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ],
'missingcontent'
462 $content = $content->getSection( $section );
465 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
466 "nosuch{$prefix}section"
470 $newRev->setContent( SlotRecord::MAIN, $content );
473 return [ $newRev, $rev, $rev ];
479 $title = $this->guessTitle();
482 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
484 $newRev =
new MutableRevisionRecord( $title ?: Title::newMainPage() );
486 foreach ( $params[
"{$prefix}slots"] as $role ) {
487 $text =
$params[
"{$prefix}text-{$role}"];
488 if ( $text ===
null ) {
490 if ( $role === SlotRecord::MAIN ) {
491 $this->
dieWithError( [
'apierror-compare-maintextrequired', $prefix ] );
496 foreach ( [
'section',
'contentmodel',
'contentformat' ] as $param ) {
497 if ( isset( $params[
"{$prefix}{$param}-{$role}"] ) ) {
499 'apierror-compare-notext',
506 $newRev->removeSlot( $role );
510 $model =
$params[
"{$prefix}contentmodel-{$role}"];
511 $format =
$params[
"{$prefix}contentformat-{$role}"];
513 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
514 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
516 if ( !$model && $title && $role === SlotRecord::MAIN ) {
518 $model = $title->getContentModel();
521 $model = $this->guessModel( $role );
525 $this->
addWarning( [
'apiwarn-compare-nocontentmodel', $model ] );
529 $content = $this->contentHandlerFactory
530 ->getContentHandler( $model )
531 ->unserializeContent( $text, $format );
534 'wrap' =>
ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
538 if ( $params[
"{$prefix}pst"] ) {
542 $popts = ParserOptions::newFromContext( $this->
getContext() );
543 $content = $this->contentTransformer->preSaveTransform(
547 $this->getUserForPreview(),
552 $section =
$params[
"{$prefix}section-{$role}"];
553 if ( $section !==
null && $section !==
'' ) {
555 $this->
dieWithError(
"apierror-compare-no{$prefix}revision" );
557 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
558 if ( !$oldContent ) {
560 [
'apierror-missingcontent-revid-role', $rev->getId(),
wfEscapeWikiText( $role ) ],
564 if ( !$oldContent->getContentHandler()->supportsSections() ) {
565 $this->
dieWithError( [
'apierror-sectionsnotsupported', $content->getModel() ] );
569 $content = $oldContent->replaceSection( $section, $content,
'' );
570 }
catch ( TimeoutException $e ) {
572 }
catch ( Exception $ex ) {
577 $this->
dieWithError( [
'apierror-sectionreplacefailed' ] );
582 if ( $role === SlotRecord::MAIN && isset( $params[
"{$prefix}section"] ) ) {
583 $section =
$params[
"{$prefix}section"];
584 $content = $content->getSection( $section );
587 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
588 "nosuch{$prefix}section"
594 $newRev->setContent( $role, $content );
596 return [ $newRev, $rev, null ];
606 private function setVals( &$vals, $prefix, $rev ) {
608 $title = Title::newFromPageIdentity( $rev->getPage() );
609 if ( isset( $this->props[
'ids'] ) ) {
610 $vals[
"{$prefix}id"] = $title->getArticleID();
611 $vals[
"{$prefix}revid"] = $rev->getId();
613 if ( isset( $this->props[
'title'] ) ) {
616 if ( isset( $this->props[
'size'] ) ) {
617 $vals[
"{$prefix}size"] = $rev->getSize();
619 if ( isset( $this->props[
'timestamp'] ) ) {
620 $revTimestamp = $rev->getTimestamp();
621 if ( $revTimestamp ) {
622 $vals[
"{$prefix}timestamp"] =
wfTimestamp( TS_ISO_8601, $revTimestamp );
627 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
628 $vals[
"{$prefix}texthidden"] =
true;
632 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
633 $vals[
"{$prefix}userhidden"] =
true;
636 if ( isset( $this->props[
'user'] ) ) {
637 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
639 $vals[
"{$prefix}user"] = $user->getName();
640 $vals[
"{$prefix}userid"] = $user->getId();
644 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
645 $vals[
"{$prefix}commenthidden"] =
true;
648 if ( isset( $this->props[
'comment'] ) || isset( $this->props[
'parsedcomment'] ) ) {
649 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
650 if ( $comment !==
null ) {
651 if ( isset( $this->props[
'comment'] ) ) {
652 $vals[
"{$prefix}comment"] = $comment->text;
654 $vals[
"{$prefix}parsedcomment"] = $this->commentFormatter->format(
655 $comment->text, $title
661 $this->
getMain()->setCacheMode(
'private' );
662 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
663 $vals[
"{$prefix}suppressed"] =
true;
667 if ( $rev instanceof RevisionArchiveRecord ) {
668 $this->
getMain()->setCacheMode(
'private' );
669 $vals[
"{$prefix}archive"] =
true;
674 private function getUserForPreview() {
676 if ( $this->tempUserCreator->shouldAutoCreate( $user,
'edit' ) ) {
677 return $this->userFactory->newUnsavedTempUser(
678 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
685 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
686 sort( $slotRoles, SORT_STRING );
692 ParamValidator::PARAM_TYPE =>
'integer'
695 ParamValidator::PARAM_TYPE =>
'integer'
699 ParamValidator::PARAM_TYPE => $slotRoles,
700 ParamValidator::PARAM_ISMULTI =>
true,
704 ParamValidator::PARAM_TYPE =>
'text',
706 'section-{slot}' => [
708 ParamValidator::PARAM_TYPE =>
'string',
710 'contentformat-{slot}' => [
712 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
714 'contentmodel-{slot}' => [
716 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
721 ParamValidator::PARAM_TYPE =>
'text',
722 ParamValidator::PARAM_DEPRECATED =>
true,
725 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
726 ParamValidator::PARAM_DEPRECATED =>
true,
729 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
730 ParamValidator::PARAM_DEPRECATED =>
true,
733 ParamValidator::PARAM_DEFAULT =>
null,
734 ParamValidator::PARAM_DEPRECATED =>
true,
739 foreach ( $fromToParams as $k => $v ) {
745 foreach ( $fromToParams as $k => $v ) {
754 [
'torelative' => [ ParamValidator::PARAM_TYPE => [
'prev',
'next',
'cur' ], ] ],
759 ParamValidator::PARAM_DEFAULT =>
'diff|ids|title',
760 ParamValidator::PARAM_TYPE => [
772 ParamValidator::PARAM_ISMULTI =>
true,
777 ParamValidator::PARAM_TYPE => $slotRoles,
778 ParamValidator::PARAM_ISMULTI =>
true,
779 ParamValidator::PARAM_ALL =>
true,
783 ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
784 ParamValidator::PARAM_DEFAULT =>
'table',
792 'action=compare&fromrev=1&torev=2'
793 =>
'apihelp-compare-example-1',
798 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';