Revision.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  1. <?php
  2. /**
  3. * @todo document
  4. * @file
  5. */
  6. /**
  7. * @todo document
  8. */
  9. class Revision {
  10. const DELETED_TEXT = 1;
  11. const DELETED_COMMENT = 2;
  12. const DELETED_USER = 4;
  13. const DELETED_RESTRICTED = 8;
  14. // Audience options for Revision::getText()
  15. const FOR_PUBLIC = 1;
  16. const FOR_THIS_USER = 2;
  17. const RAW = 3;
  18. /**
  19. * Load a page revision from a given revision ID number.
  20. * Returns null if no such revision can be found.
  21. *
  22. * @param int $id
  23. * @access public
  24. * @static
  25. */
  26. public static function newFromId( $id ) {
  27. return Revision::newFromConds(
  28. array( 'page_id=rev_page',
  29. 'rev_id' => intval( $id ) ) );
  30. }
  31. /**
  32. * Load either the current, or a specified, revision
  33. * that's attached to a given title. If not attached
  34. * to that title, will return null.
  35. *
  36. * @param Title $title
  37. * @param int $id
  38. * @return Revision
  39. */
  40. public static function newFromTitle( $title, $id = 0 ) {
  41. $conds = array(
  42. 'page_namespace' => $title->getNamespace(),
  43. 'page_title' => $title->getDBkey()
  44. );
  45. if ( $id ) {
  46. // Use the specified ID
  47. $conds['rev_id'] = $id;
  48. } elseif ( wfGetLB()->getServerCount() > 1 ) {
  49. // Get the latest revision ID from the master
  50. $dbw = wfGetDB( DB_MASTER );
  51. $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
  52. if ( $latest === false ) {
  53. // Page does not exist
  54. return null;
  55. }
  56. $conds['rev_id'] = $latest;
  57. } else {
  58. // Use a join to get the latest revision
  59. $conds[] = 'rev_id=page_latest';
  60. }
  61. $conds[] = 'page_id=rev_page';
  62. return Revision::newFromConds( $conds );
  63. }
  64. /**
  65. * Load a page revision from a given revision ID number.
  66. * Returns null if no such revision can be found.
  67. *
  68. * @param Database $db
  69. * @param int $id
  70. * @access public
  71. * @static
  72. */
  73. public static function loadFromId( $db, $id ) {
  74. return Revision::loadFromConds( $db,
  75. array( 'page_id=rev_page',
  76. 'rev_id' => intval( $id ) ) );
  77. }
  78. /**
  79. * Load either the current, or a specified, revision
  80. * that's attached to a given page. If not attached
  81. * to that page, will return null.
  82. *
  83. * @param Database $db
  84. * @param int $pageid
  85. * @param int $id
  86. * @return Revision
  87. * @access public
  88. * @static
  89. */
  90. public static function loadFromPageId( $db, $pageid, $id = 0 ) {
  91. $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
  92. if( $id ) {
  93. $conds['rev_id']=intval($id);
  94. } else {
  95. $conds[]='rev_id=page_latest';
  96. }
  97. return Revision::loadFromConds( $db, $conds );
  98. }
  99. /**
  100. * Load either the current, or a specified, revision
  101. * that's attached to a given page. If not attached
  102. * to that page, will return null.
  103. *
  104. * @param Database $db
  105. * @param Title $title
  106. * @param int $id
  107. * @return Revision
  108. * @access public
  109. * @static
  110. */
  111. public static function loadFromTitle( $db, $title, $id = 0 ) {
  112. if( $id ) {
  113. $matchId = intval( $id );
  114. } else {
  115. $matchId = 'page_latest';
  116. }
  117. return Revision::loadFromConds(
  118. $db,
  119. array( "rev_id=$matchId",
  120. 'page_id=rev_page',
  121. 'page_namespace' => $title->getNamespace(),
  122. 'page_title' => $title->getDBkey() ) );
  123. }
  124. /**
  125. * Load the revision for the given title with the given timestamp.
  126. * WARNING: Timestamps may in some circumstances not be unique,
  127. * so this isn't the best key to use.
  128. *
  129. * @param Database $db
  130. * @param Title $title
  131. * @param string $timestamp
  132. * @return Revision
  133. * @access public
  134. * @static
  135. */
  136. public static function loadFromTimestamp( $db, $title, $timestamp ) {
  137. return Revision::loadFromConds(
  138. $db,
  139. array( 'rev_timestamp' => $db->timestamp( $timestamp ),
  140. 'page_id=rev_page',
  141. 'page_namespace' => $title->getNamespace(),
  142. 'page_title' => $title->getDBkey() ) );
  143. }
  144. /**
  145. * Given a set of conditions, fetch a revision.
  146. *
  147. * @param array $conditions
  148. * @return Revision
  149. * @access private
  150. * @static
  151. */
  152. private static function newFromConds( $conditions ) {
  153. $db = wfGetDB( DB_SLAVE );
  154. $row = Revision::loadFromConds( $db, $conditions );
  155. if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) {
  156. $dbw = wfGetDB( DB_MASTER );
  157. $row = Revision::loadFromConds( $dbw, $conditions );
  158. }
  159. return $row;
  160. }
  161. /**
  162. * Given a set of conditions, fetch a revision from
  163. * the given database connection.
  164. *
  165. * @param Database $db
  166. * @param array $conditions
  167. * @return Revision
  168. * @access private
  169. * @static
  170. */
  171. private static function loadFromConds( $db, $conditions ) {
  172. $res = Revision::fetchFromConds( $db, $conditions );
  173. if( $res ) {
  174. $row = $res->fetchObject();
  175. $res->free();
  176. if( $row ) {
  177. $ret = new Revision( $row );
  178. return $ret;
  179. }
  180. }
  181. $ret = null;
  182. return $ret;
  183. }
  184. /**
  185. * Return a wrapper for a series of database rows to
  186. * fetch all of a given page's revisions in turn.
  187. * Each row can be fed to the constructor to get objects.
  188. *
  189. * @param Title $title
  190. * @return ResultWrapper
  191. * @access public
  192. * @static
  193. */
  194. public static function fetchAllRevisions( $title ) {
  195. return Revision::fetchFromConds(
  196. wfGetDB( DB_SLAVE ),
  197. array( 'page_namespace' => $title->getNamespace(),
  198. 'page_title' => $title->getDBkey(),
  199. 'page_id=rev_page' ) );
  200. }
  201. /**
  202. * Return a wrapper for a series of database rows to
  203. * fetch all of a given page's revisions in turn.
  204. * Each row can be fed to the constructor to get objects.
  205. *
  206. * @param Title $title
  207. * @return ResultWrapper
  208. * @access public
  209. * @static
  210. */
  211. public static function fetchRevision( $title ) {
  212. return Revision::fetchFromConds(
  213. wfGetDB( DB_SLAVE ),
  214. array( 'rev_id=page_latest',
  215. 'page_namespace' => $title->getNamespace(),
  216. 'page_title' => $title->getDBkey(),
  217. 'page_id=rev_page' ) );
  218. }
  219. /**
  220. * Given a set of conditions, return a ResultWrapper
  221. * which will return matching database rows with the
  222. * fields necessary to build Revision objects.
  223. *
  224. * @param Database $db
  225. * @param array $conditions
  226. * @return ResultWrapper
  227. * @access private
  228. * @static
  229. */
  230. private static function fetchFromConds( $db, $conditions ) {
  231. $fields = self::selectFields();
  232. $fields[] = 'page_namespace';
  233. $fields[] = 'page_title';
  234. $fields[] = 'page_latest';
  235. $res = $db->select(
  236. array( 'page', 'revision' ),
  237. $fields,
  238. $conditions,
  239. __METHOD__,
  240. array( 'LIMIT' => 1 ) );
  241. $ret = $db->resultObject( $res );
  242. return $ret;
  243. }
  244. /**
  245. * Return the list of revision fields that should be selected to create
  246. * a new revision.
  247. */
  248. static function selectFields() {
  249. return array(
  250. 'rev_id',
  251. 'rev_page',
  252. 'rev_text_id',
  253. 'rev_timestamp',
  254. 'rev_comment',
  255. 'rev_user_text,'.
  256. 'rev_user',
  257. 'rev_minor_edit',
  258. 'rev_deleted',
  259. 'rev_len',
  260. 'rev_parent_id'
  261. );
  262. }
  263. /**
  264. * Return the list of text fields that should be selected to read the
  265. * revision text
  266. */
  267. static function selectTextFields() {
  268. return array(
  269. 'old_text',
  270. 'old_flags'
  271. );
  272. }
  273. /**
  274. * Return the list of page fields that should be selected from page table
  275. */
  276. static function selectPageFields() {
  277. return array(
  278. 'page_namespace',
  279. 'page_title',
  280. 'page_latest'
  281. );
  282. }
  283. /**
  284. * @param object $row
  285. * @access private
  286. */
  287. function Revision( $row ) {
  288. if( is_object( $row ) ) {
  289. $this->mId = intval( $row->rev_id );
  290. $this->mPage = intval( $row->rev_page );
  291. $this->mTextId = intval( $row->rev_text_id );
  292. $this->mComment = $row->rev_comment;
  293. $this->mUserText = $row->rev_user_text;
  294. $this->mUser = intval( $row->rev_user );
  295. $this->mMinorEdit = intval( $row->rev_minor_edit );
  296. $this->mTimestamp = $row->rev_timestamp;
  297. $this->mDeleted = intval( $row->rev_deleted );
  298. if( !isset( $row->rev_parent_id ) )
  299. $this->mParentId = is_null($row->rev_parent_id) ? null : 0;
  300. else
  301. $this->mParentId = intval( $row->rev_parent_id );
  302. if( !isset( $row->rev_len ) || is_null( $row->rev_len ) )
  303. $this->mSize = null;
  304. else
  305. $this->mSize = intval( $row->rev_len );
  306. if( isset( $row->page_latest ) ) {
  307. $this->mCurrent = ( $row->rev_id == $row->page_latest );
  308. $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
  309. $this->mTitle->resetArticleID( $this->mPage );
  310. } else {
  311. $this->mCurrent = false;
  312. $this->mTitle = null;
  313. }
  314. // Lazy extraction...
  315. $this->mText = null;
  316. if( isset( $row->old_text ) ) {
  317. $this->mTextRow = $row;
  318. } else {
  319. // 'text' table row entry will be lazy-loaded
  320. $this->mTextRow = null;
  321. }
  322. } elseif( is_array( $row ) ) {
  323. // Build a new revision to be saved...
  324. global $wgUser;
  325. $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
  326. $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
  327. $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
  328. $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName();
  329. $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
  330. $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
  331. $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
  332. $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
  333. $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
  334. $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
  335. // Enforce spacing trimming on supplied text
  336. $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
  337. $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
  338. $this->mTextRow = null;
  339. $this->mTitle = null; # Load on demand if needed
  340. $this->mCurrent = false;
  341. # If we still have no len_size, see it we have the text to figure it out
  342. if ( !$this->mSize )
  343. $this->mSize = is_null($this->mText) ? null : strlen($this->mText);
  344. } else {
  345. throw new MWException( 'Revision constructor passed invalid row format.' );
  346. }
  347. $this->mUnpatrolled = NULL;
  348. }
  349. /**#@+
  350. * @access public
  351. */
  352. /**
  353. * Get revision ID
  354. * @return int
  355. */
  356. public function getId() {
  357. return $this->mId;
  358. }
  359. /**
  360. * Get text row ID
  361. * @return int
  362. */
  363. public function getTextId() {
  364. return $this->mTextId;
  365. }
  366. /**
  367. * Get parent revision ID (the original previous page revision)
  368. * @return int
  369. */
  370. public function getParentId() {
  371. return $this->mParentId;
  372. }
  373. /**
  374. * Returns the length of the text in this revision, or null if unknown.
  375. * @return int
  376. */
  377. public function getSize() {
  378. return $this->mSize;
  379. }
  380. /**
  381. * Returns the title of the page associated with this entry.
  382. * @return Title
  383. */
  384. public function getTitle() {
  385. if( isset( $this->mTitle ) ) {
  386. return $this->mTitle;
  387. }
  388. $dbr = wfGetDB( DB_SLAVE );
  389. $row = $dbr->selectRow(
  390. array( 'page', 'revision' ),
  391. array( 'page_namespace', 'page_title' ),
  392. array( 'page_id=rev_page',
  393. 'rev_id' => $this->mId ),
  394. 'Revision::getTitle' );
  395. if( $row ) {
  396. $this->mTitle = Title::makeTitle( $row->page_namespace,
  397. $row->page_title );
  398. }
  399. return $this->mTitle;
  400. }
  401. /**
  402. * Set the title of the revision
  403. * @param Title $title
  404. */
  405. public function setTitle( $title ) {
  406. $this->mTitle = $title;
  407. }
  408. /**
  409. * Get the page ID
  410. * @return int
  411. */
  412. public function getPage() {
  413. return $this->mPage;
  414. }
  415. /**
  416. * Fetch revision's user id if it's available to the specified audience.
  417. * If the specified audience does not have access to it, zero will be
  418. * returned.
  419. *
  420. * @param integer $audience One of:
  421. * Revision::FOR_PUBLIC to be displayed to all users
  422. * Revision::FOR_THIS_USER to be displayed to $wgUser
  423. * Revision::RAW get the ID regardless of permissions
  424. *
  425. *
  426. * @return int
  427. */
  428. public function getUser( $audience = self::FOR_PUBLIC ) {
  429. if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
  430. return 0;
  431. } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) {
  432. return 0;
  433. } else {
  434. return $this->mUser;
  435. }
  436. }
  437. /**
  438. * Fetch revision's user id without regard for the current user's permissions
  439. * @return string
  440. */
  441. public function getRawUser() {
  442. return $this->mUser;
  443. }
  444. /**
  445. * Fetch revision's username if it's available to the specified audience.
  446. * If the specified audience does not have access to the username, an
  447. * empty string will be returned.
  448. *
  449. * @param integer $audience One of:
  450. * Revision::FOR_PUBLIC to be displayed to all users
  451. * Revision::FOR_THIS_USER to be displayed to $wgUser
  452. * Revision::RAW get the text regardless of permissions
  453. *
  454. * @return string
  455. */
  456. public function getUserText( $audience = self::FOR_PUBLIC ) {
  457. if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
  458. return "";
  459. } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) {
  460. return "";
  461. } else {
  462. return $this->mUserText;
  463. }
  464. }
  465. /**
  466. * Fetch revision's username without regard for view restrictions
  467. * @return string
  468. */
  469. public function getRawUserText() {
  470. return $this->mUserText;
  471. }
  472. /**
  473. * Fetch revision comment if it's available to the specified audience.
  474. * If the specified audience does not have access to the comment, an
  475. * empty string will be returned.
  476. *
  477. * @param integer $audience One of:
  478. * Revision::FOR_PUBLIC to be displayed to all users
  479. * Revision::FOR_THIS_USER to be displayed to $wgUser
  480. * Revision::RAW get the text regardless of permissions
  481. *
  482. * @return string
  483. */
  484. function getComment( $audience = self::FOR_PUBLIC ) {
  485. if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
  486. return "";
  487. } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT ) ) {
  488. return "";
  489. } else {
  490. return $this->mComment;
  491. }
  492. }
  493. /**
  494. * Fetch revision comment without regard for the current user's permissions
  495. * @return string
  496. */
  497. public function getRawComment() {
  498. return $this->mComment;
  499. }
  500. /**
  501. * @return bool
  502. */
  503. public function isMinor() {
  504. return (bool)$this->mMinorEdit;
  505. }
  506. /**
  507. * @return int rcid of the unpatrolled row, zero if there isn't one
  508. */
  509. public function isUnpatrolled() {
  510. if( $this->mUnpatrolled !== NULL ) {
  511. return $this->mUnpatrolled;
  512. }
  513. $dbr = wfGetDB( DB_SLAVE );
  514. $this->mUnpatrolled = $dbr->selectField( 'recentchanges',
  515. 'rc_id',
  516. array( // Add redundant user,timestamp condition so we can use the existing index
  517. 'rc_user_text' => $this->getRawUserText(),
  518. 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
  519. 'rc_this_oldid' => $this->getId(),
  520. 'rc_patrolled' => 0
  521. ),
  522. __METHOD__
  523. );
  524. return (int)$this->mUnpatrolled;
  525. }
  526. /**
  527. * int $field one of DELETED_* bitfield constants
  528. * @return bool
  529. */
  530. public function isDeleted( $field ) {
  531. return ($this->mDeleted & $field) == $field;
  532. }
  533. /**
  534. * Get the deletion bitfield of the revision
  535. */
  536. public function getVisibility() {
  537. return (int)$this->mDeleted;
  538. }
  539. /**
  540. * Fetch revision text if it's available to the specified audience.
  541. * If the specified audience does not have the ability to view this
  542. * revision, an empty string will be returned.
  543. *
  544. * @param integer $audience One of:
  545. * Revision::FOR_PUBLIC to be displayed to all users
  546. * Revision::FOR_THIS_USER to be displayed to $wgUser
  547. * Revision::RAW get the text regardless of permissions
  548. *
  549. *
  550. * @return string
  551. */
  552. public function getText( $audience = self::FOR_PUBLIC ) {
  553. if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
  554. return "";
  555. } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT ) ) {
  556. return "";
  557. } else {
  558. return $this->getRawText();
  559. }
  560. }
  561. /**
  562. * Alias for getText(Revision::FOR_THIS_USER)
  563. */
  564. public function revText() {
  565. return $this->getText( self::FOR_THIS_USER );
  566. }
  567. /**
  568. * Fetch revision text without regard for view restrictions
  569. * @return string
  570. */
  571. public function getRawText() {
  572. if( is_null( $this->mText ) ) {
  573. // Revision text is immutable. Load on demand:
  574. $this->mText = $this->loadText();
  575. }
  576. return $this->mText;
  577. }
  578. /**
  579. * @return string
  580. */
  581. public function getTimestamp() {
  582. return wfTimestamp(TS_MW, $this->mTimestamp);
  583. }
  584. /**
  585. * @return bool
  586. */
  587. public function isCurrent() {
  588. return $this->mCurrent;
  589. }
  590. /**
  591. * Get previous revision for this title
  592. * @return Revision
  593. */
  594. public function getPrevious() {
  595. if( $this->getTitle() ) {
  596. $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
  597. if( $prev ) {
  598. return Revision::newFromTitle( $this->getTitle(), $prev );
  599. }
  600. }
  601. return null;
  602. }
  603. /**
  604. * @return Revision
  605. */
  606. public function getNext() {
  607. if( $this->getTitle() ) {
  608. $next = $this->getTitle()->getNextRevisionID( $this->getId() );
  609. if ( $next ) {
  610. return Revision::newFromTitle( $this->getTitle(), $next );
  611. }
  612. }
  613. return null;
  614. }
  615. /**
  616. * Get previous revision Id for this page_id
  617. * This is used to populate rev_parent_id on save
  618. * @param Database $db
  619. * @return int
  620. */
  621. private function getPreviousRevisionId( $db ) {
  622. if( is_null($this->mPage) ) {
  623. return 0;
  624. }
  625. # Use page_latest if ID is not given
  626. if( !$this->mId ) {
  627. $prevId = $db->selectField( 'page', 'page_latest',
  628. array( 'page_id' => $this->mPage ),
  629. __METHOD__ );
  630. } else {
  631. $prevId = $db->selectField( 'revision', 'rev_id',
  632. array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
  633. __METHOD__,
  634. array( 'ORDER BY' => 'rev_id DESC' ) );
  635. }
  636. return intval($prevId);
  637. }
  638. /**
  639. * Get revision text associated with an old or archive row
  640. * $row is usually an object from wfFetchRow(), both the flags and the text
  641. * field must be included
  642. *
  643. * @param object $row The text data
  644. * @param string $prefix table prefix (default 'old_')
  645. * @return string $text|false the text requested
  646. */
  647. public static function getRevisionText( $row, $prefix = 'old_' ) {
  648. wfProfileIn( __METHOD__ );
  649. # Get data
  650. $textField = $prefix . 'text';
  651. $flagsField = $prefix . 'flags';
  652. if( isset( $row->$flagsField ) ) {
  653. $flags = explode( ',', $row->$flagsField );
  654. } else {
  655. $flags = array();
  656. }
  657. if( isset( $row->$textField ) ) {
  658. $text = $row->$textField;
  659. } else {
  660. wfProfileOut( __METHOD__ );
  661. return false;
  662. }
  663. # Use external methods for external objects, text in table is URL-only then
  664. if ( in_array( 'external', $flags ) ) {
  665. $url=$text;
  666. @list(/* $proto */,$path)=explode('://',$url,2);
  667. if ($path=="") {
  668. wfProfileOut( __METHOD__ );
  669. return false;
  670. }
  671. $text=ExternalStore::fetchFromURL($url);
  672. }
  673. // If the text was fetched without an error, convert it
  674. if ( $text !== false ) {
  675. if( in_array( 'gzip', $flags ) ) {
  676. # Deal with optional compression of archived pages.
  677. # This can be done periodically via maintenance/compressOld.php, and
  678. # as pages are saved if $wgCompressRevisions is set.
  679. $text = gzinflate( $text );
  680. }
  681. if( in_array( 'object', $flags ) ) {
  682. # Generic compressed storage
  683. $obj = unserialize( $text );
  684. if ( !is_object( $obj ) ) {
  685. // Invalid object
  686. wfProfileOut( __METHOD__ );
  687. return false;
  688. }
  689. $text = $obj->getText();
  690. }
  691. global $wgLegacyEncoding;
  692. if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) {
  693. # Old revisions kept around in a legacy encoding?
  694. # Upconvert on demand.
  695. # ("utf8" checked for compatibility with some broken
  696. # conversion scripts 2008-12-30)
  697. global $wgInputEncoding, $wgContLang;
  698. $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text );
  699. }
  700. }
  701. wfProfileOut( __METHOD__ );
  702. return $text;
  703. }
  704. /**
  705. * If $wgCompressRevisions is enabled, we will compress data.
  706. * The input string is modified in place.
  707. * Return value is the flags field: contains 'gzip' if the
  708. * data is compressed, and 'utf-8' if we're saving in UTF-8
  709. * mode.
  710. *
  711. * @param mixed $text reference to a text
  712. * @return string
  713. */
  714. public static function compressRevisionText( &$text ) {
  715. global $wgCompressRevisions;
  716. $flags = array();
  717. # Revisions not marked this way will be converted
  718. # on load if $wgLegacyCharset is set in the future.
  719. $flags[] = 'utf-8';
  720. if( $wgCompressRevisions ) {
  721. if( function_exists( 'gzdeflate' ) ) {
  722. $text = gzdeflate( $text );
  723. $flags[] = 'gzip';
  724. } else {
  725. wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
  726. }
  727. }
  728. return implode( ',', $flags );
  729. }
  730. /**
  731. * Insert a new revision into the database, returning the new revision ID
  732. * number on success and dies horribly on failure.
  733. *
  734. * @param Database $dbw
  735. * @return int
  736. */
  737. public function insertOn( $dbw ) {
  738. global $wgDefaultExternalStore;
  739. wfProfileIn( __METHOD__ );
  740. $data = $this->mText;
  741. $flags = Revision::compressRevisionText( $data );
  742. # Write to external storage if required
  743. if( $wgDefaultExternalStore ) {
  744. // Store and get the URL
  745. $data = ExternalStore::insertToDefault( $data );
  746. if( !$data ) {
  747. throw new MWException( "Unable to store text to external storage" );
  748. }
  749. if( $flags ) {
  750. $flags .= ',';
  751. }
  752. $flags .= 'external';
  753. }
  754. # Record the text (or external storage URL) to the text table
  755. if( !isset( $this->mTextId ) ) {
  756. $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
  757. $dbw->insert( 'text',
  758. array(
  759. 'old_id' => $old_id,
  760. 'old_text' => $data,
  761. 'old_flags' => $flags,
  762. ), __METHOD__
  763. );
  764. $this->mTextId = $dbw->insertId();
  765. }
  766. # Record the edit in revisions
  767. $rev_id = isset( $this->mId )
  768. ? $this->mId
  769. : $dbw->nextSequenceValue( 'rev_rev_id_val' );
  770. $dbw->insert( 'revision',
  771. array(
  772. 'rev_id' => $rev_id,
  773. 'rev_page' => $this->mPage,
  774. 'rev_text_id' => $this->mTextId,
  775. 'rev_comment' => $this->mComment,
  776. 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
  777. 'rev_user' => $this->mUser,
  778. 'rev_user_text' => $this->mUserText,
  779. 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
  780. 'rev_deleted' => $this->mDeleted,
  781. 'rev_len' => $this->mSize,
  782. 'rev_parent_id' => is_null($this->mParentId) ?
  783. $this->getPreviousRevisionId( $dbw ) : $this->mParentId
  784. ), __METHOD__
  785. );
  786. $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
  787. wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
  788. wfProfileOut( __METHOD__ );
  789. return $this->mId;
  790. }
  791. /**
  792. * Lazy-load the revision's text.
  793. * Currently hardcoded to the 'text' table storage engine.
  794. *
  795. * @return string
  796. */
  797. private function loadText() {
  798. wfProfileIn( __METHOD__ );
  799. // Caching may be beneficial for massive use of external storage
  800. global $wgRevisionCacheExpiry, $wgMemc;
  801. $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
  802. if( $wgRevisionCacheExpiry ) {
  803. $text = $wgMemc->get( $key );
  804. if( is_string( $text ) ) {
  805. wfProfileOut( __METHOD__ );
  806. return $text;
  807. }
  808. }
  809. // If we kept data for lazy extraction, use it now...
  810. if ( isset( $this->mTextRow ) ) {
  811. $row = $this->mTextRow;
  812. $this->mTextRow = null;
  813. } else {
  814. $row = null;
  815. }
  816. if( !$row ) {
  817. // Text data is immutable; check slaves first.
  818. $dbr = wfGetDB( DB_SLAVE );
  819. $row = $dbr->selectRow( 'text',
  820. array( 'old_text', 'old_flags' ),
  821. array( 'old_id' => $this->getTextId() ),
  822. __METHOD__ );
  823. }
  824. if( !$row && wfGetLB()->getServerCount() > 1 ) {
  825. // Possible slave lag!
  826. $dbw = wfGetDB( DB_MASTER );
  827. $row = $dbw->selectRow( 'text',
  828. array( 'old_text', 'old_flags' ),
  829. array( 'old_id' => $this->getTextId() ),
  830. __METHOD__ );
  831. }
  832. $text = self::getRevisionText( $row );
  833. # No negative caching -- negative hits on text rows may be due to corrupted slave servers
  834. if( $wgRevisionCacheExpiry && $text !== false ) {
  835. $wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
  836. }
  837. wfProfileOut( __METHOD__ );
  838. return $text;
  839. }
  840. /**
  841. * Create a new null-revision for insertion into a page's
  842. * history. This will not re-save the text, but simply refer
  843. * to the text from the previous version.
  844. *
  845. * Such revisions can for instance identify page rename
  846. * operations and other such meta-modifications.
  847. *
  848. * @param Database $dbw
  849. * @param int $pageId ID number of the page to read from
  850. * @param string $summary
  851. * @param bool $minor
  852. * @return Revision
  853. */
  854. public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
  855. wfProfileIn( __METHOD__ );
  856. $current = $dbw->selectRow(
  857. array( 'page', 'revision' ),
  858. array( 'page_latest', 'rev_text_id', 'rev_len' ),
  859. array(
  860. 'page_id' => $pageId,
  861. 'page_latest=rev_id',
  862. ),
  863. __METHOD__ );
  864. if( $current ) {
  865. $revision = new Revision( array(
  866. 'page' => $pageId,
  867. 'comment' => $summary,
  868. 'minor_edit' => $minor,
  869. 'text_id' => $current->rev_text_id,
  870. 'parent_id' => $current->page_latest,
  871. 'len' => $current->rev_len
  872. ) );
  873. } else {
  874. $revision = null;
  875. }
  876. wfProfileOut( __METHOD__ );
  877. return $revision;
  878. }
  879. /**
  880. * Determine if the current user is allowed to view a particular
  881. * field of this revision, if it's marked as deleted.
  882. * @param int $field one of self::DELETED_TEXT,
  883. * self::DELETED_COMMENT,
  884. * self::DELETED_USER
  885. * @return bool
  886. */
  887. public function userCan( $field ) {
  888. if( ( $this->mDeleted & $field ) == $field ) {
  889. global $wgUser;
  890. $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
  891. ? 'suppressrevision'
  892. : 'deleterevision';
  893. wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
  894. return $wgUser->isAllowed( $permission );
  895. } else {
  896. return true;
  897. }
  898. }
  899. /**
  900. * Get rev_timestamp from rev_id, without loading the rest of the row
  901. * @param Title $title
  902. * @param integer $id
  903. */
  904. static function getTimestampFromId( $title, $id ) {
  905. $dbr = wfGetDB( DB_SLAVE );
  906. // Casting fix for DB2
  907. if ($id == '') {
  908. $id = 0;
  909. }
  910. $conds = array( 'rev_id' => $id );
  911. $conds['rev_page'] = $title->getArticleId();
  912. $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
  913. if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) {
  914. # Not in slave, try master
  915. $dbw = wfGetDB( DB_MASTER );
  916. $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
  917. }
  918. return wfTimestamp( TS_MW, $timestamp );
  919. }
  920. /**
  921. * Get count of revisions per page...not very efficient
  922. * @param Database $db
  923. * @param int $id, page id
  924. */
  925. static function countByPageId( $db, $id ) {
  926. $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
  927. array( 'rev_page' => $id ), __METHOD__ );
  928. if( $row ) {
  929. return $row->revCount;
  930. }
  931. return 0;
  932. }
  933. /**
  934. * Get count of revisions per page...not very efficient
  935. * @param Database $db
  936. * @param Title $title
  937. */
  938. static function countByTitle( $db, $title ) {
  939. $id = $title->getArticleId();
  940. if( $id ) {
  941. return Revision::countByPageId( $db, $id );
  942. }
  943. return 0;
  944. }
  945. }
  946. /**
  947. * Aliases for backwards compatibility with 1.6
  948. */
  949. define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
  950. define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
  951. define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
  952. define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );