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