Passed
Push — master ( 8b53b6...fb7f65 )
by Blizzz
16:35 queued 12s
created
apps/dav/lib/Connector/Sabre/FilesPlugin.php 1 patch
Indentation   +512 added lines, -512 removed lines patch added patch discarded remove patch
@@ -53,516 +53,516 @@
 block discarded – undo
53 53
 
54 54
 class FilesPlugin extends ServerPlugin {
55 55
 
56
-	// namespace
57
-	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
58
-	public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
59
-	public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
60
-	public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
61
-	public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
62
-	public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
63
-	public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
64
-	public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
65
-	public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
66
-	public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
67
-	public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
68
-	public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
69
-	public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
70
-	public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
71
-	public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
72
-	public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
73
-	public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
74
-	public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
75
-	public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
76
-	public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
77
-	public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
78
-	public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
79
-	public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
80
-	public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
81
-	public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
82
-
83
-	/**
84
-	 * Reference to main server object
85
-	 *
86
-	 * @var \Sabre\DAV\Server
87
-	 */
88
-	private $server;
89
-
90
-	/**
91
-	 * @var Tree
92
-	 */
93
-	private $tree;
94
-
95
-	/**
96
-	 * @var IUserSession
97
-	 */
98
-	private $userSession;
99
-
100
-	/**
101
-	 * Whether this is public webdav.
102
-	 * If true, some returned information will be stripped off.
103
-	 *
104
-	 * @var bool
105
-	 */
106
-	private $isPublic;
107
-
108
-	/**
109
-	 * @var bool
110
-	 */
111
-	private $downloadAttachment;
112
-
113
-	/**
114
-	 * @var IConfig
115
-	 */
116
-	private $config;
117
-
118
-	/**
119
-	 * @var IRequest
120
-	 */
121
-	private $request;
122
-
123
-	/**
124
-	 * @var IPreview
125
-	 */
126
-	private $previewManager;
127
-
128
-	/**
129
-	 * @param Tree $tree
130
-	 * @param IConfig $config
131
-	 * @param IRequest $request
132
-	 * @param IPreview $previewManager
133
-	 * @param bool $isPublic
134
-	 * @param bool $downloadAttachment
135
-	 */
136
-	public function __construct(Tree $tree,
137
-								IConfig $config,
138
-								IRequest $request,
139
-								IPreview $previewManager,
140
-								IUserSession $userSession,
141
-								$isPublic = false,
142
-								$downloadAttachment = true) {
143
-		$this->tree = $tree;
144
-		$this->config = $config;
145
-		$this->request = $request;
146
-		$this->userSession = $userSession;
147
-		$this->isPublic = $isPublic;
148
-		$this->downloadAttachment = $downloadAttachment;
149
-		$this->previewManager = $previewManager;
150
-	}
151
-
152
-	/**
153
-	 * This initializes the plugin.
154
-	 *
155
-	 * This function is called by \Sabre\DAV\Server, after
156
-	 * addPlugin is called.
157
-	 *
158
-	 * This method should set up the required event subscriptions.
159
-	 *
160
-	 * @param \Sabre\DAV\Server $server
161
-	 * @return void
162
-	 */
163
-	public function initialize(\Sabre\DAV\Server $server) {
164
-		$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
165
-		$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
166
-		$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
167
-		$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
168
-		$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
169
-		$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
170
-		$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
171
-		$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
172
-		$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
173
-		$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
174
-		$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
175
-		$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
176
-		$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
177
-		$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
178
-		$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
179
-		$server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
180
-		$server->protectedProperties[] = self::SHARE_NOTE;
181
-
182
-		// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
183
-		$allowedProperties = ['{DAV:}getetag'];
184
-		$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
185
-
186
-		$this->server = $server;
187
-		$this->server->on('propFind', [$this, 'handleGetProperties']);
188
-		$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
189
-		$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
190
-		$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
191
-		$this->server->on('afterMethod:GET', [$this,'httpGet']);
192
-		$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
193
-		$this->server->on('afterResponse', function ($request, ResponseInterface $response) {
194
-			$body = $response->getBody();
195
-			if (is_resource($body)) {
196
-				fclose($body);
197
-			}
198
-		});
199
-		$this->server->on('beforeMove', [$this, 'checkMove']);
200
-	}
201
-
202
-	/**
203
-	 * Plugin that checks if a move can actually be performed.
204
-	 *
205
-	 * @param string $source source path
206
-	 * @param string $destination destination path
207
-	 * @throws Forbidden
208
-	 * @throws NotFound
209
-	 */
210
-	public function checkMove($source, $destination) {
211
-		$sourceNode = $this->tree->getNodeForPath($source);
212
-		if (!$sourceNode instanceof Node) {
213
-			return;
214
-		}
215
-		[$sourceDir,] = \Sabre\Uri\split($source);
216
-		[$destinationDir,] = \Sabre\Uri\split($destination);
217
-
218
-		if ($sourceDir !== $destinationDir) {
219
-			$sourceNodeFileInfo = $sourceNode->getFileInfo();
220
-			if ($sourceNodeFileInfo === null) {
221
-				throw new NotFound($source . ' does not exist');
222
-			}
223
-
224
-			if (!$sourceNodeFileInfo->isDeletable()) {
225
-				throw new Forbidden($source . " cannot be deleted");
226
-			}
227
-		}
228
-	}
229
-
230
-	/**
231
-	 * This sets a cookie to be able to recognize the start of the download
232
-	 * the content must not be longer than 32 characters and must only contain
233
-	 * alphanumeric characters
234
-	 *
235
-	 * @param RequestInterface $request
236
-	 * @param ResponseInterface $response
237
-	 */
238
-	public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
239
-		$queryParams = $request->getQueryParameters();
240
-
241
-		/**
242
-		 * this sets a cookie to be able to recognize the start of the download
243
-		 * the content must not be longer than 32 characters and must only contain
244
-		 * alphanumeric characters
245
-		 */
246
-		if (isset($queryParams['downloadStartSecret'])) {
247
-			$token = $queryParams['downloadStartSecret'];
248
-			if (!isset($token[32])
249
-				&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
250
-				// FIXME: use $response->setHeader() instead
251
-				setcookie('ocDownloadStarted', $token, time() + 20, '/');
252
-			}
253
-		}
254
-	}
255
-
256
-	/**
257
-	 * Add headers to file download
258
-	 *
259
-	 * @param RequestInterface $request
260
-	 * @param ResponseInterface $response
261
-	 */
262
-	public function httpGet(RequestInterface $request, ResponseInterface $response) {
263
-		// Only handle valid files
264
-		$node = $this->tree->getNodeForPath($request->getPath());
265
-		if (!($node instanceof IFile)) {
266
-			return;
267
-		}
268
-
269
-		// adds a 'Content-Disposition: attachment' header in case no disposition
270
-		// header has been set before
271
-		if ($this->downloadAttachment &&
272
-			$response->getHeader('Content-Disposition') === null) {
273
-			$filename = $node->getName();
274
-			if ($this->request->isUserAgent(
275
-				[
276
-					Request::USER_AGENT_IE,
277
-					Request::USER_AGENT_ANDROID_MOBILE_CHROME,
278
-					Request::USER_AGENT_FREEBOX,
279
-				])) {
280
-				$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
281
-			} else {
282
-				$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
283
-													 . '; filename="' . rawurlencode($filename) . '"');
284
-			}
285
-		}
286
-
287
-		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
288
-			//Add OC-Checksum header
289
-			$checksum = $node->getChecksum();
290
-			if ($checksum !== null && $checksum !== '') {
291
-				$response->addHeader('OC-Checksum', $checksum);
292
-			}
293
-		}
294
-		$response->addHeader('X-Accel-Buffering', 'no');
295
-	}
296
-
297
-	/**
298
-	 * Adds all ownCloud-specific properties
299
-	 *
300
-	 * @param PropFind $propFind
301
-	 * @param \Sabre\DAV\INode $node
302
-	 * @return void
303
-	 */
304
-	public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
305
-		$httpRequest = $this->server->httpRequest;
306
-
307
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
308
-			/**
309
-			 * This was disabled, because it made dir listing throw an exception,
310
-			 * so users were unable to navigate into folders where one subitem
311
-			 * is blocked by the files_accesscontrol app, see:
312
-			 * https://github.com/nextcloud/files_accesscontrol/issues/65
313
-			 * if (!$node->getFileInfo()->isReadable()) {
314
-			 *     // avoid detecting files through this means
315
-			 *     throw new NotFound();
316
-			 * }
317
-			 */
318
-
319
-			$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
320
-				return $node->getFileId();
321
-			});
322
-
323
-			$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
324
-				return $node->getInternalFileId();
325
-			});
326
-
327
-			$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
328
-				$perms = $node->getDavPermissions();
329
-				if ($this->isPublic) {
330
-					// remove mount information
331
-					$perms = str_replace(['S', 'M'], '', $perms);
332
-				}
333
-				return $perms;
334
-			});
335
-
336
-			$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
337
-				$user = $this->userSession->getUser();
338
-				if ($user === null) {
339
-					return null;
340
-				}
341
-				return $node->getSharePermissions(
342
-					$user->getUID()
343
-				);
344
-			});
345
-
346
-			$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
347
-				$user = $this->userSession->getUser();
348
-				if ($user === null) {
349
-					return null;
350
-				}
351
-				$ncPermissions = $node->getSharePermissions(
352
-					$user->getUID()
353
-				);
354
-				$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
355
-				return json_encode($ocmPermissions);
356
-			});
357
-
358
-			$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
359
-				return $node->getETag();
360
-			});
361
-
362
-			$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
363
-				$owner = $node->getOwner();
364
-				if (!$owner) {
365
-					return null;
366
-				} else {
367
-					return $owner->getUID();
368
-				}
369
-			});
370
-			$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
371
-				$owner = $node->getOwner();
372
-				if (!$owner) {
373
-					return null;
374
-				} else {
375
-					return $owner->getDisplayName();
376
-				}
377
-			});
378
-
379
-			$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
380
-				return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
381
-			});
382
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
383
-				return $node->getSize();
384
-			});
385
-			$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
386
-				return $node->getFileInfo()->getMountPoint()->getMountType();
387
-			});
388
-
389
-			$propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
390
-				$user = $this->userSession->getUser();
391
-				if ($user === null) {
392
-					return null;
393
-				}
394
-				return $node->getNoteFromShare(
395
-					$user->getUID()
396
-				);
397
-			});
398
-
399
-			$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
400
-				return $this->config->getSystemValue('data-fingerprint', '');
401
-			});
402
-			$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
403
-				return (new \DateTimeImmutable())
404
-					->setTimestamp($node->getFileInfo()->getCreationTime())
405
-					->format(\DateTimeInterface::ATOM);
406
-			});
407
-			$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
408
-				return $node->getFileInfo()->getCreationTime();
409
-			});
410
-		}
411
-
412
-		if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
413
-			$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
414
-				try {
415
-					$directDownloadUrl = $node->getDirectDownload();
416
-					if (isset($directDownloadUrl['url'])) {
417
-						return $directDownloadUrl['url'];
418
-					}
419
-				} catch (StorageNotAvailableException $e) {
420
-					return false;
421
-				} catch (ForbiddenException $e) {
422
-					return false;
423
-				}
424
-				return false;
425
-			});
426
-
427
-			$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
428
-				$checksum = $node->getChecksum();
429
-				if ($checksum === null || $checksum === '') {
430
-					return null;
431
-				}
432
-
433
-				return new ChecksumList($checksum);
434
-			});
435
-
436
-			$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
437
-				return $node->getFileInfo()->getUploadTime();
438
-			});
439
-		}
440
-
441
-		if ($node instanceof Directory) {
442
-			$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
443
-				return $node->getSize();
444
-			});
445
-
446
-			$propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
447
-				return $node->getFileInfo()->isEncrypted() ? '1' : '0';
448
-			});
449
-
450
-			$requestProperties = $propFind->getRequestedProperties();
451
-			if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
452
-				|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
453
-				$nbFiles = 0;
454
-				$nbFolders = 0;
455
-				foreach ($node->getChildren() as $child) {
456
-					if ($child instanceof File) {
457
-						$nbFiles++;
458
-					} elseif ($child instanceof Directory) {
459
-						$nbFolders++;
460
-					}
461
-				}
462
-
463
-				$propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
464
-				$propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
465
-			}
466
-		}
467
-	}
468
-
469
-	/**
470
-	 * translate Nextcloud permissions to OCM Permissions
471
-	 *
472
-	 * @param $ncPermissions
473
-	 * @return array
474
-	 */
475
-	protected function ncPermissions2ocmPermissions($ncPermissions) {
476
-		$ocmPermissions = [];
477
-
478
-		if ($ncPermissions & Constants::PERMISSION_SHARE) {
479
-			$ocmPermissions[] = 'share';
480
-		}
481
-
482
-		if ($ncPermissions & Constants::PERMISSION_READ) {
483
-			$ocmPermissions[] = 'read';
484
-		}
485
-
486
-		if (($ncPermissions & Constants::PERMISSION_CREATE) ||
487
-			($ncPermissions & Constants::PERMISSION_UPDATE)) {
488
-			$ocmPermissions[] = 'write';
489
-		}
490
-
491
-		return $ocmPermissions;
492
-	}
493
-
494
-	/**
495
-	 * Update ownCloud-specific properties
496
-	 *
497
-	 * @param string $path
498
-	 * @param PropPatch $propPatch
499
-	 *
500
-	 * @return void
501
-	 */
502
-	public function handleUpdateProperties($path, PropPatch $propPatch) {
503
-		$node = $this->tree->getNodeForPath($path);
504
-		if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
505
-			return;
506
-		}
507
-
508
-		$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
509
-			if (empty($time)) {
510
-				return false;
511
-			}
512
-			$node->touch($time);
513
-			return true;
514
-		});
515
-		$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
516
-			if (empty($etag)) {
517
-				return false;
518
-			}
519
-			if ($node->setEtag($etag) !== -1) {
520
-				return true;
521
-			}
522
-			return false;
523
-		});
524
-		$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
525
-			if (empty($time)) {
526
-				return false;
527
-			}
528
-			$dateTime = new \DateTimeImmutable($time);
529
-			$node->setCreationTime($dateTime->getTimestamp());
530
-			return true;
531
-		});
532
-		$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
533
-			if (empty($time)) {
534
-				return false;
535
-			}
536
-			$node->setCreationTime((int) $time);
537
-			return true;
538
-		});
539
-	}
540
-
541
-	/**
542
-	 * @param string $filePath
543
-	 * @param \Sabre\DAV\INode $node
544
-	 * @throws \Sabre\DAV\Exception\BadRequest
545
-	 */
546
-	public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
547
-		// chunked upload handling
548
-		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
549
-			[$path, $name] = \Sabre\Uri\split($filePath);
550
-			$info = \OC_FileChunking::decodeName($name);
551
-			if (!empty($info)) {
552
-				$filePath = $path . '/' . $info['name'];
553
-			}
554
-		}
555
-
556
-		// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
557
-		if (!$this->server->tree->nodeExists($filePath)) {
558
-			return;
559
-		}
560
-		$node = $this->server->tree->getNodeForPath($filePath);
561
-		if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
562
-			$fileId = $node->getFileId();
563
-			if (!is_null($fileId)) {
564
-				$this->server->httpResponse->setHeader('OC-FileId', $fileId);
565
-			}
566
-		}
567
-	}
56
+    // namespace
57
+    public const NS_OWNCLOUD = 'http://owncloud.org/ns';
58
+    public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
59
+    public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
60
+    public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
61
+    public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
62
+    public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
63
+    public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
64
+    public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
65
+    public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
66
+    public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
67
+    public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
68
+    public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
69
+    public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
70
+    public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
71
+    public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
72
+    public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
73
+    public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
74
+    public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
75
+    public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
76
+    public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
77
+    public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
78
+    public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
79
+    public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
80
+    public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
81
+    public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
82
+
83
+    /**
84
+     * Reference to main server object
85
+     *
86
+     * @var \Sabre\DAV\Server
87
+     */
88
+    private $server;
89
+
90
+    /**
91
+     * @var Tree
92
+     */
93
+    private $tree;
94
+
95
+    /**
96
+     * @var IUserSession
97
+     */
98
+    private $userSession;
99
+
100
+    /**
101
+     * Whether this is public webdav.
102
+     * If true, some returned information will be stripped off.
103
+     *
104
+     * @var bool
105
+     */
106
+    private $isPublic;
107
+
108
+    /**
109
+     * @var bool
110
+     */
111
+    private $downloadAttachment;
112
+
113
+    /**
114
+     * @var IConfig
115
+     */
116
+    private $config;
117
+
118
+    /**
119
+     * @var IRequest
120
+     */
121
+    private $request;
122
+
123
+    /**
124
+     * @var IPreview
125
+     */
126
+    private $previewManager;
127
+
128
+    /**
129
+     * @param Tree $tree
130
+     * @param IConfig $config
131
+     * @param IRequest $request
132
+     * @param IPreview $previewManager
133
+     * @param bool $isPublic
134
+     * @param bool $downloadAttachment
135
+     */
136
+    public function __construct(Tree $tree,
137
+                                IConfig $config,
138
+                                IRequest $request,
139
+                                IPreview $previewManager,
140
+                                IUserSession $userSession,
141
+                                $isPublic = false,
142
+                                $downloadAttachment = true) {
143
+        $this->tree = $tree;
144
+        $this->config = $config;
145
+        $this->request = $request;
146
+        $this->userSession = $userSession;
147
+        $this->isPublic = $isPublic;
148
+        $this->downloadAttachment = $downloadAttachment;
149
+        $this->previewManager = $previewManager;
150
+    }
151
+
152
+    /**
153
+     * This initializes the plugin.
154
+     *
155
+     * This function is called by \Sabre\DAV\Server, after
156
+     * addPlugin is called.
157
+     *
158
+     * This method should set up the required event subscriptions.
159
+     *
160
+     * @param \Sabre\DAV\Server $server
161
+     * @return void
162
+     */
163
+    public function initialize(\Sabre\DAV\Server $server) {
164
+        $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
165
+        $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
166
+        $server->protectedProperties[] = self::FILEID_PROPERTYNAME;
167
+        $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
168
+        $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
169
+        $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
170
+        $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
171
+        $server->protectedProperties[] = self::SIZE_PROPERTYNAME;
172
+        $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
173
+        $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
174
+        $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
175
+        $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
176
+        $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
177
+        $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
178
+        $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
179
+        $server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
180
+        $server->protectedProperties[] = self::SHARE_NOTE;
181
+
182
+        // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
183
+        $allowedProperties = ['{DAV:}getetag'];
184
+        $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
185
+
186
+        $this->server = $server;
187
+        $this->server->on('propFind', [$this, 'handleGetProperties']);
188
+        $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
189
+        $this->server->on('afterBind', [$this, 'sendFileIdHeader']);
190
+        $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
191
+        $this->server->on('afterMethod:GET', [$this,'httpGet']);
192
+        $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
193
+        $this->server->on('afterResponse', function ($request, ResponseInterface $response) {
194
+            $body = $response->getBody();
195
+            if (is_resource($body)) {
196
+                fclose($body);
197
+            }
198
+        });
199
+        $this->server->on('beforeMove', [$this, 'checkMove']);
200
+    }
201
+
202
+    /**
203
+     * Plugin that checks if a move can actually be performed.
204
+     *
205
+     * @param string $source source path
206
+     * @param string $destination destination path
207
+     * @throws Forbidden
208
+     * @throws NotFound
209
+     */
210
+    public function checkMove($source, $destination) {
211
+        $sourceNode = $this->tree->getNodeForPath($source);
212
+        if (!$sourceNode instanceof Node) {
213
+            return;
214
+        }
215
+        [$sourceDir,] = \Sabre\Uri\split($source);
216
+        [$destinationDir,] = \Sabre\Uri\split($destination);
217
+
218
+        if ($sourceDir !== $destinationDir) {
219
+            $sourceNodeFileInfo = $sourceNode->getFileInfo();
220
+            if ($sourceNodeFileInfo === null) {
221
+                throw new NotFound($source . ' does not exist');
222
+            }
223
+
224
+            if (!$sourceNodeFileInfo->isDeletable()) {
225
+                throw new Forbidden($source . " cannot be deleted");
226
+            }
227
+        }
228
+    }
229
+
230
+    /**
231
+     * This sets a cookie to be able to recognize the start of the download
232
+     * the content must not be longer than 32 characters and must only contain
233
+     * alphanumeric characters
234
+     *
235
+     * @param RequestInterface $request
236
+     * @param ResponseInterface $response
237
+     */
238
+    public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
239
+        $queryParams = $request->getQueryParameters();
240
+
241
+        /**
242
+         * this sets a cookie to be able to recognize the start of the download
243
+         * the content must not be longer than 32 characters and must only contain
244
+         * alphanumeric characters
245
+         */
246
+        if (isset($queryParams['downloadStartSecret'])) {
247
+            $token = $queryParams['downloadStartSecret'];
248
+            if (!isset($token[32])
249
+                && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
250
+                // FIXME: use $response->setHeader() instead
251
+                setcookie('ocDownloadStarted', $token, time() + 20, '/');
252
+            }
253
+        }
254
+    }
255
+
256
+    /**
257
+     * Add headers to file download
258
+     *
259
+     * @param RequestInterface $request
260
+     * @param ResponseInterface $response
261
+     */
262
+    public function httpGet(RequestInterface $request, ResponseInterface $response) {
263
+        // Only handle valid files
264
+        $node = $this->tree->getNodeForPath($request->getPath());
265
+        if (!($node instanceof IFile)) {
266
+            return;
267
+        }
268
+
269
+        // adds a 'Content-Disposition: attachment' header in case no disposition
270
+        // header has been set before
271
+        if ($this->downloadAttachment &&
272
+            $response->getHeader('Content-Disposition') === null) {
273
+            $filename = $node->getName();
274
+            if ($this->request->isUserAgent(
275
+                [
276
+                    Request::USER_AGENT_IE,
277
+                    Request::USER_AGENT_ANDROID_MOBILE_CHROME,
278
+                    Request::USER_AGENT_FREEBOX,
279
+                ])) {
280
+                $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
281
+            } else {
282
+                $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
283
+                                                        . '; filename="' . rawurlencode($filename) . '"');
284
+            }
285
+        }
286
+
287
+        if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
288
+            //Add OC-Checksum header
289
+            $checksum = $node->getChecksum();
290
+            if ($checksum !== null && $checksum !== '') {
291
+                $response->addHeader('OC-Checksum', $checksum);
292
+            }
293
+        }
294
+        $response->addHeader('X-Accel-Buffering', 'no');
295
+    }
296
+
297
+    /**
298
+     * Adds all ownCloud-specific properties
299
+     *
300
+     * @param PropFind $propFind
301
+     * @param \Sabre\DAV\INode $node
302
+     * @return void
303
+     */
304
+    public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
305
+        $httpRequest = $this->server->httpRequest;
306
+
307
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
308
+            /**
309
+             * This was disabled, because it made dir listing throw an exception,
310
+             * so users were unable to navigate into folders where one subitem
311
+             * is blocked by the files_accesscontrol app, see:
312
+             * https://github.com/nextcloud/files_accesscontrol/issues/65
313
+             * if (!$node->getFileInfo()->isReadable()) {
314
+             *     // avoid detecting files through this means
315
+             *     throw new NotFound();
316
+             * }
317
+             */
318
+
319
+            $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
320
+                return $node->getFileId();
321
+            });
322
+
323
+            $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
324
+                return $node->getInternalFileId();
325
+            });
326
+
327
+            $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
328
+                $perms = $node->getDavPermissions();
329
+                if ($this->isPublic) {
330
+                    // remove mount information
331
+                    $perms = str_replace(['S', 'M'], '', $perms);
332
+                }
333
+                return $perms;
334
+            });
335
+
336
+            $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
337
+                $user = $this->userSession->getUser();
338
+                if ($user === null) {
339
+                    return null;
340
+                }
341
+                return $node->getSharePermissions(
342
+                    $user->getUID()
343
+                );
344
+            });
345
+
346
+            $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
347
+                $user = $this->userSession->getUser();
348
+                if ($user === null) {
349
+                    return null;
350
+                }
351
+                $ncPermissions = $node->getSharePermissions(
352
+                    $user->getUID()
353
+                );
354
+                $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
355
+                return json_encode($ocmPermissions);
356
+            });
357
+
358
+            $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node) {
359
+                return $node->getETag();
360
+            });
361
+
362
+            $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node) {
363
+                $owner = $node->getOwner();
364
+                if (!$owner) {
365
+                    return null;
366
+                } else {
367
+                    return $owner->getUID();
368
+                }
369
+            });
370
+            $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node) {
371
+                $owner = $node->getOwner();
372
+                if (!$owner) {
373
+                    return null;
374
+                } else {
375
+                    return $owner->getDisplayName();
376
+                }
377
+            });
378
+
379
+            $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
380
+                return json_encode($this->previewManager->isAvailable($node->getFileInfo()));
381
+            });
382
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
383
+                return $node->getSize();
384
+            });
385
+            $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
386
+                return $node->getFileInfo()->getMountPoint()->getMountType();
387
+            });
388
+
389
+            $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest) {
390
+                $user = $this->userSession->getUser();
391
+                if ($user === null) {
392
+                    return null;
393
+                }
394
+                return $node->getNoteFromShare(
395
+                    $user->getUID()
396
+                );
397
+            });
398
+
399
+            $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
400
+                return $this->config->getSystemValue('data-fingerprint', '');
401
+            });
402
+            $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
403
+                return (new \DateTimeImmutable())
404
+                    ->setTimestamp($node->getFileInfo()->getCreationTime())
405
+                    ->format(\DateTimeInterface::ATOM);
406
+            });
407
+            $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
408
+                return $node->getFileInfo()->getCreationTime();
409
+            });
410
+        }
411
+
412
+        if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
413
+            $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
414
+                try {
415
+                    $directDownloadUrl = $node->getDirectDownload();
416
+                    if (isset($directDownloadUrl['url'])) {
417
+                        return $directDownloadUrl['url'];
418
+                    }
419
+                } catch (StorageNotAvailableException $e) {
420
+                    return false;
421
+                } catch (ForbiddenException $e) {
422
+                    return false;
423
+                }
424
+                return false;
425
+            });
426
+
427
+            $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
428
+                $checksum = $node->getChecksum();
429
+                if ($checksum === null || $checksum === '') {
430
+                    return null;
431
+                }
432
+
433
+                return new ChecksumList($checksum);
434
+            });
435
+
436
+            $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
437
+                return $node->getFileInfo()->getUploadTime();
438
+            });
439
+        }
440
+
441
+        if ($node instanceof Directory) {
442
+            $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
443
+                return $node->getSize();
444
+            });
445
+
446
+            $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
447
+                return $node->getFileInfo()->isEncrypted() ? '1' : '0';
448
+            });
449
+
450
+            $requestProperties = $propFind->getRequestedProperties();
451
+            if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
452
+                || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
453
+                $nbFiles = 0;
454
+                $nbFolders = 0;
455
+                foreach ($node->getChildren() as $child) {
456
+                    if ($child instanceof File) {
457
+                        $nbFiles++;
458
+                    } elseif ($child instanceof Directory) {
459
+                        $nbFolders++;
460
+                    }
461
+                }
462
+
463
+                $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
464
+                $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
465
+            }
466
+        }
467
+    }
468
+
469
+    /**
470
+     * translate Nextcloud permissions to OCM Permissions
471
+     *
472
+     * @param $ncPermissions
473
+     * @return array
474
+     */
475
+    protected function ncPermissions2ocmPermissions($ncPermissions) {
476
+        $ocmPermissions = [];
477
+
478
+        if ($ncPermissions & Constants::PERMISSION_SHARE) {
479
+            $ocmPermissions[] = 'share';
480
+        }
481
+
482
+        if ($ncPermissions & Constants::PERMISSION_READ) {
483
+            $ocmPermissions[] = 'read';
484
+        }
485
+
486
+        if (($ncPermissions & Constants::PERMISSION_CREATE) ||
487
+            ($ncPermissions & Constants::PERMISSION_UPDATE)) {
488
+            $ocmPermissions[] = 'write';
489
+        }
490
+
491
+        return $ocmPermissions;
492
+    }
493
+
494
+    /**
495
+     * Update ownCloud-specific properties
496
+     *
497
+     * @param string $path
498
+     * @param PropPatch $propPatch
499
+     *
500
+     * @return void
501
+     */
502
+    public function handleUpdateProperties($path, PropPatch $propPatch) {
503
+        $node = $this->tree->getNodeForPath($path);
504
+        if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
505
+            return;
506
+        }
507
+
508
+        $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
509
+            if (empty($time)) {
510
+                return false;
511
+            }
512
+            $node->touch($time);
513
+            return true;
514
+        });
515
+        $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
516
+            if (empty($etag)) {
517
+                return false;
518
+            }
519
+            if ($node->setEtag($etag) !== -1) {
520
+                return true;
521
+            }
522
+            return false;
523
+        });
524
+        $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
525
+            if (empty($time)) {
526
+                return false;
527
+            }
528
+            $dateTime = new \DateTimeImmutable($time);
529
+            $node->setCreationTime($dateTime->getTimestamp());
530
+            return true;
531
+        });
532
+        $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
533
+            if (empty($time)) {
534
+                return false;
535
+            }
536
+            $node->setCreationTime((int) $time);
537
+            return true;
538
+        });
539
+    }
540
+
541
+    /**
542
+     * @param string $filePath
543
+     * @param \Sabre\DAV\INode $node
544
+     * @throws \Sabre\DAV\Exception\BadRequest
545
+     */
546
+    public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
547
+        // chunked upload handling
548
+        if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
549
+            [$path, $name] = \Sabre\Uri\split($filePath);
550
+            $info = \OC_FileChunking::decodeName($name);
551
+            if (!empty($info)) {
552
+                $filePath = $path . '/' . $info['name'];
553
+            }
554
+        }
555
+
556
+        // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
557
+        if (!$this->server->tree->nodeExists($filePath)) {
558
+            return;
559
+        }
560
+        $node = $this->server->tree->getNodeForPath($filePath);
561
+        if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
562
+            $fileId = $node->getFileId();
563
+            if (!is_null($fileId)) {
564
+                $this->server->httpResponse->setHeader('OC-FileId', $fileId);
565
+            }
566
+        }
567
+    }
568 568
 }
Please login to merge, or discard this patch.
lib/private/Streamer.php 2 patches
Indentation   +138 added lines, -138 removed lines patch added patch discarded remove patch
@@ -39,153 +39,153 @@
 block discarded – undo
39 39
 use ZipStreamer\ZipStreamer;
40 40
 
41 41
 class Streamer {
42
-	// array of regexp. Matching user agents will get tar instead of zip
43
-	private $preferTarFor = [ '/macintosh|mac os x/i' ];
42
+    // array of regexp. Matching user agents will get tar instead of zip
43
+    private $preferTarFor = [ '/macintosh|mac os x/i' ];
44 44
 
45
-	// streamer instance
46
-	private $streamerInstance;
45
+    // streamer instance
46
+    private $streamerInstance;
47 47
 
48
-	/**
49
-	 * Streamer constructor.
50
-	 *
51
-	 * @param IRequest $request
52
-	 * @param int $size The size of the files in bytes
53
-	 * @param int $numberOfFiles The number of files (and directories) that will
54
-	 *        be included in the streamed file
55
-	 */
56
-	public function __construct(IRequest $request, int $size, int $numberOfFiles) {
48
+    /**
49
+     * Streamer constructor.
50
+     *
51
+     * @param IRequest $request
52
+     * @param int $size The size of the files in bytes
53
+     * @param int $numberOfFiles The number of files (and directories) that will
54
+     *        be included in the streamed file
55
+     */
56
+    public function __construct(IRequest $request, int $size, int $numberOfFiles) {
57 57
 
58
-		/**
59
-		 * zip32 constraints for a basic (without compression, volumes nor
60
-		 * encryption) zip file according to the Zip specification:
61
-		 * - No file size is larger than 4 bytes (file size < 4294967296); see
62
-		 *   4.4.9 uncompressed size
63
-		 * - The size of all files plus their local headers is not larger than
64
-		 *   4 bytes; see 4.4.16 relative offset of local header and 4.4.24
65
-		 *   offset of start of central directory with respect to the starting
66
-		 *   disk number
67
-		 * - The total number of entries (files and directories) in the zip file
68
-		 *   is not larger than 2 bytes (number of entries < 65536); see 4.4.22
69
-		 *   total number of entries in the central dir
70
-		 * - The size of the central directory is not larger than 4 bytes; see
71
-		 *   4.4.23 size of the central directory
72
-		 *
73
-		 * Due to all that, zip32 is used if the size is below 4GB and there are
74
-		 * less than 65536 files; the margin between 4*1000^3 and 4*1024^3
75
-		 * should give enough room for the extra zip metadata. Technically, it
76
-		 * would still be possible to create an invalid zip32 file (for example,
77
-		 * a zip file from files smaller than 4GB with a central directory
78
-		 * larger than 4GiB), but it should not happen in the real world.
79
-		 *
80
-		 * We also have to check for a size above 0. As negative sizes could be
81
-		 * from not fully scanned external storage. And then things fall apart
82
-		 * if somebody tries to package to much.
83
-		 */
84
-		if ($size > 0 && $size < 4 * 1000 * 1000 * 1000 && $numberOfFiles < 65536) {
85
-			$this->streamerInstance = new ZipStreamer(['zip64' => false]);
86
-		} elseif ($request->isUserAgent($this->preferTarFor)) {
87
-			$this->streamerInstance = new TarStreamer();
88
-		} else {
89
-			$this->streamerInstance = new ZipStreamer(['zip64' => PHP_INT_SIZE !== 4]);
90
-		}
91
-	}
58
+        /**
59
+         * zip32 constraints for a basic (without compression, volumes nor
60
+         * encryption) zip file according to the Zip specification:
61
+         * - No file size is larger than 4 bytes (file size < 4294967296); see
62
+         *   4.4.9 uncompressed size
63
+         * - The size of all files plus their local headers is not larger than
64
+         *   4 bytes; see 4.4.16 relative offset of local header and 4.4.24
65
+         *   offset of start of central directory with respect to the starting
66
+         *   disk number
67
+         * - The total number of entries (files and directories) in the zip file
68
+         *   is not larger than 2 bytes (number of entries < 65536); see 4.4.22
69
+         *   total number of entries in the central dir
70
+         * - The size of the central directory is not larger than 4 bytes; see
71
+         *   4.4.23 size of the central directory
72
+         *
73
+         * Due to all that, zip32 is used if the size is below 4GB and there are
74
+         * less than 65536 files; the margin between 4*1000^3 and 4*1024^3
75
+         * should give enough room for the extra zip metadata. Technically, it
76
+         * would still be possible to create an invalid zip32 file (for example,
77
+         * a zip file from files smaller than 4GB with a central directory
78
+         * larger than 4GiB), but it should not happen in the real world.
79
+         *
80
+         * We also have to check for a size above 0. As negative sizes could be
81
+         * from not fully scanned external storage. And then things fall apart
82
+         * if somebody tries to package to much.
83
+         */
84
+        if ($size > 0 && $size < 4 * 1000 * 1000 * 1000 && $numberOfFiles < 65536) {
85
+            $this->streamerInstance = new ZipStreamer(['zip64' => false]);
86
+        } elseif ($request->isUserAgent($this->preferTarFor)) {
87
+            $this->streamerInstance = new TarStreamer();
88
+        } else {
89
+            $this->streamerInstance = new ZipStreamer(['zip64' => PHP_INT_SIZE !== 4]);
90
+        }
91
+    }
92 92
 
93
-	/**
94
-	 * Send HTTP headers
95
-	 * @param string $name
96
-	 */
97
-	public function sendHeaders($name) {
98
-		header('X-Accel-Buffering: no');
99
-		$extension = $this->streamerInstance instanceof ZipStreamer ? '.zip' : '.tar';
100
-		$fullName = $name . $extension;
101
-		$this->streamerInstance->sendHeaders($fullName);
102
-	}
93
+    /**
94
+     * Send HTTP headers
95
+     * @param string $name
96
+     */
97
+    public function sendHeaders($name) {
98
+        header('X-Accel-Buffering: no');
99
+        $extension = $this->streamerInstance instanceof ZipStreamer ? '.zip' : '.tar';
100
+        $fullName = $name . $extension;
101
+        $this->streamerInstance->sendHeaders($fullName);
102
+    }
103 103
 
104
-	/**
105
-	 * Stream directory recursively
106
-	 *
107
-	 * @throws NotFoundException
108
-	 * @throws NotPermittedException
109
-	 * @throws InvalidPathException
110
-	 */
111
-	public function addDirRecursive(string $dir, string $internalDir = ''): void {
112
-		$dirname = basename($dir);
113
-		$rootDir = $internalDir . $dirname;
114
-		if (!empty($rootDir)) {
115
-			$this->streamerInstance->addEmptyDir($rootDir);
116
-		}
117
-		$internalDir .= $dirname . '/';
118
-		// prevent absolute dirs
119
-		$internalDir = ltrim($internalDir, '/');
104
+    /**
105
+     * Stream directory recursively
106
+     *
107
+     * @throws NotFoundException
108
+     * @throws NotPermittedException
109
+     * @throws InvalidPathException
110
+     */
111
+    public function addDirRecursive(string $dir, string $internalDir = ''): void {
112
+        $dirname = basename($dir);
113
+        $rootDir = $internalDir . $dirname;
114
+        if (!empty($rootDir)) {
115
+            $this->streamerInstance->addEmptyDir($rootDir);
116
+        }
117
+        $internalDir .= $dirname . '/';
118
+        // prevent absolute dirs
119
+        $internalDir = ltrim($internalDir, '/');
120 120
 
121
-		$userFolder = \OC::$server->getRootFolder()->get(Filesystem::getRoot());
122
-		/** @var Folder $dirNode */
123
-		$dirNode = $userFolder->get($dir);
124
-		$files = $dirNode->getDirectoryListing();
121
+        $userFolder = \OC::$server->getRootFolder()->get(Filesystem::getRoot());
122
+        /** @var Folder $dirNode */
123
+        $dirNode = $userFolder->get($dir);
124
+        $files = $dirNode->getDirectoryListing();
125 125
 
126
-		foreach ($files as $file) {
127
-			if ($file instanceof File) {
128
-				try {
129
-					$fh = $file->fopen('r');
130
-				} catch (NotPermittedException $e) {
131
-					continue;
132
-				}
133
-				$this->addFileFromStream(
134
-					$fh,
135
-					$internalDir . $file->getName(),
136
-					$file->getSize(),
137
-					$file->getMTime()
138
-				);
139
-				fclose($fh);
140
-			} elseif ($file instanceof Folder) {
141
-				if ($file->isReadable()) {
142
-					$this->addDirRecursive($dir . '/' . $file->getName(), $internalDir);
143
-				}
144
-			}
145
-		}
146
-	}
126
+        foreach ($files as $file) {
127
+            if ($file instanceof File) {
128
+                try {
129
+                    $fh = $file->fopen('r');
130
+                } catch (NotPermittedException $e) {
131
+                    continue;
132
+                }
133
+                $this->addFileFromStream(
134
+                    $fh,
135
+                    $internalDir . $file->getName(),
136
+                    $file->getSize(),
137
+                    $file->getMTime()
138
+                );
139
+                fclose($fh);
140
+            } elseif ($file instanceof Folder) {
141
+                if ($file->isReadable()) {
142
+                    $this->addDirRecursive($dir . '/' . $file->getName(), $internalDir);
143
+                }
144
+            }
145
+        }
146
+    }
147 147
 
148
-	/**
149
-	 * Add a file to the archive at the specified location and file name.
150
-	 *
151
-	 * @param string $stream Stream to read data from
152
-	 * @param string $internalName Filepath and name to be used in the archive.
153
-	 * @param int $size Filesize
154
-	 * @param int|bool $time File mtime as int, or false
155
-	 * @return bool $success
156
-	 */
157
-	public function addFileFromStream($stream, $internalName, $size, $time) {
158
-		$options = [];
159
-		if ($time) {
160
-			$options = [
161
-				'timestamp' => $time
162
-			];
163
-		}
148
+    /**
149
+     * Add a file to the archive at the specified location and file name.
150
+     *
151
+     * @param string $stream Stream to read data from
152
+     * @param string $internalName Filepath and name to be used in the archive.
153
+     * @param int $size Filesize
154
+     * @param int|bool $time File mtime as int, or false
155
+     * @return bool $success
156
+     */
157
+    public function addFileFromStream($stream, $internalName, $size, $time) {
158
+        $options = [];
159
+        if ($time) {
160
+            $options = [
161
+                'timestamp' => $time
162
+            ];
163
+        }
164 164
 
165
-		if ($this->streamerInstance instanceof ZipStreamer) {
166
-			return $this->streamerInstance->addFileFromStream($stream, $internalName, $options);
167
-		} else {
168
-			return $this->streamerInstance->addFileFromStream($stream, $internalName, $size, $options);
169
-		}
170
-	}
165
+        if ($this->streamerInstance instanceof ZipStreamer) {
166
+            return $this->streamerInstance->addFileFromStream($stream, $internalName, $options);
167
+        } else {
168
+            return $this->streamerInstance->addFileFromStream($stream, $internalName, $size, $options);
169
+        }
170
+    }
171 171
 
172
-	/**
173
-	 * Add an empty directory entry to the archive.
174
-	 *
175
-	 * @param string $dirName Directory Path and name to be added to the archive.
176
-	 * @return bool $success
177
-	 */
178
-	public function addEmptyDir($dirName) {
179
-		return $this->streamerInstance->addEmptyDir($dirName);
180
-	}
172
+    /**
173
+     * Add an empty directory entry to the archive.
174
+     *
175
+     * @param string $dirName Directory Path and name to be added to the archive.
176
+     * @return bool $success
177
+     */
178
+    public function addEmptyDir($dirName) {
179
+        return $this->streamerInstance->addEmptyDir($dirName);
180
+    }
181 181
 
182
-	/**
183
-	 * Close the archive.
184
-	 * A closed archive can no longer have new files added to it. After
185
-	 * closing, the file is completely written to the output stream.
186
-	 * @return bool $success
187
-	 */
188
-	public function finalize() {
189
-		return $this->streamerInstance->finalize();
190
-	}
182
+    /**
183
+     * Close the archive.
184
+     * A closed archive can no longer have new files added to it. After
185
+     * closing, the file is completely written to the output stream.
186
+     * @return bool $success
187
+     */
188
+    public function finalize() {
189
+        return $this->streamerInstance->finalize();
190
+    }
191 191
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -40,7 +40,7 @@  discard block
 block discarded – undo
40 40
 
41 41
 class Streamer {
42 42
 	// array of regexp. Matching user agents will get tar instead of zip
43
-	private $preferTarFor = [ '/macintosh|mac os x/i' ];
43
+	private $preferTarFor = ['/macintosh|mac os x/i'];
44 44
 
45 45
 	// streamer instance
46 46
 	private $streamerInstance;
@@ -97,7 +97,7 @@  discard block
 block discarded – undo
97 97
 	public function sendHeaders($name) {
98 98
 		header('X-Accel-Buffering: no');
99 99
 		$extension = $this->streamerInstance instanceof ZipStreamer ? '.zip' : '.tar';
100
-		$fullName = $name . $extension;
100
+		$fullName = $name.$extension;
101 101
 		$this->streamerInstance->sendHeaders($fullName);
102 102
 	}
103 103
 
@@ -110,11 +110,11 @@  discard block
 block discarded – undo
110 110
 	 */
111 111
 	public function addDirRecursive(string $dir, string $internalDir = ''): void {
112 112
 		$dirname = basename($dir);
113
-		$rootDir = $internalDir . $dirname;
113
+		$rootDir = $internalDir.$dirname;
114 114
 		if (!empty($rootDir)) {
115 115
 			$this->streamerInstance->addEmptyDir($rootDir);
116 116
 		}
117
-		$internalDir .= $dirname . '/';
117
+		$internalDir .= $dirname.'/';
118 118
 		// prevent absolute dirs
119 119
 		$internalDir = ltrim($internalDir, '/');
120 120
 
@@ -132,14 +132,14 @@  discard block
 block discarded – undo
132 132
 				}
133 133
 				$this->addFileFromStream(
134 134
 					$fh,
135
-					$internalDir . $file->getName(),
135
+					$internalDir.$file->getName(),
136 136
 					$file->getSize(),
137 137
 					$file->getMTime()
138 138
 				);
139 139
 				fclose($fh);
140 140
 			} elseif ($file instanceof Folder) {
141 141
 				if ($file->isReadable()) {
142
-					$this->addDirRecursive($dir . '/' . $file->getName(), $internalDir);
142
+					$this->addDirRecursive($dir.'/'.$file->getName(), $internalDir);
143 143
 				}
144 144
 			}
145 145
 		}
Please login to merge, or discard this patch.
lib/private/legacy/OC_Files.php 1 patch
Indentation   +374 added lines, -374 removed lines patch added patch discarded remove patch
@@ -50,378 +50,378 @@
 block discarded – undo
50 50
  *
51 51
  */
52 52
 class OC_Files {
53
-	public const FILE = 1;
54
-	public const ZIP_FILES = 2;
55
-	public const ZIP_DIR = 3;
56
-
57
-	public const UPLOAD_MIN_LIMIT_BYTES = 1048576; // 1 MiB
58
-
59
-
60
-	private static $multipartBoundary = '';
61
-
62
-	/**
63
-	 * @return string
64
-	 */
65
-	private static function getBoundary() {
66
-		if (empty(self::$multipartBoundary)) {
67
-			self::$multipartBoundary = md5(mt_rand());
68
-		}
69
-		return self::$multipartBoundary;
70
-	}
71
-
72
-	/**
73
-	 * @param string $filename
74
-	 * @param string $name
75
-	 * @param array $rangeArray ('from'=>int,'to'=>int), ...
76
-	 */
77
-	private static function sendHeaders($filename, $name, array $rangeArray) {
78
-		OC_Response::setContentDispositionHeader($name, 'attachment');
79
-		header('Content-Transfer-Encoding: binary', true);
80
-		header('Pragma: public');// enable caching in IE
81
-		header('Expires: 0');
82
-		header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
83
-		$fileSize = \OC\Files\Filesystem::filesize($filename);
84
-		$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
85
-		if ($fileSize > -1) {
86
-			if (!empty($rangeArray)) {
87
-				http_response_code(206);
88
-				header('Accept-Ranges: bytes', true);
89
-				if (count($rangeArray) > 1) {
90
-					$type = 'multipart/byteranges; boundary='.self::getBoundary();
91
-				// no Content-Length header here
92
-				} else {
93
-					header(sprintf('Content-Range: bytes %d-%d/%d', $rangeArray[0]['from'], $rangeArray[0]['to'], $fileSize), true);
94
-					OC_Response::setContentLengthHeader($rangeArray[0]['to'] - $rangeArray[0]['from'] + 1);
95
-				}
96
-			} else {
97
-				OC_Response::setContentLengthHeader($fileSize);
98
-			}
99
-		}
100
-		header('Content-Type: '.$type, true);
101
-		header('X-Accel-Buffering: no');
102
-	}
103
-
104
-	/**
105
-	 * return the content of a file or return a zip file containing multiple files
106
-	 *
107
-	 * @param string $dir
108
-	 * @param string $files ; separated list of files to download
109
-	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
110
-	 */
111
-	public static function get($dir, $files, $params = null) {
112
-		OC_Util::setupFS();
113
-		$view = \OC\Files\Filesystem::getView();
114
-		$getType = self::FILE;
115
-		$filename = $dir;
116
-		try {
117
-			if (is_array($files) && count($files) === 1) {
118
-				$files = $files[0];
119
-			}
120
-
121
-			if (!is_array($files)) {
122
-				$filename = $dir . '/' . $files;
123
-				if (!$view->is_dir($filename)) {
124
-					self::getSingleFile($view, $dir, $files, is_null($params) ? [] : $params);
125
-					return;
126
-				}
127
-			}
128
-
129
-			$name = 'download';
130
-			if (is_array($files)) {
131
-				$getType = self::ZIP_FILES;
132
-				$basename = basename($dir);
133
-				if ($basename) {
134
-					$name = $basename;
135
-				}
136
-
137
-				$filename = $dir . '/' . $name;
138
-			} else {
139
-				$filename = $dir . '/' . $files;
140
-				$getType = self::ZIP_DIR;
141
-				// downloading root ?
142
-				if ($files !== '') {
143
-					$name = $files;
144
-				}
145
-			}
146
-
147
-			self::lockFiles($view, $dir, $files);
148
-
149
-			/* Calculate filesize and number of files */
150
-			if ($getType === self::ZIP_FILES) {
151
-				$fileInfos = [];
152
-				$fileSize = 0;
153
-				foreach ($files as $file) {
154
-					$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $file);
155
-					$fileSize += $fileInfo->getSize();
156
-					$fileInfos[] = $fileInfo;
157
-				}
158
-				$numberOfFiles = self::getNumberOfFiles($fileInfos);
159
-			} elseif ($getType === self::ZIP_DIR) {
160
-				$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $files);
161
-				$fileSize = $fileInfo->getSize();
162
-				$numberOfFiles = self::getNumberOfFiles([$fileInfo]);
163
-			}
164
-
165
-			$streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles);
166
-			OC_Util::obEnd();
167
-
168
-			$streamer->sendHeaders($name);
169
-			$executionTime = (int)OC::$server->get(IniGetWrapper::class)->getNumeric('max_execution_time');
170
-			if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
171
-				@set_time_limit(0);
172
-			}
173
-			ignore_user_abort(true);
174
-
175
-			if ($getType === self::ZIP_FILES) {
176
-				foreach ($files as $file) {
177
-					$file = $dir . '/' . $file;
178
-					if (\OC\Files\Filesystem::is_file($file)) {
179
-						$userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
180
-						$file = $userFolder->get($file);
181
-						if ($file instanceof \OC\Files\Node\File) {
182
-							try {
183
-								$fh = $file->fopen('r');
184
-							} catch (\OCP\Files\NotPermittedException $e) {
185
-								continue;
186
-							}
187
-							$fileSize = $file->getSize();
188
-							$fileTime = $file->getMTime();
189
-						} else {
190
-							// File is not a file? …
191
-							\OC::$server->getLogger()->debug(
192
-								'File given, but no Node available. Name {file}',
193
-								[ 'app' => 'files', 'file' => $file ]
194
-							);
195
-							continue;
196
-						}
197
-						$streamer->addFileFromStream($fh, $file->getName(), $fileSize, $fileTime);
198
-						fclose($fh);
199
-					} elseif (\OC\Files\Filesystem::is_dir($file)) {
200
-						$streamer->addDirRecursive($file);
201
-					}
202
-				}
203
-			} elseif ($getType === self::ZIP_DIR) {
204
-				$file = $dir . '/' . $files;
205
-				$streamer->addDirRecursive($file);
206
-			}
207
-			$streamer->finalize();
208
-			set_time_limit($executionTime);
209
-			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
210
-		} catch (\OCP\Lock\LockedException $ex) {
211
-			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
212
-			OC::$server->getLogger()->logException($ex);
213
-			$l = \OC::$server->getL10N('lib');
214
-			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
215
-			\OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint, 200);
216
-		} catch (\OCP\Files\ForbiddenException $ex) {
217
-			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
218
-			OC::$server->getLogger()->logException($ex);
219
-			$l = \OC::$server->getL10N('lib');
220
-			\OC_Template::printErrorPage($l->t('Cannot read file'), $ex->getMessage(), 200);
221
-		} catch (\Exception $ex) {
222
-			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
223
-			OC::$server->getLogger()->logException($ex);
224
-			$l = \OC::$server->getL10N('lib');
225
-			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
226
-			\OC_Template::printErrorPage($l->t('Cannot read file'), $hint, 200);
227
-		}
228
-	}
229
-
230
-	/**
231
-	 * @param string $rangeHeaderPos
232
-	 * @param int $fileSize
233
-	 * @return array $rangeArray ('from'=>int,'to'=>int), ...
234
-	 */
235
-	private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) {
236
-		$rArray = explode(',', $rangeHeaderPos);
237
-		$minOffset = 0;
238
-		$ind = 0;
239
-
240
-		$rangeArray = [];
241
-
242
-		foreach ($rArray as $value) {
243
-			$ranges = explode('-', $value);
244
-			if (is_numeric($ranges[0])) {
245
-				if ($ranges[0] < $minOffset) { // case: bytes=500-700,601-999
246
-					$ranges[0] = $minOffset;
247
-				}
248
-				if ($ind > 0 && $rangeArray[$ind - 1]['to'] + 1 == $ranges[0]) { // case: bytes=500-600,601-999
249
-					$ind--;
250
-					$ranges[0] = $rangeArray[$ind]['from'];
251
-				}
252
-			}
253
-
254
-			if (is_numeric($ranges[0]) && is_numeric($ranges[1]) && $ranges[0] < $fileSize && $ranges[0] <= $ranges[1]) {
255
-				// case: x-x
256
-				if ($ranges[1] >= $fileSize) {
257
-					$ranges[1] = $fileSize - 1;
258
-				}
259
-				$rangeArray[$ind++] = [ 'from' => $ranges[0], 'to' => $ranges[1], 'size' => $fileSize ];
260
-				$minOffset = $ranges[1] + 1;
261
-				if ($minOffset >= $fileSize) {
262
-					break;
263
-				}
264
-			} elseif (is_numeric($ranges[0]) && $ranges[0] < $fileSize) {
265
-				// case: x-
266
-				$rangeArray[$ind++] = [ 'from' => $ranges[0], 'to' => $fileSize - 1, 'size' => $fileSize ];
267
-				break;
268
-			} elseif (is_numeric($ranges[1])) {
269
-				// case: -x
270
-				if ($ranges[1] > $fileSize) {
271
-					$ranges[1] = $fileSize;
272
-				}
273
-				$rangeArray[$ind++] = [ 'from' => $fileSize - $ranges[1], 'to' => $fileSize - 1, 'size' => $fileSize ];
274
-				break;
275
-			}
276
-		}
277
-		return $rangeArray;
278
-	}
279
-
280
-	/**
281
-	 * @param View $view
282
-	 * @param string $name
283
-	 * @param string $dir
284
-	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
285
-	 */
286
-	private static function getSingleFile($view, $dir, $name, $params) {
287
-		$filename = $dir . '/' . $name;
288
-		$file = null;
289
-
290
-		try {
291
-			$userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
292
-			$file = $userFolder->get($filename);
293
-			if (!$file instanceof \OC\Files\Node\File || !$file->isReadable()) {
294
-				http_response_code(403);
295
-				die('403 Forbidden');
296
-			}
297
-			$fileSize = $file->getSize();
298
-		} catch (\OCP\Files\NotPermittedException $e) {
299
-			http_response_code(403);
300
-			die('403 Forbidden');
301
-		} catch (\OCP\Files\InvalidPathException $e) {
302
-			http_response_code(403);
303
-			die('403 Forbidden');
304
-		} catch (\OCP\Files\NotFoundException $e) {
305
-			http_response_code(404);
306
-			$tmpl = new OC_Template('', '404', 'guest');
307
-			$tmpl->printPage();
308
-			exit();
309
-		}
310
-
311
-		OC_Util::obEnd();
312
-		$view->lockFile($filename, ILockingProvider::LOCK_SHARED);
313
-
314
-		$rangeArray = [];
315
-
316
-		if (isset($params['range']) && substr($params['range'], 0, 6) === 'bytes=') {
317
-			$rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize);
318
-		}
319
-
320
-		self::sendHeaders($filename, $name, $rangeArray);
321
-
322
-		if (isset($params['head']) && $params['head']) {
323
-			return;
324
-		}
325
-
326
-		if (!empty($rangeArray)) {
327
-			try {
328
-				if (count($rangeArray) == 1) {
329
-					$view->readfilePart($filename, $rangeArray[0]['from'], $rangeArray[0]['to']);
330
-				} else {
331
-					// check if file is seekable (if not throw UnseekableException)
332
-					// we have to check it before body contents
333
-					$view->readfilePart($filename, $rangeArray[0]['size'], $rangeArray[0]['size']);
334
-
335
-					$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
336
-
337
-					foreach ($rangeArray as $range) {
338
-						echo "\r\n--".self::getBoundary()."\r\n".
339
-						 "Content-type: ".$type."\r\n".
340
-						 "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n";
341
-						$view->readfilePart($filename, $range['from'], $range['to']);
342
-					}
343
-					echo "\r\n--".self::getBoundary()."--\r\n";
344
-				}
345
-			} catch (\OCP\Files\UnseekableException $ex) {
346
-				// file is unseekable
347
-				header_remove('Accept-Ranges');
348
-				header_remove('Content-Range');
349
-				http_response_code(200);
350
-				self::sendHeaders($filename, $name, []);
351
-				$view->readfile($filename);
352
-			}
353
-		} else {
354
-			$view->readfile($filename);
355
-		}
356
-	}
357
-
358
-	/**
359
-	 * Returns the total (recursive) number of files and folders in the given
360
-	 * FileInfos.
361
-	 *
362
-	 * @param \OCP\Files\FileInfo[] $fileInfos the FileInfos to count
363
-	 * @return int the total number of files and folders
364
-	 */
365
-	private static function getNumberOfFiles($fileInfos) {
366
-		$numberOfFiles = 0;
367
-
368
-		$view = new View();
369
-
370
-		while ($fileInfo = array_pop($fileInfos)) {
371
-			$numberOfFiles++;
372
-
373
-			if ($fileInfo->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
374
-				$fileInfos = array_merge($fileInfos, $view->getDirectoryContent($fileInfo->getPath()));
375
-			}
376
-		}
377
-
378
-		return $numberOfFiles;
379
-	}
380
-
381
-	/**
382
-	 * @param View $view
383
-	 * @param string $dir
384
-	 * @param string[]|string $files
385
-	 */
386
-	public static function lockFiles($view, $dir, $files) {
387
-		if (!is_array($files)) {
388
-			$file = $dir . '/' . $files;
389
-			$files = [$file];
390
-		}
391
-		foreach ($files as $file) {
392
-			$file = $dir . '/' . $file;
393
-			$view->lockFile($file, ILockingProvider::LOCK_SHARED);
394
-			if ($view->is_dir($file)) {
395
-				$contents = $view->getDirectoryContent($file);
396
-				$contents = array_map(function ($fileInfo) use ($file) {
397
-					/** @var \OCP\Files\FileInfo $fileInfo */
398
-					return $file . '/' . $fileInfo->getName();
399
-				}, $contents);
400
-				self::lockFiles($view, $dir, $contents);
401
-			}
402
-		}
403
-	}
404
-
405
-	/**
406
-	 * @param string $dir
407
-	 * @param $files
408
-	 * @param integer $getType
409
-	 * @param View $view
410
-	 * @param string $filename
411
-	 */
412
-	private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) {
413
-		if ($getType === self::FILE) {
414
-			$view->unlockFile($filename, ILockingProvider::LOCK_SHARED);
415
-		}
416
-		if ($getType === self::ZIP_FILES) {
417
-			foreach ($files as $file) {
418
-				$file = $dir . '/' . $file;
419
-				$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
420
-			}
421
-		}
422
-		if ($getType === self::ZIP_DIR) {
423
-			$file = $dir . '/' . $files;
424
-			$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
425
-		}
426
-	}
53
+    public const FILE = 1;
54
+    public const ZIP_FILES = 2;
55
+    public const ZIP_DIR = 3;
56
+
57
+    public const UPLOAD_MIN_LIMIT_BYTES = 1048576; // 1 MiB
58
+
59
+
60
+    private static $multipartBoundary = '';
61
+
62
+    /**
63
+     * @return string
64
+     */
65
+    private static function getBoundary() {
66
+        if (empty(self::$multipartBoundary)) {
67
+            self::$multipartBoundary = md5(mt_rand());
68
+        }
69
+        return self::$multipartBoundary;
70
+    }
71
+
72
+    /**
73
+     * @param string $filename
74
+     * @param string $name
75
+     * @param array $rangeArray ('from'=>int,'to'=>int), ...
76
+     */
77
+    private static function sendHeaders($filename, $name, array $rangeArray) {
78
+        OC_Response::setContentDispositionHeader($name, 'attachment');
79
+        header('Content-Transfer-Encoding: binary', true);
80
+        header('Pragma: public');// enable caching in IE
81
+        header('Expires: 0');
82
+        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
83
+        $fileSize = \OC\Files\Filesystem::filesize($filename);
84
+        $type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
85
+        if ($fileSize > -1) {
86
+            if (!empty($rangeArray)) {
87
+                http_response_code(206);
88
+                header('Accept-Ranges: bytes', true);
89
+                if (count($rangeArray) > 1) {
90
+                    $type = 'multipart/byteranges; boundary='.self::getBoundary();
91
+                // no Content-Length header here
92
+                } else {
93
+                    header(sprintf('Content-Range: bytes %d-%d/%d', $rangeArray[0]['from'], $rangeArray[0]['to'], $fileSize), true);
94
+                    OC_Response::setContentLengthHeader($rangeArray[0]['to'] - $rangeArray[0]['from'] + 1);
95
+                }
96
+            } else {
97
+                OC_Response::setContentLengthHeader($fileSize);
98
+            }
99
+        }
100
+        header('Content-Type: '.$type, true);
101
+        header('X-Accel-Buffering: no');
102
+    }
103
+
104
+    /**
105
+     * return the content of a file or return a zip file containing multiple files
106
+     *
107
+     * @param string $dir
108
+     * @param string $files ; separated list of files to download
109
+     * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
110
+     */
111
+    public static function get($dir, $files, $params = null) {
112
+        OC_Util::setupFS();
113
+        $view = \OC\Files\Filesystem::getView();
114
+        $getType = self::FILE;
115
+        $filename = $dir;
116
+        try {
117
+            if (is_array($files) && count($files) === 1) {
118
+                $files = $files[0];
119
+            }
120
+
121
+            if (!is_array($files)) {
122
+                $filename = $dir . '/' . $files;
123
+                if (!$view->is_dir($filename)) {
124
+                    self::getSingleFile($view, $dir, $files, is_null($params) ? [] : $params);
125
+                    return;
126
+                }
127
+            }
128
+
129
+            $name = 'download';
130
+            if (is_array($files)) {
131
+                $getType = self::ZIP_FILES;
132
+                $basename = basename($dir);
133
+                if ($basename) {
134
+                    $name = $basename;
135
+                }
136
+
137
+                $filename = $dir . '/' . $name;
138
+            } else {
139
+                $filename = $dir . '/' . $files;
140
+                $getType = self::ZIP_DIR;
141
+                // downloading root ?
142
+                if ($files !== '') {
143
+                    $name = $files;
144
+                }
145
+            }
146
+
147
+            self::lockFiles($view, $dir, $files);
148
+
149
+            /* Calculate filesize and number of files */
150
+            if ($getType === self::ZIP_FILES) {
151
+                $fileInfos = [];
152
+                $fileSize = 0;
153
+                foreach ($files as $file) {
154
+                    $fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $file);
155
+                    $fileSize += $fileInfo->getSize();
156
+                    $fileInfos[] = $fileInfo;
157
+                }
158
+                $numberOfFiles = self::getNumberOfFiles($fileInfos);
159
+            } elseif ($getType === self::ZIP_DIR) {
160
+                $fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $files);
161
+                $fileSize = $fileInfo->getSize();
162
+                $numberOfFiles = self::getNumberOfFiles([$fileInfo]);
163
+            }
164
+
165
+            $streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles);
166
+            OC_Util::obEnd();
167
+
168
+            $streamer->sendHeaders($name);
169
+            $executionTime = (int)OC::$server->get(IniGetWrapper::class)->getNumeric('max_execution_time');
170
+            if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
171
+                @set_time_limit(0);
172
+            }
173
+            ignore_user_abort(true);
174
+
175
+            if ($getType === self::ZIP_FILES) {
176
+                foreach ($files as $file) {
177
+                    $file = $dir . '/' . $file;
178
+                    if (\OC\Files\Filesystem::is_file($file)) {
179
+                        $userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
180
+                        $file = $userFolder->get($file);
181
+                        if ($file instanceof \OC\Files\Node\File) {
182
+                            try {
183
+                                $fh = $file->fopen('r');
184
+                            } catch (\OCP\Files\NotPermittedException $e) {
185
+                                continue;
186
+                            }
187
+                            $fileSize = $file->getSize();
188
+                            $fileTime = $file->getMTime();
189
+                        } else {
190
+                            // File is not a file? …
191
+                            \OC::$server->getLogger()->debug(
192
+                                'File given, but no Node available. Name {file}',
193
+                                [ 'app' => 'files', 'file' => $file ]
194
+                            );
195
+                            continue;
196
+                        }
197
+                        $streamer->addFileFromStream($fh, $file->getName(), $fileSize, $fileTime);
198
+                        fclose($fh);
199
+                    } elseif (\OC\Files\Filesystem::is_dir($file)) {
200
+                        $streamer->addDirRecursive($file);
201
+                    }
202
+                }
203
+            } elseif ($getType === self::ZIP_DIR) {
204
+                $file = $dir . '/' . $files;
205
+                $streamer->addDirRecursive($file);
206
+            }
207
+            $streamer->finalize();
208
+            set_time_limit($executionTime);
209
+            self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
210
+        } catch (\OCP\Lock\LockedException $ex) {
211
+            self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
212
+            OC::$server->getLogger()->logException($ex);
213
+            $l = \OC::$server->getL10N('lib');
214
+            $hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
215
+            \OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint, 200);
216
+        } catch (\OCP\Files\ForbiddenException $ex) {
217
+            self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
218
+            OC::$server->getLogger()->logException($ex);
219
+            $l = \OC::$server->getL10N('lib');
220
+            \OC_Template::printErrorPage($l->t('Cannot read file'), $ex->getMessage(), 200);
221
+        } catch (\Exception $ex) {
222
+            self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
223
+            OC::$server->getLogger()->logException($ex);
224
+            $l = \OC::$server->getL10N('lib');
225
+            $hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
226
+            \OC_Template::printErrorPage($l->t('Cannot read file'), $hint, 200);
227
+        }
228
+    }
229
+
230
+    /**
231
+     * @param string $rangeHeaderPos
232
+     * @param int $fileSize
233
+     * @return array $rangeArray ('from'=>int,'to'=>int), ...
234
+     */
235
+    private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) {
236
+        $rArray = explode(',', $rangeHeaderPos);
237
+        $minOffset = 0;
238
+        $ind = 0;
239
+
240
+        $rangeArray = [];
241
+
242
+        foreach ($rArray as $value) {
243
+            $ranges = explode('-', $value);
244
+            if (is_numeric($ranges[0])) {
245
+                if ($ranges[0] < $minOffset) { // case: bytes=500-700,601-999
246
+                    $ranges[0] = $minOffset;
247
+                }
248
+                if ($ind > 0 && $rangeArray[$ind - 1]['to'] + 1 == $ranges[0]) { // case: bytes=500-600,601-999
249
+                    $ind--;
250
+                    $ranges[0] = $rangeArray[$ind]['from'];
251
+                }
252
+            }
253
+
254
+            if (is_numeric($ranges[0]) && is_numeric($ranges[1]) && $ranges[0] < $fileSize && $ranges[0] <= $ranges[1]) {
255
+                // case: x-x
256
+                if ($ranges[1] >= $fileSize) {
257
+                    $ranges[1] = $fileSize - 1;
258
+                }
259
+                $rangeArray[$ind++] = [ 'from' => $ranges[0], 'to' => $ranges[1], 'size' => $fileSize ];
260
+                $minOffset = $ranges[1] + 1;
261
+                if ($minOffset >= $fileSize) {
262
+                    break;
263
+                }
264
+            } elseif (is_numeric($ranges[0]) && $ranges[0] < $fileSize) {
265
+                // case: x-
266
+                $rangeArray[$ind++] = [ 'from' => $ranges[0], 'to' => $fileSize - 1, 'size' => $fileSize ];
267
+                break;
268
+            } elseif (is_numeric($ranges[1])) {
269
+                // case: -x
270
+                if ($ranges[1] > $fileSize) {
271
+                    $ranges[1] = $fileSize;
272
+                }
273
+                $rangeArray[$ind++] = [ 'from' => $fileSize - $ranges[1], 'to' => $fileSize - 1, 'size' => $fileSize ];
274
+                break;
275
+            }
276
+        }
277
+        return $rangeArray;
278
+    }
279
+
280
+    /**
281
+     * @param View $view
282
+     * @param string $name
283
+     * @param string $dir
284
+     * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
285
+     */
286
+    private static function getSingleFile($view, $dir, $name, $params) {
287
+        $filename = $dir . '/' . $name;
288
+        $file = null;
289
+
290
+        try {
291
+            $userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
292
+            $file = $userFolder->get($filename);
293
+            if (!$file instanceof \OC\Files\Node\File || !$file->isReadable()) {
294
+                http_response_code(403);
295
+                die('403 Forbidden');
296
+            }
297
+            $fileSize = $file->getSize();
298
+        } catch (\OCP\Files\NotPermittedException $e) {
299
+            http_response_code(403);
300
+            die('403 Forbidden');
301
+        } catch (\OCP\Files\InvalidPathException $e) {
302
+            http_response_code(403);
303
+            die('403 Forbidden');
304
+        } catch (\OCP\Files\NotFoundException $e) {
305
+            http_response_code(404);
306
+            $tmpl = new OC_Template('', '404', 'guest');
307
+            $tmpl->printPage();
308
+            exit();
309
+        }
310
+
311
+        OC_Util::obEnd();
312
+        $view->lockFile($filename, ILockingProvider::LOCK_SHARED);
313
+
314
+        $rangeArray = [];
315
+
316
+        if (isset($params['range']) && substr($params['range'], 0, 6) === 'bytes=') {
317
+            $rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize);
318
+        }
319
+
320
+        self::sendHeaders($filename, $name, $rangeArray);
321
+
322
+        if (isset($params['head']) && $params['head']) {
323
+            return;
324
+        }
325
+
326
+        if (!empty($rangeArray)) {
327
+            try {
328
+                if (count($rangeArray) == 1) {
329
+                    $view->readfilePart($filename, $rangeArray[0]['from'], $rangeArray[0]['to']);
330
+                } else {
331
+                    // check if file is seekable (if not throw UnseekableException)
332
+                    // we have to check it before body contents
333
+                    $view->readfilePart($filename, $rangeArray[0]['size'], $rangeArray[0]['size']);
334
+
335
+                    $type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
336
+
337
+                    foreach ($rangeArray as $range) {
338
+                        echo "\r\n--".self::getBoundary()."\r\n".
339
+                            "Content-type: ".$type."\r\n".
340
+                            "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n";
341
+                        $view->readfilePart($filename, $range['from'], $range['to']);
342
+                    }
343
+                    echo "\r\n--".self::getBoundary()."--\r\n";
344
+                }
345
+            } catch (\OCP\Files\UnseekableException $ex) {
346
+                // file is unseekable
347
+                header_remove('Accept-Ranges');
348
+                header_remove('Content-Range');
349
+                http_response_code(200);
350
+                self::sendHeaders($filename, $name, []);
351
+                $view->readfile($filename);
352
+            }
353
+        } else {
354
+            $view->readfile($filename);
355
+        }
356
+    }
357
+
358
+    /**
359
+     * Returns the total (recursive) number of files and folders in the given
360
+     * FileInfos.
361
+     *
362
+     * @param \OCP\Files\FileInfo[] $fileInfos the FileInfos to count
363
+     * @return int the total number of files and folders
364
+     */
365
+    private static function getNumberOfFiles($fileInfos) {
366
+        $numberOfFiles = 0;
367
+
368
+        $view = new View();
369
+
370
+        while ($fileInfo = array_pop($fileInfos)) {
371
+            $numberOfFiles++;
372
+
373
+            if ($fileInfo->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
374
+                $fileInfos = array_merge($fileInfos, $view->getDirectoryContent($fileInfo->getPath()));
375
+            }
376
+        }
377
+
378
+        return $numberOfFiles;
379
+    }
380
+
381
+    /**
382
+     * @param View $view
383
+     * @param string $dir
384
+     * @param string[]|string $files
385
+     */
386
+    public static function lockFiles($view, $dir, $files) {
387
+        if (!is_array($files)) {
388
+            $file = $dir . '/' . $files;
389
+            $files = [$file];
390
+        }
391
+        foreach ($files as $file) {
392
+            $file = $dir . '/' . $file;
393
+            $view->lockFile($file, ILockingProvider::LOCK_SHARED);
394
+            if ($view->is_dir($file)) {
395
+                $contents = $view->getDirectoryContent($file);
396
+                $contents = array_map(function ($fileInfo) use ($file) {
397
+                    /** @var \OCP\Files\FileInfo $fileInfo */
398
+                    return $file . '/' . $fileInfo->getName();
399
+                }, $contents);
400
+                self::lockFiles($view, $dir, $contents);
401
+            }
402
+        }
403
+    }
404
+
405
+    /**
406
+     * @param string $dir
407
+     * @param $files
408
+     * @param integer $getType
409
+     * @param View $view
410
+     * @param string $filename
411
+     */
412
+    private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) {
413
+        if ($getType === self::FILE) {
414
+            $view->unlockFile($filename, ILockingProvider::LOCK_SHARED);
415
+        }
416
+        if ($getType === self::ZIP_FILES) {
417
+            foreach ($files as $file) {
418
+                $file = $dir . '/' . $file;
419
+                $view->unlockFile($file, ILockingProvider::LOCK_SHARED);
420
+            }
421
+        }
422
+        if ($getType === self::ZIP_DIR) {
423
+            $file = $dir . '/' . $files;
424
+            $view->unlockFile($file, ILockingProvider::LOCK_SHARED);
425
+        }
426
+    }
427 427
 }
Please login to merge, or discard this patch.