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