38use Wikimedia\Timestamp\TimestampFormat as TS;
54 private const IS_DELETED = 1;
55 private const CANNOT_VIEW = 2;
57 private const LIMIT_PARSE = 1;
109 private $numUncachedDiffs = 0;
129 string $paramPrefix =
'',
141 parent::__construct( $queryModule, $moduleName, $paramPrefix );
145 $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
146 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
147 $this->parserFactory = $parserFactory ?? $services->getParserFactory();
148 $this->slotRoleRegistry = $slotRoleRegistry ?? $services->getSlotRoleRegistry();
149 $this->contentRenderer = $contentRenderer ?? $services->getContentRenderer();
150 $this->contentTransformer = $contentTransformer ?? $services->getContentTransformer();
151 $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
152 $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator();
153 $this->userFactory = $userFactory ?? $services->getUserFactory();
154 $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
163 $this->
run( $resultPageSet );
178 $prop = array_fill_keys( $params[
'prop'],
true );
180 $this->fld_ids = isset( $prop[
'ids'] );
181 $this->fld_flags = isset( $prop[
'flags'] );
182 $this->fld_timestamp = isset( $prop[
'timestamp'] );
183 $this->fld_comment = isset( $prop[
'comment'] );
184 $this->fld_parsedcomment = isset( $prop[
'parsedcomment'] );
185 $this->fld_size = isset( $prop[
'size'] );
186 $this->fld_slotsize = isset( $prop[
'slotsize'] );
187 $this->fld_sha1 = isset( $prop[
'sha1'] );
188 $this->fld_slotsha1 = isset( $prop[
'slotsha1'] );
189 $this->fld_content = isset( $prop[
'content'] );
190 $this->fld_contentmodel = isset( $prop[
'contentmodel'] );
191 $this->fld_userid = isset( $prop[
'userid'] );
192 $this->fld_user = isset( $prop[
'user'] );
193 $this->fld_tags = isset( $prop[
'tags'] );
194 $this->fld_roles = isset( $prop[
'roles'] );
195 $this->fld_parsetree = isset( $prop[
'parsetree'] );
197 $this->slotRoles = $params[
'slots'];
199 if ( $this->slotRoles !==
null ) {
200 if ( $this->fld_parsetree ) {
202 'apierror-invalidparammix-cannotusewith',
205 ],
'invalidparammix' );
208 'expandtemplates',
'generatexml',
'parse',
'diffto',
'difftotext',
'difftotextpst',
211 if ( $params[$p] !==
null && $params[$p] !==
false ) {
213 'apierror-invalidparammix-cannotusewith',
216 ],
'invalidparammix' );
219 $this->slotContentFormats = [];
220 foreach ( $this->slotRoles as $slotRole ) {
221 if ( isset( $params[
'contentformat-' . $slotRole] ) ) {
222 $this->slotContentFormats[$slotRole] = $params[
'contentformat-' . $slotRole];
227 if ( !empty( $params[
'contentformat'] ) ) {
228 $this->contentFormat = $params[
'contentformat'];
231 $this->limit = $params[
'limit'];
233 if ( $params[
'difftotext'] !==
null ) {
234 $this->difftotext = $params[
'difftotext'];
235 $this->difftotextpst = $params[
'difftotextpst'];
236 } elseif ( $params[
'diffto'] !==
null ) {
237 if ( $params[
'diffto'] ==
'cur' ) {
238 $params[
'diffto'] = 0;
240 if ( ( !ctype_digit( (
string)$params[
'diffto'] ) || $params[
'diffto'] < 0 )
241 && $params[
'diffto'] !=
'prev' && $params[
'diffto'] !=
'next'
244 $this->
dieWithError( [
'apierror-baddiffto', $p ],
'diffto' );
249 if ( is_numeric( $params[
'diffto'] ) && $params[
'diffto'] != 0 ) {
250 $difftoRev = $this->revisionStore->getRevisionById( $params[
'diffto'] );
252 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'diffto'] ] );
254 $revDel = $this->checkRevDel( $difftoRev, RevisionRecord::DELETED_TEXT );
255 if ( $revDel & self::CANNOT_VIEW ) {
256 $this->
addWarning( [
'apiwarn-difftohidden', $difftoRev->getId() ] );
257 $params[
'diffto'] =
null;
260 $this->diffto = $params[
'diffto'];
263 $this->fetchContent = $this->fld_content || $this->diffto !==
null
267 if ( $this->fetchContent ) {
269 $this->expandTemplates = $params[
'expandtemplates'];
270 $this->generateXML = $params[
'generatexml'];
271 $this->parseContent = $params[
'parse'];
272 if ( $this->parseContent ) {
274 $this->limit ??= self::LIMIT_PARSE;
276 $this->section = $params[
'section'] ??
false;
279 $userMax = $this->parseContent ? self::LIMIT_PARSE :
281 $botMax = $this->parseContent ? self::LIMIT_PARSE :
283 if ( $this->limit ==
'max' ) {
284 $this->limit = $this->
getMain()->canApiHighLimits() ? $botMax : $userMax;
285 if ( $this->setParsedLimit ) {
290 $this->limit = $this->
getMain()->getParamValidator()->validateValue(
291 $this,
'limit', $this->limit ?? 10, [
292 ParamValidator::PARAM_TYPE =>
'limit',
293 IntegerDef::PARAM_MIN => 1,
294 IntegerDef::PARAM_MAX => $userMax,
295 IntegerDef::PARAM_MAX2 => $botMax,
296 IntegerDef::PARAM_IGNORE_RANGE =>
true,
300 $this->needSlots = $this->fetchContent || $this->fld_contentmodel ||
311 private function checkRevDel(
RevisionRecord $revision, $field ) {
312 $ret = $revision->
isDeleted( $field ) ? self::IS_DELETED : 0;
315 $ret |= ( $canSee ? 0 : self::CANNOT_VIEW );
332 if ( $this->fld_ids ) {
333 $vals[
'revid'] = (int)$revision->
getId();
339 if ( $this->fld_flags ) {
340 $vals[
'minor'] = $revision->
isMinor();
343 if ( $this->fld_user || $this->fld_userid ) {
344 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER );
345 if ( $revDel & self::IS_DELETED ) {
346 $vals[
'userhidden'] =
true;
349 if ( !( $revDel & self::CANNOT_VIEW ) ) {
350 $u = $revision->
getUser( RevisionRecord::RAW );
351 if ( $this->fld_user ) {
352 $vals[
'user'] = $u->getName();
354 if ( $this->userNameUtils->isTemp( $u->getName() ) ) {
355 $vals[
'temp'] =
true;
357 if ( !$u->isRegistered() ) {
358 $vals[
'anon'] =
true;
361 if ( $this->fld_userid ) {
362 $vals[
'userid'] = $u->getId();
367 if ( $this->fld_timestamp ) {
371 if ( $this->fld_size ) {
373 $vals[
'size'] = (int)$revision->
getSize();
381 if ( $this->fld_sha1 ) {
382 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
383 if ( $revDel & self::IS_DELETED ) {
384 $vals[
'sha1hidden'] =
true;
387 if ( !( $revDel & self::CANNOT_VIEW ) ) {
389 $vals[
'sha1'] = \Wikimedia\base_convert( $revision->
getSha1(), 36, 16, 40 );
399 if ( $this->fld_roles ) {
403 if ( $this->needSlots ) {
404 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
405 if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) {
408 $vals = array_merge( $vals, $this->extractAllSlotInfo( $revision, $revDel ) );
413 $vals[
'slotsmissing'] =
true;
415 LoggerFactory::getInstance(
'api-warning' )->error(
416 'Failed to access revision slots',
417 [
'revision' => $revision->
getId(),
'exception' => $ex, ]
421 if ( $this->fld_comment || $this->fld_parsedcomment ) {
422 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT );
423 if ( $revDel & self::IS_DELETED ) {
424 $vals[
'commenthidden'] =
true;
427 if ( !( $revDel & self::CANNOT_VIEW ) ) {
428 $comment = $revision->
getComment( RevisionRecord::RAW );
429 $comment = $comment->text ??
'';
431 if ( $this->fld_comment ) {
432 $vals[
'comment'] = $comment;
435 if ( $this->fld_parsedcomment ) {
436 $vals[
'parsedcomment'] = $this->commentFormatter->format(
443 if ( $this->fld_tags ) {
444 if ( $row->ts_tags ) {
445 $tags = explode(
',', $row->ts_tags );
447 $vals[
'tags'] = $tags;
453 if ( $anyHidden && $revision->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
454 $vals[
'suppressed'] =
true;
469 private function extractAllSlotInfo(
RevisionRecord $revision, $revDel ): array {
472 if ( $this->slotRoles ===
null ) {
474 $slot = $revision->
getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
475 }
catch ( RevisionAccessException ) {
478 $vals[
'textmissing'] =
true;
484 $vals += $this->extractSlotInfo( $slot, $revDel, $content );
485 if ( !empty( $vals[
'nosuchsection'] ) ) {
488 'apierror-nosuchsection-what',
490 $this->
msg(
'revid', $revision->
getId() )
496 $vals += $this->extractDeprecatedContent( $content, $revision );
500 $roles = array_intersect( $this->slotRoles, $revision->
getSlotRoles() );
504 foreach ( $roles as $role ) {
506 $slot = $revision->
getSlot( $role, RevisionRecord::RAW );
507 }
catch ( RevisionAccessException ) {
510 $vals[
'slots'][$role][
'missing'] =
true;
514 $vals[
'slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content );
519 $model = $content->getModel();
520 $format = $this->slotContentFormats[$role] ?? $content->getDefaultFormat();
521 if ( !$content->isSupportedFormat( $format ) ) {
523 'apierror-badformat',
526 $this->
msg(
'revid', $revision->
getId() )
528 $vals[
'slots'][$role][
'badcontentformat'] =
true;
530 $vals[
'slots'][$role][
'contentmodel'] = $model;
531 $vals[
'slots'][$role][
'contentformat'] = $format;
533 $vals[
'slots'][$role],
535 $content->serialize( $format )
555 private function extractSlotInfo( SlotRecord $slot, $revDel, &$content =
null ) {
559 if ( $this->fld_slotsize ) {
560 $vals[
'size'] = (int)$slot->getSize();
563 if ( $this->fld_slotsha1 ) {
564 if ( $revDel & self::IS_DELETED ) {
565 $vals[
'sha1hidden'] =
true;
567 if ( !( $revDel & self::CANNOT_VIEW ) ) {
568 if ( $slot->getSha1() !=
'' ) {
569 $vals[
'sha1'] = \Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 );
576 if ( $this->fld_contentmodel ) {
577 $vals[
'contentmodel'] = $slot->getModel();
581 if ( $this->fetchContent ) {
582 if ( $revDel & self::IS_DELETED ) {
583 $vals[
'texthidden'] =
true;
585 if ( !( $revDel & self::CANNOT_VIEW ) ) {
587 $content = $slot->getContent();
588 }
catch ( RevisionAccessException ) {
590 $vals[
'textmissing'] =
true;
595 if ( $content && $this->section !==
false ) {
596 $content = $content->getSection( $this->section );
598 $vals[
'nosuchsection'] =
true;
613 private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) {
615 $title = Title::newFromPageIdentity( $revision->getPage() );
617 if ( !$this->
getAuthority()->authorizeRead(
'read', $title ) ) {
621 if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
624 '@phan-var WikitextContent $content';
625 $t = $content->getText(); # note: don
't set $text
627 $parser = $this->parserFactory->create();
628 $parser->startExternalParse(
630 ParserOptions::newFromContext( $this->getContext() ),
631 Parser::OT_PREPROCESS
633 $dom = $parser->preprocessToDom( $t );
634 if ( is_callable( [ $dom, 'saveXML
' ] ) ) {
635 // @phan-suppress-next-line PhanUndeclaredMethod
636 $xml = $dom->saveXML();
638 // @phan-suppress-next-line PhanUndeclaredMethod
639 $xml = $dom->__toString();
641 $vals['parsetree
'] = $xml;
643 $vals['badcontentformatforparsetree
'] = true;
646 'apierror-parsetree-notwikitext-title
',
647 wfEscapeWikiText( $title->getPrefixedText() ),
650 'parsetree-notwikitext
'
655 if ( $this->fld_content ) {
658 if ( $this->expandTemplates && !$this->parseContent ) {
659 if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
661 '@phan-var WikitextContent $content
';
662 $text = $content->getText();
664 $text = $this->parserFactory->create()->preprocess(
667 ParserOptions::newFromContext( $this->getContext() )
671 'apierror-templateexpansion-notwikitext
',
672 wfEscapeWikiText( $title->getPrefixedText() ),
675 $vals['badcontentformat
'] = true;
679 if ( $this->parseContent ) {
680 $popts = ParserOptions::newFromContext( $this->getContext() );
681 $po = $this->contentRenderer->getParserOutput(
687 // TODO T371004 move runOutputPipeline out of $parserOutput
688 $text = $po->runOutputPipeline( $popts, [] )->getContentHolderText();
691 if ( $text === null ) {
692 $format = $this->contentFormat ?: $content->getDefaultFormat();
693 $model = $content->getModel();
695 if ( !$content->isSupportedFormat( $format ) ) {
696 $name = wfEscapeWikiText( $title->getPrefixedText() );
697 $this->addWarning( [ 'apierror-badformat
', $this->contentFormat, $model, $name ] );
698 $vals['badcontentformat
'] = true;
701 $text = $content->serialize( $format );
702 // always include format and model.
703 // Format is needed to deserialize, model is needed to interpret.
704 $vals['contentformat
'] = $format;
705 $vals['contentmodel
'] = $model;
709 if ( $text !== false ) {
710 ApiResult::setContentValue( $vals, 'content
', $text );
714 if ( $content && ( $this->diffto !== null || $this->difftotext !== null ) ) {
715 if ( $this->numUncachedDiffs < $this->getConfig()->get( MainConfigNames::APIMaxUncachedDiffs ) ) {
717 $context = new DerivativeContext( $this->getContext() );
718 $context->setTitle( $title );
719 $handler = $content->getContentHandler();
721 if ( $this->difftotext !== null ) {
722 $model = $title->getContentModel();
724 if ( $this->contentFormat
725 && !$this->contentHandlerFactory->getContentHandler( $model )
726 ->isSupportedFormat( $this->contentFormat )
728 $name = wfEscapeWikiText( $title->getPrefixedText() );
729 $this->addWarning( [ 'apierror-badformat
', $this->contentFormat, $model, $name ] );
730 $vals['diff
']['badcontentformat
'] = true;
733 $difftocontent = $this->contentHandlerFactory->getContentHandler( $model )
734 ->unserializeContent( $this->difftotext, $this->contentFormat );
736 if ( $this->difftotextpst ) {
737 $popts = ParserOptions::newFromContext( $this->getContext() );
738 $difftocontent = $this->contentTransformer->preSaveTransform(
741 $this->getUserForPreview(),
746 $engine = $handler->createDifferenceEngine( $context );
747 $engine->setContent( $content, $difftocontent );
750 $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto );
751 $vals['diff
']['from
'] = $engine->getOldid();
752 $vals['diff
']['to
'] = $engine->getNewid();
755 $difftext = $engine->getDiffBody();
756 ApiResult::setContentValue( $vals['diff
'], 'body
', $difftext );
757 if ( !$engine->wasCacheHit() ) {
758 $this->numUncachedDiffs++;
760 foreach ( $engine->getRevisionLoadErrors() as $msg ) {
761 $this->addWarning( $msg );
765 $vals['diff
']['notcached
'] = true;
772 private function getUserForPreview(): UserIdentity {
773 $user = $this->getUser();
774 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit
' ) ) {
775 return $this->userFactory->newUnsavedTempUser(
776 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
788 public function getCacheMode( $params ) {
789 if ( $this->userCanSeeRevDel() ) {
793 if ( isset( $params['prop
'] ) && in_array( 'tags
', $params['prop
'], true ) ) {
794 // ts_tags output varies by the viewer's rights when restricted
796 return 'anon-public-user-private';
807 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
808 sort( $slotRoles, SORT_STRING );
813 ParamValidator::PARAM_ISMULTI =>
true,
814 ParamValidator::PARAM_DEFAULT =>
'ids|timestamp|flags|comment|user',
815 ParamValidator::PARAM_TYPE => [
835 'ids' =>
'apihelp-query+revisions+base-paramvalue-prop-ids',
836 'flags' =>
'apihelp-query+revisions+base-paramvalue-prop-flags',
837 'timestamp' =>
'apihelp-query+revisions+base-paramvalue-prop-timestamp',
838 'user' =>
'apihelp-query+revisions+base-paramvalue-prop-user',
839 'userid' =>
'apihelp-query+revisions+base-paramvalue-prop-userid',
840 'size' =>
'apihelp-query+revisions+base-paramvalue-prop-size',
841 'slotsize' =>
'apihelp-query+revisions+base-paramvalue-prop-slotsize',
842 'sha1' =>
'apihelp-query+revisions+base-paramvalue-prop-sha1',
843 'slotsha1' =>
'apihelp-query+revisions+base-paramvalue-prop-slotsha1',
844 'contentmodel' =>
'apihelp-query+revisions+base-paramvalue-prop-contentmodel',
845 'comment' =>
'apihelp-query+revisions+base-paramvalue-prop-comment',
846 'parsedcomment' =>
'apihelp-query+revisions+base-paramvalue-prop-parsedcomment',
847 'content' => [
'apihelp-query+revisions+base-paramvalue-prop-content', $smallLimit ],
848 'tags' =>
'apihelp-query+revisions+base-paramvalue-prop-tags',
849 'roles' =>
'apihelp-query+revisions+base-paramvalue-prop-roles',
850 'parsetree' => [
'apihelp-query+revisions+base-paramvalue-prop-parsetree',
853 EnumDef::PARAM_DEPRECATED_VALUES => [
858 ParamValidator::PARAM_TYPE => $slotRoles,
860 ParamValidator::PARAM_ISMULTI =>
true,
861 ParamValidator::PARAM_ALL =>
true,
863 'contentformat-{slot}' => [
866 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
869 ParamValidator::PARAM_TYPE =>
'limit',
870 IntegerDef::PARAM_MIN => 1,
874 $smallLimit, self::LIMIT_PARSE ],
876 'expandtemplates' => [
877 ParamValidator::PARAM_DEFAULT =>
false,
879 ParamValidator::PARAM_DEPRECATED =>
true,
882 ParamValidator::PARAM_DEFAULT =>
false,
883 ParamValidator::PARAM_DEPRECATED =>
true,
887 ParamValidator::PARAM_DEFAULT =>
false,
889 ParamValidator::PARAM_DEPRECATED =>
true,
896 ParamValidator::PARAM_DEPRECATED =>
true,
900 ParamValidator::PARAM_DEPRECATED =>
true,
903 ParamValidator::PARAM_DEFAULT =>
false,
905 ParamValidator::PARAM_DEPRECATED =>
true,
908 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
910 ParamValidator::PARAM_DEPRECATED =>
true,
917class_alias( ApiQueryRevisionsBase::class,
'ApiQueryRevisionsBase' );
const CONTENT_MODEL_WIKITEXT
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
This class contains a list of pages that the client has requested.
A service to render content.
A service to transform content.
Content object for wiki text pages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
An IContextSource implementation which will inherit context from another source but allow individual ...
A class containing constants representing the names of configuration variables.
Content objects represent page content, e.g.