Revision.php 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983
  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 Wikimedia\Rdbms\Database;
  23. use Wikimedia\Rdbms\IDatabase;
  24. use MediaWiki\Linker\LinkTarget;
  25. use MediaWiki\MediaWikiServices;
  26. use Wikimedia\Rdbms\ResultWrapper;
  27. use Wikimedia\Rdbms\FakeResultWrapper;
  28. /**
  29. * @todo document
  30. */
  31. class Revision implements IDBAccessObject {
  32. /** @var int|null */
  33. protected $mId;
  34. /** @var int|null */
  35. protected $mPage;
  36. /** @var string */
  37. protected $mUserText;
  38. /** @var string */
  39. protected $mOrigUserText;
  40. /** @var int */
  41. protected $mUser;
  42. /** @var bool */
  43. protected $mMinorEdit;
  44. /** @var string */
  45. protected $mTimestamp;
  46. /** @var int */
  47. protected $mDeleted;
  48. /** @var int */
  49. protected $mSize;
  50. /** @var string */
  51. protected $mSha1;
  52. /** @var int */
  53. protected $mParentId;
  54. /** @var string */
  55. protected $mComment;
  56. /** @var string */
  57. protected $mText;
  58. /** @var int */
  59. protected $mTextId;
  60. /** @var int */
  61. protected $mUnpatrolled;
  62. /** @var stdClass|null */
  63. protected $mTextRow;
  64. /** @var null|Title */
  65. protected $mTitle;
  66. /** @var bool */
  67. protected $mCurrent;
  68. /** @var string */
  69. protected $mContentModel;
  70. /** @var string */
  71. protected $mContentFormat;
  72. /** @var Content|null|bool */
  73. protected $mContent;
  74. /** @var null|ContentHandler */
  75. protected $mContentHandler;
  76. /** @var int */
  77. protected $mQueryFlags = 0;
  78. /** @var bool Used for cached values to reload user text and rev_deleted */
  79. protected $mRefreshMutableFields = false;
  80. /** @var string Wiki ID; false means the current wiki */
  81. protected $mWiki = false;
  82. // Revision deletion constants
  83. const DELETED_TEXT = 1;
  84. const DELETED_COMMENT = 2;
  85. const DELETED_USER = 4;
  86. const DELETED_RESTRICTED = 8;
  87. const SUPPRESSED_USER = 12; // convenience
  88. const SUPPRESSED_ALL = 15; // convenience
  89. // Audience options for accessors
  90. const FOR_PUBLIC = 1;
  91. const FOR_THIS_USER = 2;
  92. const RAW = 3;
  93. const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
  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. return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
  108. }
  109. /**
  110. * Load either the current, or a specified, revision
  111. * that's attached to a given link target. If not attached
  112. * to that link target, will return null.
  113. *
  114. * $flags include:
  115. * Revision::READ_LATEST : Select the data from the master
  116. * Revision::READ_LOCKING : Select & lock the data from the master
  117. *
  118. * @param LinkTarget $linkTarget
  119. * @param int $id (optional)
  120. * @param int $flags Bitfield (optional)
  121. * @return Revision|null
  122. */
  123. public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
  124. $conds = [
  125. 'page_namespace' => $linkTarget->getNamespace(),
  126. 'page_title' => $linkTarget->getDBkey()
  127. ];
  128. if ( $id ) {
  129. // Use the specified ID
  130. $conds['rev_id'] = $id;
  131. return self::newFromConds( $conds, $flags );
  132. } else {
  133. // Use a join to get the latest revision
  134. $conds[] = 'rev_id=page_latest';
  135. $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
  136. return self::loadFromConds( $db, $conds, $flags );
  137. }
  138. }
  139. /**
  140. * Load either the current, or a specified, revision
  141. * that's attached to a given page ID.
  142. * Returns null if no such revision can be found.
  143. *
  144. * $flags include:
  145. * Revision::READ_LATEST : Select the data from the master (since 1.20)
  146. * Revision::READ_LOCKING : Select & lock the data from the master
  147. *
  148. * @param int $pageId
  149. * @param int $revId (optional)
  150. * @param int $flags Bitfield (optional)
  151. * @return Revision|null
  152. */
  153. public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
  154. $conds = [ 'page_id' => $pageId ];
  155. if ( $revId ) {
  156. $conds['rev_id'] = $revId;
  157. return self::newFromConds( $conds, $flags );
  158. } else {
  159. // Use a join to get the latest revision
  160. $conds[] = 'rev_id = page_latest';
  161. $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
  162. return self::loadFromConds( $db, $conds, $flags );
  163. }
  164. }
  165. /**
  166. * Make a fake revision object from an archive table row. This is queried
  167. * for permissions or even inserted (as in Special:Undelete)
  168. * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
  169. *
  170. * @param object $row
  171. * @param array $overrides
  172. *
  173. * @throws MWException
  174. * @return Revision
  175. */
  176. public static function newFromArchiveRow( $row, $overrides = [] ) {
  177. global $wgContentHandlerUseDB;
  178. $attribs = $overrides + [
  179. 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
  180. 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
  181. 'comment' => CommentStore::newKey( 'ar_comment' )
  182. // Legacy because $row probably came from self::selectArchiveFields()
  183. ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
  184. 'user' => $row->ar_user,
  185. 'user_text' => $row->ar_user_text,
  186. 'timestamp' => $row->ar_timestamp,
  187. 'minor_edit' => $row->ar_minor_edit,
  188. 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
  189. 'deleted' => $row->ar_deleted,
  190. 'len' => $row->ar_len,
  191. 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
  192. 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
  193. 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
  194. ];
  195. if ( !$wgContentHandlerUseDB ) {
  196. unset( $attribs['content_model'] );
  197. unset( $attribs['content_format'] );
  198. }
  199. if ( !isset( $attribs['title'] )
  200. && isset( $row->ar_namespace )
  201. && isset( $row->ar_title )
  202. ) {
  203. $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
  204. }
  205. if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
  206. // Pre-1.5 ar_text row
  207. $attribs['text'] = self::getRevisionText( $row, 'ar_' );
  208. if ( $attribs['text'] === false ) {
  209. throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
  210. }
  211. }
  212. return new self( $attribs );
  213. }
  214. /**
  215. * @since 1.19
  216. *
  217. * @param object $row
  218. * @return Revision
  219. */
  220. public static function newFromRow( $row ) {
  221. return new self( $row );
  222. }
  223. /**
  224. * Load a page revision from a given revision ID number.
  225. * Returns null if no such revision can be found.
  226. *
  227. * @param IDatabase $db
  228. * @param int $id
  229. * @return Revision|null
  230. */
  231. public static function loadFromId( $db, $id ) {
  232. return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
  233. }
  234. /**
  235. * Load either the current, or a specified, revision
  236. * that's attached to a given page. If not attached
  237. * to that page, will return null.
  238. *
  239. * @param IDatabase $db
  240. * @param int $pageid
  241. * @param int $id
  242. * @return Revision|null
  243. */
  244. public static function loadFromPageId( $db, $pageid, $id = 0 ) {
  245. $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
  246. if ( $id ) {
  247. $conds['rev_id'] = intval( $id );
  248. } else {
  249. $conds[] = 'rev_id=page_latest';
  250. }
  251. return self::loadFromConds( $db, $conds );
  252. }
  253. /**
  254. * Load either the current, or a specified, revision
  255. * that's attached to a given page. If not attached
  256. * to that page, will return null.
  257. *
  258. * @param IDatabase $db
  259. * @param Title $title
  260. * @param int $id
  261. * @return Revision|null
  262. */
  263. public static function loadFromTitle( $db, $title, $id = 0 ) {
  264. if ( $id ) {
  265. $matchId = intval( $id );
  266. } else {
  267. $matchId = 'page_latest';
  268. }
  269. return self::loadFromConds( $db,
  270. [
  271. "rev_id=$matchId",
  272. 'page_namespace' => $title->getNamespace(),
  273. 'page_title' => $title->getDBkey()
  274. ]
  275. );
  276. }
  277. /**
  278. * Load the revision for the given title with the given timestamp.
  279. * WARNING: Timestamps may in some circumstances not be unique,
  280. * so this isn't the best key to use.
  281. *
  282. * @param IDatabase $db
  283. * @param Title $title
  284. * @param string $timestamp
  285. * @return Revision|null
  286. */
  287. public static function loadFromTimestamp( $db, $title, $timestamp ) {
  288. return self::loadFromConds( $db,
  289. [
  290. 'rev_timestamp' => $db->timestamp( $timestamp ),
  291. 'page_namespace' => $title->getNamespace(),
  292. 'page_title' => $title->getDBkey()
  293. ]
  294. );
  295. }
  296. /**
  297. * Given a set of conditions, fetch a revision
  298. *
  299. * This method is used then a revision ID is qualified and
  300. * will incorporate some basic replica DB/master fallback logic
  301. *
  302. * @param array $conditions
  303. * @param int $flags (optional)
  304. * @return Revision|null
  305. */
  306. private static function newFromConds( $conditions, $flags = 0 ) {
  307. $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
  308. $rev = self::loadFromConds( $db, $conditions, $flags );
  309. // Make sure new pending/committed revision are visibile later on
  310. // within web requests to certain avoid bugs like T93866 and T94407.
  311. if ( !$rev
  312. && !( $flags & self::READ_LATEST )
  313. && wfGetLB()->getServerCount() > 1
  314. && wfGetLB()->hasOrMadeRecentMasterChanges()
  315. ) {
  316. $flags = self::READ_LATEST;
  317. $db = wfGetDB( DB_MASTER );
  318. $rev = self::loadFromConds( $db, $conditions, $flags );
  319. }
  320. if ( $rev ) {
  321. $rev->mQueryFlags = $flags;
  322. }
  323. return $rev;
  324. }
  325. /**
  326. * Given a set of conditions, fetch a revision from
  327. * the given database connection.
  328. *
  329. * @param IDatabase $db
  330. * @param array $conditions
  331. * @param int $flags (optional)
  332. * @return Revision|null
  333. */
  334. private static function loadFromConds( $db, $conditions, $flags = 0 ) {
  335. $row = self::fetchFromConds( $db, $conditions, $flags );
  336. if ( $row ) {
  337. $rev = new Revision( $row );
  338. $rev->mWiki = $db->getDomainID();
  339. return $rev;
  340. }
  341. return null;
  342. }
  343. /**
  344. * Return a wrapper for a series of database rows to
  345. * fetch all of a given page's revisions in turn.
  346. * Each row can be fed to the constructor to get objects.
  347. *
  348. * @param LinkTarget $title
  349. * @return ResultWrapper
  350. * @deprecated Since 1.28
  351. */
  352. public static function fetchRevision( LinkTarget $title ) {
  353. $row = self::fetchFromConds(
  354. wfGetDB( DB_REPLICA ),
  355. [
  356. 'rev_id=page_latest',
  357. 'page_namespace' => $title->getNamespace(),
  358. 'page_title' => $title->getDBkey()
  359. ]
  360. );
  361. return new FakeResultWrapper( $row ? [ $row ] : [] );
  362. }
  363. /**
  364. * Given a set of conditions, return a ResultWrapper
  365. * which will return matching database rows with the
  366. * fields necessary to build Revision objects.
  367. *
  368. * @param IDatabase $db
  369. * @param array $conditions
  370. * @param int $flags (optional)
  371. * @return stdClass
  372. */
  373. private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
  374. $fields = array_merge(
  375. self::selectFields(),
  376. self::selectPageFields(),
  377. self::selectUserFields()
  378. );
  379. $options = [];
  380. if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
  381. $options[] = 'FOR UPDATE';
  382. }
  383. return $db->selectRow(
  384. [ 'revision', 'page', 'user' ],
  385. $fields,
  386. $conditions,
  387. __METHOD__,
  388. $options,
  389. [ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ]
  390. );
  391. }
  392. /**
  393. * Return the value of a select() JOIN conds array for the user table.
  394. * This will get user table rows for logged-in users.
  395. * @since 1.19
  396. * @return array
  397. */
  398. public static function userJoinCond() {
  399. return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
  400. }
  401. /**
  402. * Return the value of a select() page conds array for the page table.
  403. * This will assure that the revision(s) are not orphaned from live pages.
  404. * @since 1.19
  405. * @return array
  406. */
  407. public static function pageJoinCond() {
  408. return [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
  409. }
  410. /**
  411. * Return the list of revision fields that should be selected to create
  412. * a new revision.
  413. * @todo Deprecate this in favor of a method that returns tables and joins
  414. * as well, and use CommentStore::getJoin().
  415. * @return array
  416. */
  417. public static function selectFields() {
  418. global $wgContentHandlerUseDB;
  419. $fields = [
  420. 'rev_id',
  421. 'rev_page',
  422. 'rev_text_id',
  423. 'rev_timestamp',
  424. 'rev_user_text',
  425. 'rev_user',
  426. 'rev_minor_edit',
  427. 'rev_deleted',
  428. 'rev_len',
  429. 'rev_parent_id',
  430. 'rev_sha1',
  431. ];
  432. $fields += CommentStore::newKey( 'rev_comment' )->getFields();
  433. if ( $wgContentHandlerUseDB ) {
  434. $fields[] = 'rev_content_format';
  435. $fields[] = 'rev_content_model';
  436. }
  437. return $fields;
  438. }
  439. /**
  440. * Return the list of revision fields that should be selected to create
  441. * a new revision from an archive row.
  442. * @todo Deprecate this in favor of a method that returns tables and joins
  443. * as well, and use CommentStore::getJoin().
  444. * @return array
  445. */
  446. public static function selectArchiveFields() {
  447. global $wgContentHandlerUseDB;
  448. $fields = [
  449. 'ar_id',
  450. 'ar_page_id',
  451. 'ar_rev_id',
  452. 'ar_text',
  453. 'ar_text_id',
  454. 'ar_timestamp',
  455. 'ar_user_text',
  456. 'ar_user',
  457. 'ar_minor_edit',
  458. 'ar_deleted',
  459. 'ar_len',
  460. 'ar_parent_id',
  461. 'ar_sha1',
  462. ];
  463. $fields += CommentStore::newKey( 'ar_comment' )->getFields();
  464. if ( $wgContentHandlerUseDB ) {
  465. $fields[] = 'ar_content_format';
  466. $fields[] = 'ar_content_model';
  467. }
  468. return $fields;
  469. }
  470. /**
  471. * Return the list of text fields that should be selected to read the
  472. * revision text
  473. * @return array
  474. */
  475. public static function selectTextFields() {
  476. return [
  477. 'old_text',
  478. 'old_flags'
  479. ];
  480. }
  481. /**
  482. * Return the list of page fields that should be selected from page table
  483. * @return array
  484. */
  485. public static function selectPageFields() {
  486. return [
  487. 'page_namespace',
  488. 'page_title',
  489. 'page_id',
  490. 'page_latest',
  491. 'page_is_redirect',
  492. 'page_len',
  493. ];
  494. }
  495. /**
  496. * Return the list of user fields that should be selected from user table
  497. * @return array
  498. */
  499. public static function selectUserFields() {
  500. return [ 'user_name' ];
  501. }
  502. /**
  503. * Do a batched query to get the parent revision lengths
  504. * @param IDatabase $db
  505. * @param array $revIds
  506. * @return array
  507. */
  508. public static function getParentLengths( $db, array $revIds ) {
  509. $revLens = [];
  510. if ( !$revIds ) {
  511. return $revLens; // empty
  512. }
  513. $res = $db->select( 'revision',
  514. [ 'rev_id', 'rev_len' ],
  515. [ 'rev_id' => $revIds ],
  516. __METHOD__ );
  517. foreach ( $res as $row ) {
  518. $revLens[$row->rev_id] = $row->rev_len;
  519. }
  520. return $revLens;
  521. }
  522. /**
  523. * @param object|array $row Either a database row or an array
  524. * @throws MWException
  525. * @access private
  526. */
  527. function __construct( $row ) {
  528. if ( is_object( $row ) ) {
  529. $this->mId = intval( $row->rev_id );
  530. $this->mPage = intval( $row->rev_page );
  531. $this->mTextId = intval( $row->rev_text_id );
  532. $this->mComment = CommentStore::newKey( 'rev_comment' )
  533. // Legacy because $row probably came from self::selectFields()
  534. ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
  535. $this->mUser = intval( $row->rev_user );
  536. $this->mMinorEdit = intval( $row->rev_minor_edit );
  537. $this->mTimestamp = $row->rev_timestamp;
  538. $this->mDeleted = intval( $row->rev_deleted );
  539. if ( !isset( $row->rev_parent_id ) ) {
  540. $this->mParentId = null;
  541. } else {
  542. $this->mParentId = intval( $row->rev_parent_id );
  543. }
  544. if ( !isset( $row->rev_len ) ) {
  545. $this->mSize = null;
  546. } else {
  547. $this->mSize = intval( $row->rev_len );
  548. }
  549. if ( !isset( $row->rev_sha1 ) ) {
  550. $this->mSha1 = null;
  551. } else {
  552. $this->mSha1 = $row->rev_sha1;
  553. }
  554. if ( isset( $row->page_latest ) ) {
  555. $this->mCurrent = ( $row->rev_id == $row->page_latest );
  556. $this->mTitle = Title::newFromRow( $row );
  557. } else {
  558. $this->mCurrent = false;
  559. $this->mTitle = null;
  560. }
  561. if ( !isset( $row->rev_content_model ) ) {
  562. $this->mContentModel = null; # determine on demand if needed
  563. } else {
  564. $this->mContentModel = strval( $row->rev_content_model );
  565. }
  566. if ( !isset( $row->rev_content_format ) ) {
  567. $this->mContentFormat = null; # determine on demand if needed
  568. } else {
  569. $this->mContentFormat = strval( $row->rev_content_format );
  570. }
  571. // Lazy extraction...
  572. $this->mText = null;
  573. if ( isset( $row->old_text ) ) {
  574. $this->mTextRow = $row;
  575. } else {
  576. // 'text' table row entry will be lazy-loaded
  577. $this->mTextRow = null;
  578. }
  579. // Use user_name for users and rev_user_text for IPs...
  580. $this->mUserText = null; // lazy load if left null
  581. if ( $this->mUser == 0 ) {
  582. $this->mUserText = $row->rev_user_text; // IP user
  583. } elseif ( isset( $row->user_name ) ) {
  584. $this->mUserText = $row->user_name; // logged-in user
  585. }
  586. $this->mOrigUserText = $row->rev_user_text;
  587. } elseif ( is_array( $row ) ) {
  588. // Build a new revision to be saved...
  589. global $wgUser; // ugh
  590. # if we have a content object, use it to set the model and type
  591. if ( !empty( $row['content'] ) ) {
  592. // @todo when is that set? test with external store setup! check out insertOn() [dk]
  593. if ( !empty( $row['text_id'] ) ) {
  594. throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
  595. "can't serialize content object" );
  596. }
  597. $row['content_model'] = $row['content']->getModel();
  598. # note: mContentFormat is initializes later accordingly
  599. # note: content is serialized later in this method!
  600. # also set text to null?
  601. }
  602. $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
  603. $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
  604. $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
  605. $this->mUserText = isset( $row['user_text'] )
  606. ? strval( $row['user_text'] ) : $wgUser->getName();
  607. $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
  608. $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
  609. $this->mTimestamp = isset( $row['timestamp'] )
  610. ? strval( $row['timestamp'] ) : wfTimestampNow();
  611. $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
  612. $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
  613. $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
  614. $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
  615. $this->mContentModel = isset( $row['content_model'] )
  616. ? strval( $row['content_model'] ) : null;
  617. $this->mContentFormat = isset( $row['content_format'] )
  618. ? strval( $row['content_format'] ) : null;
  619. // Enforce spacing trimming on supplied text
  620. $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
  621. $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
  622. $this->mTextRow = null;
  623. $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
  624. // if we have a Content object, override mText and mContentModel
  625. if ( !empty( $row['content'] ) ) {
  626. if ( !( $row['content'] instanceof Content ) ) {
  627. throw new MWException( '`content` field must contain a Content object.' );
  628. }
  629. $handler = $this->getContentHandler();
  630. $this->mContent = $row['content'];
  631. $this->mContentModel = $this->mContent->getModel();
  632. $this->mContentHandler = null;
  633. $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
  634. } elseif ( $this->mText !== null ) {
  635. $handler = $this->getContentHandler();
  636. $this->mContent = $handler->unserializeContent( $this->mText );
  637. }
  638. // If we have a Title object, make sure it is consistent with mPage.
  639. if ( $this->mTitle && $this->mTitle->exists() ) {
  640. if ( $this->mPage === null ) {
  641. // if the page ID wasn't known, set it now
  642. $this->mPage = $this->mTitle->getArticleID();
  643. } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
  644. // Got different page IDs. This may be legit (e.g. during undeletion),
  645. // but it seems worth mentioning it in the log.
  646. wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
  647. $this->mTitle->getArticleID() . " provided by the Title object." );
  648. }
  649. }
  650. $this->mCurrent = false;
  651. // If we still have no length, see it we have the text to figure it out
  652. if ( !$this->mSize && $this->mContent !== null ) {
  653. $this->mSize = $this->mContent->getSize();
  654. }
  655. // Same for sha1
  656. if ( $this->mSha1 === null ) {
  657. $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
  658. }
  659. // force lazy init
  660. $this->getContentModel();
  661. $this->getContentFormat();
  662. } else {
  663. throw new MWException( 'Revision constructor passed invalid row format.' );
  664. }
  665. $this->mUnpatrolled = null;
  666. }
  667. /**
  668. * Get revision ID
  669. *
  670. * @return int|null
  671. */
  672. public function getId() {
  673. return $this->mId;
  674. }
  675. /**
  676. * Set the revision ID
  677. *
  678. * This should only be used for proposed revisions that turn out to be null edits
  679. *
  680. * @since 1.19
  681. * @param int $id
  682. */
  683. public function setId( $id ) {
  684. $this->mId = (int)$id;
  685. }
  686. /**
  687. * Set the user ID/name
  688. *
  689. * This should only be used for proposed revisions that turn out to be null edits
  690. *
  691. * @since 1.28
  692. * @param int $id User ID
  693. * @param string $name User name
  694. */
  695. public function setUserIdAndName( $id, $name ) {
  696. $this->mUser = (int)$id;
  697. $this->mUserText = $name;
  698. $this->mOrigUserText = $name;
  699. }
  700. /**
  701. * Get text row ID
  702. *
  703. * @return int|null
  704. */
  705. public function getTextId() {
  706. return $this->mTextId;
  707. }
  708. /**
  709. * Get parent revision ID (the original previous page revision)
  710. *
  711. * @return int|null
  712. */
  713. public function getParentId() {
  714. return $this->mParentId;
  715. }
  716. /**
  717. * Returns the length of the text in this revision, or null if unknown.
  718. *
  719. * @return int|null
  720. */
  721. public function getSize() {
  722. return $this->mSize;
  723. }
  724. /**
  725. * Returns the base36 sha1 of the text in this revision, or null if unknown.
  726. *
  727. * @return string|null
  728. */
  729. public function getSha1() {
  730. return $this->mSha1;
  731. }
  732. /**
  733. * Returns the title of the page associated with this entry or null.
  734. *
  735. * Will do a query, when title is not set and id is given.
  736. *
  737. * @return Title|null
  738. */
  739. public function getTitle() {
  740. if ( $this->mTitle !== null ) {
  741. return $this->mTitle;
  742. }
  743. // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
  744. if ( $this->mId !== null ) {
  745. $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
  746. $row = $dbr->selectRow(
  747. [ 'page', 'revision' ],
  748. self::selectPageFields(),
  749. [ 'page_id=rev_page', 'rev_id' => $this->mId ],
  750. __METHOD__
  751. );
  752. if ( $row ) {
  753. // @TODO: better foreign title handling
  754. $this->mTitle = Title::newFromRow( $row );
  755. }
  756. }
  757. if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
  758. // Loading by ID is best, though not possible for foreign titles
  759. if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
  760. $this->mTitle = Title::newFromID( $this->mPage );
  761. }
  762. }
  763. return $this->mTitle;
  764. }
  765. /**
  766. * Set the title of the revision
  767. *
  768. * @param Title $title
  769. */
  770. public function setTitle( $title ) {
  771. $this->mTitle = $title;
  772. }
  773. /**
  774. * Get the page ID
  775. *
  776. * @return int|null
  777. */
  778. public function getPage() {
  779. return $this->mPage;
  780. }
  781. /**
  782. * Fetch revision's user id if it's available to the specified audience.
  783. * If the specified audience does not have access to it, zero will be
  784. * returned.
  785. *
  786. * @param int $audience One of:
  787. * Revision::FOR_PUBLIC to be displayed to all users
  788. * Revision::FOR_THIS_USER to be displayed to the given user
  789. * Revision::RAW get the ID regardless of permissions
  790. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  791. * to the $audience parameter
  792. * @return int
  793. */
  794. public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
  795. if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
  796. return 0;
  797. } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
  798. return 0;
  799. } else {
  800. return $this->mUser;
  801. }
  802. }
  803. /**
  804. * Fetch revision's user id without regard for the current user's permissions
  805. *
  806. * @return int
  807. * @deprecated since 1.25, use getUser( Revision::RAW )
  808. */
  809. public function getRawUser() {
  810. wfDeprecated( __METHOD__, '1.25' );
  811. return $this->getUser( self::RAW );
  812. }
  813. /**
  814. * Fetch revision's username if it's available to the specified audience.
  815. * If the specified audience does not have access to the username, an
  816. * empty string will be returned.
  817. *
  818. * @param int $audience One of:
  819. * Revision::FOR_PUBLIC to be displayed to all users
  820. * Revision::FOR_THIS_USER to be displayed to the given user
  821. * Revision::RAW get the text regardless of permissions
  822. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  823. * to the $audience parameter
  824. * @return string
  825. */
  826. public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
  827. $this->loadMutableFields();
  828. if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
  829. return '';
  830. } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
  831. return '';
  832. } else {
  833. if ( $this->mUserText === null ) {
  834. $this->mUserText = User::whoIs( $this->mUser ); // load on demand
  835. if ( $this->mUserText === false ) {
  836. # This shouldn't happen, but it can if the wiki was recovered
  837. # via importing revs and there is no user table entry yet.
  838. $this->mUserText = $this->mOrigUserText;
  839. }
  840. }
  841. return $this->mUserText;
  842. }
  843. }
  844. /**
  845. * Fetch revision's username without regard for view restrictions
  846. *
  847. * @return string
  848. * @deprecated since 1.25, use getUserText( Revision::RAW )
  849. */
  850. public function getRawUserText() {
  851. wfDeprecated( __METHOD__, '1.25' );
  852. return $this->getUserText( self::RAW );
  853. }
  854. /**
  855. * Fetch revision comment if it's available to the specified audience.
  856. * If the specified audience does not have access to the comment, an
  857. * empty string will be returned.
  858. *
  859. * @param int $audience One of:
  860. * Revision::FOR_PUBLIC to be displayed to all users
  861. * Revision::FOR_THIS_USER to be displayed to the given user
  862. * Revision::RAW get the text regardless of permissions
  863. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  864. * to the $audience parameter
  865. * @return string
  866. */
  867. function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
  868. if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
  869. return '';
  870. } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
  871. return '';
  872. } else {
  873. return $this->mComment;
  874. }
  875. }
  876. /**
  877. * Fetch revision comment without regard for the current user's permissions
  878. *
  879. * @return string
  880. * @deprecated since 1.25, use getComment( Revision::RAW )
  881. */
  882. public function getRawComment() {
  883. wfDeprecated( __METHOD__, '1.25' );
  884. return $this->getComment( self::RAW );
  885. }
  886. /**
  887. * @return bool
  888. */
  889. public function isMinor() {
  890. return (bool)$this->mMinorEdit;
  891. }
  892. /**
  893. * @return int Rcid of the unpatrolled row, zero if there isn't one
  894. */
  895. public function isUnpatrolled() {
  896. if ( $this->mUnpatrolled !== null ) {
  897. return $this->mUnpatrolled;
  898. }
  899. $rc = $this->getRecentChange();
  900. if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
  901. $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
  902. } else {
  903. $this->mUnpatrolled = 0;
  904. }
  905. return $this->mUnpatrolled;
  906. }
  907. /**
  908. * Get the RC object belonging to the current revision, if there's one
  909. *
  910. * @param int $flags (optional) $flags include:
  911. * Revision::READ_LATEST : Select the data from the master
  912. *
  913. * @since 1.22
  914. * @return RecentChange|null
  915. */
  916. public function getRecentChange( $flags = 0 ) {
  917. $dbr = wfGetDB( DB_REPLICA );
  918. list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
  919. return RecentChange::newFromConds(
  920. [
  921. 'rc_user_text' => $this->getUserText( self::RAW ),
  922. 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
  923. 'rc_this_oldid' => $this->getId()
  924. ],
  925. __METHOD__,
  926. $dbType
  927. );
  928. }
  929. /**
  930. * @param int $field One of DELETED_* bitfield constants
  931. *
  932. * @return bool
  933. */
  934. public function isDeleted( $field ) {
  935. if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
  936. // Current revisions of pages cannot have the content hidden. Skipping this
  937. // check is very useful for Parser as it fetches templates using newKnownCurrent().
  938. // Calling getVisibility() in that case triggers a verification database query.
  939. return false; // no need to check
  940. }
  941. return ( $this->getVisibility() & $field ) == $field;
  942. }
  943. /**
  944. * Get the deletion bitfield of the revision
  945. *
  946. * @return int
  947. */
  948. public function getVisibility() {
  949. $this->loadMutableFields();
  950. return (int)$this->mDeleted;
  951. }
  952. /**
  953. * Fetch revision content if it's available to the specified audience.
  954. * If the specified audience does not have the ability to view this
  955. * revision, null will be returned.
  956. *
  957. * @param int $audience One of:
  958. * Revision::FOR_PUBLIC to be displayed to all users
  959. * Revision::FOR_THIS_USER to be displayed to $wgUser
  960. * Revision::RAW get the text regardless of permissions
  961. * @param User $user User object to check for, only if FOR_THIS_USER is passed
  962. * to the $audience parameter
  963. * @since 1.21
  964. * @return Content|null
  965. */
  966. public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
  967. if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
  968. return null;
  969. } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
  970. return null;
  971. } else {
  972. return $this->getContentInternal();
  973. }
  974. }
  975. /**
  976. * Get original serialized data (without checking view restrictions)
  977. *
  978. * @since 1.21
  979. * @return string
  980. */
  981. public function getSerializedData() {
  982. if ( $this->mText === null ) {
  983. // Revision is immutable. Load on demand.
  984. $this->mText = $this->loadText();
  985. }
  986. return $this->mText;
  987. }
  988. /**
  989. * Gets the content object for the revision (or null on failure).
  990. *
  991. * Note that for mutable Content objects, each call to this method will return a
  992. * fresh clone.
  993. *
  994. * @since 1.21
  995. * @return Content|null The Revision's content, or null on failure.
  996. */
  997. protected function getContentInternal() {
  998. if ( $this->mContent === null ) {
  999. $text = $this->getSerializedData();
  1000. if ( $text !== null && $text !== false ) {
  1001. // Unserialize content
  1002. $handler = $this->getContentHandler();
  1003. $format = $this->getContentFormat();
  1004. $this->mContent = $handler->unserializeContent( $text, $format );
  1005. }
  1006. }
  1007. // NOTE: copy() will return $this for immutable content objects
  1008. return $this->mContent ? $this->mContent->copy() : null;
  1009. }
  1010. /**
  1011. * Returns the content model for this revision.
  1012. *
  1013. * If no content model was stored in the database, the default content model for the title is
  1014. * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
  1015. * is used as a last resort.
  1016. *
  1017. * @return string The content model id associated with this revision,
  1018. * see the CONTENT_MODEL_XXX constants.
  1019. */
  1020. public function getContentModel() {
  1021. if ( !$this->mContentModel ) {
  1022. $title = $this->getTitle();
  1023. if ( $title ) {
  1024. $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
  1025. } else {
  1026. $this->mContentModel = CONTENT_MODEL_WIKITEXT;
  1027. }
  1028. assert( !empty( $this->mContentModel ) );
  1029. }
  1030. return $this->mContentModel;
  1031. }
  1032. /**
  1033. * Returns the content format for this revision.
  1034. *
  1035. * If no content format was stored in the database, the default format for this
  1036. * revision's content model is returned.
  1037. *
  1038. * @return string The content format id associated with this revision,
  1039. * see the CONTENT_FORMAT_XXX constants.
  1040. */
  1041. public function getContentFormat() {
  1042. if ( !$this->mContentFormat ) {
  1043. $handler = $this->getContentHandler();
  1044. $this->mContentFormat = $handler->getDefaultFormat();
  1045. assert( !empty( $this->mContentFormat ) );
  1046. }
  1047. return $this->mContentFormat;
  1048. }
  1049. /**
  1050. * Returns the content handler appropriate for this revision's content model.
  1051. *
  1052. * @throws MWException
  1053. * @return ContentHandler
  1054. */
  1055. public function getContentHandler() {
  1056. if ( !$this->mContentHandler ) {
  1057. $model = $this->getContentModel();
  1058. $this->mContentHandler = ContentHandler::getForModelID( $model );
  1059. $format = $this->getContentFormat();
  1060. if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
  1061. throw new MWException( "Oops, the content format $format is not supported for "
  1062. . "this content model, $model" );
  1063. }
  1064. }
  1065. return $this->mContentHandler;
  1066. }
  1067. /**
  1068. * @return string
  1069. */
  1070. public function getTimestamp() {
  1071. return wfTimestamp( TS_MW, $this->mTimestamp );
  1072. }
  1073. /**
  1074. * @return bool
  1075. */
  1076. public function isCurrent() {
  1077. return $this->mCurrent;
  1078. }
  1079. /**
  1080. * Get previous revision for this title
  1081. *
  1082. * @return Revision|null
  1083. */
  1084. public function getPrevious() {
  1085. if ( $this->getTitle() ) {
  1086. $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
  1087. if ( $prev ) {
  1088. return self::newFromTitle( $this->getTitle(), $prev );
  1089. }
  1090. }
  1091. return null;
  1092. }
  1093. /**
  1094. * Get next revision for this title
  1095. *
  1096. * @return Revision|null
  1097. */
  1098. public function getNext() {
  1099. if ( $this->getTitle() ) {
  1100. $next = $this->getTitle()->getNextRevisionID( $this->getId() );
  1101. if ( $next ) {
  1102. return self::newFromTitle( $this->getTitle(), $next );
  1103. }
  1104. }
  1105. return null;
  1106. }
  1107. /**
  1108. * Get previous revision Id for this page_id
  1109. * This is used to populate rev_parent_id on save
  1110. *
  1111. * @param IDatabase $db
  1112. * @return int
  1113. */
  1114. private function getPreviousRevisionId( $db ) {
  1115. if ( $this->mPage === null ) {
  1116. return 0;
  1117. }
  1118. # Use page_latest if ID is not given
  1119. if ( !$this->mId ) {
  1120. $prevId = $db->selectField( 'page', 'page_latest',
  1121. [ 'page_id' => $this->mPage ],
  1122. __METHOD__ );
  1123. } else {
  1124. $prevId = $db->selectField( 'revision', 'rev_id',
  1125. [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
  1126. __METHOD__,
  1127. [ 'ORDER BY' => 'rev_id DESC' ] );
  1128. }
  1129. return intval( $prevId );
  1130. }
  1131. /**
  1132. * Get revision text associated with an old or archive row
  1133. *
  1134. * Both the flags and the text field must be included. Including the old_id
  1135. * field will activate cache usage as long as the $wiki parameter is not set.
  1136. *
  1137. * @param stdClass $row The text data
  1138. * @param string $prefix Table prefix (default 'old_')
  1139. * @param string|bool $wiki The name of the wiki to load the revision text from
  1140. * (same as the the wiki $row was loaded from) or false to indicate the local
  1141. * wiki (this is the default). Otherwise, it must be a symbolic wiki database
  1142. * identifier as understood by the LoadBalancer class.
  1143. * @return string|false Text the text requested or false on failure
  1144. */
  1145. public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
  1146. $textField = $prefix . 'text';
  1147. $flagsField = $prefix . 'flags';
  1148. if ( isset( $row->$flagsField ) ) {
  1149. $flags = explode( ',', $row->$flagsField );
  1150. } else {
  1151. $flags = [];
  1152. }
  1153. if ( isset( $row->$textField ) ) {
  1154. $text = $row->$textField;
  1155. } else {
  1156. return false;
  1157. }
  1158. // Use external methods for external objects, text in table is URL-only then
  1159. if ( in_array( 'external', $flags ) ) {
  1160. $url = $text;
  1161. $parts = explode( '://', $url, 2 );
  1162. if ( count( $parts ) == 1 || $parts[1] == '' ) {
  1163. return false;
  1164. }
  1165. if ( isset( $row->old_id ) && $wiki === false ) {
  1166. // Make use of the wiki-local revision text cache
  1167. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  1168. // The cached value should be decompressed, so handle that and return here
  1169. return $cache->getWithSetCallback(
  1170. $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
  1171. self::getCacheTTL( $cache ),
  1172. function () use ( $url, $wiki, $flags ) {
  1173. // No negative caching per Revision::loadText()
  1174. $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
  1175. return self::decompressRevisionText( $text, $flags );
  1176. },
  1177. [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
  1178. );
  1179. } else {
  1180. $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
  1181. }
  1182. }
  1183. return self::decompressRevisionText( $text, $flags );
  1184. }
  1185. /**
  1186. * If $wgCompressRevisions is enabled, we will compress data.
  1187. * The input string is modified in place.
  1188. * Return value is the flags field: contains 'gzip' if the
  1189. * data is compressed, and 'utf-8' if we're saving in UTF-8
  1190. * mode.
  1191. *
  1192. * @param mixed &$text Reference to a text
  1193. * @return string
  1194. */
  1195. public static function compressRevisionText( &$text ) {
  1196. global $wgCompressRevisions;
  1197. $flags = [];
  1198. # Revisions not marked this way will be converted
  1199. # on load if $wgLegacyCharset is set in the future.
  1200. $flags[] = 'utf-8';
  1201. if ( $wgCompressRevisions ) {
  1202. if ( function_exists( 'gzdeflate' ) ) {
  1203. $deflated = gzdeflate( $text );
  1204. if ( $deflated === false ) {
  1205. wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
  1206. } else {
  1207. $text = $deflated;
  1208. $flags[] = 'gzip';
  1209. }
  1210. } else {
  1211. wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
  1212. }
  1213. }
  1214. return implode( ',', $flags );
  1215. }
  1216. /**
  1217. * Re-converts revision text according to it's flags.
  1218. *
  1219. * @param mixed $text Reference to a text
  1220. * @param array $flags Compression flags
  1221. * @return string|bool Decompressed text, or false on failure
  1222. */
  1223. public static function decompressRevisionText( $text, $flags ) {
  1224. global $wgLegacyEncoding, $wgContLang;
  1225. if ( $text === false ) {
  1226. // Text failed to be fetched; nothing to do
  1227. return false;
  1228. }
  1229. if ( in_array( 'gzip', $flags ) ) {
  1230. # Deal with optional compression of archived pages.
  1231. # This can be done periodically via maintenance/compressOld.php, and
  1232. # as pages are saved if $wgCompressRevisions is set.
  1233. $text = gzinflate( $text );
  1234. if ( $text === false ) {
  1235. wfLogWarning( __METHOD__ . ': gzinflate() failed' );
  1236. return false;
  1237. }
  1238. }
  1239. if ( in_array( 'object', $flags ) ) {
  1240. # Generic compressed storage
  1241. $obj = unserialize( $text );
  1242. if ( !is_object( $obj ) ) {
  1243. // Invalid object
  1244. return false;
  1245. }
  1246. $text = $obj->getText();
  1247. }
  1248. if ( $text !== false && $wgLegacyEncoding
  1249. && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
  1250. ) {
  1251. # Old revisions kept around in a legacy encoding?
  1252. # Upconvert on demand.
  1253. # ("utf8" checked for compatibility with some broken
  1254. # conversion scripts 2008-12-30)
  1255. $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
  1256. }
  1257. return $text;
  1258. }
  1259. /**
  1260. * Insert a new revision into the database, returning the new revision ID
  1261. * number on success and dies horribly on failure.
  1262. *
  1263. * @param IDatabase $dbw (master connection)
  1264. * @throws MWException
  1265. * @return int The revision ID
  1266. */
  1267. public function insertOn( $dbw ) {
  1268. global $wgDefaultExternalStore, $wgContentHandlerUseDB;
  1269. // We're inserting a new revision, so we have to use master anyway.
  1270. // If it's a null revision, it may have references to rows that
  1271. // are not in the replica yet (the text row).
  1272. $this->mQueryFlags |= self::READ_LATEST;
  1273. // Not allowed to have rev_page equal to 0, false, etc.
  1274. if ( !$this->mPage ) {
  1275. $title = $this->getTitle();
  1276. if ( $title instanceof Title ) {
  1277. $titleText = ' for page ' . $title->getPrefixedText();
  1278. } else {
  1279. $titleText = '';
  1280. }
  1281. throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
  1282. }
  1283. $this->checkContentModel();
  1284. $data = $this->mText;
  1285. $flags = self::compressRevisionText( $data );
  1286. # Write to external storage if required
  1287. if ( $wgDefaultExternalStore ) {
  1288. // Store and get the URL
  1289. $data = ExternalStore::insertToDefault( $data );
  1290. if ( !$data ) {
  1291. throw new MWException( "Unable to store text to external storage" );
  1292. }
  1293. if ( $flags ) {
  1294. $flags .= ',';
  1295. }
  1296. $flags .= 'external';
  1297. }
  1298. # Record the text (or external storage URL) to the text table
  1299. if ( $this->mTextId === null ) {
  1300. $dbw->insert( 'text',
  1301. [
  1302. 'old_text' => $data,
  1303. 'old_flags' => $flags,
  1304. ], __METHOD__
  1305. );
  1306. $this->mTextId = $dbw->insertId();
  1307. }
  1308. if ( $this->mComment === null ) {
  1309. $this->mComment = "";
  1310. }
  1311. # Record the edit in revisions
  1312. $row = [
  1313. 'rev_page' => $this->mPage,
  1314. 'rev_text_id' => $this->mTextId,
  1315. 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
  1316. 'rev_user' => $this->mUser,
  1317. 'rev_user_text' => $this->mUserText,
  1318. 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
  1319. 'rev_deleted' => $this->mDeleted,
  1320. 'rev_len' => $this->mSize,
  1321. 'rev_parent_id' => $this->mParentId === null
  1322. ? $this->getPreviousRevisionId( $dbw )
  1323. : $this->mParentId,
  1324. 'rev_sha1' => $this->mSha1 === null
  1325. ? self::base36Sha1( $this->mText )
  1326. : $this->mSha1,
  1327. ];
  1328. if ( $this->mId !== null ) {
  1329. $row['rev_id'] = $this->mId;
  1330. }
  1331. list( $commentFields, $commentCallback ) =
  1332. CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
  1333. $row += $commentFields;
  1334. if ( $wgContentHandlerUseDB ) {
  1335. // NOTE: Store null for the default model and format, to save space.
  1336. // XXX: Makes the DB sensitive to changed defaults.
  1337. // Make this behavior optional? Only in miser mode?
  1338. $model = $this->getContentModel();
  1339. $format = $this->getContentFormat();
  1340. $title = $this->getTitle();
  1341. if ( $title === null ) {
  1342. throw new MWException( "Insufficient information to determine the title of the "
  1343. . "revision's page!" );
  1344. }
  1345. $defaultModel = ContentHandler::getDefaultModelFor( $title );
  1346. $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
  1347. $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
  1348. $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
  1349. }
  1350. $dbw->insert( 'revision', $row, __METHOD__ );
  1351. if ( $this->mId === null ) {
  1352. // Only if auto-increment was used
  1353. $this->mId = $dbw->insertId();
  1354. }
  1355. $commentCallback( $this->mId );
  1356. // Assertion to try to catch T92046
  1357. if ( (int)$this->mId === 0 ) {
  1358. throw new UnexpectedValueException(
  1359. 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
  1360. var_export( $row, 1 )
  1361. );
  1362. }
  1363. // Insert IP revision into ip_changes for use when querying for a range.
  1364. if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
  1365. $ipcRow = [
  1366. 'ipc_rev_id' => $this->mId,
  1367. 'ipc_rev_timestamp' => $row['rev_timestamp'],
  1368. 'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
  1369. ];
  1370. $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
  1371. }
  1372. // Avoid PHP 7.1 warning of passing $this by reference
  1373. $revision = $this;
  1374. Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
  1375. return $this->mId;
  1376. }
  1377. protected function checkContentModel() {
  1378. global $wgContentHandlerUseDB;
  1379. // Note: may return null for revisions that have not yet been inserted
  1380. $title = $this->getTitle();
  1381. $model = $this->getContentModel();
  1382. $format = $this->getContentFormat();
  1383. $handler = $this->getContentHandler();
  1384. if ( !$handler->isSupportedFormat( $format ) ) {
  1385. $t = $title->getPrefixedDBkey();
  1386. throw new MWException( "Can't use format $format with content model $model on $t" );
  1387. }
  1388. if ( !$wgContentHandlerUseDB && $title ) {
  1389. // if $wgContentHandlerUseDB is not set,
  1390. // all revisions must use the default content model and format.
  1391. $defaultModel = ContentHandler::getDefaultModelFor( $title );
  1392. $defaultHandler = ContentHandler::getForModelID( $defaultModel );
  1393. $defaultFormat = $defaultHandler->getDefaultFormat();
  1394. if ( $this->getContentModel() != $defaultModel ) {
  1395. $t = $title->getPrefixedDBkey();
  1396. throw new MWException( "Can't save non-default content model with "
  1397. . "\$wgContentHandlerUseDB disabled: model is $model, "
  1398. . "default for $t is $defaultModel" );
  1399. }
  1400. if ( $this->getContentFormat() != $defaultFormat ) {
  1401. $t = $title->getPrefixedDBkey();
  1402. throw new MWException( "Can't use non-default content format with "
  1403. . "\$wgContentHandlerUseDB disabled: format is $format, "
  1404. . "default for $t is $defaultFormat" );
  1405. }
  1406. }
  1407. $content = $this->getContent( self::RAW );
  1408. $prefixedDBkey = $title->getPrefixedDBkey();
  1409. $revId = $this->mId;
  1410. if ( !$content ) {
  1411. throw new MWException(
  1412. "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
  1413. );
  1414. }
  1415. if ( !$content->isValid() ) {
  1416. throw new MWException(
  1417. "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
  1418. );
  1419. }
  1420. }
  1421. /**
  1422. * Get the base 36 SHA-1 value for a string of text
  1423. * @param string $text
  1424. * @return string
  1425. */
  1426. public static function base36Sha1( $text ) {
  1427. return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
  1428. }
  1429. /**
  1430. * Get the text cache TTL
  1431. *
  1432. * @param WANObjectCache $cache
  1433. * @return int
  1434. */
  1435. private static function getCacheTTL( WANObjectCache $cache ) {
  1436. global $wgRevisionCacheExpiry;
  1437. if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
  1438. // Do not cache RDBMs blobs in...the RDBMs store
  1439. $ttl = $cache::TTL_UNCACHEABLE;
  1440. } else {
  1441. $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
  1442. }
  1443. return $ttl;
  1444. }
  1445. /**
  1446. * Lazy-load the revision's text.
  1447. * Currently hardcoded to the 'text' table storage engine.
  1448. *
  1449. * @return string|bool The revision's text, or false on failure
  1450. */
  1451. private function loadText() {
  1452. $cache = ObjectCache::getMainWANInstance();
  1453. // No negative caching; negative hits on text rows may be due to corrupted replica DBs
  1454. return $cache->getWithSetCallback(
  1455. $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
  1456. self::getCacheTTL( $cache ),
  1457. function () {
  1458. return $this->fetchText();
  1459. },
  1460. [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
  1461. );
  1462. }
  1463. private function fetchText() {
  1464. $textId = $this->getTextId();
  1465. // If we kept data for lazy extraction, use it now...
  1466. if ( $this->mTextRow !== null ) {
  1467. $row = $this->mTextRow;
  1468. $this->mTextRow = null;
  1469. } else {
  1470. $row = null;
  1471. }
  1472. // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
  1473. // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
  1474. $flags = $this->mQueryFlags;
  1475. $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
  1476. ? self::READ_LATEST_IMMUTABLE
  1477. : 0;
  1478. list( $index, $options, $fallbackIndex, $fallbackOptions ) =
  1479. DBAccessObjectUtils::getDBOptions( $flags );
  1480. if ( !$row ) {
  1481. // Text data is immutable; check replica DBs first.
  1482. $row = wfGetDB( $index )->selectRow(
  1483. 'text',
  1484. [ 'old_text', 'old_flags' ],
  1485. [ 'old_id' => $textId ],
  1486. __METHOD__,
  1487. $options
  1488. );
  1489. }
  1490. // Fallback to DB_MASTER in some cases if the row was not found
  1491. if ( !$row && $fallbackIndex !== null ) {
  1492. // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
  1493. // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
  1494. $row = wfGetDB( $fallbackIndex )->selectRow(
  1495. 'text',
  1496. [ 'old_text', 'old_flags' ],
  1497. [ 'old_id' => $textId ],
  1498. __METHOD__,
  1499. $fallbackOptions
  1500. );
  1501. }
  1502. if ( !$row ) {
  1503. wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
  1504. }
  1505. $text = self::getRevisionText( $row );
  1506. if ( $row && $text === false ) {
  1507. wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
  1508. }
  1509. return is_string( $text ) ? $text : false;
  1510. }
  1511. /**
  1512. * Create a new null-revision for insertion into a page's
  1513. * history. This will not re-save the text, but simply refer
  1514. * to the text from the previous version.
  1515. *
  1516. * Such revisions can for instance identify page rename
  1517. * operations and other such meta-modifications.
  1518. *
  1519. * @param IDatabase $dbw
  1520. * @param int $pageId ID number of the page to read from
  1521. * @param string $summary Revision's summary
  1522. * @param bool $minor Whether the revision should be considered as minor
  1523. * @param User|null $user User object to use or null for $wgUser
  1524. * @return Revision|null Revision or null on error
  1525. */
  1526. public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
  1527. global $wgContentHandlerUseDB;
  1528. $fields = [ 'page_latest', 'page_namespace', 'page_title',
  1529. 'rev_text_id', 'rev_len', 'rev_sha1' ];
  1530. if ( $wgContentHandlerUseDB ) {
  1531. $fields[] = 'rev_content_model';
  1532. $fields[] = 'rev_content_format';
  1533. }
  1534. $current = $dbw->selectRow(
  1535. [ 'page', 'revision' ],
  1536. $fields,
  1537. [
  1538. 'page_id' => $pageId,
  1539. 'page_latest=rev_id',
  1540. ],
  1541. __METHOD__,
  1542. [ 'FOR UPDATE' ] // T51581
  1543. );
  1544. if ( $current ) {
  1545. if ( !$user ) {
  1546. global $wgUser;
  1547. $user = $wgUser;
  1548. }
  1549. $row = [
  1550. 'page' => $pageId,
  1551. 'user_text' => $user->getName(),
  1552. 'user' => $user->getId(),
  1553. 'comment' => $summary,
  1554. 'minor_edit' => $minor,
  1555. 'text_id' => $current->rev_text_id,
  1556. 'parent_id' => $current->page_latest,
  1557. 'len' => $current->rev_len,
  1558. 'sha1' => $current->rev_sha1
  1559. ];
  1560. if ( $wgContentHandlerUseDB ) {
  1561. $row['content_model'] = $current->rev_content_model;
  1562. $row['content_format'] = $current->rev_content_format;
  1563. }
  1564. $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
  1565. $revision = new Revision( $row );
  1566. } else {
  1567. $revision = null;
  1568. }
  1569. return $revision;
  1570. }
  1571. /**
  1572. * Determine if the current user is allowed to view a particular
  1573. * field of this revision, if it's marked as deleted.
  1574. *
  1575. * @param int $field One of self::DELETED_TEXT,
  1576. * self::DELETED_COMMENT,
  1577. * self::DELETED_USER
  1578. * @param User|null $user User object to check, or null to use $wgUser
  1579. * @return bool
  1580. */
  1581. public function userCan( $field, User $user = null ) {
  1582. return self::userCanBitfield( $this->getVisibility(), $field, $user );
  1583. }
  1584. /**
  1585. * Determine if the current user is allowed to view a particular
  1586. * field of this revision, if it's marked as deleted. This is used
  1587. * by various classes to avoid duplication.
  1588. *
  1589. * @param int $bitfield Current field
  1590. * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
  1591. * self::DELETED_COMMENT = File::DELETED_COMMENT,
  1592. * self::DELETED_USER = File::DELETED_USER
  1593. * @param User|null $user User object to check, or null to use $wgUser
  1594. * @param Title|null $title A Title object to check for per-page restrictions on,
  1595. * instead of just plain userrights
  1596. * @return bool
  1597. */
  1598. public static function userCanBitfield( $bitfield, $field, User $user = null,
  1599. Title $title = null
  1600. ) {
  1601. if ( $bitfield & $field ) { // aspect is deleted
  1602. if ( $user === null ) {
  1603. global $wgUser;
  1604. $user = $wgUser;
  1605. }
  1606. if ( $bitfield & self::DELETED_RESTRICTED ) {
  1607. $permissions = [ 'suppressrevision', 'viewsuppressed' ];
  1608. } elseif ( $field & self::DELETED_TEXT ) {
  1609. $permissions = [ 'deletedtext' ];
  1610. } else {
  1611. $permissions = [ 'deletedhistory' ];
  1612. }
  1613. $permissionlist = implode( ', ', $permissions );
  1614. if ( $title === null ) {
  1615. wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
  1616. return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
  1617. } else {
  1618. $text = $title->getPrefixedText();
  1619. wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
  1620. foreach ( $permissions as $perm ) {
  1621. if ( $title->userCan( $perm, $user ) ) {
  1622. return true;
  1623. }
  1624. }
  1625. return false;
  1626. }
  1627. } else {
  1628. return true;
  1629. }
  1630. }
  1631. /**
  1632. * Get rev_timestamp from rev_id, without loading the rest of the row
  1633. *
  1634. * @param Title $title
  1635. * @param int $id
  1636. * @param int $flags
  1637. * @return string|bool False if not found
  1638. */
  1639. static function getTimestampFromId( $title, $id, $flags = 0 ) {
  1640. $db = ( $flags & self::READ_LATEST )
  1641. ? wfGetDB( DB_MASTER )
  1642. : wfGetDB( DB_REPLICA );
  1643. // Casting fix for databases that can't take '' for rev_id
  1644. if ( $id == '' ) {
  1645. $id = 0;
  1646. }
  1647. $conds = [ 'rev_id' => $id ];
  1648. $conds['rev_page'] = $title->getArticleID();
  1649. $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
  1650. return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
  1651. }
  1652. /**
  1653. * Get count of revisions per page...not very efficient
  1654. *
  1655. * @param IDatabase $db
  1656. * @param int $id Page id
  1657. * @return int
  1658. */
  1659. static function countByPageId( $db, $id ) {
  1660. $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
  1661. [ 'rev_page' => $id ], __METHOD__ );
  1662. if ( $row ) {
  1663. return $row->revCount;
  1664. }
  1665. return 0;
  1666. }
  1667. /**
  1668. * Get count of revisions per page...not very efficient
  1669. *
  1670. * @param IDatabase $db
  1671. * @param Title $title
  1672. * @return int
  1673. */
  1674. static function countByTitle( $db, $title ) {
  1675. $id = $title->getArticleID();
  1676. if ( $id ) {
  1677. return self::countByPageId( $db, $id );
  1678. }
  1679. return 0;
  1680. }
  1681. /**
  1682. * Check if no edits were made by other users since
  1683. * the time a user started editing the page. Limit to
  1684. * 50 revisions for the sake of performance.
  1685. *
  1686. * @since 1.20
  1687. * @deprecated since 1.24
  1688. *
  1689. * @param IDatabase|int $db The Database to perform the check on. May be given as a
  1690. * Database object or a database identifier usable with wfGetDB.
  1691. * @param int $pageId The ID of the page in question
  1692. * @param int $userId The ID of the user in question
  1693. * @param string $since Look at edits since this time
  1694. *
  1695. * @return bool True if the given user was the only one to edit since the given timestamp
  1696. */
  1697. public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
  1698. if ( !$userId ) {
  1699. return false;
  1700. }
  1701. if ( is_int( $db ) ) {
  1702. $db = wfGetDB( $db );
  1703. }
  1704. $res = $db->select( 'revision',
  1705. 'rev_user',
  1706. [
  1707. 'rev_page' => $pageId,
  1708. 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
  1709. ],
  1710. __METHOD__,
  1711. [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
  1712. foreach ( $res as $row ) {
  1713. if ( $row->rev_user != $userId ) {
  1714. return false;
  1715. }
  1716. }
  1717. return true;
  1718. }
  1719. /**
  1720. * Load a revision based on a known page ID and current revision ID from the DB
  1721. *
  1722. * This method allows for the use of caching, though accessing anything that normally
  1723. * requires permission checks (aside from the text) will trigger a small DB lookup.
  1724. * The title will also be lazy loaded, though setTitle() can be used to preload it.
  1725. *
  1726. * @param IDatabase $db
  1727. * @param int $pageId Page ID
  1728. * @param int $revId Known current revision of this page
  1729. * @return Revision|bool Returns false if missing
  1730. * @since 1.28
  1731. */
  1732. public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
  1733. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  1734. return $cache->getWithSetCallback(
  1735. // Page/rev IDs passed in from DB to reflect history merges
  1736. $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
  1737. $cache::TTL_WEEK,
  1738. function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
  1739. $setOpts += Database::getCacheSetOptions( $db );
  1740. $rev = Revision::loadFromPageId( $db, $pageId, $revId );
  1741. // Reflect revision deletion and user renames
  1742. if ( $rev ) {
  1743. $rev->mTitle = null; // mutable; lazy-load
  1744. $rev->mRefreshMutableFields = true;
  1745. }
  1746. return $rev ?: false; // don't cache negatives
  1747. }
  1748. );
  1749. }
  1750. /**
  1751. * For cached revisions, make sure the user name and rev_deleted is up-to-date
  1752. */
  1753. private function loadMutableFields() {
  1754. if ( !$this->mRefreshMutableFields ) {
  1755. return; // not needed
  1756. }
  1757. $this->mRefreshMutableFields = false;
  1758. $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
  1759. $row = $dbr->selectRow(
  1760. [ 'revision', 'user' ],
  1761. [ 'rev_deleted', 'user_name' ],
  1762. [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
  1763. __METHOD__
  1764. );
  1765. if ( $row ) { // update values
  1766. $this->mDeleted = (int)$row->rev_deleted;
  1767. $this->mUserText = $row->user_name;
  1768. }
  1769. }
  1770. }