Completed
Push — master ( 5926da...79706e )
by Lukas
13:35
created

FilesPlugin::httpPost()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 39
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
nc 5
nop 2
dl 0
loc 39
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Björn Schießle <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Robin McCorkell <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Vincent Petry <[email protected]>
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
31
namespace OCA\DAV\Connector\Sabre;
32
33
use OC\Files\View;
34
use OCA\DAV\Upload\FutureFile;
35
use OCP\Files\ForbiddenException;
36
use OCP\IPreview;
37
use Sabre\DAV\Exception\Forbidden;
38
use Sabre\DAV\Exception\NotFound;
39
use Sabre\DAV\IFile;
40
use \Sabre\DAV\PropFind;
41
use \Sabre\DAV\PropPatch;
42
use Sabre\DAV\ServerPlugin;
43
use Sabre\DAV\Tree;
44
use \Sabre\HTTP\RequestInterface;
45
use \Sabre\HTTP\ResponseInterface;
46
use OCP\Files\StorageNotAvailableException;
47
use OCP\IConfig;
48
use OCP\IRequest;
49
use Sabre\DAV\Exception\BadRequest;
50
use OCA\DAV\Connector\Sabre\Directory;
51
52
class FilesPlugin extends ServerPlugin {
53
54
	// namespace
55
	const NS_OWNCLOUD = 'http://owncloud.org/ns';
56
	const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
57
	const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
58
	const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
59
	const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
60
	const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
61
	const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
62
	const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
63
	const GETETAG_PROPERTYNAME = '{DAV:}getetag';
64
	const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
65
	const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
66
	const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
67
	const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
68
	const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
69
	const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
70
71
	/**
72
	 * Reference to main server object
73
	 *
74
	 * @var \Sabre\DAV\Server
75
	 */
76
	private $server;
77
78
	/**
79
	 * @var Tree
80
	 */
81
	private $tree;
82
83
	/**
84
	 * Whether this is public webdav.
85
	 * If true, some returned information will be stripped off.
86
	 *
87
	 * @var bool
88
	 */
89
	private $isPublic;
90
91
	/**
92
	 * @var View
93
	 */
94
	private $fileView;
95
96
	/**
97
	 * @var bool
98
	 */
99
	private $downloadAttachment;
100
101
	/**
102
	 * @var IConfig
103
	 */
104
	private $config;
105
106
	/**
107
	 * @var IRequest
108
	 */
109
	private $request;
110
111
	/**
112
	 * @var IPreview
113
	 */
114
	private $previewManager;
115
116
	/**
117
	 * @param Tree $tree
118
	 * @param View $view
119
	 * @param IConfig $config
120
	 * @param IRequest $request
121
	 * @param IPreview $previewManager
122
	 * @param bool $isPublic
123
	 * @param bool $downloadAttachment
124
	 */
125 View Code Duplication
	public function __construct(Tree $tree,
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126
								View $view,
127
								IConfig $config,
128
								IRequest $request,
129
								IPreview $previewManager,
130
								$isPublic = false,
131
								$downloadAttachment = true) {
132
		$this->tree = $tree;
133
		$this->fileView = $view;
134
		$this->config = $config;
135
		$this->request = $request;
136
		$this->isPublic = $isPublic;
137
		$this->downloadAttachment = $downloadAttachment;
138
		$this->previewManager = $previewManager;
139
	}
140
141
	/**
142
	 * This initializes the plugin.
143
	 *
144
	 * This function is called by \Sabre\DAV\Server, after
145
	 * addPlugin is called.
146
	 *
147
	 * This method should set up the required event subscriptions.
148
	 *
149
	 * @param \Sabre\DAV\Server $server
150
	 * @return void
151
	 */
152
	public function initialize(\Sabre\DAV\Server $server) {
153
154
		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
155
		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
156
		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
157
		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
158
		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
159
		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
160
		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
161
		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
162
		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
163
		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
164
		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
165
		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
166
		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
167
168
		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
169
		$allowedProperties = ['{DAV:}getetag'];
170
		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
171
172
		$this->server = $server;
173
		$this->server->on('propFind', array($this, 'handleGetProperties'));
174
		$this->server->on('propPatch', array($this, 'handleUpdateProperties'));
175
		// RFC5995 to add file to the collection with a suggested name
176
		$this->server->on('method:POST', [$this, 'httpPost']);
177
		$this->server->on('afterBind', array($this, 'sendFileIdHeader'));
178
		$this->server->on('afterWriteContent', array($this, 'sendFileIdHeader'));
179
		$this->server->on('afterMethod:GET', [$this,'httpGet']);
180
		$this->server->on('afterMethod:GET', array($this, 'handleDownloadToken'));
181
		$this->server->on('afterResponse', function($request, ResponseInterface $response) {
182
			$body = $response->getBody();
183
			if (is_resource($body)) {
184
				fclose($body);
185
			}
186
		});
187
		$this->server->on('beforeMove', [$this, 'checkMove']);
188
	}
189
190
	/**
191
	 * Plugin that checks if a move can actually be performed.
192
	 *
193
	 * @param string $source source path
194
	 * @param string $destination destination path
195
	 * @throws Forbidden
196
	 * @throws NotFound
197
	 */
198
	function checkMove($source, $destination) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
199
		$sourceNode = $this->tree->getNodeForPath($source);
200
		if (!$sourceNode instanceof Node) {
201
			return;
202
		}
203
		list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source);
204
		list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination);
205
206
		if ($sourceDir !== $destinationDir) {
207
			$sourceNodeFileInfo = $sourceNode->getFileInfo();
208
			if (is_null($sourceNodeFileInfo)) {
209
				throw new NotFound($source . ' does not exist');
210
			}
211
212
			if (!$sourceNodeFileInfo->isDeletable()) {
213
				throw new Forbidden($source . " cannot be deleted");
214
			}
215
		}
216
	}
217
218
	/**
219
	 * This sets a cookie to be able to recognize the start of the download
220
	 * the content must not be longer than 32 characters and must only contain
221
	 * alphanumeric characters
222
	 *
223
	 * @param RequestInterface $request
224
	 * @param ResponseInterface $response
225
	 */
226
	function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
227
		$queryParams = $request->getQueryParameters();
228
229
		/**
230
		 * this sets a cookie to be able to recognize the start of the download
231
		 * the content must not be longer than 32 characters and must only contain
232
		 * alphanumeric characters
233
		 */
234
		if (isset($queryParams['downloadStartSecret'])) {
235
			$token = $queryParams['downloadStartSecret'];
236 View Code Duplication
			if (!isset($token[32])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237
				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
238
				// FIXME: use $response->setHeader() instead
239
				setcookie('ocDownloadStarted', $token, time() + 20, '/');
240
			}
241
		}
242
	}
243
244
	/**
245
	 * Add headers to file download
246
	 *
247
	 * @param RequestInterface $request
248
	 * @param ResponseInterface $response
249
	 */
250
	function httpGet(RequestInterface $request, ResponseInterface $response) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
251
		// Only handle valid files
252
		$node = $this->tree->getNodeForPath($request->getPath());
253
		if (!($node instanceof IFile)) return;
0 ignored issues
show
Bug introduced by
The class Sabre\DAV\IFile does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
254
255
		// adds a 'Content-Disposition: attachment' header
256
		if ($this->downloadAttachment) {
257
			$filename = $node->getName();
258
			if ($this->request->isUserAgent(
259
				[
260
					\OC\AppFramework\Http\Request::USER_AGENT_IE,
261
					\OC\AppFramework\Http\Request::USER_AGENT_ANDROID_MOBILE_CHROME,
262
					\OC\AppFramework\Http\Request::USER_AGENT_FREEBOX,
263
				])) {
264
				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
265
			} else {
266
				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
267
													 . '; filename="' . rawurlencode($filename) . '"');
268
			}
269
		}
270
271
		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
272
			//Add OC-Checksum header
273
			/** @var $node File */
274
			$checksum = $node->getChecksum();
275
			if ($checksum !== null && $checksum !== '') {
276
				$response->addHeader('OC-Checksum', $checksum);
277
			}
278
		}
279
	}
280
281
	/**
282
	 * Adds all ownCloud-specific properties
283
	 *
284
	 * @param PropFind $propFind
285
	 * @param \Sabre\DAV\INode $node
286
	 * @return void
287
	 */
288
	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
289
290
		$httpRequest = $this->server->httpRequest;
291
292
		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
293
294
			$propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) {
295
				return $node->getFileId();
296
			});
297
298
			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) {
299
				return $node->getInternalFileId();
300
			});
301
302
			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) {
303
				$perms = $node->getDavPermissions();
304
				if ($this->isPublic) {
305
					// remove mount information
306
					$perms = str_replace(['S', 'M'], '', $perms);
307
				}
308
				return $perms;
309
			});
310
311
			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) {
312
				return $node->getSharePermissions(
313
					$httpRequest->getRawServerValue('PHP_AUTH_USER')
314
				);
315
			});
316
317
			$propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) {
318
				return $node->getETag();
319
			});
320
321
			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) {
322
				$owner = $node->getOwner();
323
				return $owner->getUID();
324
			});
325
			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) {
326
				$owner = $node->getOwner();
327
				$displayName = $owner->getDisplayName();
328
				return $displayName;
329
			});
330
331
			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
332
				return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
333
			});
334
		}
335
336
		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
337
			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) {
338
				return $this->config->getSystemValue('data-fingerprint', '');
339
			});
340
		}
341
342
		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
343
			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) {
344
				/** @var $node \OCA\DAV\Connector\Sabre\File */
345
				try {
346
					$directDownloadUrl = $node->getDirectDownload();
347
					if (isset($directDownloadUrl['url'])) {
348
						return $directDownloadUrl['url'];
349
					}
350
				} catch (StorageNotAvailableException $e) {
351
					return false;
352
				} catch (ForbiddenException $e) {
353
					return false;
354
				}
355
				return false;
356
			});
357
358
			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) {
359
				$checksum = $node->getChecksum();
360
				if ($checksum === NULL || $checksum === '') {
361
					return null;
362
				}
363
364
				return new ChecksumList($checksum);
365
			});
366
367
		}
368
369
		if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) {
370
			$propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) {
371
				return $node->getSize();
372
			});
373
		}
374
	}
375
376
	/**
377
	 * Update ownCloud-specific properties
378
	 *
379
	 * @param string $path
380
	 * @param PropPatch $propPatch
381
	 *
382
	 * @return void
383
	 */
384
	public function handleUpdateProperties($path, PropPatch $propPatch) {
385 View Code Duplication
		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
386
			if (empty($time)) {
387
				return false;
388
			}
389
			$node = $this->tree->getNodeForPath($path);
390
			if (is_null($node)) {
391
				return 404;
392
			}
393
			$node->touch($time);
394
			return true;
395
		});
396 View Code Duplication
		$propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
397
			if (empty($etag)) {
398
				return false;
399
			}
400
			$node = $this->tree->getNodeForPath($path);
401
			if (is_null($node)) {
402
				return 404;
403
			}
404
			if ($node->setEtag($etag) !== -1) {
405
				return true;
406
			}
407
			return false;
408
		});
409
	}
410
411
	/**
412
	 * @param string $filePath
413
	 * @param \Sabre\DAV\INode $node
414
	 * @throws \Sabre\DAV\Exception\BadRequest
415
	 */
416
	public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
417
		// chunked upload handling
418
		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
419
			list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($filePath);
420
			$info = \OC_FileChunking::decodeName($name);
421
			if (!empty($info)) {
422
				$filePath = $path . '/' . $info['name'];
423
			}
424
		}
425
426
		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
427
		if (!$this->server->tree->nodeExists($filePath)) {
428
			return;
429
		}
430
		$node = $this->server->tree->getNodeForPath($filePath);
431
		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
432
			$fileId = $node->getFileId();
433
			if (!is_null($fileId)) {
434
				$this->server->httpResponse->setHeader('OC-FileId', $fileId);
435
			}
436
		}
437
	}
438
439
	/**
440
	 * POST operation on directories to create a new file
441
	 * with suggested name
442
	 *
443
	 * @param RequestInterface $request request object
444
	 * @param ResponseInterface $response response object
445
	 * @return null|false
446
	 */
447
	public function httpPost(RequestInterface $request, ResponseInterface $response) {
448
		// TODO: move this to another plugin ?
449
		if (!\OC::$CLI && !\OC::$server->getRequest()->passesCSRFCheck()) {
450
			throw new BadRequest('Invalid CSRF token');
451
		}
452
453
		list($parentPath, $name) = \Sabre\HTTP\URLUtil::splitPath($request->getPath());
454
455
		// Making sure the parent node exists and is a directory
456
		$node = $this->tree->getNodeForPath($parentPath);
457
458
		if ($node instanceof Directory) {
459
			// no Add-Member found
460
			if (empty($name) || $name[0] !== '&') {
461
				// suggested name required
462
				throw new BadRequest('Missing suggested file name');
463
			}
464
465
			$name = substr($name, 1);
466
467
			if (empty($name)) {
468
				// suggested name required
469
				throw new BadRequest('Missing suggested file name');
470
			}
471
472
			// make sure the name is unique
473
			$name = basename(\OC_Helper::buildNotExistingFileNameForView($parentPath, $name, $this->fileView));
474
475
			$node->createFile($name, $request->getBodyAsStream());
476
477
			list($parentUrl, ) = \Sabre\HTTP\URLUtil::splitPath($request->getUrl());
478
479
			$response->setHeader('Content-Location', $parentUrl . '/' . rawurlencode($name));
480
481
			// created
482
			$response->setStatus(201);
483
			return false;
484
		}
485
	}
486
}
487