karadav/lib/KD2/WebDAV/NextCloud.php

862 lines
23 KiB
PHP
Raw Normal View History

2022-10-24 17:13:07 +00:00
<?php
namespace KD2\WebDAV;
abstract class NextCloud
{
2023-09-10 23:15:14 +00:00
const NC_VERSION = '99.0.0';
2023-09-10 23:03:49 +00:00
2022-10-24 17:13:07 +00:00
/**
* File permissions for NextCloud clients
* https://doc.owncloud.com/desktop/next/appendices/architecture.html#server-side-permissions
2022-10-24 17:13:07 +00:00
*
* R = Shareable
* S = Shared
* M = Mounted
* D = Delete
* G = Readable
* N = Can rename
* V = Can move
* W = Can Write (Update)
* C = Can create file in folder
* K = Can create folder (mkdir)
2022-10-24 17:13:07 +00:00
*/
const PERM_READ = 'G';
const PERM_SHARE = 'R';
const PERM_SHARED = 'S';
const PERM_MOUNTED = 'M';
const PERM_DELETE = 'D';
const PERM_RENAME = 'N';
const PERM_MOVE = 'V';
2022-10-24 17:13:07 +00:00
const PERM_WRITE = 'W';
const PERM_CREATE = 'C';
const PERM_MKDIR = 'K';
// NextCloud Android is buggy and doesn't differentiate between C and K, you must use CK
// or nothing (eg. C or K alone is not recognized, same with KC)
const PERM_CREATE_FILES_DIRS = 'CK';
2022-10-24 17:13:07 +00:00
const NC_NAMESPACE = 'http://nextcloud.org/ns';
const OC_NAMESPACE = 'http://owncloud.org/ns';
const PROP_OC_ID = self::OC_NAMESPACE . ':id';
const PROP_OC_SIZE = self::OC_NAMESPACE . ':size';
const PROP_OC_DOWNLOADURL = self::OC_NAMESPACE . ':downloadURL';
const PROP_OC_PERMISSIONS = self::OC_NAMESPACE . ':permissions';
2023-02-14 12:26:40 +00:00
const PROP_OC_CHECKSUMS = self::OC_NAMESPACE . ':checksums';
2022-10-24 17:13:07 +00:00
// Preview
const PROP_NC_HAS_PREVIEW = self::NC_NAMESPACE . ':has-preview';
// If you supply Markdown content in this property
// it will be displayed at the top of a directory listing
// in Android app
const PROP_NC_RICH_WORKSPACE = self::NC_NAMESPACE . ':rich-workspace';
// Useless?
const PROP_OC_SHARETYPES = self::OC_NAMESPACE . ':share-types';
const PROP_NC_NOTE = self::NC_NAMESPACE . ':note';
const PROP_NC_IS_ENCRYPTED = self::NC_NAMESPACE . ':is-encrypted';
2022-10-25 23:04:38 +00:00
const PROP_NC_DDC = self::NC_NAMESPACE . ':dDC';
2022-10-24 17:13:07 +00:00
const NC_PROPERTIES = [
self::PROP_OC_ID,
self::PROP_OC_SIZE,
self::PROP_OC_DOWNLOADURL,
self::PROP_OC_PERMISSIONS,
2023-02-14 12:26:40 +00:00
self::PROP_OC_CHECKSUMS,
2022-10-24 17:13:07 +00:00
self::PROP_OC_SHARETYPES,
self::PROP_NC_HAS_PREVIEW,
self::PROP_NC_NOTE,
self::PROP_NC_IS_ENCRYPTED,
2022-10-25 23:04:38 +00:00
self::PROP_NC_DDC,
2022-10-24 17:13:07 +00:00
];
protected string $prefix = '';
2022-10-24 17:13:07 +00:00
protected string $root_url;
protected Server $server;
protected AbstractStorage $storage;
/**
* Handle your authentication
* you should handle real user login/password as well as app-specific passwords here
* (in a second condition) to cover all cases
*/
abstract public function auth(?string $login, ?string $password): bool;
/* This is a simple example:
session_start();
if (!empty($_SESSION['user'])) {
return true;
}
if ($login != 'admin' && $password != 'abcd') {
return false;
}
$_SESSION['user'] = 'admin';
return true;
*/
/**
* Return username (a-z_0-9) of currently logged-in user
*/
abstract public function getUserName(): ?string;
/*
return $_SESSION['user'] ?? null;
*/
/**
* Set username of currently logged-in user
* Return FALSE if user is invalid
*/
abstract public function setUserName(string $login): bool;
/*
$_SESSION['user'] = $login;
return true;
*/
/**
* Return quota for currently loggged-in user
* @return array ['free' => 123, 'used' => 123, 'total' => 246]
*/
abstract public function getUserQuota(): array;
/*
return ['free' => 123, 'used' => 123, 'total' => 246];
*/
/**
* Return a unique token for v2 login flow
*/
abstract public function generateToken(): string;
/*
return sha1(random_bytes(16));
*/
/**
* Validate the provided token to get a session, returns either NULL or a user login and app password
* @return array ['login' => ..., 'password' => ...]
*/
abstract public function validateToken(string $token): ?array;
/*
$session = $db->get('SELECT login, password FROM sessions WHERE token = ?;', $token);
if (!$session) {
return null;
}
// Make sure to have a single-use token
$db->query('UPDATE sessions SET token = NULL WHERE token = ?;', $token);
return (array)$session;
*/
abstract public function getLoginURL(?string $token): string;
/*
if ($token) {
return $this->root_url . '/admin/login.php?nc_token=' . $token;
}
else {
return $this->root_url . '/admin/login.php?nc_redirect=true';
}
*/
/**
* Direct download API
* Return a unique secret to authentify a direct URL request (for direct API)
* meaning a third party (eg. local user app) can access the file without auth
* @param string $uri
* @param string $login User name
* @return string a secret string (eg. a hash)
*/
abstract public function getDirectDownloadSecret(string $uri, string $login): string;
/**
* Chunked upload API.
* You should manage automatic removal of incomplete uploads after around 24 hours.
* @param string $login Current user name
* @return string Path to temporary storage for user
*/
abstract public function storeChunk(string $login, string $name, string $part, $pointer): void;
abstract public function deleteChunks(string $login, string $name): void;
abstract public function assembleChunks(string $login, string $name, string $target, ?int $mtime): array;
2023-01-28 02:11:31 +00:00
abstract public function listChunks(string $login, string $name): array;
/**
* Thumbnail API
* @param string $uri URI path to the file we want a thumbnail for
* @param int $width
* @param int $height
* @param bool $crop TRUE if a cropped image is desired
* @param bool $preview TRUE if the thumbnail is for preview (in NextCloud Android, see below)
*/
public function serveThumbnail(string $uri, int $width, int $height, bool $crop = false, bool $preview = false): void
{
if (!preg_match('/\.(?:jpe?g|gif|png|webp)$/', $uri)) {
http_response_code(404);
return;
}
// We don't support thumbnails, but you are free to generate cropped thumbnails and send them to the HTTP client
if (!$preview) {
http_response_code(404);
return;
}
// On Android, the app is annoying and asks to download the image
// every time ("no resized image available") if no preview is available.
// So to avoid that you can just redirect to the file if it's not too large
// But you are free to extend this method and resize the image on the fly instead.
else {
$size = $this->server->getStorage()->properties($uri, ['DAV::getcontentlength'], 0);
$size = count($size) ? current($size) : null;
if ($size > 1024*1024 || !$size) {
http_response_code(404);
return;
}
$url = '/remote.php/dav/files/' . $uri;
$this->server->log('=> Preview: redirect to %s', $url);
header('Location: ' . $url);
}
}
2022-10-24 17:13:07 +00:00
// END OF ABSTRACT METHODS
/**
* List of routes
* Order of array elements is important!
*/
const ROUTES = [
// Chunked API
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/chunking.html
'remote.php/webdav/uploads/' => 'chunked',
'remote.php/dav/uploads/' => 'chunked',
2023-01-28 02:11:31 +00:00
// There's just 3 or 4 different endpoints for avatars, this is ridiculous
'remote.php/dav/avatars/' => 'avatar',
2022-10-24 17:13:07 +00:00
// Main routes
'remote.php/webdav/' => 'webdav', // desktop client
'remote.php/dav' => 'webdav', // android client
// Login v1, for Android app
'index.php/login/flow' => 'login_v1',
// Login v2, for desktop app
'index.php/login/v2/poll' => 'poll',
'index.php/login/v2' => 'login_v2',
// Other API endpoints
'index.php/core/preview.png' => 'preview',
'index.php/apps/files/api/v1/thumbnail/' => 'thumbnail',
'ocs/v2.php/apps/text/workspace/direct' => 'workspace_edit',
'ocs/v2.php/core/apppassword' => 'delete_app_password',
'status.php' => 'status',
'ocs/v1.php/cloud/capabilities' => 'capabilities',
'ocs/v2.php/cloud/capabilities' => 'capabilities',
'ocs/v2.php/cloud/user' => 'user',
'ocs/v1.php/cloud/user' => 'user',
'ocs/v1.php/config' => 'config',
'ocs/v2.php/apps/files_sharing/api/v1/shares' => 'shares',
'ocs/v2.php/apps/user_status' => 'empty',
2022-10-24 17:13:07 +00:00
'ocs/v2.php/core/navigation/apps' => 'empty',
'index.php/avatar' => 'avatar',
2022-10-24 17:13:07 +00:00
'ocs/v2.php/apps/dav/api/v1/direct' => 'direct_url',
'remote.php/direct/' => 'direct',
2023-01-28 02:11:31 +00:00
'avatars/' => 'avatar',
2022-10-24 17:13:07 +00:00
];
const AUTH_REDIRECT_URL = 'nc://login/server:%s&user:%s&password:%s';
2023-01-28 02:11:31 +00:00
// *Every* different NextCloud/ownCloud app uses a different root path
// this is ridiculous.
// NC Desktop: /remote.php/dav/files/user// (note the double slash at the end)
// NC iOS: /remote.php/dav/files/user (note the missing end slash)
// ownCloud Android: /remote.php/dav/files/ (note the missing user part)
// -> only at first, then it queries the correct path, WTF
// See also https://github.com/nextcloud/server/issues/25867
const WEBDAV_BASE_REGEXP = '~^.*remote\.php/(?:webdav/|dav/files/(?:(?:(?!/).)+(?:/+|$)|/*$))~';
2022-10-24 17:13:07 +00:00
public function setRootURL(string $url)
{
$this->root_url = $url;
}
public function setServer(Server $server)
{
$this->server = $server;
$this->storage = $server->getStorage();
}
/**
* Handle NextCloud specific routes
*
* @param null|string If left NULL, then REQUEST_URI will be used
* @return bool Will return TRUE if no NextCloud route was requested.
*/
public function route(?string $uri = null): bool
{
if (null === $uri) {
$uri = $_SERVER['REQUEST_URI'] ?? '/';
}
2023-01-28 02:11:31 +00:00
$uri = parse_url($uri, PHP_URL_PATH);
2022-10-24 17:13:07 +00:00
$uri = ltrim($uri, '/');
$uri = rawurldecode($uri);
$route = array_filter(self::ROUTES, fn($k) => 0 === strpos($uri, $k), ARRAY_FILTER_USE_KEY);
if (count($route) < 1) {
return false;
}
$route = current($route);
$method = $_SERVER['REQUEST_METHOD'] ?? null;
$this->server->log('NC <= %s %s => routed to: %s', $method, $uri, $route);
try {
$v = $this->{'nc_' . $route}($uri);
}
catch (Exception $e) {
$this->server->log('NC => %d - %s', $e->getCode(), $e->getMessage());
http_response_code($e->getCode());
2022-11-14 00:53:25 +00:00
if ($route == 'direct') {
// Do not return any error message for the direct API endpoint.
// If you return anything, the client will consider it is part of the file
// and will generate a corrupted file!
// so if you return a 20-byte long error message, the client
// will do a normal GET on the regular URL, but with 'Range: bytes=20-'!
// see https://github.com/nextcloud/desktop/issues/5170
header('X-Error: ' . $e->getMessage());
return true;
}
2022-10-24 17:13:07 +00:00
echo json_encode(['error' => $e->getMessage()]);
return true;
}
// This route is XML only
if ($route == 'shares') {
http_response_code(200);
header('Content-Type: text/xml; charset=utf-8', true);
echo '<?xml version="1.0"?>' . $this->xml($v);
}
elseif (is_array($v)) {
http_response_code(200);
header('Content-Type: application/json', true);
$json = json_encode($v, JSON_PRETTY_PRINT);
echo $json;
$this->server->log("NC => Body:\n%s", $json);
2022-10-24 17:13:07 +00:00
}
2023-01-28 02:11:31 +00:00
$this->server->log('NC Sent response: %d', http_response_code());
2022-10-24 17:13:07 +00:00
return true;
}
protected function xml(array $array): string
{
$out = '';
foreach ($array as $key => $v) {
$out .= '<' . $key .'>';
if (is_array($v)) {
$out .= $this->xml($v);
}
else {
$out .= htmlspecialchars((string) $v, ENT_XML1);
}
$out .= '</' . $key .'>';
}
return $out;
}
protected function requireAuth(): void
{
if (!$this->auth($_SERVER['PHP_AUTH_USER'] ?? null, $_SERVER['PHP_AUTH_PW'] ?? null)) {
header('WWW-Authenticate: Basic realm="Please login"');
throw new Exception('Please login to access this resource', 401);
}
}
public function nc_webdav(string $uri): void
{
$this->requireAuth();
2023-01-28 02:11:31 +00:00
$method = $_SERVER['REQUEST_METHOD'] ?? '';
// ownCloud-Android is using a different preview API
// remote.php/dav/files/user/name.jpg?x=224&y=224&c=&preview=1
if (!empty($_GET['preview'])) {
$x = (int) $_GET['x'] ?? 0;
$y = (int) $_GET['y'] ?? 0;
$this->serveThumbnail($uri, $x, $y, $x == $y);
return;
}
$base_uri = null;
2022-10-24 17:13:07 +00:00
// Find out which route we are using and replace URI
foreach (self::ROUTES as $route => $method) {
if ($method != 'webdav') {
continue;
}
if (0 === strpos($uri, $route)) {
$base_uri = rtrim($route, '/') . '/';
break;
}
}
if (!$base_uri) {
throw new Exception('Invalid WebDAV URL', 404);
}
2023-01-28 02:11:31 +00:00
if (preg_match(self::WEBDAV_BASE_REGEXP, $uri, $match)) {
$base_uri = rtrim($match[0], '/') . '/';
2022-10-24 17:13:07 +00:00
}
$this->server->prefix = $this->prefix;
2022-10-24 17:13:07 +00:00
$this->server->setBaseURI($base_uri);
$this->server->route($uri);
}
public function nc_status(): array
{
2023-01-28 02:11:31 +00:00
if (stristr($_SERVER['HTTP_USER_AGENT'], 'owncloud')) {
$name = 'ownCloud';
$version = '10.11.0';
}
else {
$name = 'NextCloud';
2023-09-10 23:03:49 +00:00
$version = self::NC_VERSION;
2023-01-28 02:11:31 +00:00
}
2022-10-24 17:13:07 +00:00
return [
'installed' => true,
'maintenance' => false,
'needsDbUpgrade' => false,
2023-01-28 02:11:31 +00:00
'version' => $version,
'versionstring' => $version,
2022-10-24 17:13:07 +00:00
'edition' => '',
2023-01-28 02:11:31 +00:00
'productname' => $name,
2022-10-24 17:13:07 +00:00
'extendedSupport' => false,
];
}
public function nc_login_v2(): array
{
$method = $_SERVER['REQUEST_METHOD'] ?? null;
if ($method != 'POST') {
throw new Exception('Invalid request method', 405);
}
$token = $this->generateToken();
$endpoint = sprintf('%s%s', $this->root_url, array_search('poll', self::ROUTES));
return [
'poll' => compact('token', 'endpoint'),
'login' => $this->getLoginURL($token),
];
}
public function nc_poll(): array
{
$method = $_SERVER['REQUEST_METHOD'] ?? null;
if ($method != 'POST') {
throw new Exception('Invalid request method', 405);
}
if (empty($_POST['token']) || !ctype_alnum($_POST['token'])) {
throw new Exception('Invalid token', 400);
}
$session = $this->validateToken($_POST['token']);
if (!$session) {
throw new Exception('No token yet', 404);
}
return [
'server' => $this->root_url,
'loginName' => $session['login'],
'appPassword' => $session['password'],
];
}
public function nc_capabilities()
{
2023-09-10 23:03:49 +00:00
$v = explode('.', self::NC_VERSION);
2022-10-24 17:13:07 +00:00
return $this->nc_ocs([
'version' => [
2023-09-10 23:03:49 +00:00
'major' => (int)$v[0],
'minor' => (int)$v[1],
'micro' => (int)$v[1],
'string' => self::NC_VERSION,
2022-10-24 17:13:07 +00:00
'edition' => '',
'extendedSupport' => false,
],
'capabilities' => [
'core' => [
'webdav-root' => array_search('webdav', self::ROUTES),
'pollinterval' => 60,
'bruteforce' => ['delay' => 0],
],
'dav' => [
// NG chunking: https://github.com/cernbox/smashbox/blob/master/protocol/chunking.md
// "1.0" means "NG" actually... lol: https://github.com/owncloud/client/issues/7862#issuecomment-717953394
"chunking" => "1.0",
],
'files' => [
// old v1 chunking: https://github.com/cernbox/smashbox/blob/master/protocol/protocol.md#chunked-file-upload
// We don't support it, BUT it is required for OwnCloud client, see
// https://github.com/owncloud/client/blob/24ca9615f6e8ea765f6c25fb4e009b1acc262a2d/src/libsync/capabilities.cpp#L166
'bigfilechunking' => true,
'comments' => false,
'undelete' => false,
'versioning' => false,
],
'files_sharing' => [
'api_enabled' => false,
'group_sharing' => false,
'resharing' => false,
'sharebymail' => ['enabled' => false],
],
'user' => [
'expire_date' => ['enabled' => false],
'send_mail' => false,
],
'public' => [
'enabled' => false,
'expire_date' => ['enabled' => false],
'multiple_links' => false,
'send_mail' => false,
'upload' => false,
'upload_files_drop' => false,
],
2023-02-14 12:26:40 +00:00
'checksums' => [
'supportedTypes' => ['SHA1', 'MD5'],
'preferredUploadType' => 'MD5',
],
2022-10-24 17:13:07 +00:00
],
]);
}
public function nc_login_v1(): void
{
http_response_code(303);
header('Location: ' . $this->getLoginURL(null));
}
public function nc_user(): array
{
$this->requireAuth();
$quota = $this->getUserQuota();
$user = $this->getUserName() ?? 'null';
return $this->nc_ocs([
2023-01-28 02:11:31 +00:00
'id' => sha1($user),
2022-10-24 17:13:07 +00:00
'enabled' => true,
'email' => null,
2023-01-28 02:11:31 +00:00
'storageLocation' => '/secret/whoknows/' . sha1($user),
2022-10-24 17:13:07 +00:00
'role' => '',
'display-name' => $user,
'quota' => [
'quota' => -3, // fixed value
'relative' => 0, // fixed value
'free' => $quota['free'] ?? 200000000,
'total' => $quota['total'] ?? 200000000,
'used' => $quota['used'] ?? 0,
],
]);
}
public function nc_shares(): array
{
return $this->nc_ocs([]);
}
protected function nc_empty(): array
{
return $this->nc_ocs([]);
}
2023-01-28 02:11:31 +00:00
protected function nc_avatar(): void
{
throw new Exception('Not implemented', 404);
}
2022-10-24 17:13:07 +00:00
protected function nc_config(): array
{
return $this->nc_ocs([
'contact' => '',
'host' => $_SERVER['SERVER_NAME'] ?? '',
'ssl' => !empty($_SERVER['HTTPS']) || $_SERVER['SERVER_PORT'] == 443,
'version' => '1.7',
'website' => 'Nextcloud',
]);
}
2023-06-11 21:43:23 +00:00
public function getDirectDownloadURL(string $uri, string $user)
2022-10-24 17:13:07 +00:00
{
$uri = trim($uri, '/');
$expire = intval((time() - strtotime('2022-09-01'))/3600) + 8; // 8 hours
$hash = Server::hmac([$user, $expire, $uri], $this->getDirectDownloadSecret($uri, $user));
$hash = $expire . ':' . $hash;
2022-10-24 17:13:07 +00:00
$uri = rawurlencode($uri);
$uri = str_replace('%2F', '/', $uri);
return sprintf('%s%s/%s/%s?h=%s', $this->root_url, trim(array_search('direct', self::ROUTES), '/'), $user, $uri, $hash);
}
protected function nc_direct_url(): array
{
$method = $_SERVER['REQUEST_METHOD'] ?? null;
if ($method != 'POST') {
throw new Exception('Invalid request method', 405);
}
$this->requireAuth();
if (empty($_POST['fileId'])) {
throw new Exception('Missing fileId', 400);
}
2023-06-11 21:43:23 +00:00
$uri = gzuncompress(decbin($_POST['fileId']));
2022-10-24 17:13:07 +00:00
if (!$uri) {
throw new Exception('Invalid fileId', 404);
}
$user = strtok($uri, ':');
$uri = strtok('');
if (!$this->storage->exists($uri)) {
throw new Exception('Invalid fileId', 404);
}
2023-06-11 21:43:23 +00:00
$url = $this->getDirectDownloadURL($uri, $user);
2022-10-24 17:13:07 +00:00
$this->server->log('NextCloud Direct Download URL is: %s', $url);
return self::nc_ocs(compact('url'));
}
protected function nc_direct(string $uri): void
{
$method = $_SERVER['REQUEST_METHOD'] ?? null;
if ($method != 'GET') {
throw new Exception('Invalid request method', 405);
}
if (empty($_GET['h'])) {
throw new Exception('Missing hash', 400);
}
$uri = substr(trim($uri, '/'), strlen(trim(array_search('direct', self::ROUTES), '/')));
$user = strtok($uri, '/');
$uri = trim(strtok(''), '/');
if (!$user || !$uri) {
throw new Exception('Invalid URI', 400);
}
2022-10-29 19:41:48 +00:00
if (false !== strpos($uri, '..')) {
throw new Exception(sprintf('Invalid URI: "%s"', $uri), 403);
}
2022-10-29 19:31:34 +00:00
$expire = (int) strtok($_GET['h'], ':');
2022-10-24 17:13:07 +00:00
$hash = strtok('');
$expire_seconds = $expire * 3600 + strtotime('2022-09-01');
// Link has expired
if ($expire_seconds < time()) {
throw new Exception('Link has expired', 401);
}
$verify = Server::hmac([$user, $expire, $uri], $this->getDirectDownloadSecret($uri, $user));
2022-10-24 17:13:07 +00:00
// Check if the provided hash is correct
if (!hash_equals($verify, $hash)) {
throw new Exception('Link hash is invalid', 401);
}
if (!$this->setUserName($user)) {
throw new Exception('Invalid user', 404);
}
$this->server->log('Access via NextCloud direct download API');
$this->server->setBaseURI('/');
$this->server->original_uri = $uri;
$this->server->http_get($uri);
}
protected function nc_ocs(array $data = []): array
{
return ['ocs' => [
'meta' => ['status' => 'ok', 'statuscode' => 200, 'message' => 'OK'],
'data' => $data,
]];
}
/**
* File preview, large
* @see https://help.nextcloud.com/t/getting-image-preview-with-android-library-or-via-webdav/75743
*/
protected function nc_preview(string $uri): void
{
$width = $_GET['x'] ?? null;
$height = $_GET['y'] ?? null;
$crop = !($_GET['a'] ?? null);
2023-01-28 02:11:31 +00:00
$url = str_replace('%2F', '/', rawurldecode($_GET['file'] ?? ''));
2022-10-24 17:13:07 +00:00
$url = ltrim($url, '/');
2023-01-28 02:11:31 +00:00
$this->serveThumbnail($url, (int) $width, (int) $height, $crop, true);
2022-10-24 17:13:07 +00:00
}
protected function nc_thumbnail(string $uri): void
{
// Remove "/index.php/apps/files/api/v1/thumbnail/"
$uri = str_replace(array_search('thumbnail', self::ROUTES), '', $uri);
$uri = trim($uri, '/');
list($width, $height, $uri) = array_pad(explode('/', $uri, 3), 3, null);
2023-01-28 02:11:31 +00:00
$this->serveThumbnail($uri, (int)$width, (int)$height, $width == $height);
2022-10-24 17:13:07 +00:00
}
/**
* This is triggered when a user clicks the edit button for the README.md file of a directory
* (feature is called "rich workspace direct editing")
* A webview will be opened to the 'url' parameter returned.
*/
protected function nc_workspace_edit(string $uri): ?array
{
http_response_code(501);
return null;
$path = json_decode(file_get_contents('php://input'))->path ?? null;
return self::nc_ocs(['url' => '...']);
}
/**
2022-10-25 23:04:38 +00:00
* Called when removing an account from Android app
2022-10-24 17:13:07 +00:00
*/
2022-10-25 23:04:38 +00:00
protected function nc_delete_app_password(): void
2022-10-24 17:13:07 +00:00
{
$method = $_SERVER['REQUEST_METHOD'] ?? null;
if ($method == 'DELETE') {
// $_SERVER['PHP_AUTH_USER'] / $_SERVER['PHP_AUTH_PW']
}
}
protected function nc_chunked(string $uri): void
{
$this->requireAuth();
$user = $this->getUserName();
$r = '!^remote\.php/dav/uploads/([^/]+)/([\w\d_-]+)(?:/([\w\d_-]+))?(?:/\.file)?$!';
if (!preg_match($r, $uri, $match)) {
throw new Exception('Invalid URL for chunk upload', 400);
}
$method = $_SERVER['REQUEST_METHOD'] ?? null;
$login = $match[1] ?? null;
$dir = $match[2] ?? null;
$part = $match[3] ?? null;
if ($method == 'MKCOL') {
http_response_code(201);
}
elseif ($method == 'PUT') {
$this->server->log('Storing chunk: %s/%s/%s', $login, $dir, $part);
$this->storeChunk($login, $dir, $part, fopen('php://input', 'rb'));
http_response_code(201);
}
elseif ($method == 'MOVE') {
$dest = $_SERVER['HTTP_DESTINATION'];
2023-01-28 02:11:31 +00:00
$dest = preg_replace(self::WEBDAV_BASE_REGEXP, '', $dest);
2022-10-24 17:13:07 +00:00
$dest = trim(rawurldecode($dest), '/');
if (false !== strpos($dest, '..') || false !== strpos($dest, '//')) {
throw new Exception('Invalid destination');
}
$this->server->log('Assembling chunks to: %s', $dest);
$mtime = (int) $_SERVER['HTTP_X_OC_MTIME'] ?: null;
header('X-OC-MTime: accepted');
2023-09-10 22:23:27 +00:00
$props = $this->storage->properties($dest, [self::PROP_OC_ID], 0);
if (count($props)) {
header('OC-FileId: ' . current($props));
}
2022-10-24 17:13:07 +00:00
$return = $this->assembleChunks($login, $dir, $dest, $mtime);
if (!empty($return['etag'])) {
header(sprintf('ETag: "%s"', $return['etag']));
header(sprintf('OC-ETag: "%s"', $return['etag']));
}
2023-01-28 02:11:31 +00:00
$this->server->log("=> Chunks assembled to: %s", $dest);
2022-10-24 17:13:07 +00:00
if (!empty($return['created'])) {
http_response_code(201);
}
else {
http_response_code(204);
}
}
elseif ($method == 'DELETE' && !$part) {
$this->deleteChunks($login, $dir);
2023-01-28 02:11:31 +00:00
$this->server->log("=> Deleted chunks");
}
elseif ($method == 'PROPFIND') {
header('HTTP/1.1 207 Multi-Status', true);
$out = '<?xml version="1.0" encoding="utf-8"?>' . PHP_EOL;
$out .= '<d:multistatus xmlns:d="DAV:">' . PHP_EOL;
foreach ($this->listChunks($login, $dir) as $chunk) {
$out .= '<d:response>' . PHP_EOL;
$chunk = '/' . $uri . '/' . $chunk;
$out .= sprintf('<d:href>%s</d:href>', htmlspecialchars($chunk, ENT_XML1)) . PHP_EOL;
$out .= '<d:propstat><d:prop><d:getcontenttype>application/octet-stream</d:getcontenttype><d:resoucetype/></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>' . PHP_EOL;
$out .= '</d:response>' . PHP_EOL;
}
$out .= '</d:multistatus>';
echo $out;
$this->server->log("=> Body:\n%s", $out);
2022-10-24 17:13:07 +00:00
}
else {
throw new Exception('Invalid method for chunked upload', 400);
}
}
}