1<?php 2 3declare(strict_types=1); 4 5namespace Sabre\DAV; 6 7use UnexpectedValueException; 8 9/** 10 * This class represents a set of properties that are going to be updated. 11 * 12 * Usually this is simply a PROPPATCH request, but it can also be used for 13 * internal updates. 14 * 15 * Property updates must always be atomic. This means that a property update 16 * must either completely succeed, or completely fail. 17 * 18 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 19 * @author Evert Pot (http://evertpot.com/) 20 * @license http://sabre.io/license/ Modified BSD License 21 */ 22class PropPatch 23{ 24 /** 25 * Properties that are being updated. 26 * 27 * This is a key-value list. If the value is null, the property is supposed 28 * to be deleted. 29 * 30 * @var array 31 */ 32 protected $mutations; 33 34 /** 35 * A list of properties and the result of the update. The result is in the 36 * form of a HTTP status code. 37 * 38 * @var array 39 */ 40 protected $result = []; 41 42 /** 43 * This is the list of callbacks when we're performing the actual update. 44 * 45 * @var array 46 */ 47 protected $propertyUpdateCallbacks = []; 48 49 /** 50 * This property will be set to true if the operation failed. 51 * 52 * @var bool 53 */ 54 protected $failed = false; 55 56 /** 57 * Constructor. 58 * 59 * @param array $mutations A list of updates 60 */ 61 public function __construct(array $mutations) 62 { 63 $this->mutations = $mutations; 64 } 65 66 /** 67 * Call this function if you wish to handle updating certain properties. 68 * For instance, your class may be responsible for handling updates for the 69 * {DAV:}displayname property. 70 * 71 * In that case, call this method with the first argument 72 * "{DAV:}displayname" and a second argument that's a method that does the 73 * actual updating. 74 * 75 * It's possible to specify more than one property as an array. 76 * 77 * The callback must return a boolean or an it. If the result is true, the 78 * operation was considered successful. If it's false, it's consided 79 * failed. 80 * 81 * If the result is an integer, we'll use that integer as the http status 82 * code associated with the operation. 83 * 84 * @param string|string[] $properties 85 */ 86 public function handle($properties, callable $callback) 87 { 88 $usedProperties = []; 89 foreach ((array) $properties as $propertyName) { 90 if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) { 91 $usedProperties[] = $propertyName; 92 // HTTP Accepted 93 $this->result[$propertyName] = 202; 94 } 95 } 96 97 // Only registering if there's any unhandled properties. 98 if (!$usedProperties) { 99 return; 100 } 101 $this->propertyUpdateCallbacks[] = [ 102 // If the original argument to this method was a string, we need 103 // to also make sure that it stays that way, so the commit function 104 // knows how to format the arguments to the callback. 105 is_string($properties) ? $properties : $usedProperties, 106 $callback, 107 ]; 108 } 109 110 /** 111 * Call this function if you wish to handle _all_ properties that haven't 112 * been handled by anything else yet. Note that you effectively claim with 113 * this that you promise to process _all_ properties that are coming in. 114 */ 115 public function handleRemaining(callable $callback) 116 { 117 $properties = $this->getRemainingMutations(); 118 if (!$properties) { 119 // Nothing to do, don't register callback 120 return; 121 } 122 123 foreach ($properties as $propertyName) { 124 // HTTP Accepted 125 $this->result[$propertyName] = 202; 126 127 $this->propertyUpdateCallbacks[] = [ 128 $properties, 129 $callback, 130 ]; 131 } 132 } 133 134 /** 135 * Sets the result code for one or more properties. 136 * 137 * @param string|string[] $properties 138 * @param int $resultCode 139 */ 140 public function setResultCode($properties, $resultCode) 141 { 142 foreach ((array) $properties as $propertyName) { 143 $this->result[$propertyName] = $resultCode; 144 } 145 146 if ($resultCode >= 400) { 147 $this->failed = true; 148 } 149 } 150 151 /** 152 * Sets the result code for all properties that did not have a result yet. 153 * 154 * @param int $resultCode 155 */ 156 public function setRemainingResultCode($resultCode) 157 { 158 $this->setResultCode( 159 $this->getRemainingMutations(), 160 $resultCode 161 ); 162 } 163 164 /** 165 * Returns the list of properties that don't have a result code yet. 166 * 167 * This method returns a list of property names, but not its values. 168 * 169 * @return string[] 170 */ 171 public function getRemainingMutations() 172 { 173 $remaining = []; 174 foreach ($this->mutations as $propertyName => $propValue) { 175 if (!isset($this->result[$propertyName])) { 176 $remaining[] = $propertyName; 177 } 178 } 179 180 return $remaining; 181 } 182 183 /** 184 * Returns the list of properties that don't have a result code yet. 185 * 186 * This method returns list of properties and their values. 187 * 188 * @return array 189 */ 190 public function getRemainingValues() 191 { 192 $remaining = []; 193 foreach ($this->mutations as $propertyName => $propValue) { 194 if (!isset($this->result[$propertyName])) { 195 $remaining[$propertyName] = $propValue; 196 } 197 } 198 199 return $remaining; 200 } 201 202 /** 203 * Performs the actual update, and calls all callbacks. 204 * 205 * This method returns true or false depending on if the operation was 206 * successful. 207 * 208 * @return bool 209 */ 210 public function commit() 211 { 212 // First we validate if every property has a handler 213 foreach ($this->mutations as $propertyName => $value) { 214 if (!isset($this->result[$propertyName])) { 215 $this->failed = true; 216 $this->result[$propertyName] = 403; 217 } 218 } 219 220 foreach ($this->propertyUpdateCallbacks as $callbackInfo) { 221 if ($this->failed) { 222 break; 223 } 224 if (is_string($callbackInfo[0])) { 225 $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]); 226 } else { 227 $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]); 228 } 229 } 230 231 /* 232 * If anywhere in this operation updating a property failed, we must 233 * update all other properties accordingly. 234 */ 235 if ($this->failed) { 236 foreach ($this->result as $propertyName => $status) { 237 if (202 === $status) { 238 // Failed dependency 239 $this->result[$propertyName] = 424; 240 } 241 } 242 } 243 244 return !$this->failed; 245 } 246 247 /** 248 * Executes a property callback with the single-property syntax. 249 * 250 * @param string $propertyName 251 */ 252 private function doCallBackSingleProp($propertyName, callable $callback) 253 { 254 $result = $callback($this->mutations[$propertyName]); 255 if (is_bool($result)) { 256 if ($result) { 257 if (is_null($this->mutations[$propertyName])) { 258 // Delete 259 $result = 204; 260 } else { 261 // Update 262 $result = 200; 263 } 264 } else { 265 // Fail 266 $result = 403; 267 } 268 } 269 if (!is_int($result)) { 270 throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool'); 271 } 272 $this->result[$propertyName] = $result; 273 if ($result >= 400) { 274 $this->failed = true; 275 } 276 } 277 278 /** 279 * Executes a property callback with the multi-property syntax. 280 */ 281 private function doCallBackMultiProp(array $propertyList, callable $callback) 282 { 283 $argument = []; 284 foreach ($propertyList as $propertyName) { 285 $argument[$propertyName] = $this->mutations[$propertyName]; 286 } 287 288 $result = $callback($argument); 289 290 if (is_array($result)) { 291 foreach ($propertyList as $propertyName) { 292 if (!isset($result[$propertyName])) { 293 $resultCode = 500; 294 } else { 295 $resultCode = $result[$propertyName]; 296 } 297 if ($resultCode >= 400) { 298 $this->failed = true; 299 } 300 $this->result[$propertyName] = $resultCode; 301 } 302 } elseif (true === $result) { 303 // Success 304 foreach ($argument as $propertyName => $propertyValue) { 305 $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200; 306 } 307 } elseif (false === $result) { 308 // Fail :( 309 $this->failed = true; 310 foreach ($propertyList as $propertyName) { 311 $this->result[$propertyName] = 403; 312 } 313 } else { 314 throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool'); 315 } 316 } 317 318 /** 319 * Returns the result of the operation. 320 * 321 * @return array 322 */ 323 public function getResult() 324 { 325 return $this->result; 326 } 327 328 /** 329 * Returns the full list of mutations. 330 * 331 * @return array 332 */ 333 public function getMutations() 334 { 335 return $this->mutations; 336 } 337} 338