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