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