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