Revision.php 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. <?php
  2. /**
  3. * Representation of a page version.
  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. use MediaWiki\Revision\MutableRevisionRecord;
  23. use MediaWiki\Revision\RevisionAccessException;
  24. use MediaWiki\Revision\RevisionFactory;
  25. use MediaWiki\Revision\RevisionLookup;
  26. use MediaWiki\Revision\RevisionRecord;
  27. use MediaWiki\Revision\RevisionStore;
  28. use MediaWiki\Revision\RevisionStoreRecord;
  29. use MediaWiki\Revision\SlotRecord;
  30. use MediaWiki\Storage\SqlBlobStore;
  31. use Wikimedia\Assert\Assert;
  32. use Wikimedia\Rdbms\IDatabase;
  33. use MediaWiki\Linker\LinkTarget;
  34. use MediaWiki\MediaWikiServices;
  35. /**
  36. * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
  37. */
  38. class Revision implements IDBAccessObject {
  39. /** @var RevisionRecord */
  40. protected $mRecord;
  41. // Revision deletion constants
  42. const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
  43. const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
  44. const DELETED_USER = RevisionRecord::DELETED_USER;
  45. const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
  46. const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
  47. const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
  48. // Audience options for accessors
  49. const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
  50. const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
  51. const RAW = RevisionRecord::RAW;
  52. const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
  53. /**
  54. * @return RevisionStore
  55. */
  56. protected static function getRevisionStore( $wiki = false ) {
  57. if ( $wiki ) {
  58. return MediaWikiServices::getInstance()->getRevisionStoreFactory()
  59. ->getRevisionStore( $wiki );
  60. } else {
  61. return MediaWikiServices::getInstance()->getRevisionStore();
  62. }
  63. }
  64. /**
  65. * @return RevisionLookup
  66. */
  67. protected static function getRevisionLookup() {
  68. return MediaWikiServices::getInstance()->getRevisionLookup();
  69. }
  70. /**
  71. * @return RevisionFactory
  72. */
  73. protected static function getRevisionFactory() {
  74. return MediaWikiServices::getInstance()->getRevisionFactory();
  75. }
  76. /**
  77. * @param bool|string $wiki The ID of the target wiki database. Use false for the local wiki.
  78. *
  79. * @return SqlBlobStore
  80. */
  81. protected static function getBlobStore( $wiki = false ) {
  82. // @phan-suppress-next-line PhanAccessMethodInternal
  83. $store = MediaWikiServices::getInstance()
  84. ->getBlobStoreFactory()
  85. ->newSqlBlobStore( $wiki );
  86. if ( !$store instanceof SqlBlobStore ) {
  87. throw new RuntimeException(
  88. 'The backwards compatibility code in Revision currently requires the BlobStore '
  89. . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
  90. );
  91. }
  92. return $store;
  93. }
  94. /**
  95. * Load a page revision from a given revision ID number.
  96. * Returns null if no such revision can be found.
  97. *
  98. * $flags include:
  99. * Revision::READ_LATEST : Select the data from the master
  100. * Revision::READ_LOCKING : Select & lock the data from the master
  101. *
  102. * @param int $id
  103. * @param int $flags (optional)
  104. * @return Revision|null
  105. */
  106. public static function newFromId( $id, $flags = 0 ) {
  107. $rec = self::getRevisionLookup()->getRevisionById( $id, $flags );
  108. return $rec ? new Revision( $rec, $flags ) : null;
  109. }
  110. /**
  111. * Load either the current, or a specified, revision
  112. * that's attached to a given link target. If not attached
  113. * to that link target, will return null.
  114. *
  115. * $flags include:
  116. * Revision::READ_LATEST : Select the data from the master
  117. * Revision::READ_LOCKING : Select & lock the data from the master
  118. *
  119. * @param LinkTarget $linkTarget
  120. * @param int $id (optional)
  121. * @param int $flags Bitfield (optional)
  122. * @return Revision|null
  123. */
  124. public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
  125. $rec = self::getRevisionLookup()->getRevisionByTitle( $linkTarget, $id, $flags );
  126. return $rec ? new Revision( $rec, $flags ) : null;
  127. }
  128. /**
  129. * Load either the current, or a specified, revision
  130. * that's attached to a given page ID.
  131. * Returns null if no such revision can be found.
  132. *
  133. * $flags include:
  134. * Revision::READ_LATEST : Select the data from the master (since 1.20)
  135. * Revision::READ_LOCKING : Select & lock the data from the master
  136. *
  137. * @param int $pageId
  138. * @param int $revId (optional)
  139. * @param int $flags Bitfield (optional)
  140. * @return Revision|null
  141. */
  142. public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
  143. $rec = self::getRevisionLookup()->getRevisionByPageId( $pageId, $revId, $flags );
  144. return $rec ? new Revision( $rec, $flags ) : null;
  145. }
  146. /**
  147. * Make a fake revision object from an archive table row. This is queried
  148. * for permissions or even inserted (as in Special:Undelete)
  149. *
  150. * @param object $row
  151. * @param array $overrides
  152. *
  153. * @throws MWException
  154. * @return Revision
  155. */
  156. public static function newFromArchiveRow( $row, $overrides = [] ) {
  157. /**
  158. * MCR Migration: https://phabricator.wikimedia.org/T183564
  159. * This method used to overwrite attributes, then passed to Revision::__construct
  160. * RevisionStore::newRevisionFromArchiveRow instead overrides row field names
  161. * So do a conversion here.
  162. */
  163. if ( array_key_exists( 'page', $overrides ) ) {
  164. $overrides['page_id'] = $overrides['page'];
  165. unset( $overrides['page'] );
  166. }
  167. /**
  168. * We require a Title for both the Revision object and the RevisionRecord.
  169. * Below is duplicated logic from RevisionStore::newRevisionFromArchiveRow
  170. * to fetch a title in order pass it into the Revision object.
  171. */
  172. $title = null;
  173. if ( isset( $overrides['title'] ) ) {
  174. if ( !( $overrides['title'] instanceof Title ) ) {
  175. throw new MWException( 'title field override must contain a Title object.' );
  176. }
  177. $title = $overrides['title'];
  178. }
  179. if ( $title !== null ) {
  180. if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
  181. $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
  182. } else {
  183. throw new InvalidArgumentException(
  184. 'A Title or ar_namespace and ar_title must be given'
  185. );
  186. }
  187. }
  188. $rec = self::getRevisionFactory()->newRevisionFromArchiveRow( $row, 0, $title, $overrides );
  189. return new Revision( $rec, self::READ_NORMAL, $title );
  190. }
  191. /**
  192. * @since 1.19
  193. *
  194. * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
  195. * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
  196. * a MutableRevisionRecord should be constructed directly.
  197. * RevisionStore::newMutableRevisionFromArray() can be used as a temporary replacement,
  198. * but should be avoided.
  199. *
  200. * @param object|array $row
  201. * @return Revision
  202. */
  203. public static function newFromRow( $row ) {
  204. if ( is_array( $row ) ) {
  205. $rec = self::getRevisionFactory()->newMutableRevisionFromArray( $row );
  206. } else {
  207. $rec = self::getRevisionFactory()->newRevisionFromRow( $row );
  208. }
  209. return new Revision( $rec );
  210. }
  211. /**
  212. * Load a page revision from a given revision ID number.
  213. * Returns null if no such revision can be found.
  214. *
  215. * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
  216. *
  217. * @param IDatabase $db
  218. * @param int $id
  219. * @return Revision|null
  220. */
  221. public static function loadFromId( $db, $id ) {
  222. wfDeprecated( __METHOD__, '1.31' ); // no known callers
  223. $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
  224. return $rec ? new Revision( $rec ) : null;
  225. }
  226. /**
  227. * Load either the current, or a specified, revision
  228. * that's attached to a given page. If not attached
  229. * to that page, will return null.
  230. *
  231. * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
  232. *
  233. * @param IDatabase $db
  234. * @param int $pageid
  235. * @param int $id
  236. * @return Revision|null
  237. */
  238. public static function loadFromPageId( $db, $pageid, $id = 0 ) {
  239. $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
  240. return $rec ? new Revision( $rec ) : null;
  241. }
  242. /**
  243. * Load either the current, or a specified, revision
  244. * that's attached to a given page. If not attached
  245. * to that page, will return null.
  246. *
  247. * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
  248. *
  249. * @param IDatabase $db
  250. * @param Title $title
  251. * @param int $id
  252. * @return Revision|null
  253. */
  254. public static function loadFromTitle( $db, $title, $id = 0 ) {
  255. $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
  256. return $rec ? new Revision( $rec ) : null;
  257. }
  258. /**
  259. * Load the revision for the given title with the given timestamp.
  260. * WARNING: Timestamps may in some circumstances not be unique,
  261. * so this isn't the best key to use.
  262. *
  263. * @deprecated since 1.31, use RevisionStore::getRevisionByTimestamp()
  264. * or RevisionStore::loadRevisionFromTimestamp() instead.
  265. *
  266. * @param IDatabase $db
  267. * @param Title $title
  268. * @param string $timestamp
  269. * @return Revision|null
  270. */
  271. public static function loadFromTimestamp( $db, $title, $timestamp ) {
  272. $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
  273. return $rec ? new Revision( $rec ) : null;
  274. }
  275. /**
  276. * Return the tables, fields, and join conditions to be selected to create
  277. * a new revision object.
  278. * @since 1.31
  279. * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
  280. * @param array $options Any combination of the following strings
  281. * - 'page': Join with the page table, and select fields to identify the page
  282. * - 'user': Join with the user table, and select the user name
  283. * - 'text': Join with the text table, and select fields to load page text
  284. * @return array With three keys:
  285. * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
  286. * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
  287. * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
  288. */
  289. public static function getQueryInfo( $options = [] ) {
  290. return self::getRevisionStore()->getQueryInfo( $options );
  291. }
  292. /**
  293. * Return the tables, fields, and join conditions to be selected to create
  294. * a new archived revision object.
  295. * @since 1.31
  296. * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
  297. * @return array With three keys:
  298. * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
  299. * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
  300. * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
  301. */
  302. public static function getArchiveQueryInfo() {
  303. return self::getRevisionStore()->getArchiveQueryInfo();
  304. }
  305. /**
  306. * Do a batched query to get the parent revision lengths
  307. *
  308. * @deprecated in 1.31, use RevisionStore::getRevisionSizes instead.
  309. *
  310. * @param IDatabase $db
  311. * @param array $revIds
  312. * @return array
  313. */
  314. public static function getParentLengths( $db, array $revIds ) {
  315. return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
  316. }
  317. /**
  318. * @param object|array|RevisionRecord $row Either a database row or an array
  319. * @param int $queryFlags
  320. * @param Title|null $title
  321. *
  322. * @private
  323. */
  324. function __construct( $row, $queryFlags = 0, Title $title = null ) {
  325. global $wgUser;
  326. if ( $row instanceof RevisionRecord ) {
  327. $this->mRecord = $row;
  328. } elseif ( is_array( $row ) ) {
  329. // If no user is specified, fall back to using the global user object, to stay
  330. // compatible with pre-1.31 behavior.
  331. if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
  332. $row['user'] = $wgUser;
  333. }
  334. $this->mRecord = self::getRevisionFactory()->newMutableRevisionFromArray(
  335. $row,
  336. $queryFlags,
  337. $this->ensureTitle( $row, $queryFlags, $title )
  338. );
  339. } elseif ( is_object( $row ) ) {
  340. $this->mRecord = self::getRevisionFactory()->newRevisionFromRow(
  341. $row,
  342. $queryFlags,
  343. $this->ensureTitle( $row, $queryFlags, $title )
  344. );
  345. } else {
  346. throw new InvalidArgumentException(
  347. '$row must be a row object, an associative array, or a RevisionRecord'
  348. );
  349. }
  350. Assert::postcondition( $this->mRecord !== null, 'Failed to construct a RevisionRecord' );
  351. }
  352. /**
  353. * Make sure we have *some* Title object for use by the constructor.
  354. * For B/C, the constructor shouldn't fail even for a bad page ID or bad revision ID.
  355. *
  356. * @param array|object $row
  357. * @param int $queryFlags
  358. * @param Title|null $title
  359. *
  360. * @return Title $title if not null, or a Title constructed from information in $row.
  361. */
  362. private function ensureTitle( $row, $queryFlags, $title = null ) {
  363. if ( $title ) {
  364. return $title;
  365. }
  366. if ( is_array( $row ) ) {
  367. if ( isset( $row['title'] ) ) {
  368. if ( !( $row['title'] instanceof Title ) ) {
  369. throw new MWException( 'title field must contain a Title object.' );
  370. }
  371. return $row['title'];
  372. }
  373. $pageId = $row['page'] ?? 0;
  374. $revId = $row['id'] ?? 0;
  375. } else {
  376. $pageId = $row->rev_page ?? 0;
  377. $revId = $row->rev_id ?? 0;
  378. }
  379. try {
  380. $title = self::getRevisionStore()->getTitle( $pageId, $revId, $queryFlags );
  381. } catch ( RevisionAccessException $ex ) {
  382. // construct a dummy title!
  383. wfLogWarning( __METHOD__ . ': ' . $ex->getMessage() );
  384. // NOTE: this Title will only be used inside RevisionRecord
  385. $title = Title::makeTitleSafe( NS_SPECIAL, "Badtitle/ID=$pageId" );
  386. $title->resetArticleID( $pageId );
  387. }
  388. return $title;
  389. }
  390. /**
  391. * @return RevisionRecord
  392. */
  393. public function getRevisionRecord() {
  394. return $this->mRecord;
  395. }
  396. /**
  397. * Get revision ID
  398. *
  399. * @return int|null
  400. */
  401. public function getId() {
  402. return $this->mRecord->getId();
  403. }
  404. /**
  405. * Set the revision ID
  406. *
  407. * This should only be used for proposed revisions that turn out to be null edits.
  408. *
  409. * @note Only supported on Revisions that were constructed based on associative arrays,
  410. * since they are mutable.
  411. *
  412. * @since 1.19
  413. * @param int|string $id
  414. * @throws MWException
  415. */
  416. public function setId( $id ) {
  417. if ( $this->mRecord instanceof MutableRevisionRecord ) {
  418. $this->mRecord->setId( intval( $id ) );
  419. } else {
  420. throw new MWException( __METHOD__ . ' is not supported on this instance' );
  421. }
  422. }
  423. /**
  424. * Set the user ID/name
  425. *
  426. * This should only be used for proposed revisions that turn out to be null edits
  427. *
  428. * @note Only supported on Revisions that were constructed based on associative arrays,
  429. * since they are mutable.
  430. *
  431. * @since 1.28
  432. * @deprecated since 1.31, please reuse old Revision object
  433. * @param int $id User ID
  434. * @param string $name User name
  435. * @throws MWException
  436. */
  437. public function setUserIdAndName( $id, $name ) {
  438. if ( $this->mRecord instanceof MutableRevisionRecord ) {
  439. $user = User::newFromAnyId( intval( $id ), $name, null );
  440. $this->mRecord->setUser( $user );
  441. } else {
  442. throw new MWException( __METHOD__ . ' is not supported on this instance' );
  443. }
  444. }
  445. /**
  446. * @return SlotRecord
  447. */
  448. private function getMainSlotRaw() {
  449. return $this->mRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
  450. }
  451. /**
  452. * Get the ID of the row of the text table that contains the content of the
  453. * revision's main slot, if that content is stored in the text table.
  454. *
  455. * If the content is stored elsewhere, this returns null.
  456. *
  457. * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
  458. * get that actual address that can be used with BlobStore::getBlob(); or use
  459. * RevisionRecord::hasSameContent() to check if two revisions have the same content.
  460. *
  461. * @return int|null
  462. */
  463. public function getTextId() {
  464. $slot = $this->getMainSlotRaw();
  465. return $slot->hasAddress()
  466. ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
  467. : null;
  468. }
  469. /**
  470. * Get parent revision ID (the original previous page revision)
  471. *
  472. * @return int|null The ID of the parent revision. 0 indicates that there is no
  473. * parent revision. Null indicates that the parent revision is not known.
  474. */
  475. public function getParentId() {
  476. return $this->mRecord->getParentId();
  477. }
  478. /**
  479. * Returns the length of the text in this revision, or null if unknown.
  480. *
  481. * @return int|null
  482. */
  483. public function getSize() {
  484. try {
  485. return $this->mRecord->getSize();
  486. } catch ( RevisionAccessException $ex ) {
  487. return null;
  488. }
  489. }
  490. /**
  491. * Returns the base36 sha1 of the content in this revision, or null if unknown.
  492. *
  493. * @return string|null
  494. */
  495. public function getSha1() {
  496. try {
  497. return $this->mRecord->getSha1();
  498. } catch ( RevisionAccessException $ex ) {
  499. return null;
  500. }
  501. }
  502. /**
  503. * Returns the title of the page associated with this entry.
  504. * Since 1.31, this will never return null.
  505. *
  506. * Will do a query, when title is not set and id is given.
  507. *
  508. * @return Title
  509. */
  510. public function getTitle() {
  511. $linkTarget = $this->mRecord->getPageAsLinkTarget();
  512. return Title::newFromLinkTarget( $linkTarget );
  513. }
  514. /**
  515. * Set the title of the revision
  516. *
  517. * @deprecated since 1.31, this is now a noop. Pass the Title to the constructor instead.
  518. *
  519. * @param Title $title
  520. */
  521. public function setTitle( $title ) {
  522. if ( !$title->equals( $this->getTitle() ) ) {
  523. throw new InvalidArgumentException(
  524. $title->getPrefixedText()
  525. . ' is not the same as '
  526. . $this->mRecord->getPageAsLinkTarget()->__toString()
  527. );
  528. }
  529. }
  530. /**
  531. * Get the page ID
  532. *
  533. * @return int|null
  534. */
  535. public function getPage() {
  536. return $this->mRecord->getPageId();
  537. }
  538. /**
  539. * Fetch revision's user id if it's available to the specified audience.
  540. * If the specified audience does not have access to it, zero will be
  541. * returned.
  542. *
  543. * @param int $audience One of:
  544. * Revision::FOR_PUBLIC to be displayed to all users
  545. * Revision::FOR_THIS_USER to be displayed to the given user
  546. * Revision::RAW get the ID regardless of permissions
  547. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  548. * to the $audience parameter
  549. * @return int
  550. */
  551. public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
  552. global $wgUser;
  553. if ( $audience === self::FOR_THIS_USER && !$user ) {
  554. $user = $wgUser;
  555. }
  556. $user = $this->mRecord->getUser( $audience, $user );
  557. return $user ? $user->getId() : 0;
  558. }
  559. /**
  560. * Fetch revision's username if it's available to the specified audience.
  561. * If the specified audience does not have access to the username, an
  562. * empty string will be returned.
  563. *
  564. * @param int $audience One of:
  565. * Revision::FOR_PUBLIC to be displayed to all users
  566. * Revision::FOR_THIS_USER to be displayed to the given user
  567. * Revision::RAW get the text regardless of permissions
  568. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  569. * to the $audience parameter
  570. * @return string
  571. */
  572. public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
  573. global $wgUser;
  574. if ( $audience === self::FOR_THIS_USER && !$user ) {
  575. $user = $wgUser;
  576. }
  577. $user = $this->mRecord->getUser( $audience, $user );
  578. return $user ? $user->getName() : '';
  579. }
  580. /**
  581. * @param int $audience One of:
  582. * Revision::FOR_PUBLIC to be displayed to all users
  583. * Revision::FOR_THIS_USER to be displayed to the given user
  584. * Revision::RAW get the text regardless of permissions
  585. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  586. * to the $audience parameter
  587. *
  588. * @return string|null Returns null if the specified audience does not have access to the
  589. * comment.
  590. */
  591. function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
  592. global $wgUser;
  593. if ( $audience === self::FOR_THIS_USER && !$user ) {
  594. $user = $wgUser;
  595. }
  596. $comment = $this->mRecord->getComment( $audience, $user );
  597. return $comment === null ? null : $comment->text;
  598. }
  599. /**
  600. * @return bool
  601. */
  602. public function isMinor() {
  603. return $this->mRecord->isMinor();
  604. }
  605. /**
  606. * @return int Rcid of the unpatrolled row, zero if there isn't one
  607. */
  608. public function isUnpatrolled() {
  609. return self::getRevisionStore()->getRcIdIfUnpatrolled( $this->mRecord );
  610. }
  611. /**
  612. * Get the RC object belonging to the current revision, if there's one
  613. *
  614. * @param int $flags (optional) $flags include:
  615. * Revision::READ_LATEST : Select the data from the master
  616. *
  617. * @since 1.22
  618. * @return RecentChange|null
  619. */
  620. public function getRecentChange( $flags = 0 ) {
  621. return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
  622. }
  623. /**
  624. * @param int $field One of DELETED_* bitfield constants
  625. *
  626. * @return bool
  627. */
  628. public function isDeleted( $field ) {
  629. return $this->mRecord->isDeleted( $field );
  630. }
  631. /**
  632. * Get the deletion bitfield of the revision
  633. *
  634. * @return int
  635. */
  636. public function getVisibility() {
  637. return $this->mRecord->getVisibility();
  638. }
  639. /**
  640. * Fetch revision content if it's available to the specified audience.
  641. * If the specified audience does not have the ability to view this
  642. * revision, or the content could not be loaded, null will be returned.
  643. *
  644. * @param int $audience One of:
  645. * Revision::FOR_PUBLIC to be displayed to all users
  646. * Revision::FOR_THIS_USER to be displayed to $user
  647. * Revision::RAW get the text regardless of permissions
  648. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  649. * to the $audience parameter
  650. * @since 1.21
  651. * @return Content|null
  652. */
  653. public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
  654. global $wgUser;
  655. if ( $audience === self::FOR_THIS_USER && !$user ) {
  656. $user = $wgUser;
  657. }
  658. try {
  659. return $this->mRecord->getContent( SlotRecord::MAIN, $audience, $user );
  660. }
  661. catch ( RevisionAccessException $e ) {
  662. return null;
  663. }
  664. }
  665. /**
  666. * Get original serialized data (without checking view restrictions)
  667. *
  668. * @since 1.21
  669. * @deprecated since 1.31, use BlobStore::getBlob instead.
  670. *
  671. * @return string
  672. */
  673. public function getSerializedData() {
  674. $slot = $this->getMainSlotRaw();
  675. return $slot->getContent()->serialize();
  676. }
  677. /**
  678. * Returns the content model for the main slot of this revision.
  679. *
  680. * If no content model was stored in the database, the default content model for the title is
  681. * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
  682. * is used as a last resort.
  683. *
  684. * @todo drop this, with MCR, there no longer is a single model associated with a revision.
  685. *
  686. * @return string The content model id associated with this revision,
  687. * see the CONTENT_MODEL_XXX constants.
  688. */
  689. public function getContentModel() {
  690. return $this->getMainSlotRaw()->getModel();
  691. }
  692. /**
  693. * Returns the content format for the main slot of this revision.
  694. *
  695. * If no content format was stored in the database, the default format for this
  696. * revision's content model is returned.
  697. *
  698. * @todo drop this, the format is irrelevant to the revision!
  699. *
  700. * @return string The content format id associated with this revision,
  701. * see the CONTENT_FORMAT_XXX constants.
  702. */
  703. public function getContentFormat() {
  704. $format = $this->getMainSlotRaw()->getFormat();
  705. if ( $format === null ) {
  706. // if no format was stored along with the blob, fall back to default format
  707. $format = $this->getContentHandler()->getDefaultFormat();
  708. }
  709. return $format;
  710. }
  711. /**
  712. * Returns the content handler appropriate for this revision's content model.
  713. *
  714. * @throws MWException
  715. * @return ContentHandler
  716. */
  717. public function getContentHandler() {
  718. return ContentHandler::getForModelID( $this->getContentModel() );
  719. }
  720. /**
  721. * @return string
  722. */
  723. public function getTimestamp() {
  724. return $this->mRecord->getTimestamp();
  725. }
  726. /**
  727. * @return bool
  728. */
  729. public function isCurrent() {
  730. return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
  731. }
  732. /**
  733. * Get previous revision for this title
  734. *
  735. * @return Revision|null
  736. */
  737. public function getPrevious() {
  738. $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord );
  739. return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null;
  740. }
  741. /**
  742. * Get next revision for this title
  743. *
  744. * @return Revision|null
  745. */
  746. public function getNext() {
  747. $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord );
  748. return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null;
  749. }
  750. /**
  751. * Get revision text associated with an old or archive row
  752. *
  753. * If the text field is not included, this uses RevisionStore to load the appropriate slot
  754. * and return its serialized content. This is the default backwards-compatibility behavior
  755. * when reading from the MCR aware database schema is enabled. For this to work, either
  756. * the revision ID or the page ID must be included in the row.
  757. *
  758. * When using the old text field, the flags field must also be set. Including the old_id
  759. * field will activate cache usage as long as the $wiki parameter is not set.
  760. *
  761. * @deprecated since 1.32, use RevisionStore::newRevisionFromRow instead.
  762. *
  763. * @param stdClass $row The text data. If a falsy value is passed instead, false is returned.
  764. * @param string $prefix Table prefix (default 'old_')
  765. * @param string|bool $wiki The name of the wiki to load the revision text from
  766. * (same as the wiki $row was loaded from) or false to indicate the local
  767. * wiki (this is the default). Otherwise, it must be a symbolic wiki database
  768. * identifier as understood by the LoadBalancer class.
  769. * @return string|false Text the text requested or false on failure
  770. */
  771. public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
  772. global $wgMultiContentRevisionSchemaMigrationStage;
  773. if ( !$row ) {
  774. return false;
  775. }
  776. $textField = $prefix . 'text';
  777. $flagsField = $prefix . 'flags';
  778. if ( isset( $row->$textField ) ) {
  779. if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
  780. // The text field was read, but it's no longer being populated!
  781. // We could gloss over this by using the text when it's there and loading
  782. // if when it's not, but it seems preferable to complain loudly about a
  783. // query that is no longer guaranteed to work reliably.
  784. throw new LogicException(
  785. 'Cannot use ' . __METHOD__ . ' with the ' . $textField . ' field when'
  786. . ' $wgMultiContentRevisionSchemaMigrationStage does not include'
  787. . ' SCHEMA_COMPAT_WRITE_OLD. The field may not be populated for all revisions!'
  788. );
  789. }
  790. $text = $row->$textField;
  791. } else {
  792. // Missing text field, we are probably looking at the MCR-enabled DB schema.
  793. if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
  794. // This method should no longer be used with the new schema. Ideally, we
  795. // would already trigger a deprecation warning when SCHEMA_COMPAT_READ_NEW is set.
  796. wfDeprecated( __METHOD__ . ' (MCR without SCHEMA_COMPAT_WRITE_OLD)', '1.32' );
  797. }
  798. $store = self::getRevisionStore( $wiki );
  799. $rev = $prefix === 'ar_'
  800. ? $store->newRevisionFromArchiveRow( $row )
  801. : $store->newRevisionFromRow( $row );
  802. $content = $rev->getContent( SlotRecord::MAIN );
  803. return $content ? $content->serialize() : false;
  804. }
  805. if ( isset( $row->$flagsField ) ) {
  806. $flags = explode( ',', $row->$flagsField );
  807. } else {
  808. $flags = [];
  809. }
  810. $cacheKey = isset( $row->old_id )
  811. ? SqlBlobStore::makeAddressFromTextId( $row->old_id )
  812. : null;
  813. $revisionText = self::getBlobStore( $wiki )->expandBlob( $text, $flags, $cacheKey );
  814. if ( $revisionText === false ) {
  815. if ( isset( $row->old_id ) ) {
  816. wfLogWarning( __METHOD__ . ": Bad data in text row {$row->old_id}! " );
  817. } else {
  818. wfLogWarning( __METHOD__ . ": Bad data in text row! " );
  819. }
  820. return false;
  821. }
  822. return $revisionText;
  823. }
  824. /**
  825. * If $wgCompressRevisions is enabled, we will compress data.
  826. * The input string is modified in place.
  827. * Return value is the flags field: contains 'gzip' if the
  828. * data is compressed, and 'utf-8' if we're saving in UTF-8
  829. * mode.
  830. *
  831. * @param mixed &$text Reference to a text
  832. * @return string
  833. */
  834. public static function compressRevisionText( &$text ) {
  835. return self::getBlobStore()->compressData( $text );
  836. }
  837. /**
  838. * Re-converts revision text according to it's flags.
  839. *
  840. * @param mixed $text Reference to a text
  841. * @param array $flags Compression flags
  842. * @return string|bool Decompressed text, or false on failure
  843. */
  844. public static function decompressRevisionText( $text, $flags ) {
  845. if ( $text === false ) {
  846. // Text failed to be fetched; nothing to do
  847. return false;
  848. }
  849. return self::getBlobStore()->decompressData( $text, $flags );
  850. }
  851. /**
  852. * Insert a new revision into the database, returning the new revision ID
  853. * number on success and dies horribly on failure.
  854. *
  855. * @param IDatabase $dbw (master connection)
  856. * @throws MWException
  857. * @return int The revision ID
  858. */
  859. public function insertOn( $dbw ) {
  860. global $wgUser;
  861. // Note that $this->mRecord->getId() will typically return null here, but not always,
  862. // e.g. not when restoring a revision.
  863. if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
  864. if ( $this->mRecord instanceof MutableRevisionRecord ) {
  865. $this->mRecord->setUser( $wgUser );
  866. } else {
  867. throw new MWException( 'Cannot insert revision with no associated user.' );
  868. }
  869. }
  870. $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
  871. $this->mRecord = $rec;
  872. Assert::postcondition( $this->mRecord !== null, 'Failed to acquire a RevisionRecord' );
  873. return $rec->getId();
  874. }
  875. /**
  876. * Get the base 36 SHA-1 value for a string of text
  877. * @param string $text
  878. * @return string
  879. */
  880. public static function base36Sha1( $text ) {
  881. return SlotRecord::base36Sha1( $text );
  882. }
  883. /**
  884. * Create a new null-revision for insertion into a page's
  885. * history. This will not re-save the text, but simply refer
  886. * to the text from the previous version.
  887. *
  888. * Such revisions can for instance identify page rename
  889. * operations and other such meta-modifications.
  890. *
  891. * @param IDatabase $dbw
  892. * @param int $pageId ID number of the page to read from
  893. * @param string $summary Revision's summary
  894. * @param bool $minor Whether the revision should be considered as minor
  895. * @param User|null $user User object to use or null for $wgUser
  896. * @return Revision|null Revision or null on error
  897. */
  898. public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
  899. global $wgUser;
  900. if ( !$user ) {
  901. $user = $wgUser;
  902. }
  903. $comment = CommentStoreComment::newUnsavedComment( $summary, null );
  904. $title = Title::newFromID( $pageId, Title::READ_LATEST );
  905. if ( $title === null ) {
  906. return null;
  907. }
  908. $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
  909. return $rec ? new Revision( $rec ) : null;
  910. }
  911. /**
  912. * Determine if the current user is allowed to view a particular
  913. * field of this revision, if it's marked as deleted.
  914. *
  915. * @param int $field One of self::DELETED_TEXT,
  916. * self::DELETED_COMMENT,
  917. * self::DELETED_USER
  918. * @param User|null $user User object to check, or null to use $wgUser
  919. * @return bool
  920. */
  921. public function userCan( $field, User $user = null ) {
  922. return self::userCanBitfield( $this->getVisibility(), $field, $user );
  923. }
  924. /**
  925. * Determine if the current user is allowed to view a particular
  926. * field of this revision, if it's marked as deleted. This is used
  927. * by various classes to avoid duplication.
  928. *
  929. * @param int $bitfield Current field
  930. * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
  931. * self::DELETED_COMMENT = File::DELETED_COMMENT,
  932. * self::DELETED_USER = File::DELETED_USER
  933. * @param User|null $user User object to check, or null to use $wgUser
  934. * @param Title|null $title A Title object to check for per-page restrictions on,
  935. * instead of just plain userrights
  936. * @return bool
  937. */
  938. public static function userCanBitfield( $bitfield, $field, User $user = null,
  939. Title $title = null
  940. ) {
  941. global $wgUser;
  942. if ( !$user ) {
  943. $user = $wgUser;
  944. }
  945. return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
  946. }
  947. /**
  948. * Get rev_timestamp from rev_id, without loading the rest of the row
  949. *
  950. * @param Title $title (ignored since 1.34)
  951. * @param int $id
  952. * @param int $flags
  953. * @return string|bool False if not found
  954. */
  955. static function getTimestampFromId( $title, $id, $flags = 0 ) {
  956. return self::getRevisionStore()->getTimestampFromId( $id, $flags );
  957. }
  958. /**
  959. * Get count of revisions per page...not very efficient
  960. *
  961. * @param IDatabase $db
  962. * @param int $id Page id
  963. * @return int
  964. */
  965. static function countByPageId( $db, $id ) {
  966. return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
  967. }
  968. /**
  969. * Get count of revisions per page...not very efficient
  970. *
  971. * @param IDatabase $db
  972. * @param Title $title
  973. * @return int
  974. */
  975. static function countByTitle( $db, $title ) {
  976. return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
  977. }
  978. /**
  979. * Check if no edits were made by other users since
  980. * the time a user started editing the page. Limit to
  981. * 50 revisions for the sake of performance.
  982. *
  983. * @since 1.20
  984. * @deprecated since 1.24
  985. *
  986. * @param IDatabase|int $db The Database to perform the check on. May be given as a
  987. * Database object or a database identifier usable with wfGetDB.
  988. * @param int $pageId The ID of the page in question
  989. * @param int $userId The ID of the user in question
  990. * @param string $since Look at edits since this time
  991. *
  992. * @return bool True if the given user was the only one to edit since the given timestamp
  993. */
  994. public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
  995. if ( is_int( $db ) ) {
  996. $db = wfGetDB( $db );
  997. }
  998. return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
  999. }
  1000. /**
  1001. * Load a revision based on a known page ID and current revision ID from the DB
  1002. *
  1003. * This method allows for the use of caching, though accessing anything that normally
  1004. * requires permission checks (aside from the text) will trigger a small DB lookup.
  1005. * The title will also be loaded if $pageIdOrTitle is an integer ID.
  1006. *
  1007. * @param IDatabase $db ignored!
  1008. * @param int|Title $pageIdOrTitle Page ID or Title object
  1009. * @param int $revId Known current revision of this page. Determined automatically if not given.
  1010. * @return Revision|bool Returns false if missing
  1011. * @since 1.28
  1012. */
  1013. public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
  1014. $title = $pageIdOrTitle instanceof Title
  1015. ? $pageIdOrTitle
  1016. : Title::newFromID( $pageIdOrTitle );
  1017. if ( !$title ) {
  1018. return false;
  1019. }
  1020. $record = self::getRevisionLookup()->getKnownCurrentRevision( $title, $revId );
  1021. return $record ? new Revision( $record ) : false;
  1022. }
  1023. }