Neah/node_modules/webdav/dist/node/tools/dav.js
2025-04-24 19:10:05 +02:00

206 lines
7.3 KiB
JavaScript

import path from "path-posix";
import { XMLParser } from "fast-xml-parser";
import nestedProp from "nested-property";
import { encodePath, normalisePath } from "./path.js";
var PropertyType;
(function (PropertyType) {
PropertyType["Array"] = "array";
PropertyType["Object"] = "object";
PropertyType["Original"] = "original";
})(PropertyType || (PropertyType = {}));
function getParser() {
return new XMLParser({
allowBooleanAttributes: true,
attributeNamePrefix: "",
textNodeName: "text",
ignoreAttributes: false,
removeNSPrefix: true,
numberParseOptions: {
hex: true,
leadingZeros: false
},
attributeValueProcessor(attrName, attrValue, jPath) {
// handle boolean attributes
if (attrValue === "true" || attrValue === "false") {
return attrValue === "true";
}
return attrValue;
},
tagValueProcessor(tagName, tagValue, jPath) {
if (jPath.endsWith("propstat.prop.displayname")) {
// Do not parse the display name, because this causes e.g. '2024.10' to result in number 2024.1
return;
}
return tagValue;
}
// We don't use the processors here as decoding is done manually
// later on - decoding early would break some path checks.
});
}
function getPropertyOfType(obj, prop, type = PropertyType.Original) {
const val = nestedProp.get(obj, prop);
if (type === "array" && Array.isArray(val) === false) {
return [val];
}
else if (type === "object" && Array.isArray(val)) {
return val[0];
}
return val;
}
function normaliseResponse(response) {
const output = Object.assign({}, response);
// Only either status OR propstat is allowed
if (output.status) {
nestedProp.set(output, "status", getPropertyOfType(output, "status", PropertyType.Object));
}
else {
nestedProp.set(output, "propstat", getPropertyOfType(output, "propstat", PropertyType.Object));
nestedProp.set(output, "propstat.prop", getPropertyOfType(output, "propstat.prop", PropertyType.Object));
}
return output;
}
function normaliseResult(result) {
const { multistatus } = result;
if (multistatus === "") {
return {
multistatus: {
response: []
}
};
}
if (!multistatus) {
throw new Error("Invalid response: No root multistatus found");
}
const output = {
multistatus: Array.isArray(multistatus) ? multistatus[0] : multistatus
};
nestedProp.set(output, "multistatus.response", getPropertyOfType(output, "multistatus.response", PropertyType.Array));
nestedProp.set(output, "multistatus.response", nestedProp.get(output, "multistatus.response").map(response => normaliseResponse(response)));
return output;
}
/**
* Parse an XML response from a WebDAV service,
* converting it to an internal DAV result
* @param xml The raw XML string
* @returns A parsed and processed DAV result
*/
export function parseXML(xml) {
return new Promise(resolve => {
const result = getParser().parse(xml);
resolve(normaliseResult(result));
});
}
/**
* Get a file stat result from given DAV properties
* @param props DAV properties
* @param filename The filename for the file stat
* @param isDetailed Whether or not the raw props of the resource should be returned
* @returns A file stat result
*/
export function prepareFileFromProps(props, filename, isDetailed = false) {
// Last modified time, raw size, item type and mime
const { getlastmodified: lastMod = null, getcontentlength: rawSize = "0", resourcetype: resourceType = null, getcontenttype: mimeType = null, getetag: etag = null } = props;
const type = resourceType &&
typeof resourceType === "object" &&
typeof resourceType.collection !== "undefined"
? "directory"
: "file";
const stat = {
filename,
basename: path.basename(filename),
lastmod: lastMod,
size: parseInt(rawSize, 10),
type,
etag: typeof etag === "string" ? etag.replace(/"/g, "") : null
};
if (type === "file") {
stat.mime = mimeType && typeof mimeType === "string" ? mimeType.split(";")[0] : "";
}
if (isDetailed) {
// The XML parser tries to interpret values, but the display name is required to be a string
if (typeof props.displayname !== "undefined") {
props.displayname = String(props.displayname);
}
stat.props = props;
}
return stat;
}
/**
* Parse a DAV result for file stats
* @param result The resulting DAV response
* @param filename The filename that was stat'd
* @param isDetailed Whether or not the raw props of
* the resource should be returned
* @returns A file stat result
*/
export function parseStat(result, filename, isDetailed = false) {
let responseItem = null;
try {
// should be a propstat response, if not the if below will throw an error
if (result.multistatus.response[0].propstat) {
responseItem = result.multistatus.response[0];
}
}
catch (e) {
/* ignore */
}
if (!responseItem) {
throw new Error("Failed getting item stat: bad response");
}
const { propstat: { prop: props, status: statusLine } } = responseItem;
// As defined in https://tools.ietf.org/html/rfc2068#section-6.1
const [_, statusCodeStr, statusText] = statusLine.split(" ", 3);
const statusCode = parseInt(statusCodeStr, 10);
if (statusCode >= 400) {
const err = new Error(`Invalid response: ${statusCode} ${statusText}`);
err.status = statusCode;
throw err;
}
const filePath = normalisePath(filename);
return prepareFileFromProps(props, filePath, isDetailed);
}
/**
* Parse a DAV result for a search request
*
* @param result The resulting DAV response
* @param searchArbiter The collection path that was searched
* @param isDetailed Whether or not the raw props of the resource should be returned
*/
export function parseSearch(result, searchArbiter, isDetailed) {
const response = {
truncated: false,
results: []
};
response.truncated = result.multistatus.response.some(v => {
return ((v.status || v.propstat?.status).split(" ", 3)?.[1] === "507" &&
v.href.replace(/\/$/, "").endsWith(encodePath(searchArbiter).replace(/\/$/, "")));
});
result.multistatus.response.forEach(result => {
if (result.propstat === undefined) {
return;
}
const filename = result.href.split("/").map(decodeURIComponent).join("/");
response.results.push(prepareFileFromProps(result.propstat.prop, filename, isDetailed));
});
return response;
}
/**
* Translate a disk quota indicator to a recognised
* value (includes "unlimited" and "unknown")
* @param value The quota indicator, eg. "-3"
* @returns The value in bytes, or another indicator
*/
export function translateDiskSpace(value) {
switch (String(value)) {
case "-3":
return "unlimited";
case "-2":
/* falls-through */
case "-1":
// -1 is non-computed
return "unknown";
default:
return parseInt(String(value), 10);
}
}