1<?php
2
3namespace Sabre\DAV;
4
5use UnexpectedValueException;
6
7/**
8 * This class represents a set of properties that are going to be updated.
9 *
10 * Usually this is simply a PROPPATCH request, but it can also be used for
11 * internal updates.
12 *
13 * Property updates must always be atomic. This means that a property update
14 * must either completely succeed, or completely fail.
15 *
16 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
17 * @author Evert Pot (http://evertpot.com/)
18 * @license http://sabre.io/license/ Modified BSD License
19 */
20class PropPatch {
21
22    /**
23     * Properties that are being updated.
24     *
25     * This is a key-value list. If the value is null, the property is supposed
26     * to be deleted.
27     *
28     * @var array
29     */
30    protected $mutations;
31
32    /**
33     * A list of properties and the result of the update. The result is in the
34     * form of a HTTP status code.
35     *
36     * @var array
37     */
38    protected $result = [];
39
40    /**
41     * This is the list of callbacks when we're performing the actual update.
42     *
43     * @var array
44     */
45    protected $propertyUpdateCallbacks = [];
46
47    /**
48     * This property will be set to true if the operation failed.
49     *
50     * @var bool
51     */
52    protected $failed = false;
53
54    /**
55     * Constructor
56     *
57     * @param array $mutations A list of updates
58     */
59    function __construct(array $mutations) {
60
61        $this->mutations = $mutations;
62
63    }
64
65    /**
66     * Call this function if you wish to handle updating certain properties.
67     * For instance, your class may be responsible for handling updates for the
68     * {DAV:}displayname property.
69     *
70     * In that case, call this method with the first argument
71     * "{DAV:}displayname" and a second argument that's a method that does the
72     * actual updating.
73     *
74     * It's possible to specify more than one property.
75     *
76     * @param string|string[] $properties
77     * @param callable $callback
78     * @return void
79     */
80    function handle($properties, callable $callback) {
81
82        $usedProperties = [];
83        foreach ((array)$properties as $propertyName) {
84
85            if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) {
86
87                $usedProperties[] = $propertyName;
88                // HTTP Accepted
89                $this->result[$propertyName] = 202;
90            }
91
92        }
93
94        // Only registering if there's any unhandled properties.
95        if (!$usedProperties) {
96            return;
97        }
98        $this->propertyUpdateCallbacks[] = [
99            // If the original argument to this method was a string, we need
100            // to also make sure that it stays that way, so the commit function
101            // knows how to format the arguments to the callback.
102            is_string($properties) ? $properties : $usedProperties,
103            $callback
104        ];
105
106    }
107
108    /**
109     * Call this function if you wish to handle _all_ properties that haven't
110     * been handled by anything else yet. Note that you effectively claim with
111     * this that you promise to process _all_ properties that are coming in.
112     *
113     * @param callable $callback
114     * @return void
115     */
116    function handleRemaining(callable $callback) {
117
118        $properties = $this->getRemainingMutations();
119        if (!$properties) {
120            // Nothing to do, don't register callback
121            return;
122        }
123
124        foreach ($properties as $propertyName) {
125            // HTTP Accepted
126            $this->result[$propertyName] = 202;
127
128            $this->propertyUpdateCallbacks[] = [
129                $properties,
130                $callback
131            ];
132        }
133
134    }
135
136    /**
137     * Sets the result code for one or more properties.
138     *
139     * @param string|string[] $properties
140     * @param int $resultCode
141     * @return void
142     */
143    function setResultCode($properties, $resultCode) {
144
145        foreach ((array)$properties as $propertyName) {
146            $this->result[$propertyName] = $resultCode;
147        }
148
149        if ($resultCode >= 400) {
150            $this->failed = true;
151        }
152
153    }
154
155    /**
156     * Sets the result code for all properties that did not have a result yet.
157     *
158     * @param int $resultCode
159     * @return void
160     */
161    function setRemainingResultCode($resultCode) {
162
163        $this->setResultCode(
164            $this->getRemainingMutations(),
165            $resultCode
166        );
167
168    }
169
170    /**
171     * Returns the list of properties that don't have a result code yet.
172     *
173     * This method returns a list of property names, but not its values.
174     *
175     * @return string[]
176     */
177    function getRemainingMutations() {
178
179        $remaining = [];
180        foreach ($this->mutations as $propertyName => $propValue) {
181            if (!isset($this->result[$propertyName])) {
182                $remaining[] = $propertyName;
183            }
184        }
185
186        return $remaining;
187
188    }
189
190    /**
191     * Returns the list of properties that don't have a result code yet.
192     *
193     * This method returns list of properties and their values.
194     *
195     * @return array
196     */
197    function getRemainingValues() {
198
199        $remaining = [];
200        foreach ($this->mutations as $propertyName => $propValue) {
201            if (!isset($this->result[$propertyName])) {
202                $remaining[$propertyName] = $propValue;
203            }
204        }
205
206        return $remaining;
207
208    }
209
210    /**
211     * Performs the actual update, and calls all callbacks.
212     *
213     * This method returns true or false depending on if the operation was
214     * successful.
215     *
216     * @return bool
217     */
218    function commit() {
219
220        // First we validate if every property has a handler
221        foreach ($this->mutations as $propertyName => $value) {
222
223            if (!isset($this->result[$propertyName])) {
224                $this->failed = true;
225                $this->result[$propertyName] = 403;
226            }
227
228        }
229
230        foreach ($this->propertyUpdateCallbacks as $callbackInfo) {
231
232            if ($this->failed) {
233                break;
234            }
235            if (is_string($callbackInfo[0])) {
236                $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]);
237            } else {
238                $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]);
239            }
240
241        }
242
243        /**
244         * If anywhere in this operation updating a property failed, we must
245         * update all other properties accordingly.
246         */
247        if ($this->failed) {
248
249            foreach ($this->result as $propertyName => $status) {
250                if ($status === 202) {
251                    // Failed dependency
252                    $this->result[$propertyName] = 424;
253                }
254            }
255
256        }
257
258        return !$this->failed;
259
260    }
261
262    /**
263     * Executes a property callback with the single-property syntax.
264     *
265     * @param string $propertyName
266     * @param callable $callback
267     * @return void
268     */
269    private function doCallBackSingleProp($propertyName, callable $callback) {
270
271        $result = $callback($this->mutations[$propertyName]);
272        if (is_bool($result)) {
273            if ($result) {
274                if (is_null($this->mutations[$propertyName])) {
275                    // Delete
276                    $result = 204;
277                } else {
278                    // Update
279                    $result = 200;
280                }
281            } else {
282                // Fail
283                $result = 403;
284            }
285        }
286        if (!is_int($result)) {
287            throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool');
288        }
289        $this->result[$propertyName] = $result;
290        if ($result >= 400) {
291            $this->failed = true;
292        }
293
294    }
295
296    /**
297     * Executes a property callback with the multi-property syntax.
298     *
299     * @param array $propertyList
300     * @param callable $callback
301     * @return void
302     */
303    private function doCallBackMultiProp(array $propertyList, callable $callback) {
304
305        $argument = [];
306        foreach ($propertyList as $propertyName) {
307            $argument[$propertyName] = $this->mutations[$propertyName];
308        }
309
310        $result = $callback($argument);
311
312        if (is_array($result)) {
313            foreach ($propertyList as $propertyName) {
314                if (!isset($result[$propertyName])) {
315                    $resultCode = 500;
316                } else {
317                    $resultCode = $result[$propertyName];
318                }
319                if ($resultCode >= 400) {
320                    $this->failed = true;
321                }
322                $this->result[$propertyName] = $resultCode;
323
324            }
325        } elseif ($result === true) {
326
327            // Success
328            foreach ($argument as $propertyName => $propertyValue) {
329                $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200;
330            }
331
332        } elseif ($result === false) {
333            // Fail :(
334            $this->failed = true;
335            foreach ($propertyList as $propertyName) {
336                $this->result[$propertyName] = 403;
337            }
338        } else {
339            throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool');
340        }
341
342    }
343
344    /**
345     * Returns the result of the operation.
346     *
347     * @return array
348     */
349    function getResult() {
350
351        return $this->result;
352
353    }
354
355    /**
356     * Returns the full list of mutations
357     *
358     * @return array
359     */
360    function getMutations() {
361
362        return $this->mutations;
363
364    }
365
366}
367