nephele
Version:
Highly customizable and extensible WebDAV server for Node.js and Express.
277 lines • 13.3 kB
JavaScript
import { BadRequestError, ForbiddenError, NotAcceptableError, PropertyNotFoundError, UnauthorizedError, } from '../Errors/index.js';
import { MultiStatus, Status, PropStatStatus } from '../MultiStatus.js';
import { Method } from './Method.js';
export class PROPFIND extends Method {
async run(request, response) {
const { url, encoding } = this.getRequestData(request, response);
if (await this.runPlugins(request, response, 'beginPropfind', {
method: this,
url,
})) {
return;
}
await this.checkAuthorization(request, response, 'PROPFIND');
const contentType = request.accepts('application/xml', 'text/xml');
if (!contentType) {
throw new NotAcceptableError('Requested content type is not supported.');
}
const depth = request.get('Depth') || 'infinity';
const resource = await response.locals.adapter.getResource(url, response.locals.baseUrl);
if ((await resource.isCollection()) && !url.toString().endsWith('/')) {
response.set({
'Content-Location': `${url}/`,
});
}
if (await this.runPlugins(request, response, 'prePropfind', {
method: this,
resource,
depth,
})) {
return;
}
if (!['0', '1', 'infinity'].includes(depth)) {
throw new BadRequestError('Depth header must be one of "0", "1", or "infinity".');
}
const xmlBody = await this.getBodyXML(request, response);
const { output: xml, prefixes } = xmlBody
? await this.parseXml(xmlBody)
: { output: null, prefixes: {} };
let requestedProps = [];
let allprop = true;
let propname = false;
if (xml != null) {
if (!('propfind' in xml)) {
throw new BadRequestError('PROPFIND methods requires a propfind element.');
}
if ('propname' in xml.propfind) {
propname = true;
}
if (!('allprop' in xml.propfind)) {
allprop = false;
}
else if ('include' in xml.propfind) {
for (let include of xml.propfind.include) {
requestedProps = [
...requestedProps,
...Object.keys(include).filter((name) => name !== '$'),
];
}
}
if ('prop' in xml.propfind) {
for (let prop of xml.propfind.prop) {
requestedProps = [
...requestedProps,
...Object.keys(prop).filter((name) => name !== '$'),
];
}
}
}
await this.checkConditionalHeaders(request, response);
if (await this.runPlugins(request, response, 'beforePropfind', {
method: this,
resource,
depth,
})) {
return;
}
const multiStatus = new MultiStatus();
if (propname) {
response.locals.debug(`Requested prop names.`);
}
else if (allprop) {
response.locals.debug(`Requested all props.${requestedProps.length ? ` Includes: ${requestedProps.join(', ')}` : ''}`);
}
else {
response.locals.debug(`Requested props: ${requestedProps.join(', ')}`);
}
response.locals.debug(`Requested depth: ${depth}`);
let level = 0;
const addResourceProps = async (curResource, skipRootCheck = false) => {
const url = await curResource.getCanonicalUrl();
response.locals.debug(`Retrieving props for ${await curResource.getCanonicalPath()}`);
try {
if (!skipRootCheck &&
(await this.isAdapterRoot(request, response, url))) {
const absoluteUrl = new URL(url.toString().replace(/\/?$/, () => '/'));
const adapter = await this.getAdapter(request, response, decodeURIComponent(absoluteUrl.pathname.substring(request.baseUrl.length)));
curResource = await adapter.getResource(absoluteUrl, absoluteUrl);
}
}
catch (e) {
const error = new Status(url, 500);
error.description = 'An internal server error occurred.';
response.locals.errors.push(error);
multiStatus.addStatus(error);
return;
}
if (!(await curResource.adapter.isAuthorized(url, 'PROPFIND', curResource.baseUrl, response.locals.user))) {
const error = new Status(url, 401);
error.description =
'The user is not authorized to get properties for this resource.';
response.locals.errors.push(error);
multiStatus.addStatus(error);
return;
}
const status = new Status(url, 207);
const props = await curResource.getProperties();
try {
const supportsLocks = (await curResource.adapter.getComplianceClasses(url, request, response)).includes('2');
if (propname) {
const propnames = await props.listByUser(response.locals.user);
const propStatStatus = new PropStatStatus(200);
const propObj = {};
for (let name of propnames) {
propObj[name] = {};
}
if (supportsLocks) {
propObj.lockdiscovery = {};
}
propStatStatus.setProp(propObj);
status.addPropStatStatus(propStatStatus);
}
else {
let propObj = {};
const forbiddenProps = [];
const unauthorizedProps = [];
const notFoundProps = [];
const errorProps = [];
if (allprop) {
propObj = await props.getAllByUser(response.locals.user);
for (let name in propObj) {
if (propObj[name] instanceof ForbiddenError) {
forbiddenProps.push(name);
delete propObj[name];
}
else if (propObj[name] instanceof UnauthorizedError) {
unauthorizedProps.push(name);
delete propObj[name];
}
else if (propObj[name] instanceof PropertyNotFoundError) {
notFoundProps.push(name);
delete propObj[name];
}
else if (propObj[name] instanceof Error) {
errorProps.push(name);
delete propObj[name];
}
}
}
for (let name of requestedProps) {
if (name in propObj) {
continue;
}
if (name === 'lockdiscovery') {
if (!supportsLocks) {
notFoundProps.push(name);
}
continue;
}
try {
const value = await props.getByUser(name, response.locals.user);
propObj[name] = value;
}
catch (e) {
if (e instanceof ForbiddenError) {
forbiddenProps.push(name);
}
else if (e instanceof UnauthorizedError) {
unauthorizedProps.push(name);
}
else if (e instanceof PropertyNotFoundError) {
notFoundProps.push(name);
}
else {
errorProps.push(name);
}
}
}
if (supportsLocks &&
(allprop || requestedProps.includes('lockdiscovery'))) {
const currentLocks = await this.getLocks(request, response, curResource);
propObj.lockdiscovery = await this.formatLocks(currentLocks.all);
}
if (Object.keys(propObj).length) {
const propStatStatus = new PropStatStatus(200);
propStatStatus.setProp(propObj);
status.addPropStatStatus(propStatStatus);
}
if (forbiddenProps.length) {
const propStatStatus = new PropStatStatus(403);
propStatStatus.description = `The user does not have access to the ${forbiddenProps
.map((name) => name.replace('%%', ''))
.join(', ')} propert${forbiddenProps.length === 1 ? 'y' : 'ies'}.`;
propStatStatus.setProp(Object.fromEntries(forbiddenProps.map((name) => [name, {}])));
response.locals.errors.push(propStatStatus);
status.addPropStatStatus(propStatStatus);
}
if (unauthorizedProps.length) {
const propStatStatus = new PropStatStatus(401);
propStatStatus.description = `The user is not authorized to retrieve the ${unauthorizedProps
.map((name) => name.replace('%%', ''))
.join(', ')} propert${unauthorizedProps.length === 1 ? 'y' : 'ies'}.`;
propStatStatus.setProp(Object.fromEntries(unauthorizedProps.map((name) => [name, {}])));
response.locals.errors.push(propStatStatus);
status.addPropStatStatus(propStatStatus);
}
if (notFoundProps.length) {
const propStatStatus = new PropStatStatus(404);
propStatStatus.description = `The ${notFoundProps
.map((name) => name.replace('%%', ''))
.join(', ')} propert${notFoundProps.length === 1 ? 'y was' : 'ies were'} not found.`;
propStatStatus.setProp(Object.fromEntries(notFoundProps.map((name) => [name, {}])));
response.locals.errors.push(propStatStatus);
status.addPropStatStatus(propStatStatus);
}
if (errorProps.length) {
const propStatStatus = new PropStatStatus(500);
propStatStatus.description = `An error occurred while trying to retrieve the ${errorProps
.map((name) => name.replace('%%', ''))
.join(', ')} propert${errorProps.length === 1 ? 'y' : 'ies'}.`;
propStatStatus.setProp(Object.fromEntries(errorProps.map((name) => [name, {}])));
response.locals.errors.push(propStatStatus);
status.addPropStatStatus(propStatStatus);
}
}
}
catch (e) {
const propStatStatus = new PropStatStatus(500);
propStatStatus.description = 'An internal server error occurred.';
response.locals.errors.push(propStatStatus);
status.addPropStatStatus(propStatStatus);
}
multiStatus.addStatus(status);
if (depth === '0' || (level === 1 && depth === '1')) {
return;
}
if (await curResource.isCollection()) {
let children = [];
try {
children = await curResource.getInternalMembers(response.locals.user);
}
catch (e) {
if (!(e instanceof UnauthorizedError)) {
throw e;
}
}
level++;
for (let child of children) {
await addResourceProps(child);
}
level--;
}
};
await addResourceProps(resource, true);
const responseXml = await this.renderXml(multiStatus.render(), prefixes);
response.status(207);
response.set({
'Content-Type': `${contentType}; charset=utf-8`,
});
this.sendBodyContent(response, responseXml, encoding);
await this.runPlugins(request, response, 'afterPropfind', {
method: this,
resource,
depth,
});
}
}
//# sourceMappingURL=PROPFIND.js.map