"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;