Tofu/app/tools/webdav/parsePropfindXml.js

71 lines
2.9 KiB
JavaScript

// WebDAV namespace. Nextcloud serves the 'd:' prefix, but the namespace is what
// identifies a tag, so nothing here ever matches on the prefix itself.
const DAV_NAMESPACE = 'DAV:'
// Reads a PROPFIND (Depth: 1) body and returns the children of the requested folder.
// `parseXml` is injectable on purpose: the default parser needs DOMParser, which only
// exists in the browser, so a test can hand over a minimal parser instead.
// Unreadable XML gives an empty list; this never throws.
export function parsePropfindXml(xmlText, basePath, parseXml = defaultParseXml) {
const responses = readResponses(xmlText, parseXml)
const depths = responses.map((response) => readHrefSegments(response).length)
// A Depth: 1 body also describes the requested folder itself. It is the only
// response whose href has the fewest segments, so it is the one we drop.
const ownDepth = Math.min(...depths)
return responses
.filter((response, index) => depths[index] > ownDepth)
.map((response) => readEntry(response, basePath))
}
function defaultParseXml(xmlText) {
if (typeof DOMParser === 'undefined') return null
const parsed = new DOMParser().parseFromString(xmlText, 'application/xml')
// DOMParser never throws: it reports a broken document with a parsererror node.
if (parsed.getElementsByTagName('parsererror').length > 0) return null
return parsed
}
function readResponses(xmlText, parseXml) {
if (typeof xmlText !== 'string' || xmlText.trim() === '') return []
let parsed = null
try {
parsed = parseXml(xmlText)
} catch (error) {
console.error('parsePropfindXml: the XML parser failed', error)
return []
}
if (!parsed || typeof parsed.getElementsByTagNameNS !== 'function') return []
return Array.from(parsed.getElementsByTagNameNS(DAV_NAMESPACE, 'response'))
}
function readEntry(response, basePath) {
const segments = readHrefSegments(response)
const segment = segments[segments.length - 1] ?? ''
const prefix = String(basePath ?? '').split('/').filter(Boolean).join('/')
return {
name: readProperty(response, 'displayname') || segment,
path: prefix === '' ? segment : `${prefix}/${segment}`,
isFolder: response.getElementsByTagNameNS(DAV_NAMESPACE, 'collection').length > 0,
size: Number(readProperty(response, 'getcontentlength')) || 0,
lastModified: readProperty(response, 'getlastmodified') || null,
}
}
// The href is percent-encoded by the server; segments are returned decoded, which
// is the form the rest of the app uses (buildDavUrl encodes them again).
function readHrefSegments(response) {
const href = readProperty(response, 'href')
let decoded = href
try {
decoded = decodeURIComponent(href)
} catch (error) {
decoded = href
}
return decoded.split('/').filter(Boolean)
}
function readProperty(response, localName) {
const found = response.getElementsByTagNameNS(DAV_NAMESPACE, localName)
if (!found || found.length === 0) return ''
return String(found[0].textContent ?? '').trim()
}