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 ||
302 if ( $this->needSlots && $this->slotRoles ===
null ) {
306 $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) );
308 [
'apiwarn-deprecation-missingparam', $encParam ],
309 "action=query&{$parentParam}={$name}&!{$encParam}"
321 private function checkRevDel(
RevisionRecord $revision, $field ) {
322 $ret = $revision->
isDeleted( $field ) ? self::IS_DELETED : 0;
325 $ret |= ( $canSee ? 0 : self::CANNOT_VIEW );
342 if ( $this->fld_ids ) {
343 $vals[
'revid'] = (int)$revision->
getId();
349 if ( $this->fld_flags ) {
350 $vals[
'minor'] = $revision->
isMinor();
353 if ( $this->fld_user || $this->fld_userid ) {
354 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER );
355 if ( $revDel & self::IS_DELETED ) {
356 $vals[
'userhidden'] =
true;
359 if ( !( $revDel & self::CANNOT_VIEW ) ) {
360 $u = $revision->
getUser( RevisionRecord::RAW );
361 if ( $this->fld_user ) {
362 $vals[
'user'] = $u->getName();
364 if ( $this->userNameUtils->isTemp( $u->getName() ) ) {
365 $vals[
'temp'] =
true;
367 if ( !$u->isRegistered() ) {
368 $vals[
'anon'] =
true;
371 if ( $this->fld_userid ) {
372 $vals[
'userid'] = $u->getId();
377 if ( $this->fld_timestamp ) {
381 if ( $this->fld_size ) {
383 $vals[
'size'] = (int)$revision->
getSize();
391 if ( $this->fld_sha1 ) {
392 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
393 if ( $revDel & self::IS_DELETED ) {
394 $vals[
'sha1hidden'] =
true;
397 if ( !( $revDel & self::CANNOT_VIEW ) ) {
399 $vals[
'sha1'] = \Wikimedia\base_convert( $revision->
getSha1(), 36, 16, 40 );
409 if ( $this->fld_roles ) {
413 if ( $this->needSlots ) {
414 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
415 if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) {
418 $vals = array_merge( $vals, $this->extractAllSlotInfo( $revision, $revDel ) );
423 $vals[
'slotsmissing'] =
true;
425 LoggerFactory::getInstance(
'api-warning' )->error(
426 'Failed to access revision slots',
427 [
'revision' => $revision->
getId(),
'exception' => $ex, ]
431 if ( $this->fld_comment || $this->fld_parsedcomment ) {
432 $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT );
433 if ( $revDel & self::IS_DELETED ) {
434 $vals[
'commenthidden'] =
true;
437 if ( !( $revDel & self::CANNOT_VIEW ) ) {
438 $comment = $revision->
getComment( RevisionRecord::RAW );
439 $comment = $comment->text ??
'';
441 if ( $this->fld_comment ) {
442 $vals[
'comment'] = $comment;
445 if ( $this->fld_parsedcomment ) {
446 $vals[
'parsedcomment'] = $this->commentFormatter->format(
453 if ( $this->fld_tags ) {
454 if ( $row->ts_tags ) {
455 $tags = explode(
',', $row->ts_tags );
457 $vals[
'tags'] = $tags;
463 if ( $anyHidden && $revision->
isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
464 $vals[
'suppressed'] =
true;
479 private function extractAllSlotInfo(
RevisionRecord $revision, $revDel ): array {
482 if ( $this->slotRoles ===
null ) {
484 $slot = $revision->
getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
485 }
catch ( RevisionAccessException ) {
488 $vals[
'textmissing'] =
true;
494 $vals += $this->extractSlotInfo( $slot, $revDel, $content );
495 if ( !empty( $vals[
'nosuchsection'] ) ) {
498 'apierror-nosuchsection-what',
500 $this->
msg(
'revid', $revision->
getId() )
506 $vals += $this->extractDeprecatedContent( $content, $revision );
510 $roles = array_intersect( $this->slotRoles, $revision->
getSlotRoles() );
514 foreach ( $roles as $role ) {
516 $slot = $revision->
getSlot( $role, RevisionRecord::RAW );
517 }
catch ( RevisionAccessException ) {
520 $vals[
'slots'][$role][
'missing'] =
true;
524 $vals[
'slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content );
529 $model = $content->getModel();
530 $format = $this->slotContentFormats[$role] ?? $content->getDefaultFormat();
531 if ( !$content->isSupportedFormat( $format ) ) {
533 'apierror-badformat',
536 $this->
msg(
'revid', $revision->
getId() )
538 $vals[
'slots'][$role][
'badcontentformat'] =
true;
540 $vals[
'slots'][$role][
'contentmodel'] = $model;
541 $vals[
'slots'][$role][
'contentformat'] = $format;
543 $vals[
'slots'][$role],
545 $content->serialize( $format )
565 private function extractSlotInfo( SlotRecord $slot, $revDel, &$content =
null ) {
569 if ( $this->fld_slotsize ) {
570 $vals[
'size'] = (int)$slot->getSize();
573 if ( $this->fld_slotsha1 ) {
574 if ( $revDel & self::IS_DELETED ) {
575 $vals[
'sha1hidden'] =
true;
577 if ( !( $revDel & self::CANNOT_VIEW ) ) {
578 if ( $slot->getSha1() !=
'' ) {
579 $vals[
'sha1'] = \Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 );
586 if ( $this->fld_contentmodel ) {
587 $vals[
'contentmodel'] = $slot->getModel();
591 if ( $this->fetchContent ) {
592 if ( $revDel & self::IS_DELETED ) {
593 $vals[
'texthidden'] =
true;
595 if ( !( $revDel & self::CANNOT_VIEW ) ) {
597 $content = $slot->getContent();
598 }
catch ( RevisionAccessException ) {
600 $vals[
'textmissing'] =
true;
605 if ( $content && $this->section !==
false ) {
606 $content = $content->getSection( $this->section );
608 $vals[
'nosuchsection'] =
true;
623 private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) {
625 $title = Title::newFromPageIdentity( $revision->getPage() );
627 if ( !$this->
getAuthority()->authorizeRead(
'read', $title ) ) {
631 if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
634 '@phan-var WikitextContent $content';
635 $t = $content->getText(); # note: don
't set $text
637 $parser = $this->parserFactory->create();
638 $parser->startExternalParse(
640 ParserOptions::newFromContext( $this->getContext() ),
641 Parser::OT_PREPROCESS
643 $dom = $parser->preprocessToDom( $t );
644 if ( is_callable( [ $dom, 'saveXML
' ] ) ) {
645 // @phan-suppress-next-line PhanUndeclaredMethod
646 $xml = $dom->saveXML();
648 // @phan-suppress-next-line PhanUndeclaredMethod
649 $xml = $dom->__toString();
651 $vals['parsetree
'] = $xml;
653 $vals['badcontentformatforparsetree
'] = true;
656 'apierror-parsetree-notwikitext-title
',
657 wfEscapeWikiText( $title->getPrefixedText() ),
660 'parsetree-notwikitext
'
665 if ( $this->fld_content ) {
668 if ( $this->expandTemplates && !$this->parseContent ) {
669 if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
671 '@phan-var WikitextContent $content
';
672 $text = $content->getText();
674 $text = $this->parserFactory->create()->preprocess(
677 ParserOptions::newFromContext( $this->getContext() )
681 'apierror-templateexpansion-notwikitext
',
682 wfEscapeWikiText( $title->getPrefixedText() ),
685 $vals['badcontentformat
'] = true;
689 if ( $this->parseContent ) {
690 $popts = ParserOptions::newFromContext( $this->getContext() );
691 $po = $this->contentRenderer->getParserOutput(
697 // TODO T371004 move runOutputPipeline out of $parserOutput
698 $text = $po->runOutputPipeline( $popts, [] )->getContentHolderText();
701 if ( $text === null ) {
702 $format = $this->contentFormat ?: $content->getDefaultFormat();
703 $model = $content->getModel();
705 if ( !$content->isSupportedFormat( $format ) ) {
706 $name = wfEscapeWikiText( $title->getPrefixedText() );
707 $this->addWarning( [ 'apierror-badformat
', $this->contentFormat, $model, $name ] );
708 $vals['badcontentformat
'] = true;
711 $text = $content->serialize( $format );
712 // always include format and model.
713 // Format is needed to deserialize, model is needed to interpret.
714 $vals['contentformat
'] = $format;
715 $vals['contentmodel
'] = $model;
719 if ( $text !== false ) {
720 ApiResult::setContentValue( $vals, 'content
', $text );
724 if ( $content && ( $this->diffto !== null || $this->difftotext !== null ) ) {
725 if ( $this->numUncachedDiffs < $this->getConfig()->get( MainConfigNames::APIMaxUncachedDiffs ) ) {
727 $context = new DerivativeContext( $this->getContext() );
728 $context->setTitle( $title );
729 $handler = $content->getContentHandler();
731 if ( $this->difftotext !== null ) {
732 $model = $title->getContentModel();
734 if ( $this->contentFormat
735 && !$this->contentHandlerFactory->getContentHandler( $model )
736 ->isSupportedFormat( $this->contentFormat )
738 $name = wfEscapeWikiText( $title->getPrefixedText() );
739 $this->addWarning( [ 'apierror-badformat
', $this->contentFormat, $model, $name ] );
740 $vals['diff
']['badcontentformat
'] = true;
743 $difftocontent = $this->contentHandlerFactory->getContentHandler( $model )
744 ->unserializeContent( $this->difftotext, $this->contentFormat );
746 if ( $this->difftotextpst ) {
747 $popts = ParserOptions::newFromContext( $this->getContext() );
748 $difftocontent = $this->contentTransformer->preSaveTransform(
751 $this->getUserForPreview(),
756 $engine = $handler->createDifferenceEngine( $context );
757 $engine->setContent( $content, $difftocontent );
760 $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto );
761 $vals['diff
']['from
'] = $engine->getOldid();
762 $vals['diff
']['to
'] = $engine->getNewid();
765 $difftext = $engine->getDiffBody();
766 ApiResult::setContentValue( $vals['diff
'], 'body
', $difftext );
767 if ( !$engine->wasCacheHit() ) {
768 $this->numUncachedDiffs++;
770 foreach ( $engine->getRevisionLoadErrors() as $msg ) {
771 $this->addWarning( $msg );
775 $vals['diff
']['notcached
'] = true;
782 private function getUserForPreview(): UserIdentity {
783 $user = $this->getUser();
784 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit
' ) ) {
785 return $this->userFactory->newUnsavedTempUser(
786 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
798 public function getCacheMode( $params ) {
799 if ( $this->userCanSeeRevDel() ) {
810 public function getAllowedParams() {
811 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
812 sort( $slotRoles, SORT_STRING );
813 $smallLimit = $this->getMain()->canApiHighLimits() ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_SML1;
817 ParamValidator::PARAM_ISMULTI => true,
818 ParamValidator::PARAM_DEFAULT => 'ids|timestamp|flags|comment|user
',
819 ParamValidator::PARAM_TYPE => [
837 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop
',
838 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
839 'ids
' => 'apihelp-query+revisions+base-paramvalue-prop-ids
',
840 'flags
' => 'apihelp-query+revisions+base-paramvalue-prop-flags
',
841 'timestamp
' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp
',
842 'user
' => 'apihelp-query+revisions+base-paramvalue-prop-user
',
843 'userid
' => 'apihelp-query+revisions+base-paramvalue-prop-userid
',
844 'size
' => 'apihelp-query+revisions+base-paramvalue-prop-size
',
845 'slotsize
' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize
',
846 'sha1
' => 'apihelp-query+revisions+base-paramvalue-prop-sha1
',
847 'slotsha1
' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1
',
848 'contentmodel
' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel
',
849 'comment
' => 'apihelp-query+revisions+base-paramvalue-prop-comment
',
850 'parsedcomment
' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment
',
851 'content
' => [ 'apihelp-query+revisions+base-paramvalue-prop-content
', $smallLimit ],
852 'tags
' => 'apihelp-query+revisions+base-paramvalue-prop-tags
',
853 'roles
' => 'apihelp-query+revisions+base-paramvalue-prop-roles
',
854 'parsetree
' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree
',
855 CONTENT_MODEL_WIKITEXT, $smallLimit ],
857 EnumDef::PARAM_DEPRECATED_VALUES => [
862 ParamValidator::PARAM_TYPE => $slotRoles,
863 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-slots
',
864 ParamValidator::PARAM_ISMULTI => true,
865 ParamValidator::PARAM_ALL => true,
867 'contentformat-{slot}
' => [
868 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot
' => 'slots
' ],
869 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat-slot
',
870 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
873 ParamValidator::PARAM_TYPE => 'limit
',
874 IntegerDef::PARAM_MIN => 1,
875 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
876 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
877 ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-limit
',
878 $smallLimit, self::LIMIT_PARSE ],
880 'expandtemplates
' => [
881 ParamValidator::PARAM_DEFAULT => false,
882 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates
',
883 ParamValidator::PARAM_DEPRECATED => true,
886 ParamValidator::PARAM_DEFAULT => false,
887 ParamValidator::PARAM_DEPRECATED => true,
888 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml
',
891 ParamValidator::PARAM_DEFAULT => false,
892 ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-parse
', self::LIMIT_PARSE ],
893 ParamValidator::PARAM_DEPRECATED => true,
896 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section
',
899 ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-diffto
', $smallLimit ],
900 ParamValidator::PARAM_DEPRECATED => true,
903 ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-difftotext
', $smallLimit ],
904 ParamValidator::PARAM_DEPRECATED => true,
907 ParamValidator::PARAM_DEFAULT => false,
908 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst
',
909 ParamValidator::PARAM_DEPRECATED => true,
912 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
913 ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat
',
914 ParamValidator::PARAM_DEPRECATED => true,
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.