1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Bjoern Schiessle <bjoern@schiessle.org> 6 * @author Björn Schießle <bjoern@schiessle.org> 7 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 8 * @author Julius Härtl <jus@bitgrid.net> 9 * @author Morris Jobke <hey@morrisjobke.de> 10 * @author Robin Appelman <robin@icewind.nl> 11 * @author Roeland Jago Douma <roeland@famdouma.nl> 12 * @author Vincent Petry <vincent@nextcloud.com> 13 * 14 * @license AGPL-3.0 15 * 16 * This code is free software: you can redistribute it and/or modify 17 * it under the terms of the GNU Affero General Public License, version 3, 18 * as published by the Free Software Foundation. 19 * 20 * This program is distributed in the hope that it will be useful, 21 * but WITHOUT ANY WARRANTY; without even the implied warranty of 22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 * GNU Affero General Public License for more details. 24 * 25 * You should have received a copy of the GNU Affero General Public License, version 3, 26 * along with this program. If not, see <http://www.gnu.org/licenses/> 27 * 28 */ 29namespace OCA\Files_Trashbin; 30 31use OC\Files\Filesystem; 32use OC\Files\Storage\Wrapper\Wrapper; 33use OCA\Files_Trashbin\Events\MoveToTrashEvent; 34use OCA\Files_Trashbin\Trash\ITrashManager; 35use OCP\Encryption\Exceptions\GenericEncryptionException; 36use OCP\Files\IRootFolder; 37use OCP\Files\Mount\IMountPoint; 38use OCP\Files\Node; 39use OCP\Files\Storage\IStorage; 40use OCP\ILogger; 41use OCP\IUserManager; 42use Symfony\Component\EventDispatcher\EventDispatcherInterface; 43 44class Storage extends Wrapper { 45 /** @var IMountPoint */ 46 private $mountPoint; 47 48 /** @var IUserManager */ 49 private $userManager; 50 51 /** @var ILogger */ 52 private $logger; 53 54 /** @var EventDispatcherInterface */ 55 private $eventDispatcher; 56 57 /** @var IRootFolder */ 58 private $rootFolder; 59 60 /** @var ITrashManager */ 61 private $trashManager; 62 63 private $trashEnabled = true; 64 65 /** 66 * Storage constructor. 67 * 68 * @param array $parameters 69 * @param ITrashManager $trashManager 70 * @param IUserManager|null $userManager 71 * @param ILogger|null $logger 72 * @param EventDispatcherInterface|null $eventDispatcher 73 * @param IRootFolder|null $rootFolder 74 */ 75 public function __construct( 76 $parameters, 77 ITrashManager $trashManager = null, 78 IUserManager $userManager = null, 79 ILogger $logger = null, 80 EventDispatcherInterface $eventDispatcher = null, 81 IRootFolder $rootFolder = null 82 ) { 83 $this->mountPoint = $parameters['mountPoint']; 84 $this->trashManager = $trashManager; 85 $this->userManager = $userManager; 86 $this->logger = $logger; 87 $this->eventDispatcher = $eventDispatcher; 88 $this->rootFolder = $rootFolder; 89 parent::__construct($parameters); 90 } 91 92 /** 93 * Deletes the given file by moving it into the trashbin. 94 * 95 * @param string $path path of file or folder to delete 96 * 97 * @return bool true if the operation succeeded, false otherwise 98 */ 99 public function unlink($path) { 100 if ($this->trashEnabled) { 101 try { 102 return $this->doDelete($path, 'unlink'); 103 } catch (GenericEncryptionException $e) { 104 // in case of a encryption exception we delete the file right away 105 $this->logger->info( 106 "Can't move file " . $path . 107 " to the trash bin, therefore it was deleted right away"); 108 109 return $this->storage->unlink($path); 110 } 111 } else { 112 return $this->storage->unlink($path); 113 } 114 } 115 116 /** 117 * Deletes the given folder by moving it into the trashbin. 118 * 119 * @param string $path path of folder to delete 120 * 121 * @return bool true if the operation succeeded, false otherwise 122 */ 123 public function rmdir($path) { 124 if ($this->trashEnabled) { 125 return $this->doDelete($path, 'rmdir'); 126 } else { 127 return $this->storage->rmdir($path); 128 } 129 } 130 131 /** 132 * check if it is a file located in data/user/files only files in the 133 * 'files' directory should be moved to the trash 134 * 135 * @param $path 136 * @return bool 137 */ 138 protected function shouldMoveToTrash($path) { 139 $normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path); 140 $parts = explode('/', $normalized); 141 if (count($parts) < 4 || strpos($normalized, '/appdata_') === 0) { 142 return false; 143 } 144 145 // check if there is a app which want to disable the trash bin for this file 146 $fileId = $this->storage->getCache()->getId($path); 147 $owner = $this->storage->getOwner($path); 148 if ($owner === false || $this->storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class)) { 149 $nodes = $this->rootFolder->getById($fileId); 150 } else { 151 $nodes = $this->rootFolder->getUserFolder($owner)->getById($fileId); 152 } 153 154 foreach ($nodes as $node) { 155 $event = $this->createMoveToTrashEvent($node); 156 $this->eventDispatcher->dispatch('OCA\Files_Trashbin::moveToTrash', $event); 157 if ($event->shouldMoveToTrashBin() === false) { 158 return false; 159 } 160 } 161 162 if ($parts[2] === 'files' && $this->userManager->userExists($parts[1])) { 163 return true; 164 } 165 166 return false; 167 } 168 169 /** 170 * get move to trash event 171 * 172 * @param Node $node 173 * @return MoveToTrashEvent 174 */ 175 protected function createMoveToTrashEvent(Node $node) { 176 return new MoveToTrashEvent($node); 177 } 178 179 /** 180 * Run the delete operation with the given method 181 * 182 * @param string $path path of file or folder to delete 183 * @param string $method either "unlink" or "rmdir" 184 * 185 * @return bool true if the operation succeeded, false otherwise 186 */ 187 private function doDelete($path, $method) { 188 if ( 189 !\OC::$server->getAppManager()->isEnabledForUser('files_trashbin') 190 || (pathinfo($path, PATHINFO_EXTENSION) === 'part') 191 || $this->shouldMoveToTrash($path) === false 192 ) { 193 return call_user_func([$this->storage, $method], $path); 194 } 195 196 // check permissions before we continue, this is especially important for 197 // shared files 198 if (!$this->isDeletable($path)) { 199 return false; 200 } 201 202 $isMovedToTrash = $this->trashManager->moveToTrash($this, $path); 203 if (!$isMovedToTrash) { 204 return call_user_func([$this->storage, $method], $path); 205 } else { 206 return true; 207 } 208 } 209 210 /** 211 * Setup the storate wrapper callback 212 */ 213 public static function setupStorage() { 214 \OC\Files\Filesystem::addStorageWrapper('oc_trashbin', function ($mountPoint, $storage) { 215 return new \OCA\Files_Trashbin\Storage( 216 ['storage' => $storage, 'mountPoint' => $mountPoint], 217 \OC::$server->query(ITrashManager::class), 218 \OC::$server->getUserManager(), 219 \OC::$server->getLogger(), 220 \OC::$server->getEventDispatcher(), 221 \OC::$server->getLazyRootFolder() 222 ); 223 }, 1); 224 } 225 226 public function getMountPoint() { 227 return $this->mountPoint; 228 } 229 230 public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { 231 $sourceIsTrashbin = $sourceStorage->instanceOfStorage(Storage::class); 232 try { 233 // the fallback for moving between storage involves a copy+delete 234 // we don't want to trigger the trashbin when doing the delete 235 if ($sourceIsTrashbin) { 236 /** @var Storage $sourceStorage */ 237 $sourceStorage->disableTrash(); 238 } 239 $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); 240 if ($sourceIsTrashbin) { 241 /** @var Storage $sourceStorage */ 242 $sourceStorage->enableTrash(); 243 } 244 return $result; 245 } catch (\Exception $e) { 246 if ($sourceIsTrashbin) { 247 /** @var Storage $sourceStorage */ 248 $sourceStorage->enableTrash(); 249 } 250 throw $e; 251 } 252 } 253 254 protected function disableTrash() { 255 $this->trashEnabled = false; 256 } 257 258 protected function enableTrash() { 259 $this->trashEnabled = true; 260 } 261} 262