Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.30% covered (success)
96.30%
52 / 54
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionSlotsUpdate
96.30% covered (success)
96.30%
52 / 54
85.71% covered (warning)
85.71%
12 / 14
31
0.00% covered (danger)
0.00%
0 / 1
 newFromRevisionSlots
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 newFromContent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getModifiedRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRemovedRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTouchedRoles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 modifySlot
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 modifyContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 removeSlot
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getModifiedSlot
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 isModifiedSlot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isRemovedSlot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSameUpdates
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 apply
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Storage;
8
9use MediaWiki\Content\Content;
10use MediaWiki\Revision\MutableRevisionSlots;
11use MediaWiki\Revision\RevisionAccessException;
12use MediaWiki\Revision\RevisionSlots;
13use MediaWiki\Revision\SlotRecord;
14
15/**
16 * Value object representing a modification of revision slots.
17 *
18 * @since 1.32
19 */
20class RevisionSlotsUpdate {
21
22    /**
23     * @var SlotRecord[] modified slots, using the slot role as the key.
24     */
25    private $modifiedSlots = [];
26
27    /**
28     * @var bool[] removed roles, stored in the keys of the array.
29     */
30    private $removedRoles = [];
31
32    /**
33     * Constructs a RevisionSlotsUpdate representing the update that turned $parentSlots
34     * into $newSlots. If $parentSlots is not given, $newSlots is assumed to come from a
35     * page's first revision.
36     *
37     * @param RevisionSlots $newSlots
38     * @param RevisionSlots|null $parentSlots
39     *
40     * @return RevisionSlotsUpdate
41     */
42    public static function newFromRevisionSlots(
43        RevisionSlots $newSlots,
44        ?RevisionSlots $parentSlots = null
45    ) {
46        $modified = $newSlots->getSlots();
47        $removed = [];
48
49        if ( $parentSlots ) {
50            foreach ( $parentSlots->getSlots() as $role => $slot ) {
51                if ( !isset( $modified[$role] ) ) {
52                    $removed[] = $role;
53                } elseif ( $slot->hasSameContent( $modified[$role] ) ) {
54                    // Unset slots that had the same content in the parent revision from $modified.
55                    unset( $modified[$role] );
56                }
57            }
58        }
59
60        return new RevisionSlotsUpdate( $modified, $removed );
61    }
62
63    /**
64     * Constructs a RevisionSlotsUpdate representing the update of $parentSlots
65     * when changing $newContent. If a slot has the same content in $newContent
66     * as in $parentSlots, that slot is considered inherited and thus omitted from
67     * the resulting RevisionSlotsUpdate.
68     *
69     * In contrast to newFromRevisionSlots(), slots in $parentSlots that are not present
70     * in $newContent are not considered removed. They are instead assumed to be inherited.
71     *
72     * @param Content[] $newContent The new content, using slot roles as array keys.
73     * @param RevisionSlots|null $parentSlots
74     *
75     * @return RevisionSlotsUpdate
76     */
77    public static function newFromContent( array $newContent, ?RevisionSlots $parentSlots = null ) {
78        $modified = [];
79
80        foreach ( $newContent as $role => $content ) {
81            $slot = SlotRecord::newUnsaved( $role, $content );
82
83            if ( $parentSlots
84                && $parentSlots->hasSlot( $role )
85                && $slot->hasSameContent( $parentSlots->getSlot( $role ) )
86            ) {
87                // Skip slots that had the same content in the parent revision from $modified.
88                continue;
89            }
90
91            $modified[$role] = $slot;
92        }
93
94        return new RevisionSlotsUpdate( $modified );
95    }
96
97    /**
98     * @param SlotRecord[] $modifiedSlots
99     * @param string[] $removedRoles
100     */
101    public function __construct( array $modifiedSlots = [], array $removedRoles = [] ) {
102        foreach ( $modifiedSlots as $slot ) {
103            $this->modifySlot( $slot );
104        }
105
106        foreach ( $removedRoles as $role ) {
107            $this->removeSlot( $role );
108        }
109    }
110
111    /**
112     * Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),
113     * and not later removed by calling removeSlot().
114     *
115     * Note that slots in modified roles may still be inherited slots. This is for instance
116     * the case when the RevisionSlotsUpdate objects represents some kind of rollback
117     * operation, in which slots that existed in an earlier revision are restored in
118     * a new revision.
119     *
120     * @return string[]
121     */
122    public function getModifiedRoles() {
123        return array_keys( $this->modifiedSlots );
124    }
125
126    /**
127     * Returns a list of removed slot roles, that is, roles removed by calling removeSlot(),
128     * and not later re-introduced by calling modifySlot().
129     *
130     * @return string[]
131     */
132    public function getRemovedRoles() {
133        return array_keys( $this->removedRoles );
134    }
135
136    /**
137     * Returns a list of all slot roles that modified or removed.
138     *
139     * @return string[]
140     */
141    public function getTouchedRoles() {
142        return array_merge( $this->getModifiedRoles(), $this->getRemovedRoles() );
143    }
144
145    /**
146     * Sets the given slot to be modified.
147     * If a slot with the same role is already present, it is replaced.
148     *
149     * The roles used with modifySlot() will be returned from getModifiedRoles(),
150     * unless overwritten with removeSlot().
151     */
152    public function modifySlot( SlotRecord $slot ) {
153        $role = $slot->getRole();
154
155        // XXX: We should perhaps require this to be an unsaved slot!
156        unset( $this->removedRoles[$role] );
157        $this->modifiedSlots[$role] = $slot;
158    }
159
160    /**
161     * Sets the content for the slot with the given role to be modified.
162     * If a slot with the same role is already present, it is replaced.
163     *
164     * @param string $role
165     * @param Content $content
166     */
167    public function modifyContent( $role, Content $content ) {
168        $slot = SlotRecord::newUnsaved( $role, $content );
169        $this->modifySlot( $slot );
170    }
171
172    /**
173     * Remove the slot for the given role, discontinue the corresponding stream.
174     *
175     * The roles used with removeSlot() will be returned from getRemovedSlots(),
176     * unless overwritten with modifySlot().
177     *
178     * @param string $role
179     */
180    public function removeSlot( $role ) {
181        unset( $this->modifiedSlots[$role] );
182        $this->removedRoles[$role] = true;
183    }
184
185    /**
186     * Returns the SlotRecord associated with the given role, if the slot with that role
187     * was modified (and not again removed).
188     *
189     * @note If the SlotRecord returned by this method returns a non-inherited slot,
190     *       the content of that slot may or may not already have PST applied. Methods
191     *       that take a RevisionSlotsUpdate as a parameter should specify whether they
192     *       expect PST to already have been applied to all slots. Inherited slots
193     *       should never have PST applied again.
194     *
195     * @param string $role The role name of the desired slot
196     *
197     * @throws RevisionAccessException if the slot does not exist or was removed.
198     * @return SlotRecord
199     */
200    public function getModifiedSlot( $role ) {
201        if ( isset( $this->modifiedSlots[$role] ) ) {
202            return $this->modifiedSlots[$role];
203        } else {
204            throw new RevisionAccessException(
205                'No such slot: {role}',
206                [ 'role' => $role ]
207            );
208        }
209    }
210
211    /**
212     * Returns whether getModifiedSlot() will return a SlotRecord for the given role.
213     *
214     * Will return true for the role names returned by getModifiedRoles(), false otherwise.
215     *
216     * @param string $role The role name of the desired slot
217     *
218     * @return bool
219     */
220    public function isModifiedSlot( $role ) {
221        return isset( $this->modifiedSlots[$role] );
222    }
223
224    /**
225     * Returns whether the given role is to be removed from the page.
226     *
227     * Will return true for the role names returned by getRemovedRoles(), false otherwise.
228     *
229     * @param string $role The role name of the desired slot
230     *
231     * @return bool
232     */
233    public function isRemovedSlot( $role ) {
234        return isset( $this->removedRoles[$role] );
235    }
236
237    /**
238     * Returns true if $other represents the same update - that is,
239     * if all methods defined by RevisionSlotsUpdate when called on $this or $other
240     * will yield the same result when called with the same parameters.
241     *
242     * SlotRecords for the same role are compared based on their model and content.
243     *
244     * @param RevisionSlotsUpdate $other
245     * @return bool
246     */
247    public function hasSameUpdates( RevisionSlotsUpdate $other ) {
248        // NOTE: use != not !==, since the order of entries is not significant!
249
250        if ( $this->getModifiedRoles() != $other->getModifiedRoles() ) {
251            return false;
252        }
253
254        if ( $this->getRemovedRoles() != $other->getRemovedRoles() ) {
255            return false;
256        }
257
258        foreach ( $this->getModifiedRoles() as $role ) {
259            $s = $this->getModifiedSlot( $role );
260            $t = $other->getModifiedSlot( $role );
261
262            if ( !$s->hasSameContent( $t ) ) {
263                return false;
264            }
265        }
266
267        return true;
268    }
269
270    /**
271     * Applies this update to the given MutableRevisionSlots, setting all modified slots,
272     * and removing all removed roles.
273     */
274    public function apply( MutableRevisionSlots $slots ) {
275        foreach ( $this->getModifiedRoles() as $role ) {
276            $slots->setSlot( $this->getModifiedSlot( $role ) );
277        }
278
279        foreach ( $this->getRemovedRoles() as $role ) {
280            $slots->removeSlot( $role );
281        }
282    }
283
284}