RevisionRecord.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. <?php
  2. /**
  3. * Page revision base class.
  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. namespace MediaWiki\Storage;
  23. use CommentStoreComment;
  24. use Content;
  25. use InvalidArgumentException;
  26. use LogicException;
  27. use MediaWiki\Linker\LinkTarget;
  28. use MediaWiki\User\UserIdentity;
  29. use MWException;
  30. use Title;
  31. use User;
  32. use Wikimedia\Assert\Assert;
  33. /**
  34. * Page revision base class.
  35. *
  36. * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
  37. * Note that while the base class has no setters, subclasses may offer a mutable interface.
  38. *
  39. * @since 1.31
  40. */
  41. abstract class RevisionRecord {
  42. // RevisionRecord deletion constants
  43. const DELETED_TEXT = 1;
  44. const DELETED_COMMENT = 2;
  45. const DELETED_USER = 4;
  46. const DELETED_RESTRICTED = 8;
  47. const SUPPRESSED_USER = 12; // convenience
  48. const SUPPRESSED_ALL = 15; // convenience
  49. // Audience options for accessors
  50. const FOR_PUBLIC = 1;
  51. const FOR_THIS_USER = 2;
  52. const RAW = 3;
  53. /** @var string Wiki ID; false means the current wiki */
  54. protected $mWiki = false;
  55. /** @var int|null */
  56. protected $mId;
  57. /** @var int|null */
  58. protected $mPageId;
  59. /** @var UserIdentity|null */
  60. protected $mUser;
  61. /** @var bool */
  62. protected $mMinorEdit = false;
  63. /** @var string|null */
  64. protected $mTimestamp;
  65. /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
  66. protected $mDeleted = 0;
  67. /** @var int|null */
  68. protected $mSize;
  69. /** @var string|null */
  70. protected $mSha1;
  71. /** @var int|null */
  72. protected $mParentId;
  73. /** @var CommentStoreComment|null */
  74. protected $mComment;
  75. /** @var Title */
  76. protected $mTitle; // TODO: we only need the title for permission checks!
  77. /** @var RevisionSlots */
  78. protected $mSlots;
  79. /**
  80. * @note Avoid calling this constructor directly. Use the appropriate methods
  81. * in RevisionStore instead.
  82. *
  83. * @param Title $title The title of the page this Revision is associated with.
  84. * @param RevisionSlots $slots The slots of this revision.
  85. * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
  86. * or false for the local site.
  87. *
  88. * @throws MWException
  89. */
  90. function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
  91. Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
  92. $this->mTitle = $title;
  93. $this->mSlots = $slots;
  94. $this->mWiki = $wikiId;
  95. // XXX: this is a sensible default, but we may not have a Title object here in the future.
  96. $this->mPageId = $title->getArticleID();
  97. }
  98. /**
  99. * Implemented to defy serialization.
  100. *
  101. * @throws LogicException always
  102. */
  103. public function __sleep() {
  104. throw new LogicException( __CLASS__ . ' is not serializable.' );
  105. }
  106. /**
  107. * @param RevisionRecord $rec
  108. *
  109. * @return bool True if this RevisionRecord is known to have same content as $rec.
  110. * False if the content is different (or not known to be the same).
  111. */
  112. public function hasSameContent( RevisionRecord $rec ) {
  113. if ( $rec === $this ) {
  114. return true;
  115. }
  116. if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
  117. return true;
  118. }
  119. // check size before hash, since size is quicker to compute
  120. if ( $this->getSize() !== $rec->getSize() ) {
  121. return false;
  122. }
  123. // instead of checking the hash, we could also check the content addresses of all slots.
  124. if ( $this->getSha1() === $rec->getSha1() ) {
  125. return true;
  126. }
  127. return false;
  128. }
  129. /**
  130. * Returns the Content of the given slot of this revision.
  131. * Call getSlotNames() to get a list of available slots.
  132. *
  133. * Note that for mutable Content objects, each call to this method will return a
  134. * fresh clone.
  135. *
  136. * MCR migration note: this replaces Revision::getContent
  137. *
  138. * @param string $role The role name of the desired slot
  139. * @param int $audience
  140. * @param User|null $user
  141. *
  142. * @throws RevisionAccessException if the slot does not exist or slot data
  143. * could not be lazy-loaded.
  144. * @return Content|null The content of the given slot, or null if access is forbidden.
  145. */
  146. public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
  147. // XXX: throwing an exception would be nicer, but would a further
  148. // departure from the signature of Revision::getContent(), and thus
  149. // more complex and error prone refactoring.
  150. if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
  151. return null;
  152. }
  153. $content = $this->getSlot( $role, $audience, $user )->getContent();
  154. return $content->copy();
  155. }
  156. /**
  157. * Returns meta-data for the given slot.
  158. *
  159. * @param string $role The role name of the desired slot
  160. * @param int $audience
  161. * @param User|null $user
  162. *
  163. * @throws RevisionAccessException if the slot does not exist or slot data
  164. * could not be lazy-loaded.
  165. * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
  166. * calling getContent() on the SlotRecord will throw an exception.
  167. */
  168. public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
  169. $slot = $this->mSlots->getSlot( $role );
  170. if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
  171. return SlotRecord::newWithSuppressedContent( $slot );
  172. }
  173. return $slot;
  174. }
  175. /**
  176. * Returns whether the given slot is defined in this revision.
  177. *
  178. * @param string $role The role name of the desired slot
  179. *
  180. * @return bool
  181. */
  182. public function hasSlot( $role ) {
  183. return $this->mSlots->hasSlot( $role );
  184. }
  185. /**
  186. * Returns the slot names (roles) of all slots present in this revision.
  187. * getContent() will succeed only for the names returned by this method.
  188. *
  189. * @return string[]
  190. */
  191. public function getSlotRoles() {
  192. return $this->mSlots->getSlotRoles();
  193. }
  194. /**
  195. * Get revision ID. Depending on the concrete subclass, this may return null if
  196. * the revision ID is not known (e.g. because the revision does not yet exist
  197. * in the database).
  198. *
  199. * MCR migration note: this replaces Revision::getId
  200. *
  201. * @return int|null
  202. */
  203. public function getId() {
  204. return $this->mId;
  205. }
  206. /**
  207. * Get parent revision ID (the original previous page revision).
  208. * If there is no parent revision, this returns 0.
  209. * If the parent revision is undefined or unknown, this returns null.
  210. *
  211. * @note As of MW 1.31, the database schema allows the parent ID to be
  212. * NULL to indicate that it is unknown.
  213. *
  214. * MCR migration note: this replaces Revision::getParentId
  215. *
  216. * @return int|null
  217. */
  218. public function getParentId() {
  219. return $this->mParentId;
  220. }
  221. /**
  222. * Returns the nominal size of this revision, in bogo-bytes.
  223. * May be calculated on the fly if not known, which may in the worst
  224. * case may involve loading all content.
  225. *
  226. * MCR migration note: this replaces Revision::getSize
  227. *
  228. * @throws RevisionAccessException if the size was unknown and could not be calculated.
  229. * @return int
  230. */
  231. abstract public function getSize();
  232. /**
  233. * Returns the base36 sha1 of this revision. This hash is derived from the
  234. * hashes of all slots associated with the revision.
  235. * May be calculated on the fly if not known, which may in the worst
  236. * case may involve loading all content.
  237. *
  238. * MCR migration note: this replaces Revision::getSha1
  239. *
  240. * @throws RevisionAccessException if the hash was unknown and could not be calculated.
  241. * @return string
  242. */
  243. abstract public function getSha1();
  244. /**
  245. * Get the page ID. If the page does not yet exist, the page ID is 0.
  246. *
  247. * MCR migration note: this replaces Revision::getPage
  248. *
  249. * @return int
  250. */
  251. public function getPageId() {
  252. return $this->mPageId;
  253. }
  254. /**
  255. * Get the ID of the wiki this revision belongs to.
  256. *
  257. * @return string|false The wiki's logical name, of false to indicate the local wiki.
  258. */
  259. public function getWikiId() {
  260. return $this->mWiki;
  261. }
  262. /**
  263. * Returns the title of the page this revision is associated with as a LinkTarget object.
  264. *
  265. * MCR migration note: this replaces Revision::getTitle
  266. *
  267. * @return LinkTarget
  268. */
  269. public function getPageAsLinkTarget() {
  270. return $this->mTitle;
  271. }
  272. /**
  273. * Fetch revision's author's user identity, if it's available to the specified audience.
  274. * If the specified audience does not have access to it, null will be
  275. * returned. Depending on the concrete subclass, null may also be returned if the user is
  276. * not yet specified.
  277. *
  278. * MCR migration note: this replaces Revision::getUser
  279. *
  280. * @param int $audience One of:
  281. * RevisionRecord::FOR_PUBLIC to be displayed to all users
  282. * RevisionRecord::FOR_THIS_USER to be displayed to the given user
  283. * RevisionRecord::RAW get the ID regardless of permissions
  284. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  285. * to the $audience parameter
  286. * @return UserIdentity|null
  287. */
  288. public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
  289. if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
  290. return null;
  291. } else {
  292. return $this->mUser;
  293. }
  294. }
  295. /**
  296. * Fetch revision comment, if it's available to the specified audience.
  297. * If the specified audience does not have access to the comment,
  298. * this will return null. Depending on the concrete subclass, null may also be returned
  299. * if the comment is not yet specified.
  300. *
  301. * MCR migration note: this replaces Revision::getComment
  302. *
  303. * @param int $audience One of:
  304. * RevisionRecord::FOR_PUBLIC to be displayed to all users
  305. * RevisionRecord::FOR_THIS_USER to be displayed to the given user
  306. * RevisionRecord::RAW get the text regardless of permissions
  307. * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
  308. * to the $audience parameter
  309. *
  310. * @return CommentStoreComment|null
  311. */
  312. public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
  313. if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
  314. return null;
  315. } else {
  316. return $this->mComment;
  317. }
  318. }
  319. /**
  320. * MCR migration note: this replaces Revision::isMinor
  321. *
  322. * @return bool
  323. */
  324. public function isMinor() {
  325. return (bool)$this->mMinorEdit;
  326. }
  327. /**
  328. * MCR migration note: this replaces Revision::isDeleted
  329. *
  330. * @param int $field One of DELETED_* bitfield constants
  331. *
  332. * @return bool
  333. */
  334. public function isDeleted( $field ) {
  335. return ( $this->getVisibility() & $field ) == $field;
  336. }
  337. /**
  338. * Get the deletion bitfield of the revision
  339. *
  340. * MCR migration note: this replaces Revision::getVisibility
  341. *
  342. * @return int
  343. */
  344. public function getVisibility() {
  345. return (int)$this->mDeleted;
  346. }
  347. /**
  348. * MCR migration note: this replaces Revision::getTimestamp.
  349. *
  350. * May return null if the timestamp was not specified.
  351. *
  352. * @return string|null
  353. */
  354. public function getTimestamp() {
  355. return $this->mTimestamp;
  356. }
  357. /**
  358. * Check that the given audience has access to the given field.
  359. *
  360. * MCR migration note: this corresponds to Revision::userCan
  361. *
  362. * @param int $field One of self::DELETED_TEXT,
  363. * self::DELETED_COMMENT,
  364. * self::DELETED_USER
  365. * @param int $audience One of:
  366. * RevisionRecord::FOR_PUBLIC to be displayed to all users
  367. * RevisionRecord::FOR_THIS_USER to be displayed to the given user
  368. * RevisionRecord::RAW get the text regardless of permissions
  369. * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
  370. * ignored otherwise.
  371. *
  372. * @return bool
  373. */
  374. protected function audienceCan( $field, $audience, User $user = null ) {
  375. if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
  376. return false;
  377. } elseif ( $audience == self::FOR_THIS_USER ) {
  378. if ( !$user ) {
  379. throw new InvalidArgumentException(
  380. 'A User object must be given when checking FOR_THIS_USER audience.'
  381. );
  382. }
  383. if ( !$this->userCan( $field, $user ) ) {
  384. return false;
  385. }
  386. }
  387. return true;
  388. }
  389. /**
  390. * Determine if the current user is allowed to view a particular
  391. * field of this revision, if it's marked as deleted.
  392. *
  393. * MCR migration note: this corresponds to Revision::userCan
  394. *
  395. * @param int $field One of self::DELETED_TEXT,
  396. * self::DELETED_COMMENT,
  397. * self::DELETED_USER
  398. * @param User $user User object to check
  399. * @return bool
  400. */
  401. protected function userCan( $field, User $user ) {
  402. // TODO: use callback for permission checks, so we don't need to know a Title object!
  403. return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
  404. }
  405. /**
  406. * Determine if the current user is allowed to view a particular
  407. * field of this revision, if it's marked as deleted. This is used
  408. * by various classes to avoid duplication.
  409. *
  410. * MCR migration note: this replaces Revision::userCanBitfield
  411. *
  412. * @param int $bitfield Current field
  413. * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
  414. * self::DELETED_COMMENT = File::DELETED_COMMENT,
  415. * self::DELETED_USER = File::DELETED_USER
  416. * @param User $user User object to check
  417. * @param Title|null $title A Title object to check for per-page restrictions on,
  418. * instead of just plain userrights
  419. * @return bool
  420. */
  421. public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
  422. if ( $bitfield & $field ) { // aspect is deleted
  423. if ( $bitfield & self::DELETED_RESTRICTED ) {
  424. $permissions = [ 'suppressrevision', 'viewsuppressed' ];
  425. } elseif ( $field & self::DELETED_TEXT ) {
  426. $permissions = [ 'deletedtext' ];
  427. } else {
  428. $permissions = [ 'deletedhistory' ];
  429. }
  430. $permissionlist = implode( ', ', $permissions );
  431. if ( $title === null ) {
  432. wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
  433. return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
  434. } else {
  435. $text = $title->getPrefixedText();
  436. wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
  437. foreach ( $permissions as $perm ) {
  438. if ( $title->userCan( $perm, $user ) ) {
  439. return true;
  440. }
  441. }
  442. return false;
  443. }
  444. } else {
  445. return true;
  446. }
  447. }
  448. }