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