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