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