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 
60  private $derived;
61 
70  public static function newWithSuppressedContent( SlotRecord $slot ) {
71  $row = $slot->row;
72 
73  return new SlotRecord( $row, static function () {
74  throw new SuppressedDataException( 'Content suppressed!' );
75  } );
76  }
77 
87  public static function newDerived( string $role, Content $content ) {
88  return self::newUnsaved( $role, $content, true );
89  }
90 
100  private static function newFromSlotRecord( SlotRecord $slot, array $overrides = [] ) {
101  $row = clone $slot->row;
102  $row->slot_id = null; // never copy the row ID!
103 
104  foreach ( $overrides as $key => $value ) {
105  $row->$key = $value;
106  }
107 
108  return new SlotRecord( $row, $slot->content, $slot->isDerived() );
109  }
110 
123  public static function newInherited( SlotRecord $slot ) {
124  // Sanity check - we can't inherit from a Slot that's not attached to a revision.
125  $slot->getRevision();
126  $slot->getOrigin();
127  $slot->getAddress();
128 
129  // NOTE: slot_origin and content_address are copied from $slot.
130  return self::newFromSlotRecord( $slot, [
131  'slot_revision_id' => null,
132  ] );
133  }
134 
149  public static function newUnsaved( $role, Content $content, bool $derived = false ) {
150  Assert::parameterType( 'string', $role, '$role' );
151 
152  $row = [
153  'slot_id' => null, // not yet known
154  'slot_revision_id' => null, // not yet known
155  'slot_origin' => null, // not yet known, will be set in newSaved()
156  'content_size' => null, // compute later
157  'content_sha1' => null, // compute later
158  'slot_content_id' => null, // not yet known, will be set in newSaved()
159  'content_address' => null, // not yet known, will be set in newSaved()
160  'role_name' => $role,
161  'model_name' => $content->getModel(),
162  ];
163 
164  return new SlotRecord( (object)$row, $content, $derived );
165  }
166 
184  public static function newSaved(
185  $revisionId,
186  $contentId,
187  $contentAddress,
188  SlotRecord $protoSlot
189  ) {
190  Assert::parameterType( 'integer', $revisionId, '$revisionId' );
191  // TODO once migration is over $contentId must be an integer
192  Assert::parameterType( 'integer|null', $contentId, '$contentId' );
193  Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
194 
195  if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
196  throw new LogicException(
197  "Mismatching revision ID $revisionId: "
198  . "The slot already belongs to revision {$protoSlot->getRevision()}. "
199  . "Use SlotRecord::newInherited() to re-use content between revisions."
200  );
201  }
202 
203  if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
204  throw new LogicException(
205  "Mismatching blob address $contentAddress: "
206  . "The slot already has content at {$protoSlot->getAddress()}."
207  );
208  }
209 
210  if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
211  throw new LogicException(
212  "Mismatching content ID $contentId: "
213  . "The slot already has content row {$protoSlot->getContentId()} associated."
214  );
215  }
216 
217  if ( $protoSlot->isInherited() ) {
218  if ( !$protoSlot->hasAddress() ) {
219  throw new InvalidArgumentException(
220  "An inherited blob should have a content address!"
221  );
222  }
223  if ( !$protoSlot->hasField( 'slot_origin' ) ) {
224  throw new InvalidArgumentException(
225  "A saved inherited slot should have an origin set!"
226  );
227  }
228  $origin = $protoSlot->getOrigin();
229  } else {
230  $origin = $revisionId;
231  }
232 
233  return self::newFromSlotRecord( $protoSlot, [
234  'slot_revision_id' => $revisionId,
235  'slot_content_id' => $contentId,
236  'slot_origin' => $origin,
237  'content_address' => $contentAddress,
238  ] );
239  }
240 
259  public function __construct( $row, $content, bool $derived = false ) {
260  Assert::parameterType( \stdClass::class, $row, '$row' );
261  Assert::parameterType( 'Content|callable', $content, '$content' );
262 
263  Assert::parameter(
264  property_exists( $row, 'slot_revision_id' ),
265  '$row->slot_revision_id',
266  'must exist'
267  );
268  Assert::parameter(
269  property_exists( $row, 'slot_content_id' ),
270  '$row->slot_content_id',
271  'must exist'
272  );
273  Assert::parameter(
274  property_exists( $row, 'content_address' ),
275  '$row->content_address',
276  'must exist'
277  );
278  Assert::parameter(
279  property_exists( $row, 'model_name' ),
280  '$row->model_name',
281  'must exist'
282  );
283  Assert::parameter(
284  property_exists( $row, 'slot_origin' ),
285  '$row->slot_origin',
286  'must exist'
287  );
288  Assert::parameter(
289  !property_exists( $row, 'slot_inherited' ),
290  '$row->slot_inherited',
291  'must not exist'
292  );
293  Assert::parameter(
294  !property_exists( $row, 'slot_revision' ),
295  '$row->slot_revision',
296  'must not exist'
297  );
298 
299  $this->row = $row;
300  $this->content = $content;
301  $this->derived = $derived;
302  }
303 
319  public function getContent() {
320  if ( $this->content instanceof Content ) {
321  return $this->content;
322  }
323 
324  $obj = call_user_func( $this->content, $this );
325 
326  Assert::postcondition(
327  $obj instanceof Content,
328  'Slot content callback should return a Content object'
329  );
330 
331  $this->content = $obj;
332 
333  return $this->content;
334  }
335 
346  private function getField( $name ) {
347  if ( !isset( $this->row->$name ) ) {
348  // distinguish between unknown and uninitialized fields
349  if ( property_exists( $this->row, $name ) ) {
350  throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
351  } else {
352  throw new OutOfBoundsException( 'No such field: ' . $name );
353  }
354  }
355 
356  $value = $this->row->$name;
357 
358  // NOTE: allow callbacks, but don't trust plain string callables from the database!
359  if ( !is_string( $value ) && is_callable( $value ) ) {
360  $value = call_user_func( $value, $this );
361  $this->setField( $name, $value );
362  }
363 
364  return $value;
365  }
366 
376  private function getStringField( $name ) {
377  return strval( $this->getField( $name ) );
378  }
379 
389  private function getIntField( $name ) {
390  return intval( $this->getField( $name ) );
391  }
392 
397  private function hasField( $name ) {
398  if ( isset( $this->row->$name ) ) {
399  // if the field is a callback, resolve first, then re-check
400  if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
401  $this->getField( $name );
402  }
403  }
404 
405  return isset( $this->row->$name );
406  }
407 
413  public function getRevision() {
414  return $this->getIntField( 'slot_revision_id' );
415  }
416 
422  public function getOrigin() {
423  return $this->getIntField( 'slot_origin' );
424  }
425 
437  public function isInherited() {
438  if ( $this->hasRevision() ) {
439  return $this->getRevision() !== $this->getOrigin();
440  } else {
441  return $this->hasAddress();
442  }
443  }
444 
452  public function hasAddress() {
453  return $this->hasField( 'content_address' );
454  }
455 
463  public function hasOrigin() {
464  return $this->hasField( 'slot_origin' );
465  }
466 
486  public function hasContentId() {
487  return $this->hasField( 'slot_content_id' );
488  }
489 
497  public function hasRevision() {
498  return $this->hasField( 'slot_revision_id' );
499  }
500 
506  public function getRole() {
507  return $this->getStringField( 'role_name' );
508  }
509 
516  public function getAddress() {
517  return $this->getStringField( 'content_address' );
518  }
519 
530  public function getContentId() {
531  return $this->getIntField( 'slot_content_id' );
532  }
533 
539  public function getSize() {
540  try {
541  $size = $this->getIntField( 'content_size' );
542  } catch ( IncompleteRevisionException $ex ) {
543  $size = $this->getContent()->getSize();
544  $this->setField( 'content_size', $size );
545  }
546 
547  return $size;
548  }
549 
555  public function getSha1() {
556  try {
557  $sha1 = $this->getStringField( 'content_sha1' );
558  } catch ( IncompleteRevisionException $ex ) {
559  $sha1 = null;
560  }
561 
562  // Compute if missing. Missing could mean null or empty.
563  if ( $sha1 === null || $sha1 === '' ) {
564  $format = $this->hasField( 'format_name' )
565  ? $this->getStringField( 'format_name' )
566  : null;
567 
568  $data = $this->getContent()->serialize( $format );
569  $sha1 = self::base36Sha1( $data );
570  $this->setField( 'content_sha1', $sha1 );
571  }
572 
573  return $sha1;
574  }
575 
583  public function getModel() {
584  try {
585  $model = $this->getStringField( 'model_name' );
586  } catch ( IncompleteRevisionException $ex ) {
587  $model = $this->getContent()->getModel();
588  $this->setField( 'model_name', $model );
589  }
590 
591  return $model;
592  }
593 
603  public function getFormat() {
604  // XXX: we currently do not plan to store the format for each slot!
605 
606  if ( $this->hasField( 'format_name' ) ) {
607  return $this->getStringField( 'format_name' );
608  }
609 
610  return null;
611  }
612 
617  private function setField( $name, $value ) {
618  $this->row->$name = $value;
619  }
620 
629  public static function base36Sha1( $blob ) {
630  return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
631  }
632 
652  public function hasSameContent( SlotRecord $other ) {
653  if ( $other === $this ) {
654  return true;
655  }
656 
657  if ( $this->getModel() !== $other->getModel() ) {
658  return false;
659  }
660 
661  if ( $this->hasAddress()
662  && $other->hasAddress()
663  && $this->getAddress() == $other->getAddress()
664  ) {
665  return true;
666  }
667 
668  if ( $this->getSize() !== $other->getSize() ) {
669  return false;
670  }
671 
672  if ( $this->getSha1() !== $other->getSha1() ) {
673  return false;
674  }
675 
676  return true;
677  }
678 
683  public function isDerived() : bool {
684  return $this->derived;
685  }
686 
687 }
688 
693 class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
Revision\SlotRecord\hasField
hasField( $name)
Definition: SlotRecord.php:397
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:319
Revision\SlotRecord\isDerived
isDerived()
Definition: SlotRecord.php:683
Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:452
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:123
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:603
Revision\SlotRecord\getIntField
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:389
Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:463
Revision\SlotRecord\$derived
bool $derived
Definition: SlotRecord.php:60
Revision\SlotRecord\isInherited
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:437
Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:413
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:422
$blob
$blob
Definition: testCompression.php:70
Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:506
Revision\SlotRecord\newFromSlotRecord
static newFromSlotRecord(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:100
Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:486
Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:583
Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:516
Revision\SlotRecord\base36Sha1
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:629
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:70
Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:555
Revision\SlotRecord\__construct
__construct( $row, $content, bool $derived=false)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:259
Revision\SlotRecord\hasRevision
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:497
Revision\SlotRecord\newDerived
static newDerived(string $role, Content $content)
Returns a SlotRecord for a derived slot.
Definition: SlotRecord.php:87
Revision\SlotRecord\newUnsaved
static newUnsaved( $role, Content $content, bool $derived=false)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:149
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:539
Revision\SlotRecord\getField
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:346
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:376
Content
Base interface for content objects.
Definition: Content.php:35
Revision\SlotRecord\setField
setField( $name, $value)
Definition: SlotRecord.php:617
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:184
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:530
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:652