owncloud/core

View on GitHub
apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php

Summary

Maintainability
C
1 day
Test Coverage
<?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;
    }
}