1<?php
2
3use MediaWiki\Revision\MutableRevisionRecord;
4use MediaWiki\Revision\RevisionStore;
5use MediaWiki\Revision\SlotRoleRegistry;
6use Psr\Log\LoggerInterface;
7use Wikimedia\Rdbms\ILoadBalancer;
8
9/**
10 * @since 1.31
11 */
12class ImportableOldRevisionImporter implements OldRevisionImporter {
13
14	/**
15	 * @var LoggerInterface
16	 */
17	private $logger;
18
19	/**
20	 * @var bool
21	 */
22	private $doUpdates;
23
24	/**
25	 * @var ILoadBalancer
26	 */
27	private $loadBalancer;
28
29	/**
30	 * @var RevisionStore
31	 */
32	private $revisionStore;
33
34	/**
35	 * @var SlotRoleRegistry
36	 */
37	private $slotRoleRegistry;
38
39	/**
40	 * @param bool $doUpdates
41	 * @param LoggerInterface $logger
42	 * @param ILoadBalancer $loadBalancer
43	 * @param RevisionStore $revisionStore
44	 * @param SlotRoleRegistry|null $slotRoleRegistry
45	 */
46	public function __construct(
47		$doUpdates,
48		LoggerInterface $logger,
49		ILoadBalancer $loadBalancer,
50		RevisionStore $revisionStore,
51		SlotRoleRegistry $slotRoleRegistry = null
52	) {
53		$this->doUpdates = $doUpdates;
54		$this->logger = $logger;
55		$this->loadBalancer = $loadBalancer;
56		$this->revisionStore = $revisionStore;
57		// @todo: temporary - remove when FileImporter extension is updated
58		if ( !$slotRoleRegistry ) {
59			$slotRoleRegistry = \MediaWiki\MediaWikiServices::getInstance()->getSlotRoleRegistry();
60		}
61		$this->slotRoleRegistry = $slotRoleRegistry;
62	}
63
64	public function import( ImportableOldRevision $importableRevision, $doUpdates = true ) {
65		$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
66
67		# Sneak a single revision into place
68		$user = $importableRevision->getUserObj() ?: User::newFromName( $importableRevision->getUser() );
69		if ( $user ) {
70			$userId = intval( $user->getId() );
71			$userText = $user->getName();
72		} else {
73			$userId = 0;
74			$userText = $importableRevision->getUser();
75			$user = new User;
76		}
77
78		// avoid memory leak...?
79		Title::clearCaches();
80
81		$page = WikiPage::factory( $importableRevision->getTitle() );
82		$page->loadPageData( 'fromdbmaster' );
83		if ( !$page->exists() ) {
84			// must create the page...
85			$pageId = $page->insertOn( $dbw );
86			$created = true;
87			$oldcountable = null;
88		} else {
89			$pageId = $page->getId();
90			$created = false;
91
92			// Note: sha1 has been in XML dumps since 2012. If you have an
93			// older dump, the duplicate detection here won't work.
94			if ( $importableRevision->getSha1Base36() !== false ) {
95				$prior = $dbw->selectField( 'revision', '1',
96					[ 'rev_page' => $pageId,
97					'rev_timestamp' => $dbw->timestamp( $importableRevision->getTimestamp() ),
98					'rev_sha1' => $importableRevision->getSha1Base36() ],
99					__METHOD__
100				);
101				if ( $prior ) {
102					// @todo FIXME: This could fail slightly for multiple matches :P
103					$this->logger->debug( __METHOD__ . ": skipping existing revision for [[" .
104						$importableRevision->getTitle()->getPrefixedText() . "]], timestamp " .
105						$importableRevision->getTimestamp() . "\n" );
106					return false;
107				}
108			}
109		}
110
111		if ( !$pageId ) {
112			// This seems to happen if two clients simultaneously try to import the
113			// same page
114			$this->logger->debug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
115				$importableRevision->getTitle()->getPrefixedText() . ']], timestamp ' .
116				$importableRevision->getTimestamp() . "\n" );
117			return false;
118		}
119
120		// Select previous version to make size diffs correct
121		// @todo This assumes that multiple revisions of the same page are imported
122		// in order from oldest to newest.
123		$qi = $this->revisionStore->getQueryInfo();
124		$prevRevRow = $dbw->selectRow( $qi['tables'], $qi['fields'],
125			[
126				'rev_page' => $pageId,
127				'rev_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $importableRevision->getTimestamp() ) ),
128			],
129			__METHOD__,
130			[ 'ORDER BY' => [
131				'rev_timestamp DESC',
132				'rev_id DESC', // timestamp is not unique per page
133			]
134			],
135			$qi['joins']
136		);
137
138		# @todo FIXME: Use original rev_id optionally (better for backups)
139		# Insert the row
140		$revisionRecord = new MutableRevisionRecord( $importableRevision->getTitle() );
141		$revisionRecord->setParentId( $prevRevRow ? (int)$prevRevRow->rev_id : 0 );
142		$revisionRecord->setComment(
143			CommentStoreComment::newUnsavedComment( $importableRevision->getComment() )
144		);
145
146		try {
147			$revUser = User::newFromAnyId(
148				$userId,
149				$userText,
150				null
151			);
152		} catch ( InvalidArgumentException $ex ) {
153			$revUser = RequestContext::getMain()->getUser();
154		}
155		$revisionRecord->setUser( $revUser );
156
157		$originalRevision = $prevRevRow
158			? $this->revisionStore->newRevisionFromRow(
159				$prevRevRow,
160				IDBAccessObject::READ_LATEST,
161				$importableRevision->getTitle()
162			)
163			: null;
164
165		foreach ( $importableRevision->getSlotRoles() as $role ) {
166			if ( !$this->slotRoleRegistry->isDefinedRole( $role ) ) {
167				throw new MWException( "Undefined slot role $role" );
168			}
169
170			$newContent = $importableRevision->getContent( $role );
171			if ( !$originalRevision || !$originalRevision->hasSlot( $role ) ) {
172				$revisionRecord->setContent( $role, $newContent );
173			} else {
174				$originalSlot = $originalRevision->getSlot( $role );
175				if ( !$originalSlot->hasSameContent( $importableRevision->getSlot( $role ) ) ) {
176					$revisionRecord->setContent( $role, $newContent );
177				} else {
178					$revisionRecord->inheritSlot( $originalRevision->getSlot( $role ) );
179				}
180			}
181		}
182
183		$revisionRecord->setTimestamp( $importableRevision->getTimestamp() );
184		$revisionRecord->setMinorEdit( $importableRevision->getMinor() );
185		$revisionRecord->setPageId( $pageId );
186
187		$latestRevId = $page->getLatest();
188
189		$inserted = $this->revisionStore->insertRevisionOn( $revisionRecord, $dbw );
190		if ( $latestRevId ) {
191			// If not found (false), cast to 0 so that the page is updated
192			// Just to be on the safe side, even though it should always be found
193			$latestRevTimestamp = (int)$this->revisionStore->getTimestampFromId(
194				$latestRevId,
195				RevisionStore::READ_LATEST
196			);
197		} else {
198			$latestRevTimestamp = 0;
199		}
200		if ( $importableRevision->getTimestamp() > $latestRevTimestamp ) {
201			$changed = $page->updateRevisionOn( $dbw, $inserted, $latestRevId );
202		} else {
203			$changed = false;
204		}
205
206		$tags = $importableRevision->getTags();
207		if ( $tags !== [] ) {
208			ChangeTags::addTags( $tags, null, $inserted->getId() );
209		}
210
211		if ( $changed !== false && $this->doUpdates ) {
212			$this->logger->debug( __METHOD__ . ": running updates" );
213			// countable/oldcountable stuff is handled in WikiImporter::finishImportPage
214			// @todo replace deprecated function
215			$page->doEditUpdates(
216				$inserted,
217				$user,
218				[ 'created' => $created, 'oldcountable' => 'no-change' ]
219			);
220		}
221
222		return true;
223	}
224
225}
226