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) 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 as an array.
75     *
76     * The callback must return a boolean or an it. If the result is true, the
77     * operation was considered successful. If it's false, it's consided
78     * failed.
79     *
80     * If the result is an integer, we'll use that integer as the http status
81     * code associated with the operation.
82     *
83     * @param string|string[] $properties
84     * @param callable $callback
85     * @return void
86     */
87    function handle($properties, callable $callback) {
88
89        $usedProperties = [];
90        foreach ((array)$properties as $propertyName) {
91
92            if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) {
93
94                $usedProperties[] = $propertyName;
95                // HTTP Accepted
96                $this->result[$propertyName] = 202;
97            }
98
99        }
100
101        // Only registering if there's any unhandled properties.
102        if (!$usedProperties) {
103            return;
104        }
105        $this->propertyUpdateCallbacks[] = [
106            // If the original argument to this method was a string, we need
107            // to also make sure that it stays that way, so the commit function
108            // knows how to format the arguments to the callback.
109            is_string($properties) ? $properties : $usedProperties,
110            $callback
111        ];
112
113    }
114
115    /**
116     * Call this function if you wish to handle _all_ properties that haven't
117     * been handled by anything else yet. Note that you effectively claim with
118     * this that you promise to process _all_ properties that are coming in.
119     *
120     * @param callable $callback
121     * @return void
122     */
123    function handleRemaining(callable $callback) {
124
125        $properties = $this->getRemainingMutations();
126        if (!$properties) {
127            // Nothing to do, don't register callback
128            return;
129        }
130
131        foreach ($properties as $propertyName) {
132            // HTTP Accepted
133            $this->result[$propertyName] = 202;
134
135            $this->propertyUpdateCallbacks[] = [
136                $properties,
137                $callback
138            ];
139        }
140
141    }
142
143    /**
144     * Sets the result code for one or more properties.
145     *
146     * @param string|string[] $properties
147     * @param int $resultCode
148     * @return void
149     */
150    function setResultCode($properties, $resultCode) {
151
152        foreach ((array)$properties as $propertyName) {
153            $this->result[$propertyName] = $resultCode;
154        }
155
156        if ($resultCode >= 400) {
157            $this->failed = true;
158        }
159
160    }
161
162    /**
163     * Sets the result code for all properties that did not have a result yet.
164     *
165     * @param int $resultCode
166     * @return void
167     */
168    function setRemainingResultCode($resultCode) {
169
170        $this->setResultCode(
171            $this->getRemainingMutations(),
172            $resultCode
173        );
174
175    }
176
177    /**
178     * Returns the list of properties that don't have a result code yet.
179     *
180     * This method returns a list of property names, but not its values.
181     *
182     * @return string[]
183     */
184    function getRemainingMutations() {
185
186        $remaining = [];
187        foreach ($this->mutations as $propertyName => $propValue) {
188            if (!isset($this->result[$propertyName])) {
189                $remaining[] = $propertyName;
190            }
191        }
192
193        return $remaining;
194
195    }
196
197    /**
198     * Returns the list of properties that don't have a result code yet.
199     *
200     * This method returns list of properties and their values.
201     *
202     * @return array
203     */
204    function getRemainingValues() {
205
206        $remaining = [];
207        foreach ($this->mutations as $propertyName => $propValue) {
208            if (!isset($this->result[$propertyName])) {
209                $remaining[$propertyName] = $propValue;
210            }
211        }
212
213        return $remaining;
214
215    }
216
217    /**
218     * Performs the actual update, and calls all callbacks.
219     *
220     * This method returns true or false depending on if the operation was
221     * successful.
222     *
223     * @return bool
224     */
225    function commit() {
226
227        // First we validate if every property has a handler
228        foreach ($this->mutations as $propertyName => $value) {
229
230            if (!isset($this->result[$propertyName])) {
231                $this->failed = true;
232                $this->result[$propertyName] = 403;
233            }
234
235        }
236
237        foreach ($this->propertyUpdateCallbacks as $callbackInfo) {
238
239            if ($this->failed) {
240                break;
241            }
242            if (is_string($callbackInfo[0])) {
243                $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]);
244            } else {
245                $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]);
246            }
247
248        }
249
250        /**
251         * If anywhere in this operation updating a property failed, we must
252         * update all other properties accordingly.
253         */
254        if ($this->failed) {
255
256            foreach ($this->result as $propertyName => $status) {
257                if ($status === 202) {
258                    // Failed dependency
259                    $this->result[$propertyName] = 424;
260                }
261            }
262
263        }
264
265        return !$this->failed;
266
267    }
268
269    /**
270     * Executes a property callback with the single-property syntax.
271     *
272     * @param string $propertyName
273     * @param callable $callback
274     * @return void
275     */
276    private function doCallBackSingleProp($propertyName, callable $callback) {
277
278        $result = $callback($this->mutations[$propertyName]);
279        if (is_bool($result)) {
280            if ($result) {
281                if (is_null($this->mutations[$propertyName])) {
282                    // Delete
283                    $result = 204;
284                } else {
285                    // Update
286                    $result = 200;
287                }
288            } else {
289                // Fail
290                $result = 403;
291            }
292        }
293        if (!is_int($result)) {
294            throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool');
295        }
296        $this->result[$propertyName] = $result;
297        if ($result >= 400) {
298            $this->failed = true;
299        }
300
301    }
302
303    /**
304     * Executes a property callback with the multi-property syntax.
305     *
306     * @param array $propertyList
307     * @param callable $callback
308     * @return void
309     */
310    private function doCallBackMultiProp(array $propertyList, callable $callback) {
311
312        $argument = [];
313        foreach ($propertyList as $propertyName) {
314            $argument[$propertyName] = $this->mutations[$propertyName];
315        }
316
317        $result = $callback($argument);
318
319        if (is_array($result)) {
320            foreach ($propertyList as $propertyName) {
321                if (!isset($result[$propertyName])) {
322                    $resultCode = 500;
323                } else {
324                    $resultCode = $result[$propertyName];
325                }
326                if ($resultCode >= 400) {
327                    $this->failed = true;
328                }
329                $this->result[$propertyName] = $resultCode;
330
331            }
332        } elseif ($result === true) {
333
334            // Success
335            foreach ($argument as $propertyName => $propertyValue) {
336                $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200;
337            }
338
339        } elseif ($result === false) {
340            // Fail :(
341            $this->failed = true;
342            foreach ($propertyList as $propertyName) {
343                $this->result[$propertyName] = 403;
344            }
345        } else {
346            throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool');
347        }
348
349    }
350
351    /**
352     * Returns the result of the operation.
353     *
354     * @return array
355     */
356    function getResult() {
357
358        return $this->result;
359
360    }
361
362    /**
363     * Returns the full list of mutations
364     *
365     * @return array
366     */
367    function getMutations() {
368
369        return $this->mutations;
370
371    }
372
373}
374