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