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(
74  $row,
78  static function () {
79  throw new SuppressedDataException( 'Content suppressed!' );
80  }
81  );
82  }
83 
93  public static function newDerived( string $role, Content $content ) {
94  return self::newUnsaved( $role, $content, true );
95  }
96 
106  private static function newFromSlotRecord( SlotRecord $slot, array $overrides = [] ) {
107  $row = clone $slot->row;
108  $row->slot_id = null; // never copy the row ID!
109 
110  foreach ( $overrides as $key => $value ) {
111  $row->$key = $value;
112  }
113 
114  return new SlotRecord( $row, $slot->content, $slot->isDerived() );
115  }
116 
129  public static function newInherited( SlotRecord $slot ) {
130  // Sanity check - we can't inherit from a Slot that's not attached to a revision.
131  $slot->getRevision();
132  $slot->getOrigin();
133  $slot->getAddress();
134 
135  // NOTE: slot_origin and content_address are copied from $slot.
136  return self::newFromSlotRecord( $slot, [
137  'slot_revision_id' => null,
138  ] );
139  }
140 
155  public static function newUnsaved( string $role, Content $content, bool $derived = false ) {
156  $row = [
157  'slot_id' => null, // not yet known
158  'slot_revision_id' => null, // not yet known
159  'slot_origin' => null, // not yet known, will be set in newSaved()
160  'content_size' => null, // compute later
161  'content_sha1' => null, // compute later
162  'slot_content_id' => null, // not yet known, will be set in newSaved()
163  'content_address' => null, // not yet known, will be set in newSaved()
164  'role_name' => $role,
165  'model_name' => $content->getModel(),
166  ];
167 
168  return new SlotRecord( (object)$row, $content, $derived );
169  }
170 
188  public static function newSaved(
189  int $revisionId,
190  ?int $contentId,
191  string $contentAddress,
192  SlotRecord $protoSlot
193  ) {
194  if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
195  throw new LogicException(
196  "Mismatching revision ID $revisionId: "
197  . "The slot already belongs to revision {$protoSlot->getRevision()}. "
198  . "Use SlotRecord::newInherited() to re-use content between revisions."
199  );
200  }
201 
202  if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
203  throw new LogicException(
204  "Mismatching blob address $contentAddress: "
205  . "The slot already has content at {$protoSlot->getAddress()}."
206  );
207  }
208 
209  if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
210  throw new LogicException(
211  "Mismatching content ID $contentId: "
212  . "The slot already has content row {$protoSlot->getContentId()} associated."
213  );
214  }
215 
216  if ( $protoSlot->isInherited() ) {
217  if ( !$protoSlot->hasAddress() ) {
218  throw new InvalidArgumentException(
219  "An inherited blob should have a content address!"
220  );
221  }
222  if ( !$protoSlot->hasField( 'slot_origin' ) ) {
223  throw new InvalidArgumentException(
224  "A saved inherited slot should have an origin set!"
225  );
226  }
227  $origin = $protoSlot->getOrigin();
228  } else {
229  $origin = $revisionId;
230  }
231 
232  return self::newFromSlotRecord( $protoSlot, [
233  'slot_revision_id' => $revisionId,
234  'slot_content_id' => $contentId,
235  'slot_origin' => $origin,
236  'content_address' => $contentAddress,
237  ] );
238  }
239 
258  public function __construct( \stdClass $row, $content, bool $derived = false ) {
259  Assert::parameterType( 'Content|callable', $content, '$content' );
260 
261  Assert::parameter(
262  property_exists( $row, 'slot_revision_id' ),
263  '$row->slot_revision_id',
264  'must exist'
265  );
266  Assert::parameter(
267  property_exists( $row, 'slot_content_id' ),
268  '$row->slot_content_id',
269  'must exist'
270  );
271  Assert::parameter(
272  property_exists( $row, 'content_address' ),
273  '$row->content_address',
274  'must exist'
275  );
276  Assert::parameter(
277  property_exists( $row, 'model_name' ),
278  '$row->model_name',
279  'must exist'
280  );
281  Assert::parameter(
282  property_exists( $row, 'slot_origin' ),
283  '$row->slot_origin',
284  'must exist'
285  );
286  Assert::parameter(
287  !property_exists( $row, 'slot_inherited' ),
288  '$row->slot_inherited',
289  'must not exist'
290  );
291  Assert::parameter(
292  !property_exists( $row, 'slot_revision' ),
293  '$row->slot_revision',
294  'must not exist'
295  );
296 
297  $this->row = $row;
298  $this->content = $content;
299  $this->derived = $derived;
300  }
301 
317  public function getContent() {
318  if ( $this->content instanceof Content ) {
319  return $this->content;
320  }
321 
322  $obj = call_user_func( $this->content, $this );
323 
324  Assert::postcondition(
325  $obj instanceof Content,
326  'Slot content callback should return a Content object'
327  );
328 
329  $this->content = $obj;
330 
331  return $this->content;
332  }
333 
344  private function getField( $name ) {
345  if ( !isset( $this->row->$name ) ) {
346  // distinguish between unknown and uninitialized fields
347  if ( property_exists( $this->row, $name ) ) {
348  throw new IncompleteRevisionException(
349  'Uninitialized field: {name}',
350  [ 'name' => $name ]
351  );
352  } else {
353  throw new OutOfBoundsException( 'No such field: ' . $name );
354  }
355  }
356 
357  $value = $this->row->$name;
358 
359  // NOTE: allow callbacks, but don't trust plain string callables from the database!
360  if ( !is_string( $value ) && is_callable( $value ) ) {
361  $value = call_user_func( $value, $this );
362  $this->setField( $name, $value );
363  }
364 
365  return $value;
366  }
367 
377  private function getStringField( $name ) {
378  return strval( $this->getField( $name ) );
379  }
380 
390  private function getIntField( $name ) {
391  return intval( $this->getField( $name ) );
392  }
393 
398  private function hasField( $name ) {
399  if ( isset( $this->row->$name ) ) {
400  // if the field is a callback, resolve first, then re-check
401  if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
402  $this->getField( $name );
403  }
404  }
405 
406  return isset( $this->row->$name );
407  }
408 
414  public function getRevision() {
415  return $this->getIntField( 'slot_revision_id' );
416  }
417 
423  public function getOrigin() {
424  return $this->getIntField( 'slot_origin' );
425  }
426 
438  public function isInherited() {
439  if ( $this->hasRevision() ) {
440  return $this->getRevision() !== $this->getOrigin();
441  } else {
442  return $this->hasAddress();
443  }
444  }
445 
453  public function hasAddress() {
454  return $this->hasField( 'content_address' );
455  }
456 
464  public function hasOrigin() {
465  return $this->hasField( 'slot_origin' );
466  }
467 
487  public function hasContentId() {
488  return $this->hasField( 'slot_content_id' );
489  }
490 
498  public function hasRevision() {
499  return $this->hasField( 'slot_revision_id' );
500  }
501 
507  public function getRole() {
508  return $this->getStringField( 'role_name' );
509  }
510 
517  public function getAddress() {
518  return $this->getStringField( 'content_address' );
519  }
520 
531  public function getContentId() {
532  return $this->getIntField( 'slot_content_id' );
533  }
534 
540  public function getSize() {
541  try {
542  $size = $this->getIntField( 'content_size' );
543  } catch ( IncompleteRevisionException $ex ) {
544  $size = $this->getContent()->getSize();
545  $this->setField( 'content_size', $size );
546  }
547 
548  return $size;
549  }
550 
556  public function getSha1() {
557  try {
558  $sha1 = $this->getStringField( 'content_sha1' );
559  } catch ( IncompleteRevisionException $ex ) {
560  $sha1 = null;
561  }
562 
563  // Compute if missing. Missing could mean null or empty.
564  if ( $sha1 === null || $sha1 === '' ) {
565  $format = $this->hasField( 'format_name' )
566  ? $this->getStringField( 'format_name' )
567  : null;
568 
569  $data = $this->getContent()->serialize( $format );
570  $sha1 = self::base36Sha1( $data );
571  $this->setField( 'content_sha1', $sha1 );
572  }
573 
574  return $sha1;
575  }
576 
584  public function getModel() {
585  try {
586  $model = $this->getStringField( 'model_name' );
587  } catch ( IncompleteRevisionException $ex ) {
588  $model = $this->getContent()->getModel();
589  $this->setField( 'model_name', $model );
590  }
591 
592  return $model;
593  }
594 
604  public function getFormat() {
605  // XXX: we currently do not plan to store the format for each slot!
606 
607  if ( $this->hasField( 'format_name' ) ) {
608  return $this->getStringField( 'format_name' );
609  }
610 
611  return null;
612  }
613 
618  private function setField( $name, $value ) {
619  $this->row->$name = $value;
620  }
621 
630  public static function base36Sha1( $blob ) {
631  return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
632  }
633 
653  public function hasSameContent( SlotRecord $other ) {
654  if ( $other === $this ) {
655  return true;
656  }
657 
658  if ( $this->getModel() !== $other->getModel() ) {
659  return false;
660  }
661 
662  if ( $this->hasAddress()
663  && $other->hasAddress()
664  && $this->getAddress() == $other->getAddress()
665  ) {
666  return true;
667  }
668 
669  if ( $this->getSize() !== $other->getSize() ) {
670  return false;
671  }
672 
673  if ( $this->getSha1() !== $other->getSha1() ) {
674  return false;
675  }
676 
677  return true;
678  }
679 
684  public function isDerived(): bool {
685  return $this->derived;
686  }
687 
688 }
689 
694 class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
MediaWiki\Revision\SlotRecord\getFormat
getFormat()
Returns the blob serialization format as a MIME type.
Definition: SlotRecord.php:604
MediaWiki\Revision\SlotRecord\__construct
__construct(\stdClass $row, $content, bool $derived=false)
The following fields are supported by the $row parameter:
Definition: SlotRecord.php:258
MediaWiki\Revision\IncompleteRevisionException
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Definition: IncompleteRevisionException.php:32
MediaWiki\Revision\SlotRecord\getField
getField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:344
MediaWiki\Revision\SlotRecord\hasOrigin
hasOrigin()
Whether this slot has an origin (revision ID that originated the slot's content.
Definition: SlotRecord.php:464
MediaWiki\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
MediaWiki\Revision\SlotRecord\getIntField
getIntField( $name)
Returns the int value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:390
MediaWiki\Revision\SuppressedDataException
Exception raised in response to an audience check when attempting to access suppressed information wi...
Definition: SuppressedDataException.php:33
MediaWiki\Revision\SlotRecord\$row
stdClass $row
database result row, as a raw object.
Definition: SlotRecord.php:50
MediaWiki\Revision\SlotRecord\getStringField
getStringField( $name)
Returns the string value of a data field from the database row supplied to the constructor.
Definition: SlotRecord.php:377
MediaWiki\Revision\SlotRecord\newSaved
static newSaved(int $revisionId, ?int $contentId, string $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
Definition: SlotRecord.php:188
MediaWiki\Revision\SlotRecord\getSha1
getSha1()
Returns the content size.
Definition: SlotRecord.php:556
MediaWiki\Revision
Definition: ContributionsLookup.php:3
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
MediaWiki\Revision\SlotRecord\newFromSlotRecord
static newFromSlotRecord(SlotRecord $slot, array $overrides=[])
Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
Definition: SlotRecord.php:106
$blob
$blob
Definition: testCompression.php:70
MediaWiki\Revision\SlotRecord\newUnsaved
static newUnsaved(string $role, Content $content, bool $derived=false)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:155
MediaWiki\Revision\SlotRecord\setField
setField( $name, $value)
Definition: SlotRecord.php:618
MediaWiki\Revision\SlotRecord\getAddress
getAddress()
Returns the address of this slot's content.
Definition: SlotRecord.php:517
MediaWiki\Revision\SlotRecord\hasRevision
hasRevision()
Whether this slot has revision ID associated.
Definition: SlotRecord.php:498
MediaWiki\Revision\SlotRecord\hasAddress
hasAddress()
Whether this slot has an address.
Definition: SlotRecord.php:453
MediaWiki\Revision\SlotRecord\newDerived
static newDerived(string $role, Content $content)
Returns a SlotRecord for a derived slot.
Definition: SlotRecord.php:93
MediaWiki\Revision\SlotRecord\getOrigin
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
Definition: SlotRecord.php:423
MediaWiki\Revision\SlotRecord\isInherited
isInherited()
Whether this slot was inherited from an older revision.
Definition: SlotRecord.php:438
MediaWiki\Revision\SlotRecord\hasContentId
hasContentId()
Whether this slot has a content ID.
Definition: SlotRecord.php:487
MediaWiki\Revision\SlotRecord\hasSameContent
hasSameContent(SlotRecord $other)
Returns true if $other has the same content as this slot.
Definition: SlotRecord.php:653
MediaWiki\Revision\SlotRecord\getContentId
getContentId()
Returns the ID of the content meta data row associated with the slot.
Definition: SlotRecord.php:531
MediaWiki\Revision\SlotRecord\$derived
bool $derived
Definition: SlotRecord.php:60
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\Revision\SlotRecord\getSize
getSize()
Returns the content size.
Definition: SlotRecord.php:540
MediaWiki\Revision\SlotRecord\isDerived
isDerived()
Definition: SlotRecord.php:684
MediaWiki\Revision\SlotRecord\base36Sha1
static base36Sha1( $blob)
Get the base 36 SHA-1 value for a string of text.
Definition: SlotRecord.php:630
Content\getModel
getModel()
Returns the ID of the content model used by this Content object.
MediaWiki\Revision\SlotRecord\getRole
getRole()
Returns the role of the slot.
Definition: SlotRecord.php:507
MediaWiki\Revision\SlotRecord\$content
Content callable $content
Definition: SlotRecord.php:55
MediaWiki\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:129
MediaWiki\Revision\SlotRecord\hasField
hasField( $name)
Definition: SlotRecord.php:398
MediaWiki\Revision\SlotRecord\getContent
getContent()
Returns the Content of the given slot.
Definition: SlotRecord.php:317
MediaWiki\Revision\SlotRecord\getModel
getModel()
Returns the content model.
Definition: SlotRecord.php:584
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
MediaWiki\Revision\SlotRecord\getRevision
getRevision()
Returns the ID of the revision this slot is associated with.
Definition: SlotRecord.php:414