server = $server; $server->on('afterMethod:GET', [$this, 'afterGet']); $server->on('propFind', [$this, 'propFind']); $server->on('propPatch', [$this, 'propPatch']); } /** * Handles the GET request for versioned files. * * Validates the request path, checks node type, and sets appropriate download headers * to ensure compatibility across different clients and browsers. */ public function afterGet(RequestInterface $request, ResponseInterface $response): void { $path = $request->getPath(); if (!str_starts_with($path, 'versions/')) { return; } try { $node = $this->server->tree->getNodeForPath($path); } catch (NotFound $e) { return; } if (!($node instanceof VersionFile)) { return; } $filename = $node->getVersion()->getSourceFileName(); $this->addContentDispositionHeader($response, $filename); } /** * WebDAV PROPFIND event handler for versioned files. * * Provides read-only access to version-related information if the * current node is a VersionFile. */ public function propFind(PropFind $propFind, INode $node): void { if (!($node instanceof VersionFile)) { return; } $propFind->handle( self::VERSION_LABEL, fn () => $node->getMetadataValue(self::LABEL) ); $propFind->handle( self::VERSION_AUTHOR, fn () => $node->getMetadataValue(self::AUTHOR) ); $propFind->handle( FilesPlugin::HAS_PREVIEW_PROPERTYNAME, fn (): string => $this->previewManager->isMimeSupported($node->getContentType()) ? 'true' : 'false', ); } /** * WebDAV PROPPATCH event handler for versioned files. * * Updates version related properties on VersionFile nodes. */ public function propPatch(string $path, PropPatch $propPatch): void { $node = $this->server->tree->getNodeForPath($path); if (!($node instanceof VersionFile)) { return; } $propPatch->handle( self::VERSION_LABEL, fn (string $label) => $node->setMetadataValue(self::LABEL, $label) ); } /** * Add a Content-Disposition header in a way that attempts to be broadly compatible with various user agents. * * Sends both 'filename' (legacy quoted) and 'filename*' (UTF-8 encoded) per RFC 6266, * except for known quirky agents known to mishandle the `filename*`, which only get `filename`. * * Note: The quoting/escaping should strictly follow RFC 6266 and RFC 5987. * * TODO: Currently uses rawurlencode($filename) for both parameters, which is wrong: filename= should be plain * quoted ASCII (with necessary escaping), while filename* should be UTF-8 percent-encoded. * TODO: This logic appears elsewhere (sometimes with different quoting/filename handling) and could benefit * from a shared utility function. See Symfony example: * - https://github.com/symfony/symfony/blob/175775eb21508becf7e7a16d65959488e522c39a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php#L146-L155 * - https://github.com/symfony/symfony/blob/175775eb21508becf7e7a16d65959488e522c39a/src/Symfony/Component/HttpFoundation/HeaderUtils.php#L152-L165 * * @param ResponseInterface $response HTTP response object to add the header to * @param string $filename Download filename */ private function addContentDispositionHeader(ResponseInterface $response, string $filename): void { if (!$this->request->isUserAgent(self::LEGACY_FILENAME_HEADER_USER_AGENTS)) { // Modern clients will use 'filename*'; older clients will refer to `filename`. // The older fallback must be listed first per RFC. // In theory this is all we actually need to handle both client types. $response->addHeader( 'Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"; filename*=UTF-8\'\'' . rawurlencode($filename) ); } else { // Quirky clients that choke on `filename*`: only send `filename=` $response->addHeader( 'Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"'); } } }