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