Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.47% |
415 / 444 |
|
45.45% |
5 / 11 |
CRAP | |
0.00% |
0 / 1 |
ApiComparePages | |
93.47% |
415 / 444 |
|
45.45% |
5 / 11 |
140.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
99.09% |
109 / 110 |
|
0.00% |
0 / 1 |
34 | |||
getRevisionById | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
guessTitle | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
10 | |||
guessModel | |
68.42% |
13 / 19 |
|
0.00% |
0 / 1 |
14.81 | |||
getDiffRevision | |
89.78% |
123 / 137 |
|
0.00% |
0 / 1 |
50.46 | |||
setVals | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
19 | |||
getUserForPreview | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
getAllowedParams | |
100.00% |
89 / 89 |
|
100.00% |
1 / 1 |
5 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\CommentFormatter\CommentFormatter; |
22 | use MediaWiki\Content\IContentHandlerFactory; |
23 | use MediaWiki\Content\Transform\ContentTransformer; |
24 | use MediaWiki\Revision\ArchivedRevisionLookup; |
25 | use MediaWiki\Revision\MutableRevisionRecord; |
26 | use MediaWiki\Revision\RevisionArchiveRecord; |
27 | use MediaWiki\Revision\RevisionRecord; |
28 | use MediaWiki\Revision\RevisionStore; |
29 | use MediaWiki\Revision\SlotRecord; |
30 | use MediaWiki\Revision\SlotRoleRegistry; |
31 | use MediaWiki\Title\Title; |
32 | use MediaWiki\User\TempUser\TempUserCreator; |
33 | use MediaWiki\User\UserFactory; |
34 | use Wikimedia\ParamValidator\ParamValidator; |
35 | use Wikimedia\RequestTimeout\TimeoutException; |
36 | |
37 | /** |
38 | * @ingroup API |
39 | */ |
40 | class ApiComparePages extends ApiBase { |
41 | |
42 | private RevisionStore $revisionStore; |
43 | private ArchivedRevisionLookup $archivedRevisionLookup; |
44 | private SlotRoleRegistry $slotRoleRegistry; |
45 | |
46 | /** @var Title|null|false */ |
47 | private $guessedTitle = false; |
48 | private $props; |
49 | |
50 | private IContentHandlerFactory $contentHandlerFactory; |
51 | private ContentTransformer $contentTransformer; |
52 | private CommentFormatter $commentFormatter; |
53 | private TempUserCreator $tempUserCreator; |
54 | private UserFactory $userFactory; |
55 | private DifferenceEngine $differenceEngine; |
56 | |
57 | /** |
58 | * @param ApiMain $mainModule |
59 | * @param string $moduleName |
60 | * @param RevisionStore $revisionStore |
61 | * @param ArchivedRevisionLookup $archivedRevisionLookup |
62 | * @param SlotRoleRegistry $slotRoleRegistry |
63 | * @param IContentHandlerFactory $contentHandlerFactory |
64 | * @param ContentTransformer $contentTransformer |
65 | * @param CommentFormatter $commentFormatter |
66 | * @param TempUserCreator $tempUserCreator |
67 | * @param UserFactory $userFactory |
68 | */ |
69 | public function __construct( |
70 | ApiMain $mainModule, |
71 | $moduleName, |
72 | RevisionStore $revisionStore, |
73 | ArchivedRevisionLookup $archivedRevisionLookup, |
74 | SlotRoleRegistry $slotRoleRegistry, |
75 | IContentHandlerFactory $contentHandlerFactory, |
76 | ContentTransformer $contentTransformer, |
77 | CommentFormatter $commentFormatter, |
78 | TempUserCreator $tempUserCreator, |
79 | UserFactory $userFactory |
80 | ) { |
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; |
90 | $this->differenceEngine = new DifferenceEngine; |
91 | } |
92 | |
93 | public function execute() { |
94 | $params = $this->extractRequestParams(); |
95 | |
96 | // Parameter validation |
97 | $this->requireAtLeastOneParameter( |
98 | $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots' |
99 | ); |
100 | $this->requireAtLeastOneParameter( |
101 | $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots' |
102 | ); |
103 | |
104 | $this->props = array_fill_keys( $params['prop'], true ); |
105 | |
106 | // Cache responses publicly by default. This may be overridden later. |
107 | $this->getMain()->setCacheMode( 'public' ); |
108 | |
109 | // Get the 'from' RevisionRecord |
110 | [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision( 'from', $params ); |
111 | |
112 | // Get the 'to' RevisionRecord |
113 | if ( $params['torelative'] !== null ) { |
114 | if ( !$fromRelRev ) { |
115 | $this->dieWithError( 'apierror-compare-relative-to-nothing' ); |
116 | } |
117 | if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) { |
118 | // RevisionStore's getPreviousRevision/getNextRevision blow up |
119 | // when passed an RevisionArchiveRecord for a deleted page |
120 | $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] ); |
121 | } |
122 | switch ( $params['torelative'] ) { |
123 | case 'prev': |
124 | // Swap 'from' and 'to' |
125 | [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ]; |
126 | $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev ); |
127 | $fromRelRev = $fromRev; |
128 | $fromValsRev = $fromRev; |
129 | if ( !$fromRev ) { |
130 | $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ); |
131 | $this->addWarning( [ |
132 | 'apiwarn-compare-no-prev', |
133 | wfEscapeWikiText( $title->getPrefixedText() ), |
134 | $toRelRev->getId() |
135 | ] ); |
136 | |
137 | // (T203433) Create an empty dummy revision as the "previous". |
138 | // The main slot has to exist, the rest will be handled by DifferenceEngine. |
139 | $fromRev = new MutableRevisionRecord( |
140 | $title ?: $toRev->getPage() |
141 | ); |
142 | $fromRev->setContent( |
143 | SlotRecord::MAIN, |
144 | $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ) |
145 | ->getContentHandler() |
146 | ->makeEmptyContent() |
147 | ); |
148 | } |
149 | break; |
150 | |
151 | case 'next': |
152 | $toRev = $this->revisionStore->getNextRevision( $fromRelRev ); |
153 | $toRelRev = $toRev; |
154 | $toValsRev = $toRev; |
155 | if ( !$toRev ) { |
156 | $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ); |
157 | $this->addWarning( [ |
158 | 'apiwarn-compare-no-next', |
159 | wfEscapeWikiText( $title->getPrefixedText() ), |
160 | $fromRelRev->getId() |
161 | ] ); |
162 | |
163 | // (T203433) The web UI treats "next" as "cur" in this case. |
164 | // Avoid repeating metadata by making a MutableRevisionRecord with no changes. |
165 | $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev ); |
166 | } |
167 | break; |
168 | |
169 | case 'cur': |
170 | $title = $fromRelRev->getPageAsLinkTarget(); |
171 | $toRev = $this->revisionStore->getRevisionByTitle( $title ); |
172 | if ( !$toRev ) { |
173 | $title = Title::newFromLinkTarget( $title ); |
174 | $this->dieWithError( |
175 | [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], |
176 | 'nosuchrevid' |
177 | ); |
178 | } |
179 | $toRelRev = $toRev; |
180 | $toValsRev = $toRev; |
181 | break; |
182 | } |
183 | } else { |
184 | [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision( 'to', $params ); |
185 | } |
186 | |
187 | // Handle missing from or to revisions (should never happen) |
188 | // @codeCoverageIgnoreStart |
189 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141 |
190 | if ( !$fromRev || !$toRev ) { |
191 | $this->dieWithError( 'apierror-baddiff' ); |
192 | } |
193 | // @codeCoverageIgnoreEnd |
194 | |
195 | // Handle revdel |
196 | if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
197 | $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' ); |
198 | } |
199 | if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
200 | $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' ); |
201 | } |
202 | |
203 | // Get the diff |
204 | $context = new DerivativeContext( $this->getContext() ); |
205 | if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) { |
206 | $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) ); |
207 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141 |
208 | } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) { |
209 | $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) ); |
210 | } else { |
211 | $guessedTitle = $this->guessTitle(); |
212 | if ( $guessedTitle ) { |
213 | $context->setTitle( $guessedTitle ); |
214 | } |
215 | } |
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 ) { |
222 | $this->dieWithError( 'apierror-baddiff' ); |
223 | } |
224 | } else { |
225 | $difftext = []; |
226 | foreach ( $params['slots'] as $role ) { |
227 | $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role ); |
228 | } |
229 | } |
230 | foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) { |
231 | $this->addWarning( $msg ); |
232 | } |
233 | |
234 | // Fill in the response |
235 | $vals = []; |
236 | $this->setVals( $vals, 'from', $fromValsRev ); |
237 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141 |
238 | $this->setVals( $vals, 'to', $toValsRev ); |
239 | |
240 | if ( isset( $this->props['rel'] ) ) { |
241 | if ( !$fromRev instanceof MutableRevisionRecord ) { |
242 | $rev = $this->revisionStore->getPreviousRevision( $fromRev ); |
243 | if ( $rev ) { |
244 | $vals['prev'] = $rev->getId(); |
245 | } |
246 | } |
247 | if ( !$toRev instanceof MutableRevisionRecord ) { |
248 | $rev = $this->revisionStore->getNextRevision( $toRev ); |
249 | if ( $rev ) { |
250 | $vals['next'] = $rev->getId(); |
251 | } |
252 | } |
253 | } |
254 | |
255 | if ( isset( $this->props['diffsize'] ) ) { |
256 | $vals['diffsize'] = 0; |
257 | foreach ( (array)$difftext as $text ) { |
258 | $vals['diffsize'] += strlen( $text ); |
259 | } |
260 | } |
261 | if ( isset( $this->props['diff'] ) ) { |
262 | if ( is_array( $difftext ) ) { |
263 | ApiResult::setArrayType( $difftext, 'kvp', 'diff' ); |
264 | $vals['bodies'] = $difftext; |
265 | } else { |
266 | ApiResult::setContentValue( $vals, 'body', $difftext ); |
267 | } |
268 | } |
269 | |
270 | // Diffs can be really big and there's little point in having |
271 | // ApiResult truncate it to an empty response since the diff is the |
272 | // whole reason this module exists. So pass NO_SIZE_CHECK here. |
273 | $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK ); |
274 | } |
275 | |
276 | /** |
277 | * Load a revision by ID |
278 | * |
279 | * Falls back to checking the archive table if appropriate. |
280 | * |
281 | * @param int $id |
282 | * @return RevisionRecord|null |
283 | */ |
284 | private function getRevisionById( $id ) { |
285 | $rev = $this->revisionStore->getRevisionById( $id ); |
286 | if ( !$rev && $this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) { |
287 | // Try the 'archive' table |
288 | $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id ); |
289 | } |
290 | return $rev; |
291 | } |
292 | |
293 | /** |
294 | * Guess an appropriate default Title for this request |
295 | * |
296 | * @return Title|null |
297 | */ |
298 | private function guessTitle() { |
299 | if ( $this->guessedTitle !== false ) { |
300 | return $this->guessedTitle; |
301 | } |
302 | |
303 | $this->guessedTitle = null; |
304 | $params = $this->extractRequestParams(); |
305 | |
306 | foreach ( [ 'from', 'to' ] as $prefix ) { |
307 | if ( $params["{$prefix}rev"] !== null ) { |
308 | $rev = $this->getRevisionById( $params["{$prefix}rev"] ); |
309 | if ( $rev ) { |
310 | $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); |
311 | break; |
312 | } |
313 | } |
314 | |
315 | if ( $params["{$prefix}title"] !== null ) { |
316 | $title = Title::newFromText( $params["{$prefix}title"] ); |
317 | if ( $title && !$title->isExternal() ) { |
318 | $this->guessedTitle = $title; |
319 | break; |
320 | } |
321 | } |
322 | |
323 | if ( $params["{$prefix}id"] !== null ) { |
324 | $title = Title::newFromID( $params["{$prefix}id"] ); |
325 | if ( $title ) { |
326 | $this->guessedTitle = $title; |
327 | break; |
328 | } |
329 | } |
330 | } |
331 | |
332 | return $this->guessedTitle; |
333 | } |
334 | |
335 | /** |
336 | * Guess an appropriate default content model for this request |
337 | * @param string $role Slot for which to guess the model |
338 | * @return string|null Guessed content model |
339 | */ |
340 | private function guessModel( $role ) { |
341 | $params = $this->extractRequestParams(); |
342 | |
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(); |
348 | } |
349 | } |
350 | } |
351 | |
352 | $guessedTitle = $this->guessTitle(); |
353 | if ( $guessedTitle ) { |
354 | return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle ); |
355 | } |
356 | |
357 | if ( isset( $params["fromcontentmodel-$role"] ) ) { |
358 | return $params["fromcontentmodel-$role"]; |
359 | } |
360 | if ( isset( $params["tocontentmodel-$role"] ) ) { |
361 | return $params["tocontentmodel-$role"]; |
362 | } |
363 | |
364 | if ( $role === SlotRecord::MAIN ) { |
365 | if ( isset( $params['fromcontentmodel'] ) ) { |
366 | return $params['fromcontentmodel']; |
367 | } |
368 | if ( isset( $params['tocontentmodel'] ) ) { |
369 | return $params['tocontentmodel']; |
370 | } |
371 | } |
372 | |
373 | return null; |
374 | } |
375 | |
376 | /** |
377 | * Get the RevisionRecord for one side of the diff |
378 | * |
379 | * This uses the appropriate set of parameters to determine what content |
380 | * should be diffed. |
381 | * |
382 | * Returns three values: |
383 | * - A RevisionRecord holding the content |
384 | * - The revision specified, if any, even if content was supplied |
385 | * - The revision to pass to setVals(), if any |
386 | * |
387 | * @param string $prefix 'from' or 'to' |
388 | * @param array $params |
389 | * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ] |
390 | */ |
391 | private function getDiffRevision( $prefix, array $params ) { |
392 | // Back compat params |
393 | $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" ); |
394 | $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" ); |
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"]; |
401 | } |
402 | |
403 | $title = null; |
404 | $rev = null; |
405 | $suppliedContent = $params["{$prefix}slots"] !== null; |
406 | |
407 | // Get the revision and title, if applicable |
408 | $revId = 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() ) { |
415 | $this->dieWithError( |
416 | [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ] |
417 | ); |
418 | } |
419 | } else { |
420 | $title = Title::newFromID( $params["{$prefix}id"] ); |
421 | if ( !$title ) { |
422 | $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] ); |
423 | } |
424 | } |
425 | $revId = $title->getLatestRevID(); |
426 | if ( !$revId ) { |
427 | $revId = null; |
428 | // Only die here if we're not using supplied text |
429 | if ( !$suppliedContent ) { |
430 | if ( $title->exists() ) { |
431 | $this->dieWithError( |
432 | [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], |
433 | 'nosuchrevid' |
434 | ); |
435 | } else { |
436 | $this->dieWithError( |
437 | [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ], |
438 | 'missingtitle' |
439 | ); |
440 | } |
441 | } |
442 | } |
443 | } |
444 | if ( $revId !== null ) { |
445 | $rev = $this->getRevisionById( $revId ); |
446 | if ( !$rev ) { |
447 | $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] ); |
448 | } |
449 | $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); |
450 | |
451 | // If we don't have supplied content, return here. Otherwise, |
452 | // continue on below with the supplied content. |
453 | if ( !$suppliedContent ) { |
454 | $newRev = $rev; |
455 | |
456 | // Deprecated 'fromsection'/'tosection' |
457 | if ( isset( $params["{$prefix}section"] ) ) { |
458 | $section = $params["{$prefix}section"]; |
459 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
460 | $newRev = MutableRevisionRecord::newFromParentRevision( $rev ); |
461 | $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, |
462 | $this->getAuthority() ); |
463 | if ( !$content ) { |
464 | $this->dieWithError( |
465 | [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent' |
466 | ); |
467 | } |
468 | $content = $content->getSection( $section ); |
469 | if ( !$content ) { |
470 | $this->dieWithError( |
471 | [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ], |
472 | "nosuch{$prefix}section" |
473 | ); |
474 | } |
475 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
476 | $newRev->setContent( SlotRecord::MAIN, $content ); |
477 | } |
478 | |
479 | return [ $newRev, $rev, $rev ]; |
480 | } |
481 | } |
482 | |
483 | // Override $content based on supplied text |
484 | if ( !$title ) { |
485 | $title = $this->guessTitle(); |
486 | } |
487 | if ( $rev ) { |
488 | $newRev = MutableRevisionRecord::newFromParentRevision( $rev ); |
489 | } else { |
490 | $newRev = new MutableRevisionRecord( $title ?: Title::newMainPage() ); |
491 | } |
492 | foreach ( $params["{$prefix}slots"] as $role ) { |
493 | $text = $params["{$prefix}text-{$role}"]; |
494 | if ( $text === null ) { |
495 | // The SlotRecord::MAIN role can't be deleted |
496 | if ( $role === SlotRecord::MAIN ) { |
497 | $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] ); |
498 | } |
499 | |
500 | // These parameters make no sense without text. Reject them to avoid |
501 | // confusion. |
502 | foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) { |
503 | if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) { |
504 | $this->dieWithError( [ |
505 | 'apierror-compare-notext', |
506 | wfEscapeWikiText( "{$prefix}{$param}-{$role}" ), |
507 | wfEscapeWikiText( "{$prefix}text-{$role}" ), |
508 | ] ); |
509 | } |
510 | } |
511 | |
512 | $newRev->removeSlot( $role ); |
513 | continue; |
514 | } |
515 | |
516 | $model = $params["{$prefix}contentmodel-{$role}"]; |
517 | $format = $params["{$prefix}contentformat-{$role}"]; |
518 | |
519 | if ( !$model && $rev && $rev->hasSlot( $role ) ) { |
520 | $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel(); |
521 | } |
522 | if ( !$model && $title && $role === SlotRecord::MAIN ) { |
523 | // @todo: Use SlotRoleRegistry and do this for all slots |
524 | $model = $title->getContentModel(); |
525 | } |
526 | if ( !$model ) { |
527 | $model = $this->guessModel( $role ); |
528 | } |
529 | if ( !$model ) { |
530 | $model = CONTENT_MODEL_WIKITEXT; |
531 | $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] ); |
532 | } |
533 | |
534 | try { |
535 | $content = $this->contentHandlerFactory |
536 | ->getContentHandler( $model ) |
537 | ->unserializeContent( $text, $format ); |
538 | } catch ( MWContentSerializationException $ex ) { |
539 | $this->dieWithException( $ex, [ |
540 | 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' ) |
541 | ] ); |
542 | } |
543 | |
544 | if ( $params["{$prefix}pst"] ) { |
545 | if ( !$title ) { |
546 | $this->dieWithError( 'apierror-compare-no-title' ); |
547 | } |
548 | $popts = ParserOptions::newFromContext( $this->getContext() ); |
549 | $content = $this->contentTransformer->preSaveTransform( |
550 | $content, |
551 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
552 | $title, |
553 | $this->getUserForPreview(), |
554 | $popts |
555 | ); |
556 | } |
557 | |
558 | $section = $params["{$prefix}section-{$role}"]; |
559 | if ( $section !== null && $section !== '' ) { |
560 | if ( !$rev ) { |
561 | $this->dieWithError( "apierror-compare-no{$prefix}revision" ); |
562 | } |
563 | $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getAuthority() ); |
564 | if ( !$oldContent ) { |
565 | $this->dieWithError( |
566 | [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ], |
567 | 'missingcontent' |
568 | ); |
569 | } |
570 | if ( !$oldContent->getContentHandler()->supportsSections() ) { |
571 | $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] ); |
572 | } |
573 | try { |
574 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
575 | $content = $oldContent->replaceSection( $section, $content, '' ); |
576 | } catch ( TimeoutException $e ) { |
577 | throw $e; |
578 | } catch ( Exception $ex ) { |
579 | // Probably a content model mismatch. |
580 | $content = null; |
581 | } |
582 | if ( !$content ) { |
583 | $this->dieWithError( [ 'apierror-sectionreplacefailed' ] ); |
584 | } |
585 | } |
586 | |
587 | // Deprecated 'fromsection'/'tosection' |
588 | if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) { |
589 | $section = $params["{$prefix}section"]; |
590 | $content = $content->getSection( $section ); |
591 | if ( !$content ) { |
592 | $this->dieWithError( |
593 | [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ], |
594 | "nosuch{$prefix}section" |
595 | ); |
596 | } |
597 | } |
598 | |
599 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
600 | $newRev->setContent( $role, $content ); |
601 | } |
602 | return [ $newRev, $rev, null ]; |
603 | } |
604 | |
605 | /** |
606 | * Set value fields from a RevisionRecord object |
607 | * |
608 | * @param array &$vals Result array to set data into |
609 | * @param string $prefix 'from' or 'to' |
610 | * @param RevisionRecord|null $rev |
611 | */ |
612 | private function setVals( &$vals, $prefix, $rev ) { |
613 | if ( $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(); |
618 | } |
619 | if ( isset( $this->props['title'] ) ) { |
620 | ApiQueryBase::addTitleInfo( $vals, $title, $prefix ); |
621 | } |
622 | if ( isset( $this->props['size'] ) ) { |
623 | $vals["{$prefix}size"] = $rev->getSize(); |
624 | } |
625 | if ( isset( $this->props['timestamp'] ) ) { |
626 | $revTimestamp = $rev->getTimestamp(); |
627 | if ( $revTimestamp ) { |
628 | $vals["{$prefix}timestamp"] = wfTimestamp( TS_ISO_8601, $revTimestamp ); |
629 | } |
630 | } |
631 | |
632 | $anyHidden = false; |
633 | if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
634 | $vals["{$prefix}texthidden"] = true; |
635 | $anyHidden = true; |
636 | } |
637 | |
638 | if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) { |
639 | $vals["{$prefix}userhidden"] = true; |
640 | $anyHidden = true; |
641 | } |
642 | if ( isset( $this->props['user'] ) ) { |
643 | $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() ); |
644 | if ( $user ) { |
645 | $vals["{$prefix}user"] = $user->getName(); |
646 | $vals["{$prefix}userid"] = $user->getId(); |
647 | } |
648 | } |
649 | |
650 | if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) { |
651 | $vals["{$prefix}commenthidden"] = true; |
652 | $anyHidden = true; |
653 | } |
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; |
659 | } |
660 | $vals["{$prefix}parsedcomment"] = $this->commentFormatter->format( |
661 | $comment->text, $title |
662 | ); |
663 | } |
664 | } |
665 | |
666 | if ( $anyHidden ) { |
667 | $this->getMain()->setCacheMode( 'private' ); |
668 | if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) { |
669 | $vals["{$prefix}suppressed"] = true; |
670 | } |
671 | } |
672 | |
673 | if ( $rev instanceof RevisionArchiveRecord ) { |
674 | $this->getMain()->setCacheMode( 'private' ); |
675 | $vals["{$prefix}archive"] = true; |
676 | } |
677 | } |
678 | } |
679 | |
680 | private function getUserForPreview() { |
681 | $user = $this->getUser(); |
682 | if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) { |
683 | return $this->userFactory->newUnsavedTempUser( |
684 | $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() ) |
685 | ); |
686 | } |
687 | return $user; |
688 | } |
689 | |
690 | public function getAllowedParams() { |
691 | $slotRoles = $this->slotRoleRegistry->getKnownRoles(); |
692 | sort( $slotRoles, SORT_STRING ); |
693 | |
694 | // Parameters for the 'from' and 'to' content |
695 | $fromToParams = [ |
696 | 'title' => null, |
697 | 'id' => [ |
698 | ParamValidator::PARAM_TYPE => 'integer' |
699 | ], |
700 | 'rev' => [ |
701 | ParamValidator::PARAM_TYPE => 'integer' |
702 | ], |
703 | |
704 | 'slots' => [ |
705 | ParamValidator::PARAM_TYPE => $slotRoles, |
706 | ParamValidator::PARAM_ISMULTI => true, |
707 | ], |
708 | 'text-{slot}' => [ |
709 | ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below |
710 | ParamValidator::PARAM_TYPE => 'text', |
711 | ], |
712 | 'section-{slot}' => [ |
713 | ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below |
714 | ParamValidator::PARAM_TYPE => 'string', |
715 | ], |
716 | 'contentformat-{slot}' => [ |
717 | ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below |
718 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(), |
719 | ], |
720 | 'contentmodel-{slot}' => [ |
721 | ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below |
722 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(), |
723 | ], |
724 | 'pst' => false, |
725 | |
726 | 'text' => [ |
727 | ParamValidator::PARAM_TYPE => 'text', |
728 | ParamValidator::PARAM_DEPRECATED => true, |
729 | ], |
730 | 'contentformat' => [ |
731 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(), |
732 | ParamValidator::PARAM_DEPRECATED => true, |
733 | ], |
734 | 'contentmodel' => [ |
735 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(), |
736 | ParamValidator::PARAM_DEPRECATED => true, |
737 | ], |
738 | 'section' => [ |
739 | ParamValidator::PARAM_DEFAULT => null, |
740 | ParamValidator::PARAM_DEPRECATED => true, |
741 | ], |
742 | ]; |
743 | |
744 | $ret = []; |
745 | foreach ( $fromToParams as $k => $v ) { |
746 | if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) { |
747 | $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots'; |
748 | } |
749 | $ret["from$k"] = $v; |
750 | } |
751 | foreach ( $fromToParams as $k => $v ) { |
752 | if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) { |
753 | $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots'; |
754 | } |
755 | $ret["to$k"] = $v; |
756 | } |
757 | |
758 | $ret = wfArrayInsertAfter( |
759 | $ret, |
760 | [ 'torelative' => [ ParamValidator::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ], |
761 | 'torev' |
762 | ); |
763 | |
764 | $ret['prop'] = [ |
765 | ParamValidator::PARAM_DEFAULT => 'diff|ids|title', |
766 | ParamValidator::PARAM_TYPE => [ |
767 | 'diff', |
768 | 'diffsize', |
769 | 'rel', |
770 | 'ids', |
771 | 'title', |
772 | 'user', |
773 | 'comment', |
774 | 'parsedcomment', |
775 | 'size', |
776 | 'timestamp', |
777 | ], |
778 | ParamValidator::PARAM_ISMULTI => true, |
779 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
780 | ]; |
781 | |
782 | $ret['slots'] = [ |
783 | ParamValidator::PARAM_TYPE => $slotRoles, |
784 | ParamValidator::PARAM_ISMULTI => true, |
785 | ParamValidator::PARAM_ALL => true, |
786 | ]; |
787 | |
788 | $ret['difftype'] = [ |
789 | ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(), |
790 | ParamValidator::PARAM_DEFAULT => 'table', |
791 | ]; |
792 | |
793 | return $ret; |
794 | } |
795 | |
796 | protected function getExamplesMessages() { |
797 | return [ |
798 | 'action=compare&fromrev=1&torev=2' |
799 | => 'apihelp-compare-example-1', |
800 | ]; |
801 | } |
802 | |
803 | public function getHelpUrls() { |
804 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare'; |
805 | } |
806 | } |