123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- <?php
- /**
- * Value object representing a content slot associated with a page revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
- namespace MediaWiki\Revision;
- use Content;
- use InvalidArgumentException;
- use LogicException;
- use OutOfBoundsException;
- use Wikimedia\Assert\Assert;
- /**
- * Value object representing a content slot associated with a page revision.
- * SlotRecord provides direct access to a Content object.
- * That access may be implemented through a callback.
- *
- * @since 1.31
- * @since 1.32 Renamed from MediaWiki\Storage\SlotRecord
- */
- class SlotRecord {
- const MAIN = 'main';
- /**
- * @var object database result row, as a raw object. Callbacks are supported for field values,
- * to enable on-demand emulation of these values. This is primarily intended for use
- * during schema migration.
- */
- private $row;
- /**
- * @var Content|callable
- */
- private $content;
- /**
- * Returns a new SlotRecord just like the given $slot, except that calling getContent()
- * will fail with an exception.
- *
- * @param SlotRecord $slot
- *
- * @return SlotRecord
- */
- public static function newWithSuppressedContent( SlotRecord $slot ) {
- $row = $slot->row;
- return new SlotRecord( $row, function () {
- throw new SuppressedDataException( 'Content suppressed!' );
- } );
- }
- /**
- * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
- * The slot's content cannot be overwritten.
- *
- * @param SlotRecord $slot
- * @param array $overrides
- *
- * @return SlotRecord
- */
- private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
- $row = clone $slot->row;
- $row->slot_id = null; // never copy the row ID!
- foreach ( $overrides as $key => $value ) {
- $row->$key = $value;
- }
- return new SlotRecord( $row, $slot->content );
- }
- /**
- * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
- * of a previous revision.
- *
- * Note that a SlotRecord constructed this way are intended as prototypes,
- * to be used wit newSaved(). They are incomplete, so some getters such as
- * getRevision() will fail.
- *
- * @param SlotRecord $slot
- *
- * @return SlotRecord
- */
- public static function newInherited( SlotRecord $slot ) {
- // Sanity check - we can't inherit from a Slot that's not attached to a revision.
- $slot->getRevision();
- $slot->getOrigin();
- $slot->getAddress();
- // NOTE: slot_origin and content_address are copied from $slot.
- return self::newDerived( $slot, [
- 'slot_revision_id' => null,
- ] );
- }
- /**
- * Constructs a new Slot from a Content object for a new revision.
- * This is the preferred way to construct a slot for storing Content that
- * resulted from a user edit. The slot is assumed to be not inherited.
- *
- * Note that a SlotRecord constructed this way are intended as prototypes,
- * to be used wit newSaved(). They are incomplete, so some getters such as
- * getAddress() will fail.
- *
- * @param string $role
- * @param Content $content
- *
- * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
- */
- public static function newUnsaved( $role, Content $content ) {
- Assert::parameterType( 'string', $role, '$role' );
- $row = [
- 'slot_id' => null, // not yet known
- 'slot_revision_id' => null, // not yet known
- 'slot_origin' => null, // not yet known, will be set in newSaved()
- 'content_size' => null, // compute later
- 'content_sha1' => null, // compute later
- 'slot_content_id' => null, // not yet known, will be set in newSaved()
- 'content_address' => null, // not yet known, will be set in newSaved()
- 'role_name' => $role,
- 'model_name' => $content->getModel(),
- ];
- return new SlotRecord( (object)$row, $content );
- }
- /**
- * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
- * proto-slot. This adds information that has only become available during saving,
- * particularly the revision ID, content ID and content address.
- *
- * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
- * If $protoSlot already has a revision, it must be the same.
- * @param int|null $contentId the ID of the row in the content table describing the content
- * referenced by $contentAddress (field slot_content_id).
- * If $protoSlot already has a content ID, it must be the same.
- * @param string $contentAddress the slot's content address (field content_address).
- * If $protoSlot already has an address, it must be the same.
- * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
- * revision. $protoSlot must have a content address if inherited.
- *
- * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
- */
- public static function newSaved(
- $revisionId,
- $contentId,
- $contentAddress,
- SlotRecord $protoSlot
- ) {
- Assert::parameterType( 'integer', $revisionId, '$revisionId' );
- // TODO once migration is over $contentId must be an integer
- Assert::parameterType( 'integer|null', $contentId, '$contentId' );
- Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
- if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
- throw new LogicException(
- "Mismatching revision ID $revisionId: "
- . "The slot already belongs to revision {$protoSlot->getRevision()}. "
- . "Use SlotRecord::newInherited() to re-use content between revisions."
- );
- }
- if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
- throw new LogicException(
- "Mismatching blob address $contentAddress: "
- . "The slot already has content at {$protoSlot->getAddress()}."
- );
- }
- if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
- throw new LogicException(
- "Mismatching content ID $contentId: "
- . "The slot already has content row {$protoSlot->getContentId()} associated."
- );
- }
- if ( $protoSlot->isInherited() ) {
- if ( !$protoSlot->hasAddress() ) {
- throw new InvalidArgumentException(
- "An inherited blob should have a content address!"
- );
- }
- if ( !$protoSlot->hasField( 'slot_origin' ) ) {
- throw new InvalidArgumentException(
- "A saved inherited slot should have an origin set!"
- );
- }
- $origin = $protoSlot->getOrigin();
- } else {
- $origin = $revisionId;
- }
- return self::newDerived( $protoSlot, [
- 'slot_revision_id' => $revisionId,
- 'slot_content_id' => $contentId,
- 'slot_origin' => $origin,
- 'content_address' => $contentAddress,
- ] );
- }
- /**
- * The following fields are supported by the $row parameter:
- *
- * $row->blob_data
- * $row->blob_address
- *
- * @param object $row A database row composed of fields of the slot and content tables,
- * as a raw object. Any field value can be a callback that produces the field value
- * given this SlotRecord as a parameter. However, plain strings cannot be used as
- * callbacks here, for security reasons.
- * @param Content|callable $content The content object associated with the slot, or a
- * callback that will return that Content object, given this SlotRecord as a parameter.
- */
- public function __construct( $row, $content ) {
- Assert::parameterType( 'object', $row, '$row' );
- Assert::parameterType( 'Content|callable', $content, '$content' );
- Assert::parameter(
- property_exists( $row, 'slot_revision_id' ),
- '$row->slot_revision_id',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'slot_content_id' ),
- '$row->slot_content_id',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'content_address' ),
- '$row->content_address',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'model_name' ),
- '$row->model_name',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'slot_origin' ),
- '$row->slot_origin',
- 'must exist'
- );
- Assert::parameter(
- !property_exists( $row, 'slot_inherited' ),
- '$row->slot_inherited',
- 'must not exist'
- );
- Assert::parameter(
- !property_exists( $row, 'slot_revision' ),
- '$row->slot_revision',
- 'must not exist'
- );
- $this->row = $row;
- $this->content = $content;
- }
- /**
- * Implemented to defy serialization.
- *
- * @throws LogicException always
- */
- public function __sleep() {
- throw new LogicException( __CLASS__ . ' is not serializable.' );
- }
- /**
- * Returns the Content of the given slot.
- *
- * @note This is free to load Content from whatever subsystem is necessary,
- * performing potentially expensive operations and triggering I/O-related
- * failure modes.
- *
- * @note This method does not apply audience filtering.
- *
- * @throws SuppressedDataException if access to the content is not allowed according
- * to the audience check performed by RevisionRecord::getSlot().
- *
- * @return Content The slot's content. This is a direct reference to the internal instance,
- * copy before exposing to application logic!
- */
- public function getContent() {
- if ( $this->content instanceof Content ) {
- return $this->content;
- }
- $obj = call_user_func( $this->content, $this );
- Assert::postcondition(
- $obj instanceof Content,
- 'Slot content callback should return a Content object'
- );
- $this->content = $obj;
- return $this->content;
- }
- /**
- * Returns the string value of a data field from the database row supplied to the constructor.
- * If the field was set to a callback, that callback is invoked and the result returned.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return mixed Returns the field's value, never null.
- */
- private function getField( $name ) {
- if ( !isset( $this->row->$name ) ) {
- // distinguish between unknown and uninitialized fields
- if ( property_exists( $this->row, $name ) ) {
- throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
- } else {
- throw new OutOfBoundsException( 'No such field: ' . $name );
- }
- }
- $value = $this->row->$name;
- // NOTE: allow callbacks, but don't trust plain string callables from the database!
- if ( !is_string( $value ) && is_callable( $value ) ) {
- $value = call_user_func( $value, $this );
- $this->setField( $name, $value );
- }
- return $value;
- }
- /**
- * Returns the string value of a data field from the database row supplied to the constructor.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return string Returns the string value
- */
- private function getStringField( $name ) {
- return strval( $this->getField( $name ) );
- }
- /**
- * Returns the int value of a data field from the database row supplied to the constructor.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return int Returns the int value
- */
- private function getIntField( $name ) {
- return intval( $this->getField( $name ) );
- }
- /**
- * @param string $name
- * @return bool whether this record contains the given field
- */
- private function hasField( $name ) {
- if ( isset( $this->row->$name ) ) {
- // if the field is a callback, resolve first, then re-check
- if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
- $this->getField( $name );
- }
- }
- return isset( $this->row->$name );
- }
- /**
- * Returns the ID of the revision this slot is associated with.
- *
- * @return int
- */
- public function getRevision() {
- return $this->getIntField( 'slot_revision_id' );
- }
- /**
- * Returns the revision ID of the revision that originated the slot's content.
- *
- * @return int
- */
- public function getOrigin() {
- return $this->getIntField( 'slot_origin' );
- }
- /**
- * Whether this slot was inherited from an older revision.
- *
- * If this SlotRecord is already attached to a revision, this returns true
- * if the slot's revision of origin is the same as the revision it belongs to.
- *
- * If this SlotRecord is not yet attached to a revision, this returns true
- * if the slot already has an address.
- *
- * @return bool
- */
- public function isInherited() {
- if ( $this->hasRevision() ) {
- return $this->getRevision() !== $this->getOrigin();
- } else {
- return $this->hasAddress();
- }
- }
- /**
- * Whether this slot has an address. Slots will have an address if their
- * content has been stored. While building a new revision,
- * SlotRecords will not have an address associated.
- *
- * @return bool
- */
- public function hasAddress() {
- return $this->hasField( 'content_address' );
- }
- /**
- * Whether this slot has an origin (revision ID that originated the slot's content.
- *
- * @since 1.32
- *
- * @return bool
- */
- public function hasOrigin() {
- return $this->hasField( 'slot_origin' );
- }
- /**
- * Whether this slot has a content ID. Slots will have a content ID if their
- * content has been stored in the content table. While building a new revision,
- * SlotRecords will not have an ID associated.
- *
- * Also, during schema migration, hasContentId() may return false when encountering an
- * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode.
- * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode,
- * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID
- * is used, derived from the revision's text ID.
- *
- * Note that hasContentId() returning false while hasRevision() returns true always
- * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above.
- * For an unsaved slot, both these methods would return false.
- *
- * @since 1.32
- *
- * @return bool
- */
- public function hasContentId() {
- return $this->hasField( 'slot_content_id' );
- }
- /**
- * Whether this slot has revision ID associated. Slots will have a revision ID associated
- * only if they were loaded as part of an existing revision. While building a new revision,
- * Slotrecords will not have a revision ID associated.
- *
- * @return bool
- */
- public function hasRevision() {
- return $this->hasField( 'slot_revision_id' );
- }
- /**
- * Returns the role of the slot.
- *
- * @return string
- */
- public function getRole() {
- return $this->getStringField( 'role_name' );
- }
- /**
- * Returns the address of this slot's content.
- * This address can be used with BlobStore to load the Content object.
- *
- * @return string
- */
- public function getAddress() {
- return $this->getStringField( 'content_address' );
- }
- /**
- * Returns the ID of the content meta data row associated with the slot.
- * This information should be irrelevant to application logic, it is here to allow
- * the construction of a full row for the revision table.
- *
- * Note that this method may return an emulated value during schema migration in
- * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information.
- *
- * @return int
- */
- public function getContentId() {
- return $this->getIntField( 'slot_content_id' );
- }
- /**
- * Returns the content size
- *
- * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
- */
- public function getSize() {
- try {
- $size = $this->getIntField( 'content_size' );
- } catch ( IncompleteRevisionException $ex ) {
- $size = $this->getContent()->getSize();
- $this->setField( 'content_size', $size );
- }
- return $size;
- }
- /**
- * Returns the content size
- *
- * @return string hash of the content.
- */
- public function getSha1() {
- try {
- $sha1 = $this->getStringField( 'content_sha1' );
- } catch ( IncompleteRevisionException $ex ) {
- $sha1 = null;
- }
- // Compute if missing. Missing could mean null or empty.
- if ( $sha1 === null || $sha1 === '' ) {
- $format = $this->hasField( 'format_name' )
- ? $this->getStringField( 'format_name' )
- : null;
- $data = $this->getContent()->serialize( $format );
- $sha1 = self::base36Sha1( $data );
- $this->setField( 'content_sha1', $sha1 );
- }
- return $sha1;
- }
- /**
- * Returns the content model. This is the model name that decides
- * which ContentHandler is appropriate for interpreting the
- * data of the blob referenced by the address returned by getAddress().
- *
- * @return string the content model of the content
- */
- public function getModel() {
- try {
- $model = $this->getStringField( 'model_name' );
- } catch ( IncompleteRevisionException $ex ) {
- $model = $this->getContent()->getModel();
- $this->setField( 'model_name', $model );
- }
- return $model;
- }
- /**
- * Returns the blob serialization format as a MIME type.
- *
- * @note When this method returns null, the caller is expected
- * to auto-detect the serialization format, or to rely on
- * the default format associated with the content model.
- *
- * @return string|null
- */
- public function getFormat() {
- // XXX: we currently do not plan to store the format for each slot!
- if ( $this->hasField( 'format_name' ) ) {
- return $this->getStringField( 'format_name' );
- }
- return null;
- }
- /**
- * @param string $name
- * @param string|int|null $value
- */
- private function setField( $name, $value ) {
- $this->row->$name = $value;
- }
- /**
- * Get the base 36 SHA-1 value for a string of text
- *
- * MCR migration note: this replaces Revision::base36Sha1
- *
- * @param string $blob
- * @return string
- */
- public static function base36Sha1( $blob ) {
- return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
- }
- /**
- * Returns true if $other has the same content as this slot.
- * The check is performed based on the model, address size, and hash.
- * Two slots can have the same content if they use different content addresses,
- * but if they have the same address and the same model, they have the same content.
- * Two slots can have the same content if they belong to different
- * revisions or pages.
- *
- * Note that hasSameContent() may return false even if Content::equals returns true for
- * the content of two slots. This may happen if the two slots have different serializations
- * representing equivalent Content. Such false negatives are considered acceptable. Code
- * that has to be absolutely sure the Content is really not the same if hasSameContent()
- * returns false should call getContent() and compare the Content objects directly.
- *
- * @since 1.32
- *
- * @param SlotRecord $other
- * @return bool
- */
- public function hasSameContent( SlotRecord $other ) {
- if ( $other === $this ) {
- return true;
- }
- if ( $this->getModel() !== $other->getModel() ) {
- return false;
- }
- if ( $this->hasAddress()
- && $other->hasAddress()
- && $this->getAddress() == $other->getAddress()
- ) {
- return true;
- }
- if ( $this->getSize() !== $other->getSize() ) {
- return false;
- }
- if ( $this->getSha1() !== $other->getSha1() ) {
- return false;
- }
- return true;
- }
- }
- /**
- * Retain the old class name for backwards compatibility.
- * @deprecated since 1.32
- */
- class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
|