1<?php
2
3namespace Sabre\DAV;
4
5/**
6 * This class holds all the information about a PROPFIND request.
7 *
8 * It contains the type of PROPFIND request, which properties were requested
9 * and also the returned items.
10 */
11class PropFind {
12
13    /**
14     * A normal propfind
15     */
16    const NORMAL = 0;
17
18    /**
19     * An allprops request.
20     *
21     * While this was originally intended for instructing the server to really
22     * fetch every property, because it was used so often and it's so heavy
23     * this turned into a small list of default properties after a while.
24     *
25     * So 'all properties' now means a hardcoded list.
26     */
27    const ALLPROPS = 1;
28
29    /**
30     * A propname request. This just returns a list of properties that are
31     * defined on a node, without their values.
32     */
33    const PROPNAME = 2;
34
35    /**
36     * Creates the PROPFIND object
37     *
38     * @param string $path
39     * @param array $properties
40     * @param int $depth
41     * @param int $requestType
42     */
43    function __construct($path, array $properties, $depth = 0, $requestType = self::NORMAL) {
44
45        $this->path = $path;
46        $this->properties = $properties;
47        $this->depth = $depth;
48        $this->requestType = $requestType;
49
50        if ($requestType === self::ALLPROPS) {
51            $this->properties = [
52                '{DAV:}getlastmodified',
53                '{DAV:}getcontentlength',
54                '{DAV:}resourcetype',
55                '{DAV:}quota-used-bytes',
56                '{DAV:}quota-available-bytes',
57                '{DAV:}getetag',
58                '{DAV:}getcontenttype',
59            ];
60        }
61
62        foreach ($this->properties as $propertyName) {
63
64            // Seeding properties with 404's.
65            $this->result[$propertyName] = [404, null];
66
67        }
68        $this->itemsLeft = count($this->result);
69
70    }
71
72    /**
73     * Handles a specific property.
74     *
75     * This method checks whether the specified property was requested in this
76     * PROPFIND request, and if so, it will call the callback and use the
77     * return value for it's value.
78     *
79     * Example:
80     *
81     * $propFind->handle('{DAV:}displayname', function() {
82     *      return 'hello';
83     * });
84     *
85     * Note that handle will only work the first time. If null is returned, the
86     * value is ignored.
87     *
88     * It's also possible to not pass a callback, but immediately pass a value
89     *
90     * @param string $propertyName
91     * @param mixed $valueOrCallBack
92     * @return void
93     */
94    function handle($propertyName, $valueOrCallBack) {
95
96        if ($this->itemsLeft && isset($this->result[$propertyName]) && $this->result[$propertyName][0] === 404) {
97            if (is_callable($valueOrCallBack)) {
98                $value = $valueOrCallBack();
99            } else {
100                $value = $valueOrCallBack;
101            }
102            if (!is_null($value)) {
103                $this->itemsLeft--;
104                $this->result[$propertyName] = [200, $value];
105            }
106        }
107
108    }
109
110    /**
111     * Sets the value of the property
112     *
113     * If status is not supplied, the status will default to 200 for non-null
114     * properties, and 404 for null properties.
115     *
116     * @param string $propertyName
117     * @param mixed $value
118     * @param int $status
119     * @return void
120     */
121    function set($propertyName, $value, $status = null) {
122
123        if (is_null($status)) {
124            $status = is_null($value) ? 404 : 200;
125        }
126        // If this is an ALLPROPS request and the property is
127        // unknown, add it to the result; else ignore it:
128        if (!isset($this->result[$propertyName])) {
129            if ($this->requestType === self::ALLPROPS) {
130                $this->result[$propertyName] = [$status, $value];
131            }
132            return;
133        }
134        if ($status !== 404 && $this->result[$propertyName][0] === 404) {
135            $this->itemsLeft--;
136        } elseif ($status === 404 && $this->result[$propertyName][0] !== 404) {
137            $this->itemsLeft++;
138        }
139        $this->result[$propertyName] = [$status, $value];
140
141    }
142
143    /**
144     * Returns the current value for a property.
145     *
146     * @param string $propertyName
147     * @return mixed
148     */
149    function get($propertyName) {
150
151        return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null;
152
153    }
154
155    /**
156     * Returns the current status code for a property name.
157     *
158     * If the property does not appear in the list of requested properties,
159     * null will be returned.
160     *
161     * @param string $propertyName
162     * @return int|null
163     */
164    function getStatus($propertyName) {
165
166        return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null;
167
168    }
169
170    /**
171     * Updates the path for this PROPFIND.
172     *
173     * @param string $path
174     * @return void
175     */
176    function setPath($path) {
177
178        $this->path = $path;
179
180    }
181
182    /**
183     * Returns the path this PROPFIND request is for.
184     *
185     * @return string
186     */
187    function getPath() {
188
189        return $this->path;
190
191    }
192
193    /**
194     * Returns the depth of this propfind request.
195     *
196     * @return int
197     */
198    function getDepth() {
199
200        return $this->depth;
201
202    }
203
204    /**
205     * Updates the depth of this propfind request.
206     *
207     * @param int $depth
208     * @return void
209     */
210    function setDepth($depth) {
211
212        $this->depth = $depth;
213
214    }
215
216    /**
217     * Returns all propertynames that have a 404 status, and thus don't have a
218     * value yet.
219     *
220     * @return array
221     */
222    function get404Properties() {
223
224        if ($this->itemsLeft === 0) {
225            return [];
226        }
227        $result = [];
228        foreach ($this->result as $propertyName => $stuff) {
229            if ($stuff[0] === 404) {
230                $result[] = $propertyName;
231            }
232        }
233        return $result;
234
235    }
236
237    /**
238     * Returns the full list of requested properties.
239     *
240     * This returns just their names, not a status or value.
241     *
242     * @return array
243     */
244    function getRequestedProperties() {
245
246        return $this->properties;
247
248    }
249
250    /**
251     * Returns true if this was an '{DAV:}allprops' request.
252     *
253     * @return bool
254     */
255    function isAllProps() {
256
257        return $this->requestType === self::ALLPROPS;
258
259    }
260
261    /**
262     * Returns a result array that's often used in multistatus responses.
263     *
264     * The array uses status codes as keys, and property names and value pairs
265     * as the value of the top array.. such as :
266     *
267     * [
268     *  200 => [ '{DAV:}displayname' => 'foo' ],
269     * ]
270     *
271     * @return array
272     */
273    function getResultForMultiStatus() {
274
275        $r = [
276            200 => [],
277            404 => [],
278        ];
279        foreach ($this->result as $propertyName => $info) {
280            if (!isset($r[$info[0]])) {
281                $r[$info[0]] = [$propertyName => $info[1]];
282            } else {
283                $r[$info[0]][$propertyName] = $info[1];
284            }
285        }
286        // Removing the 404's for multi-status requests.
287        if ($this->requestType === self::ALLPROPS) unset($r[404]);
288        return $r;
289
290    }
291
292    /**
293     * The path that we're fetching properties for.
294     *
295     * @var string
296     */
297    protected $path;
298
299    /**
300     * The Depth of the request.
301     *
302     * 0 means only the current item. 1 means the current item + its children.
303     * It can also be DEPTH_INFINITY if this is enabled in the server.
304     *
305     * @var int
306     */
307    protected $depth = 0;
308
309    /**
310     * The type of request. See the TYPE constants
311     */
312    protected $requestType;
313
314    /**
315     * A list of requested properties
316     *
317     * @var array
318     */
319    protected $properties = [];
320
321    /**
322     * The result of the operation.
323     *
324     * The keys in this array are property names.
325     * The values are an array with two elements: the http status code and then
326     * optionally a value.
327     *
328     * Example:
329     *
330     * [
331     *    "{DAV:}owner" : [404],
332     *    "{DAV:}displayname" : [200, "Admin"]
333     * ]
334     *
335     * @var array
336     */
337    protected $result = [];
338
339    /**
340     * This is used as an internal counter for the number of properties that do
341     * not yet have a value.
342     *
343     * @var int
344     */
345    protected $itemsLeft;
346
347}
348