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