35 use Wikimedia\RequestTimeout\TimeoutException;
47 private $guessedTitle =
false;
81 parent::__construct( $mainModule, $moduleName );
82 $this->revisionStore = $revisionStore;
83 $this->archivedRevisionLookup = $archivedRevisionLookup;
84 $this->slotRoleRegistry = $slotRoleRegistry;
85 $this->contentHandlerFactory = $contentHandlerFactory;
86 $this->contentTransformer = $contentTransformer;
87 $this->commentFormatter = $commentFormatter;
88 $this->tempUserCreator = $tempUserCreator;
89 $this->userFactory = $userFactory;
98 $params,
'fromtitle',
'fromid',
'fromrev',
'fromtext',
'fromslots'
101 $params,
'totitle',
'toid',
'torev',
'totext',
'torelative',
'toslots'
104 $this->props = array_fill_keys( $params[
'prop'],
true );
107 $this->
getMain()->setCacheMode(
'public' );
110 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision(
'from', $params );
113 if ( $params[
'torelative'] !==
null ) {
114 if ( !$fromRelRev ) {
115 $this->
dieWithError(
'apierror-compare-relative-to-nothing' );
120 $this->
dieWithError( [
'apierror-compare-relative-to-deleted', $params[
'torelative'] ] );
122 switch ( $params[
'torelative'] ) {
125 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
126 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
127 $fromRelRev = $fromRev;
128 $fromValsRev = $fromRev;
130 $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
132 'apiwarn-compare-no-prev',
140 $title ?: $toRev->getPage()
142 $fromRev->setContent(
144 $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
145 ->getContentHandler()
152 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
156 $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
158 'apiwarn-compare-no-next',
165 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
170 $title = $fromRelRev->getPageAsLinkTarget();
171 $toRev = $this->revisionStore->getRevisionByTitle( $title );
173 $title = Title::newFromLinkTarget( $title );
175 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
184 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision(
'to', $params );
190 if ( !$fromRev || !$toRev ) {
196 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
197 $this->
dieWithError( [
'apierror-missingcontent-revid', $fromRev->getId() ],
'missingcontent' );
199 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
200 $this->
dieWithError( [
'apierror-missingcontent-revid', $toRev->getId() ],
'missingcontent' );
205 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
206 $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
208 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
209 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
211 $guessedTitle = $this->guessTitle();
212 if ( $guessedTitle ) {
213 $context->setTitle( $guessedTitle );
216 $this->differenceEngine->setContext( $context );
217 $this->differenceEngine->setSlotDiffOptions( [
'diff-type' => $params[
'difftype'] ] );
218 $this->differenceEngine->setRevisions( $fromRev, $toRev );
219 if ( $params[
'slots'] ===
null ) {
220 $difftext = $this->differenceEngine->getDiffBody();
221 if ( $difftext ===
false ) {
226 foreach ( $params[
'slots'] as $role ) {
227 $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
230 foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
236 $this->setVals( $vals,
'from', $fromValsRev );
238 $this->setVals( $vals,
'to', $toValsRev );
240 if ( isset( $this->props[
'rel'] ) ) {
242 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
244 $vals[
'prev'] = $rev->getId();
248 $rev = $this->revisionStore->getNextRevision( $toRev );
250 $vals[
'next'] = $rev->getId();
255 if ( isset( $this->props[
'diffsize'] ) ) {
256 $vals[
'diffsize'] = 0;
257 foreach ( (array)$difftext as $text ) {
258 $vals[
'diffsize'] += strlen( $text );
261 if ( isset( $this->props[
'diff'] ) ) {
262 if ( is_array( $difftext ) ) {
264 $vals[
'bodies'] = $difftext;
284 private function getRevisionById( $id ) {
285 $rev = $this->revisionStore->getRevisionById( $id );
286 if ( !$rev && $this->
getAuthority()->isAllowedAny(
'deletedtext',
'undelete' ) ) {
288 $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord(
null, $id );
298 private function guessTitle() {
299 if ( $this->guessedTitle !==
false ) {
300 return $this->guessedTitle;
303 $this->guessedTitle =
null;
306 foreach ( [
'from',
'to' ] as $prefix ) {
307 if ( $params[
"{$prefix}rev"] !==
null ) {
308 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
310 $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
315 if ( $params[
"{$prefix}title"] !==
null ) {
316 $title = Title::newFromText( $params[
"{$prefix}title"] );
317 if ( $title && !$title->isExternal() ) {
318 $this->guessedTitle = $title;
323 if ( $params[
"{$prefix}id"] !==
null ) {
324 $title = Title::newFromID( $params[
"{$prefix}id"] );
326 $this->guessedTitle = $title;
332 return $this->guessedTitle;
340 private function guessModel( $role ) {
343 foreach ( [
'from',
'to' ] as $prefix ) {
344 if ( $params[
"{$prefix}rev"] !==
null ) {
345 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
346 if ( $rev && $rev->hasSlot( $role ) ) {
347 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
352 $guessedTitle = $this->guessTitle();
353 if ( $guessedTitle ) {
354 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
357 if ( isset( $params[
"fromcontentmodel-$role"] ) ) {
358 return $params[
"fromcontentmodel-$role"];
360 if ( isset( $params[
"tocontentmodel-$role"] ) ) {
361 return $params[
"tocontentmodel-$role"];
364 if ( $role === SlotRecord::MAIN ) {
365 if ( isset( $params[
'fromcontentmodel'] ) ) {
366 return $params[
'fromcontentmodel'];
368 if ( isset( $params[
'tocontentmodel'] ) ) {
369 return $params[
'tocontentmodel'];
391 private function getDiffRevision( $prefix, array $params ) {
395 if ( $params[
"{$prefix}text"] !==
null ) {
396 $params[
"{$prefix}slots"] = [ SlotRecord::MAIN ];
397 $params[
"{$prefix}text-main"] = $params[
"{$prefix}text"];
398 $params[
"{$prefix}section-main"] =
null;
399 $params[
"{$prefix}contentmodel-main"] = $params[
"{$prefix}contentmodel"];
400 $params[
"{$prefix}contentformat-main"] = $params[
"{$prefix}contentformat"];
405 $suppliedContent = $params[
"{$prefix}slots"] !==
null;
409 if ( $params[
"{$prefix}rev"] !==
null ) {
410 $revId = $params[
"{$prefix}rev"];
411 } elseif ( $params[
"{$prefix}title"] !==
null || $params[
"{$prefix}id"] !==
null ) {
412 if ( $params[
"{$prefix}title"] !==
null ) {
413 $title = Title::newFromText( $params[
"{$prefix}title"] );
414 if ( !$title || $title->isExternal() ) {
420 $title = Title::newFromID( $params[
"{$prefix}id"] );
422 $this->
dieWithError( [
'apierror-nosuchpageid', $params[
"{$prefix}id"] ] );
425 $revId = $title->getLatestRevID();
429 if ( !$suppliedContent ) {
430 if ( $title->exists() ) {
432 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
437 [
'apierror-missingtitle-byname',
wfEscapeWikiText( $title->getPrefixedText() ) ],
444 if ( $revId !==
null ) {
445 $rev = $this->getRevisionById( $revId );
447 $this->
dieWithError( [
'apierror-nosuchrevid', $revId ] );
449 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
453 if ( !$suppliedContent ) {
457 if ( isset( $params[
"{$prefix}section"] ) ) {
458 $section = $params[
"{$prefix}section"];
460 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
461 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
465 [
'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ],
'missingcontent'
471 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
472 "nosuch{$prefix}section"
476 $newRev->setContent( SlotRecord::MAIN,
$content );
479 return [ $newRev, $rev, $rev ];
485 $title = $this->guessTitle();
488 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
492 foreach ( $params[
"{$prefix}slots"] as $role ) {
493 $text = $params[
"{$prefix}text-{$role}"];
494 if ( $text ===
null ) {
496 if ( $role === SlotRecord::MAIN ) {
497 $this->
dieWithError( [
'apierror-compare-maintextrequired', $prefix ] );
502 foreach ( [
'section',
'contentmodel',
'contentformat' ] as $param ) {
503 if ( isset( $params[
"{$prefix}{$param}-{$role}"] ) ) {
505 'apierror-compare-notext',
512 $newRev->removeSlot( $role );
516 $model = $params[
"{$prefix}contentmodel-{$role}"];
517 $format = $params[
"{$prefix}contentformat-{$role}"];
519 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
520 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
522 if ( !$model && $title && $role === SlotRecord::MAIN ) {
524 $model = $title->getContentModel();
527 $model = $this->guessModel( $role );
531 $this->
addWarning( [
'apiwarn-compare-nocontentmodel', $model ] );
535 $content = $this->contentHandlerFactory
536 ->getContentHandler( $model )
537 ->unserializeContent( $text, $format );
540 'wrap' =>
ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
544 if ( $params[
"{$prefix}pst"] ) {
549 $content = $this->contentTransformer->preSaveTransform(
553 $this->getUserForPreview(),
558 $section = $params[
"{$prefix}section-{$role}"];
559 if ( $section !==
null && $section !==
'' ) {
561 $this->
dieWithError(
"apierror-compare-no{$prefix}revision" );
563 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
564 if ( !$oldContent ) {
566 [
'apierror-missingcontent-revid-role', $rev->getId(),
wfEscapeWikiText( $role ) ],
570 if ( !$oldContent->getContentHandler()->supportsSections() ) {
576 }
catch ( TimeoutException $e ) {
578 }
catch ( Exception $ex ) {
583 $this->
dieWithError( [
'apierror-sectionreplacefailed' ] );
588 if ( $role === SlotRecord::MAIN && isset( $params[
"{$prefix}section"] ) ) {
589 $section = $params[
"{$prefix}section"];
593 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
594 "nosuch{$prefix}section"
600 $newRev->setContent( $role,
$content );
602 return [ $newRev, $rev, null ];
612 private function setVals( &$vals, $prefix, $rev ) {
614 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
615 if ( isset( $this->props[
'ids'] ) ) {
616 $vals[
"{$prefix}id"] = $title->getArticleID();
617 $vals[
"{$prefix}revid"] = $rev->getId();
619 if ( isset( $this->props[
'title'] ) ) {
622 if ( isset( $this->props[
'size'] ) ) {
623 $vals[
"{$prefix}size"] = $rev->getSize();
625 if ( isset( $this->props[
'timestamp'] ) ) {
626 $revTimestamp = $rev->getTimestamp();
627 if ( $revTimestamp ) {
628 $vals[
"{$prefix}timestamp"] =
wfTimestamp( TS_ISO_8601, $revTimestamp );
633 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
634 $vals[
"{$prefix}texthidden"] =
true;
638 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
639 $vals[
"{$prefix}userhidden"] =
true;
642 if ( isset( $this->props[
'user'] ) ) {
643 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
645 $vals[
"{$prefix}user"] = $user->getName();
646 $vals[
"{$prefix}userid"] = $user->getId();
650 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
651 $vals[
"{$prefix}commenthidden"] =
true;
654 if ( isset( $this->props[
'comment'] ) || isset( $this->props[
'parsedcomment'] ) ) {
655 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
656 if ( $comment !==
null ) {
657 if ( isset( $this->props[
'comment'] ) ) {
658 $vals[
"{$prefix}comment"] = $comment->text;
660 $vals[
"{$prefix}parsedcomment"] = $this->commentFormatter->format(
661 $comment->text, $title
667 $this->
getMain()->setCacheMode(
'private' );
668 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
669 $vals[
"{$prefix}suppressed"] =
true;
674 $this->
getMain()->setCacheMode(
'private' );
675 $vals[
"{$prefix}archive"] =
true;
680 private function getUserForPreview() {
682 if ( $this->tempUserCreator->shouldAutoCreate( $user,
'edit' ) ) {
683 return $this->userFactory->newUnsavedTempUser(
684 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
691 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
692 sort( $slotRoles, SORT_STRING );
698 ParamValidator::PARAM_TYPE =>
'integer'
701 ParamValidator::PARAM_TYPE =>
'integer'
705 ParamValidator::PARAM_TYPE => $slotRoles,
706 ParamValidator::PARAM_ISMULTI =>
true,
710 ParamValidator::PARAM_TYPE =>
'text',
712 'section-{slot}' => [
714 ParamValidator::PARAM_TYPE =>
'string',
716 'contentformat-{slot}' => [
718 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
720 'contentmodel-{slot}' => [
722 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
727 ParamValidator::PARAM_TYPE =>
'text',
728 ParamValidator::PARAM_DEPRECATED =>
true,
731 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
732 ParamValidator::PARAM_DEPRECATED =>
true,
735 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
736 ParamValidator::PARAM_DEPRECATED =>
true,
739 ParamValidator::PARAM_DEFAULT =>
null,
740 ParamValidator::PARAM_DEPRECATED =>
true,
745 foreach ( $fromToParams as $k => $v ) {
751 foreach ( $fromToParams as $k => $v ) {
760 [
'torelative' => [ ParamValidator::PARAM_TYPE => [
'prev',
'next',
'cur' ], ] ],
765 ParamValidator::PARAM_DEFAULT =>
'diff|ids|title',
766 ParamValidator::PARAM_TYPE => [
778 ParamValidator::PARAM_ISMULTI =>
true,
783 ParamValidator::PARAM_TYPE => $slotRoles,
784 ParamValidator::PARAM_ISMULTI =>
true,
785 ParamValidator::PARAM_ALL =>
true,
789 ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
790 ParamValidator::PARAM_DEFAULT =>
'table',
798 'action=compare&fromrev=1&torev=2'
799 =>
'apihelp-compare-example-1',
804 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
const CONTENT_MODEL_WIKITEXT
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
getMain()
Get the main module.
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
const PARAM_TEMPLATE_VARS
(array) Indicate that this is a templated parameter, and specify replacements.
getResult()
Get the result object.
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
getModuleName()
Get the name of the module being executed by this instance.
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
getHelpUrls()
Return links to more detailed help pages about the module.
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiMain $mainModule, $moduleName, RevisionStore $revisionStore, ArchivedRevisionLookup $archivedRevisionLookup, SlotRoleRegistry $slotRoleRegistry, IContentHandlerFactory $contentHandlerFactory, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter, TempUserCreator $tempUserCreator, UserFactory $userFactory)
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.
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
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.
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don't use this unless yo...
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
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.
A service to transform content.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.