Merge "Chinese Conversion Table Update 2017-6"
[lhc/web/wiklou.git] / includes / Storage / SlotRecord.php
1 <?php
2 /**
3 * Value object representing a content slot associated with a page revision.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 */
22
23 namespace MediaWiki\Storage;
24
25 use Content;
26 use LogicException;
27 use OutOfBoundsException;
28 use Wikimedia\Assert\Assert;
29
30 /**
31 * Value object representing a content slot associated with a page revision.
32 * SlotRecord provides direct access to a Content object.
33 * That access may be implemented through a callback.
34 *
35 * @since 1.31
36 */
37 class SlotRecord {
38
39 /**
40 * @var object database result row, as a raw object
41 */
42 private $row;
43
44 /**
45 * @var Content|callable
46 */
47 private $content;
48
49 /**
50 * Returns a new SlotRecord just like the given $slot, except that calling getContent()
51 * will fail with an exception.
52 *
53 * @param SlotRecord $slot
54 *
55 * @return SlotRecord
56 */
57 public static function newWithSuppressedContent( SlotRecord $slot ) {
58 $row = $slot->row;
59
60 return new SlotRecord( $row, function () {
61 throw new SuppressedDataException( 'Content suppressed!' );
62 } );
63 }
64
65 /**
66 * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
67 * The slot's content cannot be overwritten.
68 *
69 * @param SlotRecord $slot
70 * @param array $overrides
71 *
72 * @return SlotRecord
73 */
74 private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
75 $row = $slot->row;
76
77 foreach ( $overrides as $key => $value ) {
78 $row->$key = $value;
79 }
80
81 return new SlotRecord( $row, $slot->content );
82 }
83
84 /**
85 * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
86 * of a previous revision.
87 *
88 * @param SlotRecord $slot
89 *
90 * @return SlotRecord
91 */
92 public static function newInherited( SlotRecord $slot ) {
93 return self::newDerived( $slot, [
94 'slot_inherited' => true,
95 'slot_revision' => null,
96 ] );
97 }
98
99 /**
100 * Constructs a new Slot from a Content object for a new revision.
101 * This is the preferred way to construct a slot for storing Content that
102 * resulted from a user edit.
103 *
104 * @param string $role
105 * @param Content $content
106 * @param bool $inherited
107 *
108 * @return SlotRecord
109 */
110 public static function newUnsaved( $role, Content $content, $inherited = false ) {
111 Assert::parameterType( 'boolean', $inherited, '$inherited' );
112 Assert::parameterType( 'string', $role, '$role' );
113
114 $row = [
115 'slot_id' => null, // not yet known
116 'slot_address' => null, // not yet known. need setter?
117 'slot_revision' => null, // not yet known
118 'slot_inherited' => $inherited,
119 'cont_size' => null, // compute later
120 'cont_sha1' => null, // compute later
121 'role_name' => $role,
122 'model_name' => $content->getModel(),
123 ];
124
125 return new SlotRecord( (object)$row, $content );
126 }
127
128 /**
129 * Constructs a SlotRecord for a newly saved revision, based on the proto-slot that was
130 * supplied to the code that performed the save operation. This adds information that
131 * has only become available during saving, particularly the revision ID and blob address.
132 *
133 * @param int $revisionId
134 * @param string $blobAddress
135 * @param SlotRecord $protoSlot The proto-slot that was provided to the code that then
136 *
137 * @return SlotRecord
138 */
139 public static function newSaved( $revisionId, $blobAddress, SlotRecord $protoSlot ) {
140 Assert::parameterType( 'integer', $revisionId, '$revisionId' );
141 Assert::parameterType( 'string', $blobAddress, '$blobAddress' );
142
143 return self::newDerived( $protoSlot, [
144 'slot_revision' => $revisionId,
145 'cont_address' => $blobAddress,
146 ] );
147 }
148
149 /**
150 * SlotRecord constructor.
151 *
152 * The following fields are supported by the $row parameter:
153 *
154 * $row->blob_data
155 * $row->blob_address
156 *
157 * @param object $row A database row composed of fields of the slot and content tables,
158 * as a raw object. Any field value can be a callback that produces the field value
159 * given this SlotRecord as a parameter. However, plain strings cannot be used as
160 * callbacks here, for security reasons.
161 * @param Content|callable $content The content object associated with the slot, or a
162 * callback that will return that Content object, given this SlotRecord as a parameter.
163 */
164 public function __construct( $row, $content ) {
165 Assert::parameterType( 'object', $row, '$row' );
166 Assert::parameterType( 'Content|callable', $content, '$content' );
167
168 $this->row = $row;
169 $this->content = $content;
170 }
171
172 /**
173 * Implemented to defy serialization.
174 *
175 * @throws LogicException always
176 */
177 public function __sleep() {
178 throw new LogicException( __CLASS__ . ' is not serializable.' );
179 }
180
181 /**
182 * Returns the Content of the given slot.
183 *
184 * @note This is free to load Content from whatever subsystem is necessary,
185 * performing potentially expensive operations and triggering I/O-related
186 * failure modes.
187 *
188 * @note This method does not apply audience filtering.
189 *
190 * @throws SuppressedDataException if access to the content is not allowed according
191 * to the audience check performed by RevisionRecord::getSlot().
192 *
193 * @return Content The slot's content. This is a direct reference to the internal instance,
194 * copy before exposing to application logic!
195 */
196 public function getContent() {
197 if ( $this->content instanceof Content ) {
198 return $this->content;
199 }
200
201 $obj = call_user_func( $this->content, $this );
202
203 Assert::postcondition(
204 $obj instanceof Content,
205 'Slot content callback should return a Content object'
206 );
207
208 $this->content = $obj;
209
210 return $this->content;
211 }
212
213 /**
214 * Returns the string value of a data field from the database row supplied to the constructor.
215 * If the field was set to a callback, that callback is invoked and the result returned.
216 *
217 * @param string $name
218 *
219 * @throws OutOfBoundsException
220 * @return mixed Returns the field's value, or null if the field is NULL in the DB row.
221 */
222 private function getField( $name ) {
223 if ( !isset( $this->row->$name ) ) {
224 // distinguish between unknown and uninitialized fields
225 if ( property_exists( $this->row, $name ) ) {
226 throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
227 } else {
228 throw new OutOfBoundsException( 'No such field: ' . $name );
229 }
230 }
231
232 $value = $this->row->$name;
233
234 // NOTE: allow callbacks, but don't trust plain string callables from the database!
235 if ( !is_string( $value ) && is_callable( $value ) ) {
236 $value = call_user_func( $value, $this );
237 $this->setField( $name, $value );
238 }
239
240 return $value;
241 }
242
243 /**
244 * Returns the string value of a data field from the database row supplied to the constructor.
245 *
246 * @param string $name
247 *
248 * @throws OutOfBoundsException
249 * @throws IncompleteRevisionException
250 * @return string Returns the string value
251 */
252 private function getStringField( $name ) {
253 return strval( $this->getField( $name ) );
254 }
255
256 /**
257 * Returns the int value of a data field from the database row supplied to the constructor.
258 *
259 * @param string $name
260 *
261 * @throws OutOfBoundsException
262 * @throws IncompleteRevisionException
263 * @return int Returns the int value
264 */
265 private function getIntField( $name ) {
266 return intval( $this->getField( $name ) );
267 }
268
269 /**
270 * @param string $name
271 * @return bool whether this record contains the given field
272 */
273 private function hasField( $name ) {
274 return isset( $this->row->$name );
275 }
276
277 /**
278 * Returns the ID of the revision this slot is associated with.
279 *
280 * @return int
281 */
282 public function getRevision() {
283 return $this->getIntField( 'slot_revision' );
284 }
285
286 /**
287 * Whether this slot was inherited from an older revision.
288 *
289 * @return bool
290 */
291 public function isInherited() {
292 return $this->getIntField( 'slot_inherited' ) !== 0;
293 }
294
295 /**
296 * Whether this slot has an address. Slots will have an address if their
297 * content has been stored. While building a new revision,
298 * SlotRecords will not have an address associated.
299 *
300 * @return bool
301 */
302 public function hasAddress() {
303 return $this->hasField( 'cont_address' );
304 }
305
306 /**
307 * Whether this slot has revision ID associated. Slots will have a revision ID associated
308 * only if they were loaded as part of an existing revision. While building a new revision,
309 * Slotrecords will not have a revision ID associated.
310 *
311 * @return bool
312 */
313 public function hasRevision() {
314 return $this->hasField( 'slot_revision' );
315 }
316
317 /**
318 * Returns the role of the slot.
319 *
320 * @return string
321 */
322 public function getRole() {
323 return $this->getStringField( 'role_name' );
324 }
325
326 /**
327 * Returns the address of this slot's content.
328 * This address can be used with BlobStore to load the Content object.
329 *
330 * @return string
331 */
332 public function getAddress() {
333 return $this->getStringField( 'cont_address' );
334 }
335
336 /**
337 * Returns the content size
338 *
339 * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
340 */
341 public function getSize() {
342 try {
343 $size = $this->getIntField( 'cont_size' );
344 } catch ( IncompleteRevisionException $ex ) {
345 $size = $this->getContent()->getSize();
346 $this->setField( 'cont_size', $size );
347 }
348
349 return $size;
350 }
351
352 /**
353 * Returns the content size
354 *
355 * @return string hash of the content.
356 */
357 public function getSha1() {
358 try {
359 $sha1 = $this->getStringField( 'cont_sha1' );
360 } catch ( IncompleteRevisionException $ex ) {
361 $format = $this->hasField( 'format_name' )
362 ? $this->getStringField( 'format_name' )
363 : null;
364
365 $data = $this->getContent()->serialize( $format );
366 $sha1 = self::base36Sha1( $data );
367 $this->setField( 'cont_sha1', $sha1 );
368 }
369
370 return $sha1;
371 }
372
373 /**
374 * Returns the content model. This is the model name that decides
375 * which ContentHandler is appropriate for interpreting the
376 * data of the blob referenced by the address returned by getAddress().
377 *
378 * @return string the content model of the content
379 */
380 public function getModel() {
381 try {
382 $model = $this->getStringField( 'model_name' );
383 } catch ( IncompleteRevisionException $ex ) {
384 $model = $this->getContent()->getModel();
385 $this->setField( 'model_name', $model );
386 }
387
388 return $model;
389 }
390
391 /**
392 * Returns the blob serialization format as a MIME type.
393 *
394 * @note When this method returns null, the caller is expected
395 * to auto-detect the serialization format, or to rely on
396 * the default format associated with the content model.
397 *
398 * @return string|null
399 */
400 public function getFormat() {
401 // XXX: we currently do not plan to store the format for each slot!
402
403 if ( $this->hasField( 'format_name' ) ) {
404 return $this->getStringField( 'format_name' );
405 }
406
407 return null;
408 }
409
410 /**
411 * @param string $name
412 * @param string|int|null $value
413 */
414 private function setField( $name, $value ) {
415 $this->row->$name = $value;
416 }
417
418 /**
419 * Get the base 36 SHA-1 value for a string of text
420 *
421 * MCR migration note: this replaces Revision::base36Sha1
422 *
423 * @param string $blob
424 * @return string
425 */
426 public static function base36Sha1( $blob ) {
427 return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
428 }
429
430 }