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) 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 if ($basePath !== '') $basePath .= '/'; 198 199 foreach ($children as $child) { 200 201 $this->cache[$basePath . $child->getName()] = $child; 202 203 } 204 return $children; 205 206 } 207 208 /** 209 * This method is called with every tree update 210 * 211 * Examples of tree updates are: 212 * * node deletions 213 * * node creations 214 * * copy 215 * * move 216 * * renaming nodes 217 * 218 * If Tree classes implement a form of caching, this will allow 219 * them to make sure caches will be expired. 220 * 221 * If a path is passed, it is assumed that the entire subtree is dirty 222 * 223 * @param string $path 224 * @return void 225 */ 226 function markDirty($path) { 227 228 // We don't care enough about sub-paths 229 // flushing the entire cache 230 $path = trim($path, '/'); 231 foreach ($this->cache as $nodePath => $node) { 232 if ($path === '' || $nodePath == $path || strpos($nodePath, $path . '/') === 0) 233 unset($this->cache[$nodePath]); 234 235 } 236 237 } 238 239 /** 240 * This method tells the tree system to pre-fetch and cache a list of 241 * children of a single parent. 242 * 243 * There are a bunch of operations in the WebDAV stack that request many 244 * children (based on uris), and sometimes fetching many at once can 245 * optimize this. 246 * 247 * This method returns an array with the found nodes. It's keys are the 248 * original paths. The result may be out of order. 249 * 250 * @param array $paths List of nodes that must be fetched. 251 * @return array 252 */ 253 function getMultipleNodes($paths) { 254 255 // Finding common parents 256 $parents = []; 257 foreach ($paths as $path) { 258 list($parent, $node) = URLUtil::splitPath($path); 259 if (!isset($parents[$parent])) { 260 $parents[$parent] = [$node]; 261 } else { 262 $parents[$parent][] = $node; 263 } 264 } 265 266 $result = []; 267 268 foreach ($parents as $parent => $children) { 269 270 $parentNode = $this->getNodeForPath($parent); 271 if ($parentNode instanceof IMultiGet) { 272 foreach ($parentNode->getMultipleChildren($children) as $childNode) { 273 $fullPath = $parent . '/' . $childNode->getName(); 274 $result[$fullPath] = $childNode; 275 $this->cache[$fullPath] = $childNode; 276 } 277 } else { 278 foreach ($children as $child) { 279 $fullPath = $parent . '/' . $child; 280 $result[$fullPath] = $this->getNodeForPath($fullPath); 281 } 282 } 283 284 } 285 286 return $result; 287 288 } 289 290 291 /** 292 * copyNode 293 * 294 * @param INode $source 295 * @param ICollection $destinationParent 296 * @param string $destinationName 297 * @return void 298 */ 299 protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null) { 300 301 if ((string)$destinationName === '') { 302 $destinationName = $source->getName(); 303 } 304 305 if ($source instanceof IFile) { 306 307 $data = $source->get(); 308 309 // If the body was a string, we need to convert it to a stream 310 if (is_string($data)) { 311 $stream = fopen('php://temp', 'r+'); 312 fwrite($stream, $data); 313 rewind($stream); 314 $data = $stream; 315 } 316 $destinationParent->createFile($destinationName, $data); 317 $destination = $destinationParent->getChild($destinationName); 318 319 } elseif ($source instanceof ICollection) { 320 321 $destinationParent->createDirectory($destinationName); 322 323 $destination = $destinationParent->getChild($destinationName); 324 foreach ($source->getChildren() as $child) { 325 326 $this->copyNode($child, $destination); 327 328 } 329 330 } 331 if ($source instanceof IProperties && $destination instanceof IProperties) { 332 333 $props = $source->getProperties([]); 334 $propPatch = new PropPatch($props); 335 $destination->propPatch($propPatch); 336 $propPatch->commit(); 337 338 } 339 340 } 341 342} 343