31use Wikimedia\RequestTimeout\TimeoutException;
39 private $revisionStore;
42 private $slotRoleRegistry;
45 private $guessedTitle =
false;
49 private $contentHandlerFactory;
52 private $contentTransformer;
55 private $commentFormatter;
57 private bool $inlineSupported;
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';
92 $params,
'fromtitle',
'fromid',
'fromrev',
'fromtext',
'fromslots'
95 $params,
'totitle',
'toid',
'torev',
'totext',
'torelative',
'toslots'
98 $this->props = array_fill_keys( $params[
'prop'],
true );
101 $this->
getMain()->setCacheMode(
'public' );
104 list( $fromRev, $fromRelRev, $fromValsRev ) = $this->getDiffRevision(
'from', $params );
107 if ( $params[
'torelative'] !==
null ) {
108 if ( !$fromRelRev ) {
109 $this->
dieWithError(
'apierror-compare-relative-to-nothing' );
114 $this->
dieWithError( [
'apierror-compare-relative-to-deleted', $params[
'torelative'] ] );
116 switch ( $params[
'torelative'] ) {
119 list( $toRev, $toRelRev, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ];
120 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
121 $fromRelRev = $fromRev;
122 $fromValsRev = $fromRev;
124 $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
126 'apiwarn-compare-no-prev',
134 $title ?: $toRev->getPage()
138 $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
139 ->getContentHandler()
146 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
150 $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
152 'apiwarn-compare-no-next',
159 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
164 $title = $fromRelRev->getPageAsLinkTarget();
165 $toRev = $this->revisionStore->getRevisionByTitle(
$title );
178 list( $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::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
202 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
203 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
205 $guessedTitle = $this->guessTitle();
206 if ( $guessedTitle ) {
207 $context->setTitle( $guessedTitle );
212 if ( $this->inlineSupported ) {
213 $de->setSlotDiffOptions( [
'diff-type' => $params[
'difftype'] ] );
215 $de->setRevisions( $fromRev, $toRev );
216 if ( $params[
'slots'] ===
null ) {
217 $difftext = $de->getDiffBody();
218 if ( $difftext ===
false ) {
223 foreach ( $params[
'slots'] as $role ) {
224 $difftext[$role] = $de->getDiffBodyForRole( $role );
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 ) ) {
257 ApiResult::setArrayType( $difftext,
'kvp',
'diff' );
258 $vals[
'bodies'] = $difftext;
260 ApiResult::setContentValue( $vals,
'body', $difftext );
278 private function getRevisionById( $id ) {
279 $rev = $this->revisionStore->getRevisionById( $id );
280 if ( !$rev && $this->
getAuthority()->isAllowedAny(
'deletedtext',
'undelete' ) ) {
282 $arQuery = $this->revisionStore->getArchiveQueryInfo();
283 $row = $this->
getDB()->selectRow(
287 [
'ar_namespace',
'ar_title' ]
289 [
'ar_rev_id' => $id ],
295 $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
306 private function guessTitle() {
307 if ( $this->guessedTitle !==
false ) {
308 return $this->guessedTitle;
311 $this->guessedTitle =
null;
314 foreach ( [
'from',
'to' ] as $prefix ) {
315 if ( $params[
"{$prefix}rev"] !==
null ) {
316 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
323 if ( $params[
"{$prefix}title"] !==
null ) {
326 $this->guessedTitle =
$title;
331 if ( $params[
"{$prefix}id"] !==
null ) {
334 $this->guessedTitle =
$title;
340 return $this->guessedTitle;
348 private function guessModel( $role ) {
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();
360 $guessedTitle = $this->guessTitle();
361 if ( $guessedTitle ) {
362 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
365 if ( isset( $params[
"fromcontentmodel-$role"] ) ) {
366 return $params[
"fromcontentmodel-$role"];
368 if ( isset( $params[
"tocontentmodel-$role"] ) ) {
369 return $params[
"tocontentmodel-$role"];
372 if ( $role === SlotRecord::MAIN ) {
373 if ( isset( $params[
'fromcontentmodel'] ) ) {
374 return $params[
'fromcontentmodel'];
376 if ( isset( $params[
'tocontentmodel'] ) ) {
377 return $params[
'tocontentmodel'];
399 private function getDiffRevision( $prefix, array $params ) {
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"];
413 $suppliedContent = $params[
"{$prefix}slots"] !==
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 ) {
430 $this->
dieWithError( [
'apierror-nosuchpageid', $params[
"{$prefix}id"] ] );
433 $revId =
$title->getLatestRevID();
437 if ( !$suppliedContent ) {
452 if ( $revId !==
null ) {
453 $rev = $this->getRevisionById( $revId );
455 $this->
dieWithError( [
'apierror-nosuchrevid', $revId ] );
461 if ( !$suppliedContent ) {
465 if ( isset( $params[
"{$prefix}section"] ) ) {
466 $section = $params[
"{$prefix}section"];
468 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
469 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
473 [
'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ],
'missingcontent'
479 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
480 "nosuch{$prefix}section"
484 $newRev->setContent( SlotRecord::MAIN,
$content );
487 return [ $newRev, $rev, $rev ];
493 $title = $this->guessTitle();
496 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
500 foreach ( $params[
"{$prefix}slots"] as $role ) {
501 $text = $params[
"{$prefix}text-{$role}"];
502 if ( $text ===
null ) {
504 if ( $role === SlotRecord::MAIN ) {
505 $this->
dieWithError( [
'apierror-compare-maintextrequired', $prefix ] );
510 foreach ( [
'section',
'contentmodel',
'contentformat' ] as $param ) {
511 if ( isset( $params[
"{$prefix}{$param}-{$role}"] ) ) {
513 'apierror-compare-notext',
520 $newRev->removeSlot( $role );
524 $model = $params[
"{$prefix}contentmodel-{$role}"];
525 $format = $params[
"{$prefix}contentformat-{$role}"];
527 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
528 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
530 if ( !$model &&
$title && $role === SlotRecord::MAIN ) {
532 $model =
$title->getContentModel();
535 $model = $this->guessModel( $role );
539 $this->
addWarning( [
'apiwarn-compare-nocontentmodel', $model ] );
543 $content = $this->contentHandlerFactory
544 ->getContentHandler( $model )
545 ->unserializeContent( $text, $format );
548 'wrap' => ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
552 if ( $params[
"{$prefix}pst"] ) {
557 $content = $this->contentTransformer->preSaveTransform(
566 $section = $params[
"{$prefix}section-{$role}"];
567 if ( $section !==
null && $section !==
'' ) {
569 $this->
dieWithError(
"apierror-compare-no{$prefix}revision" );
571 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
572 if ( !$oldContent ) {
574 [
'apierror-missingcontent-revid-role', $rev->getId(),
wfEscapeWikiText( $role ) ],
578 if ( !$oldContent->getContentHandler()->supportsSections() ) {
584 }
catch ( TimeoutException $e ) {
586 }
catch ( Exception $ex ) {
591 $this->
dieWithError( [
'apierror-sectionreplacefailed' ] );
596 if ( $role === SlotRecord::MAIN && isset( $params[
"{$prefix}section"] ) ) {
597 $section = $params[
"{$prefix}section"];
601 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
602 "nosuch{$prefix}section"
608 $newRev->setContent( $role,
$content );
610 return [ $newRev, $rev, null ];
620 private function setVals( &$vals, $prefix, $rev ) {
623 if ( isset( $this->props[
'ids'] ) ) {
624 $vals[
"{$prefix}id"] =
$title->getArticleID();
625 $vals[
"{$prefix}revid"] = $rev->getId();
627 if ( isset( $this->props[
'title'] ) ) {
630 if ( isset( $this->props[
'size'] ) ) {
631 $vals[
"{$prefix}size"] = $rev->getSize();
633 if ( isset( $this->props[
'timestamp'] ) ) {
634 $revTimestamp = $rev->getTimestamp();
635 if ( $revTimestamp ) {
636 $vals[
"{$prefix}timestamp"] =
wfTimestamp( TS_ISO_8601, $revTimestamp );
641 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
642 $vals[
"{$prefix}texthidden"] =
true;
646 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
647 $vals[
"{$prefix}userhidden"] =
true;
650 if ( isset( $this->props[
'user'] ) ) {
651 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
653 $vals[
"{$prefix}user"] = $user->getName();
654 $vals[
"{$prefix}userid"] = $user->getId();
658 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
659 $vals[
"{$prefix}commenthidden"] =
true;
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;
668 $vals[
"{$prefix}parsedcomment"] = $this->commentFormatter->format(
675 $this->
getMain()->setCacheMode(
'private' );
676 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
677 $vals[
"{$prefix}suppressed"] =
true;
682 $this->
getMain()->setCacheMode(
'private' );
683 $vals[
"{$prefix}archive"] =
true;
689 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
690 sort( $slotRoles, SORT_STRING );
696 ParamValidator::PARAM_TYPE =>
'integer'
699 ParamValidator::PARAM_TYPE =>
'integer'
703 ParamValidator::PARAM_TYPE => $slotRoles,
704 ParamValidator::PARAM_ISMULTI =>
true,
708 ParamValidator::PARAM_TYPE =>
'text',
710 'section-{slot}' => [
712 ParamValidator::PARAM_TYPE =>
'string',
714 'contentformat-{slot}' => [
716 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
718 'contentmodel-{slot}' => [
720 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
725 ParamValidator::PARAM_TYPE =>
'text',
726 ParamValidator::PARAM_DEPRECATED =>
true,
729 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
730 ParamValidator::PARAM_DEPRECATED =>
true,
733 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
734 ParamValidator::PARAM_DEPRECATED =>
true,
737 ParamValidator::PARAM_DEFAULT =>
null,
738 ParamValidator::PARAM_DEPRECATED =>
true,
743 foreach ( $fromToParams as $k => $v ) {
749 foreach ( $fromToParams as $k => $v ) {
758 [
'torelative' => [ ParamValidator::PARAM_TYPE => [
'prev',
'next',
'cur' ], ] ],
763 ParamValidator::PARAM_DEFAULT =>
'diff|ids|title',
764 ParamValidator::PARAM_TYPE => [
776 ParamValidator::PARAM_ISMULTI =>
true,
781 ParamValidator::PARAM_TYPE => $slotRoles,
782 ParamValidator::PARAM_ISMULTI =>
true,
783 ParamValidator::PARAM_ALL =>
true,
787 if ( $this->inlineSupported ) {
789 ParamValidator::PARAM_TYPE => [
'table',
'inline' ],
790 ParamValidator::PARAM_DEFAULT =>
'table',
799 'action=compare&fromrev=1&torev=2'
800 =>
'apihelp-compare-example-1',
805 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.
getDB()
Gets a default replica DB connection object.
getMain()
Get the main module.
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
requireAtLeastOneParameter( $params,... $required)
Die if none of a certain set of parameters is set and not false.
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is 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.
__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.
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.
A service to transform content.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Represents a title within MediaWiki.
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.