1<?php
2/**
3 * Value object representing the set of slots belonging to a 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
23namespace MediaWiki\Revision;
24
25use Content;
26use Wikimedia\Assert\Assert;
27use Wikimedia\NonSerializable\NonSerializableTrait;
28
29/**
30 * Value object representing the set of slots belonging to a revision.
31 *
32 * @note RevisionSlots provides "raw" access to the slots and does not apply audience checks.
33 * If audience checks are desired, use RevisionRecord::getSlot() or RevisionRecord::getContent()
34 * instead.
35 *
36 * @newable
37 *
38 * @since 1.31
39 * @since 1.32 Renamed from MediaWiki\Storage\RevisionSlots
40 */
41class RevisionSlots {
42	use NonSerializableTrait;
43
44	/** @var SlotRecord[]|callable */
45	protected $slots;
46
47	/**
48	 * @stable to call.
49	 *
50	 * @param SlotRecord[]|callable $slots SlotRecords,
51	 *        or a callback that returns such a structure.
52	 */
53	public function __construct( $slots ) {
54		Assert::parameterType( 'array|callable', $slots, '$slots' );
55
56		if ( is_callable( $slots ) ) {
57			$this->slots = $slots;
58		} else {
59			$this->setSlotsInternal( $slots );
60		}
61	}
62
63	/**
64	 * @param SlotRecord[] $slots
65	 */
66	private function setSlotsInternal( array $slots ) {
67		Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
68
69		$this->slots = [];
70
71		// re-key the slot array
72		foreach ( $slots as $slot ) {
73			$role = $slot->getRole();
74			$this->slots[$role] = $slot;
75		}
76	}
77
78	/**
79	 * Returns the Content of the given slot.
80	 * Call getSlotNames() to get a list of available slots.
81	 *
82	 * Note that for mutable Content objects, each call to this method will return a
83	 * fresh clone.
84	 *
85	 * @see SlotRecord::getContent()
86	 *
87	 * @param string $role The role name of the desired slot
88	 *
89	 * @throws RevisionAccessException if the slot does not exist or slot data
90	 *        could not be lazy-loaded. See SlotRecord::getContent() for details.
91	 * @return Content
92	 */
93	public function getContent( $role ) {
94		// Return a copy to be safe. Immutable content objects return $this from copy().
95		return $this->getSlot( $role )->getContent()->copy();
96	}
97
98	/**
99	 * Returns the SlotRecord of the given slot.
100	 * Call getSlotNames() to get a list of available slots.
101	 *
102	 * @param string $role The role name of the desired slot
103	 *
104	 * @throws RevisionAccessException if the slot does not exist or slot data
105	 *        could not be lazy-loaded.
106	 * @return SlotRecord
107	 */
108	public function getSlot( $role ) {
109		$slots = $this->getSlots();
110
111		if ( isset( $slots[$role] ) ) {
112			return $slots[$role];
113		} else {
114			throw new RevisionAccessException( 'No such slot: ' . $role );
115		}
116	}
117
118	/**
119	 * Returns whether the given slot is set.
120	 *
121	 * @param string $role The role name of the desired slot
122	 *
123	 * @return bool
124	 */
125	public function hasSlot( $role ) {
126		$slots = $this->getSlots();
127
128		return isset( $slots[$role] );
129	}
130
131	/**
132	 * Returns the slot names (roles) of all slots present in this revision.
133	 * getContent() will succeed only for the names returned by this method.
134	 *
135	 * @return string[]
136	 */
137	public function getSlotRoles() {
138		$slots = $this->getSlots();
139		return array_keys( $slots );
140	}
141
142	/**
143	 * Computes the total nominal size of the revision's slots, in bogo-bytes.
144	 *
145	 * @warning This is potentially expensive! It may cause some slots' content to be loaded
146	 * and deserialized.
147	 *
148	 * @return int
149	 */
150	public function computeSize() {
151		return array_reduce( $this->getPrimarySlots(), static function ( $accu, SlotRecord $slot ) {
152			return $accu + $slot->getSize();
153		}, 0 );
154	}
155
156	/**
157	 * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
158	 * represents the content meta-data of a slot, together they define the content of
159	 * a revision.
160	 *
161	 * @note This may cause the content meta-data for the revision to be lazy-loaded.
162	 *
163	 * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
164	 */
165	public function getSlots() {
166		if ( is_callable( $this->slots ) ) {
167			$slots = call_user_func( $this->slots );
168
169			Assert::postcondition(
170				is_array( $slots ),
171				'Slots info callback should return an array of objects'
172			);
173
174			$this->setSlotsInternal( $slots );
175		}
176
177		return $this->slots;
178	}
179
180	/**
181	 * Computes the combined hash of the revisions's slots.
182	 *
183	 * @note For backwards compatibility, the combined hash of a single slot
184	 * is that slot's hash. For consistency, the combined hash of an empty set of slots
185	 * is the hash of the empty string.
186	 *
187	 * @warning This is potentially expensive! It may cause some slots' content to be loaded
188	 * and deserialized, then re-serialized and hashed.
189	 *
190	 * @return string
191	 */
192	public function computeSha1() {
193		$slots = $this->getPrimarySlots();
194		ksort( $slots );
195
196		if ( empty( $slots ) ) {
197			return SlotRecord::base36Sha1( '' );
198		}
199
200		return array_reduce( $slots, static function ( $accu, SlotRecord $slot ) {
201			return $accu === null
202				? $slot->getSha1()
203				: SlotRecord::base36Sha1( $accu . $slot->getSha1() );
204		}, null );
205	}
206
207	/**
208	 * Return all slots that belong to the revision they originate from (that is,
209	 * they are not inherited from some other revision).
210	 *
211	 * @note This may cause the slot meta-data for the revision to be lazy-loaded.
212	 *
213	 * @return SlotRecord[]
214	 */
215	public function getOriginalSlots() {
216		return array_filter(
217			$this->getSlots(),
218			static function ( SlotRecord $slot ) {
219				return !$slot->isInherited();
220			}
221		);
222	}
223
224	/**
225	 * Return all slots that are not originate in the revision they belong to (that is,
226	 * they are inherited from some other revision).
227	 *
228	 * @note This may cause the slot meta-data for the revision to be lazy-loaded.
229	 *
230	 * @return SlotRecord[]
231	 */
232	public function getInheritedSlots() {
233		return array_filter(
234			$this->getSlots(),
235			static function ( SlotRecord $slot ) {
236				return $slot->isInherited();
237			}
238		);
239	}
240
241	/**
242	 * Return all primary slots (those that are not derived).
243	 *
244	 * @return SlotRecord[]
245	 * @since 1.36
246	 */
247	public function getPrimarySlots() : array {
248		return array_filter(
249			$this->getSlots(),
250			static function ( SlotRecord $slot ) {
251				return !$slot->isDerived();
252			}
253		);
254	}
255
256	/**
257	 * Checks whether the other RevisionSlots instance has the same content
258	 * as this instance. Note that this does not mean that the slots have to be the same:
259	 * they could for instance belong to different revisions.
260	 *
261	 * @param RevisionSlots $other
262	 *
263	 * @return bool
264	 */
265	public function hasSameContent( RevisionSlots $other ) {
266		if ( $other === $this ) {
267			return true;
268		}
269
270		$aSlots = $this->getSlots();
271		$bSlots = $other->getSlots();
272
273		ksort( $aSlots );
274		ksort( $bSlots );
275
276		if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
277			return false;
278		}
279
280		foreach ( $aSlots as $role => $s ) {
281			$t = $bSlots[$role];
282
283			if ( !$s->hasSameContent( $t ) ) {
284				return false;
285			}
286		}
287
288		return true;
289	}
290
291	/**
292	 * Find roles for which the $other RevisionSlots object has different content
293	 * as this RevisionSlots object, including any roles that are present in one
294	 * but not the other.
295	 *
296	 * @param RevisionSlots $other
297	 *
298	 * @return string[] a list of slot roles that are different.
299	 */
300	public function getRolesWithDifferentContent( RevisionSlots $other ) {
301		if ( $other === $this ) {
302			return [];
303		}
304
305		$aSlots = $this->getSlots();
306		$bSlots = $other->getSlots();
307
308		ksort( $aSlots );
309		ksort( $bSlots );
310
311		$different = array_keys( array_merge(
312			array_diff_key( $aSlots, $bSlots ),
313			array_diff_key( $bSlots, $aSlots )
314		) );
315
316		/** @var SlotRecord[] $common */
317		$common = array_intersect_key( $aSlots, $bSlots );
318
319		foreach ( $common as $role => $s ) {
320			$t = $bSlots[$role];
321
322			if ( !$s->hasSameContent( $t ) ) {
323				$different[] = $role;
324			}
325		}
326
327		return $different;
328	}
329
330}
331
332/**
333 * Retain the old class name for backwards compatibility.
334 * @deprecated since 1.32
335 */
336class_alias( RevisionSlots::class, 'MediaWiki\Storage\RevisionSlots' );
337