71 lines
2.9 KiB
JavaScript
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()
|
|
}
|