1<?php
2/**
3 * @author Björn Schießle <bjoern@schiessle.org>
4 * @author Morris Jobke <hey@morrisjobke.de>
5 * @author Robin Appelman <icewind@owncloud.com>
6 * @author Thomas Müller <thomas.mueller@tmit.eu>
7 * @author Vincent Petry <pvince81@owncloud.com>
8 *
9 * @copyright Copyright (c) 2018, ownCloud GmbH
10 * @license AGPL-3.0
11 *
12 * This code is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU Affero General Public License, version 3,
14 * as published by the Free Software Foundation.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU Affero General Public License for more details.
20 *
21 * You should have received a copy of the GNU Affero General Public License, version 3,
22 * along with this program.  If not, see <http://www.gnu.org/licenses/>
23 *
24 */
25
26namespace OCA\Files_Trashbin;
27
28use OC\Files\Filesystem;
29use OC\Files\Storage\Wrapper\Wrapper;
30use OC\Files\View;
31use OCP\IUserManager;
32
33class Storage extends Wrapper {
34	private $mountPoint;
35	// remember already deleted files to avoid infinite loops if the trash bin
36	// move files across storages
37	private $deletedFiles = [];
38
39	/**
40	 * Disable trash logic
41	 *
42	 * @var bool
43	 */
44	private static $disableTrash = false;
45
46	/** @var  IUserManager */
47	private $userManager;
48
49	/** @var  TrashbinSkipChecker */
50	private $trashbinSkipChecker;
51
52	public function __construct($parameters, IUserManager $userManager = null, TrashbinSkipChecker $trashbinSkipChecker = null) {
53		$this->mountPoint = $parameters['mountPoint'];
54		$this->userManager = $userManager;
55		$this->trashbinSkipChecker = $trashbinSkipChecker;
56		parent::__construct($parameters);
57	}
58
59	/**
60	 * @internal
61	 */
62	public static function preRenameHook($params) {
63		// in cross-storage cases, a rename is a copy + unlink,
64		// that last unlink must not go to trash
65		self::$disableTrash = true;
66
67		$path1 = $params[Filesystem::signal_param_oldpath];
68		$path2 = $params[Filesystem::signal_param_newpath];
69
70		$view = Filesystem::getView();
71		$absolutePath1 = Filesystem::normalizePath($view->getAbsolutePath($path1));
72
73		$mount1 = $view->getMount($path1);
74		$mount2 = $view->getMount($path2);
75		$sourceStorage = $mount1->getStorage();
76		$targetStorage = $mount2->getStorage();
77		$sourceInternalPath = $mount1->getInternalPath($absolutePath1);
78		// check whether this is a cross-storage move from a *local* shared storage
79		if ($sourceInternalPath !== '' && $sourceStorage !== $targetStorage && $sourceStorage->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
80			'@phan-var \OCA\Files_Sharing\SharedStorage $sourceStorage';
81			$ownerPath = $sourceStorage->getSourcePath($sourceInternalPath);
82			$owner = $sourceStorage->getOwner($sourceInternalPath);
83			if ($owner !== null && $owner !== '' && $ownerPath !== null && \substr($ownerPath, 0, 6) === 'files/') {
84				// ownerPath is in the format "files/path/to/file.txt", strip "files"
85				$ownerPath = \substr($ownerPath, 6);
86
87				// make a backup copy for the owner
88				\OCA\Files_Trashbin\Trashbin::copyBackupForOwner($ownerPath, $owner, \time());
89			}
90		}
91	}
92
93	/**
94	 * @internal
95	 */
96	public static function postRenameHook($params) {
97		self::$disableTrash = false;
98	}
99
100	/**
101	 * Rename path1 to path2 by calling the wrapped storage.
102	 *
103	 * @param string $path1 first path
104	 * @param string $path2 second path
105	 */
106	public function rename($path1, $path2) {
107		$result = $this->storage->rename($path1, $path2);
108		if ($result === false) {
109			// when rename failed, the post_rename hook isn't triggered,
110			// but we still want to reenable the trash logic
111			self::$disableTrash = false;
112		}
113		return $result;
114	}
115
116	/**
117	 * Deletes the given file by moving it into the trashbin.
118	 *
119	 * @param string $path path of file or folder to delete
120	 *
121	 * @return bool true if the operation succeeded, false otherwise
122	 */
123	public function unlink($path) {
124		return $this->doDelete($path, 'unlink');
125	}
126
127	/**
128	 * Deletes the given folder by moving it into the trashbin.
129	 *
130	 * @param string $path path of folder to delete
131	 *
132	 * @return bool true if the operation succeeded, false otherwise
133	 */
134	public function rmdir($path) {
135		return $this->doDelete($path, 'rmdir');
136	}
137
138	/**
139	 * check if it is a file located in data/user/files only files in the
140	 * 'files' directory should be moved to the trash
141	 *
142	 * @param $path
143	 * @return bool
144	 */
145	protected function shouldMoveToTrash($path) {
146		$normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path);
147		$parts = \explode('/', $normalized);
148		if (\count($parts) < 4) {
149			return false;
150		}
151
152		if ($this->userManager->userExists($parts[1]) && $parts[2] == 'files') {
153			return true;
154		}
155
156		return false;
157	}
158
159	/**
160	 * Run the delete operation with the given method
161	 *
162	 * @param string $path path of file or folder to delete
163	 * @param string $method either "unlink" or "rmdir"
164	 *
165	 * @return bool true if the operation succeeded, false otherwise
166	 */
167	private function doDelete($path, $method) {
168		if (self::$disableTrash
169			|| !\OC_App::isEnabled('files_trashbin')
170			|| (\pathinfo($path, PATHINFO_EXTENSION) === 'part')
171			|| $this->shouldMoveToTrash($path) === false
172		) {
173			return \call_user_func_array([$this->storage, $method], [$path]);
174		}
175
176		// check permissions before we continue, this is especially important for
177		// shared files
178		if (!$this->isDeletable($path)) {
179			return false;
180		}
181
182		$normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path, true, false, true);
183		$result = true;
184		$view = Filesystem::getView();
185
186		if ($view instanceof View) {
187			$relativePath = $view->getRelativePath($normalized);
188
189			// Skip trashbin based on config
190			if ($relativePath && $this->trashbinSkipChecker->shouldSkipPath($view, $relativePath) === true) {
191				return \call_user_func_array([$this->storage, $method], [$path]);
192			}
193		}
194
195		if (!isset($this->deletedFiles[$normalized]) && $view instanceof View) {
196			$this->deletedFiles[$normalized] = $normalized;
197			if ($filesPath = $view->getRelativePath($normalized)) {
198				$filesPath = \trim($filesPath, '/');
199				$result = \OCA\Files_Trashbin\Trashbin::move2trash($filesPath);
200				// in cross-storage cases the file will be copied
201				// but not deleted, so we delete it here
202				if ($result) {
203					\call_user_func_array([$this->storage, $method], [$path]);
204				}
205			} else {
206				$result = \call_user_func_array([$this->storage, $method], [$path]);
207			}
208			unset($this->deletedFiles[$normalized]);
209		} elseif ($this->storage->file_exists($path)) {
210			$result = \call_user_func_array([$this->storage, $method], [$path]);
211		}
212
213		return $result;
214	}
215
216	/**
217	 * Retain the encryption keys
218	 *
219	 * @param $filename
220	 * @param $owner
221	 * @param $ownerPath
222	 * @param $timestamp
223	 * @param $sourceStorage
224	 * @return bool
225	 */
226
227	public function retainKeys($filename, $owner, $ownerPath, $timestamp, $sourceStorage) {
228		if (\OC::$server->getEncryptionManager()->isEnabled()) {
229			if ($sourceStorage !== null) {
230				$sourcePath = '/' . $owner . '/files_trashbin/files/'. $filename . '.d' . $timestamp;
231				$targetPath = '/' . $owner . '/files/' . $ownerPath;
232				return $sourceStorage->copyKeys($sourcePath, $targetPath);
233			}
234		}
235		return false;
236	}
237
238	/**
239	 * Setup the storate wrapper callback
240	 */
241	public static function setupStorage() {
242		\OC\Files\Filesystem::addStorageWrapper('oc_trashbin', function ($mountPoint, $storage) {
243			return new \OCA\Files_Trashbin\Storage(
244				['storage' => $storage, 'mountPoint' => $mountPoint],
245				\OC::$server->getUserManager(),
246				new TrashbinSkipChecker(
247					\OC::$server->getLogger(),
248					\OC::$server->getConfig()
249				)
250			);
251		}, 1);
252	}
253}
254