UNPKG

nephele

Version:

Highly customizable and extensible WebDAV server for Node.js and Express.

378 lines (336 loc) 12.1 kB
import type { Request } from 'express'; import type { AuthResponse, Resource } from '../Interfaces/index.js'; 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: Request, response: AuthResponse) { 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: string[] = []; 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: Resource, skipRootCheck = false, ) => { const url = await curResource.getCanonicalUrl(); response.locals.debug( `Retrieving props for ${await curResource.getCanonicalPath()}`, ); try { // If the resource is the root of another adapter, we need its copy of the // resource in order to continue getting props. 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: any) { const error = new Status(url, 500); error.description = 'An internal server error occurred.'; response.locals.errors.push(error); multiStatus.addStatus(error); return; } // Use the resource's adapter and baseUrl, because this could be on // another adapter than the request. 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: { [k: string]: {} } = {}; for (let name of propnames) { propObj[name] = {}; } if (supportsLocks) { propObj.lockdiscovery = {}; } propStatStatus.setProp(propObj); status.addPropStatStatus(propStatStatus); } else { let propObj: { [k: string]: any } = {}; const forbiddenProps: string[] = []; const unauthorizedProps: string[] = []; const notFoundProps: string[] = []; const errorProps: string[] = []; 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: any) { 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: any) { 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: Resource[] = []; try { children = await curResource.getInternalMembers(response.locals.user); } catch (e: any) { if (!(e instanceof UnauthorizedError)) { throw e; } // Silently exclude members not visible to the user. } 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); // Multi-Status response.set({ 'Content-Type': `${contentType}; charset=utf-8`, }); this.sendBodyContent(response, responseXml, encoding); await this.runPlugins(request, response, 'afterPropfind', { method: this, resource, depth, }); } }