39 private $guessedTitle =
false;
57 parent::__construct( $mainModule, $moduleName );
66 $params,
'fromtitle',
'fromid',
'fromrev',
'fromtext',
'fromslots'
69 $params,
'totitle',
'toid',
'torev',
'totext',
'torelative',
'toslots'
72 $this->props = array_fill_keys( $params[
'prop'],
true );
75 $this->
getMain()->setCacheMode(
'public' );
78 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision(
'from', $params );
81 if ( $params[
'torelative'] !==
null ) {
83 $this->
dieWithError(
'apierror-compare-relative-to-nothing' );
88 $this->
dieWithError( [
'apierror-compare-relative-to-deleted', $params[
'torelative'] ] );
90 switch ( $params[
'torelative'] ) {
93 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
94 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
95 $fromRelRev = $fromRev;
96 $fromValsRev = $fromRev;
98 $title = Title::newFromPageIdentity( $toRelRev->getPage() );
100 'apiwarn-compare-no-prev',
108 $fromRev->setContent(
110 $toRelRev->getMainContentRaw()
111 ->getContentHandler()
118 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
122 $title = Title::newFromPageIdentity( $fromRelRev->getPage() );
124 'apiwarn-compare-no-next',
131 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
132 $toRev->setVisibility( $fromRelRev->getVisibility() );
137 $title = $fromRelRev->getPage();
138 $toRev = $this->revisionStore->getRevisionByTitle( $title );
140 $title = Title::newFromPageIdentity( $title );
142 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
153 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision(
'to', $params );
158 if ( !$fromRev || !$toRev ) {
164 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
165 $this->
dieWithError( [
'apierror-missingcontent-revid', $fromRev->getId() ],
'missingcontent' );
167 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
168 $this->
dieWithError( [
'apierror-missingcontent-revid', $toRev->getId() ],
'missingcontent' );
174 $context->setTitle( Title::newFromPageIdentity( $fromRelRev->getPage() ) );
175 } elseif ( $toRelRev ) {
176 $context->setTitle( Title::newFromPageIdentity( $toRelRev->getPage() ) );
178 $guessedTitle = $this->guessTitle();
179 if ( $guessedTitle ) {
180 $context->setTitle( $guessedTitle );
183 $this->differenceEngine->setContext( $context );
184 $this->differenceEngine->setSlotDiffOptions( [
'diff-type' => $params[
'difftype'] ] );
185 $this->differenceEngine->setRevisions( $fromRev, $toRev );
186 if ( $params[
'slots'] ===
null ) {
187 $difftext = $this->differenceEngine->getDiffBody();
188 if ( $difftext ===
false ) {
193 foreach ( $params[
'slots'] as $role ) {
194 $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
197 foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
203 $this->setVals( $vals,
'from', $fromValsRev );
204 $this->setVals( $vals,
'to', $toValsRev );
206 if ( isset( $this->props[
'rel'] ) ) {
208 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
210 $vals[
'prev'] = $rev->getId();
214 $rev = $this->revisionStore->getNextRevision( $toRev );
216 $vals[
'next'] = $rev->getId();
221 if ( isset( $this->props[
'diffsize'] ) ) {
222 $vals[
'diffsize'] = 0;
223 foreach ( (array)$difftext as $text ) {
224 $vals[
'diffsize'] += strlen( $text );
227 if ( isset( $this->props[
'diff'] ) ) {
228 if ( is_array( $difftext ) ) {
230 $vals[
'bodies'] = $difftext;
250 private function getRevisionById( $id ) {
251 $rev = $this->revisionStore->getRevisionById( $id );
257 if ( !$rev && $this->
getAuthority()->isAllowedAny(
'deletedtext',
'undelete' ) ) {
259 $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord(
null, $id );
273 private function guessTitle() {
274 if ( $this->guessedTitle !==
false ) {
275 return $this->guessedTitle;
278 $this->guessedTitle =
null;
281 foreach ( [
'from',
'to' ] as $prefix ) {
282 if ( $params[
"{$prefix}rev"] !==
null ) {
283 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
285 $this->guessedTitle = Title::newFromPageIdentity( $rev->getPage() );
290 if ( $params[
"{$prefix}title"] !==
null ) {
291 $title = Title::newFromText( $params[
"{$prefix}title"] );
292 if ( $title && !$title->isExternal() ) {
293 $this->guessedTitle = $title;
298 if ( $params[
"{$prefix}id"] !==
null ) {
299 $title = Title::newFromID( $params[
"{$prefix}id"] );
301 $this->guessedTitle = $title;
307 return $this->guessedTitle;
315 private function guessModel( $role ) {
318 foreach ( [
'from',
'to' ] as $prefix ) {
319 if ( $params[
"{$prefix}rev"] !==
null ) {
320 $rev = $this->getRevisionById( $params[
"{$prefix}rev"] );
321 if ( $rev && $rev->hasSlot( $role ) ) {
322 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
327 $guessedTitle = $this->guessTitle();
328 if ( $guessedTitle ) {
329 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
332 if ( isset( $params[
"fromcontentmodel-$role"] ) ) {
333 return $params[
"fromcontentmodel-$role"];
335 if ( isset( $params[
"tocontentmodel-$role"] ) ) {
336 return $params[
"tocontentmodel-$role"];
339 if ( $role === SlotRecord::MAIN ) {
340 if ( isset( $params[
'fromcontentmodel'] ) ) {
341 return $params[
'fromcontentmodel'];
343 if ( isset( $params[
'tocontentmodel'] ) ) {
344 return $params[
'tocontentmodel'];
366 private function getDiffRevision( $prefix, array $params ) {
370 if ( $params[
"{$prefix}text"] !==
null ) {
371 $params[
"{$prefix}slots"] = [ SlotRecord::MAIN ];
373 $params[
"{$prefix}section-main"] =
null;
374 $params[
"{$prefix}contentmodel-main"] =
$params[
"{$prefix}contentmodel"];
375 $params[
"{$prefix}contentformat-main"] =
$params[
"{$prefix}contentformat"];
380 $suppliedContent =
$params[
"{$prefix}slots"] !==
null;
384 if ( $params[
"{$prefix}rev"] !==
null ) {
385 $revId =
$params[
"{$prefix}rev"];
386 } elseif ( $params[
"{$prefix}title"] !==
null || $params[
"{$prefix}id"] !==
null ) {
387 if ( $params[
"{$prefix}title"] !==
null ) {
388 $title = Title::newFromText( $params[
"{$prefix}title"] );
389 if ( !$title || $title->isExternal() ) {
395 $title = Title::newFromID( $params[
"{$prefix}id"] );
397 $this->
dieWithError( [
'apierror-nosuchpageid', $params[
"{$prefix}id"] ] );
400 $revId = $title->getLatestRevID();
404 if ( !$suppliedContent ) {
405 if ( $title->exists() ) {
407 [
'apierror-missingrev-title',
wfEscapeWikiText( $title->getPrefixedText() ) ],
412 [
'apierror-missingtitle-byname',
wfEscapeWikiText( $title->getPrefixedText() ) ],
419 if ( $revId !==
null ) {
420 $rev = $this->getRevisionById( $revId );
422 $this->
dieWithError( [
'apierror-nosuchrevid', $revId ] );
424 $title = Title::newFromPageIdentity( $rev->getPage() );
428 if ( !$suppliedContent ) {
432 if ( isset( $params[
"{$prefix}section"] ) ) {
433 $section =
$params[
"{$prefix}section"];
434 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
435 $newRev->setVisibility( $rev->getVisibility() );
436 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
440 [
'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ],
'missingcontent'
443 $content = $content->getSection( $section );
446 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
447 "nosuch{$prefix}section"
450 $newRev->setContent( SlotRecord::MAIN, $content );
453 return [ $newRev, $rev, $rev ];
459 $title = $this->guessTitle();
462 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
463 $newRev->setVisibility( $rev->getVisibility() );
465 $newRev =
new MutableRevisionRecord( $title ?: Title::newMainPage() );
467 foreach ( $params[
"{$prefix}slots"] as $role ) {
468 $text =
$params[
"{$prefix}text-{$role}"];
469 if ( $text ===
null ) {
471 if ( $role === SlotRecord::MAIN ) {
472 $this->
dieWithError( [
'apierror-compare-maintextrequired', $prefix ] );
477 foreach ( [
'section',
'contentmodel',
'contentformat' ] as $param ) {
478 if ( isset( $params[
"{$prefix}{$param}-{$role}"] ) ) {
480 'apierror-compare-notext',
487 $newRev->removeSlot( $role );
491 $model =
$params[
"{$prefix}contentmodel-{$role}"];
492 $format =
$params[
"{$prefix}contentformat-{$role}"];
494 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
495 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
497 if ( !$model && $title && $role === SlotRecord::MAIN ) {
499 $model = $title->getContentModel();
502 $model = $this->guessModel( $role );
506 $this->
addWarning( [
'apiwarn-compare-nocontentmodel', $model ] );
510 $content = $this->contentHandlerFactory
511 ->getContentHandler( $model )
512 ->unserializeContent( $text, $format );
513 }
catch ( ContentSerializationException $ex ) {
515 'wrap' =>
ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
519 if ( $params[
"{$prefix}pst"] ) {
523 $popts = ParserOptions::newFromContext( $this->
getContext() );
524 $content = $this->contentTransformer->preSaveTransform(
527 $this->getUserForPreview(),
532 $section =
$params[
"{$prefix}section-{$role}"];
533 if ( $section !==
null && $section !==
'' ) {
535 $this->
dieWithError(
"apierror-compare-no{$prefix}revision" );
537 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
538 if ( !$oldContent ) {
540 [
'apierror-missingcontent-revid-role', $rev->getId(),
wfEscapeWikiText( $role ) ],
544 if ( !$oldContent->getContentHandler()->supportsSections() ) {
545 $this->
dieWithError( [
'apierror-sectionsnotsupported', $content->getModel() ] );
548 $content = $oldContent->replaceSection( $section, $content,
'' );
549 }
catch ( TimeoutException $e ) {
551 }
catch ( Exception ) {
556 $this->
dieWithError( [
'apierror-sectionreplacefailed' ] );
561 if ( $role === SlotRecord::MAIN && isset( $params[
"{$prefix}section"] ) ) {
562 $section =
$params[
"{$prefix}section"];
563 $content = $content->getSection( $section );
566 [
"apierror-compare-nosuch{$prefix}section",
wfEscapeWikiText( $section ) ],
567 "nosuch{$prefix}section"
572 $newRev->setContent( $role, $content );
574 return [ $newRev, $rev, null ];
584 private function setVals( &$vals, $prefix, $rev ) {
586 $title = Title::newFromPageIdentity( $rev->getPage() );
587 if ( isset( $this->props[
'ids'] ) ) {
588 $vals[
"{$prefix}id"] = $title->getArticleID();
589 $vals[
"{$prefix}revid"] = $rev->getId();
591 if ( isset( $this->props[
'title'] ) ) {
594 if ( isset( $this->props[
'size'] ) ) {
595 $vals[
"{$prefix}size"] = $rev->getSize();
597 if ( isset( $this->props[
'timestamp'] ) ) {
598 $revTimestamp = $rev->getTimestamp();
599 if ( $revTimestamp ) {
600 $vals[
"{$prefix}timestamp"] =
wfTimestamp( TS::ISO_8601, $revTimestamp );
605 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
606 $vals[
"{$prefix}texthidden"] =
true;
610 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
611 $vals[
"{$prefix}userhidden"] =
true;
614 if ( isset( $this->props[
'user'] ) ) {
615 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
617 $vals[
"{$prefix}user"] = $user->getName();
618 $vals[
"{$prefix}userid"] = $user->getId();
622 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
623 $vals[
"{$prefix}commenthidden"] =
true;
626 if ( isset( $this->props[
'comment'] ) || isset( $this->props[
'parsedcomment'] ) ) {
627 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->
getAuthority() );
628 if ( $comment !==
null ) {
629 if ( isset( $this->props[
'comment'] ) ) {
630 $vals[
"{$prefix}comment"] = $comment->text;
632 $vals[
"{$prefix}parsedcomment"] = $this->commentFormatter->format(
633 $comment->text, $title
639 $this->
getMain()->setCacheMode(
'private' );
640 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
641 $vals[
"{$prefix}suppressed"] =
true;
645 if ( $rev instanceof RevisionArchiveRecord ) {
646 $this->
getMain()->setCacheMode(
'private' );
647 $vals[
"{$prefix}archive"] =
true;
652 private function getUserForPreview(): UserIdentity {
654 if ( $this->tempUserCreator->shouldAutoCreate( $user,
'edit' ) ) {
655 return $this->userFactory->newUnsavedTempUser(
656 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
664 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
665 sort( $slotRoles, SORT_STRING );
671 ParamValidator::PARAM_TYPE =>
'integer'
674 ParamValidator::PARAM_TYPE =>
'integer'
678 ParamValidator::PARAM_TYPE => $slotRoles,
679 ParamValidator::PARAM_ISMULTI =>
true,
683 ParamValidator::PARAM_TYPE =>
'text',
685 'section-{slot}' => [
687 ParamValidator::PARAM_TYPE =>
'string',
689 'contentformat-{slot}' => [
691 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
693 'contentmodel-{slot}' => [
695 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
700 ParamValidator::PARAM_TYPE =>
'text',
701 ParamValidator::PARAM_DEPRECATED =>
true,
704 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
705 ParamValidator::PARAM_DEPRECATED =>
true,
708 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
709 ParamValidator::PARAM_DEPRECATED =>
true,
712 ParamValidator::PARAM_DEFAULT =>
null,
713 ParamValidator::PARAM_DEPRECATED =>
true,
718 foreach ( $fromToParams as $k => $v ) {
724 foreach ( $fromToParams as $k => $v ) {
733 [
'torelative' => [ ParamValidator::PARAM_TYPE => [
'prev',
'next',
'cur' ], ] ],
738 ParamValidator::PARAM_DEFAULT =>
'diff|ids|title',
739 ParamValidator::PARAM_TYPE => [
751 ParamValidator::PARAM_ISMULTI =>
true,
756 ParamValidator::PARAM_TYPE => $slotRoles,
757 ParamValidator::PARAM_ISMULTI =>
true,
758 ParamValidator::PARAM_ALL =>
true,
762 ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
763 ParamValidator::PARAM_DEFAULT =>
'table',
772 'action=compare&fromrev=1&torev=2'
773 =>
'apihelp-compare-example-1',
779 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';