"use strict";
const path = require("path");
const paths = require("@nextcloud/paths");
const logger$1 = require("@nextcloud/logger");
const auth = require("@nextcloud/auth");
const router = require("@nextcloud/router");
const cancelablePromise = require("cancelable-promise");
const webdav = require("webdav");
const _public = require("@nextcloud/sharing/public");
const logger = logger$1.getLoggerBuilder().setApp("@nextcloud/files").detectUser().build();
var Permission = /* @__PURE__ */ ((Permission2) => {
Permission2[Permission2["NONE"] = 0] = "NONE";
Permission2[Permission2["CREATE"] = 4] = "CREATE";
Permission2[Permission2["READ"] = 1] = "READ";
Permission2[Permission2["UPDATE"] = 2] = "UPDATE";
Permission2[Permission2["DELETE"] = 8] = "DELETE";
Permission2[Permission2["SHARE"] = 16] = "SHARE";
Permission2[Permission2["ALL"] = 31] = "ALL";
return Permission2;
})(Permission || {});
var FileType = /* @__PURE__ */ ((FileType2) => {
FileType2["Folder"] = "folder";
FileType2["File"] = "file";
return FileType2;
})(FileType || {});
const isDavResource = function(source, davService) {
return source.match(davService) !== null;
};
const validateData = (data, davService) => {
if (data.id && typeof data.id !== "number") {
throw new Error("Invalid id type of value");
}
if (!data.source) {
throw new Error("Missing mandatory source");
}
try {
new URL(data.source);
} catch (e) {
throw new Error("Invalid source format, source must be a valid URL");
}
if (!data.source.startsWith("http")) {
throw new Error("Invalid source format, only http(s) is supported");
}
if (data.displayname && typeof data.displayname !== "string") {
throw new Error("Invalid displayname type");
}
if (data.mtime && !(data.mtime instanceof Date)) {
throw new Error("Invalid mtime type");
}
if (data.crtime && !(data.crtime instanceof Date)) {
throw new Error("Invalid crtime type");
}
if (!data.mime || typeof data.mime !== "string" || !data.mime.match(/^[-\w.]+\/[-+\w.]+$/gi)) {
throw new Error("Missing or invalid mandatory mime");
}
if ("size" in data && typeof data.size !== "number" && data.size !== void 0) {
throw new Error("Invalid size type");
}
if ("permissions" in data && data.permissions !== void 0 && !(typeof data.permissions === "number" && data.permissions >= Permission.NONE && data.permissions <= Permission.ALL)) {
throw new Error("Invalid permissions");
}
if (data.owner && data.owner !== null && typeof data.owner !== "string") {
throw new Error("Invalid owner type");
}
if (data.attributes && typeof data.attributes !== "object") {
throw new Error("Invalid attributes type");
}
if (data.root && typeof data.root !== "string") {
throw new Error("Invalid root type");
}
if (data.root && !data.root.startsWith("/")) {
throw new Error("Root must start with a leading slash");
}
if (data.root && !data.source.includes(data.root)) {
throw new Error("Root must be part of the source");
}
if (data.root && isDavResource(data.source, davService)) {
const service = data.source.match(davService)[0];
if (!data.source.includes(path.join(service, data.root))) {
throw new Error("The root must be relative to the service. e.g /files/emma");
}
}
if (data.status && !Object.values(NodeStatus).includes(data.status)) {
throw new Error("Status must be a valid NodeStatus");
}
};
var NodeStatus = /* @__PURE__ */ ((NodeStatus2) => {
NodeStatus2["NEW"] = "new";
NodeStatus2["FAILED"] = "failed";
NodeStatus2["LOADING"] = "loading";
NodeStatus2["LOCKED"] = "locked";
return NodeStatus2;
})(NodeStatus || {});
class Node {
_data;
_attributes;
_knownDavService = /(remote|public)\.php\/(web)?dav/i;
readonlyAttributes = Object.entries(Object.getOwnPropertyDescriptors(Node.prototype)).filter((e) => typeof e[1].get === "function" && e[0] !== "__proto__").map((e) => e[0]);
handler = {
set: (target, prop, value) => {
if (this.readonlyAttributes.includes(prop)) {
return false;
}
return Reflect.set(target, prop, value);
},
deleteProperty: (target, prop) => {
if (this.readonlyAttributes.includes(prop)) {
return false;
}
return Reflect.deleteProperty(target, prop);
},
// TODO: This is deprecated and only needed for files v3
get: (target, prop, receiver) => {
if (this.readonlyAttributes.includes(prop)) {
logger.warn(`Accessing "Node.attributes.${prop}" is deprecated, access it directly on the Node instance.`);
return Reflect.get(this, prop);
}
return Reflect.get(target, prop, receiver);
}
};
constructor(data, davService) {
validateData(data, davService || this._knownDavService);
this._data = {
// TODO: Remove with next major release, this is just for compatibility
displayname: data.attributes?.displayname,
...data,
attributes: {}
};
this._attributes = new Proxy(this._data.attributes, this.handler);
this.update(data.attributes ?? {});
if (davService) {
this._knownDavService = davService;
}
}
/**
* Get the source url to this object
* There is no setter as the source is not meant to be changed manually.
* You can use the rename or move method to change the source.
*/
get source() {
return this._data.source.replace(/\/$/i, "");
}
/**
* Get the encoded source url to this object for requests purposes
*/
get encodedSource() {
const { origin } = new URL(this.source);
return origin + paths.encodePath(this.source.slice(origin.length));
}
/**
* Get this object name
* There is no setter as the source is not meant to be changed manually.
* You can use the rename or move method to change the source.
*/
get basename() {
return path.basename(this.source);
}
/**
* The nodes displayname
* By default the display name and the `basename` are identical,
* but it is possible to have a different name. This happens
* on the files app for example for shared folders.
*/
get displayname() {
return this._data.displayname || this.basename;
}
/**
* Set the displayname
*/
set displayname(displayname) {
this._data.displayname = displayname;
}
/**
* Get this object's extension
* There is no setter as the source is not meant to be changed manually.
* You can use the rename or move method to change the source.
*/
get extension() {
return path.extname(this.source);
}
/**
* Get the directory path leading to this object
* Will use the relative path to root if available
*
* There is no setter as the source is not meant to be changed manually.
* You can use the rename or move method to change the source.
*/
get dirname() {
if (this.root) {
let source = this.source;
if (this.isDavResource) {
source = source.split(this._knownDavService).pop();
}
const firstMatch = source.indexOf(this.root);
const root = this.root.replace(/\/$/, "");
return path.dirname(source.slice(firstMatch + root.length) || "/");
}
const url = new URL(this.source);
return path.dirname(url.pathname);
}
/**
* Get the file mime
* There is no setter as the mime is not meant to be changed
*/
get mime() {
return this._data.mime;
}
/**
* Get the file modification time
*/
get mtime() {
return this._data.mtime;
}
/**
* Set the file modification time
*/
set mtime(mtime) {
this._data.mtime = mtime;
}
/**
* Get the file creation time
* There is no setter as the creation time is not meant to be changed
*/
get crtime() {
return this._data.crtime;
}
/**
* Get the file size
*/
get size() {
return this._data.size;
}
/**
* Set the file size
*/
set size(size) {
this.updateMtime();
this._data.size = size;
}
/**
* Get the file attribute
* This contains all additional attributes not provided by the Node class
*/
get attributes() {
return this._attributes;
}
/**
* Get the file permissions
*/
get permissions() {
if (this.owner === null && !this.isDavResource) {
return Permission.READ;
}
return this._data.permissions !== void 0 ? this._data.permissions : Permission.NONE;
}
/**
* Set the file permissions
*/
set permissions(permissions) {
this.updateMtime();
this._data.permissions = permissions;
}
/**
* Get the file owner
* There is no setter as the owner is not meant to be changed
*/
get owner() {
if (!this.isDavResource) {
return null;
}
return this._data.owner;
}
/**
* Is this a dav-related resource ?
*/
get isDavResource() {
return isDavResource(this.source, this._knownDavService);
}
/**
* @deprecated use `isDavResource` instead - will be removed in next major version.
*/
get isDavRessource() {
return this.isDavResource;
}
/**
* Get the dav root of this object
* There is no setter as the root is not meant to be changed
*/
get root() {
if (this._data.root) {
return this._data.root.replace(/^(.+)\/$/, "$1");
}
if (this.isDavResource) {
const root = path.dirname(this.source);
return root.split(this._knownDavService).pop() || null;
}
return null;
}
/**
* Get the absolute path of this object relative to the root
*/
get path() {
if (this.root) {
let source = this.source;
if (this.isDavResource) {
source = source.split(this._knownDavService).pop();
}
const firstMatch = source.indexOf(this.root);
const root = this.root.replace(/\/$/, "");
return source.slice(firstMatch + root.length) || "/";
}
return (this.dirname + "/" + this.basename).replace(/\/\//g, "/");
}
/**
* Get the node id if defined.
* There is no setter as the fileid is not meant to be changed
*/
get fileid() {
return this._data?.id;
}
/**
* Get the node status.
*/
get status() {
return this._data?.status;
}
/**
* Set the node status.
*/
set status(status) {
this._data.status = status;
}
/**
* Get the node data
*/
get data() {
return structuredClone(this._data);
}
/**
* Move the node to a new destination
*
* @param {string} destination the new source.
* e.g. https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg
*/
move(destination) {
validateData({ ...this._data, source: destination }, this._knownDavService);
const oldBasename = this.basename;
this._data.source = destination;
if (this.displayname === oldBasename && this.basename !== oldBasename) {
this.displayname = this.basename;
}
this.updateMtime();
}
/**
* Rename the node
* This aliases the move method for easier usage
*
* @param basename The new name of the node
*/
rename(basename2) {
if (basename2.includes("/")) {
throw new Error("Invalid basename");
}
this.move(path.dirname(this.source) + "/" + basename2);
}
/**
* Update the mtime if exists
*/
updateMtime() {
if (this._data.mtime) {
this._data.mtime = /* @__PURE__ */ new Date();
}
}
/**
* Update the attributes of the node
* Warning, updating attributes will NOT automatically update the mtime.
*
* @param attributes The new attributes to update on the Node attributes
*/
update(attributes) {
for (const [name, value] of Object.entries(attributes)) {
try {
if (value === void 0) {
delete this.attributes[name];
} else {
this.attributes[name] = value;
}
} catch (e) {
if (e instanceof TypeError) {
continue;
}
throw e;
}
}
}
}
class File extends Node {
get type() {
return FileType.File;
}
/**
* Returns a clone of the file
*/
clone() {
return new File(this.data);
}
}
class Folder extends Node {
constructor(data) {
super({
...data,
mime: "httpd/unix-directory"
});
}
get type() {
return FileType.Folder;
}
get extension() {
return null;
}
get mime() {
return "httpd/unix-directory";
}
/**
* Returns a clone of the folder
*/
clone() {
return new Folder(this.data);
}
}
const parsePermissions = function(permString = "") {
let permissions = Permission.NONE;
if (!permString) {
return permissions;
}
if (permString.includes("C") || permString.includes("K")) {
permissions |= Permission.CREATE;
}
if (permString.includes("G")) {
permissions |= Permission.READ;
}
if (permString.includes("W") || permString.includes("N") || permString.includes("V")) {
permissions |= Permission.UPDATE;
}
if (permString.includes("D")) {
permissions |= Permission.DELETE;
}
if (permString.includes("R")) {
permissions |= Permission.SHARE;
}
return permissions;
};
const defaultDavProperties = [
"d:getcontentlength",
"d:getcontenttype",
"d:getetag",
"d:getlastmodified",
"d:creationdate",
"d:displayname",
"d:quota-available-bytes",
"d:resourcetype",
"nc:has-preview",
"nc:is-encrypted",
"nc:mount-type",
"oc:comments-unread",
"oc:favorite",
"oc:fileid",
"oc:owner-display-name",
"oc:owner-id",
"oc:permissions",
"oc:size"
];
const defaultDavNamespaces = {
d: "DAV:",
nc: "http://nextcloud.org/ns",
oc: "http://owncloud.org/ns",
ocs: "http://open-collaboration-services.org/ns"
};
const registerDavProperty = function(prop, namespace = { nc: "http://nextcloud.org/ns" }) {
if (typeof window._nc_dav_properties === "undefined") {
window._nc_dav_properties = [...defaultDavProperties];
window._nc_dav_namespaces = { ...defaultDavNamespaces };
}
const namespaces = { ...window._nc_dav_namespaces, ...namespace };
if (window._nc_dav_properties.find((search) => search === prop)) {
logger.warn(`${prop} already registered`, { prop });
return false;
}
if (prop.startsWith("<") || prop.split(":").length !== 2) {
logger.error(`${prop} is not valid. See example: 'oc:fileid'`, { prop });
return false;
}
const ns = prop.split(":")[0];
if (!namespaces[ns]) {
logger.error(`${prop} namespace unknown`, { prop, namespaces });
return false;
}
window._nc_dav_properties.push(prop);
window._nc_dav_namespaces = namespaces;
return true;
};
const getDavProperties = function() {
if (typeof window._nc_dav_properties === "undefined") {
window._nc_dav_properties = [...defaultDavProperties];
}
return window._nc_dav_properties.map((prop) => `<${prop} />`).join(" ");
};
const getDavNameSpaces = function() {
if (typeof window._nc_dav_namespaces === "undefined") {
window._nc_dav_namespaces = { ...defaultDavNamespaces };
}
return Object.keys(window._nc_dav_namespaces).map((ns) => `xmlns:${ns}="${window._nc_dav_namespaces?.[ns]}"`).join(" ");
};
const getDefaultPropfind = function() {
return `
${getDavProperties()}
`;
};
const getFavoritesReport = function() {
return `
${getDavProperties()}
1
`;
};
const getRecentSearch = function(lastModified) {
return `
${getDavProperties()}
/files/${auth.getCurrentUser()?.uid}/
infinity
httpd/unix-directory
0
${lastModified}
100
0
`;
};
function getRootPath() {
if (_public.isPublicShare()) {
return `/files/${_public.getSharingToken()}`;
}
return `/files/${auth.getCurrentUser()?.uid}`;
}
const defaultRootPath = getRootPath();
function getRemoteURL() {
const url = router.generateRemoteUrl("dav");
if (_public.isPublicShare()) {
return url.replace("remote.php", "public.php");
}
return url;
}
const defaultRemoteURL = getRemoteURL();
const getClient = function(remoteURL = defaultRemoteURL, headers = {}) {
const client = webdav.createClient(remoteURL, { headers });
function setHeaders(token) {
client.setHeaders({
...headers,
// Add this so the server knows it is an request from the browser
"X-Requested-With": "XMLHttpRequest",
// Inject user auth
requesttoken: token ?? ""
});
}
auth.onRequestTokenUpdate(setHeaders);
setHeaders(auth.getRequestToken());
const patcher = webdav.getPatcher();
patcher.patch("fetch", (url, options) => {
const headers2 = options.headers;
if (headers2?.method) {
options.method = headers2.method;
delete headers2.method;
}
return fetch(url, options);
});
return client;
};
const getFavoriteNodes = (davClient, path2 = "/", davRoot = defaultRootPath) => {
const controller = new AbortController();
return new cancelablePromise.CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort());
try {
const contentsResponse = await davClient.getDirectoryContents(`${davRoot}${path2}`, {
signal: controller.signal,
details: true,
data: getFavoritesReport(),
headers: {
// see getClient for patched webdav client
method: "REPORT"
},
includeSelf: true
});
const nodes = contentsResponse.data.filter((node) => node.filename !== path2).map((result) => resultToNode(result, davRoot));
resolve(nodes);
} catch (error) {
reject(error);
}
});
};
const resultToNode = function(node, filesRoot = defaultRootPath, remoteURL = defaultRemoteURL) {
let userId = auth.getCurrentUser()?.uid;
if (_public.isPublicShare()) {
userId = userId ?? "anonymous";
} else if (!userId) {
throw new Error("No user id found");
}
const props = node.props;
const permissions = parsePermissions(props?.permissions);
const owner = String(props?.["owner-id"] || userId);
const id = props.fileid || 0;
const mtime = new Date(Date.parse(node.lastmod));
const crtime = new Date(Date.parse(props.creationdate));
const nodeData = {
id,
source: `${remoteURL}${node.filename}`,
mtime: !isNaN(mtime.getTime()) && mtime.getTime() !== 0 ? mtime : void 0,
crtime: !isNaN(crtime.getTime()) && crtime.getTime() !== 0 ? crtime : void 0,
mime: node.mime || "application/octet-stream",
// Manually cast to work around for https://github.com/perry-mitchell/webdav-client/pull/380
displayname: props.displayname !== void 0 ? String(props.displayname) : void 0,
size: props?.size || Number.parseInt(props.getcontentlength || "0"),
// The fileid is set to -1 for failed requests
status: id < 0 ? NodeStatus.FAILED : void 0,
permissions,
owner,
root: filesRoot,
attributes: {
...node,
...props,
hasPreview: props?.["has-preview"]
}
};
delete nodeData.attributes?.props;
return node.type === "file" ? new File(nodeData) : new Folder(nodeData);
};
exports.File = File;
exports.FileType = FileType;
exports.Folder = Folder;
exports.Node = Node;
exports.NodeStatus = NodeStatus;
exports.Permission = Permission;
exports.defaultDavNamespaces = defaultDavNamespaces;
exports.defaultDavProperties = defaultDavProperties;
exports.defaultRemoteURL = defaultRemoteURL;
exports.defaultRootPath = defaultRootPath;
exports.getClient = getClient;
exports.getDavNameSpaces = getDavNameSpaces;
exports.getDavProperties = getDavProperties;
exports.getDefaultPropfind = getDefaultPropfind;
exports.getFavoriteNodes = getFavoriteNodes;
exports.getFavoritesReport = getFavoritesReport;
exports.getRecentSearch = getRecentSearch;
exports.getRemoteURL = getRemoteURL;
exports.getRootPath = getRootPath;
exports.logger = logger;
exports.parsePermissions = parsePermissions;
exports.registerDavProperty = registerDavProperty;
exports.resultToNode = resultToNode;