1<?php
2/**
3 * @author Thomas Müller <thomas.mueller@tmit.eu>
4 *
5 * @copyright Copyright (c) 2018, ownCloud GmbH
6 * @license AGPL-3.0
7 *
8 * This code is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License, version 3,
10 * as published by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License, version 3,
18 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19 *
20 */
21
22namespace OCA\DAV\Files;
23
24use OCA\DAV\Connector\Sabre\Node;
25use OCA\DAV\Files\PublicFiles\PublicSharedRootNode;
26use OCA\DAV\Files\PublicFiles\SharedFile;
27use OCA\DAV\Files\PublicFiles\SharedFolder;
28use OCP\Files\NotFoundException;
29use OCP\Files\Storage\IPersistentLockingStorage;
30use OCP\Lock\Persistent\ILock;
31use Sabre\DAV\Exception\NotFound;
32use Sabre\DAV\Locks;
33use Sabre\DAV\Locks\Backend\BackendInterface;
34use Sabre\DAV\Tree;
35use OCP\AppFramework\Utility\ITimeFactory;
36use OCA\DAV\Connector\Sabre\ObjectTree;
37use OCA\DAV\Connector\Sabre\Exception\Forbidden;
38
39class FileLocksBackend implements BackendInterface {
40
41	/** @var Tree */
42	private $tree;
43	/** @var bool */
44	private $useV1;
45	/** @var ITimeFactory */
46	private $timeFactory;
47	/** @var bool */
48	private $isPublicEndpoint;
49
50	public function __construct($tree, $useV1, $timeFactory, $isPublicEndpoint = false) {
51		$this->tree = $tree;
52		$this->useV1 = $useV1;
53		$this->timeFactory = $timeFactory;
54		$this->isPublicEndpoint = $isPublicEndpoint;
55	}
56
57	/**
58	 * Returns a list of Sabre\DAV\Locks\LockInfo objects
59	 *
60	 * This method should return all the locks for a particular uri, including
61	 * locks that might be set on a parent uri.
62	 *
63	 * If returnChildLocks is set to true, this method should also look for
64	 * any locks in the subtree of the uri for locks.
65	 *
66	 * @param string $uri
67	 * @param bool $returnChildLocks
68	 * @return array
69	 */
70	public function getLocks($uri, $returnChildLocks) {
71		try {
72			$node = $this->tree->getNodeForPath($uri);
73
74			if ($node instanceof SharedFile || $node instanceof SharedFolder) {
75				$node = $node->getNode();
76			} elseif ($node instanceof PublicSharedRootNode) {
77				try {
78					$node = $node->getShare()->getNode();
79				} catch (NotFoundException $e) {
80					return [];
81				}
82			} elseif (!$node instanceof Node) {
83				return [];
84			}
85
86			$storage = $node->getFileInfo()->getStorage();
87			if (!$storage->instanceOfStorage(IPersistentLockingStorage::class)) {
88				return [];
89			}
90
91			/** @var IPersistentLockingStorage $storage */
92			'@phan-var IPersistentLockingStorage $storage';
93			$locks = $storage->getLocks($node->getFileInfo()->getInternalPath(), $returnChildLocks);
94		} catch (NotFound $e) {
95			if ($uri === '') {
96				// no more parents
97				return [];
98			}
99
100			// get parent storage and check for locks on the target path
101			list($parentPath, $childPath) = \Sabre\Uri\split($uri);
102
103			try {
104				$node = $this->tree->getNodeForPath($parentPath);
105			} catch (NotFound $e) {
106				return [];
107			}
108
109			if ($node instanceof SharedFile || $node instanceof SharedFolder) {
110				$node = $node->getNode();
111			} elseif ($node instanceof PublicSharedRootNode) {
112				try {
113					$node = $node->getShare()->getNode();
114				} catch (NotFoundException $e) {
115					return [];
116				}
117			} elseif (!$node instanceof Node) {
118				return [];
119			}
120
121			// use storage of parent
122			$storage = $node->getFileInfo()->getStorage();
123			if (!$storage->instanceOfStorage(IPersistentLockingStorage::class)) {
124				return [];
125			}
126
127			/** @var IPersistentLockingStorage $storage */
128			'@phan-var IPersistentLockingStorage $storage';
129			$locks = $storage->getLocks($node->getFileInfo()->getInternalPath() . '/' . $childPath, $returnChildLocks);
130		}
131
132		$davLocks = [];
133		foreach ($locks as $lock) {
134			$lockInfo = new Locks\LockInfo();
135			$fileName = $lock->getAbsoluteDavPath();
136			$uid = $lock->getDavUserId();
137
138			$pathInView = "/$uid/files/$fileName";
139
140			// v1 object tree, also used by public link
141			if ($this->tree instanceof ObjectTree) {
142				// get path relative to the view root, which can
143				// either be the user's home or a public link share's root
144				$subPath = $this->tree->getView()->getRelativePath($pathInView);
145				if ($subPath === null) {
146					// path is above the current view, just tell that the lock is on the root then
147					$lockInfo->uri = '';
148				} else {
149					$subPath = \ltrim($subPath, '/');
150					$lockInfo->uri = $subPath;
151				}
152			} else {
153				if ($this->useV1) {
154					$lockInfo->uri = $fileName;
155				} else {
156					$lockInfo->uri = "files/$uid/$fileName";
157				}
158			}
159
160			if (!$this->isPublicEndpoint) {
161				$lockInfo->token = $lock->getToken();
162				$lockInfo->owner = $lock->getOwner();
163			}
164			$lockInfo->created = $lock->getCreatedAt();
165			$lockInfo->depth = $lock->getDepth();
166			if ($lock->getScope() === ILock::LOCK_SCOPE_EXCLUSIVE) {
167				$lockInfo->scope = Locks\LockInfo::EXCLUSIVE;
168			} else {
169				$lockInfo->scope = Locks\LockInfo::SHARED;
170			}
171			$lockInfo->timeout = $lock->getTimeout() - ($this->timeFactory->getTime() - $lock->getCreatedAt());
172
173			$davLocks[] = $lockInfo;
174		}
175		return $davLocks;
176	}
177
178	/**
179	 * Locks a uri
180	 *
181	 * @param string $uri
182	 * @param Locks\LockInfo $lockInfo
183	 * @return bool
184	 */
185	public function lock($uri, Locks\LockInfo $lockInfo) {
186		if ($this->isPublicEndpoint) {
187			throw new Forbidden('Forbidden to lock from public endpoint');
188		}
189		try {
190			$node = $this->tree->getNodeForPath($uri);
191		} catch (NotFound $e) {
192			return false;
193		}
194		if (!$node instanceof Node) {
195			return false;
196		}
197
198		$storage = $node->getFileInfo()->getStorage();
199		if (!$storage->instanceOfStorage(IPersistentLockingStorage::class)) {
200			return false;
201		}
202
203		/** @var IPersistentLockingStorage $storage */
204		'@phan-var IPersistentLockingStorage $storage';
205		$lock = $storage->lockNodePersistent($node->getFileInfo()->getInternalPath(), [
206			'token' => $lockInfo->token,
207			'scope' => $lockInfo->scope === Locks\LockInfo::EXCLUSIVE ? ILock::LOCK_SCOPE_EXCLUSIVE : ILock::LOCK_SCOPE_SHARED,
208			'depth' => $lockInfo->depth,
209			'owner' => $lockInfo->owner,
210			'timeout' => $lockInfo->timeout
211		]);
212
213		// in case the timeout has not been accepted, adjust in lock info
214		$lockInfo->timeout = $lock->getTimeout();
215		$lockInfo->owner = $lock->getOwner();
216
217		return !empty($lock);
218	}
219
220	/**
221	 * Removes a lock from a uri
222	 *
223	 * @param string $uri
224	 * @param Locks\LockInfo $lockInfo
225	 * @return bool
226	 */
227	public function unlock($uri, Locks\LockInfo $lockInfo) {
228		if ($this->isPublicEndpoint) {
229			throw new Forbidden('Forbidden to unlock from public endpoint');
230		}
231		try {
232			$node = $this->tree->getNodeForPath($uri);
233		} catch (NotFound $e) {
234			return false;
235		}
236		if (!$node instanceof Node) {
237			return false;
238		}
239
240		$storage = $node->getFileInfo()->getStorage();
241		if (!$storage->instanceOfStorage(IPersistentLockingStorage::class)) {
242			return false;
243		}
244
245		/** @var IPersistentLockingStorage $storage */
246		'@phan-var IPersistentLockingStorage $storage';
247		return $storage->unlockNodePersistent($node->getFileInfo()->getInternalPath(), [
248			'token' => $lockInfo->token
249		]);
250	}
251}
252