MediaWiki  master
SlotRecord.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Revision;
24 
25 use Content;
26 use InvalidArgumentException;
27 use LogicException;
28 use OutOfBoundsException;
29 use Wikimedia\Assert\Assert;
30 use Wikimedia\NonSerializable\NonSerializableTrait;
31 
40 class SlotRecord {
41  use NonSerializableTrait;
42 
43  public const MAIN = 'main';
44 
50  private $row;
51 
55  private $content;
56 
65  public static function newWithSuppressedContent( SlotRecord $slot ) {
66  $row = $slot->row;
67 
68  return new SlotRecord( $row, function () {
69  throw new SuppressedDataException( 'Content suppressed!' );
70  } );
71  }
72 
82  private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
83  $row = clone $slot->row;
84  $row->slot_id = null; // never copy the row ID!
85 
86  foreach ( $overrides as $key => $value ) {
87  $row->$key = $value;
88  }
89 
90  return new SlotRecord( $row, $slot->content );
91  }
92 
105  public static function newInherited( SlotRecord $slot ) {
106  // Sanity check - we can't inherit from a Slot that's not attached to a revision.
107  $slot->getRevision();
108  $slot->getOrigin();
109  $slot->getAddress();
110 
111  // NOTE: slot_origin and content_address are copied from $slot.
112  return self::newDerived( $slot, [
113  'slot_revision_id' => null,
114  ] );
115  }
116 
131  public static function newUnsaved( $role, Content $content ) {
132  Assert::parameterType( 'string', $role, '$role' );
133 
134  $row = [
135  'slot_id' => null, // not yet known
136  'slot_revision_id' => null, // not yet known
137  'slot_origin' => null, // not yet known, will be set in newSaved()
138  'content_size' => null, // compute later
139  'content_sha1' => null, // compute later
140  'slot_content_id' => null, // not yet known, will be set in newSaved()
141  'content_address' => null, // not yet known, will be set in newSaved()
142  'role_name' => $role,
143  'model_name' => $content->getModel(),
144  ];
145 
146  return new SlotRecord( (object)$row, $content );
147  }
148 
166  public static function newSaved(
167  $revisionId,
168  $contentId,
169  $contentAddress,
170  SlotRecord $protoSlot
171  ) {
172  Assert::parameterType( 'integer', $revisionId, '$revisionId' );
173  // TODO once migration is over $contentId must be an integer
174  Assert::parameterType( 'integer|null', $contentId, '$contentId' );
175  Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
176 
177  if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
178  throw new LogicException(
179  "Mismatching revision ID $revisionId: "
180  . "The slot already belongs to revision {$protoSlot->getRevision()}. "
181  . "Use SlotRecord::newInherited() to re-use content between revisions."
182  );
183  }
184 
185  if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
186  throw new LogicException(
187  "Mismatching blob address $contentAddress: "
188  . "The slot already has content at {$protoSlot->getAddress()}."
189  );
190  }
191 
192  if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
193  throw new LogicException(
194  "Mismatching content ID $contentId: "
195  . "The slot already has content row {$protoSlot->getContentId()} associated."
196  );
197  }
198 
199  if ( $protoSlot->isInherited() ) {
200  if ( !$protoSlot->hasAddress() ) {
201  throw new InvalidArgumentException(
202  "An inherited blob should have a content address!"
203  );
204  }
205  if ( !$protoSlot->hasField( 'slot_origin' ) ) {
206  throw new InvalidArgumentException(
207  "A saved inherited slot should have an origin set!"
208  );
209  }
210  $origin = $protoSlot->getOrigin();
211  } else {
212  $origin = $revisionId;
213  }
214 
215  return self::newDerived( $protoSlot, [
216  'slot_revision_id' => $revisionId,
217  'slot_content_id' => $contentId,
218  'slot_origin' => $origin,
219  'content_address' => $contentAddress,
220  ] );
221  }
222 
236  public function __construct( $row, $content ) {
237  Assert::parameterType( \stdClass::class, $row, '$row' );
238  Assert::parameterType( 'Content|callable', $content, '$content' );
239 
240  Assert::parameter(
241  property_exists( $row, 'slot_revision_id' ),
242  '$row->slot_revision_id',
243  'must exist'
244  );
245  Assert::parameter(
246  property_exists( $row, 'slot_content_id' ),
247  '$row->slot_content_id',
248  'must exist'
249  );
250  Assert::parameter(
251  property_exists( $row, 'content_address' ),
252  '$row->content_address',
253  'must exist'
254  );
255  Assert::parameter(
256  property_exists( $row, 'model_name' ),
257  '$row->model_name',
258  'must exist'
259  );
260  Assert::parameter(
261  property_exists( $row, 'slot_origin' ),
262  '$row->slot_origin',
263  'must exist'
264  );
265  Assert::parameter(
266  !property_exists( $row, 'slot_inherited' ),
267  '$row->slot_inherited',
268  'must not exist'
269  );
270  Assert::parameter(
271  !property_exists( $row, 'slot_revision' ),
272  '$row->slot_revision',
273  'must not exist'
274  );
275 
276  $this->row = $row;
277  $this->content = $content;
278  }
279 
295  public function getContent() {
296  if ( $this->content instanceof Content ) {
297  return $this->content;
298  }
299 
300  $obj = call_user_func( $this->content, $this );
301 
302  Assert::postcondition(
303  $obj instanceof Content,
304  'Slot content callback should return a Content object'
305  );
306 
307  $this->content = $obj;
308 
309  return $this->content;
310  }
311 
322  private function getField( $name ) {
323  if ( !isset( $this->row->$name ) ) {
324  // distinguish between unknown and uninitialized fields
325  if ( property_exists( $this->row, $name ) ) {
326  throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
327  } else {
328  throw new OutOfBoundsException( 'No such field: ' . $name );
329  }
330  }
331 
332  $value = $this->row->$name;
333 
334  // NOTE: allow callbacks, but don't trust plain string callables from the database!
335  if ( !is_string( $value ) && is_callable( $value ) ) {
336  $value = call_user_func( $value, $this );
337  $this->setField( $name, $value );
338  }
339 
340  return $value;
341  }
342 
352  private function getStringField( $name ) {
353  return strval( $this->getField( $name ) );
354  }
355 
365  private function getIntField( $name ) {
366  return intval( $this->getField( $name ) );
367  }
368 
373  private function hasField( $name ) {
374  if ( isset( $this->row->$name ) ) {
375  // if the field is a callback, resolve first, then re-check
376  if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
377  $this->getField( $name );
378  }
379  }
380 
381  return isset( $this->row->$name );
382  }
383 
389  public function getRevision() {
390  return $this->getIntField( 'slot_revision_id' );
391  }
392 
398  public function getOrigin() {
399  return $this->getIntField( 'slot_origin' );
400  }
401 
413  public function isInherited() {
414  if ( $this->hasRevision() ) {
415  return $this->getRevision() !== $this->getOrigin();
416  } else {
417  return $this->hasAddress();
418  }
419  }
420 
428  public function hasAddress() {
429  return $this->hasField( 'content_address' );
430  }
431 
439  public function hasOrigin() {
440  return $this->hasField( 'slot_origin' );
441  }
442 
462  public function hasContentId() {
463  return $this->hasField( 'slot_content_id' );
464  }
465 
473  public function hasRevision() {
474  return $this->hasField( 'slot_revision_id' );
475  }
476 
482  public function getRole() {
483  return $this->getStringField( 'role_name' );
484  }
485 
492  public function getAddress() {
493  return $this->getStringField( 'content_address' );
494  }
495 
506  public function getContentId() {
507  return $this->getIntField( 'slot_content_id' );
508  }
509 
515  public function getSize() {
516  try {
517  $size = $this->getIntField( 'content_size' );
518  } catch ( IncompleteRevisionException $ex ) {
519  $size = $this->getContent()->getSize();
520  $this->setField( 'content_size', $size );
521  }
522 
523  return $size;
524  }
525 
531  public function getSha1() {
532  try {
533  $sha1 = $this->getStringField( 'content_sha1' );
534  } catch ( IncompleteRevisionException $ex ) {
535  $sha1 = null;
536  }
537 
538  // Compute if missing. Missing could mean null or empty.
539  if ( $sha1 === null || $sha1 === '' ) {
540  $format = $this->hasField( 'format_name' )
541  ? $this->getStringField( 'format_name' )
542  : null;
543 
544  $data = $this->getContent()->serialize( $format );
545  $sha1 = self::base36Sha1( $data );
546  $this->setField( 'content_sha1', $sha1 );
547  }
548 
549  return $sha1;
550  }
551 
559  public function getModel() {
560  try {
561  $model = $this->getStringField( 'model_name' );
562  } catch ( IncompleteRevisionException $ex ) {
563  $model = $this->getContent()->getModel();
564  $this->setField( 'model_name', $model );
565  }
566 
567  return $model;
568  }
569 
579  public function getFormat() {
580  // XXX: we currently do not plan to store the format for each slot!
581 
582  if ( $this->hasField( 'format_name' ) ) {
583  return $this->getStringField( 'format_name' );
584  }
585 
586  return null;
587  }
588 
593  private function setField( $name, $value ) {
594  $this->row->$name = $value;
595  }
596 
605  public static function base36Sha1( $blob ) {
606  return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
607  }
608 
628  public function hasSameContent( SlotRecord $other ) {
629  if ( $other === $this ) {
630  return true;
631  }
632 
633  if ( $this->getModel() !== $other->getModel() ) {
634  return false;
635  }
636 
637  if ( $this->hasAddress()
638  && $other->hasAddress()
639  && $this->getAddress() == $other->getAddress()
640  ) {
641  return true;
642  }
643 
644  if ( $this->getSize() !== $other->getSize() ) {
645  return false;
646  }
647 
648  if ( $this->getSha1() !== $other->getSha1() ) {
649  return false;
650  }
651 
652  return true;
653  }
654 
655 }
656 
661 class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
Revision\SlotRecord\hasField
hasField( $name)
Definition: SlotRecord.php:373
Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:295
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:428
Revision\SlotRecord\newInherited
static newInherited(SlotRecord $slot)
Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord of a p...
Definition: SlotRecord.php:105
Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:33
Revision\SlotRecord\getFormat
getFormat()
Returns the blob serialization format as a MIME type.
Definition: SlotRecord.php:579
Revision\SlotRecord\getIntField
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:365
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:439
Revision\SlotRecord\isInherited
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:413
Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:389
Revision\SlotRecord\newDerived
static newDerived(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:82
MediaWiki\Revision
Definition: ContributionsLookup.php:3
Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:398
$blob
$blob
Definition: testCompression.php:70
Revision\SlotRecord\__construct
__construct( $row, $content)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:236
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:482
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:462
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:559
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:492
Revision\SlotRecord\base36Sha1
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:605
Revision\SlotRecord\newWithSuppressedContent
static newWithSuppressedContent(SlotRecord $slot)
Returns a new SlotRecord just like the given $slot, except that calling getContent() will fail with a...
Definition: SlotRecord.php:65
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:531
Revision\SlotRecord\newUnsaved
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:131
Revision\SlotRecord\hasRevision
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:473
Revision\SlotRecord\$row
stdClass $row
database result row, as a raw object.
Definition: SlotRecord.php:50
Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:515
Revision\SlotRecord\getField
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:322
Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
Revision\SlotRecord\getStringField
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:352
Content
Base interface for content objects.
Definition: Content.php:35
Revision\SlotRecord\setField
setField( $name, $value)
Definition: SlotRecord.php:593
Revision\SlotRecord\newSaved
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:166
Content\getModel
getModel()
Returns the ID of the content model used by this Content object.
Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:506
Revision\SlotRecord\$content
Content callable $content
Definition: SlotRecord.php:55
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Revision\SlotRecord\hasSameContent
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
Definition: SlotRecord.php:628