3 * Value object representing a content slot associated with a page revision.
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.
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.
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
23 namespace MediaWiki\Storage
;
26 use InvalidArgumentException
;
28 use OutOfBoundsException
;
29 use Wikimedia\Assert\Assert
;
32 * Value object representing a content slot associated with a page revision.
33 * SlotRecord provides direct access to a Content object.
34 * That access may be implemented through a callback.
41 * @var object database result row, as a raw object
46 * @var Content|callable
51 * Returns a new SlotRecord just like the given $slot, except that calling getContent()
52 * will fail with an exception.
54 * @param SlotRecord $slot
58 public static function newWithSuppressedContent( SlotRecord
$slot ) {
61 return new SlotRecord( $row, function () {
62 throw new SuppressedDataException( 'Content suppressed!' );
67 * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
68 * The slot's content cannot be overwritten.
70 * @param SlotRecord $slot
71 * @param array $overrides
75 private static function newDerived( SlotRecord
$slot, array $overrides = [] ) {
76 $row = clone $slot->row
;
77 $row->slot_id
= null; // never copy the row ID!
79 foreach ( $overrides as $key => $value ) {
83 return new SlotRecord( $row, $slot->content
);
87 * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
88 * of a previous revision.
90 * Note that a SlotRecord constructed this way are intended as prototypes,
91 * to be used wit newSaved(). They are incomplete, so some getters such as
92 * getRevision() will fail.
94 * @param SlotRecord $slot
98 public static function newInherited( SlotRecord
$slot ) {
99 // Sanity check - we can't inherit from a Slot that's not attached to a revision.
100 $slot->getRevision();
104 // NOTE: slot_origin and content_address are copied from $slot.
105 return self
::newDerived( $slot, [
106 'slot_revision_id' => null,
111 * Constructs a new Slot from a Content object for a new revision.
112 * This is the preferred way to construct a slot for storing Content that
113 * resulted from a user edit. The slot is assumed to be not inherited.
115 * Note that a SlotRecord constructed this way are intended as prototypes,
116 * to be used wit newSaved(). They are incomplete, so some getters such as
117 * getAddress() will fail.
119 * @param string $role
120 * @param Content $content
122 * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
124 public static function newUnsaved( $role, Content
$content ) {
125 Assert
::parameterType( 'string', $role, '$role' );
128 'slot_id' => null, // not yet known
129 'slot_revision_id' => null, // not yet known
130 'slot_origin' => null, // not yet known, will be set in newSaved()
131 'content_size' => null, // compute later
132 'content_sha1' => null, // compute later
133 'slot_content_id' => null, // not yet known, will be set in newSaved()
134 'content_address' => null, // not yet known, will be set in newSaved()
135 'role_name' => $role,
136 'model_name' => $content->getModel(),
139 return new SlotRecord( (object)$row, $content );
143 * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
144 * proto-slot. This adds information that has only become available during saving,
145 * particularly the revision ID and content address.
147 * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
148 * If $protoSlot already has a revision, it must be the same.
149 * @param int $contentId the ID of the row in the content table describing the content
150 * referenced by $contentAddress (field slot_content_id).
151 * If $protoSlot already has a content ID, it must be the same.
152 * @param string $contentAddress the slot's content address (field content_address).
153 * If $protoSlot already has an address, it must be the same.
154 * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
155 * revision. $protoSlot must have a content address if inherited.
157 * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
159 public static function newSaved(
163 SlotRecord
$protoSlot
165 Assert
::parameterType( 'integer', $revisionId, '$revisionId' );
166 Assert
::parameterType( 'integer', $contentId, '$contentId' );
167 Assert
::parameterType( 'string', $contentAddress, '$contentAddress' );
169 if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
170 throw new LogicException(
171 "Mismatching revision ID $revisionId: "
172 . "The slot already belongs to revision {$protoSlot->getRevision()}. "
173 . "Use SlotRecord::newInherited() to re-use content between revisions."
177 if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
178 throw new LogicException(
179 "Mismatching blob address $contentAddress: "
180 . "The slot already has content at {$protoSlot->getAddress()}."
184 if ( $protoSlot->hasAddress() && $protoSlot->getContentId() !== $contentId ) {
185 throw new LogicException(
186 "Mismatching content ID $contentId: "
187 . "The slot already has content row {$protoSlot->getContentId()} associated."
191 if ( $protoSlot->isInherited() ) {
192 if ( !$protoSlot->hasAddress() ) {
193 throw new InvalidArgumentException(
194 "An inherited blob should have a content address!"
197 if ( !$protoSlot->hasField( 'slot_origin' ) ) {
198 throw new InvalidArgumentException(
199 "A saved inherited slot should have an origin set!"
202 $origin = $protoSlot->getOrigin();
204 $origin = $revisionId;
207 return self
::newDerived( $protoSlot, [
208 'slot_revision_id' => $revisionId,
209 'slot_content_id' => $contentId,
210 'slot_origin' => $origin,
211 'content_address' => $contentAddress,
216 * SlotRecord constructor.
218 * The following fields are supported by the $row parameter:
223 * @param object $row A database row composed of fields of the slot and content tables,
224 * as a raw object. Any field value can be a callback that produces the field value
225 * given this SlotRecord as a parameter. However, plain strings cannot be used as
226 * callbacks here, for security reasons.
227 * @param Content|callable $content The content object associated with the slot, or a
228 * callback that will return that Content object, given this SlotRecord as a parameter.
230 public function __construct( $row, $content ) {
231 Assert
::parameterType( 'object', $row, '$row' );
232 Assert
::parameterType( 'Content|callable', $content, '$content' );
235 property_exists( $row, 'slot_id' ),
240 property_exists( $row, 'slot_revision_id' ),
241 '$row->slot_revision_id',
245 property_exists( $row, 'slot_content_id' ),
246 '$row->slot_content_id',
250 property_exists( $row, 'content_address' ),
251 '$row->content_address',
255 property_exists( $row, 'model_name' ),
260 property_exists( $row, 'slot_origin' ),
265 !property_exists( $row, 'slot_inherited' ),
266 '$row->slot_inherited',
270 !property_exists( $row, 'slot_revision' ),
271 '$row->slot_revision',
276 $this->content
= $content;
280 * Implemented to defy serialization.
282 * @throws LogicException always
284 public function __sleep() {
285 throw new LogicException( __CLASS__
. ' is not serializable.' );
289 * Returns the Content of the given slot.
291 * @note This is free to load Content from whatever subsystem is necessary,
292 * performing potentially expensive operations and triggering I/O-related
295 * @note This method does not apply audience filtering.
297 * @throws SuppressedDataException if access to the content is not allowed according
298 * to the audience check performed by RevisionRecord::getSlot().
300 * @return Content The slot's content. This is a direct reference to the internal instance,
301 * copy before exposing to application logic!
303 public function getContent() {
304 if ( $this->content
instanceof Content
) {
305 return $this->content
;
308 $obj = call_user_func( $this->content
, $this );
310 Assert
::postcondition(
311 $obj instanceof Content
,
312 'Slot content callback should return a Content object'
315 $this->content
= $obj;
317 return $this->content
;
321 * Returns the string value of a data field from the database row supplied to the constructor.
322 * If the field was set to a callback, that callback is invoked and the result returned.
324 * @param string $name
326 * @throws OutOfBoundsException
327 * @throws IncompleteRevisionException
328 * @return mixed Returns the field's value, never null.
330 private function getField( $name ) {
331 if ( !isset( $this->row
->$name ) ) {
332 // distinguish between unknown and uninitialized fields
333 if ( property_exists( $this->row
, $name ) ) {
334 throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
336 throw new OutOfBoundsException( 'No such field: ' . $name );
340 $value = $this->row
->$name;
342 // NOTE: allow callbacks, but don't trust plain string callables from the database!
343 if ( !is_string( $value ) && is_callable( $value ) ) {
344 $value = call_user_func( $value, $this );
345 $this->setField( $name, $value );
352 * Returns the string value of a data field from the database row supplied to the constructor.
354 * @param string $name
356 * @throws OutOfBoundsException
357 * @throws IncompleteRevisionException
358 * @return string Returns the string value
360 private function getStringField( $name ) {
361 return strval( $this->getField( $name ) );
365 * Returns the int value of a data field from the database row supplied to the constructor.
367 * @param string $name
369 * @throws OutOfBoundsException
370 * @throws IncompleteRevisionException
371 * @return int Returns the int value
373 private function getIntField( $name ) {
374 return intval( $this->getField( $name ) );
378 * @param string $name
379 * @return bool whether this record contains the given field
381 private function hasField( $name ) {
382 return isset( $this->row
->$name );
386 * Returns the ID of the revision this slot is associated with.
390 public function getRevision() {
391 return $this->getIntField( 'slot_revision_id' );
395 * Returns the revision ID of the revision that originated the slot's content.
399 public function getOrigin() {
400 return $this->getIntField( 'slot_origin' );
404 * Whether this slot was inherited from an older revision.
406 * If this SlotRecord is already attached to a revision, this returns true
407 * if the slot's revision of origin is the same as the revision it belongs to.
409 * If this SlotRecord is not yet attached to a revision, this returns true
410 * if the slot already has an address.
414 public function isInherited() {
415 if ( $this->hasRevision() ) {
416 return $this->getRevision() !== $this->getOrigin();
418 return $this->hasAddress();
423 * Whether this slot has an address. Slots will have an address if their
424 * content has been stored. While building a new revision,
425 * SlotRecords will not have an address associated.
429 public function hasAddress() {
430 return $this->hasField( 'content_address' );
434 * Whether this slot has revision ID associated. Slots will have a revision ID associated
435 * only if they were loaded as part of an existing revision. While building a new revision,
436 * Slotrecords will not have a revision ID associated.
440 public function hasRevision() {
441 return $this->hasField( 'slot_revision_id' );
445 * Returns the role of the slot.
449 public function getRole() {
450 return $this->getStringField( 'role_name' );
454 * Returns the address of this slot's content.
455 * This address can be used with BlobStore to load the Content object.
459 public function getAddress() {
460 return $this->getStringField( 'content_address' );
464 * Returns the ID of the content meta data row associated with the slot.
465 * This information should be irrelevant to application logic, it is here to allow
466 * the construction of a full row for the revision table.
470 public function getContentId() {
471 return $this->getIntField( 'slot_content_id' );
475 * Returns the content size
477 * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
479 public function getSize() {
481 $size = $this->getIntField( 'content_size' );
482 } catch ( IncompleteRevisionException
$ex ) {
483 $size = $this->getContent()->getSize();
484 $this->setField( 'content_size', $size );
491 * Returns the content size
493 * @return string hash of the content.
495 public function getSha1() {
497 $sha1 = $this->getStringField( 'content_sha1' );
498 } catch ( IncompleteRevisionException
$ex ) {
499 $format = $this->hasField( 'format_name' )
500 ?
$this->getStringField( 'format_name' )
503 $data = $this->getContent()->serialize( $format );
504 $sha1 = self
::base36Sha1( $data );
505 $this->setField( 'content_sha1', $sha1 );
512 * Returns the content model. This is the model name that decides
513 * which ContentHandler is appropriate for interpreting the
514 * data of the blob referenced by the address returned by getAddress().
516 * @return string the content model of the content
518 public function getModel() {
520 $model = $this->getStringField( 'model_name' );
521 } catch ( IncompleteRevisionException
$ex ) {
522 $model = $this->getContent()->getModel();
523 $this->setField( 'model_name', $model );
530 * Returns the blob serialization format as a MIME type.
532 * @note When this method returns null, the caller is expected
533 * to auto-detect the serialization format, or to rely on
534 * the default format associated with the content model.
536 * @return string|null
538 public function getFormat() {
539 // XXX: we currently do not plan to store the format for each slot!
541 if ( $this->hasField( 'format_name' ) ) {
542 return $this->getStringField( 'format_name' );
549 * @param string $name
550 * @param string|int|null $value
552 private function setField( $name, $value ) {
553 $this->row
->$name = $value;
557 * Get the base 36 SHA-1 value for a string of text
559 * MCR migration note: this replaces Revision::base36Sha1
561 * @param string $blob
564 public static function base36Sha1( $blob ) {
565 return \Wikimedia\base_convert
( sha1( $blob ), 16, 36, 31 );