3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
23 use MediaWiki\Revision\RevisionAccessException
;
24 use MediaWiki\Revision\RevisionRecord
;
25 use MediaWiki\Revision\SlotRecord
;
26 use MediaWiki\MediaWikiServices
;
29 * A base class for functions common to producing a list of revisions.
33 abstract class ApiQueryRevisionsBase
extends ApiQueryGeneratorBase
{
36 * @name Constants for internal use. Don't use externally.
40 // Bits to indicate the results of the revdel permission check on a revision,
41 // see self::checkRevDel()
42 const IS_DELETED
= 1; // Whether the field is revision-deleted
43 const CANNOT_VIEW
= 2; // Whether the user cannot view the field due to revdel
47 protected $limit, $diffto, $difftotext, $difftotextpst, $expandTemplates, $generateXML,
48 $section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true,
49 $slotRoles = null, $needSlots;
51 protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
52 $fld_size = false, $fld_slotsize = false, $fld_sha1 = false, $fld_slotsha1 = false,
53 $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
54 $fld_content = false, $fld_tags = false, $fld_contentmodel = false, $fld_roles = false,
55 $fld_parsetree = false;
57 public function execute() {
61 public function executeGenerator( $resultPageSet ) {
62 $this->run( $resultPageSet );
66 * @param ApiPageSet|null $resultPageSet
69 abstract protected function run( ApiPageSet
$resultPageSet = null );
72 * Parse the parameters into the various instance fields.
74 * @param array $params
76 protected function parseParameters( $params ) {
77 $prop = array_flip( $params['prop'] );
79 $this->fld_ids
= isset( $prop['ids'] );
80 $this->fld_flags
= isset( $prop['flags'] );
81 $this->fld_timestamp
= isset( $prop['timestamp'] );
82 $this->fld_comment
= isset( $prop['comment'] );
83 $this->fld_parsedcomment
= isset( $prop['parsedcomment'] );
84 $this->fld_size
= isset( $prop['size'] );
85 $this->fld_slotsize
= isset( $prop['slotsize'] );
86 $this->fld_sha1
= isset( $prop['sha1'] );
87 $this->fld_slotsha1
= isset( $prop['slotsha1'] );
88 $this->fld_content
= isset( $prop['content'] );
89 $this->fld_contentmodel
= isset( $prop['contentmodel'] );
90 $this->fld_userid
= isset( $prop['userid'] );
91 $this->fld_user
= isset( $prop['user'] );
92 $this->fld_tags
= isset( $prop['tags'] );
93 $this->fld_roles
= isset( $prop['roles'] );
94 $this->fld_parsetree
= isset( $prop['parsetree'] );
96 $this->slotRoles
= $params['slots'];
98 if ( $this->slotRoles
!== null ) {
99 if ( $this->fld_parsetree
) {
100 $this->dieWithError( [
101 'apierror-invalidparammix-cannotusewith',
102 $this->encodeParamName( 'prop=parsetree' ),
103 $this->encodeParamName( 'slots' ),
104 ], 'invalidparammix' );
107 'expandtemplates', 'generatexml', 'parse', 'diffto', 'difftotext', 'difftotextpst',
110 if ( $params[$p] !== null && $params[$p] !== false ) {
111 $this->dieWithError( [
112 'apierror-invalidparammix-cannotusewith',
113 $this->encodeParamName( $p ),
114 $this->encodeParamName( 'slots' ),
115 ], 'invalidparammix' );
120 if ( !empty( $params['contentformat'] ) ) {
121 $this->contentFormat
= $params['contentformat'];
124 $this->limit
= $params['limit'];
126 if ( !is_null( $params['difftotext'] ) ) {
127 $this->difftotext
= $params['difftotext'];
128 $this->difftotextpst
= $params['difftotextpst'];
129 } elseif ( !is_null( $params['diffto'] ) ) {
130 if ( $params['diffto'] == 'cur' ) {
131 $params['diffto'] = 0;
133 if ( ( !ctype_digit( $params['diffto'] ) ||
$params['diffto'] < 0 )
134 && $params['diffto'] != 'prev' && $params['diffto'] != 'next'
136 $p = $this->getModulePrefix();
137 $this->dieWithError( [ 'apierror-baddiffto', $p ], 'diffto' );
139 // Check whether the revision exists and is readable,
140 // DifferenceEngine returns a rather ambiguous empty
141 // string if that's not the case
142 if ( $params['diffto'] != 0 ) {
143 $difftoRev = MediaWikiServices
::getInstance()->getRevisionStore()
144 ->getRevisionById( $params['diffto'] );
146 $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] );
148 $revDel = $this->checkRevDel( $difftoRev, RevisionRecord
::DELETED_TEXT
);
149 if ( $revDel & self
::CANNOT_VIEW
) {
150 $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] );
151 $params['diffto'] = null;
154 $this->diffto
= $params['diffto'];
157 $this->fetchContent
= $this->fld_content ||
!is_null( $this->diffto
)
158 ||
!is_null( $this->difftotext
) ||
$this->fld_parsetree
;
161 if ( $this->fetchContent
) {
163 $this->expandTemplates
= $params['expandtemplates'];
164 $this->generateXML
= $params['generatexml'];
165 $this->parseContent
= $params['parse'];
166 if ( $this->parseContent
) {
167 // Must manually initialize unset limit
168 if ( is_null( $this->limit
) ) {
172 $this->section
= $params['section'] ??
false;
175 $userMax = $this->parseContent ?
1 : ( $smallLimit ? ApiBase
::LIMIT_SML1
: ApiBase
::LIMIT_BIG1
);
176 $botMax = $this->parseContent ?
1 : ( $smallLimit ? ApiBase
::LIMIT_SML2
: ApiBase
::LIMIT_BIG2
);
177 if ( $this->limit
== 'max' ) {
178 $this->limit
= $this->getMain()->canApiHighLimits() ?
$botMax : $userMax;
179 if ( $this->setParsedLimit
) {
180 $this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit
);
184 if ( is_null( $this->limit
) ) {
187 $this->validateLimit( 'limit', $this->limit
, 1, $userMax, $botMax );
189 $this->needSlots
= $this->fetchContent ||
$this->fld_contentmodel ||
190 $this->fld_slotsize ||
$this->fld_slotsha1
;
191 if ( $this->needSlots
&& $this->slotRoles
=== null ) {
192 $encParam = $this->encodeParamName( 'slots' );
193 $name = $this->getModuleName();
194 $parent = $this->getParent();
195 $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) );
196 $this->addDeprecation(
197 [ 'apiwarn-deprecation-missingparam', $encParam ],
198 "action=query&{$parentParam}={$name}&!{$encParam}"
204 * Test revision deletion status
205 * @param RevisionRecord $revision Revision to check
206 * @param int $field One of the RevisionRecord::DELETED_* constants
207 * @return int Revision deletion status flags. Bitwise OR of
208 * self::IS_DELETED and self::CANNOT_VIEW, as appropriate.
210 private function checkRevDel( RevisionRecord
$revision, $field ) {
211 $ret = $revision->isDeleted( $field ) ? self
::IS_DELETED
: 0;
213 $canSee = $revision->audienceCan( $field, RevisionRecord
::FOR_THIS_USER
, $this->getUser() );
214 $ret = $ret |
( $canSee ?
0 : self
::CANNOT_VIEW
);
220 * Extract information from the RevisionRecord
222 * @since 1.32, takes a RevisionRecord instead of a Revision
223 * @param RevisionRecord $revision Revision
224 * @param object $row Should have a field 'ts_tags' if $this->fld_tags is set
227 protected function extractRevisionInfo( RevisionRecord
$revision, $row ) {
231 if ( $this->fld_ids
) {
232 $vals['revid'] = intval( $revision->getId() );
233 if ( !is_null( $revision->getParentId() ) ) {
234 $vals['parentid'] = intval( $revision->getParentId() );
238 if ( $this->fld_flags
) {
239 $vals['minor'] = $revision->isMinor();
242 if ( $this->fld_user ||
$this->fld_userid
) {
243 $revDel = $this->checkRevDel( $revision, RevisionRecord
::DELETED_USER
);
244 if ( ( $revDel & self
::IS_DELETED
) ) {
245 $vals['userhidden'] = true;
248 if ( !( $revDel & self
::CANNOT_VIEW
) ) {
249 $u = $revision->getUser( RevisionRecord
::RAW
);
250 if ( $this->fld_user
) {
251 $vals['user'] = $u->getName();
253 $userid = $u->getId();
255 $vals['anon'] = true;
258 if ( $this->fld_userid
) {
259 $vals['userid'] = $userid;
264 if ( $this->fld_timestamp
) {
265 $vals['timestamp'] = wfTimestamp( TS_ISO_8601
, $revision->getTimestamp() );
268 if ( $this->fld_size
) {
270 $vals['size'] = intval( $revision->getSize() );
271 } catch ( RevisionAccessException
$e ) {
272 // Back compat: If there's no size, return 0.
273 // @todo: Gergő says to mention T198099 as a "todo" here.
278 if ( $this->fld_sha1
) {
279 $revDel = $this->checkRevDel( $revision, RevisionRecord
::DELETED_TEXT
);
280 if ( ( $revDel & self
::IS_DELETED
) ) {
281 $vals['sha1hidden'] = true;
284 if ( !( $revDel & self
::CANNOT_VIEW
) ) {
286 $vals['sha1'] = Wikimedia\base_convert
( $revision->getSha1(), 36, 16, 40 );
287 } catch ( RevisionAccessException
$e ) {
288 // Back compat: If there's no sha1, return emtpy string.
289 // @todo: Gergő says to mention T198099 as a "todo" here.
295 if ( $this->fld_roles
) {
296 $vals['roles'] = $revision->getSlotRoles();
299 if ( $this->needSlots
) {
300 $revDel = $this->checkRevDel( $revision, RevisionRecord
::DELETED_TEXT
);
301 if ( ( $this->fld_slotsha1 ||
$this->fetchContent
) && ( $revDel & self
::IS_DELETED
) ) {
304 if ( $this->slotRoles
=== null ) {
306 $slot = $revision->getSlot( SlotRecord
::MAIN
, RevisionRecord
::RAW
);
307 } catch ( RevisionAccessException
$e ) {
308 // Back compat: If there's no slot, there's no content, so set 'textmissing'
309 // @todo: Gergő says to mention T198099 as a "todo" here.
310 $vals['textmissing'] = true;
316 $vals +
= $this->extractSlotInfo( $slot, $revDel, $content );
317 if ( !empty( $vals['nosuchsection'] ) ) {
320 'apierror-nosuchsection-what',
321 wfEscapeWikiText( $this->section
),
322 $this->msg( 'revid', $revision->getId() )
328 $vals +
= $this->extractDeprecatedContent( $content, $revision );
332 $roles = array_intersect( $this->slotRoles
, $revision->getSlotRoles() );
334 ApiResult
::META_KVP_MERGE
=> true,
336 foreach ( $roles as $role ) {
338 $slot = $revision->getSlot( $role, RevisionRecord
::RAW
);
339 } catch ( RevisionAccessException
$e ) {
340 // Don't error out here so the client can still process other slots/revisions.
341 // @todo: Gergő says to mention T198099 as a "todo" here.
342 $vals['slots'][$role]['missing'] = true;
346 $vals['slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content );
347 // @todo Move this into extractSlotInfo() (and remove its $content parameter)
348 // when extractDeprecatedContent() is no more.
350 $vals['slots'][$role]['contentmodel'] = $content->getModel();
351 $vals['slots'][$role]['contentformat'] = $content->getDefaultFormat();
352 ApiResult
::setContentValue( $vals['slots'][$role], 'content', $content->serialize() );
355 ApiResult
::setArrayType( $vals['slots'], 'kvp', 'role' );
356 ApiResult
::setIndexedTagName( $vals['slots'], 'slot' );
360 if ( $this->fld_comment ||
$this->fld_parsedcomment
) {
361 $revDel = $this->checkRevDel( $revision, RevisionRecord
::DELETED_COMMENT
);
362 if ( ( $revDel & self
::IS_DELETED
) ) {
363 $vals['commenthidden'] = true;
366 if ( !( $revDel & self
::CANNOT_VIEW
) ) {
367 $comment = $revision->getComment( RevisionRecord
::RAW
);
368 $comment = $comment ?
$comment->text
: '';
370 if ( $this->fld_comment
) {
371 $vals['comment'] = $comment;
374 if ( $this->fld_parsedcomment
) {
375 $vals['parsedcomment'] = Linker
::formatComment(
376 $comment, Title
::newFromLinkTarget( $revision->getPageAsLinkTarget() )
382 if ( $this->fld_tags
) {
383 if ( $row->ts_tags
) {
384 $tags = explode( ',', $row->ts_tags
);
385 ApiResult
::setIndexedTagName( $tags, 'tag' );
386 $vals['tags'] = $tags;
392 if ( $anyHidden && $revision->isDeleted( RevisionRecord
::DELETED_RESTRICTED
) ) {
393 $vals['suppressed'] = true;
400 * Extract information from the SlotRecord
402 * @param SlotRecord $slot
403 * @param int $revDel Revdel status flags, from self::checkRevDel()
404 * @param Content|null &$content Set to the slot's content, if available
405 * and $this->fetchContent is true
408 private function extractSlotInfo( SlotRecord
$slot, $revDel, &$content = null ) {
410 ApiResult
::setArrayType( $vals, 'assoc' );
412 if ( $this->fld_slotsize
) {
413 $vals['size'] = intval( $slot->getSize() );
416 if ( $this->fld_slotsha1
) {
417 if ( ( $revDel & self
::IS_DELETED
) ) {
418 $vals['sha1hidden'] = true;
420 if ( !( $revDel & self
::CANNOT_VIEW
) ) {
421 if ( $slot->getSha1() != '' ) {
422 $vals['sha1'] = Wikimedia\base_convert
( $slot->getSha1(), 36, 16, 40 );
429 if ( $this->fld_contentmodel
) {
430 $vals['contentmodel'] = $slot->getModel();
434 if ( $this->fetchContent
) {
435 if ( ( $revDel & self
::IS_DELETED
) ) {
436 $vals['texthidden'] = true;
438 if ( !( $revDel & self
::CANNOT_VIEW
) ) {
440 $content = $slot->getContent();
441 } catch ( RevisionAccessException
$e ) {
442 // @todo: Gergő says to mention T198099 as a "todo" here.
443 $vals['textmissing'] = true;
445 // Expand templates after getting section content because
446 // template-added sections don't count and Parser::preprocess()
447 // will have less input
448 if ( $content && $this->section
!== false ) {
449 $content = $content->getSection( $this->section
, false );
451 $vals['nosuchsection'] = true;
461 * Format a Content using deprecated options
462 * @param Content $content Content to format
463 * @param RevisionRecord $revision Revision being processed
466 private function extractDeprecatedContent( Content
$content, RevisionRecord
$revision ) {
470 $title = Title
::newFromLinkTarget( $revision->getPageAsLinkTarget() );
472 if ( $this->fld_parsetree ||
( $this->fld_content
&& $this->generateXML
) ) {
473 if ( $content->getModel() === CONTENT_MODEL_WIKITEXT
) {
474 $t = $content->getNativeData(); # note: don't set $text
476 $wgParser->startExternalParse(
478 ParserOptions
::newFromContext( $this->getContext() ),
479 Parser
::OT_PREPROCESS
481 $dom = $wgParser->preprocessToDom( $t );
482 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
483 $xml = $dom->saveXML();
485 $xml = $dom->__toString();
487 $vals['parsetree'] = $xml;
489 $vals['badcontentformatforparsetree'] = true;
492 'apierror-parsetree-notwikitext-title',
493 wfEscapeWikiText( $title->getPrefixedText() ),
496 'parsetree-notwikitext'
501 if ( $this->fld_content
) {
504 if ( $this->expandTemplates
&& !$this->parseContent
) {
505 if ( $content->getModel() === CONTENT_MODEL_WIKITEXT
) {
506 $text = $content->getNativeData();
508 $text = $wgParser->preprocess(
511 ParserOptions
::newFromContext( $this->getContext() )
515 'apierror-templateexpansion-notwikitext',
516 wfEscapeWikiText( $title->getPrefixedText() ),
519 $vals['badcontentformat'] = true;
523 if ( $this->parseContent
) {
524 $po = $content->getParserOutput(
527 ParserOptions
::newFromContext( $this->getContext() )
529 $text = $po->getText();
532 if ( $text === null ) {
533 $format = $this->contentFormat ?
: $content->getDefaultFormat();
534 $model = $content->getModel();
536 if ( !$content->isSupportedFormat( $format ) ) {
537 $name = wfEscapeWikiText( $title->getPrefixedText() );
538 $this->addWarning( [ 'apierror-badformat', $this->contentFormat
, $model, $name ] );
539 $vals['badcontentformat'] = true;
542 $text = $content->serialize( $format );
543 // always include format and model.
544 // Format is needed to deserialize, model is needed to interpret.
545 $vals['contentformat'] = $format;
546 $vals['contentmodel'] = $model;
550 if ( $text !== false ) {
551 ApiResult
::setContentValue( $vals, 'content', $text );
555 if ( $content && ( !is_null( $this->diffto
) ||
!is_null( $this->difftotext
) ) ) {
556 static $n = 0; // Number of uncached diffs we've had
558 if ( $n < $this->getConfig()->get( 'APIMaxUncachedDiffs' ) ) {
560 $context = new DerivativeContext( $this->getContext() );
561 $context->setTitle( $title );
562 $handler = $content->getContentHandler();
564 if ( !is_null( $this->difftotext
) ) {
565 $model = $title->getContentModel();
567 if ( $this->contentFormat
568 && !ContentHandler
::getForModelID( $model )->isSupportedFormat( $this->contentFormat
)
570 $name = wfEscapeWikiText( $title->getPrefixedText() );
571 $this->addWarning( [ 'apierror-badformat', $this->contentFormat
, $model, $name ] );
572 $vals['diff']['badcontentformat'] = true;
575 $difftocontent = ContentHandler
::makeContent(
582 if ( $this->difftotextpst
) {
583 $popts = ParserOptions
::newFromContext( $this->getContext() );
584 $difftocontent = $difftocontent->preSaveTransform( $title, $this->getUser(), $popts );
587 $engine = $handler->createDifferenceEngine( $context );
588 $engine->setContent( $content, $difftocontent );
591 $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto
);
592 $vals['diff']['from'] = $engine->getOldid();
593 $vals['diff']['to'] = $engine->getNewid();
596 $difftext = $engine->getDiffBody();
597 ApiResult
::setContentValue( $vals['diff'], 'body', $difftext );
598 if ( !$engine->wasCacheHit() ) {
603 $vals['diff']['notcached'] = true;
610 public function getCacheMode( $params ) {
611 if ( $this->userCanSeeRevDel() ) {
618 public function getAllowedParams() {
619 $slotRoles = MediaWikiServices
::getInstance()->getSlotRoleRegistry()->getKnownRoles();
620 sort( $slotRoles, SORT_STRING
);
624 ApiBase
::PARAM_ISMULTI
=> true,
625 ApiBase
::PARAM_DFLT
=> 'ids|timestamp|flags|comment|user',
626 ApiBase
::PARAM_TYPE
=> [
644 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-prop',
645 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [
646 'ids' => 'apihelp-query+revisions+base-paramvalue-prop-ids',
647 'flags' => 'apihelp-query+revisions+base-paramvalue-prop-flags',
648 'timestamp' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp',
649 'user' => 'apihelp-query+revisions+base-paramvalue-prop-user',
650 'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid',
651 'size' => 'apihelp-query+revisions+base-paramvalue-prop-size',
652 'slotsize' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize',
653 'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1',
654 'slotsha1' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1',
655 'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel',
656 'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment',
657 'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment',
658 'content' => 'apihelp-query+revisions+base-paramvalue-prop-content',
659 'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags',
660 'roles' => 'apihelp-query+revisions+base-paramvalue-prop-roles',
661 'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree',
662 CONTENT_MODEL_WIKITEXT
],
664 ApiBase
::PARAM_DEPRECATED_VALUES
=> [
669 ApiBase
::PARAM_TYPE
=> $slotRoles,
670 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-slots',
671 ApiBase
::PARAM_ISMULTI
=> true,
672 ApiBase
::PARAM_ALL
=> true,
675 ApiBase
::PARAM_TYPE
=> 'limit',
676 ApiBase
::PARAM_MIN
=> 1,
677 ApiBase
::PARAM_MAX
=> ApiBase
::LIMIT_BIG1
,
678 ApiBase
::PARAM_MAX2
=> ApiBase
::LIMIT_BIG2
,
679 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-limit',
681 'expandtemplates' => [
682 ApiBase
::PARAM_DFLT
=> false,
683 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-expandtemplates',
684 ApiBase
::PARAM_DEPRECATED
=> true,
687 ApiBase
::PARAM_DFLT
=> false,
688 ApiBase
::PARAM_DEPRECATED
=> true,
689 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-generatexml',
692 ApiBase
::PARAM_DFLT
=> false,
693 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-parse',
694 ApiBase
::PARAM_DEPRECATED
=> true,
697 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-section',
700 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-diffto',
701 ApiBase
::PARAM_DEPRECATED
=> true,
704 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-difftotext',
705 ApiBase
::PARAM_DEPRECATED
=> true,
708 ApiBase
::PARAM_DFLT
=> false,
709 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-difftotextpst',
710 ApiBase
::PARAM_DEPRECATED
=> true,
713 ApiBase
::PARAM_TYPE
=> ContentHandler
::getAllContentFormats(),
714 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-query+revisions+base-param-contentformat',
715 ApiBase
::PARAM_DEPRECATED
=> true,