+ private function guessTitleAndModel() {
+ if ( $this->guessed ) {
+ return;
+ }
+
+ $this->guessed = true;
+ $params = $this->extractRequestParams();
+
+ foreach ( [ 'from', 'to' ] as $prefix ) {
+ if ( $params["{$prefix}rev"] !== null ) {
+ $revId = $params["{$prefix}rev"];
+ $rev = Revision::newFromId( $revId );
+ if ( !$rev ) {
+ // Titles of deleted revisions aren't secret, per T51088
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $this->getDB()->selectRow(
+ $arQuery['tables'],
+ array_merge(
+ $arQuery['fields'],
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
+ if ( $row ) {
+ $rev = Revision::newFromArchiveRow( $row );
+ }
+ }
+ if ( $rev ) {
+ $this->guessedTitle = $rev->getTitle();
+ $this->guessedModel = $rev->getContentModel();
+ break;
+ }
+ }
+
+ if ( $params["{$prefix}title"] !== null ) {
+ $title = Title::newFromText( $params["{$prefix}title"] );
+ if ( $title && !$title->isExternal() ) {
+ $this->guessedTitle = $title;
+ break;
+ }
+ }
+
+ if ( $params["{$prefix}id"] !== null ) {
+ $title = Title::newFromID( $params["{$prefix}id"] );
+ if ( $title ) {
+ $this->guessedTitle = $title;
+ break;
+ }
+ }
+ }
+
+ if ( !$this->guessedModel ) {
+ if ( $this->guessedTitle ) {
+ $this->guessedModel = $this->guessedTitle->getContentModel();
+ } elseif ( $params['fromcontentmodel'] !== null ) {
+ $this->guessedModel = $params['fromcontentmodel'];
+ } elseif ( $params['tocontentmodel'] !== null ) {
+ $this->guessedModel = $params['tocontentmodel'];
+ }
+ }
+ }
+
+ /**
+ * Get the Revision and Content for one side of the diff
+ *
+ * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
+ * 'contentmodel', and 'contentformat' parameters to determine what content
+ * should be diffed.
+ *
+ * Returns three values:
+ * - The revision used to retrieve the content, if any
+ * - The content to be diffed
+ * - The revision specified, if any, even if not used to retrieve the
+ * Content
+ *
+ * @param string $prefix 'from' or 'to'
+ * @param array $params
+ * @return array [ Revision|null, Content, Revision|null ]
+ */
+ private function getDiffContent( $prefix, array $params ) {
+ $title = null;
+ $rev = null;
+ $suppliedContent = $params["{$prefix}text"] !== null;
+
+ // Get the revision and title, if applicable
+ $revId = null;
+ if ( $params["{$prefix}rev"] !== null ) {
+ $revId = $params["{$prefix}rev"];
+ } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
+ if ( $params["{$prefix}title"] !== null ) {
+ $title = Title::newFromText( $params["{$prefix}title"] );
+ if ( !$title || $title->isExternal() ) {
+ $this->dieWithError(
+ [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
+ );
+ }
+ } else {
+ $title = Title::newFromID( $params["{$prefix}id"] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
+ }
+ }
+ $revId = $title->getLatestRevID();
+ if ( !$revId ) {
+ $revId = null;
+ // Only die here if we're not using supplied text
+ if ( !$suppliedContent ) {
+ if ( $title->exists() ) {
+ $this->dieWithError(
+ [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
+ );
+ } else {
+ $this->dieWithError(
+ [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
+ 'missingtitle'
+ );
+ }
+ }
+ }
+ }
+ if ( $revId !== null ) {
+ $rev = Revision::newFromId( $revId );
+ if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
+ // Try the 'archive' table
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $this->getDB()->selectRow(
+ $arQuery['tables'],
+ array_merge(
+ $arQuery['fields'],
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
+ if ( $row ) {
+ $rev = Revision::newFromArchiveRow( $row );
+ $rev->isArchive = true;
+ }
+ }
+ if ( !$rev ) {
+ $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
+ }
+ $title = $rev->getTitle();
+
+ // If we don't have supplied content, return here. Otherwise,
+ // continue on below with the supplied content.
+ if ( !$suppliedContent ) {
+ $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( !$content ) {
+ $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
+ }
+ return [ $rev, $content, $rev ];
+ }
+ }
+
+ // Override $content based on supplied text
+ $model = $params["{$prefix}contentmodel"];
+ $format = $params["{$prefix}contentformat"];
+
+ if ( !$model && $rev ) {
+ $model = $rev->getContentModel();
+ }
+ if ( !$model && $title ) {
+ $model = $title->getContentModel();
+ }
+ if ( !$model ) {
+ $this->guessTitleAndModel();
+ $model = $this->guessedModel;
+ }
+ if ( !$model ) {
+ $model = CONTENT_MODEL_WIKITEXT;
+ $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
+ }
+
+ if ( !$title ) {
+ $this->guessTitleAndModel();
+ $title = $this->guessedTitle;
+ }
+
+ try {
+ $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
+ } catch ( MWContentSerializationException $ex ) {
+ $this->dieWithException( $ex, [
+ 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+ ] );
+ }
+
+ if ( $params["{$prefix}pst"] ) {