You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

435 lines
13 KiB

9 years ago
9 years ago
10 years ago
9 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Björn Schießle <bjoern@schiessle.org>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Robin McCorkell <robin@mccorkell.me.uk>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. * @author Vincent Petry <pvince81@owncloud.com>
  14. *
  15. * @license AGPL-3.0
  16. *
  17. * This code is free software: you can redistribute it and/or modify
  18. * it under the terms of the GNU Affero General Public License, version 3,
  19. * as published by the Free Software Foundation.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License, version 3,
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>
  28. *
  29. */
  30. namespace OCA\DAV\Connector\Sabre;
  31. use OC\Files\View;
  32. use OCA\DAV\Upload\FutureFile;
  33. use OCP\Files\ForbiddenException;
  34. use OCP\IPreview;
  35. use Sabre\DAV\Exception\Forbidden;
  36. use Sabre\DAV\Exception\NotFound;
  37. use Sabre\DAV\IFile;
  38. use \Sabre\DAV\PropFind;
  39. use \Sabre\DAV\PropPatch;
  40. use Sabre\DAV\ServerPlugin;
  41. use Sabre\DAV\Tree;
  42. use \Sabre\HTTP\RequestInterface;
  43. use \Sabre\HTTP\ResponseInterface;
  44. use OCP\Files\StorageNotAvailableException;
  45. use OCP\IConfig;
  46. use OCP\IRequest;
  47. class FilesPlugin extends ServerPlugin {
  48. // namespace
  49. const NS_OWNCLOUD = 'http://owncloud.org/ns';
  50. const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
  51. const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
  52. const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
  53. const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
  54. const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
  55. const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
  56. const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
  57. const GETETAG_PROPERTYNAME = '{DAV:}getetag';
  58. const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
  59. const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
  60. const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
  61. const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
  62. const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
  63. const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
  64. /**
  65. * Reference to main server object
  66. *
  67. * @var \Sabre\DAV\Server
  68. */
  69. private $server;
  70. /**
  71. * @var Tree
  72. */
  73. private $tree;
  74. /**
  75. * Whether this is public webdav.
  76. * If true, some returned information will be stripped off.
  77. *
  78. * @var bool
  79. */
  80. private $isPublic;
  81. /**
  82. * @var View
  83. */
  84. private $fileView;
  85. /**
  86. * @var bool
  87. */
  88. private $downloadAttachment;
  89. /**
  90. * @var IConfig
  91. */
  92. private $config;
  93. /**
  94. * @var IRequest
  95. */
  96. private $request;
  97. /**
  98. * @var IPreview
  99. */
  100. private $previewManager;
  101. /**
  102. * @param Tree $tree
  103. * @param View $view
  104. * @param IConfig $config
  105. * @param IRequest $request
  106. * @param IPreview $previewManager
  107. * @param bool $isPublic
  108. * @param bool $downloadAttachment
  109. */
  110. public function __construct(Tree $tree,
  111. View $view,
  112. IConfig $config,
  113. IRequest $request,
  114. IPreview $previewManager,
  115. $isPublic = false,
  116. $downloadAttachment = true) {
  117. $this->tree = $tree;
  118. $this->fileView = $view;
  119. $this->config = $config;
  120. $this->request = $request;
  121. $this->isPublic = $isPublic;
  122. $this->downloadAttachment = $downloadAttachment;
  123. $this->previewManager = $previewManager;
  124. }
  125. /**
  126. * This initializes the plugin.
  127. *
  128. * This function is called by \Sabre\DAV\Server, after
  129. * addPlugin is called.
  130. *
  131. * This method should set up the required event subscriptions.
  132. *
  133. * @param \Sabre\DAV\Server $server
  134. * @return void
  135. */
  136. public function initialize(\Sabre\DAV\Server $server) {
  137. $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
  138. $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
  139. $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
  140. $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
  141. $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
  142. $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
  143. $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
  144. $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
  145. $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
  146. $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
  147. $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
  148. $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
  149. $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
  150. // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
  151. $allowedProperties = ['{DAV:}getetag'];
  152. $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
  153. $this->server = $server;
  154. $this->server->on('propFind', array($this, 'handleGetProperties'));
  155. $this->server->on('propPatch', array($this, 'handleUpdateProperties'));
  156. $this->server->on('afterBind', array($this, 'sendFileIdHeader'));
  157. $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader'));
  158. $this->server->on('afterMethod:GET', [$this,'httpGet']);
  159. $this->server->on('afterMethod:GET', array($this, 'handleDownloadToken'));
  160. $this->server->on('afterResponse', function($request, ResponseInterface $response) {
  161. $body = $response->getBody();
  162. if (is_resource($body)) {
  163. fclose($body);
  164. }
  165. });
  166. $this->server->on('beforeMove', [$this, 'checkMove']);
  167. }
  168. /**
  169. * Plugin that checks if a move can actually be performed.
  170. *
  171. * @param string $source source path
  172. * @param string $destination destination path
  173. * @throws Forbidden
  174. * @throws NotFound
  175. */
  176. function checkMove($source, $destination) {
  177. $sourceNode = $this->tree->getNodeForPath($source);
  178. if (!$sourceNode instanceof Node) {
  179. return;
  180. }
  181. list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source);
  182. list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination);
  183. if ($sourceDir !== $destinationDir) {
  184. $sourceNodeFileInfo = $sourceNode->getFileInfo();
  185. if (is_null($sourceNodeFileInfo)) {
  186. throw new NotFound($source . ' does not exist');
  187. }
  188. if (!$sourceNodeFileInfo->isDeletable()) {
  189. throw new Forbidden($source . " cannot be deleted");
  190. }
  191. }
  192. }
  193. /**
  194. * This sets a cookie to be able to recognize the start of the download
  195. * the content must not be longer than 32 characters and must only contain
  196. * alphanumeric characters
  197. *
  198. * @param RequestInterface $request
  199. * @param ResponseInterface $response
  200. */
  201. function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
  202. $queryParams = $request->getQueryParameters();
  203. /**
  204. * this sets a cookie to be able to recognize the start of the download
  205. * the content must not be longer than 32 characters and must only contain
  206. * alphanumeric characters
  207. */
  208. if (isset($queryParams['downloadStartSecret'])) {
  209. $token = $queryParams['downloadStartSecret'];
  210. if (!isset($token[32])
  211. && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
  212. // FIXME: use $response->setHeader() instead
  213. setcookie('ocDownloadStarted', $token, time() + 20, '/');
  214. }
  215. }
  216. }
  217. /**
  218. * Add headers to file download
  219. *
  220. * @param RequestInterface $request
  221. * @param ResponseInterface $response
  222. */
  223. function httpGet(RequestInterface $request, ResponseInterface $response) {
  224. // Only handle valid files
  225. $node = $this->tree->getNodeForPath($request->getPath());
  226. if (!($node instanceof IFile)) return;
  227. // adds a 'Content-Disposition: attachment' header
  228. if ($this->downloadAttachment) {
  229. $filename = $node->getName();
  230. if ($this->request->isUserAgent(
  231. [
  232. \OC\AppFramework\Http\Request::USER_AGENT_IE,
  233. \OC\AppFramework\Http\Request::USER_AGENT_ANDROID_MOBILE_CHROME,
  234. \OC\AppFramework\Http\Request::USER_AGENT_FREEBOX,
  235. ])) {
  236. $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
  237. } else {
  238. $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
  239. . '; filename="' . rawurlencode($filename) . '"');
  240. }
  241. }
  242. if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
  243. //Add OC-Checksum header
  244. /** @var $node File */
  245. $checksum = $node->getChecksum();
  246. if ($checksum !== null && $checksum !== '') {
  247. $response->addHeader('OC-Checksum', $checksum);
  248. }
  249. }
  250. }
  251. /**
  252. * Adds all ownCloud-specific properties
  253. *
  254. * @param PropFind $propFind
  255. * @param \Sabre\DAV\INode $node
  256. * @return void
  257. */
  258. public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
  259. $httpRequest = $this->server->httpRequest;
  260. if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
  261. $propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) {
  262. return $node->getFileId();
  263. });
  264. $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) {
  265. return $node->getInternalFileId();
  266. });
  267. $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) {
  268. $perms = $node->getDavPermissions();
  269. if ($this->isPublic) {
  270. // remove mount information
  271. $perms = str_replace(['S', 'M'], '', $perms);
  272. }
  273. return $perms;
  274. });
  275. $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) {
  276. return $node->getSharePermissions(
  277. $httpRequest->getRawServerValue('PHP_AUTH_USER')
  278. );
  279. });
  280. $propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) {
  281. return $node->getETag();
  282. });
  283. $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) {
  284. $owner = $node->getOwner();
  285. return $owner->getUID();
  286. });
  287. $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) {
  288. $owner = $node->getOwner();
  289. $displayName = $owner->getDisplayName();
  290. return $displayName;
  291. });
  292. $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
  293. return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
  294. });
  295. }
  296. if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
  297. $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) {
  298. return $this->config->getSystemValue('data-fingerprint', '');
  299. });
  300. }
  301. if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
  302. $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) {
  303. /** @var $node \OCA\DAV\Connector\Sabre\File */
  304. try {
  305. $directDownloadUrl = $node->getDirectDownload();
  306. if (isset($directDownloadUrl['url'])) {
  307. return $directDownloadUrl['url'];
  308. }
  309. } catch (StorageNotAvailableException $e) {
  310. return false;
  311. } catch (ForbiddenException $e) {
  312. return false;
  313. }
  314. return false;
  315. });
  316. $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) {
  317. $checksum = $node->getChecksum();
  318. if ($checksum === NULL || $checksum === '') {
  319. return null;
  320. }
  321. return new ChecksumList($checksum);
  322. });
  323. }
  324. if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
  325. $propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) {
  326. return $node->getSize();
  327. });
  328. }
  329. }
  330. /**
  331. * Update ownCloud-specific properties
  332. *
  333. * @param string $path
  334. * @param PropPatch $propPatch
  335. *
  336. * @return void
  337. */
  338. public function handleUpdateProperties($path, PropPatch $propPatch) {
  339. $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($path) {
  340. if (empty($time)) {
  341. return false;
  342. }
  343. $node = $this->tree->getNodeForPath($path);
  344. if (is_null($node)) {
  345. return 404;
  346. }
  347. $node->touch($time);
  348. return true;
  349. });
  350. $propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($path) {
  351. if (empty($etag)) {
  352. return false;
  353. }
  354. $node = $this->tree->getNodeForPath($path);
  355. if (is_null($node)) {
  356. return 404;
  357. }
  358. if ($node->setEtag($etag) !== -1) {
  359. return true;
  360. }
  361. return false;
  362. });
  363. }
  364. /**
  365. * @param string $filePath
  366. * @param \Sabre\DAV\INode $node
  367. * @throws \Sabre\DAV\Exception\BadRequest
  368. */
  369. public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
  370. // chunked upload handling
  371. if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
  372. list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($filePath);
  373. $info = \OC_FileChunking::decodeName($name);
  374. if (!empty($info)) {
  375. $filePath = $path . '/' . $info['name'];
  376. }
  377. }
  378. // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
  379. if (!$this->server->tree->nodeExists($filePath)) {
  380. return;
  381. }
  382. $node = $this->server->tree->getNodeForPath($filePath);
  383. if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
  384. $fileId = $node->getFileId();
  385. if (!is_null($fileId)) {
  386. $this->server->httpResponse->setHeader('OC-FileId', $fileId);
  387. }
  388. }
  389. }
  390. }