/** * principalSearchPropertySetReport * * This method responsible for handing the * {DAV:}principal-search-property-set report. This report returns a list * of properties the client may search on, using the * {DAV:}principal-property-search report. * * @param Xml\Request\PrincipalSearchPropertySetReport $report * @return void */ protected function principalSearchPropertySetReport($report) { $httpDepth = $this->server->getHTTPDepth(0); if ($httpDepth !== 0) { throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); } $writer = $this->server->xml->getWriter(); $writer->openMemory(); $writer->startDocument(); $writer->startElement('{DAV:}principal-search-property-set'); foreach ($this->principalSearchPropertySet as $propertyName => $description) { $writer->startElement('{DAV:}principal-search-property'); $writer->startElement('{DAV:}prop'); $writer->writeElement($propertyName); $writer->endElement(); // prop if ($description) { $writer->write([['name' => '{DAV:}description', 'value' => $description, 'attributes' => ['xml:lang' => 'en']]]); } $writer->endElement(); // principal-search-property } $writer->endElement(); // principal-search-property-set $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setStatus(200); $this->server->httpResponse->setBody($writer->outputMemory()); }
/** * aclPrincipalPropSet REPORT * * This method is responsible for handling the {DAV:}acl-principal-prop-set * REPORT, as defined in: * * https://tools.ietf.org/html/rfc3744#section-9.2 * * This REPORT allows a user to quickly fetch information about all * principals specified in the access control list. Most commonly this * is used to for example generate a UI with ACL rules, allowing you * to show names for principals for every entry. * * @param string $path * @param Xml\Request\AclPrincipalPropSetReport $report * @return void */ protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report) { if ($this->server->getHTTPDepth(0) !== 0) { throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0'); } // Fetching ACL rules for the given path. We're using the property // API and not the local getACL, because it will ensure that all // business rules and restrictions are applied. $acl = $this->server->getProperties($path, '{DAV:}acl'); if (!$acl || !isset($acl['{DAV:}acl'])) { throw new Forbidden('Could not fetch ACL rules for this path'); } $principals = []; foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) { if ($ace['principal'][0] === '{') { // It's not a principal, it's one of the special rules such as {DAV:}authenticated continue; } $principals[] = $ace['principal']; } $properties = $this->server->getPropertiesForMultiplePaths($principals, $report->properties); $this->server->httpResponse->setStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setBody($this->server->generateMultiStatus($properties)); }
/** * This function handles the addressbook-query REPORT * * This report is used by the client to filter an addressbook based on a * complex query. * * @param \DOMNode $dom * @return void */ protected function addressbookQueryReport($dom) { $query = new AddressBookQueryParser($dom); $query->parse(); $depth = $this->server->getHTTPDepth(0); if ($depth == 0) { $candidateNodes = [$this->server->tree->getNodeForPath($this->server->getRequestUri())]; if (!$candidateNodes[0] instanceof ICard) { throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0'); } } else { $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri()); } $xpath = new \DOMXPath($dom); $xpath->registerNameSpace('card', Plugin::NS_CARDDAV); $xpath->registerNameSpace('dav', 'urn:DAV'); $contentType = $xpath->evaluate("string(/card:addressbook-query/dav:prop/card:address-data/@content-type)"); $version = $xpath->evaluate("string(/card:addressbook-query/dav:prop/card:address-data/@version)"); if ($version) { $contentType .= '; version=' . $version; } $vcardType = $this->negotiateVCard($contentType); $validNodes = []; foreach ($candidateNodes as $node) { if (!$node instanceof ICard) { continue; } $blob = $node->get(); if (is_resource($blob)) { $blob = stream_get_contents($blob); } if (!$this->validateFilters($blob, $query->filters, $query->test)) { continue; } $validNodes[] = $node; if ($query->limit && $query->limit <= count($validNodes)) { // We hit the maximum number of items, we can stop now. break; } } $result = []; foreach ($validNodes as $validNode) { if ($depth == 0) { $href = $this->server->getRequestUri(); } else { $href = $this->server->getRequestUri() . '/' . $validNode->getName(); } list($props) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0); if (isset($props[200]['{' . self::NS_CARDDAV . '}address-data'])) { $props[200]['{' . self::NS_CARDDAV . '}address-data'] = $this->convertVCard($props[200]['{' . self::NS_CARDDAV . '}address-data'], $vcardType); } $result[] = $props; } $prefer = $this->server->getHTTPPRefer(); $this->server->httpResponse->setStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return-minimal'])); }
/** * Locks an uri * * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type * of lock (shared or exclusive) and the owner of the lock * * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock * * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3 * * @param string $uri * @return void */ protected function httpLock($uri) { $lastLock = null; if (!$this->validateLock($uri, $lastLock)) { // If the existing lock was an exclusive lock, we need to fail if (!$lastLock || $lastLock->scope == LockInfo::EXCLUSIVE) { //var_dump($lastLock); throw new DAV\Exception\ConflictingLock($lastLock); } } if ($body = $this->server->httpRequest->getBody(true)) { // This is a new lock request $lockInfo = $this->parseLockRequest($body); $lockInfo->depth = $this->server->getHTTPDepth(); $lockInfo->uri = $uri; if ($lastLock && $lockInfo->scope != LockInfo::SHARED) { throw new DAV\Exception\ConflictingLock($lastLock); } } elseif ($lastLock) { // This must have been a lock refresh $lockInfo = $lastLock; // The resource could have been locked through another uri. if ($uri != $lockInfo->uri) { $uri = $lockInfo->uri; } } else { // There was neither a lock refresh nor a new lock request throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); } if ($timeout = $this->getTimeoutHeader()) { $lockInfo->timeout = $timeout; } $newFile = false; // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first try { $this->server->tree->getNodeForPath($uri); // We need to call the beforeWriteContent event for RFC3744 // Edit: looks like this is not used, and causing problems now. // // See Issue 222 // $this->server->broadcastEvent('beforeWriteContent',array($uri)); } catch (DAV\Exception\NotFound $e) { // It didn't, lets create it $this->server->createFile($uri, fopen('php://memory', 'r')); $newFile = true; } $this->lockNode($uri, $lockInfo); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Lock-Token', '<opaquelocktoken:' . $lockInfo->token . '>'); $this->server->httpResponse->sendStatus($newFile ? 201 : 200); $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo)); }
/** * This function handles the addressbook-query REPORT * * This report is used by the client to filter an addressbook based on a * complex query. * * @param \DOMNode $dom * @return void */ protected function addressbookQueryReport($dom) { $query = new AddressBookQueryParser($dom); $query->parse(); $depth = $this->server->getHTTPDepth(0); if ($depth == 0) { $candidateNodes = array($this->server->tree->getNodeForPath($this->server->getRequestUri())); } else { $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri()); } $validNodes = array(); foreach ($candidateNodes as $node) { if (!$node instanceof ICard) { continue; } $blob = $node->get(); if (is_resource($blob)) { $blob = stream_get_contents($blob); } if (!$this->validateFilters($blob, $query->filters, $query->test)) { continue; } $validNodes[] = $node; if ($query->limit && $query->limit <= count($validNodes)) { // We hit the maximum number of items, we can stop now. break; } } $result = array(); foreach ($validNodes as $validNode) { if ($depth == 0) { $href = $this->server->getRequestUri(); } else { $href = $this->server->getRequestUri() . '/' . $validNode->getName(); } list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0); } $prefer = $this->server->getHTTPPRefer(); $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal'])); }
/** * parsePrincipalPropertySearchReportRequest * * This method parses the request body from a * {DAV:}principal-property-search report. * * This method returns an array with two elements: * 1. an array with properties to search on, and their values * 2. a list of propertyvalues that should be returned for the request. * * @param \DOMDocument $dom * @return array */ protected function parsePrincipalPropertySearchReportRequest($dom) { $httpDepth = $this->server->getHTTPDepth(0); if ($httpDepth !== 0) { throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); } $searchProperties = []; $applyToPrincipalCollectionSet = false; $test = $dom->firstChild->getAttribute('test') === 'anyof' ? 'anyof' : 'allof'; // Parsing the search request foreach ($dom->firstChild->childNodes as $searchNode) { if (DAV\XMLUtil::toClarkNotation($searchNode) == '{DAV:}apply-to-principal-collection-set') { $applyToPrincipalCollectionSet = true; } if (DAV\XMLUtil::toClarkNotation($searchNode) !== '{DAV:}property-search') { continue; } $propertyName = null; $propertyValue = null; foreach ($searchNode->childNodes as $childNode) { switch (DAV\XMLUtil::toClarkNotation($childNode)) { case '{DAV:}prop': $property = DAV\XMLUtil::parseProperties($searchNode); reset($property); $propertyName = key($property); break; case '{DAV:}match': $propertyValue = $childNode->textContent; break; } } if (is_null($propertyName) || is_null($propertyValue)) { throw new DAV\Exception\BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue); } $searchProperties[$propertyName] = $propertyValue; } return [$searchProperties, array_keys(DAV\XMLUtil::parseProperties($dom->firstChild)), $applyToPrincipalCollectionSet, $test]; }
/** * Locks an uri * * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type * of lock (shared or exclusive) and the owner of the lock * * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock * * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3 * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpLock(RequestInterface $request, ResponseInterface $response) { $uri = $request->getPath(); $existingLocks = $this->getLocks($uri); if ($body = $request->getBodyAsString()) { // This is a new lock request $existingLock = null; // Checking if there's already non-shared locks on the uri. foreach ($existingLocks as $existingLock) { if ($existingLock->scope === LockInfo::EXCLUSIVE) { throw new DAV\Exception\ConflictingLock($existingLock); } } $lockInfo = $this->parseLockRequest($body); $lockInfo->depth = $this->server->getHTTPDepth(); $lockInfo->uri = $uri; if ($existingLock && $lockInfo->scope != LockInfo::SHARED) { throw new DAV\Exception\ConflictingLock($existingLock); } } else { // Gonna check if this was a lock refresh. $existingLocks = $this->getLocks($uri); $conditions = $this->server->getIfConditions($request); $found = null; foreach ($existingLocks as $existingLock) { foreach ($conditions as $condition) { foreach ($condition['tokens'] as $token) { if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) { $found = $existingLock; break 3; } } } } // If none were found, this request is in error. if (is_null($found)) { if ($existingLocks) { throw new DAV\Exception\Locked(reset($existingLocks)); } else { throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); } } // This must have been a lock refresh $lockInfo = $found; // The resource could have been locked through another uri. if ($uri != $lockInfo->uri) { $uri = $lockInfo->uri; } } if ($timeout = $this->getTimeoutHeader()) { $lockInfo->timeout = $timeout; } $newFile = false; // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first try { $this->server->tree->getNodeForPath($uri); // We need to call the beforeWriteContent event for RFC3744 // Edit: looks like this is not used, and causing problems now. // // See Issue 222 // $this->server->emit('beforeWriteContent',array($uri)); } catch (DAV\Exception\NotFound $e) { // It didn't, lets create it $this->server->createFile($uri, fopen('php://memory', 'r')); $newFile = true; } $this->lockNode($uri, $lockInfo); $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $response->setHeader('Lock-Token', '<opaquelocktoken:' . $lockInfo->token . '>'); $response->setStatus($newFile ? 201 : 200); $response->setBody($this->generateLockResponse($lockInfo)); // Returning false will interupt the event chain and mark this method // as 'handled'. return false; }