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