migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,75 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAVACL\Xml\Property\Principal;
class LegacyDAVACL extends DavAclPlugin {
/**
* @inheritdoc
*/
public function getCurrentUserPrincipals() {
$principalV2 = $this->getCurrentUserPrincipal();
if (is_null($principalV2)) {
return [];
}
$principalV1 = $this->convertPrincipal($principalV2, false);
return array_merge(
[
$principalV2,
$principalV1
],
$this->getPrincipalMembership($principalV1)
);
}
private function convertPrincipal($principal, $toV2) {
[, $name] = \Sabre\Uri\split($principal);
if ($toV2) {
return "principals/users/$name";
}
return "principals/$name";
}
public function propFind(PropFind $propFind, INode $node) {
/* Overload current-user-principal */
$propFind->handle('{DAV:}current-user-principal', function () {
if ($url = parent::getCurrentUserPrincipal()) {
return new Principal(Principal::HREF, $url . '/');
} else {
return new Principal(Principal::UNAUTHENTICATED);
}
});
return parent::propFind($propFind, $node);
}
}
@@ -0,0 +1,127 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Sabre\DAV\Auth\Backend\AbstractBasic;
/**
* Class PublicAuth
*
* @package OCA\DAV\Connector
*/
class PublicAuth extends AbstractBasic {
private const BRUTEFORCE_ACTION = 'public_webdav_auth';
private ?IShare $share = null;
private IManager $shareManager;
private ISession $session;
private IRequest $request;
private IThrottler $throttler;
public function __construct(IRequest $request,
IManager $shareManager,
ISession $session,
IThrottler $throttler) {
$this->request = $request;
$this->shareManager = $shareManager;
$this->session = $session;
$this->throttler = $throttler;
// setup realm
$defaults = new \OCP\Defaults();
$this->realm = $defaults->getName();
}
/**
* Validates a username and password
*
* This method should return true or false depending on if login
* succeeded.
*
* @param string $username
* @param string $password
* @return bool
* @throws \Sabre\DAV\Exception\NotAuthenticated
*/
protected function validateUserPass($username, $password) {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
try {
$share = $this->shareManager->getShareByToken($username);
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
$this->share = $share;
\OC_User::setIncognitoMode(true);
// check if the share is password protected
if ($share->getPassword() !== null) {
if ($share->getShareType() === IShare::TYPE_LINK
|| $share->getShareType() === IShare::TYPE_EMAIL
|| $share->getShareType() === IShare::TYPE_CIRCLE) {
if ($this->shareManager->checkPassword($share, $password)) {
return true;
} elseif ($this->session->exists('public_link_authenticated')
&& $this->session->get('public_link_authenticated') === (string)$share->getId()) {
return true;
} else {
if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {
// do not re-authenticate over ajax, use dummy auth name to prevent browser popup
http_response_code(401);
header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"');
throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
}
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
return true;
} else {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
}
return true;
}
public function getShare(): IShare {
assert($this->share !== null);
return $this->share;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
*
* @author Bastien Durel <bastien@durel.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\CorePlugin;
use Sabre\DAV\FS\Directory;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class AnonymousOptionsPlugin extends ServerPlugin {
/**
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
// before auth
$this->server->on('beforeMethod:*', [$this, 'handleAnonymousOptions'], 9);
}
/**
* @return bool
*/
public function isRequestInRoot($path) {
return $path === '' || (is_string($path) && !str_contains($path, '/'));
}
/**
* @throws \Sabre\DAV\Exception\Forbidden
* @return bool
*/
public function handleAnonymousOptions(RequestInterface $request, ResponseInterface $response) {
$isOffice = preg_match('/Microsoft Office/i', $request->getHeader('User-Agent') ?? '');
$emptyAuth = $request->getHeader('Authorization') === null
|| $request->getHeader('Authorization') === ''
|| trim($request->getHeader('Authorization')) === 'Bearer';
$isAnonymousOfficeOption = $request->getMethod() === 'OPTIONS' && $isOffice && $emptyAuth;
$isOfficeHead = $request->getMethod() === 'HEAD' && $isOffice && $emptyAuth;
if ($isAnonymousOfficeOption || $isOfficeHead) {
/** @var CorePlugin $corePlugin */
$corePlugin = $this->server->getPlugin('core');
// setup a fake tree for anonymous access
$this->server->tree = new Tree(new Directory(''));
$corePlugin->httpOptions($request, $response);
$this->server->emit('afterMethod:*', [$request, $response]);
$this->server->emit('afterMethod:OPTIONS', [$request, $response]);
$this->server->sapi->sendResponse($response);
return false;
}
}
}
@@ -0,0 +1,128 @@
<?php
/**
* @copyright Copyright (c) 2023 Claus-Justus Heine
*
* @author Claus-Justus Heine <himself@claus-justus-heine.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* A plugin which tries to work-around peculiarities of the MacOS DAV client
* apps. The following problems are addressed:
*
* - OSX calendar client sends REPORT requests to a random principal
* collection but expects to find all principals (forgot to set
* {DAV:}principal-property-search flag?)
*/
class AppleQuirksPlugin extends ServerPlugin {
/*
private const OSX_CALENDAR_AGENT = 'CalendarAgent';
private const OSX_DATAACCESSD_AGENT = 'dataaccessd';
private const OSX_ACCOUNTSD_AGENT = 'accountsd';
private const OSX_CONTACTS_AGENT = 'AddressBookCore';
*/
private const OSX_AGENT_PREFIX = 'macOS';
/** @var bool */
private $isMacOSDavAgent = false;
/**
* Sets up the plugin.
*
* This method is automatically called by the server class.
*
* @return void
*/
public function initialize(Server $server) {
$server->on('beforeMethod:REPORT', [$this, 'beforeReport'], 0);
$server->on('report', [$this, 'report'], 0);
}
/**
* Triggered before any method is handled.
*
* @return void
*/
public function beforeReport(RequestInterface $request, ResponseInterface $response) {
$userAgent = $request->getRawServerValue('HTTP_USER_AGENT') ?? 'unknown';
$this->isMacOSDavAgent = $this->isMacOSUserAgent($userAgent);
}
/**
* This method handles HTTP REPORT requests.
*
* @param string $reportName
* @param mixed $report
* @param mixed $path
*
* @return bool
*/
public function report($reportName, $report, $path) {
if ($reportName == '{DAV:}principal-property-search' && $this->isMacOSDavAgent) {
/** @var \Sabre\DAVACL\Xml\Request\PrincipalPropertySearchReport $report */
$report->applyToPrincipalCollectionSet = true;
}
return true;
}
/**
* Check whether the given $userAgent string pretends to originate from OSX.
*
* @param string $userAgent
*
* @return bool
*/
protected function isMacOSUserAgent(string $userAgent):bool {
return str_starts_with(self::OSX_AGENT_PREFIX, $userAgent);
}
/**
* Decode the given OSX DAV agent string.
*
* @param string $agent
*
* @return null|array
*/
protected function decodeMacOSAgentString(string $userAgent):?array {
// OSX agent string is like: macOS/13.2.1 (22D68) dataaccessd/1.0
if (preg_match('|^' . self::OSX_AGENT_PREFIX . '/([0-9]+)\\.([0-9]+)\\.([0-9]+)\s+\((\w+)\)\s+([^/]+)/([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?$|i', $userAgent, $matches)) {
return [
'macOSVersion' => [
'major' => $matches[1],
'minor' => $matches[2],
'patch' => $matches[3],
],
'macOSAgent' => $matches[5],
'macOSAgentVersion' => [
'major' => $matches[6],
'minor' => $matches[7] ?? null,
'patch' => $matches[8] ?? null,
],
];
}
return null;
}
}
@@ -0,0 +1,239 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jakob Sack <mail@jakobsack.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Markus Goetz <markus@woboq.com>
* @author Michael Gapczynski <GapczynskiM@gmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use Exception;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
private ISession $session;
private Session $userSession;
private IRequest $request;
private ?string $currentUser = null;
private Manager $twoFactorManager;
private IThrottler $throttler;
public function __construct(ISession $session,
Session $userSession,
IRequest $request,
Manager $twoFactorManager,
IThrottler $throttler,
string $principalPrefix = 'principals/users/') {
$this->session = $session;
$this->userSession = $userSession;
$this->twoFactorManager = $twoFactorManager;
$this->request = $request;
$this->throttler = $throttler;
$this->principalPrefix = $principalPrefix;
// setup realm
$defaults = new \OCP\Defaults();
$this->realm = $defaults->getName();
}
/**
* Whether the user has initially authenticated via DAV
*
* This is required for WebDAV clients that resent the cookies even when the
* account was changed.
*
* @see https://github.com/owncloud/core/issues/13245
*/
public function isDavAuthenticated(string $username): bool {
return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
$this->session->get(self::DAV_AUTHENTICATED) === $username;
}
/**
* Validates a username and password
*
* This method should return true or false depending on if login
* succeeded.
*
* @param string $username
* @param string $password
* @return bool
* @throws PasswordLoginForbidden
*/
protected function validateUserPass($username, $password) {
if ($this->userSession->isLoggedIn() &&
$this->isDavAuthenticated($this->userSession->getUser()->getUID())
) {
$this->session->close();
return true;
} else {
try {
if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
$this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
$this->session->close();
return true;
} else {
$this->session->close();
return false;
}
} catch (PasswordLoginForbiddenException $ex) {
$this->session->close();
throw new PasswordLoginForbidden();
} catch (MaxDelayReached $ex) {
$this->session->close();
throw new TooManyRequests();
}
}
}
/**
* @return array{bool, string}
* @throws NotAuthenticated
* @throws ServiceUnavailable
*/
public function check(RequestInterface $request, ResponseInterface $response) {
try {
return $this->auth($request, $response);
} catch (NotAuthenticated $e) {
throw $e;
} catch (Exception $e) {
$class = get_class($e);
$msg = $e->getMessage();
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
throw new ServiceUnavailable("$class: $msg");
}
}
/**
* Checks whether a CSRF check is required on the request
*/
private function requiresCSRFCheck(): bool {
// GET requires no check at all
if ($this->request->getMethod() === 'GET') {
return false;
}
// Official Nextcloud clients require no checks
if ($this->request->isUserAgent([
IRequest::USER_AGENT_CLIENT_DESKTOP,
IRequest::USER_AGENT_CLIENT_ANDROID,
IRequest::USER_AGENT_CLIENT_IOS,
])) {
return false;
}
// If not logged-in no check is required
if (!$this->userSession->isLoggedIn()) {
return false;
}
// POST always requires a check
if ($this->request->getMethod() === 'POST') {
return true;
}
// If logged-in AND DAV authenticated no check is required
if ($this->userSession->isLoggedIn() &&
$this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
return false;
}
return true;
}
/**
* @return array{bool, string}
* @throws NotAuthenticated
*/
private function auth(RequestInterface $request, ResponseInterface $response): array {
$forcedLogout = false;
if (!$this->request->passesCSRFCheck() &&
$this->requiresCSRFCheck()) {
// In case of a fail with POST we need to recheck the credentials
if ($this->request->getMethod() === 'POST') {
$forcedLogout = true;
} else {
$response->setStatus(401);
throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
}
}
if ($forcedLogout) {
$this->userSession->logout();
} else {
if ($this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) {
throw new \Sabre\DAV\Exception\NotAuthenticated('2FA challenge not passed.');
}
if (
//Fix for broken webdav clients
($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
//Well behaved clients that only send the cookie are allowed
($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) ||
\OC_User::handleApacheAuth()
) {
$user = $this->userSession->getUser()->getUID();
$this->currentUser = $user;
$this->session->close();
return [true, $this->principalPrefix . $user];
}
}
if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) {
// do not re-authenticate over ajax, use dummy auth name to prevent browser popup
$response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"');
$response->setStatus(401);
throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
}
$data = parent::check($request, $response);
if ($data[0] === true) {
$startPos = strrpos($data[1], '/') + 1;
$user = $this->userSession->getUser()->getUID();
$data[1] = substr_replace($data[1], $user, $startPos);
}
return $data;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
use Sabre\DAV\Auth\Backend\AbstractBearer;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BearerAuth extends AbstractBearer {
private IUserSession $userSession;
private ISession $session;
private IRequest $request;
private string $principalPrefix;
public function __construct(IUserSession $userSession,
ISession $session,
IRequest $request,
$principalPrefix = 'principals/users/') {
$this->userSession = $userSession;
$this->session = $session;
$this->request = $request;
$this->principalPrefix = $principalPrefix;
// setup realm
$defaults = new \OCP\Defaults();
$this->realm = $defaults->getName();
}
private function setupUserFs($userId) {
\OC_Util::setupFS($userId);
$this->session->close();
return $this->principalPrefix . $userId;
}
/**
* {@inheritdoc}
*/
public function validateBearerToken($bearerToken) {
\OC_Util::setupFS();
if (!$this->userSession->isLoggedIn()) {
$this->userSession->tryTokenLogin($this->request);
}
if ($this->userSession->isLoggedIn()) {
return $this->setupUserFs($this->userSession->getUser()->getUID());
}
return false;
}
/**
* \Sabre\DAV\Auth\Backend\AbstractBearer::challenge sets an WWW-Authenticate
* header which some DAV clients can't handle. Thus we override this function
* and make it simply return a 401.
*
* @param RequestInterface $request
* @param ResponseInterface $response
*/
public function challenge(RequestInterface $request, ResponseInterface $response): void {
$response->setStatus(401);
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector\Sabre;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
/**
* Class BlockLegacyClientPlugin is used to detect old legacy sync clients and
* returns a 403 status to those clients
*
* @package OCA\DAV\Connector\Sabre
*/
class BlockLegacyClientPlugin extends ServerPlugin {
protected ?Server $server = null;
protected IConfig $config;
public function __construct(IConfig $config) {
$this->config = $config;
}
/**
* @return void
*/
public function initialize(Server $server) {
$this->server = $server;
$this->server->on('beforeMethod:*', [$this, 'beforeHandler'], 200);
}
/**
* Detects all unsupported clients and throws a \Sabre\DAV\Exception\Forbidden
* exception which will result in a 403 to them.
* @param RequestInterface $request
* @throws \Sabre\DAV\Exception\Forbidden If the client version is not supported
*/
public function beforeHandler(RequestInterface $request) {
$userAgent = $request->getHeader('User-Agent');
if ($userAgent === null) {
return;
}
$minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.3.0');
preg_match(IRequest::USER_AGENT_CLIENT_DESKTOP, $userAgent, $versionMatches);
if (isset($versionMatches[1]) &&
version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
}
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\Tree;
class CachingTree extends Tree {
/**
* Store a node in the cache
*/
public function cacheNode(Node $node, ?string $path = null): void {
if (is_null($path)) {
$path = $node->getPath();
}
$this->cache[trim($path, '/')] = $node;
}
/**
* @param string $path
* @return void
*/
public function markDirty($path) {
// We don't care enough about sub-paths
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
$nodePath = (string) $nodePath;
if ('' === $path || $nodePath == $path || str_starts_with($nodePath, $path . '/')) {
unset($this->cache[$nodePath]);
}
}
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\Connector\Sabre;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* Checksumlist property
*
* This property contains multiple "checksum" elements, each containing a
* checksum name.
*/
class ChecksumList implements XmlSerializable {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/** @var string[] of TYPE:CHECKSUM */
private array $checksums;
public function __construct(string $checksum) {
$this->checksums = explode(' ', $checksum);
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializble should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*
* @param Writer $writer
* @return void
*/
public function xmlSerialize(Writer $writer) {
foreach ($this->checksums as $checksum) {
$writer->writeElement('{' . self::NS_OWNCLOUD . '}checksum', $checksum);
}
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class ChecksumUpdatePlugin extends ServerPlugin {
protected ?Server $server = null;
public function initialize(Server $server) {
$this->server = $server;
$server->on('method:PATCH', [$this, 'httpPatch']);
}
public function getPluginName(): string {
return 'checksumupdate';
}
/** @return string[] */
public function getHTTPMethods($path): array {
$tree = $this->server->tree;
if ($tree->nodeExists($path)) {
$node = $tree->getNodeForPath($path);
if ($node instanceof File) {
return ['PATCH'];
}
}
return [];
}
/** @return string[] */
public function getFeatures(): array {
return ['nextcloud-checksum-update'];
}
public function httpPatch(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
if ($node instanceof File) {
$type = strtolower(
(string)$request->getHeader('X-Recalculate-Hash')
);
$hash = $node->hash($type);
if ($hash) {
$checksum = strtoupper($type) . ':' . $hash;
$node->setChecksum($checksum);
$response->addHeader('OC-Checksum', $checksum);
$response->setHeader('Content-Length', '0');
$response->setStatus(204);
return false;
}
}
}
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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\Connector\Sabre;
use OCP\Comments\ICommentsManager;
use OCP\IUserSession;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
class CommentPropertiesPlugin extends ServerPlugin {
public const PROPERTY_NAME_HREF = '{http://owncloud.org/ns}comments-href';
public const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count';
public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread';
protected ?Server $server = null;
private ICommentsManager $commentsManager;
private IUserSession $userSession;
private array $cachedUnreadCount = [];
public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) {
$this->commentsManager = $commentsManager;
$this->userSession = $userSession;
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
}
private function cacheDirectory(Directory $directory): void {
$children = $directory->getChildren();
$ids = [];
foreach ($children as $child) {
if (!($child instanceof File || $child instanceof Directory)) {
continue;
}
$id = $child->getId();
if ($id === null) {
continue;
}
$ids[] = (string)$id;
}
$ids[] = (string) $directory->getId();
$unread = $this->commentsManager->getNumberOfUnreadCommentsForObjects('files', $ids, $this->userSession->getUser());
foreach ($unread as $id => $count) {
$this->cachedUnreadCount[(int)$id] = $count;
}
}
/**
* Adds tags and favorites properties to the response,
* if requested.
*
* @param PropFind $propFind
* @param \Sabre\DAV\INode $node
* @return void
*/
public function handleGetProperties(
PropFind $propFind,
\Sabre\DAV\INode $node
) {
if (!($node instanceof File) && !($node instanceof Directory)) {
return;
}
// need prefetch ?
if ($node instanceof Directory
&& $propFind->getDepth() !== 0
&& !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD))
) {
$this->cacheDirectory($node);
}
$propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node): int {
return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId());
});
$propFind->handle(self::PROPERTY_NAME_HREF, function () use ($node): ?string {
return $this->getCommentsLink($node);
});
$propFind->handle(self::PROPERTY_NAME_UNREAD, function () use ($node): ?int {
return $this->cachedUnreadCount[$node->getId()] ?? $this->getUnreadCount($node);
});
}
/**
* Returns a reference to the comments node
*/
public function getCommentsLink(Node $node): ?string {
$href = $this->server->getBaseUri();
$entryPoint = strpos($href, '/remote.php/');
if ($entryPoint === false) {
// in case we end up somewhere else, unexpectedly.
return null;
}
$commentsPart = 'dav/comments/files/' . rawurldecode((string)$node->getId());
return substr_replace($href, $commentsPart, $entryPoint + strlen('/remote.php/'));
}
/**
* Returns the number of unread comments for the currently logged in user
* on the given file or directory node
*/
public function getUnreadCount(Node $node): ?int {
$user = $this->userSession->getUser();
if (is_null($user)) {
return null;
}
$lastRead = $this->commentsManager->getReadMark('files', (string)$node->getId(), $user);
return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId(), $lastRead);
}
}
@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Copies the "Etag" header to "OC-Etag" after any request.
* This is a workaround for setups that automatically strip
* or mangle Etag headers.
*/
class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin {
private ?Server $server = null;
/**
* This initializes the plugin.
*
* @param \Sabre\DAV\Server $server Sabre server
*
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$server->on('afterMethod:*', [$this, 'afterMethod']);
$server->on('afterMove', [$this, 'afterMove']);
}
/**
* After method, copy the "Etag" header to "OC-Etag" header.
*
* @param RequestInterface $request request
* @param ResponseInterface $response response
*/
public function afterMethod(RequestInterface $request, ResponseInterface $response) {
$eTag = $response->getHeader('Etag');
if (!empty($eTag)) {
$response->setHeader('OC-ETag', $eTag);
}
}
/**
* Called after a node is moved.
*
* This allows the backend to move all the associated properties.
*
* @param string $source
* @param string $destination
* @return void
*/
public function afterMove($source, $destination) {
try {
$node = $this->server->tree->getNodeForPath($destination);
} catch (NotFound $e) {
// Don't care
return;
}
if ($node instanceof File) {
$eTag = $node->getETag();
$this->server->httpResponse->setHeader('OC-ETag', $eTag);
$this->server->httpResponse->setHeader('ETag', $eTag);
}
}
}
@@ -0,0 +1,122 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @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\Connector\Sabre;
use OCA\DAV\CardDAV\AddressBook;
use Sabre\CalDAV\Principal\User;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Class DavAclPlugin is a wrapper around \Sabre\DAVACL\Plugin that returns 404
* responses in case the resource to a response has been forbidden instead of
* a 403. This is used to prevent enumeration of valid resources.
*
* @see https://github.com/owncloud/core/issues/22578
* @package OCA\DAV\Connector\Sabre
*/
class DavAclPlugin extends \Sabre\DAVACL\Plugin {
public function __construct() {
$this->hideNodesFromListings = true;
$this->allowUnauthenticatedAccess = false;
}
public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) {
$access = parent::checkPrivileges($uri, $privileges, $recursion, false);
if ($access === false && $throwExceptions) {
/** @var INode $node */
$node = $this->server->tree->getNodeForPath($uri);
switch (get_class($node)) {
case AddressBook::class:
$type = 'Addressbook';
break;
default:
$type = 'Node';
break;
}
throw new NotFound(
sprintf(
"%s with name '%s' could not be found",
$type,
$node->getName()
)
);
}
return $access;
}
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof Node) {
// files don't use dav acls
return;
}
// If the node is neither readable nor writable then fail unless its of
// the standard user-principal
if (!($node instanceof User)) {
$path = $propFind->getPath();
$readPermissions = $this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false);
$writePermissions = $this->checkPrivileges($path, '{DAV:}write', self::R_PARENT, false);
if ($readPermissions === false && $writePermissions === false) {
$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, true);
$this->checkPrivileges($path, '{DAV:}write', self::R_PARENT, true);
}
}
return parent::propFind($propFind, $node);
}
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
// prevent the plugin from causing an unneeded overhead for file requests
if (str_starts_with($path, 'files/')) {
return;
}
parent::beforeMethod($request, $response);
if (!str_starts_with($path, 'addressbooks/') && !str_starts_with($path, 'calendars/')) {
return;
}
[$parentName] = \Sabre\Uri\split($path);
if ($request->getMethod() === 'REPORT') {
// is calendars/users/bob or addressbooks/users/bob readable?
$this->checkPrivileges($parentName, '{DAV:}read');
} elseif ($request->getMethod() === 'MKCALENDAR' || $request->getMethod() === 'MKCOL') {
// is calendars/users/bob or addressbooks/users/bob writeable?
$this->checkPrivileges($parentName, '{DAV:}write');
}
}
}
@@ -0,0 +1,494 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jakob Sack <mail@jakobsack.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\View;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\ForbiddenException;
use OCP\Files\InvalidPathException;
use OCP\Files\NotPermittedException;
use OCP\Files\StorageNotAvailableException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\L10N\IFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Share\IManager as IShareManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Locked;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
use Sabre\DAV\INode;
class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
/**
* Cached directory content
* @var \OCP\Files\FileInfo[]
*/
private ?array $dirContent = null;
/** Cached quota info */
private ?array $quotaInfo = null;
private ?CachingTree $tree = null;
/**
* Sets up the node, expects a full path name
*/
public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, IShareManager $shareManager = null) {
parent::__construct($view, $info, $shareManager);
$this->tree = $tree;
}
/**
* Creates a new file in the directory
*
* Data will either be supplied as a stream resource, or in certain cases
* as a string. Keep in mind that you may have to support either.
*
* After successful creation of the file, you may choose to return the ETag
* of the new file here.
*
* The returned ETag must be surrounded by double-quotes (The quotes should
* be part of the actual string).
*
* If you cannot accurately determine the ETag, you should not return it.
* If you don't store the file exactly as-is (you're transforming it
* somehow) you should also not return an ETag.
*
* This means that if a subsequent GET to this new file does not exactly
* return the same contents of what was submitted here, you are strongly
* recommended to omit the ETag.
*
* @param string $name Name of the file
* @param resource|string $data Initial payload
* @return null|string
* @throws Exception\EntityTooLarge
* @throws Exception\UnsupportedMediaType
* @throws FileLocked
* @throws InvalidPath
* @throws \Sabre\DAV\Exception
* @throws \Sabre\DAV\Exception\BadRequest
* @throws \Sabre\DAV\Exception\Forbidden
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function createFile($name, $data = null) {
try {
// for chunked upload also updating a existing file is a "createFile"
// because we create all the chunks before re-assemble them to the existing file.
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
// exit if we can't create a new file and we don't updatable existing file
$chunkInfo = \OC_FileChunking::decodeName($name);
if (!$this->fileView->isCreatable($this->path) &&
!$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
) {
throw new \Sabre\DAV\Exception\Forbidden();
}
} else {
// For non-chunked upload it is enough to check if we can create a new file
if (!$this->fileView->isCreatable($this->path)) {
throw new \Sabre\DAV\Exception\Forbidden();
}
}
$this->fileView->verifyPath($this->path, $name);
$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
// in case the file already exists/overwriting
$info = $this->fileView->getFileInfo($this->path . '/' . $name);
if (!$info) {
// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
$info = new \OC\Files\FileInfo($path, null, null, [
'type' => FileInfo::TYPE_FILE
], null);
}
$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
$node->acquireLock(ILockingProvider::LOCK_SHARED);
$this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$result = $node->put($data);
$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$node->releaseLock(ILockingProvider::LOCK_SHARED);
return $result;
} catch (\OCP\Files\StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Creates a new subdirectory
*
* @param string $name
* @throws FileLocked
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\Forbidden
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function createDirectory($name) {
try {
if (!$this->info->isCreatable()) {
throw new \Sabre\DAV\Exception\Forbidden();
}
$this->fileView->verifyPath($this->path, $name);
$newPath = $this->path . '/' . $name;
if (!$this->fileView->mkdir($newPath)) {
throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
}
} catch (\OCP\Files\StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Returns a specific child node, referenced by its name
*
* @param string $name
* @param \OCP\Files\FileInfo $info
* @return \Sabre\DAV\INode
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\NotFound
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getChild($name, $info = null, IRequest $request = null, IL10N $l10n = null) {
if (!$this->info->isReadable()) {
// avoid detecting files through this way
throw new NotFound();
}
$path = $this->path . '/' . $name;
if (is_null($info)) {
try {
$this->fileView->verifyPath($this->path, $name);
$info = $this->fileView->getFileInfo($path);
} catch (\OCP\Files\StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
} catch (ForbiddenException $e) {
throw new \Sabre\DAV\Exception\Forbidden();
}
}
if (!$info) {
throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
}
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
} else {
$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n);
}
if ($this->tree) {
$this->tree->cacheNode($node);
}
return $node;
}
/**
* Returns an array with all the child nodes
*
* @return \Sabre\DAV\INode[]
* @throws \Sabre\DAV\Exception\Locked
* @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
*/
public function getChildren() {
if (!is_null($this->dirContent)) {
return $this->dirContent;
}
try {
if (!$this->info->isReadable()) {
// return 403 instead of 404 because a 404 would make
// the caller believe that the collection itself does not exist
if (\OCP\Server::get(\OCP\App\IAppManager::class)->isInstalled('files_accesscontrol')) {
throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
} else {
throw new Forbidden('No read permissions');
}
}
$folderContent = $this->getNode()->getDirectoryListing();
} catch (LockedException $e) {
throw new Locked();
}
$nodes = [];
$request = \OC::$server->get(IRequest::class);
$l10nFactory = \OC::$server->get(IFactory::class);
$l10n = $l10nFactory->get(Application::APP_ID);
foreach ($folderContent as $info) {
$node = $this->getChild($info->getName(), $info, $request, $l10n);
$nodes[] = $node;
}
$this->dirContent = $nodes;
return $this->dirContent;
}
/**
* Checks if a child exists.
*
* @param string $name
* @return bool
*/
public function childExists($name) {
// note: here we do NOT resolve the chunk file name to the real file name
// to make sure we return false when checking for file existence with a chunk
// file name.
// This is to make sure that "createFile" is still triggered
// (required old code) instead of "updateFile".
//
// TODO: resolve chunk file name here and implement "updateFile"
$path = $this->path . '/' . $name;
return $this->fileView->file_exists($path);
}
/**
* Deletes all files in this directory, and then itself
*
* @return void
* @throws FileLocked
* @throws \Sabre\DAV\Exception\Forbidden
*/
public function delete() {
if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
throw new \Sabre\DAV\Exception\Forbidden();
}
try {
if (!$this->fileView->rmdir($this->path)) {
// assume it wasn't possible to remove due to permission issue
throw new \Sabre\DAV\Exception\Forbidden();
}
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
private function getLogger(): LoggerInterface {
return \OC::$server->get(LoggerInterface::class);
}
/**
* Returns available diskspace information
*
* @return array
*/
public function getQuotaInfo() {
if ($this->quotaInfo) {
return $this->quotaInfo;
}
$relativePath = $this->fileView->getRelativePath($this->info->getPath());
if ($relativePath === null) {
$this->getLogger()->warning("error while getting quota as the relative path cannot be found");
return [0, 0];
}
try {
$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
$free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
} else {
$free = $storageInfo['free'];
}
$this->quotaInfo = [
$storageInfo['used'],
$free
];
return $this->quotaInfo;
} catch (\OCP\Files\NotFoundException $e) {
$this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
return [0, 0];
} catch (\OCP\Files\StorageNotAvailableException $e) {
$this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
return [0, 0];
} catch (NotPermittedException $e) {
$this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
return [0, 0];
}
}
/**
* Moves a node into this collection.
*
* It is up to the implementors to:
* 1. Create the new resource.
* 2. Remove the old resource.
* 3. Transfer any properties or other data.
*
* Generally you should make very sure that your collection can easily move
* the move.
*
* If you don't, just return false, which will trigger sabre/dav to handle
* the move itself. If you return true from this function, the assumption
* is that the move was successful.
*
* @param string $targetName New local file/collection name.
* @param string $fullSourcePath Full path to source node
* @param INode $sourceNode Source node itself
* @return bool
* @throws BadRequest
* @throws ServiceUnavailable
* @throws Forbidden
* @throws FileLocked
* @throws \Sabre\DAV\Exception\Forbidden
*/
public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
if (!$sourceNode instanceof Node) {
// it's a file of another kind, like FutureFile
if ($sourceNode instanceof IFile) {
// fallback to default copy+delete handling
return false;
}
throw new BadRequest('Incompatible node types');
}
if (!$this->fileView) {
throw new ServiceUnavailable('filesystem not setup');
}
$destinationPath = $this->getPath() . '/' . $targetName;
$targetNodeExists = $this->childExists($targetName);
// at getNodeForPath we also check the path for isForbiddenFileOrDir
// with that we have covered both source and destination
if ($sourceNode instanceof Directory && $targetNodeExists) {
throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
}
[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
$destinationDir = $this->getPath();
$sourcePath = $sourceNode->getPath();
$isMovableMount = false;
$sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
if ($sourceMount instanceof MoveableMount && $internalPath === '') {
$isMovableMount = true;
}
try {
$sameFolder = ($sourceDir === $destinationDir);
// if we're overwriting or same folder
if ($targetNodeExists || $sameFolder) {
// note that renaming a share mount point is always allowed
if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
throw new \Sabre\DAV\Exception\Forbidden();
}
} else {
if (!$this->fileView->isCreatable($destinationDir)) {
throw new \Sabre\DAV\Exception\Forbidden();
}
}
if (!$sameFolder) {
// moving to a different folder, source will be gone, like a deletion
// note that moving a share mount point is always allowed
if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
throw new \Sabre\DAV\Exception\Forbidden();
}
}
$fileName = basename($destinationPath);
try {
$this->fileView->verifyPath($destinationDir, $fileName);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
if (!$renameOkay) {
throw new \Sabre\DAV\Exception\Forbidden('');
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($e->getMessage());
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
return true;
}
public function copyInto($targetName, $sourcePath, INode $sourceNode) {
if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
$destinationPath = $this->getPath() . '/' . $targetName;
$sourcePath = $sourceNode->getPath();
if (!$this->fileView->isCreatable($this->getPath())) {
throw new \Sabre\DAV\Exception\Forbidden();
}
try {
$this->fileView->verifyPath($this->getPath(), $targetName);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
return $this->fileView->copy($sourcePath, $destinationPath);
}
return false;
}
public function getNode(): Folder {
return $this->node;
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wickert <cwickert@suse.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector\Sabre;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Class DummyGetResponsePlugin is a plugin used to not show a "Not implemented"
* error to clients that rely on verifying the functionality of the Nextcloud
* WebDAV backend using a simple GET to /.
*
* This is considered a legacy behaviour and implementers should consider sending
* a PROPFIND request instead to verify whether the WebDAV component is working
* properly.
*
* FIXME: Remove once clients are all compliant.
*
* @package OCA\DAV\Connector\Sabre
*/
class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
protected ?Server $server = null;
/**
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('method:GET', [$this, 'httpGet'], 200);
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @return false
*/
public function httpGet(RequestInterface $request, ResponseInterface $response) {
$string = 'This is the WebDAV interface. It can only be accessed by ' .
'WebDAV clients such as the Nextcloud desktop sync client.';
$stream = fopen('php://memory', 'r+');
fwrite($stream, $string);
rewind($stream);
$response->setStatus(200);
$response->setBody($stream);
return false;
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
*
* @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\Connector\Sabre\Exception;
/**
* Bad Gateway
*
* This exception is thrown whenever the server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*
*/
class BadGateway extends \Sabre\DAV\Exception {
/**
* Returns the HTTP status code for this exception
*
* @return int
*/
public function getHTTPCode() {
return 502;
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre\Exception;
/**
* Entity Too Large
*
* This exception is thrown whenever a user tries to upload a file which exceeds hard limitations
*
*/
class EntityTooLarge extends \Sabre\DAV\Exception {
/**
* Returns the HTTP status code for this exception
*
* @return int
*/
public function getHTTPCode() {
return 413;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Owen Winkler <a_github@midnightcircus.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre\Exception;
use Exception;
class FileLocked extends \Sabre\DAV\Exception {
public function __construct($message = "", $code = 0, Exception $previous = null) {
if ($previous instanceof \OCP\Files\LockNotAcquiredException) {
$message = sprintf('Target file %s is locked by another process.', $previous->path);
}
parent::__construct($message, $code, $previous);
}
/**
* Returns the HTTP status code for this exception
*
* @return int
*/
public function getHTTPCode() {
return 423;
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @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\Connector\Sabre\Exception;
class Forbidden extends \Sabre\DAV\Exception\Forbidden {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* @var bool
*/
private $retry;
/**
* @param string $message
* @param bool $retry
* @param \Exception $previous
*/
public function __construct($message, $retry = false, \Exception $previous = null) {
parent::__construct($message, 0, $previous);
$this->retry = $retry;
}
/**
* This method allows the exception to include additional information
* into the WebDAV error response
*
* @param \Sabre\DAV\Server $server
* @param \DOMElement $errorNode
* @return void
*/
public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector\Sabre\Exception;
use Sabre\DAV\Exception;
class InvalidPath extends Exception {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* @var bool
*/
private $retry;
/**
* @param string $message
* @param bool $retry
* @param \Exception|null $previous
*/
public function __construct($message, $retry = false, \Exception $previous = null) {
parent::__construct($message, 0, $previous);
$this->retry = $retry;
}
/**
* Returns the HTTP status code for this exception
*
* @return int
*/
public function getHTTPCode() {
return 400;
}
/**
* This method allows the exception to include additional information
* into the WebDAV error response
*
* @param \Sabre\DAV\Server $server
* @param \DOMElement $errorNode
* @return void
*/
public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
// adding the retry node
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:retry', var_export($this->retry, true));
$errorNode->appendChild($error);
// adding the message node
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());
$errorNode->appendChild($error);
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre\Exception;
use DOMElement;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Server;
class PasswordLoginForbidden extends NotAuthenticated {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public function getHTTPCode() {
return 401;
}
/**
* This method allows the exception to include additional information
* into the WebDAV error response
*
* @param Server $server
* @param DOMElement $errorNode
* @return void
*/
public function serialize(Server $server, DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:hint', 'password login forbidden');
$errorNode->appendChild($error);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre\Exception;
use DOMElement;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Server;
class TooManyRequests extends NotAuthenticated {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public function getHTTPCode() {
return 429;
}
/**
* This method allows the exception to include additional information
* into the WebDAV error response
*
* @param Server $server
* @param DOMElement $errorNode
* @return void
*/
public function serialize(Server $server, DOMElement $errorNode) {
// set ownCloud namespace
$errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD);
$error = $errorNode->ownerDocument->createElementNS('o:', 'o:hint', 'too many requests');
$errorNode->appendChild($error);
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre\Exception;
/**
* Unsupported Media Type
*
* This exception is thrown whenever a user tries to upload a file which holds content which is not allowed
*
*/
class UnsupportedMediaType extends \Sabre\DAV\Exception {
/**
* Returns the HTTP status code for this exception
*
* @return int
*/
public function getHTTPCode() {
return 415;
}
}
@@ -0,0 +1,131 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\Files\StorageNotAvailableException;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\InvalidSyncToken;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\RequestedRangeNotSatisfiable;
class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
protected $nonFatalExceptions = [
NotAuthenticated::class => true,
// If tokenauth can throw this exception (which is basically as
// NotAuthenticated. So not fatal.
PasswordLoginForbidden::class => true,
// basically a NotAuthenticated
InvalidSyncToken::class => true,
// the sync client uses this to find out whether files exist,
// so it is not always an error, log it as debug
NotFound::class => true,
// the sync client messed up their request
// (e.g. propfind for tags with string instead of int)
// so it is not always an error, log it as debug
BadRequest::class => true,
// this one mostly happens when the same file is uploaded at
// exactly the same time from two clients, only one client
// wins, the second one gets "Precondition failed"
PreconditionFailed::class => true,
// forbidden can be expected when trying to upload to
// read-only folders for example
Forbidden::class => true,
// Happens when an external storage or federated share is temporarily
// not available
StorageNotAvailableException::class => true,
// happens if some a client uses the wrong method for a given URL
// the error message itself is visible on the client side anyways
NotImplemented::class => true,
// happens when the parent directory is not present (for example when a
// move is done to a non-existent directory)
Conflict::class => true,
// happens when a certain method is not allowed to be called
// for example creating a folder that already exists
MethodNotAllowed::class => true,
// A locked file is perfectly valid and can happen in various cases
FileLocked::class => true,
// An invalid range is requested
RequestedRangeNotSatisfiable::class => true,
ServerMaintenanceMode::class => true,
];
private string $appName;
private LoggerInterface $logger;
/**
* @param string $loggerAppName app name to use when logging
*/
public function __construct(string $loggerAppName, LoggerInterface $logger) {
$this->appName = $loggerAppName;
$this->logger = $logger;
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->on('exception', [$this, 'logException'], 10);
}
/**
* Log exception
*/
public function logException(\Throwable $ex) {
$exceptionClass = get_class($ex);
if (isset($this->nonFatalExceptions[$exceptionClass])) {
$this->logger->debug($ex->getMessage(), [
'app' => $this->appName,
'exception' => $ex,
]);
return;
}
$this->logger->critical($ex->getMessage(), [
'app' => $this->appName,
'exception' => $ex,
]);
}
}
@@ -0,0 +1,159 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector\Sabre;
use Sabre\DAV\INode;
use Sabre\DAV\Locks\LockInfo;
use Sabre\DAV\PropFind;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Xml\Property\LockDiscovery;
use Sabre\DAV\Xml\Property\SupportedLock;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Class FakeLockerPlugin is a plugin only used when connections come in from
* OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support
* (locking of files) which allows Finder to access the storage in write mode as
* well.
*
* No real locking is performed, instead the plugin just returns always positive
* responses.
*
* @see https://github.com/owncloud/core/issues/17732
* @package OCA\DAV\Connector\Sabre
*/
class FakeLockerPlugin extends ServerPlugin {
/** @var \Sabre\DAV\Server */
private $server;
/** {@inheritDoc} */
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1);
$this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1);
$server->on('propFind', [$this, 'propFind']);
$server->on('validateTokens', [$this, 'validateTokens']);
}
/**
* Indicate that we support LOCK and UNLOCK
*
* @param string $path
* @return string[]
*/
public function getHTTPMethods($path) {
return [
'LOCK',
'UNLOCK',
];
}
/**
* Indicate that we support locking
*
* @return integer[]
*/
public function getFeatures() {
return [2];
}
/**
* Return some dummy response for PROPFIND requests with regard to locking
*
* @param PropFind $propFind
* @param INode $node
* @return void
*/
public function propFind(PropFind $propFind, INode $node) {
$propFind->handle('{DAV:}supportedlock', function () {
return new SupportedLock();
});
$propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) {
return new LockDiscovery([]);
});
}
/**
* Mark a locking token always as valid
*
* @param RequestInterface $request
* @param array $conditions
*/
public function validateTokens(RequestInterface $request, &$conditions) {
foreach ($conditions as &$fileCondition) {
if (isset($fileCondition['tokens'])) {
foreach ($fileCondition['tokens'] as &$token) {
if (isset($token['token'])) {
if (substr($token['token'], 0, 16) === 'opaquelocktoken:') {
$token['validToken'] = true;
}
}
}
}
}
}
/**
* Fakes a successful LOCK
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
public function fakeLockProvider(RequestInterface $request,
ResponseInterface $response) {
$lockInfo = new LockInfo();
$lockInfo->token = md5($request->getPath());
$lockInfo->uri = $request->getPath();
$lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY;
$lockInfo->timeout = 1800;
$body = $this->server->xml->write('{DAV:}prop', [
'{DAV:}lockdiscovery' =>
new LockDiscovery([$lockInfo])
]);
$response->setStatus(200);
$response->setBody($body);
return false;
}
/**
* Fakes a successful LOCK
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
public function fakeUnlockProvider(RequestInterface $request,
ResponseInterface $response) {
$response->setStatus(204);
$response->setHeader('Content-Length', '0');
return false;
}
}
@@ -0,0 +1,795 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
* @author Jakob Sack <mail@jakobsack.de>
* @author Jan-Philipp Litza <jplitza@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Owen Winkler <a_github@midnightcircus.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Semih Serhat Karakaya <karakayasemi@itu.edu.tr>
* @author Stefan Schneider <stefan.schneider@squareweave.com.au>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use Icewind\Streams\CallbackWrapper;
use OC\AppFramework\Http\Request;
use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\BadGateway;
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\EntityTooLargeException;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
use OCP\Files\InvalidContentException;
use OCP\Files\InvalidPathException;
use OCP\Files\LockNotAcquiredException;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\Storage;
use OCP\Files\StorageNotAvailableException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\L10N\IFactory as IL10NFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
class File extends Node implements IFile {
protected IRequest $request;
protected IL10N $l10n;
/**
* Sets up the node, expects a full path name
*
* @param \OC\Files\View $view
* @param \OCP\Files\FileInfo $info
* @param ?\OCP\Share\IManager $shareManager
* @param ?IRequest $request
* @param ?IL10N $l10n
*/
public function __construct(View $view, FileInfo $info, IManager $shareManager = null, IRequest $request = null, IL10N $l10n = null) {
parent::__construct($view, $info, $shareManager);
if ($l10n) {
$this->l10n = $l10n;
} else {
// Querying IL10N directly results in a dependency loop
/** @var IL10NFactory $l10nFactory */
$l10nFactory = \OC::$server->get(IL10NFactory::class);
$this->l10n = $l10nFactory->get(Application::APP_ID);
}
if (isset($request)) {
$this->request = $request;
} else {
$this->request = \OC::$server->get(IRequest::class);
}
}
/**
* Updates the data
*
* The data argument is a readable stream resource.
*
* After a successful put operation, you may choose to return an ETag. The
* etag must always be surrounded by double-quotes. These quotes must
* appear in the actual string you're returning.
*
* Clients may use the ETag from a PUT request to later on make sure that
* when they update the file, the contents haven't changed in the mean
* time.
*
* If you don't plan to store the file byte-by-byte, and you return a
* different object on a subsequent GET you are strongly recommended to not
* return an ETag, and just return null.
*
* @param resource $data
*
* @throws Forbidden
* @throws UnsupportedMediaType
* @throws BadRequest
* @throws Exception
* @throws EntityTooLarge
* @throws ServiceUnavailable
* @throws FileLocked
* @return string|null
*/
public function put($data) {
try {
$exists = $this->fileView->file_exists($this->path);
if ($exists && !$this->info->isUpdateable()) {
throw new Forbidden();
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
}
// verify path of the target
$this->verifyPath();
// chunked handling
$chunkedHeader = $this->request->getHeader('oc-chunked');
if ($chunkedHeader) {
try {
return $this->createFileChunked($data);
} catch (\Exception $e) {
$this->convertToSabreException($e);
}
}
/** @var Storage $partStorage */
[$partStorage] = $this->fileView->resolvePath($this->path);
$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
$view = \OC\Files\Filesystem::getView();
if ($needsPartFile) {
// mark file as partial while uploading (ignored by the scanner)
$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
$needsPartFile = false;
}
}
if (!$needsPartFile) {
// upload file directly as the final path
$partFilePath = $this->path;
if ($view && !$this->emitPreHooks($exists)) {
throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
}
}
// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
/** @var \OC\Files\Storage\Storage $partStorage */
[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
/** @var \OC\Files\Storage\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
try {
if (!$needsPartFile) {
try {
$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $e) {
// during very large uploads, the shared lock we got at the start might have been expired
// meaning that the above lock can fail not just only because somebody else got a shared lock
// or because there is no existing shared lock to make exclusive
//
// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
// lock this will still fail, if our original shared lock expired the new lock will be successful and
// the entire operation will be safe
try {
$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $ex) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
}
if (!is_resource($data)) {
$tmpData = fopen('php://temp', 'r+');
if ($data !== null) {
fwrite($tmpData, $data);
rewind($tmpData);
}
$data = $tmpData;
}
if ($this->request->getHeader('X-HASH') !== '') {
$hash = $this->request->getHeader('X-HASH');
if ($hash === 'all' || $hash === 'md5') {
$data = HashWrapper::wrap($data, 'md5', function ($hash) {
$this->header('X-Hash-MD5: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha1') {
$data = HashWrapper::wrap($data, 'sha1', function ($hash) {
$this->header('X-Hash-SHA1: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha256') {
$data = HashWrapper::wrap($data, 'sha256', function ($hash) {
$this->header('X-Hash-SHA256: ' . $hash);
});
}
}
if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
$isEOF = false;
$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
$isEOF = feof($stream);
});
$result = true;
$count = -1;
try {
$count = $partStorage->writeStream($internalPartPath, $wrappedData);
} catch (GenericFileException $e) {
$result = false;
} catch (BadGateway $e) {
throw $e;
}
if ($result === false) {
$result = $isEOF;
if (is_resource($wrappedData)) {
$result = feof($wrappedData);
}
}
} else {
$target = $partStorage->fopen($internalPartPath, 'wb');
if ($target === false) {
\OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
// because we have no clue about the cause we can only throw back a 500/Internal Server Error
throw new Exception($this->l10n->t('Could not write file contents'));
}
[$count, $result] = \OC_Helper::streamCopy($data, $target);
fclose($target);
}
if ($result === false) {
$expected = -1;
$lengthHeader = $this->request->getHeader('content-length');
if ($lengthHeader) {
$expected = (int)$lengthHeader;
}
if ($expected !== 0) {
throw new Exception(
$this->l10n->t(
'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
[
$this->l10n->n('%n byte', '%n bytes', $count),
$this->l10n->n('%n byte', '%n bytes', $expected),
],
)
);
}
}
// if content length is sent by client:
// double check if the file was fully received
// compare expected and actual size
$lengthHeader = $this->request->getHeader('content-length');
if ($lengthHeader && $this->request->getMethod() === 'PUT') {
$expected = (int)$lengthHeader;
if ($count !== $expected) {
throw new BadRequest(
$this->l10n->t(
'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
[
$this->l10n->n('%n byte', '%n bytes', $expected),
$this->l10n->n('%n byte', '%n bytes', $count),
],
)
);
}
}
} catch (\Exception $e) {
if ($e instanceof LockedException) {
\OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
} else {
\OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
if ($needsPartFile) {
$partStorage->unlink($internalPartPath);
}
$this->convertToSabreException($e);
}
try {
if ($needsPartFile) {
if ($view && !$this->emitPreHooks($exists)) {
$partStorage->unlink($internalPartPath);
throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
}
try {
$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $e) {
// during very large uploads, the shared lock we got at the start might have been expired
// meaning that the above lock can fail not just only because somebody else got a shared lock
// or because there is no existing shared lock to make exclusive
//
// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
// lock this will still fail, if our original shared lock expired the new lock will be successful and
// the entire operation will be safe
try {
$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $ex) {
if ($needsPartFile) {
$partStorage->unlink($internalPartPath);
}
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
// rename to correct path
try {
$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
$fileExists = $storage->file_exists($internalPath);
if ($renameOkay === false || $fileExists === false) {
\OC::$server->get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
throw new Exception($this->l10n->t('Could not rename part file to final file'));
}
} catch (ForbiddenException $ex) {
if (!$ex->getRetry()) {
$partStorage->unlink($internalPartPath);
}
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (\Exception $e) {
$partStorage->unlink($internalPartPath);
$this->convertToSabreException($e);
}
}
// since we skipped the view we need to scan and emit the hooks ourselves
$storage->getUpdater()->update($internalPath);
try {
$this->changeLock(ILockingProvider::LOCK_SHARED);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
// allow sync clients to send the mtime along in a header
$mtimeHeader = $this->request->getHeader('x-oc-mtime');
if ($mtimeHeader !== '') {
$mtime = $this->sanitizeMtime($mtimeHeader);
if ($this->fileView->touch($this->path, $mtime)) {
$this->header('X-OC-MTime: accepted');
}
}
$fileInfoUpdate = [
'upload_time' => time()
];
// allow sync clients to send the creation time along in a header
$ctimeHeader = $this->request->getHeader('x-oc-ctime');
if ($ctimeHeader) {
$ctime = $this->sanitizeMtime($ctimeHeader);
$fileInfoUpdate['creation_time'] = $ctime;
$this->header('X-OC-CTime: accepted');
}
$this->fileView->putFileInfo($this->path, $fileInfoUpdate);
if ($view) {
$this->emitPostHooks($exists);
}
$this->refreshInfo();
$checksumHeader = $this->request->getHeader('oc-checksum');
if ($checksumHeader) {
$checksum = trim($checksumHeader);
$this->setChecksum($checksum);
} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
$this->setChecksum('');
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
}
return '"' . $this->info->getEtag() . '"';
}
private function getPartFileBasePath($path) {
$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
if ($partFileInStorage) {
return $path;
} else {
return md5($path); // will place it in the root of the view with a unique name
}
}
private function emitPreHooks(bool $exists, ?string $path = null): bool {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
if ($hookPath === null) {
// We only trigger hooks from inside default view
return true;
}
$run = true;
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
]);
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
]);
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
]);
return $run;
}
private function emitPostHooks(bool $exists, ?string $path = null): void {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
if ($hookPath === null) {
// We only trigger hooks from inside default view
return;
}
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [
\OC\Files\Filesystem::signal_param_path => $hookPath
]);
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [
\OC\Files\Filesystem::signal_param_path => $hookPath
]);
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [
\OC\Files\Filesystem::signal_param_path => $hookPath
]);
}
/**
* Returns the data
*
* @return resource
* @throws Forbidden
* @throws ServiceUnavailable
*/
public function get() {
//throw exception if encryption is disabled but files are still encrypted
try {
if (!$this->info->isReadable()) {
// do a if the file did not exist
throw new NotFound();
}
try {
$res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
} catch (\Exception $e) {
$this->convertToSabreException($e);
}
if ($res === false) {
throw new ServiceUnavailable($this->l10n->t('Could not open file'));
}
// comparing current file size with the one in DB
// if different, fix DB and refresh cache.
if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
$logger = \OC::$server->get(LoggerInterface::class);
$logger->warning('fixing cached size of file id=' . $this->getId());
$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
$this->refreshInfo();
}
return $res;
} catch (GenericEncryptionException $e) {
// returning 503 will allow retry of the operation at a later point in time
throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
} catch (ForbiddenException $ex) {
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Delete the current file
*
* @throws Forbidden
* @throws ServiceUnavailable
*/
public function delete() {
if (!$this->info->isDeletable()) {
throw new Forbidden();
}
try {
if (!$this->fileView->unlink($this->path)) {
// assume it wasn't possible to delete due to permissions
throw new Forbidden();
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()]));
} catch (ForbiddenException $ex) {
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Returns the mime-type for a file
*
* If null is returned, we'll assume application/octet-stream
*
* @return string
*/
public function getContentType() {
$mimeType = $this->info->getMimetype();
// PROPFIND needs to return the correct mime type, for consistency with the web UI
if ($this->request->getMethod() === 'PROPFIND') {
return $mimeType;
}
return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
}
/**
* @return array|bool
*/
public function getDirectDownload() {
if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) {
return [];
}
/** @var \OCP\Files\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
if (is_null($storage)) {
return [];
}
return $storage->getDirectDownload($internalPath);
}
/**
* @param resource $data
* @return null|string
* @throws Exception
* @throws BadRequest
* @throws NotImplemented
* @throws ServiceUnavailable
*/
private function createFileChunked($data) {
[$path, $name] = \Sabre\Uri\split($this->path);
$info = \OC_FileChunking::decodeName($name);
if (empty($info)) {
throw new NotImplemented($this->l10n->t('Invalid chunk name'));
}
$chunk_handler = new \OC_FileChunking($info);
$bytesWritten = $chunk_handler->store($info['index'], $data);
//detect aborted upload
if ($this->request->getMethod() === 'PUT') {
$lengthHeader = $this->request->getHeader('content-length');
if ($lengthHeader) {
$expected = (int)$lengthHeader;
if ($bytesWritten !== $expected) {
$chunk_handler->remove($info['index']);
throw new BadRequest(
$this->l10n->t(
'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
[
$this->l10n->n('%n byte', '%n bytes', $expected),
$this->l10n->n('%n byte', '%n bytes', $bytesWritten),
],
)
);
}
}
}
if ($chunk_handler->isComplete()) {
/** @var Storage $storage */
[$storage,] = $this->fileView->resolvePath($path);
$needsPartFile = $storage->needsPartFile();
$partFile = null;
$targetPath = $path . '/' . $info['name'];
/** @var \OC\Files\Storage\Storage $targetStorage */
[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
$exists = $this->fileView->file_exists($targetPath);
try {
$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
$this->emitPreHooks($exists, $targetPath);
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
/** @var \OC\Files\Storage\Storage $targetStorage */
[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
if ($needsPartFile) {
// we first assembly the target file as a part file
$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
/** @var \OC\Files\Storage\Storage $targetStorage */
[$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
$chunk_handler->file_assemble($partStorage, $partInternalPath);
// here is the final atomic rename
$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
$fileExists = $targetStorage->file_exists($targetInternalPath);
if ($renameOkay === false || $fileExists === false) {
\OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
// only delete if an error occurred and the target file was already created
if ($fileExists) {
// set to null to avoid double-deletion when handling exception
// stray part file
$partFile = null;
$targetStorage->unlink($targetInternalPath);
}
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
}
} else {
// assemble directly into the final file
$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
}
// allow sync clients to send the mtime along in a header
$mtimeHeader = $this->request->getHeader('x-oc-mtime');
if ($mtimeHeader !== '') {
$mtime = $this->sanitizeMtime($mtimeHeader);
if ($targetStorage->touch($targetInternalPath, $mtime)) {
$this->header('X-OC-MTime: accepted');
}
}
// since we skipped the view we need to scan and emit the hooks ourselves
$targetStorage->getUpdater()->update($targetInternalPath);
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
$this->emitPostHooks($exists, $targetPath);
// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
$info = $this->fileView->getFileInfo($targetPath);
$checksumHeader = $this->request->getHeader('oc-checksum');
if ($checksumHeader) {
$checksum = trim($checksumHeader);
$this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
} elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
$this->fileView->putFileInfo($this->path, ['checksum' => '']);
}
$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
return $info->getEtag();
} catch (\Exception $e) {
if ($partFile !== null) {
$targetStorage->unlink($targetInternalPath);
}
$this->convertToSabreException($e);
}
}
return null;
}
/**
* Convert the given exception to a SabreException instance
*
* @param \Exception $e
*
* @throws \Sabre\DAV\Exception
*/
private function convertToSabreException(\Exception $e) {
if ($e instanceof \Sabre\DAV\Exception) {
throw $e;
}
if ($e instanceof NotPermittedException) {
// a more general case - due to whatever reason the content could not be written
throw new Forbidden($e->getMessage(), 0, $e);
}
if ($e instanceof ForbiddenException) {
// the path for the file was forbidden
throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
}
if ($e instanceof EntityTooLargeException) {
// the file is too big to be stored
throw new EntityTooLarge($e->getMessage(), 0, $e);
}
if ($e instanceof InvalidContentException) {
// the file content is not permitted
throw new UnsupportedMediaType($e->getMessage(), 0, $e);
}
if ($e instanceof InvalidPathException) {
// the path for the file was not valid
// TODO: find proper http status code for this case
throw new Forbidden($e->getMessage(), 0, $e);
}
if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
// the file is currently being written to by another process
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
if ($e instanceof GenericEncryptionException) {
// returning 503 will allow retry of the operation at a later point in time
throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
}
if ($e instanceof StorageNotAvailableException) {
throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
}
if ($e instanceof NotFoundException) {
throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
}
throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
}
/**
* Get the checksum for this file
*
* @return string|null
*/
public function getChecksum() {
return $this->info->getChecksum();
}
public function setChecksum(string $checksum) {
$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
$this->refreshInfo();
}
protected function header($string) {
if (!\OC::$CLI) {
\header($string);
}
}
public function hash(string $type) {
return $this->fileView->hash($type, $this->path);
}
public function getNode(): \OCP\Files\File {
return $this->node;
}
}
@@ -0,0 +1,696 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Michael Jobst <mjobst+github@tecratech.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tobias Kaminsky <tobias@kaminsky.me>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OC\AppFramework\Http\Request;
use OCP\Constants;
use OCP\Files\ForbiddenException;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\Exceptions\FilesMetadataException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class FilesPlugin extends ServerPlugin {
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id';
public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid';
public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified';
public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate';
public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname';
public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id';
public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name';
public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden';
/** Reference to main server object */
private ?Server $server = null;
private Tree $tree;
private IUserSession $userSession;
/**
* Whether this is public webdav.
* If true, some returned information will be stripped off.
*/
private bool $isPublic;
private bool $downloadAttachment;
private IConfig $config;
private IRequest $request;
private IPreview $previewManager;
public function __construct(Tree $tree,
IConfig $config,
IRequest $request,
IPreview $previewManager,
IUserSession $userSession,
bool $isPublic = false,
bool $downloadAttachment = true) {
$this->tree = $tree;
$this->config = $config;
$this->request = $request;
$this->userSession = $userSession;
$this->isPublic = $isPublic;
$this->downloadAttachment = $downloadAttachment;
$this->previewManager = $previewManager;
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @return void
*/
public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
$server->protectedProperties[] = self::FILEID_PROPERTYNAME;
$server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME;
$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
$server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME;
$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
$server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_NOTE;
// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
$allowedProperties = ['{DAV:}getetag'];
$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
$this->server->on('afterMethod:GET', [$this,'httpGet']);
$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
$this->server->on('afterResponse', function ($request, ResponseInterface $response) {
$body = $response->getBody();
if (is_resource($body)) {
fclose($body);
}
});
$this->server->on('beforeMove', [$this, 'checkMove']);
}
/**
* Plugin that checks if a move can actually be performed.
*
* @param string $source source path
* @param string $destination destination path
* @throws Forbidden
* @throws NotFound
*/
public function checkMove($source, $destination) {
$sourceNode = $this->tree->getNodeForPath($source);
if (!$sourceNode instanceof Node) {
return;
}
[$sourceDir,] = \Sabre\Uri\split($source);
[$destinationDir,] = \Sabre\Uri\split($destination);
if ($sourceDir !== $destinationDir) {
$sourceNodeFileInfo = $sourceNode->getFileInfo();
if ($sourceNodeFileInfo === null) {
throw new NotFound($source . ' does not exist');
}
if (!$sourceNodeFileInfo->isDeletable()) {
throw new Forbidden($source . " cannot be deleted");
}
}
}
/**
* This sets a cookie to be able to recognize the start of the download
* the content must not be longer than 32 characters and must only contain
* alphanumeric characters
*
* @param RequestInterface $request
* @param ResponseInterface $response
*/
public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) {
$queryParams = $request->getQueryParameters();
/**
* this sets a cookie to be able to recognize the start of the download
* the content must not be longer than 32 characters and must only contain
* alphanumeric characters
*/
if (isset($queryParams['downloadStartSecret'])) {
$token = $queryParams['downloadStartSecret'];
if (!isset($token[32])
&& preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) {
// FIXME: use $response->setHeader() instead
setcookie('ocDownloadStarted', $token, time() + 20, '/');
}
}
}
/**
* Add headers to file download
*
* @param RequestInterface $request
* @param ResponseInterface $response
*/
public function httpGet(RequestInterface $request, ResponseInterface $response) {
// Only handle valid files
$node = $this->tree->getNodeForPath($request->getPath());
if (!($node instanceof IFile)) {
return;
}
// adds a 'Content-Disposition: attachment' header in case no disposition
// header has been set before
if ($this->downloadAttachment &&
$response->getHeader('Content-Disposition') === null) {
$filename = $node->getName();
if ($this->request->isUserAgent(
[
Request::USER_AGENT_IE,
Request::USER_AGENT_ANDROID_MOBILE_CHROME,
Request::USER_AGENT_FREEBOX,
])) {
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
} else {
$response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
. '; filename="' . rawurlencode($filename) . '"');
}
}
if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
//Add OC-Checksum header
$checksum = $node->getChecksum();
if ($checksum !== null && $checksum !== '') {
$response->addHeader('OC-Checksum', $checksum);
}
}
$response->addHeader('X-Accel-Buffering', 'no');
}
/**
* Adds all ownCloud-specific properties
*
* @param PropFind $propFind
* @param \Sabre\DAV\INode $node
* @return void
*/
public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
$httpRequest = $this->server->httpRequest;
if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
/**
* This was disabled, because it made dir listing throw an exception,
* so users were unable to navigate into folders where one subitem
* is blocked by the files_accesscontrol app, see:
* https://github.com/nextcloud/files_accesscontrol/issues/65
* if (!$node->getFileInfo()->isReadable()) {
* // avoid detecting files through this means
* throw new NotFound();
* }
*/
$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) {
return $node->getFileId();
});
$propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) {
return $node->getInternalFileId();
});
$propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) {
$perms = $node->getDavPermissions();
if ($this->isPublic) {
// remove mount information
$perms = str_replace(['S', 'M'], '', $perms);
}
return $perms;
});
$propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) {
$user = $this->userSession->getUser();
if ($user === null) {
return null;
}
return $node->getSharePermissions(
$user->getUID()
);
});
$propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string {
$user = $this->userSession->getUser();
if ($user === null) {
return null;
}
$ncPermissions = $node->getSharePermissions(
$user->getUID()
);
$ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions);
return json_encode($ocmPermissions, JSON_THROW_ON_ERROR);
});
$propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR);
});
$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
return $node->getETag();
});
$propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string {
$owner = $node->getOwner();
if (!$owner) {
return null;
} else {
return $owner->getUID();
}
});
$propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string {
$owner = $node->getOwner();
if (!$owner) {
return null;
} else {
return $owner->getDisplayName();
}
});
$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR);
});
$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float {
return $node->getSize();
});
$propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getMountPoint()->getMountType();
});
/**
* This is a special property which is used to determine if a node
* is a mount root or not, e.g. a shared folder.
* If so, then the node can only be unshared and not deleted.
* @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
*/
$propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
});
$propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest): ?string {
$user = $this->userSession->getUser();
if ($user === null) {
return null;
}
return $node->getNoteFromShare(
$user->getUID()
);
});
$propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
return $this->config->getSystemValue('data-fingerprint', '');
});
$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
return (new \DateTimeImmutable())
->setTimestamp($node->getFileInfo()->getCreationTime())
->format(\DateTimeInterface::ATOM);
});
$propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getCreationTime();
});
foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
}
$propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) {
$isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']);
$isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime';
return ($isLivePhoto && $isMovFile) ? 'true' : 'false';
});
/**
* Return file/folder name as displayname. The primary reason to
* implement it this way is to avoid costly fallback to
* CustomPropertiesBackend (esp. visible when querying all files
* in a folder).
*/
$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
return $node->getName();
});
}
if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
try {
$directDownloadUrl = $node->getDirectDownload();
if (isset($directDownloadUrl['url'])) {
return $directDownloadUrl['url'];
}
} catch (StorageNotAvailableException $e) {
return false;
} catch (ForbiddenException $e) {
return false;
}
return false;
});
$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
$checksum = $node->getChecksum();
if ($checksum === null || $checksum === '') {
return null;
}
return new ChecksumList($checksum);
});
$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getUploadTime();
});
}
if ($node instanceof Directory) {
$propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) {
return $node->getSize();
});
$propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->isEncrypted() ? '1' : '0';
});
$requestProperties = $propFind->getRequestedProperties();
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
$nbFiles = 0;
$nbFolders = 0;
foreach ($node->getChildren() as $child) {
if ($child instanceof File) {
$nbFiles++;
} elseif ($child instanceof Directory) {
$nbFolders++;
}
}
$propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles);
$propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders);
}
}
}
/**
* translate Nextcloud permissions to OCM Permissions
*
* @param $ncPermissions
* @return array
*/
protected function ncPermissions2ocmPermissions($ncPermissions) {
$ocmPermissions = [];
if ($ncPermissions & Constants::PERMISSION_SHARE) {
$ocmPermissions[] = 'share';
}
if ($ncPermissions & Constants::PERMISSION_READ) {
$ocmPermissions[] = 'read';
}
if (($ncPermissions & Constants::PERMISSION_CREATE) ||
($ncPermissions & Constants::PERMISSION_UPDATE)) {
$ocmPermissions[] = 'write';
}
return $ocmPermissions;
}
/**
* Update ownCloud-specific properties
*
* @param string $path
* @param PropPatch $propPatch
*
* @return void
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
return;
}
$propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) {
if (empty($time)) {
return false;
}
$node->touch($time);
return true;
});
$propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) {
if (empty($etag)) {
return false;
}
return $node->setEtag($etag) !== -1;
});
$propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) {
if (empty($time)) {
return false;
}
$dateTime = new \DateTimeImmutable($time);
$node->setCreationTime($dateTime->getTimestamp());
return true;
});
$propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) {
if (empty($time)) {
return false;
}
$node->setCreationTime((int) $time);
return true;
});
$this->handleUpdatePropertiesMetadata($propPatch, $node);
/**
* Disable modification of the displayname property for files and
* folders via PROPPATCH. See PROPFIND for more information.
*/
$propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) {
return 403;
});
}
/**
* handle the update of metadata from PROPPATCH requests
*
* @param PropPatch $propPatch
* @param Node $node
*
* @throws FilesMetadataException
*/
private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void {
$userId = $this->userSession->getUser()?->getUID();
if (null === $userId) {
return;
}
$accessRight = $this->getMetadataFileAccessRight($node, $userId);
$filesMetadataManager = $this->initFilesMetadataManager();
$knownMetadata = $filesMetadataManager->getKnownMetadata();
foreach ($propPatch->getRemainingMutations() as $mutation) {
if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
continue;
}
$propPatch->handle(
$mutation,
function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
// confirm metadata key is editable via PROPPATCH
if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) {
throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
}
// If the metadata is unknown, it defaults to string.
try {
$type = $knownMetadata->getType($metadataKey);
} catch (FilesMetadataNotFoundException) {
$type = IMetadataValueWrapper::TYPE_STRING;
}
switch ($type) {
case IMetadataValueWrapper::TYPE_STRING:
$metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_INT:
$metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_FLOAT:
$metadata->setFloat($metadataKey, $value);
break;
case IMetadataValueWrapper::TYPE_BOOL:
$metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_ARRAY:
$metadata->setArray($metadataKey, $value);
break;
case IMetadataValueWrapper::TYPE_STRING_LIST:
$metadata->setStringList(
$metadataKey, $value, $knownMetadata->isIndex($metadataKey)
);
break;
case IMetadataValueWrapper::TYPE_INT_LIST:
$metadata->setIntList(
$metadataKey, $value, $knownMetadata->isIndex($metadataKey)
);
break;
}
$filesMetadataManager->saveMetadata($metadata);
return true;
}
);
}
}
/**
* init default internal metadata
*
* @return IFilesMetadataManager
*/
private function initFilesMetadataManager(): IFilesMetadataManager {
/** @var IFilesMetadataManager $manager */
$manager = \OCP\Server::get(IFilesMetadataManager::class);
$manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP);
return $manager;
}
/**
* based on owner and shares, returns the bottom limit to update related metadata
*
* @param Node $node
* @param string $userId
*
* @return int
*/
private function getMetadataFileAccessRight(Node $node, string $userId): int {
if ($node->getOwner()?->getUID() === $userId) {
return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP;
} else {
$filePermissions = $node->getSharePermissions($userId);
if ($filePermissions & Constants::PERMISSION_UPDATE) {
return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION;
}
}
return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
}
/**
* @param string $filePath
* @param \Sabre\DAV\INode $node
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
// chunked upload handling
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
[$path, $name] = \Sabre\Uri\split($filePath);
$info = \OC_FileChunking::decodeName($name);
if (!empty($info)) {
$filePath = $path . '/' . $info['name'];
}
}
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
if (!$this->server->tree->nodeExists($filePath)) {
return;
}
$node = $this->server->tree->getNodeForPath($filePath);
if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
$fileId = $node->getFileId();
if (!is_null($fileId)) {
$this->server->httpResponse->setHeader('OC-FileId', $fileId);
}
}
}
}
@@ -0,0 +1,460 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\Connector\Sabre;
use OC\Files\View;
use OCP\App\IAppManager;
use OCP\Files\Folder;
use OCP\Files\Node as INode;
use OCP\IGroupManager;
use OCP\ITagManager;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\PropFind;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\DAV\Xml\Element\Response;
use Sabre\DAV\Xml\Response\MultiStatus;
class FilesReportPlugin extends ServerPlugin {
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @var Tree
*/
private $tree;
/**
* @var View
*/
private $fileView;
/**
* @var ISystemTagManager
*/
private $tagManager;
/**
* @var ISystemTagObjectMapper
*/
private $tagMapper;
/**
* Manager for private tags
*
* @var ITagManager
*/
private $fileTagger;
/**
* @var IUserSession
*/
private $userSession;
/**
* @var IGroupManager
*/
private $groupManager;
/**
* @var Folder
*/
private $userFolder;
/**
* @var IAppManager
*/
private $appManager;
/**
* @param Tree $tree
* @param View $view
* @param ISystemTagManager $tagManager
* @param ISystemTagObjectMapper $tagMapper
* @param ITagManager $fileTagger manager for private tags
* @param IUserSession $userSession
* @param IGroupManager $groupManager
* @param Folder $userFolder
* @param IAppManager $appManager
*/
public function __construct(Tree $tree,
View $view,
ISystemTagManager $tagManager,
ISystemTagObjectMapper $tagMapper,
ITagManager $fileTagger,
IUserSession $userSession,
IGroupManager $groupManager,
Folder $userFolder,
IAppManager $appManager
) {
$this->tree = $tree;
$this->fileView = $view;
$this->tagManager = $tagManager;
$this->tagMapper = $tagMapper;
$this->fileTagger = $fileTagger;
$this->userSession = $userSession;
$this->groupManager = $groupManager;
$this->userFolder = $userFolder;
$this->appManager = $appManager;
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$this->server = $server;
$this->server->on('report', [$this, 'onReport']);
}
/**
* Returns a list of reports this plugin supports.
*
* This will be used in the {DAV:}supported-report-set property.
*
* @param string $uri
* @return array
*/
public function getSupportedReportSet($uri) {
return [self::REPORT_NAME];
}
/**
* REPORT operations to look for files
*
* @param string $reportName
* @param $report
* @param string $uri
* @return bool
* @throws BadRequest
* @throws PreconditionFailed
* @internal param $ [] $report
*/
public function onReport($reportName, $report, $uri) {
$reportTargetNode = $this->server->tree->getNodeForPath($uri);
if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) {
return;
}
$ns = '{' . $this::NS_OWNCLOUD . '}';
$ncns = '{' . $this::NS_NEXTCLOUD . '}';
$requestedProps = [];
$filterRules = [];
// parse report properties and gather filter info
foreach ($report as $reportProps) {
$name = $reportProps['name'];
if ($name === $ns . 'filter-rules') {
$filterRules = $reportProps['value'];
} elseif ($name === '{DAV:}prop') {
// propfind properties
foreach ($reportProps['value'] as $propVal) {
$requestedProps[] = $propVal['name'];
}
} elseif ($name === '{DAV:}limit') {
foreach ($reportProps['value'] as $propVal) {
if ($propVal['name'] === '{DAV:}nresults') {
$limit = (int)$propVal['value'];
} elseif ($propVal['name'] === $ncns . 'firstresult') {
$offset = (int)$propVal['value'];
}
}
}
}
if (empty($filterRules)) {
// an empty filter would return all existing files which would be slow
throw new BadRequest('Missing filter-rule block in request');
}
// gather all file ids matching filter
try {
$resultFileIds = $this->processFilterRulesForFileIDs($filterRules);
// no logic in circles and favorites for paging, we always have all results, and slice later on
$resultFileIds = array_slice($resultFileIds, $offset ?? 0, $limit ?? null);
// fetching nodes has paging on DB level therefore we cannot mix and slice the results, similar
// to user backends. I.e. the final result may return more results than requested.
$resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null);
} catch (TagNotFoundException $e) {
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
}
$results = [];
foreach ($resultNodes as $entry) {
if ($entry) {
$results[] = $this->wrapNode($entry);
}
}
// find sabre nodes by file id, restricted to the root node path
$additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
if ($additionalNodes && $results) {
$results = array_uintersect($results, $additionalNodes, function (Node $a, Node $b): int {
return $a->getId() - $b->getId();
});
} elseif (!$results && $additionalNodes) {
$results = $additionalNodes;
}
$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
$xml = $this->server->xml->write(
'{DAV:}multistatus',
new MultiStatus($responses)
);
$this->server->httpResponse->setStatus(207);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setBody($xml);
return false;
}
/**
* Returns the base uri of the files root by removing
* the subpath from the URI
*
* @param string $uri URI from this request
* @param string $subPath subpath to remove from the URI
*
* @return string files base uri
*/
private function getFilesBaseUri(string $uri, string $subPath): string {
$uri = trim($uri, '/');
$subPath = trim($subPath, '/');
if (empty($subPath)) {
$filesUri = $uri;
} else {
$filesUri = substr($uri, 0, strlen($uri) - strlen($subPath));
}
$filesUri = trim($filesUri, '/');
if (empty($filesUri)) {
return '';
}
return '/' . $filesUri;
}
/**
* Find file ids matching the given filter rules
*
* @param array $filterRules
* @return array array of unique file id results
*/
protected function processFilterRulesForFileIDs(array $filterRules): array {
$ns = '{' . $this::NS_OWNCLOUD . '}';
$resultFileIds = [];
$circlesIds = [];
$favoriteFilter = null;
foreach ($filterRules as $filterRule) {
if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
$circlesIds[] = $filterRule['value'];
}
if ($filterRule['name'] === $ns . 'favorite') {
$favoriteFilter = true;
}
}
if ($favoriteFilter !== null) {
$resultFileIds = $this->fileTagger->load('files')->getFavorites();
if (empty($resultFileIds)) {
return [];
}
}
if (!empty($circlesIds)) {
$fileIds = $this->getCirclesFileIds($circlesIds);
if (empty($resultFileIds)) {
$resultFileIds = $fileIds;
} else {
$resultFileIds = array_intersect($fileIds, $resultFileIds);
}
}
return $resultFileIds;
}
protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array {
$systemTagIds = [];
foreach ($filterRules as $filterRule) {
if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) {
$systemTagIds[] = $filterRule['value'];
}
}
$nodes = [];
if (!empty($systemTagIds)) {
$tags = $this->tagManager->getTagsByIds($systemTagIds, $this->userSession->getUser());
// For we run DB queries per tag and require intersection, we cannot apply limit and offset for DB queries on multi tag search.
$oneTagSearch = count($tags) === 1;
$dbLimit = $oneTagSearch ? $limit ?? 0 : 0;
$dbOffset = $oneTagSearch ? $offset ?? 0 : 0;
foreach ($tags as $tag) {
$tagName = $tag->getName();
$tmpNodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $dbLimit, $dbOffset);
if (count($nodes) === 0) {
$nodes = $tmpNodes;
} else {
$nodes = array_uintersect($nodes, $tmpNodes, function (INode $a, INode $b): int {
return $a->getId() - $b->getId();
});
}
if ($nodes === []) {
// there cannot be a common match when nodes are empty early.
return $nodes;
}
}
if (!$oneTagSearch && ($limit !== null || $offset !== null)) {
$nodes = array_slice($nodes, $offset, $limit);
}
}
return $nodes;
}
/**
* @suppress PhanUndeclaredClassMethod
* @param array $circlesIds
* @return array
*/
private function getCirclesFileIds(array $circlesIds) {
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
return [];
}
return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds);
}
/**
* Prepare propfind response for the given nodes
*
* @param string $filesUri $filesUri URI leading to root of the files URI,
* with a leading slash but no trailing slash
* @param string[] $requestedProps requested properties
* @param Node[] nodes nodes for which to fetch and prepare responses
* @return Response[]
*/
public function prepareResponses($filesUri, $requestedProps, $nodes) {
$responses = [];
foreach ($nodes as $node) {
$propFind = new PropFind($filesUri . $node->getPath(), $requestedProps);
$this->server->getPropertiesByNode($propFind, $node);
// copied from Sabre Server's getPropertiesForPath
$result = $propFind->getResultForMultiStatus();
$result['href'] = $propFind->getPath();
$resourceType = $this->server->getResourceTypeForNode($node);
if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
$result['href'] .= '/';
}
$responses[] = new Response(
rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
$result,
);
}
return $responses;
}
/**
* Find Sabre nodes by file ids
*
* @param Node $rootNode root node for search
* @param array $fileIds file ids
* @return Node[] array of Sabre nodes
*/
public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
if (empty($fileIds)) {
return [];
}
$folder = $this->userFolder;
if (trim($rootNode->getPath(), '/') !== '') {
$folder = $folder->get($rootNode->getPath());
}
$results = [];
foreach ($fileIds as $fileId) {
$entry = $folder->getById($fileId);
if ($entry) {
$entry = current($entry);
$results[] = $this->wrapNode($entry);
}
}
return $results;
}
protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory {
if ($node instanceof \OCP\Files\File) {
return new File($this->fileView, $node);
} else {
return new Directory($this->fileView, $node);
}
}
/**
* Returns whether the currently logged in user is an administrator
*/
private function isAdmin() {
$user = $this->userSession->getUser();
if ($user !== null) {
return $this->groupManager->isAdmin($user->getUID());
}
return false;
}
}
@@ -0,0 +1,100 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Jaakko Salo <jaakkos@gmail.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Stefan Weil <sw@weilnetz.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
class LockPlugin extends ServerPlugin {
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* State of the lock
*
* @var bool
*/
private $isLocked;
/**
* {@inheritdoc}
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('beforeMethod:*', [$this, 'getLock'], 50);
$this->server->on('afterMethod:*', [$this, 'releaseLock'], 50);
$this->isLocked = false;
}
public function getLock(RequestInterface $request) {
// we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
// so instead we limit ourselves to the PUT method manually
if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
return;
}
try {
$node = $this->server->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
if ($node instanceof Node) {
try {
$node->acquireLock(ILockingProvider::LOCK_SHARED);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
$this->isLocked = true;
}
}
public function releaseLock(RequestInterface $request) {
// don't try to release the lock if we never locked one
if ($this->isLocked === false) {
return;
}
if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
return;
}
try {
$node = $this->server->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
if ($node instanceof Node) {
$node->releaseLock(ILockingProvider::LOCK_SHARED);
$this->isLocked = false;
}
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Valdnet <47037905+Valdnet@users.noreply.github.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OCA\DAV\Exception\ServerMaintenanceMode;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Util;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\ServerPlugin;
class MaintenancePlugin extends ServerPlugin {
/** @var IConfig */
private $config;
/** @var \OCP\IL10N */
private $l10n;
/**
* Reference to main server object
*
* @var Server
*/
private $server;
/**
* @param IConfig $config
*/
public function __construct(IConfig $config, IL10N $l10n) {
$this->config = $config;
$this->l10n = \OC::$server->getL10N('dav');
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('beforeMethod:*', [$this, 'checkMaintenanceMode'], 1);
}
/**
* This method is called before any HTTP method and returns http status code 503
* in case the system is in maintenance mode.
*
* @throws ServiceUnavailable
* @return bool
*/
public function checkMaintenanceMode() {
if ($this->config->getSystemValueBool('maintenance')) {
throw new ServerMaintenanceMode($this->l10n->t('System is in maintenance mode.'));
}
if (Util::needUpgrade()) {
throw new ServerMaintenanceMode($this->l10n->t('Upgrade needed'));
}
return true;
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
*
* @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\Connector\Sabre;
class MtimeSanitizer {
public static function sanitizeMtime(string $mtimeFromRequest): int {
// In PHP 5.X "is_numeric" returns true for strings in hexadecimal
// notation. This is no longer the case in PHP 7.X, so this check
// ensures that strings with hexadecimal notations fail too in PHP 5.X.
$isHexadecimal = preg_match('/^\s*0[xX]/', $mtimeFromRequest);
if ($isHexadecimal || !is_numeric($mtimeFromRequest)) {
throw new \InvalidArgumentException('X-OC-MTime header must be an integer (unix timestamp).');
}
// Prevent writing invalid mtime (timezone-proof)
if ((int)$mtimeFromRequest <= 24 * 60 * 60) {
throw new \InvalidArgumentException('X-OC-MTime header must be a valid positive integer');
}
return (int)$mtimeFromRequest;
}
}
@@ -0,0 +1,427 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Björn Schießle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
* @author Jakob Sack <mail@jakobsack.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Klaas Freitag <freitag@owncloud.com>
* @author Markus Goetz <markus@woboq.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tobias Kaminsky <tobias@kaminsky.me>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\Node\File;
use OC\Files\Node\Folder;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\DavUtil;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\Files\StorageNotAvailableException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
abstract class Node implements \Sabre\DAV\INode {
/**
* @var View
*/
protected $fileView;
/**
* The path to the current node
*
* @var string
*/
protected $path;
/**
* node properties cache
*
* @var array
*/
protected $property_cache = null;
protected FileInfo $info;
/**
* @var IManager
*/
protected $shareManager;
protected \OCP\Files\Node $node;
/**
* Sets up the node, expects a full path name
*/
public function __construct(View $view, FileInfo $info, IManager $shareManager = null) {
$this->fileView = $view;
$this->path = $this->fileView->getRelativePath($info->getPath());
$this->info = $info;
if ($shareManager) {
$this->shareManager = $shareManager;
} else {
$this->shareManager = \OC::$server->getShareManager();
}
if ($info instanceof Folder || $info instanceof File) {
$this->node = $info;
} else {
// The Node API assumes that the view passed doesn't have a fake root
$rootView = \OC::$server->get(View::class);
$root = \OC::$server->get(IRootFolder::class);
if ($info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
} else {
$this->node = new File($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
}
}
}
protected function refreshInfo(): void {
$info = $this->fileView->getFileInfo($this->path);
if ($info === false) {
throw new \Sabre\DAV\Exception('Failed to get fileinfo for '. $this->path);
}
$this->info = $info;
$root = \OC::$server->get(IRootFolder::class);
$rootView = \OC::$server->get(View::class);
if ($this->info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->path, $this->info);
} else {
$this->node = new File($root, $rootView, $this->path, $this->info);
}
}
/**
* Returns the name of the node
*
* @return string
*/
public function getName() {
return $this->info->getName();
}
/**
* Returns the full path
*
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* Renames the node
*
* @param string $name The new name
* @throws \Sabre\DAV\Exception\BadRequest
* @throws \Sabre\DAV\Exception\Forbidden
*/
public function setName($name) {
// rename is only allowed if the update privilege is granted
if (!($this->info->isUpdateable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
throw new \Sabre\DAV\Exception\Forbidden();
}
[$parentPath,] = \Sabre\Uri\split($this->path);
[, $newName] = \Sabre\Uri\split($name);
// verify path of the target
$this->verifyPath();
$newPath = $parentPath . '/' . $newName;
if (!$this->fileView->rename($this->path, $newPath)) {
throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath);
}
$this->path = $newPath;
$this->refreshInfo();
}
public function setPropertyCache($property_cache) {
$this->property_cache = $property_cache;
}
/**
* Returns the last modification time, as a unix timestamp
*
* @return int timestamp as integer
*/
public function getLastModified() {
$timestamp = $this->info->getMtime();
if (!empty($timestamp)) {
return (int)$timestamp;
}
return $timestamp;
}
/**
* sets the last modification time of the file (mtime) to the value given
* in the second parameter or to now if the second param is empty.
* Even if the modification time is set to a custom value the access time is set to now.
*/
public function touch($mtime) {
$mtime = $this->sanitizeMtime($mtime);
$this->fileView->touch($this->path, $mtime);
$this->refreshInfo();
}
/**
* Returns the ETag for a file
*
* An ETag is a unique identifier representing the current version of the
* file. If the file changes, the ETag MUST change. The ETag is an
* arbitrary string, but MUST be surrounded by double-quotes.
*
* Return null if the ETag can not effectively be determined
*
* @return string
*/
public function getETag() {
return '"' . $this->info->getEtag() . '"';
}
/**
* Sets the ETag
*
* @param string $etag
*
* @return int file id of updated file or -1 on failure
*/
public function setETag($etag) {
return $this->fileView->putFileInfo($this->path, ['etag' => $etag]);
}
public function setCreationTime(int $time) {
return $this->fileView->putFileInfo($this->path, ['creation_time' => $time]);
}
public function setUploadTime(int $time) {
return $this->fileView->putFileInfo($this->path, ['upload_time' => $time]);
}
/**
* Returns the size of the node, in bytes
*
* @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
* @return int|float
*/
public function getSize(): int|float {
return $this->info->getSize();
}
/**
* Returns the cache's file id
*
* @return int
*/
public function getId() {
return $this->info->getId();
}
/**
* @return string|null
*/
public function getFileId() {
if ($id = $this->info->getId()) {
return DavUtil::getDavFileId($id);
}
return null;
}
/**
* @return integer
*/
public function getInternalFileId() {
return $this->info->getId();
}
public function getInternalPath(): string {
return $this->info->getInternalPath();
}
/**
* @param string $user
* @return int
*/
public function getSharePermissions($user) {
// check of we access a federated share
if ($user !== null) {
try {
$share = $this->shareManager->getShareByToken($user);
return $share->getPermissions();
} catch (ShareNotFound $e) {
// ignore
}
}
try {
$storage = $this->info->getStorage();
} catch (StorageNotAvailableException $e) {
$storage = null;
}
if ($storage && $storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$permissions = (int)$storage->getShare()->getPermissions();
} else {
$permissions = $this->info->getPermissions();
}
/*
* We can always share non moveable mount points with DELETE and UPDATE
* Eventually we need to do this properly
*/
$mountpoint = $this->info->getMountPoint();
if (!($mountpoint instanceof MoveableMount)) {
$mountpointpath = $mountpoint->getMountPoint();
if (substr($mountpointpath, -1) === '/') {
$mountpointpath = substr($mountpointpath, 0, -1);
}
if (!$mountpoint->getOption('readonly', false) && $mountpointpath === $this->info->getPath()) {
$permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
}
}
/*
* Files can't have create or delete permissions
*/
if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
$permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE);
}
return $permissions;
}
/**
* @return array
*/
public function getShareAttributes(): array {
$attributes = [];
try {
$storage = $this->info->getStorage();
} catch (StorageNotAvailableException $e) {
$storage = null;
}
if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$attributes = $storage->getShare()->getAttributes();
if ($attributes === null) {
return [];
} else {
return $attributes->toArray();
}
}
return $attributes;
}
/**
* @param string $user
* @return string
*/
public function getNoteFromShare($user) {
if ($user === null) {
return '';
}
// Retrieve note from the share object already loaded into
// memory, to avoid additional database queries.
$storage = $this->getNode()->getStorage();
if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
return '';
}
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$share = $storage->getShare();
$note = $share->getNote();
if ($share->getShareOwner() !== $user) {
return $note;
}
return '';
}
/**
* @return string
*/
public function getDavPermissions() {
return DavUtil::getDavPermissions($this->info);
}
public function getOwner() {
return $this->info->getOwner();
}
protected function verifyPath() {
try {
$fileName = basename($this->info->getPath());
$this->fileView->verifyPath($this->path, $fileName);
} catch (\OCP\Files\InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function acquireLock($type) {
$this->fileView->lockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function releaseLock($type) {
$this->fileView->unlockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function changeLock($type) {
$this->fileView->changeLock($this->path, $type);
}
public function getFileInfo() {
return $this->info;
}
public function getNode(): \OCP\Files\Node {
return $this->node;
}
protected function sanitizeMtime($mtimeFromRequest) {
return MtimeSanitizer::sanitizeMtime($mtimeFromRequest);
}
}
@@ -0,0 +1,242 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OC\Files\FileInfo;
use OC\Files\Storage\FailedStorage;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\ForbiddenException;
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Lock\LockedException;
class ObjectTree extends CachingTree {
/**
* @var \OC\Files\View
*/
protected $fileView;
/**
* @var \OCP\Files\Mount\IMountManager
*/
protected $mountManager;
/**
* Creates the object
*/
public function __construct() {
}
/**
* @param \Sabre\DAV\INode $rootNode
* @param \OC\Files\View $view
* @param \OCP\Files\Mount\IMountManager $mountManager
*/
public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) {
$this->rootNode = $rootNode;
$this->fileView = $view;
$this->mountManager = $mountManager;
}
/**
* If the given path is a chunked file name, converts it
* to the real file name. Only applies if the OC-CHUNKED header
* is present.
*
* @param string $path chunk file path to convert
*
* @return string path to real file
*/
private function resolveChunkFile($path) {
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
// resolve to real file name to find the proper node
[$dir, $name] = \Sabre\Uri\split($path);
if ($dir === '/' || $dir === '.') {
$dir = '';
}
$info = \OC_FileChunking::decodeName($name);
// only replace path if it was really the chunked file
if (isset($info['transferid'])) {
// getNodePath is called for multiple nodes within a chunk
// upload call
$path = $dir . '/' . $info['name'];
$path = ltrim($path, '/');
}
}
return $path;
}
/**
* Returns the INode object for the requested path
*
* @param string $path
* @return \Sabre\DAV\INode
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\Locked
* @throws \Sabre\DAV\Exception\NotFound
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getNodeForPath($path) {
if (!$this->fileView) {
throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup');
}
$path = trim($path, '/');
if (isset($this->cache[$path])) {
return $this->cache[$path];
}
if ($path) {
try {
$this->fileView->verifyPath($path, basename($path));
} catch (\OCP\Files\InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
// Is it the root node?
if (!strlen($path)) {
return $this->rootNode;
}
if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
// read from storage
$absPath = $this->fileView->getAbsolutePath($path);
$mount = $this->fileView->getMount($path);
$storage = $mount->getStorage();
$internalPath = $mount->getInternalPath($absPath);
if ($storage && $storage->file_exists($internalPath)) {
/**
* @var \OC\Files\Storage\Storage $storage
*/
// get data directly
$data = $storage->getMetaData($internalPath);
$info = new FileInfo($absPath, $storage, $internalPath, $data, $mount);
} else {
$info = null;
}
} else {
// resolve chunk file name to real name, if applicable
$path = $this->resolveChunkFile($path);
// read from cache
try {
$info = $this->fileView->getFileInfo($path);
if ($info instanceof \OCP\Files\FileInfo && $info->getStorage()->instanceOfStorage(FailedStorage::class)) {
throw new StorageNotAvailableException();
}
} catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable('Storage is temporarily not available', 0, $e);
} catch (StorageInvalidException $e) {
throw new \Sabre\DAV\Exception\NotFound('Storage ' . $path . ' is invalid');
} catch (LockedException $e) {
throw new \Sabre\DAV\Exception\Locked();
} catch (ForbiddenException $e) {
throw new \Sabre\DAV\Exception\Forbidden();
}
}
if (!$info) {
throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
}
if ($info->getType() === 'dir') {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this);
} else {
$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
}
$this->cache[$path] = $node;
return $node;
}
/**
* Copies a file or directory.
*
* This method must work recursively and delete the destination
* if it exists
*
* @param string $sourcePath
* @param string $destinationPath
* @throws FileLocked
* @throws Forbidden
* @throws InvalidPath
* @throws \Exception
* @throws \Sabre\DAV\Exception\Forbidden
* @throws \Sabre\DAV\Exception\Locked
* @throws \Sabre\DAV\Exception\NotFound
* @throws \Sabre\DAV\Exception\ServiceUnavailable
* @return void
*/
public function copy($sourcePath, $destinationPath) {
if (!$this->fileView) {
throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup');
}
$info = $this->fileView->getFileInfo(dirname($destinationPath));
if ($this->fileView->file_exists($destinationPath)) {
$destinationPermission = $info && $info->isUpdateable();
} else {
$destinationPermission = $info && $info->isCreatable();
}
if (!$destinationPermission) {
throw new Forbidden('No permissions to copy object.');
}
// this will trigger existence check
$this->getNodeForPath($sourcePath);
[$destinationDir, $destinationName] = \Sabre\Uri\split($destinationPath);
try {
$this->fileView->verifyPath($destinationDir, $destinationName);
} catch (\OCP\Files\InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
try {
$this->fileView->copy($sourcePath, $destinationPath);
} catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry());
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
[$destinationDir,] = \Sabre\Uri\split($destinationPath);
$this->markDirty($destinationDir);
}
}
@@ -0,0 +1,655 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2018, Georg Ehrke
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Seitz <christoph.seitz@posteo.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Jakob Sack <mail@jakobsack.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
* @author Vinicius Cubas Brand <vinicius@eita.org.br>
*
* @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\Connector\Sabre;
use OC\KnownUser\KnownUserService;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Traits\PrincipalProxyTrait;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\Accounts\PropertyDoesNotExistException;
use OCP\App\IAppManager;
use OCP\AppFramework\QueryException;
use OCP\Constants;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Share\IManager as IShareManager;
use Sabre\DAV\Exception;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class Principal implements BackendInterface {
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IAccountManager */
private $accountManager;
/** @var IShareManager */
private $shareManager;
/** @var IUserSession */
private $userSession;
/** @var IAppManager */
private $appManager;
/** @var string */
private $principalPrefix;
/** @var bool */
private $hasGroups;
/** @var bool */
private $hasCircles;
/** @var ProxyMapper */
private $proxyMapper;
/** @var KnownUserService */
private $knownUserService;
/** @var IConfig */
private $config;
/** @var IFactory */
private $languageFactory;
public function __construct(IUserManager $userManager,
IGroupManager $groupManager,
IAccountManager $accountManager,
IShareManager $shareManager,
IUserSession $userSession,
IAppManager $appManager,
ProxyMapper $proxyMapper,
KnownUserService $knownUserService,
IConfig $config,
IFactory $languageFactory,
string $principalPrefix = 'principals/users/') {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->accountManager = $accountManager;
$this->shareManager = $shareManager;
$this->userSession = $userSession;
$this->appManager = $appManager;
$this->principalPrefix = trim($principalPrefix, '/');
$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
$this->proxyMapper = $proxyMapper;
$this->knownUserService = $knownUserService;
$this->config = $config;
$this->languageFactory = $languageFactory;
}
use PrincipalProxyTrait {
getGroupMembership as protected traitGetGroupMembership;
}
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
*
* @param string $prefixPath
* @return string[]
*/
public function getPrincipalsByPrefix($prefixPath) {
$principals = [];
if ($prefixPath === $this->principalPrefix) {
foreach ($this->userManager->search('') as $user) {
$principals[] = $this->userToPrincipal($user);
}
}
return $principals;
}
/**
* Returns a specific principal, specified by it's path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path) {
[$prefix, $name] = \Sabre\Uri\split($path);
$decodedName = urldecode($name);
if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
[$prefix2, $name2] = \Sabre\Uri\split($prefix);
if ($prefix2 === $this->principalPrefix) {
$user = $this->userManager->get($name2);
if ($user !== null) {
return [
'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
];
}
return null;
}
}
if ($prefix === $this->principalPrefix) {
// Depending on where it is called, it may happen that this function
// is called either with a urlencoded version of the name or with a non-urlencoded one.
// The urldecode function replaces %## and +, both of which are forbidden in usernames.
// Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
$user = $this->userManager->get($decodedName);
if ($user !== null) {
return $this->userToPrincipal($user);
}
} elseif ($prefix === 'principals/circles') {
if ($this->userSession->getUser() !== null) {
// At the time of writing - 2021-01-19 — a mixed state is possible.
// The second condition can be removed when this is fixed.
return $this->circleToPrincipal($decodedName)
?: $this->circleToPrincipal($name);
}
} elseif ($prefix === 'principals/groups') {
// At the time of writing - 2021-01-19 — a mixed state is possible.
// The second condition can be removed when this is fixed.
$group = $this->groupManager->get($decodedName)
?: $this->groupManager->get($name);
if ($group instanceof IGroup) {
return [
'uri' => 'principals/groups/' . $name,
'{DAV:}displayname' => $group->getDisplayName(),
];
}
} elseif ($prefix === 'principals/system') {
return [
'uri' => 'principals/system/' . $name,
'{DAV:}displayname' => $this->languageFactory->get('dav')->t("Accounts"),
];
}
return null;
}
/**
* Returns the list of groups a principal is a member of
*
* @param string $principal
* @param bool $needGroups
* @return array
* @throws Exception
*/
public function getGroupMembership($principal, $needGroups = false) {
[$prefix, $name] = \Sabre\Uri\split($principal);
if ($prefix !== $this->principalPrefix) {
return [];
}
$user = $this->userManager->get($name);
if (!$user) {
throw new Exception('Principal not found');
}
$groups = [];
if ($this->hasGroups || $needGroups) {
$userGroups = $this->groupManager->getUserGroups($user);
foreach ($userGroups as $userGroup) {
$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
}
}
$groups = array_unique(array_merge(
$groups,
$this->traitGetGroupMembership($principal, $needGroups)
));
return $groups;
}
/**
* @param string $path
* @param PropPatch $propPatch
* @return int
*/
public function updatePrincipal($path, PropPatch $propPatch) {
return 0;
}
/**
* Search user principals
*
* @param array $searchProperties
* @param string $test
* @return array
*/
protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
$results = [];
// If sharing is disabled, return the empty array
$shareAPIEnabled = $this->shareManager->shareApiEnabled();
if (!$shareAPIEnabled) {
return [];
}
$allowEnumeration = $this->shareManager->allowEnumeration();
$limitEnumerationGroup = $this->shareManager->limitEnumerationToGroups();
$limitEnumerationPhone = $this->shareManager->limitEnumerationToPhone();
$allowEnumerationFullMatch = $this->shareManager->allowEnumerationFullMatch();
$ignoreSecondDisplayName = $this->shareManager->ignoreSecondDisplayName();
$matchEmail = $this->shareManager->matchEmail();
// If sharing is restricted to group members only,
// return only members that have groups in common
$restrictGroups = false;
$currentUser = $this->userSession->getUser();
if ($this->shareManager->shareWithGroupMembersOnly()) {
if (!$currentUser instanceof IUser) {
return [];
}
$restrictGroups = $this->groupManager->getUserGroupIds($currentUser);
}
$currentUserGroups = [];
if ($limitEnumerationGroup) {
if ($currentUser instanceof IUser) {
$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
}
}
$searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
if ($searchLimit <= 0) {
$searchLimit = null;
}
foreach ($searchProperties as $prop => $value) {
switch ($prop) {
case '{http://sabredav.org/ns}email-address':
if (!$allowEnumeration) {
if ($allowEnumerationFullMatch && $matchEmail) {
$users = $this->userManager->getByEmail($value);
} else {
$users = [];
}
} else {
$users = $this->userManager->getByEmail($value);
$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
if ($allowEnumerationFullMatch && $user->getSystemEMailAddress() === $value) {
return true;
}
if ($limitEnumerationPhone
&& $currentUser instanceof IUser
&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
// Synced phonebook match
return true;
}
if (!$limitEnumerationGroup) {
// No limitation on enumeration, all allowed
return true;
}
return !empty($currentUserGroups) && !empty(array_intersect(
$this->groupManager->getUserGroupIds($user),
$currentUserGroups
));
});
}
$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
// is sharing restricted to groups only?
if ($restrictGroups !== false) {
$userGroups = $this->groupManager->getUserGroupIds($user);
if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
return $carry;
}
}
$carry[] = $this->principalPrefix . '/' . $user->getUID();
return $carry;
}, []);
break;
case '{DAV:}displayname':
if (!$allowEnumeration) {
if ($allowEnumerationFullMatch) {
$lowerSearch = strtolower($value);
$users = $this->userManager->searchDisplayName($value, $searchLimit);
$users = \array_filter($users, static function (IUser $user) use ($lowerSearch, $ignoreSecondDisplayName) {
$lowerDisplayName = strtolower($user->getDisplayName());
return $lowerDisplayName === $lowerSearch || ($ignoreSecondDisplayName && trim(preg_replace('/ \(.*\)$/', '', $lowerDisplayName)) === $lowerSearch);
});
} else {
$users = [];
}
} else {
$users = $this->userManager->searchDisplayName($value, $searchLimit);
$users = \array_filter($users, function (IUser $user) use ($currentUser, $value, $limitEnumerationPhone, $limitEnumerationGroup, $allowEnumerationFullMatch, $currentUserGroups) {
if ($allowEnumerationFullMatch && $user->getDisplayName() === $value) {
return true;
}
if ($limitEnumerationPhone
&& $currentUser instanceof IUser
&& $this->knownUserService->isKnownToUser($currentUser->getUID(), $user->getUID())) {
// Synced phonebook match
return true;
}
if (!$limitEnumerationGroup) {
// No limitation on enumeration, all allowed
return true;
}
return !empty($currentUserGroups) && !empty(array_intersect(
$this->groupManager->getUserGroupIds($user),
$currentUserGroups
));
});
}
$results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
// is sharing restricted to groups only?
if ($restrictGroups !== false) {
$userGroups = $this->groupManager->getUserGroupIds($user);
if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
return $carry;
}
}
$carry[] = $this->principalPrefix . '/' . $user->getUID();
return $carry;
}, []);
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
// If you add support for more search properties that qualify as a user-address,
// please also add them to the array below
$results[] = $this->searchUserPrincipals([
// In theory this should also search for principal:principals/users/...
// but that's used internally only anyway and i don't know of any client querying that
'{http://sabredav.org/ns}email-address' => $value,
], 'anyof');
break;
default:
$results[] = [];
break;
}
}
// results is an array of arrays, so this is not the first search result
// but the results of the first searchProperty
if (count($results) === 1) {
return $results[0];
}
switch ($test) {
case 'anyof':
return array_values(array_unique(array_merge(...$results)));
case 'allof':
default:
return array_values(array_intersect(...$results));
}
}
/**
* @param string $prefixPath
* @param array $searchProperties
* @param string $test
* @return array
*/
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
if (count($searchProperties) === 0) {
return [];
}
switch ($prefixPath) {
case 'principals/users':
return $this->searchUserPrincipals($searchProperties, $test);
default:
return [];
}
}
/**
* @param string $uri
* @param string $principalPrefix
* @return string
*/
public function findByUri($uri, $principalPrefix) {
// If sharing is disabled, return the empty array
$shareAPIEnabled = $this->shareManager->shareApiEnabled();
if (!$shareAPIEnabled) {
return null;
}
// If sharing is restricted to group members only,
// return only members that have groups in common
$restrictGroups = false;
if ($this->shareManager->shareWithGroupMembersOnly()) {
$user = $this->userSession->getUser();
if (!$user) {
return null;
}
$restrictGroups = $this->groupManager->getUserGroupIds($user);
}
if (str_starts_with($uri, 'mailto:')) {
if ($principalPrefix === 'principals/users') {
$users = $this->userManager->getByEmail(substr($uri, 7));
if (count($users) !== 1) {
return null;
}
$user = $users[0];
if ($restrictGroups !== false) {
$userGroups = $this->groupManager->getUserGroupIds($user);
if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
return null;
}
}
return $this->principalPrefix . '/' . $user->getUID();
}
}
if (substr($uri, 0, 10) === 'principal:') {
$principal = substr($uri, 10);
$principal = $this->getPrincipalByPath($principal);
if ($principal !== null) {
return $principal['uri'];
}
}
return null;
}
/**
* @param IUser $user
* @return array
* @throws PropertyDoesNotExistException
*/
protected function userToPrincipal($user) {
$userId = $user->getUID();
$displayName = $user->getDisplayName();
$principal = [
'uri' => $this->principalPrefix . '/' . $userId,
'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
'{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user),
];
$account = $this->accountManager->getAccount($user);
$alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties());
$email = $user->getSystemEMailAddress();
if (!empty($email)) {
$principal['{http://sabredav.org/ns}email-address'] = $email;
}
if (!empty($alternativeEmails)) {
$principal['{DAV:}alternate-URI-set'] = $alternativeEmails;
}
return $principal;
}
public function getPrincipalPrefix() {
return $this->principalPrefix;
}
/**
* @param string $circleUniqueId
* @return array|null
*/
protected function circleToPrincipal($circleUniqueId) {
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
return null;
}
try {
$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
} catch (QueryException $ex) {
return null;
} catch (CircleNotFoundException $ex) {
return null;
}
if (!$circle) {
return null;
}
$principal = [
'uri' => 'principals/circles/' . $circleUniqueId,
'{DAV:}displayname' => $circle->getDisplayName(),
];
return $principal;
}
/**
* Returns the list of circles a principal is a member of
*
* @param string $principal
* @return array
* @throws Exception
* @throws \OCP\AppFramework\QueryException
* @suppress PhanUndeclaredClassMethod
*/
public function getCircleMembership($principal):array {
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
return [];
}
[$prefix, $name] = \Sabre\Uri\split($principal);
if ($this->hasCircles && $prefix === $this->principalPrefix) {
$user = $this->userManager->get($name);
if (!$user) {
throw new Exception('Principal not found');
}
$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
$circles = array_map(function ($circle) {
/** @var \OCA\Circles\Model\Circle $circle */
return 'principals/circles/' . urlencode($circle->getSingleId());
}, $circles);
return $circles;
}
return [];
}
/**
* Get all email addresses associated to a principal.
*
* @param array $principal Data from getPrincipal*()
* @return string[] All email addresses without the mailto: prefix
*/
public function getEmailAddressesOfPrincipal(array $principal): array {
$emailAddresses = [];
if (isset($principal['{http://sabredav.org/ns}email-address'])) {
$emailAddresses[] = $principal['{http://sabredav.org/ns}email-address'];
}
if (isset($principal['{DAV:}alternate-URI-set'])) {
foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
if (str_starts_with($address, 'mailto:')) {
$emailAddresses[] = substr($address, 7);
}
}
}
if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
if (str_starts_with($address, 'mailto:')) {
$emailAddresses[] = substr($address, 7);
}
}
}
if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
if (str_starts_with($address, 'mailto:')) {
$emailAddresses[] = substr($address, 7);
}
}
}
return array_values(array_unique($emailAddresses));
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\Request;
use Sabre\HTTP\Response;
class PropfindCompressionPlugin extends ServerPlugin {
/**
* Reference to main server object
*
* @var Server
*/
private $server;
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('afterMethod:PROPFIND', [$this, 'compressResponse'], 100);
}
public function compressResponse(Request $request, Response $response) {
$header = $request->getHeader('Accept-Encoding');
if ($header === null) {
return $response;
}
if (str_contains($header, 'gzip')) {
$body = $response->getBody();
if (is_string($body)) {
$response->setHeader('Content-Encoding', 'gzip');
$response->setBody(gzencode($body));
}
}
return $response;
}
}
@@ -0,0 +1,256 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
* @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Felix Moeller <mail@felixmoeller.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author scambra <sergio@entrecables.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OCA\DAV\Upload\FutureFile;
use OCP\Files\StorageNotAvailableException;
use Sabre\DAV\Exception\InsufficientStorage;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\INode;
/**
* This plugin check user quota and deny creating files when they exceeds the quota.
*
* @author Sergio Cambra
* @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
/** @var \OC\Files\View */
private $view;
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @param \OC\Files\View $view
*/
public function __construct($view) {
$this->view = $view;
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the requires event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
$server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
$server->on('beforeMove', [$this, 'beforeMove'], 10);
$server->on('beforeCopy', [$this, 'beforeCopy'], 10);
}
/**
* Check quota before creating file
*
* @param string $uri target file URI
* @param resource $data data
* @param INode $parent Sabre Node
* @param bool $modified modified
*/
public function beforeCreateFile($uri, $data, INode $parent, $modified) {
if (!$parent instanceof Node) {
return;
}
return $this->checkQuota($parent->getPath() . '/' . basename($uri));
}
/**
* Check quota before writing content
*
* @param string $uri target file URI
* @param INode $node Sabre Node
* @param resource $data data
* @param bool $modified modified
*/
public function beforeWriteContent($uri, INode $node, $data, $modified) {
if (!$node instanceof Node) {
return;
}
return $this->checkQuota($node->getPath());
}
/**
* Check if we're moving a Futurefile in which case we need to check
* the quota on the target destination.
*
* @param string $source source path
* @param string $destination destination path
*/
public function beforeMove($source, $destination) {
$sourceNode = $this->server->tree->getNodeForPath($source);
if (!$sourceNode instanceof FutureFile) {
return;
}
// get target node for proper path conversion
if ($this->server->tree->nodeExists($destination)) {
$destinationNode = $this->server->tree->getNodeForPath($destination);
$path = $destinationNode->getPath();
} else {
$parent = dirname($destination);
if ($parent === '.') {
$parent = '';
}
$parentNode = $this->server->tree->getNodeForPath($parent);
$path = $parentNode->getPath();
}
return $this->checkQuota($path, $sourceNode->getSize());
}
/**
* Check quota on the target destination before a copy.
*/
public function beforeCopy(string $sourcePath, string $destinationPath): bool {
$sourceNode = $this->server->tree->getNodeForPath($sourcePath);
if (!$sourceNode instanceof Node) {
return true;
}
// get target node for proper path conversion
if ($this->server->tree->nodeExists($destinationPath)) {
$destinationNode = $this->server->tree->getNodeForPath($destinationPath);
if (!$destinationNode instanceof Node) {
return true;
}
$path = $destinationNode->getPath();
} else {
$parent = dirname($destinationPath);
if ($parent === '.') {
$parent = '';
}
$parentNode = $this->server->tree->getNodeForPath($parent);
if (!$parentNode instanceof Node) {
return true;
}
$path = $parentNode->getPath();
}
return $this->checkQuota($path, $sourceNode->getSize());
}
/**
* This method is called before any HTTP method and validates there is enough free space to store the file
*
* @param string $path relative to the users home
* @param int|float|null $length
* @throws InsufficientStorage
* @return bool
*/
public function checkQuota($path, $length = null) {
if ($length === null) {
$length = $this->getLength();
}
if ($length) {
[$parentPath, $newName] = \Sabre\Uri\split($path);
if (is_null($parentPath)) {
$parentPath = '';
}
$req = $this->server->httpRequest;
// If chunked upload
if ($req->getHeader('OC-Chunked')) {
$info = \OC_FileChunking::decodeName($newName);
$chunkHandler = $this->getFileChunking($info);
// subtract the already uploaded size to see whether
// there is still enough space for the remaining chunks
$length -= $chunkHandler->getCurrentSize();
// use target file name for free space check in case of shared files
$path = rtrim($parentPath, '/') . '/' . $info['name'];
}
// Strip any duplicate slashes
$path = str_replace('//', '/', $path);
$freeSpace = $this->getFreeSpace($path);
if ($freeSpace >= 0 && $length > $freeSpace) {
if (isset($chunkHandler)) {
$chunkHandler->cleanup();
}
throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
}
}
return true;
}
public function getFileChunking($info) {
// FIXME: need a factory for better mocking support
return new \OC_FileChunking($info);
}
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;
}
/**
* @param string $uri
* @return mixed
* @throws ServiceUnavailable
*/
public function getFreeSpace($uri) {
try {
$freeSpace = $this->view->free_space(ltrim($uri, '/'));
return $freeSpace;
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($e->getMessage());
}
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use OCP\IRequest;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class RequestIdHeaderPlugin extends \Sabre\DAV\ServerPlugin {
/** @var IRequest */
private $request;
public function __construct(IRequest $request) {
$this->request = $request;
}
public function initialize(\Sabre\DAV\Server $server) {
$server->on('afterMethod:*', [$this, 'afterMethod']);
}
/**
* Add the request id as a header in the response
*
* @param RequestInterface $request request
* @param ResponseInterface $response response
*/
public function afterMethod(RequestInterface $request, ResponseInterface $response) {
$response->setHeader('X-Request-Id', $this->request->getId());
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author scolebrook <scolebrook@mac.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
/**
* Class \OCA\DAV\Connector\Sabre\Server
*
* This class overrides some methods from @see \Sabre\DAV\Server.
*
* @see \Sabre\DAV\Server
*/
class Server extends \Sabre\DAV\Server {
/** @var CachingTree $tree */
/**
* @see \Sabre\DAV\Server
*/
public function __construct($treeOrNode = null) {
parent::__construct($treeOrNode);
self::$exposeVersion = false;
$this->enablePropfindDepthInfinity = true;
}
}
@@ -0,0 +1,213 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\Mount\IMountManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ITagManager;
use OCP\IUserSession;
use OCP\SabrePluginEvent;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Plugin;
class ServerFactory {
private IConfig $config;
private LoggerInterface $logger;
private IDBConnection $databaseConnection;
private IUserSession $userSession;
private IMountManager $mountManager;
private ITagManager $tagManager;
private IRequest $request;
private IPreview $previewManager;
private IEventDispatcher $eventDispatcher;
private IL10N $l10n;
public function __construct(
IConfig $config,
LoggerInterface $logger,
IDBConnection $databaseConnection,
IUserSession $userSession,
IMountManager $mountManager,
ITagManager $tagManager,
IRequest $request,
IPreview $previewManager,
IEventDispatcher $eventDispatcher,
IL10N $l10n
) {
$this->config = $config;
$this->logger = $logger;
$this->databaseConnection = $databaseConnection;
$this->userSession = $userSession;
$this->mountManager = $mountManager;
$this->tagManager = $tagManager;
$this->request = $request;
$this->previewManager = $previewManager;
$this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n;
}
/**
* @param callable $viewCallBack callback that should return the view for the dav endpoint
*/
public function createServer(string $baseUri,
string $requestUri,
Plugin $authPlugin,
callable $viewCallBack): Server {
// Fire up server
$objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
$server = new \OCA\DAV\Connector\Sabre\Server($objectTree);
// Set URL explicitly due to reverse-proxy situations
$server->httpRequest->setUrl($requestUri);
$server->setBaseUri($baseUri);
// Load plugins
$server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin());
$server->addPlugin($authPlugin);
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
$server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
$server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class)));
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
// we do not provide locking we emulate it using a fake locking plugin.
if ($this->request->isUserAgent([
'/WebDAVFS/',
'/OneNote/',
'/Microsoft-WebDAV-MiniRedir/',
])) {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin());
}
if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) {
$server->addPlugin(new BrowserErrorPagePlugin());
}
// wait with registering these until auth is handled and the filesystem is setup
$server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack) {
// ensure the skeleton is copied
$userFolder = \OC::$server->getUserFolder();
/** @var \OC\Files\View $view */
$view = $viewCallBack($server);
if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) {
$rootInfo = $userFolder;
} else {
$rootInfo = $view->getFileInfo('');
}
// Create Nextcloud Dir
if ($rootInfo->getType() === 'dir') {
$root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree);
} else {
$root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
}
$objectTree->init($root, $view, $this->mountManager);
$server->addPlugin(
new \OCA\DAV\Connector\Sabre\FilesPlugin(
$objectTree,
$this->config,
$this->request,
$this->previewManager,
$this->userSession,
false,
!$this->config->getSystemValue('debug', false)
)
);
$server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin());
// Allow view-only plugin for webdav requests
$server->addPlugin(new ViewOnlyPlugin(
$userFolder,
));
if ($this->userSession->isLoggedIn()) {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(
$objectTree,
$this->userSession,
$userFolder,
\OC::$server->getShareManager()
));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin(
$objectTree,
$view,
\OC::$server->getSystemTagManager(),
\OC::$server->getSystemTagObjectMapper(),
\OC::$server->getTagManager(),
$this->userSession,
\OC::$server->getGroupManager(),
$userFolder,
\OC::$server->getAppManager()
));
// custom properties plugin must be the last one
$server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
new \OCA\DAV\DAV\CustomPropertiesBackend(
$objectTree,
$this->databaseConnection,
$this->userSession->getUser()
)
)
);
}
$server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin());
// Load dav plugins from apps
$event = new SabrePluginEvent($server);
$this->eventDispatcher->dispatchTyped($event);
$pluginManager = new PluginManager(
\OC::$server,
\OC::$server->getAppManager()
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$server->addPlugin($appPlugin);
}
}, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request
return $server;
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* ShareTypeList property
*
* This property contains multiple "share-type" elements, each containing a share type.
*/
class ShareTypeList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* Share types
*
* @var int[]
*/
private $shareTypes;
/**
* @param int[] $shareTypes
*/
public function __construct($shareTypes) {
$this->shareTypes = $shareTypes;
}
/**
* Returns the share types
*
* @return int[]
*/
public function getShareTypes() {
return $this->shareTypes;
}
/**
* The deserialize method is called during xml parsing.
*
* @param Reader $reader
* @return mixed
*/
public static function xmlDeserialize(Reader $reader) {
$shareTypes = [];
$tree = $reader->parseInnerTree();
if ($tree === null) {
return null;
}
foreach ($tree as $elem) {
if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}share-type') {
$shareTypes[] = (int)$elem['value'];
}
}
return new self($shareTypes);
}
/**
* The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
*/
public function xmlSerialize(Writer $writer) {
foreach ($this->shareTypes as $shareType) {
$writer->writeElement('{' . self::NS_OWNCLOUD . '}share-type', $shareType);
}
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Connector\Sabre;
use OCP\Share\IShare;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* This property contains multiple "sharee" elements, each containing a share sharee
*/
class ShareeList implements XmlSerializable {
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
/** @var IShare[] */
private $shares;
public function __construct(array $shares) {
$this->shares = $shares;
}
/**
* The xmlSerialize method is called during xml writing.
*
* @param Writer $writer
* @return void
*/
public function xmlSerialize(Writer $writer) {
foreach ($this->shares as $share) {
$writer->startElement('{' . self::NS_NEXTCLOUD . '}sharee');
$writer->writeElement('{' . self::NS_NEXTCLOUD . '}id', $share->getSharedWith());
$writer->writeElement('{' . self::NS_NEXTCLOUD . '}display-name', $share->getSharedWithDisplayName());
$writer->writeElement('{' . self::NS_NEXTCLOUD . '}type', $share->getShareType());
$writer->endElement();
}
}
}
@@ -0,0 +1,217 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Node as DavNode;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
/**
* Sabre Plugin to provide share-related properties
*/
class SharesPlugin extends \Sabre\DAV\ServerPlugin {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const SHARETYPES_PROPERTYNAME = '{http://owncloud.org/ns}share-types';
public const SHAREES_PROPERTYNAME = '{http://nextcloud.org/ns}sharees';
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
private IManager $shareManager;
private Tree $tree;
private string $userId;
private Folder $userFolder;
/** @var IShare[][] */
private array $cachedShares = [];
/** @var string[] */
private array $cachedFolders = [];
public function __construct(
Tree $tree,
IUserSession $userSession,
Folder $userFolder,
IManager $shareManager
) {
$this->tree = $tree;
$this->shareManager = $shareManager;
$this->userFolder = $userFolder;
$this->userId = $userSession->getUser()->getUID();
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @return void
*/
public function initialize(Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = ShareTypeList::class;
$server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME;
$server->protectedProperties[] = self::SHAREES_PROPERTYNAME;
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
}
/**
* @param Node $node
* @return IShare[]
*/
private function getShare(Node $node): array {
$result = [];
$requestedShareTypes = [
IShare::TYPE_USER,
IShare::TYPE_GROUP,
IShare::TYPE_LINK,
IShare::TYPE_REMOTE,
IShare::TYPE_EMAIL,
IShare::TYPE_ROOM,
IShare::TYPE_CIRCLE,
IShare::TYPE_DECK,
IShare::TYPE_SCIENCEMESH,
];
foreach ($requestedShareTypes as $requestedShareType) {
$shares = $this->shareManager->getSharesBy(
$this->userId,
$requestedShareType,
$node,
false,
-1
);
foreach ($shares as $share) {
$result[] = $share;
}
}
return $result;
}
/**
* @param Folder $node
* @return IShare[][]
*/
private function getSharesFolder(Folder $node): array {
return $this->shareManager->getSharesInFolder(
$this->userId,
$node,
true
);
}
/**
* @param DavNode $sabreNode
* @return IShare[]
*/
private function getShares(DavNode $sabreNode): array {
if (isset($this->cachedShares[$sabreNode->getId()])) {
$shares = $this->cachedShares[$sabreNode->getId()];
} else {
[$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
if ($parentPath === '') {
$parentPath = '/';
}
// if we already cached the folder this file is in we know there are no shares for this file
if (array_search($parentPath, $this->cachedFolders) === false) {
try {
$node = $sabreNode->getNode();
} catch (NotFoundException $e) {
return [];
}
$shares = $this->getShare($node);
$this->cachedShares[$sabreNode->getId()] = $shares;
} else {
return [];
}
}
return $shares;
}
/**
* Adds shares to propfind response
*
* @param PropFind $propFind propfind object
* @param \Sabre\DAV\INode $sabreNode sabre node
*/
public function handleGetProperties(
PropFind $propFind,
\Sabre\DAV\INode $sabreNode
) {
if (!($sabreNode instanceof DavNode)) {
return;
}
// need prefetch ?
if ($sabreNode instanceof Directory
&& $propFind->getDepth() !== 0
&& (
!is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) ||
!is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME))
)
) {
$folderNode = $sabreNode->getNode();
$this->cachedFolders[] = $sabreNode->getPath();
$childShares = $this->getSharesFolder($folderNode);
foreach ($childShares as $id => $shares) {
$this->cachedShares[$id] = $shares;
}
}
$propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode): ShareTypeList {
$shares = $this->getShares($sabreNode);
$shareTypes = array_unique(array_map(function (IShare $share) {
return $share->getShareType();
}, $shares));
return new ShareTypeList($shareTypes);
});
$propFind->handle(self::SHAREES_PROPERTYNAME, function () use ($sabreNode): ShareeList {
$shares = $this->getShares($sabreNode);
return new ShareeList($shares);
});
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* TagList property
*
* This property contains multiple "tag" elements, each containing a tag name.
*/
class TagList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* tags
*
* @var array
*/
private $tags;
/**
* @param array $tags
*/
public function __construct(array $tags) {
$this->tags = $tags;
}
/**
* Returns the tags
*
* @return array
*/
public function getTags() {
return $this->tags;
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statictly, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @param Reader $reader
* @return mixed
*/
public static function xmlDeserialize(Reader $reader) {
$tags = [];
$tree = $reader->parseInnerTree();
if ($tree === null) {
return null;
}
foreach ($tree as $elem) {
if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}tag') {
$tags[] = $elem['value'];
}
}
return new self($tags);
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializble should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*
* @param Writer $writer
* @return void
*/
public function xmlSerialize(Writer $writer) {
foreach ($this->tags as $tag) {
$writer->writeElement('{' . self::NS_OWNCLOUD . '}tag', $tag);
}
}
}
@@ -0,0 +1,299 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Sergio Bertolín <sbertolin@solidgear.es>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\Connector\Sabre;
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
class TagsPlugin extends \Sabre\DAV\ServerPlugin {
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
public const TAG_FAVORITE = '_$!<Favorite>!$_';
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @var \OCP\ITagManager
*/
private $tagManager;
/**
* @var \OCP\ITags
*/
private $tagger;
/**
* Array of file id to tags array
* The null value means the cache wasn't initialized.
*
* @var array
*/
private $cachedTags;
/**
* @var \Sabre\DAV\Tree
*/
private $tree;
/**
* @param \Sabre\DAV\Tree $tree tree
* @param \OCP\ITagManager $tagManager tag manager
*/
public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
$this->tree = $tree;
$this->tagManager = $tagManager;
$this->tagger = null;
$this->cachedTags = [];
}
/**
* This initializes the plugin.
*
* This function is called by \Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param \Sabre\DAV\Server $server
* @return void
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
}
/**
* Returns the tagger
*
* @return \OCP\ITags tagger
*/
private function getTagger() {
if (!$this->tagger) {
$this->tagger = $this->tagManager->load('files');
}
return $this->tagger;
}
/**
* Returns tags and favorites.
*
* @param integer $fileId file id
* @return array list($tags, $favorite) with $tags as tag array
* and $favorite is a boolean whether the file was favorited
*/
private function getTagsAndFav($fileId) {
$isFav = false;
$tags = $this->getTags($fileId);
if ($tags) {
$favPos = array_search(self::TAG_FAVORITE, $tags);
if ($favPos !== false) {
$isFav = true;
unset($tags[$favPos]);
}
}
return [$tags, $isFav];
}
/**
* Returns tags for the given file id
*
* @param integer $fileId file id
* @return array list of tags for that file
*/
private function getTags($fileId) {
if (isset($this->cachedTags[$fileId])) {
return $this->cachedTags[$fileId];
} else {
$tags = $this->getTagger()->getTagsForObjects([$fileId]);
if ($tags !== false) {
if (empty($tags)) {
return [];
}
return current($tags);
}
}
return null;
}
/**
* Updates the tags of the given file id
*
* @param int $fileId
* @param array $tags array of tag strings
*/
private function updateTags($fileId, $tags) {
$tagger = $this->getTagger();
$currentTags = $this->getTags($fileId);
$newTags = array_diff($tags, $currentTags);
foreach ($newTags as $tag) {
if ($tag === self::TAG_FAVORITE) {
continue;
}
$tagger->tagAs($fileId, $tag);
}
$deletedTags = array_diff($currentTags, $tags);
foreach ($deletedTags as $tag) {
if ($tag === self::TAG_FAVORITE) {
continue;
}
$tagger->unTag($fileId, $tag);
}
}
/**
* Adds tags and favorites properties to the response,
* if requested.
*
* @param PropFind $propFind
* @param \Sabre\DAV\INode $node
* @return void
*/
public function handleGetProperties(
PropFind $propFind,
\Sabre\DAV\INode $node
) {
if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
return;
}
// need prefetch ?
if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
&& $propFind->getDepth() !== 0
&& (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
|| !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
)) {
// note: pre-fetching only supported for depth <= 1
$folderContent = $node->getChildren();
$fileIds[] = (int)$node->getId();
foreach ($folderContent as $info) {
$fileIds[] = (int)$info->getId();
}
$tags = $this->getTagger()->getTagsForObjects($fileIds);
if ($tags === false) {
// the tags API returns false on error...
$tags = [];
}
$this->cachedTags = $this->cachedTags + $tags;
$emptyFileIds = array_diff($fileIds, array_keys($tags));
// also cache the ones that were not found
foreach ($emptyFileIds as $fileId) {
$this->cachedTags[$fileId] = [];
}
}
$isFav = null;
$propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
[$tags, $isFav] = $this->getTagsAndFav($node->getId());
return new TagList($tags);
});
$propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
if (is_null($isFav)) {
[, $isFav] = $this->getTagsAndFav($node->getId());
}
if ($isFav) {
return 1;
} else {
return 0;
}
});
}
/**
* Updates tags and favorites properties, if applicable.
*
* @param string $path
* @param PropPatch $propPatch
*
* @return void
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
return;
}
$propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
$this->updateTags($node->getId(), $tagList->getTags());
return true;
});
$propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
if ((int)$favState === 1 || $favState === 'true') {
$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
} else {
$this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
}
if (is_null($favState)) {
// confirm deletion
return 204;
}
return 200;
});
}
}