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