1<?php 2 3namespace Sabre\DAV; 4 5use Sabre\HTTP\URLUtil; 6 7/** 8 * The tree object is responsible for basic tree operations. 9 * 10 * It allows for fetching nodes by path, facilitates deleting, copying and 11 * moving. 12 * 13 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 14 * @author Evert Pot (http://evertpot.com/) 15 * @license http://sabre.io/license/ Modified BSD License 16 */ 17class Tree { 18 19 /** 20 * The root node 21 * 22 * @var ICollection 23 */ 24 protected $rootNode; 25 26 /** 27 * This is the node cache. Accessed nodes are stored here. 28 * Arrays keys are path names, values are the actual nodes. 29 * 30 * @var array 31 */ 32 protected $cache = []; 33 34 /** 35 * Creates the object 36 * 37 * This method expects the rootObject to be passed as a parameter 38 * 39 * @param ICollection $rootNode 40 */ 41 function __construct(ICollection $rootNode) { 42 43 $this->rootNode = $rootNode; 44 45 } 46 47 /** 48 * Returns the INode object for the requested path 49 * 50 * @param string $path 51 * @return INode 52 */ 53 function getNodeForPath($path) { 54 55 $path = trim($path, '/'); 56 if (isset($this->cache[$path])) return $this->cache[$path]; 57 58 // Is it the root node? 59 if (!strlen($path)) { 60 return $this->rootNode; 61 } 62 63 // Attempting to fetch its parent 64 list($parentName, $baseName) = URLUtil::splitPath($path); 65 66 // If there was no parent, we must simply ask it from the root node. 67 if ($parentName === "") { 68 $node = $this->rootNode->getChild($baseName); 69 } else { 70 // Otherwise, we recursively grab the parent and ask him/her. 71 $parent = $this->getNodeForPath($parentName); 72 73 if (!($parent instanceof ICollection)) 74 throw new Exception\NotFound('Could not find node at path: ' . $path); 75 76 $node = $parent->getChild($baseName); 77 78 } 79 80 $this->cache[$path] = $node; 81 return $node; 82 83 } 84 85 /** 86 * This function allows you to check if a node exists. 87 * 88 * Implementors of this class should override this method to make 89 * it cheaper. 90 * 91 * @param string $path 92 * @return bool 93 */ 94 function nodeExists($path) { 95 96 try { 97 98 // The root always exists 99 if ($path === '') return true; 100 101 list($parent, $base) = URLUtil::splitPath($path); 102 103 $parentNode = $this->getNodeForPath($parent); 104 if (!$parentNode instanceof ICollection) return false; 105 return $parentNode->childExists($base); 106 107 } catch (Exception\NotFound $e) { 108 109 return false; 110 111 } 112 113 } 114 115 /** 116 * Copies a file from path to another 117 * 118 * @param string $sourcePath The source location 119 * @param string $destinationPath The full destination path 120 * @return void 121 */ 122 function copy($sourcePath, $destinationPath) { 123 124 $sourceNode = $this->getNodeForPath($sourcePath); 125 126 // grab the dirname and basename components 127 list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath); 128 129 $destinationParent = $this->getNodeForPath($destinationDir); 130 $this->copyNode($sourceNode, $destinationParent, $destinationName); 131 132 $this->markDirty($destinationDir); 133 134 } 135 136 /** 137 * Moves a file from one location to another 138 * 139 * @param string $sourcePath The path to the file which should be moved 140 * @param string $destinationPath The full destination path, so not just the destination parent node 141 * @return int 142 */ 143 function move($sourcePath, $destinationPath) { 144 145 list($sourceDir) = URLUtil::splitPath($sourcePath); 146 list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath); 147 148 if ($sourceDir === $destinationDir) { 149 // If this is a 'local' rename, it means we can just trigger a rename. 150 $sourceNode = $this->getNodeForPath($sourcePath); 151 $sourceNode->setName($destinationName); 152 } else { 153 $newParentNode = $this->getNodeForPath($destinationDir); 154 $moveSuccess = false; 155 if ($newParentNode instanceof IMoveTarget) { 156 // The target collection may be able to handle the move 157 $sourceNode = $this->getNodeForPath($sourcePath); 158 $moveSuccess = $newParentNode->moveInto($destinationName, $sourcePath, $sourceNode); 159 } 160 if (!$moveSuccess) { 161 $this->copy($sourcePath, $destinationPath); 162 $this->getNodeForPath($sourcePath)->delete(); 163 } 164 } 165 $this->markDirty($sourceDir); 166 $this->markDirty($destinationDir); 167 168 } 169 170 /** 171 * Deletes a node from the tree 172 * 173 * @param string $path 174 * @return void 175 */ 176 function delete($path) { 177 178 $node = $this->getNodeForPath($path); 179 $node->delete(); 180 181 list($parent) = URLUtil::splitPath($path); 182 $this->markDirty($parent); 183 184 } 185 186 /** 187 * Returns a list of childnodes for a given path. 188 * 189 * @param string $path 190 * @return array 191 */ 192 function getChildren($path) { 193 194 $node = $this->getNodeForPath($path); 195 $children = $node->getChildren(); 196 $basePath = trim($path, '/') . '/'; 197 foreach ($children as $child) { 198 199 $this->cache[$basePath . $child->getName()] = $child; 200 201 } 202 return $children; 203 204 } 205 206 /** 207 * This method is called with every tree update 208 * 209 * Examples of tree updates are: 210 * * node deletions 211 * * node creations 212 * * copy 213 * * move 214 * * renaming nodes 215 * 216 * If Tree classes implement a form of caching, this will allow 217 * them to make sure caches will be expired. 218 * 219 * If a path is passed, it is assumed that the entire subtree is dirty 220 * 221 * @param string $path 222 * @return void 223 */ 224 function markDirty($path) { 225 226 // We don't care enough about sub-paths 227 // flushing the entire cache 228 $path = trim($path, '/'); 229 foreach ($this->cache as $nodePath => $node) { 230 if ($nodePath == $path || strpos($nodePath, $path . '/') === 0) 231 unset($this->cache[$nodePath]); 232 233 } 234 235 } 236 237 /** 238 * This method tells the tree system to pre-fetch and cache a list of 239 * children of a single parent. 240 * 241 * There are a bunch of operations in the WebDAV stack that request many 242 * children (based on uris), and sometimes fetching many at once can 243 * optimize this. 244 * 245 * This method returns an array with the found nodes. It's keys are the 246 * original paths. The result may be out of order. 247 * 248 * @param array $paths List of nodes that must be fetched. 249 * @return array 250 */ 251 function getMultipleNodes($paths) { 252 253 // Finding common parents 254 $parents = []; 255 foreach ($paths as $path) { 256 list($parent, $node) = URLUtil::splitPath($path); 257 if (!isset($parents[$parent])) { 258 $parents[$parent] = [$node]; 259 } else { 260 $parents[$parent][] = $node; 261 } 262 } 263 264 $result = []; 265 266 foreach ($parents as $parent => $children) { 267 268 $parentNode = $this->getNodeForPath($parent); 269 if ($parentNode instanceof IMultiGet) { 270 foreach ($parentNode->getMultipleChildren($children) as $childNode) { 271 $fullPath = $parent . '/' . $childNode->getName(); 272 $result[$fullPath] = $childNode; 273 $this->cache[$fullPath] = $childNode; 274 } 275 } else { 276 foreach ($children as $child) { 277 $fullPath = $parent . '/' . $child; 278 $result[$fullPath] = $this->getNodeForPath($fullPath); 279 } 280 } 281 282 } 283 284 return $result; 285 286 } 287 288 289 /** 290 * copyNode 291 * 292 * @param INode $source 293 * @param ICollection $destinationParent 294 * @param string $destinationName 295 * @return void 296 */ 297 protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null) { 298 299 if (!$destinationName) $destinationName = $source->getName(); 300 301 if ($source instanceof IFile) { 302 303 $data = $source->get(); 304 305 // If the body was a string, we need to convert it to a stream 306 if (is_string($data)) { 307 $stream = fopen('php://temp', 'r+'); 308 fwrite($stream, $data); 309 rewind($stream); 310 $data = $stream; 311 } 312 $destinationParent->createFile($destinationName, $data); 313 $destination = $destinationParent->getChild($destinationName); 314 315 } elseif ($source instanceof ICollection) { 316 317 $destinationParent->createDirectory($destinationName); 318 319 $destination = $destinationParent->getChild($destinationName); 320 foreach ($source->getChildren() as $child) { 321 322 $this->copyNode($child, $destination); 323 324 } 325 326 } 327 if ($source instanceof IProperties && $destination instanceof IProperties) { 328 329 $props = $source->getProperties([]); 330 $propPatch = new PropPatch($props); 331 $destination->propPatch($propPatch); 332 $propPatch->commit(); 333 334 } 335 336 } 337 338} 339