2016-10-22 16:13:18 -04:00
|
|
|
<?php
|
|
|
|
|
/**
|
2024-05-27 11:39:07 -04:00
|
|
|
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
2016-10-22 16:13:18 -04:00
|
|
|
*/
|
|
|
|
|
namespace OCA\DAV\Files\Sharing;
|
|
|
|
|
|
2025-05-14 11:02:44 -04:00
|
|
|
use OCP\Files\Folder;
|
2024-07-17 10:48:47 -04:00
|
|
|
use OCP\Share\IShare;
|
2016-12-02 04:03:02 -05:00
|
|
|
use Sabre\DAV\Exception\MethodNotAllowed;
|
2016-10-22 16:13:18 -04:00
|
|
|
use Sabre\DAV\ServerPlugin;
|
|
|
|
|
use Sabre\HTTP\RequestInterface;
|
|
|
|
|
use Sabre\HTTP\ResponseInterface;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make sure that the destination is writable
|
|
|
|
|
*/
|
|
|
|
|
class FilesDropPlugin extends ServerPlugin {
|
|
|
|
|
|
2024-07-17 10:48:47 -04:00
|
|
|
private ?IShare $share = null;
|
|
|
|
|
private bool $enabled = false;
|
2016-10-22 16:13:18 -04:00
|
|
|
|
2024-07-17 10:48:47 -04:00
|
|
|
public function setShare(IShare $share): void {
|
|
|
|
|
$this->share = $share;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function enable(): void {
|
2016-10-24 15:44:12 -04:00
|
|
|
$this->enabled = true;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-22 16:13:18 -04:00
|
|
|
/**
|
|
|
|
|
* This initializes the plugin.
|
2025-05-11 08:56:59 -04:00
|
|
|
* It is ONLY initialized by the server on a file drop request.
|
2016-10-22 16:13:18 -04:00
|
|
|
*/
|
2024-07-17 10:48:47 -04:00
|
|
|
public function initialize(\Sabre\DAV\Server $server): void {
|
2020-03-09 11:32:04 -04:00
|
|
|
$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
|
2025-05-11 08:56:59 -04:00
|
|
|
$server->on('method:MKCOL', [$this, 'onMkcol']);
|
2016-10-24 15:44:12 -04:00
|
|
|
$this->enabled = false;
|
2016-10-22 16:13:18 -04:00
|
|
|
}
|
|
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
public function onMkcol(RequestInterface $request, ResponseInterface $response) {
|
2025-05-14 11:02:44 -04:00
|
|
|
if (!$this->enabled || $this->share === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$node = $this->share->getNode();
|
|
|
|
|
if (!($node instanceof Folder)) {
|
2016-10-24 15:44:12 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
// If this is a folder creation request we need
|
|
|
|
|
// to fake a success so we can pretend every
|
|
|
|
|
// folder now exists.
|
|
|
|
|
$response->setStatus(201);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
|
2025-05-14 11:02:44 -04:00
|
|
|
if (!$this->enabled || $this->share === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$node = $this->share->getNode();
|
|
|
|
|
if (!($node instanceof Folder)) {
|
2025-05-11 08:56:59 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Retrieve the nickname from the request
|
|
|
|
|
$nickname = $request->hasHeader('X-NC-Nickname')
|
|
|
|
|
? trim(urldecode($request->getHeader('X-NC-Nickname')))
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
//
|
2024-07-17 10:48:47 -04:00
|
|
|
if ($request->getMethod() !== 'PUT') {
|
2025-05-11 08:56:59 -04:00
|
|
|
// If uploading subfolders we need to ensure they get created
|
|
|
|
|
// within the nickname folder
|
|
|
|
|
if ($request->getMethod() === 'MKCOL') {
|
|
|
|
|
if (!$nickname) {
|
|
|
|
|
throw new MethodNotAllowed('A nickname header is required when uploading subfolders');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw new MethodNotAllowed('Only PUT is allowed on files drop');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this is a folder creation request
|
|
|
|
|
// let's stop there and let the onMkcol handle it
|
|
|
|
|
if ($request->getMethod() === 'MKCOL') {
|
|
|
|
|
return;
|
2016-10-22 16:13:18 -04:00
|
|
|
}
|
|
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
// Now if we create a file, we need to create the
|
|
|
|
|
// full path along the way. We'll only handle conflict
|
|
|
|
|
// resolution on file conflicts, but not on folders.
|
|
|
|
|
|
|
|
|
|
// e.g files/dCP8yn3N86EK9sL/Folder/image.jpg
|
|
|
|
|
$path = $request->getPath();
|
|
|
|
|
$token = $this->share->getToken();
|
|
|
|
|
|
|
|
|
|
// e.g files/dCP8yn3N86EK9sL
|
|
|
|
|
$rootPath = substr($path, 0, strpos($path, $token) + strlen($token));
|
|
|
|
|
// e.g /Folder/image.jpg
|
|
|
|
|
$relativePath = substr($path, strlen($rootPath));
|
|
|
|
|
$isRootUpload = substr_count($relativePath, '/') === 1;
|
2016-10-22 16:13:18 -04:00
|
|
|
|
2024-07-17 10:48:47 -04:00
|
|
|
// Extract the attributes for the file request
|
2024-07-18 14:59:50 -04:00
|
|
|
$isFileRequest = false;
|
2024-07-17 10:48:47 -04:00
|
|
|
$attributes = $this->share->getAttributes();
|
2024-07-18 14:59:50 -04:00
|
|
|
if ($attributes !== null) {
|
|
|
|
|
$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
|
|
|
|
|
}
|
2024-07-17 10:48:47 -04:00
|
|
|
|
|
|
|
|
// We need a valid nickname for file requests
|
2025-05-11 08:56:59 -04:00
|
|
|
if ($isFileRequest && !$nickname) {
|
|
|
|
|
throw new MethodNotAllowed('A nickname header is required for file requests');
|
2024-07-17 10:48:47 -04:00
|
|
|
}
|
2025-04-14 09:16:44 -04:00
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
// We're only allowing the upload of
|
|
|
|
|
// long path with subfolders if a nickname is set.
|
|
|
|
|
// This prevents confusion when uploading files and help
|
|
|
|
|
// classify them by uploaders.
|
|
|
|
|
if (!$nickname && !$isRootUpload) {
|
|
|
|
|
throw new MethodNotAllowed('A nickname header is required when uploading subfolders');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we have a nickname, let's put everything inside
|
|
|
|
|
if ($nickname) {
|
2024-07-17 10:48:47 -04:00
|
|
|
// Put all files in the subfolder
|
2025-05-11 08:56:59 -04:00
|
|
|
$relativePath = '/' . $nickname . '/' . $relativePath;
|
|
|
|
|
$relativePath = str_replace('//', '/', $relativePath);
|
2024-07-17 10:48:47 -04:00
|
|
|
}
|
2025-04-14 09:16:44 -04:00
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
// Create the folders along the way
|
|
|
|
|
$folders = $this->getPathSegments(dirname($relativePath));
|
|
|
|
|
foreach ($folders as $folder) {
|
|
|
|
|
if ($folder === '') {
|
|
|
|
|
continue;
|
|
|
|
|
} // skip empty parts
|
2025-05-14 11:02:44 -04:00
|
|
|
if (!$node->nodeExists($folder)) {
|
|
|
|
|
$node->newFolder($folder);
|
2025-05-11 08:56:59 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finally handle conflicts on the end files
|
2025-05-14 11:02:44 -04:00
|
|
|
/** @var Folder */
|
|
|
|
|
$folder = $node->get(dirname($relativePath));
|
|
|
|
|
$uniqueName = $folder->getNonExistingName(basename(($relativePath)));
|
|
|
|
|
$path = '/files/' . $token . '/' . dirname($relativePath) . '/' . $uniqueName;
|
2025-05-11 08:56:59 -04:00
|
|
|
$url = $request->getBaseUrl() . str_replace('//', '/', $path);
|
2016-12-02 04:03:02 -05:00
|
|
|
$request->setUrl($url);
|
2016-10-22 16:13:18 -04:00
|
|
|
}
|
2024-07-17 10:48:47 -04:00
|
|
|
|
2025-05-11 08:56:59 -04:00
|
|
|
private function getPathSegments(string $path): array {
|
|
|
|
|
// Normalize slashes and remove trailing slash
|
|
|
|
|
$path = rtrim(str_replace('\\', '/', $path), '/');
|
|
|
|
|
|
|
|
|
|
// Handle absolute paths starting with /
|
|
|
|
|
$isAbsolute = str_starts_with($path, '/');
|
|
|
|
|
|
|
|
|
|
$segments = explode('/', $path);
|
|
|
|
|
|
|
|
|
|
// Add back the leading slash for the first segment if needed
|
|
|
|
|
$result = [];
|
|
|
|
|
$current = $isAbsolute ? '/' : '';
|
|
|
|
|
|
|
|
|
|
foreach ($segments as $segment) {
|
|
|
|
|
if ($segment === '') {
|
|
|
|
|
// skip empty parts
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$current = rtrim($current, '/') . '/' . $segment;
|
|
|
|
|
$result[] = $current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2016-10-22 16:13:18 -04:00
|
|
|
}
|