Merge "Chinese Conversion Table Update 2017-6"
[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 the slot names (roles) of all slots present in this revision.
201 * getContent() will succeed only for the names returned by this method.
202 *
203 * @return string[]
204 */
205 public function getSlotRoles() {
206 return $this->mSlots->getSlotRoles();
207 }
208
209 /**
210 * Get revision ID. Depending on the concrete subclass, this may return null if
211 * the revision ID is not known (e.g. because the revision does not yet exist
212 * in the database).
213 *
214 * MCR migration note: this replaces Revision::getId
215 *
216 * @return int|null
217 */
218 public function getId() {
219 return $this->mId;
220 }
221
222 /**
223 * Get parent revision ID (the original previous page revision).
224 * If there is no parent revision, this returns 0.
225 * If the parent revision is undefined or unknown, this returns null.
226 *
227 * @note As of MW 1.31, the database schema allows the parent ID to be
228 * NULL to indicate that it is unknown.
229 *
230 * MCR migration note: this replaces Revision::getParentId
231 *
232 * @return int|null
233 */
234 public function getParentId() {
235 return $this->mParentId;
236 }
237
238 /**
239 * Returns the nominal size of this revision, in bogo-bytes.
240 * May be calculated on the fly if not known, which may in the worst
241 * case may involve loading all content.
242 *
243 * MCR migration note: this replaces Revision::getSize
244 *
245 * @return int
246 */
247 abstract public function getSize();
248
249 /**
250 * Returns the base36 sha1 of this revision. This hash is derived from the
251 * hashes of all slots associated with the revision.
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::getSha1
256 *
257 * @return string
258 */
259 abstract public function getSha1();
260
261 /**
262 * Get the page ID. If the page does not yet exist, the page ID is 0.
263 *
264 * MCR migration note: this replaces Revision::getPage
265 *
266 * @return int
267 */
268 public function getPageId() {
269 return $this->mPageId;
270 }
271
272 /**
273 * Get the ID of the wiki this revision belongs to.
274 *
275 * @return string|false The wiki's logical name, of false to indicate the local wiki.
276 */
277 public function getWikiId() {
278 return $this->mWiki;
279 }
280
281 /**
282 * Returns the title of the page this revision is associated with as a LinkTarget object.
283 *
284 * MCR migration note: this replaces Revision::getTitle
285 *
286 * @return LinkTarget
287 */
288 public function getPageAsLinkTarget() {
289 return $this->mTitle;
290 }
291
292 /**
293 * Fetch revision's author's user identity, if it's available to the specified audience.
294 * If the specified audience does not have access to it, null will be
295 * returned. Depending on the concrete subclass, null may also be returned if the user is
296 * not yet specified.
297 *
298 * MCR migration note: this replaces Revision::getUser
299 *
300 * @param int $audience One of:
301 * RevisionRecord::FOR_PUBLIC to be displayed to all users
302 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
303 * RevisionRecord::RAW get the ID regardless of permissions
304 * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
305 * to the $audience parameter
306 * @return UserIdentity|null
307 */
308 public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
309 if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
310 return null;
311 } else {
312 return $this->mUser;
313 }
314 }
315
316 /**
317 * Fetch revision comment, if it's available to the specified audience.
318 * If the specified audience does not have access to the comment,
319 * this will return null. Depending on the concrete subclass, null may also be returned
320 * if the comment is not yet specified.
321 *
322 * MCR migration note: this replaces Revision::getComment
323 *
324 * @param int $audience One of:
325 * RevisionRecord::FOR_PUBLIC to be displayed to all users
326 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
327 * RevisionRecord::RAW get the text regardless of permissions
328 * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
329 * to the $audience parameter
330 *
331 * @return CommentStoreComment|null
332 */
333 public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
334 if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
335 return null;
336 } else {
337 return $this->mComment;
338 }
339 }
340
341 /**
342 * MCR migration note: this replaces Revision::isMinor
343 *
344 * @return bool
345 */
346 public function isMinor() {
347 return (bool)$this->mMinorEdit;
348 }
349
350 /**
351 * MCR migration note: this replaces Revision::isDeleted
352 *
353 * @param int $field One of DELETED_* bitfield constants
354 *
355 * @return bool
356 */
357 public function isDeleted( $field ) {
358 return ( $this->getVisibility() & $field ) == $field;
359 }
360
361 /**
362 * Get the deletion bitfield of the revision
363 *
364 * MCR migration note: this replaces Revision::getVisibility
365 *
366 * @return int
367 */
368 public function getVisibility() {
369 return (int)$this->mDeleted;
370 }
371
372 /**
373 * MCR migration note: this replaces Revision::getTimestamp.
374 *
375 * May return null if the timestamp was not specified.
376 *
377 * @return string|null
378 */
379 public function getTimestamp() {
380 return $this->mTimestamp;
381 }
382
383 /**
384 * Check that the given audience has access to the given field.
385 *
386 * MCR migration note: this corresponds to Revision::userCan
387 *
388 * @param int $field One of self::DELETED_TEXT,
389 * self::DELETED_COMMENT,
390 * self::DELETED_USER
391 * @param int $audience One of:
392 * RevisionRecord::FOR_PUBLIC to be displayed to all users
393 * RevisionRecord::FOR_THIS_USER to be displayed to the given user
394 * RevisionRecord::RAW get the text regardless of permissions
395 * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
396 * ignored otherwise.
397 *
398 * @return bool
399 */
400 protected function audienceCan( $field, $audience, User $user = null ) {
401 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
402 return false;
403 } elseif ( $audience == self::FOR_THIS_USER ) {
404 if ( !$user ) {
405 throw new InvalidArgumentException(
406 'A User object must be given when checking FOR_THIS_USER audience.'
407 );
408 }
409
410 if ( !$this->userCan( $field, $user ) ) {
411 return false;
412 }
413 }
414
415 return true;
416 }
417
418 /**
419 * Determine if the current user is allowed to view a particular
420 * field of this revision, if it's marked as deleted.
421 *
422 * MCR migration note: this corresponds to Revision::userCan
423 *
424 * @param int $field One of self::DELETED_TEXT,
425 * self::DELETED_COMMENT,
426 * self::DELETED_USER
427 * @param User $user User object to check
428 * @return bool
429 */
430 protected function userCan( $field, User $user ) {
431 // TODO: use callback for permission checks, so we don't need to know a Title object!
432 return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
433 }
434
435 /**
436 * Determine if the current user is allowed to view a particular
437 * field of this revision, if it's marked as deleted. This is used
438 * by various classes to avoid duplication.
439 *
440 * MCR migration note: this replaces Revision::userCanBitfield
441 *
442 * @param int $bitfield Current field
443 * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
444 * self::DELETED_COMMENT = File::DELETED_COMMENT,
445 * self::DELETED_USER = File::DELETED_USER
446 * @param User $user User object to check
447 * @param Title|null $title A Title object to check for per-page restrictions on,
448 * instead of just plain userrights
449 * @return bool
450 */
451 public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
452 if ( $bitfield & $field ) { // aspect is deleted
453 if ( $bitfield & self::DELETED_RESTRICTED ) {
454 $permissions = [ 'suppressrevision', 'viewsuppressed' ];
455 } elseif ( $field & self::DELETED_TEXT ) {
456 $permissions = [ 'deletedtext' ];
457 } else {
458 $permissions = [ 'deletedhistory' ];
459 }
460 $permissionlist = implode( ', ', $permissions );
461 if ( $title === null ) {
462 wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
463 return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
464 } else {
465 $text = $title->getPrefixedText();
466 wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
467 foreach ( $permissions as $perm ) {
468 if ( $title->userCan( $perm, $user ) ) {
469 return true;
470 }
471 }
472 return false;
473 }
474 } else {
475 return true;
476 }
477 }
478
479 }