Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 487 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryRevisionsBase | |
0.00% |
0 / 487 |
|
0.00% |
0 / 12 |
18360 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
parseParameters | |
0.00% |
0 / 100 |
|
0.00% |
0 / 1 |
1406 | |||
checkRevDel | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
extractRevisionInfo | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
1260 | |||
extractAllSlotInfo | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
110 | |||
extractSlotInfo | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
210 | |||
extractDeprecatedContent | |
0.00% |
0 / 102 |
|
0.00% |
0 / 1 |
702 | |||
getUserForPreview | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getCacheMode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedParams | |
0.00% |
0 / 105 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | use MediaWiki\CommentFormatter\CommentFormatter; |
24 | use MediaWiki\Content\IContentHandlerFactory; |
25 | use MediaWiki\Content\Renderer\ContentRenderer; |
26 | use MediaWiki\Content\Transform\ContentTransformer; |
27 | use MediaWiki\Logger\LoggerFactory; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\MediaWikiServices; |
30 | use MediaWiki\Parser\Parser; |
31 | use MediaWiki\Revision\RevisionAccessException; |
32 | use MediaWiki\Revision\RevisionRecord; |
33 | use MediaWiki\Revision\RevisionStore; |
34 | use MediaWiki\Revision\SlotRecord; |
35 | use MediaWiki\Revision\SlotRoleRegistry; |
36 | use MediaWiki\Title\Title; |
37 | use MediaWiki\User\TempUser\TempUserCreator; |
38 | use MediaWiki\User\UserFactory; |
39 | use MediaWiki\User\UserNameUtils; |
40 | use Wikimedia\ParamValidator\ParamValidator; |
41 | use Wikimedia\ParamValidator\TypeDef\EnumDef; |
42 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
43 | |
44 | /** |
45 | * A base class for functions common to producing a list of revisions. |
46 | * |
47 | * @stable to extend |
48 | * |
49 | * @ingroup API |
50 | */ |
51 | abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { |
52 | |
53 | // region Constants for internal use. Don't use externally. |
54 | /** @name Constants for internal use. Don't use externally. */ |
55 | |
56 | // Bits to indicate the results of the revdel permission check on a revision, |
57 | // see self::checkRevDel() |
58 | private const IS_DELETED = 1; // Whether the field is revision-deleted |
59 | private const CANNOT_VIEW = 2; // Whether the user cannot view the field due to revdel |
60 | |
61 | private const LIMIT_PARSE = 1; |
62 | |
63 | // endregion |
64 | |
65 | // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.WrongStyle |
66 | protected $limit; |
67 | protected $diffto; |
68 | protected $difftotext; |
69 | protected $difftotextpst; |
70 | protected $expandTemplates; |
71 | protected $generateXML; |
72 | protected $section; |
73 | protected $parseContent; |
74 | protected $fetchContent; |
75 | protected $contentFormat; |
76 | protected bool $setParsedLimit = true; |
77 | protected ?array $slotRoles = null; |
78 | protected $slotContentFormats; |
79 | protected $needSlots; |
80 | |
81 | protected bool $fld_ids = false; |
82 | protected bool $fld_flags = false; |
83 | protected bool $fld_timestamp = false; |
84 | protected bool $fld_size = false; |
85 | protected bool $fld_slotsize = false; |
86 | protected bool $fld_sha1 = false; |
87 | protected bool $fld_slotsha1 = false; |
88 | protected bool $fld_comment = false; |
89 | protected bool $fld_parsedcomment = false; |
90 | protected bool $fld_user = false; |
91 | protected bool $fld_userid = false; |
92 | protected bool $fld_content = false; |
93 | protected bool $fld_tags = false; |
94 | protected bool $fld_contentmodel = false; |
95 | protected bool $fld_roles = false; |
96 | protected bool $fld_parsetree = false; |
97 | |
98 | /** |
99 | * The number of uncached diffs that had to be generated for this request. |
100 | * @var int |
101 | */ |
102 | private $numUncachedDiffs = 0; |
103 | |
104 | private RevisionStore $revisionStore; |
105 | private IContentHandlerFactory $contentHandlerFactory; |
106 | private ParserFactory $parserFactory; |
107 | private SlotRoleRegistry $slotRoleRegistry; |
108 | private ContentRenderer $contentRenderer; |
109 | private ContentTransformer $contentTransformer; |
110 | private CommentFormatter $commentFormatter; |
111 | private TempUserCreator $tempUserCreator; |
112 | private UserFactory $userFactory; |
113 | private UserNameUtils $userNameUtils; |
114 | |
115 | /** |
116 | * @since 1.37 Support injection of services |
117 | * @stable to call |
118 | * @param ApiQuery $queryModule |
119 | * @param string $moduleName |
120 | * @param string $paramPrefix |
121 | * @param RevisionStore|null $revisionStore |
122 | * @param IContentHandlerFactory|null $contentHandlerFactory |
123 | * @param ParserFactory|null $parserFactory |
124 | * @param SlotRoleRegistry|null $slotRoleRegistry |
125 | * @param ContentRenderer|null $contentRenderer |
126 | * @param ContentTransformer|null $contentTransformer |
127 | * @param CommentFormatter|null $commentFormatter |
128 | * @param TempUserCreator|null $tempUserCreator |
129 | * @param UserFactory|null $userFactory |
130 | * @param UserNameUtils|null $userNameUtils |
131 | */ |
132 | public function __construct( |
133 | ApiQuery $queryModule, |
134 | $moduleName, |
135 | $paramPrefix = '', |
136 | RevisionStore $revisionStore = null, |
137 | IContentHandlerFactory $contentHandlerFactory = null, |
138 | ParserFactory $parserFactory = null, |
139 | SlotRoleRegistry $slotRoleRegistry = null, |
140 | ContentRenderer $contentRenderer = null, |
141 | ContentTransformer $contentTransformer = null, |
142 | CommentFormatter $commentFormatter = null, |
143 | TempUserCreator $tempUserCreator = null, |
144 | UserFactory $userFactory = null, |
145 | UserNameUtils $userNameUtils = null |
146 | ) { |
147 | parent::__construct( $queryModule, $moduleName, $paramPrefix ); |
148 | // This class is part of the stable interface and |
149 | // therefor fallback to global state, if services are not provided |
150 | $services = MediaWikiServices::getInstance(); |
151 | $this->revisionStore = $revisionStore ?? $services->getRevisionStore(); |
152 | $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory(); |
153 | $this->parserFactory = $parserFactory ?? $services->getParserFactory(); |
154 | $this->slotRoleRegistry = $slotRoleRegistry ?? $services->getSlotRoleRegistry(); |
155 | $this->contentRenderer = $contentRenderer ?? $services->getContentRenderer(); |
156 | $this->contentTransformer = $contentTransformer ?? $services->getContentTransformer(); |
157 | $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter(); |
158 | $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator(); |
159 | $this->userFactory = $userFactory ?? $services->getUserFactory(); |
160 | $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils(); |
161 | } |
162 | |
163 | public function execute() { |
164 | $this->run(); |
165 | } |
166 | |
167 | public function executeGenerator( $resultPageSet ) { |
168 | $this->run( $resultPageSet ); |
169 | } |
170 | |
171 | /** |
172 | * @param ApiPageSet|null $resultPageSet |
173 | * @return void |
174 | */ |
175 | abstract protected function run( ApiPageSet $resultPageSet = null ); |
176 | |
177 | /** |
178 | * Parse the parameters into the various instance fields. |
179 | * |
180 | * @param array $params |
181 | */ |
182 | protected function parseParameters( $params ) { |
183 | $prop = array_fill_keys( $params['prop'], true ); |
184 | |
185 | $this->fld_ids = isset( $prop['ids'] ); |
186 | $this->fld_flags = isset( $prop['flags'] ); |
187 | $this->fld_timestamp = isset( $prop['timestamp'] ); |
188 | $this->fld_comment = isset( $prop['comment'] ); |
189 | $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); |
190 | $this->fld_size = isset( $prop['size'] ); |
191 | $this->fld_slotsize = isset( $prop['slotsize'] ); |
192 | $this->fld_sha1 = isset( $prop['sha1'] ); |
193 | $this->fld_slotsha1 = isset( $prop['slotsha1'] ); |
194 | $this->fld_content = isset( $prop['content'] ); |
195 | $this->fld_contentmodel = isset( $prop['contentmodel'] ); |
196 | $this->fld_userid = isset( $prop['userid'] ); |
197 | $this->fld_user = isset( $prop['user'] ); |
198 | $this->fld_tags = isset( $prop['tags'] ); |
199 | $this->fld_roles = isset( $prop['roles'] ); |
200 | $this->fld_parsetree = isset( $prop['parsetree'] ); |
201 | |
202 | $this->slotRoles = $params['slots']; |
203 | |
204 | if ( $this->slotRoles !== null ) { |
205 | if ( $this->fld_parsetree ) { |
206 | $this->dieWithError( [ |
207 | 'apierror-invalidparammix-cannotusewith', |
208 | $this->encodeParamName( 'prop=parsetree' ), |
209 | $this->encodeParamName( 'slots' ), |
210 | ], 'invalidparammix' ); |
211 | } |
212 | foreach ( [ |
213 | 'expandtemplates', 'generatexml', 'parse', 'diffto', 'difftotext', 'difftotextpst', |
214 | 'contentformat' |
215 | ] as $p ) { |
216 | if ( $params[$p] !== null && $params[$p] !== false ) { |
217 | $this->dieWithError( [ |
218 | 'apierror-invalidparammix-cannotusewith', |
219 | $this->encodeParamName( $p ), |
220 | $this->encodeParamName( 'slots' ), |
221 | ], 'invalidparammix' ); |
222 | } |
223 | } |
224 | $this->slotContentFormats = []; |
225 | foreach ( $this->slotRoles as $slotRole ) { |
226 | if ( isset( $params['contentformat-' . $slotRole] ) ) { |
227 | $this->slotContentFormats[$slotRole] = $params['contentformat-' . $slotRole]; |
228 | } |
229 | } |
230 | } |
231 | |
232 | if ( !empty( $params['contentformat'] ) ) { |
233 | $this->contentFormat = $params['contentformat']; |
234 | } |
235 | |
236 | $this->limit = $params['limit']; |
237 | |
238 | if ( $params['difftotext'] !== null ) { |
239 | $this->difftotext = $params['difftotext']; |
240 | $this->difftotextpst = $params['difftotextpst']; |
241 | } elseif ( $params['diffto'] !== null ) { |
242 | if ( $params['diffto'] == 'cur' ) { |
243 | $params['diffto'] = 0; |
244 | } |
245 | if ( ( !ctype_digit( $params['diffto'] ) || $params['diffto'] < 0 ) |
246 | && $params['diffto'] != 'prev' && $params['diffto'] != 'next' |
247 | ) { |
248 | $p = $this->getModulePrefix(); |
249 | $this->dieWithError( [ 'apierror-baddiffto', $p ], 'diffto' ); |
250 | } |
251 | // Check whether the revision exists and is readable, |
252 | // DifferenceEngine returns a rather ambiguous empty |
253 | // string if that's not the case |
254 | if ( is_numeric( $params['diffto'] ) && $params['diffto'] != 0 ) { |
255 | $difftoRev = $this->revisionStore->getRevisionById( $params['diffto'] ); |
256 | if ( !$difftoRev ) { |
257 | $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] ); |
258 | } |
259 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141 |
260 | $revDel = $this->checkRevDel( $difftoRev, RevisionRecord::DELETED_TEXT ); |
261 | if ( $revDel & self::CANNOT_VIEW ) { |
262 | $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] ); |
263 | $params['diffto'] = null; |
264 | } |
265 | } |
266 | $this->diffto = $params['diffto']; |
267 | } |
268 | |
269 | $this->fetchContent = $this->fld_content || $this->diffto !== null |
270 | || $this->difftotext !== null || $this->fld_parsetree; |
271 | |
272 | $smallLimit = false; |
273 | if ( $this->fetchContent ) { |
274 | $smallLimit = true; |
275 | $this->expandTemplates = $params['expandtemplates']; |
276 | $this->generateXML = $params['generatexml']; |
277 | $this->parseContent = $params['parse']; |
278 | if ( $this->parseContent ) { |
279 | // Must manually initialize unset limit |
280 | $this->limit ??= self::LIMIT_PARSE; |
281 | } |
282 | $this->section = $params['section'] ?? false; |
283 | } |
284 | |
285 | $userMax = $this->parseContent ? self::LIMIT_PARSE : |
286 | ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); |
287 | $botMax = $this->parseContent ? self::LIMIT_PARSE : |
288 | ( $smallLimit ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); |
289 | if ( $this->limit == 'max' ) { |
290 | $this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; |
291 | if ( $this->setParsedLimit ) { |
292 | $this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit ); |
293 | } |
294 | } |
295 | |
296 | $this->limit = $this->getMain()->getParamValidator()->validateValue( |
297 | $this, 'limit', $this->limit ?? 10, [ |
298 | ParamValidator::PARAM_TYPE => 'limit', |
299 | IntegerDef::PARAM_MIN => 1, |
300 | IntegerDef::PARAM_MAX => $userMax, |
301 | IntegerDef::PARAM_MAX2 => $botMax, |
302 | IntegerDef::PARAM_IGNORE_RANGE => true, |
303 | ] |
304 | ); |
305 | |
306 | $this->needSlots = $this->fetchContent || $this->fld_contentmodel || |
307 | $this->fld_slotsize || $this->fld_slotsha1; |
308 | if ( $this->needSlots && $this->slotRoles === null ) { |
309 | $encParam = $this->encodeParamName( 'slots' ); |
310 | $name = $this->getModuleName(); |
311 | $parent = $this->getParent(); |
312 | $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) ); |
313 | $this->addDeprecation( |
314 | [ 'apiwarn-deprecation-missingparam', $encParam ], |
315 | "action=query&{$parentParam}={$name}&!{$encParam}" |
316 | ); |
317 | } |
318 | } |
319 | |
320 | /** |
321 | * Test revision deletion status |
322 | * @param RevisionRecord $revision Revision to check |
323 | * @param int $field One of the RevisionRecord::DELETED_* constants |
324 | * @return int Revision deletion status flags. Bitwise OR of |
325 | * self::IS_DELETED and self::CANNOT_VIEW, as appropriate. |
326 | */ |
327 | private function checkRevDel( RevisionRecord $revision, $field ) { |
328 | $ret = $revision->isDeleted( $field ) ? self::IS_DELETED : 0; |
329 | if ( $ret ) { |
330 | $canSee = $revision->userCan( $field, $this->getAuthority() ); |
331 | $ret |= ( $canSee ? 0 : self::CANNOT_VIEW ); |
332 | } |
333 | return $ret; |
334 | } |
335 | |
336 | /** |
337 | * Extract information from the RevisionRecord |
338 | * |
339 | * @since 1.32, takes a RevisionRecord instead of a Revision |
340 | * @param RevisionRecord $revision |
341 | * @param stdClass $row Should have a field 'ts_tags' if $this->fld_tags is set |
342 | * @return array |
343 | */ |
344 | protected function extractRevisionInfo( RevisionRecord $revision, $row ) { |
345 | $vals = []; |
346 | $anyHidden = false; |
347 | |
348 | if ( $this->fld_ids ) { |
349 | $vals['revid'] = (int)$revision->getId(); |
350 | if ( $revision->getParentId() !== null ) { |
351 | $vals['parentid'] = (int)$revision->getParentId(); |
352 | } |
353 | } |
354 | |
355 | if ( $this->fld_flags ) { |
356 | $vals['minor'] = $revision->isMinor(); |
357 | } |
358 | |
359 | if ( $this->fld_user || $this->fld_userid ) { |
360 | $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER ); |
361 | if ( $revDel & self::IS_DELETED ) { |
362 | $vals['userhidden'] = true; |
363 | $anyHidden = true; |
364 | } |
365 | if ( !( $revDel & self::CANNOT_VIEW ) ) { |
366 | $u = $revision->getUser( RevisionRecord::RAW ); |
367 | if ( $this->fld_user ) { |
368 | $vals['user'] = $u->getName(); |
369 | } |
370 | if ( $this->userNameUtils->isTemp( $u->getName() ) ) { |
371 | $vals['temp'] = true; |
372 | } |
373 | if ( !$u->isRegistered() ) { |
374 | $vals['anon'] = true; |
375 | } |
376 | |
377 | if ( $this->fld_userid ) { |
378 | $vals['userid'] = $u->getId(); |
379 | } |
380 | } |
381 | } |
382 | |
383 | if ( $this->fld_timestamp ) { |
384 | $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $revision->getTimestamp() ); |
385 | } |
386 | |
387 | if ( $this->fld_size ) { |
388 | try { |
389 | $vals['size'] = (int)$revision->getSize(); |
390 | } catch ( RevisionAccessException $e ) { |
391 | // Back compat: If there's no size, return 0. |
392 | // @todo: Gergő says to mention T198099 as a "todo" here. |
393 | $vals['size'] = 0; |
394 | } |
395 | } |
396 | |
397 | if ( $this->fld_sha1 ) { |
398 | $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT ); |
399 | if ( $revDel & self::IS_DELETED ) { |
400 | $vals['sha1hidden'] = true; |
401 | $anyHidden = true; |
402 | } |
403 | if ( !( $revDel & self::CANNOT_VIEW ) ) { |
404 | try { |
405 | $vals['sha1'] = Wikimedia\base_convert( $revision->getSha1(), 36, 16, 40 ); |
406 | } catch ( RevisionAccessException $e ) { |
407 | // Back compat: If there's no sha1, return empty string. |
408 | // @todo: Gergő says to mention T198099 as a "todo" here. |
409 | $vals['sha1'] = ''; |
410 | } |
411 | } |
412 | } |
413 | |
414 | try { |
415 | if ( $this->fld_roles ) { |
416 | $vals['roles'] = $revision->getSlotRoles(); |
417 | } |
418 | |
419 | if ( $this->needSlots ) { |
420 | $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT ); |
421 | if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) { |
422 | $anyHidden = true; |
423 | } |
424 | $vals = array_merge( $vals, $this->extractAllSlotInfo( $revision, $revDel ) ); |
425 | } |
426 | } catch ( RevisionAccessException $ex ) { |
427 | // This is here so T212428 doesn't spam the log. |
428 | // TODO: find out why T212428 happens in the first place! |
429 | $vals['slotsmissing'] = true; |
430 | |
431 | LoggerFactory::getInstance( 'api-warning' )->error( |
432 | 'Failed to access revision slots', |
433 | [ 'revision' => $revision->getId(), 'exception' => $ex, ] |
434 | ); |
435 | } |
436 | |
437 | if ( $this->fld_comment || $this->fld_parsedcomment ) { |
438 | $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT ); |
439 | if ( $revDel & self::IS_DELETED ) { |
440 | $vals['commenthidden'] = true; |
441 | $anyHidden = true; |
442 | } |
443 | if ( !( $revDel & self::CANNOT_VIEW ) ) { |
444 | $comment = $revision->getComment( RevisionRecord::RAW ); |
445 | $comment = $comment->text ?? ''; |
446 | |
447 | if ( $this->fld_comment ) { |
448 | $vals['comment'] = $comment; |
449 | } |
450 | |
451 | if ( $this->fld_parsedcomment ) { |
452 | $vals['parsedcomment'] = $this->commentFormatter->format( |
453 | $comment, Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ) |
454 | ); |
455 | } |
456 | } |
457 | } |
458 | |
459 | if ( $this->fld_tags ) { |
460 | if ( $row->ts_tags ) { |
461 | $tags = explode( ',', $row->ts_tags ); |
462 | ApiResult::setIndexedTagName( $tags, 'tag' ); |
463 | $vals['tags'] = $tags; |
464 | } else { |
465 | $vals['tags'] = []; |
466 | } |
467 | } |
468 | |
469 | if ( $anyHidden && $revision->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) { |
470 | $vals['suppressed'] = true; |
471 | } |
472 | |
473 | return $vals; |
474 | } |
475 | |
476 | /** |
477 | * Extracts information about all relevant slots. |
478 | * |
479 | * @param RevisionRecord $revision |
480 | * @param int $revDel |
481 | * |
482 | * @return array |
483 | * @throws ApiUsageException |
484 | */ |
485 | private function extractAllSlotInfo( RevisionRecord $revision, $revDel ): array { |
486 | $vals = []; |
487 | |
488 | if ( $this->slotRoles === null ) { |
489 | try { |
490 | $slot = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ); |
491 | } catch ( RevisionAccessException $e ) { |
492 | // Back compat: If there's no slot, there's no content, so set 'textmissing' |
493 | // @todo: Gergő says to mention T198099 as a "todo" here. |
494 | $vals['textmissing'] = true; |
495 | $slot = null; |
496 | } |
497 | |
498 | if ( $slot ) { |
499 | $content = null; |
500 | $vals += $this->extractSlotInfo( $slot, $revDel, $content ); |
501 | if ( !empty( $vals['nosuchsection'] ) ) { |
502 | $this->dieWithError( |
503 | [ |
504 | 'apierror-nosuchsection-what', |
505 | wfEscapeWikiText( $this->section ), |
506 | $this->msg( 'revid', $revision->getId() ) |
507 | ], |
508 | 'nosuchsection' |
509 | ); |
510 | } |
511 | if ( $content ) { |
512 | $vals += $this->extractDeprecatedContent( $content, $revision ); |
513 | } |
514 | } |
515 | } else { |
516 | $roles = array_intersect( $this->slotRoles, $revision->getSlotRoles() ); |
517 | $vals['slots'] = [ |
518 | ApiResult::META_KVP_MERGE => true, |
519 | ]; |
520 | foreach ( $roles as $role ) { |
521 | try { |
522 | $slot = $revision->getSlot( $role, RevisionRecord::RAW ); |
523 | } catch ( RevisionAccessException $e ) { |
524 | // Don't error out here so the client can still process other slots/revisions. |
525 | // @todo: Gergő says to mention T198099 as a "todo" here. |
526 | $vals['slots'][$role]['missing'] = true; |
527 | continue; |
528 | } |
529 | $content = null; |
530 | $vals['slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content ); |
531 | // @todo Move this into extractSlotInfo() (and remove its $content parameter) |
532 | // when extractDeprecatedContent() is no more. |
533 | if ( $content ) { |
534 | /** @var Content $content */ |
535 | $model = $content->getModel(); |
536 | $format = $this->slotContentFormats[$role] ?? $content->getDefaultFormat(); |
537 | if ( !$content->isSupportedFormat( $format ) ) { |
538 | $this->addWarning( [ |
539 | 'apierror-badformat', |
540 | $format, |
541 | $model, |
542 | $this->msg( 'revid', $revision->getId() ) |
543 | ] ); |
544 | $vals['slots'][$role]['badcontentformat'] = true; |
545 | } else { |
546 | $vals['slots'][$role]['contentmodel'] = $model; |
547 | $vals['slots'][$role]['contentformat'] = $format; |
548 | ApiResult::setContentValue( |
549 | $vals['slots'][$role], |
550 | 'content', |
551 | $content->serialize( $format ) |
552 | ); |
553 | } |
554 | } |
555 | } |
556 | ApiResult::setArrayType( $vals['slots'], 'kvp', 'role' ); |
557 | ApiResult::setIndexedTagName( $vals['slots'], 'slot' ); |
558 | } |
559 | return $vals; |
560 | } |
561 | |
562 | /** |
563 | * Extract information from the SlotRecord |
564 | * |
565 | * @param SlotRecord $slot |
566 | * @param int $revDel Revdel status flags, from self::checkRevDel() |
567 | * @param Content|null &$content Set to the slot's content, if available |
568 | * and $this->fetchContent is true |
569 | * @return array |
570 | */ |
571 | private function extractSlotInfo( SlotRecord $slot, $revDel, &$content = null ) { |
572 | $vals = []; |
573 | ApiResult::setArrayType( $vals, 'assoc' ); |
574 | |
575 | if ( $this->fld_slotsize ) { |
576 | $vals['size'] = (int)$slot->getSize(); |
577 | } |
578 | |
579 | if ( $this->fld_slotsha1 ) { |
580 | if ( $revDel & self::IS_DELETED ) { |
581 | $vals['sha1hidden'] = true; |
582 | } |
583 | if ( !( $revDel & self::CANNOT_VIEW ) ) { |
584 | if ( $slot->getSha1() != '' ) { |
585 | $vals['sha1'] = Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 ); |
586 | } else { |
587 | $vals['sha1'] = ''; |
588 | } |
589 | } |
590 | } |
591 | |
592 | if ( $this->fld_contentmodel ) { |
593 | $vals['contentmodel'] = $slot->getModel(); |
594 | } |
595 | |
596 | $content = null; |
597 | if ( $this->fetchContent ) { |
598 | if ( $revDel & self::IS_DELETED ) { |
599 | $vals['texthidden'] = true; |
600 | } |
601 | if ( !( $revDel & self::CANNOT_VIEW ) ) { |
602 | try { |
603 | $content = $slot->getContent(); |
604 | } catch ( RevisionAccessException $e ) { |
605 | // @todo: Gergő says to mention T198099 as a "todo" here. |
606 | $vals['textmissing'] = true; |
607 | } |
608 | // Expand templates after getting section content because |
609 | // template-added sections don't count and Parser::preprocess() |
610 | // will have less input |
611 | if ( $content && $this->section !== false ) { |
612 | $content = $content->getSection( $this->section ); |
613 | if ( !$content ) { |
614 | $vals['nosuchsection'] = true; |
615 | } |
616 | } |
617 | } |
618 | } |
619 | |
620 | return $vals; |
621 | } |
622 | |
623 | /** |
624 | * Format a Content using deprecated options |
625 | * @param Content $content Content to format |
626 | * @param RevisionRecord $revision Revision being processed |
627 | * @return array |
628 | */ |
629 | private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) { |
630 | $vals = []; |
631 | $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); |
632 | |
633 | if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) { |
634 | if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { |
635 | /** @var WikitextContent $content */ |
636 | '@phan-var WikitextContent $content'; |
637 | $t = $content->getText(); # note: don't set $text |
638 | |
639 | $parser = $this->parserFactory->create(); |
640 | $parser->startExternalParse( |
641 | $title, |
642 | ParserOptions::newFromContext( $this->getContext() ), |
643 | Parser::OT_PREPROCESS |
644 | ); |
645 | $dom = $parser->preprocessToDom( $t ); |
646 | // @phan-suppress-next-line PhanUndeclaredMethodInCallable |
647 | if ( is_callable( [ $dom, 'saveXML' ] ) ) { |
648 | // @phan-suppress-next-line PhanUndeclaredMethod |
649 | $xml = $dom->saveXML(); |
650 | } else { |
651 | // @phan-suppress-next-line PhanUndeclaredMethod |
652 | $xml = $dom->__toString(); |
653 | } |
654 | $vals['parsetree'] = $xml; |
655 | } else { |
656 | $vals['badcontentformatforparsetree'] = true; |
657 | $this->addWarning( |
658 | [ |
659 | 'apierror-parsetree-notwikitext-title', |
660 | wfEscapeWikiText( $title->getPrefixedText() ), |
661 | $content->getModel() |
662 | ], |
663 | 'parsetree-notwikitext' |
664 | ); |
665 | } |
666 | } |
667 | |
668 | if ( $this->fld_content ) { |
669 | $text = null; |
670 | |
671 | if ( $this->expandTemplates && !$this->parseContent ) { |
672 | if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { |
673 | /** @var WikitextContent $content */ |
674 | '@phan-var WikitextContent $content'; |
675 | $text = $content->getText(); |
676 | |
677 | $text = $this->parserFactory->create()->preprocess( |
678 | $text, |
679 | $title, |
680 | ParserOptions::newFromContext( $this->getContext() ) |
681 | ); |
682 | } else { |
683 | $this->addWarning( [ |
684 | 'apierror-templateexpansion-notwikitext', |
685 | wfEscapeWikiText( $title->getPrefixedText() ), |
686 | $content->getModel() |
687 | ] ); |
688 | $vals['badcontentformat'] = true; |
689 | $text = false; |
690 | } |
691 | } |
692 | if ( $this->parseContent ) { |
693 | $po = $this->contentRenderer->getParserOutput( |
694 | $content, |
695 | $title, |
696 | $revision, |
697 | ParserOptions::newFromContext( $this->getContext() ) |
698 | ); |
699 | $text = $po->getText(); |
700 | } |
701 | |
702 | if ( $text === null ) { |
703 | $format = $this->contentFormat ?: $content->getDefaultFormat(); |
704 | $model = $content->getModel(); |
705 | |
706 | if ( !$content->isSupportedFormat( $format ) ) { |
707 | $name = wfEscapeWikiText( $title->getPrefixedText() ); |
708 | $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] ); |
709 | $vals['badcontentformat'] = true; |
710 | $text = false; |
711 | } else { |
712 | $text = $content->serialize( $format ); |
713 | // always include format and model. |
714 | // Format is needed to deserialize, model is needed to interpret. |
715 | $vals['contentformat'] = $format; |
716 | $vals['contentmodel'] = $model; |
717 | } |
718 | } |
719 | |
720 | if ( $text !== false ) { |
721 | ApiResult::setContentValue( $vals, 'content', $text ); |
722 | } |
723 | } |
724 | |
725 | if ( $content && ( $this->diffto !== null || $this->difftotext !== null ) ) { |
726 | if ( $this->numUncachedDiffs < $this->getConfig()->get( MainConfigNames::APIMaxUncachedDiffs ) ) { |
727 | $vals['diff'] = []; |
728 | $context = new DerivativeContext( $this->getContext() ); |
729 | $context->setTitle( $title ); |
730 | $handler = $content->getContentHandler(); |
731 | |
732 | if ( $this->difftotext !== null ) { |
733 | $model = $title->getContentModel(); |
734 | |
735 | if ( $this->contentFormat |
736 | && !$this->contentHandlerFactory->getContentHandler( $model ) |
737 | ->isSupportedFormat( $this->contentFormat ) |
738 | ) { |
739 | $name = wfEscapeWikiText( $title->getPrefixedText() ); |
740 | $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] ); |
741 | $vals['diff']['badcontentformat'] = true; |
742 | $engine = null; |
743 | } else { |
744 | $difftocontent = $this->contentHandlerFactory->getContentHandler( $model ) |
745 | ->unserializeContent( $this->difftotext, $this->contentFormat ); |
746 | |
747 | if ( $this->difftotextpst ) { |
748 | $popts = ParserOptions::newFromContext( $this->getContext() ); |
749 | $difftocontent = $this->contentTransformer->preSaveTransform( |
750 | $difftocontent, |
751 | $title, |
752 | $this->getUserForPreview(), |
753 | $popts |
754 | ); |
755 | } |
756 | |
757 | $engine = $handler->createDifferenceEngine( $context ); |
758 | $engine->setContent( $content, $difftocontent ); |
759 | } |
760 | } else { |
761 | $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto ); |
762 | $vals['diff']['from'] = $engine->getOldid(); |
763 | $vals['diff']['to'] = $engine->getNewid(); |
764 | } |
765 | if ( $engine ) { |
766 | $difftext = $engine->getDiffBody(); |
767 | ApiResult::setContentValue( $vals['diff'], 'body', $difftext ); |
768 | if ( !$engine->wasCacheHit() ) { |
769 | $this->numUncachedDiffs++; |
770 | } |
771 | foreach ( $engine->getRevisionLoadErrors() as $msg ) { |
772 | $this->addWarning( $msg ); |
773 | } |
774 | } |
775 | } else { |
776 | $vals['diff']['notcached'] = true; |
777 | } |
778 | } |
779 | |
780 | return $vals; |
781 | } |
782 | |
783 | private function getUserForPreview() { |
784 | $user = $this->getUser(); |
785 | if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) { |
786 | return $this->userFactory->newUnsavedTempUser( |
787 | $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() ) |
788 | ); |
789 | } |
790 | return $user; |
791 | } |
792 | |
793 | /** |
794 | * @stable to override |
795 | * @param array $params |
796 | * |
797 | * @return string |
798 | */ |
799 | public function getCacheMode( $params ) { |
800 | if ( $this->userCanSeeRevDel() ) { |
801 | return 'private'; |
802 | } |
803 | |
804 | return 'public'; |
805 | } |
806 | |
807 | /** |
808 | * @stable to override |
809 | * @return array |
810 | */ |
811 | public function getAllowedParams() { |
812 | $slotRoles = $this->slotRoleRegistry->getKnownRoles(); |
813 | sort( $slotRoles, SORT_STRING ); |
814 | $smallLimit = $this->getMain()->canApiHighLimits() ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_SML1; |
815 | |
816 | return [ |
817 | 'prop' => [ |
818 | ParamValidator::PARAM_ISMULTI => true, |
819 | ParamValidator::PARAM_DEFAULT => 'ids|timestamp|flags|comment|user', |
820 | ParamValidator::PARAM_TYPE => [ |
821 | 'ids', |
822 | 'flags', |
823 | 'timestamp', |
824 | 'user', |
825 | 'userid', |
826 | 'size', |
827 | 'slotsize', |
828 | 'sha1', |
829 | 'slotsha1', |
830 | 'contentmodel', |
831 | 'comment', |
832 | 'parsedcomment', |
833 | 'content', |
834 | 'tags', |
835 | 'roles', |
836 | 'parsetree', |
837 | ], |
838 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop', |
839 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [ |
840 | 'ids' => 'apihelp-query+revisions+base-paramvalue-prop-ids', |
841 | 'flags' => 'apihelp-query+revisions+base-paramvalue-prop-flags', |
842 | 'timestamp' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp', |
843 | 'user' => 'apihelp-query+revisions+base-paramvalue-prop-user', |
844 | 'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid', |
845 | 'size' => 'apihelp-query+revisions+base-paramvalue-prop-size', |
846 | 'slotsize' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize', |
847 | 'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1', |
848 | 'slotsha1' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1', |
849 | 'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel', |
850 | 'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment', |
851 | 'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment', |
852 | 'content' => [ 'apihelp-query+revisions+base-paramvalue-prop-content', $smallLimit ], |
853 | 'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags', |
854 | 'roles' => 'apihelp-query+revisions+base-paramvalue-prop-roles', |
855 | 'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree', |
856 | CONTENT_MODEL_WIKITEXT, $smallLimit ], |
857 | ], |
858 | EnumDef::PARAM_DEPRECATED_VALUES => [ |
859 | 'parsetree' => true, |
860 | ], |
861 | ], |
862 | 'slots' => [ |
863 | ParamValidator::PARAM_TYPE => $slotRoles, |
864 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-slots', |
865 | ParamValidator::PARAM_ISMULTI => true, |
866 | ParamValidator::PARAM_ALL => true, |
867 | ], |
868 | 'contentformat-{slot}' => [ |
869 | ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], |
870 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat-slot', |
871 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(), |
872 | ], |
873 | 'limit' => [ |
874 | ParamValidator::PARAM_TYPE => 'limit', |
875 | IntegerDef::PARAM_MIN => 1, |
876 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
877 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2, |
878 | ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-limit', |
879 | $smallLimit, self::LIMIT_PARSE ], |
880 | ], |
881 | 'expandtemplates' => [ |
882 | ParamValidator::PARAM_DEFAULT => false, |
883 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates', |
884 | ParamValidator::PARAM_DEPRECATED => true, |
885 | ], |
886 | 'generatexml' => [ |
887 | ParamValidator::PARAM_DEFAULT => false, |
888 | ParamValidator::PARAM_DEPRECATED => true, |
889 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml', |
890 | ], |
891 | 'parse' => [ |
892 | ParamValidator::PARAM_DEFAULT => false, |
893 | ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-parse', self::LIMIT_PARSE ], |
894 | ParamValidator::PARAM_DEPRECATED => true, |
895 | ], |
896 | 'section' => [ |
897 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section', |
898 | ], |
899 | 'diffto' => [ |
900 | ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-diffto', $smallLimit ], |
901 | ParamValidator::PARAM_DEPRECATED => true, |
902 | ], |
903 | 'difftotext' => [ |
904 | ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-difftotext', $smallLimit ], |
905 | ParamValidator::PARAM_DEPRECATED => true, |
906 | ], |
907 | 'difftotextpst' => [ |
908 | ParamValidator::PARAM_DEFAULT => false, |
909 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst', |
910 | ParamValidator::PARAM_DEPRECATED => true, |
911 | ], |
912 | 'contentformat' => [ |
913 | ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(), |
914 | ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat', |
915 | ParamValidator::PARAM_DEPRECATED => true, |
916 | ], |
917 | ]; |
918 | } |
919 | } |