Revision.php 39 KB

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