Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.91% |
80 / 88 |
|
78.57% |
11 / 14 |
CRAP | |
0.00% |
0 / 1 |
RevisionSlots | |
90.91% |
80 / 88 |
|
78.57% |
11 / 14 |
27.55 | |
0.00% |
0 / 1 |
__construct | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
setSlotsInternal | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSlot | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
hasSlot | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getSlotRoles | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
computeSize | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getSlots | |
25.00% |
2 / 8 |
|
0.00% |
0 / 1 |
3.69 | |||
computeSha1 | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
getOriginalSlots | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getInheritedSlots | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getPrimarySlots | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
hasSameContent | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
getRolesWithDifferentContent | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 |
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\Revision; |
24 | |
25 | use MediaWiki\Content\Content; |
26 | use Wikimedia\Assert\Assert; |
27 | use 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 | */ |
41 | class 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 ): void { |
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 ): Content { |
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 ): SlotRecord { |
109 | $slots = $this->getSlots(); |
110 | |
111 | if ( isset( $slots[$role] ) ) { |
112 | return $slots[$role]; |
113 | } else { |
114 | throw new RevisionAccessException( |
115 | 'No such slot: {role}', |
116 | [ 'role' => $role ] |
117 | ); |
118 | } |
119 | } |
120 | |
121 | /** |
122 | * Returns whether the given slot is set. |
123 | * |
124 | * @param string $role The role name of the desired slot |
125 | * |
126 | * @return bool |
127 | */ |
128 | public function hasSlot( $role ): bool { |
129 | $slots = $this->getSlots(); |
130 | |
131 | return isset( $slots[$role] ); |
132 | } |
133 | |
134 | /** |
135 | * Returns the slot names (roles) of all slots present in this revision. |
136 | * getContent() will succeed only for the names returned by this method. |
137 | * |
138 | * @return string[] |
139 | */ |
140 | public function getSlotRoles(): array { |
141 | $slots = $this->getSlots(); |
142 | return array_keys( $slots ); |
143 | } |
144 | |
145 | /** |
146 | * Computes the total nominal size of the revision's slots, in bogo-bytes. |
147 | * |
148 | * @warning This is potentially expensive! It may cause some slots' content to be loaded |
149 | * and deserialized. |
150 | * |
151 | * @return int |
152 | */ |
153 | public function computeSize(): int { |
154 | return array_reduce( $this->getPrimarySlots(), static function ( $accu, SlotRecord $slot ) { |
155 | return $accu + $slot->getSize(); |
156 | }, 0 ); |
157 | } |
158 | |
159 | /** |
160 | * Returns an associative array that maps role names to SlotRecords. Each SlotRecord |
161 | * represents the content meta-data of a slot, together they define the content of |
162 | * a revision. |
163 | * |
164 | * @note This may cause the content meta-data for the revision to be lazy-loaded. |
165 | * |
166 | * @return SlotRecord[] revision slot/content rows, keyed by slot role name. |
167 | */ |
168 | public function getSlots(): array { |
169 | if ( is_callable( $this->slots ) ) { |
170 | $slots = call_user_func( $this->slots ); |
171 | |
172 | Assert::postcondition( |
173 | is_array( $slots ), |
174 | 'Slots info callback should return an array of objects' |
175 | ); |
176 | |
177 | $this->setSlotsInternal( $slots ); |
178 | } |
179 | |
180 | return $this->slots; |
181 | } |
182 | |
183 | /** |
184 | * Computes the combined hash of the revisions's slots. |
185 | * |
186 | * @note For backwards compatibility, the combined hash of a single slot |
187 | * is that slot's hash. For consistency, the combined hash of an empty set of slots |
188 | * is the hash of the empty string. |
189 | * |
190 | * @warning This is potentially expensive! It may cause some slots' content to be loaded |
191 | * and deserialized, then re-serialized and hashed. |
192 | * |
193 | * @return string |
194 | */ |
195 | public function computeSha1(): string { |
196 | $slots = $this->getPrimarySlots(); |
197 | ksort( $slots ); |
198 | |
199 | if ( !$slots ) { |
200 | return SlotRecord::base36Sha1( '' ); |
201 | } |
202 | |
203 | return array_reduce( $slots, static function ( $accu, SlotRecord $slot ) { |
204 | return $accu === null |
205 | ? $slot->getSha1() |
206 | : SlotRecord::base36Sha1( $accu . $slot->getSha1() ); |
207 | }, null ); |
208 | } |
209 | |
210 | /** |
211 | * Return all slots that belong to the revision they originate from (that is, |
212 | * they are not inherited from some other revision). |
213 | * |
214 | * @note This may cause the slot meta-data for the revision to be lazy-loaded. |
215 | * |
216 | * @return SlotRecord[] |
217 | */ |
218 | public function getOriginalSlots(): array { |
219 | return array_filter( |
220 | $this->getSlots(), |
221 | static function ( SlotRecord $slot ) { |
222 | return !$slot->isInherited(); |
223 | } |
224 | ); |
225 | } |
226 | |
227 | /** |
228 | * Return all slots that are not originate in the revision they belong to (that is, |
229 | * they are inherited from some other revision). |
230 | * |
231 | * @note This may cause the slot meta-data for the revision to be lazy-loaded. |
232 | * |
233 | * @return SlotRecord[] |
234 | */ |
235 | public function getInheritedSlots(): array { |
236 | return array_filter( |
237 | $this->getSlots(), |
238 | static function ( SlotRecord $slot ) { |
239 | return $slot->isInherited(); |
240 | } |
241 | ); |
242 | } |
243 | |
244 | /** |
245 | * Return all primary slots (those that are not derived). |
246 | * |
247 | * @return SlotRecord[] |
248 | * @since 1.36 |
249 | */ |
250 | public function getPrimarySlots(): array { |
251 | return array_filter( |
252 | $this->getSlots(), |
253 | static function ( SlotRecord $slot ) { |
254 | return !$slot->isDerived(); |
255 | } |
256 | ); |
257 | } |
258 | |
259 | /** |
260 | * Checks whether the other RevisionSlots instance has the same content |
261 | * as this instance. Note that this does not mean that the slots have to be the same: |
262 | * they could for instance belong to different revisions. |
263 | * |
264 | * @param RevisionSlots $other |
265 | * |
266 | * @return bool |
267 | */ |
268 | public function hasSameContent( RevisionSlots $other ): bool { |
269 | if ( $other === $this ) { |
270 | return true; |
271 | } |
272 | |
273 | $aSlots = $this->getSlots(); |
274 | $bSlots = $other->getSlots(); |
275 | |
276 | ksort( $aSlots ); |
277 | ksort( $bSlots ); |
278 | |
279 | if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) { |
280 | return false; |
281 | } |
282 | |
283 | foreach ( $aSlots as $role => $s ) { |
284 | $t = $bSlots[$role]; |
285 | |
286 | if ( !$s->hasSameContent( $t ) ) { |
287 | return false; |
288 | } |
289 | } |
290 | |
291 | return true; |
292 | } |
293 | |
294 | /** |
295 | * Find roles for which the $other RevisionSlots object has different content |
296 | * as this RevisionSlots object, including any roles that are present in one |
297 | * but not the other. |
298 | * |
299 | * @param RevisionSlots $other |
300 | * |
301 | * @return string[] a list of slot roles that are different. |
302 | */ |
303 | public function getRolesWithDifferentContent( RevisionSlots $other ): array { |
304 | if ( $other === $this ) { |
305 | return []; |
306 | } |
307 | |
308 | $aSlots = $this->getSlots(); |
309 | $bSlots = $other->getSlots(); |
310 | |
311 | ksort( $aSlots ); |
312 | ksort( $bSlots ); |
313 | |
314 | $different = array_keys( array_merge( |
315 | array_diff_key( $aSlots, $bSlots ), |
316 | array_diff_key( $bSlots, $aSlots ) |
317 | ) ); |
318 | |
319 | /** @var SlotRecord[] $common */ |
320 | $common = array_intersect_key( $aSlots, $bSlots ); |
321 | |
322 | foreach ( $common as $role => $s ) { |
323 | $t = $bSlots[$role]; |
324 | |
325 | if ( !$s->hasSameContent( $t ) ) { |
326 | $different[] = $role; |
327 | } |
328 | } |
329 | |
330 | return $different; |
331 | } |
332 | |
333 | } |