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 * 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 MediaWiki\Content\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 ): 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}