apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Files\PublicFiles;
use OC\Files\Node\Folder;
use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCP\Files\FileInfo;
use OCP\Files\NotFoundException;
use Sabre\DAV\Exception\InsufficientStorage;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Xml\Property\GetLastModified;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Class PublicFilesPlugin - additional PROPFIND properties for public shared
* files and folders are handled with this plugin
*
* @package OCA\DAV\Files\PublicFiles
*/
class PublicFilesPlugin extends ServerPlugin {
public const PUBLIC_LINK_ITEM_TYPE = '{http://owncloud.org/ns}public-link-item-type';
public const PUBLIC_LINK_PERMISSION = '{http://owncloud.org/ns}public-link-permission';
public const PUBLIC_LINK_EXPIRATION = '{http://owncloud.org/ns}public-link-expiration';
public const PUBLIC_LINK_SHARE_DATETIME = '{http://owncloud.org/ns}public-link-share-datetime';
public const PUBLIC_LINK_SHARE_OWNER = '{http://owncloud.org/ns}public-link-share-owner';
/** @var Server */
private $server;
public function initialize(Server $server) {
$this->server = $server;
$this->server->on('propFind', [$this, 'propFind']);
$this->server->on('beforeMethod:PUT', [$this, 'beforePut'], 1);
$this->server->on('beforeWriteContent', [$this, 'handleBeforeWriteContent'], 10);
$this->server->on('beforeCreateFile', [$this, 'handleBeforeCreateFile'], 10);
}
public function beforePut(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
if (!$this->server->tree->nodeExists($path)) {
return;
}
list($parentPath, ) = \Sabre\Uri\split($path);
$parent = $this->server->tree->getNodeForPath($parentPath);
// only in share roots of file drop folders auto renaming will be applied
if (!$parent instanceof PublicSharedRootNode) {
return;
}
if (!$parent->isFileDropFolder()) {
return;
}
// node already exists at target path - the path will be rewritten
$newPath = $this->newFileName($path);
$request->setUrl($request->getBaseUrl() . $newPath);
}
private function newFileName($path) {
$pathInfo = \pathinfo($path);
$dirName = $pathInfo['dirname'];
$fileName = $pathInfo['filename'];
$ext = $pathInfo['extension'];
$i = 2;
while ($this->server->tree->nodeExists("$dirName/$fileName ($i).$ext")) {
$i++;
}
return "$dirName/$fileName ($i).$ext";
}
public function propFind(PropFind $propFind, INode $node) {
// properties about the share
if ($node instanceof PublicSharedRootNode) {
$propFind->handle(self::PUBLIC_LINK_ITEM_TYPE, static function () use ($node) {
return $node->getShare()->getNodeType();
});
$propFind->handle(self::PUBLIC_LINK_PERMISSION, static function () use ($node) {
return $node->getShare()->getPermissions();
});
$propFind->handle(self::PUBLIC_LINK_EXPIRATION, static function () use ($node) {
$expire = $node->getShare()->getExpirationDate();
if ($expire) {
return new GetLastModified($expire);
}
return null;
});
$propFind->handle(self::PUBLIC_LINK_SHARE_DATETIME, static function () use ($node) {
return new GetLastModified($node->getShare()->getShareTime());
});
$propFind->handle(self::PUBLIC_LINK_SHARE_OWNER, static function () use ($node) {
return $node->getShare()->getShareOwner();
});
$propFind->handle(FilesPlugin::PERMISSIONS_PROPERTYNAME, static function () use ($node) {
return $node->getPermissions();
});
}
// properties about the resources within the public link
if ($node instanceof IPublicSharedNode) {
$propFind->handle(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, static function () use ($node) {
return $node->getNode()->getId();
});
$propFind->handle(FilesPlugin::PERMISSIONS_PROPERTYNAME, static function () use ($node) {
return $node->getDavPermissions();
});
$propFind->handle(FilesPlugin::OWNER_ID_PROPERTYNAME, static function () use ($node) {
$owner = $node->getNode()->getOwner();
return $owner->getUID();
});
$propFind->handle(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, static function () use ($node) {
$owner = $node->getNode()->getOwner();
return $owner->getDisplayName();
});
$propFind->handle(FilesPlugin::SIZE_PROPERTYNAME, static function () use ($node) {
return $node->getNode()->getSize();
});
if ($node->getNode()->getType() === FileInfo::TYPE_FILE) {
$server = $this->server;
$propFind->handle(FilesPlugin::DOWNLOADURL_PROPERTYNAME, static function () use ($node, $server) {
$share = $node->getShare();
$shareNode = $share->getNode();
// We want to get the relative path of the shared file.
// If the shared resource is a folder e.g.
// - <shared folder>/
// - subfolder/
// - meme.jpg
// - somefile.txt
// And we want the path of 'meme.jpg' we expect the resource
// path to be '/subfolder/meme.jpg'.
// If the shared resource is a file we can just take the
// name of that file prefixed with a slash like '/cool.gif'
if ($shareNode->getType() === FileInfo::TYPE_FOLDER) {
$shareRoot = $shareNode->getPath();
$sharedResourcePath = \substr($node->getNode()->getPath(), \strlen($shareRoot));
} else {
$sharedResourcePath = '/' . $shareNode->getName();
}
$path = \OC::$server->getURLGenerator()->getAbsoluteURL('/remote.php/dav/' . $server->getRequestUri());
// Let's assume we have this share
// - <shared folder>/
// - subfolder/
// - meme.jpg
// - somefile.txt
// If the PROPFIND request is done against
// 'remote.php/dav/public-files/{token}/subfolder/meme.jpg'
// then we don't need to append the file name for response as
// it is already in the path.
// Otherwise if the PROPFIND is done against
// 'remote.php/dav/public-files/{token}/subfolder/'
// then we need to append the file name to the path.
if (\substr($path, -\strlen($sharedResourcePath)) !== $sharedResourcePath) {
$path .= '/' . $node->getNode()->getName();
}
if ($share->getPassword() === null) {
return $path;
}
$validUntil = new \DateTime();
$validUntil->add(new \DateInterval("PT30M")); // valid for 30 minutes
$key = \hash_hkdf('sha256', $share->getPassword());
$s = new PublicShareSigner($share->getToken(), $sharedResourcePath, $validUntil, $key);
return $path . '?signature=' . $s->getSignature() . '&expires=' . \urlencode($validUntil->format(\DateTime::ATOM));
});
}
}
}
/**
* Check quota before writing content
*
* @param string $uri target file URI
* @param INode $node Sabre Node
* @param resource $data data
* @param bool $modified modified
* @return bool|void
* @throws InsufficientStorage
* @throws NotFoundException
*/
public function handleBeforeWriteContent($uri, $node, $data, $modified) {
if (!$node instanceof SharedFile) {
return;
}
$node = $node->getShare()->getNode();
if (!$node instanceof Folder) {
return;
}
return $this->checkQuota($node);
}
/**
* Check quota before creating file
*
* @param string $uri target file URI
* @param resource $data data
* @param INode $parent Sabre Node
* @param bool $modified modified
* @return bool
* @throws InsufficientStorage
* @throws NotFound
*/
public function handleBeforeCreateFile($uri, $data, $parent, $modified) {
$share = null;
if ($parent instanceof SharedFolder) {
$share = $parent->getShare();
}
if ($parent instanceof PublicSharedRootNode) {
$share = $parent->getShare();
}
if ($share === null) {
return;
}
try {
$node = $share->getNode();
} catch (NotFoundException $e) {
throw new NotFound();
}
if (!$node instanceof Folder) {
return;
}
return $this->checkQuota($node);
}
/**
* This method is called before any HTTP method and validates there is enough free space to store the file
*
* @param Folder $folder
* @param int $length size to check whether it fits
* @return bool
* @throws InsufficientStorage
*/
public function checkQuota(Folder $folder, $length = null) {
if ($length === null) {
$length = $this->getLength();
}
if ($length) {
$freeSpace = $folder->getFreeSpace();
if ($freeSpace !== FileInfo::SPACE_UNKNOWN && $freeSpace !== FileInfo::SPACE_UNLIMITED && $length > $freeSpace) {
throw new InsufficientStorage();
}
}
return true;
}
public function getLength() {
$req = $this->server->httpRequest;
$length = $req->getHeader('X-Expected-Entity-Length');
if (!\is_numeric($length)) {
$length = $req->getHeader('Content-Length');
$length = \is_numeric($length) ? $length : null;
}
$ocLength = $req->getHeader('OC-Total-Length');
if (\is_numeric($length) && \is_numeric($ocLength)) {
return \max($length, $ocLength);
}
return $length;
}
}