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