Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ The available configuration options are as follows:
| `authType` | `null` | The authentication type to use. If not provided, defaults to trying to detect based upon whether `username` and `password` were provided. |
| `attributeNamePrefix` | `@` | Prefix used to identify attributes on the property object |
| `contactHref` | _[This URL](https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md)_ | Contact URL used for LOCKs. |
| `entityDecoder` | _None_ | Entity decoder configuration for XML parsing. See [entity decoder](#entity-decoder). |
| `headers` | `{}` | Additional headers provided to all requests. Headers provided here are overridden by method-specific headers, including `Authorization`. |
| `httpAgent` | _None_ | HTTP agent instance. Available only in Node. See [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent). |
| `httpsAgent` | _None_ | HTTPS agent instance. Available only in Node. See [https.Agent](https://nodejs.org/api/https.html#https_class_https_agent). |
Expand All @@ -230,6 +231,29 @@ The available configuration options are as follows:
| `username` | _None_ | Username for authentication. |
| `withCredentials` | _None_ | Credentials inclusion setting for the request, |

### Entity decoder

The `entityDecoder` option controls how XML entity references are decoded during XML parsing. When not set, entity expansion limits are unlimited (no restrictions).

```typescript
const client = createClient("https://some-server.org", {
username: "user",
password: "pass",
entityDecoder: {
limit: {
maxTotalExpansions: 1000,
maxExpandedLength: 50000
}
}
});
```

| Property | Default | Description |
|-----------------|----------|-------------------------------------------------------|
| `limit` | _None_ | Security limits for entity expansion. |
| `limit.maxTotalExpansions` | `0` | Maximum number of entity references expanded per document. `0` means unlimited. |
| `limit.maxExpandedLength` | `0` | Maximum number of characters added by entity expansion per document. `0` means unlimited. |

### Client methods

The `WebDAVClient` interface type contains all the methods and signatures for the WebDAV client instance.
Expand Down
46 changes: 23 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"base-64": "^1.0.0",
"byte-length": "^1.0.2",
"entities": "^6.0.1",
"fast-xml-parser": "^5.6.0",
"fast-xml-parser": "^5.7.2",
"hot-patcher": "^2.0.1",
"layerr": "^3.0.0",
"md5": "^2.3.0",
Expand Down
2 changes: 2 additions & 0 deletions source/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
authType: authTypeRaw = null,
remoteBasePath,
contactHref = DEFAULT_CONTACT_HREF,
entityDecoder,
ha1,
headers = {},
httpAgent,
Expand All @@ -77,6 +78,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
parsing: {
attributeNamePrefix: options.attributeNamePrefix ?? "@",
attributeParsers: [],
entityDecoder,
tagParsers: [displaynameTagParser]
},
remotePath: extractURLPath(remoteURL),
Expand Down
20 changes: 16 additions & 4 deletions source/tools/dav.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import path from "path-posix";
import { XMLParser } from "fast-xml-parser";
// @ts-expect-error Types declare default export but runtime provides named export
import { EntityDecoder } from "@nodable/entities";
import nestedProp from "nested-property";
import { encodePath, normalisePath } from "./path.js";
import type {
Expand Down Expand Up @@ -33,9 +35,10 @@ function toJPathString(
function getParser({
attributeNamePrefix,
attributeParsers,
entityDecoder: entityDecoderOptions,
tagParsers
}: WebDAVParsingContext): XMLParser {
return new XMLParser({
const parserOptions: Record<string, unknown> = {
allowBooleanAttributes: true,
attributeNamePrefix,
textNodeName: "text",
Expand All @@ -55,7 +58,7 @@ function getParser({
return value;
}
} catch (error) {
// skipping this invalid parser
// skipping this invalid processor
}
}
return attrValue;
Expand All @@ -69,12 +72,21 @@ function getParser({
return value;
}
} catch (error) {
// skipping this invalid parser
// skipping this invalid processor
}
}
return tagValue;
}
});
};
if (entityDecoderOptions) {
parserOptions.entityDecoder = new EntityDecoder({
limit: {
maxTotalExpansions: entityDecoderOptions.limit?.maxTotalExpansions ?? 0,
maxExpandedLength: entityDecoderOptions.limit?.maxExpandedLength ?? 0
}
});
}
return new XMLParser(parserOptions);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export interface WebDAVClientOptions {
authType?: AuthType;
remoteBasePath?: string;
contactHref?: string;
entityDecoder?: WebDAVEntityDecoderOptions;
ha1?: string;
headers?: Headers;
httpAgent?: any;
Expand Down Expand Up @@ -397,8 +398,16 @@ export type WebDAVAttributeParser = (
*/
export type WebDAVTagParser = (jPath: string, tagValue: string) => string | unknown | undefined;

export interface WebDAVEntityDecoderOptions {
limit?: {
maxTotalExpansions?: number;
maxExpandedLength?: number;
};
}

export interface WebDAVParsingContext {
attributeNamePrefix?: string;
attributeParsers: WebDAVAttributeParser[];
entityDecoder?: WebDAVEntityDecoderOptions;
tagParsers: WebDAVTagParser[];
}
84 changes: 83 additions & 1 deletion test/node/tools/dav.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { readFile } from "fs/promises";
import { parseXML } from "../../../source/index.js";
import { parseXML, type WebDAVEntityDecoderOptions } from "../../../source/index.js";

describe("parseXML", function () {
it("keeps numeric-looking displaynames", async function () {
Expand Down Expand Up @@ -149,4 +149,86 @@ describe("parseXML", function () {
}
]);
});

describe("entityDecoder", function () {
it("parses XML with entities when entityDecoder is not set", async function () {
const xml = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/file.txt</d:href>
<d:propstat>
<d:prop>
<displayname>A &amp; B &lt; C</displayname>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>`;

const parsed = await parseXML(xml);
expect(parsed.multistatus.response).to.have.length(1);
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B < C");
});

it("parses XML with entities when entityDecoder limit is set", async function () {
const decoderOptions: WebDAVEntityDecoderOptions = {
limit: {
maxTotalExpansions: 0,
maxExpandedLength: 0
}
};

const xml = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/file.txt</d:href>
<d:propstat>
<d:prop>
<displayname>A &amp; B &lt; C</displayname>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>`;

const parsed = await parseXML(xml, {
attributeNamePrefix: "@",
attributeParsers: [],
entityDecoder: decoderOptions,
tagParsers: []
});
expect(parsed.multistatus.response).to.have.length(1);
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B < C");
});

it("applies maxTotalExpansions limit when set", async function () {
const decoderOptions: WebDAVEntityDecoderOptions = {
limit: {
maxTotalExpansions: 1
}
};

const xml = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/file.txt</d:href>
<d:propstat>
<d:prop>
<displayname>A &amp; B</displayname>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>`;

const parsed = await parseXML(xml, {
attributeNamePrefix: "@",
attributeParsers: [],
entityDecoder: decoderOptions,
tagParsers: []
});
expect(parsed.multistatus.response).to.have.length(1);
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B");
});
});
});
Loading