/** * WebDAV MKCOL * * The MKCOL method is used to create a new collection (directory) on the server * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpMkcol(RequestInterface $request, ResponseInterface $response) { $requestBody = $request->getBodyAsString(); $path = $request->getPath(); if ($requestBody) { $contentType = $request->getHeader('Content-Type'); if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) { // We must throw 415 for unsupported mkcol bodies throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); } try { $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); } catch (\Sabre\Xml\ParseException $e) { throw new Exception\BadRequest($e->getMessage(), null, $e); } $properties = $mkcol->getProperties(); if (!isset($properties['{DAV:}resourcetype'])) { throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); } $resourceType = $properties['{DAV:}resourcetype']->getValue(); unset($properties['{DAV:}resourcetype']); } else { $properties = []; $resourceType = ['{DAV:}collection']; } $mkcol = new MkCol($resourceType, $properties); $result = $this->server->createCollection($path, $mkcol); if (is_array($result)) { $response->setStatus(207); $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $response->setBody($this->server->generateMultiStatus([$result])); } else { $response->setHeader('Content-Length', '0'); $response->setStatus(201); } // Sending back false will interupt the event chain and tell the server // we've handled this method. return false; }
/** * HTTP REPORT method implementation * * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) * It's used in a lot of extensions, so it made sense to implement it into the core. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpReport(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $body = $request->getBodyAsString(); $dom = XMLUtil::loadDOMDocument($body); $reportName = XMLUtil::toClarkNotation($dom->firstChild); if ($this->server->emit('report', [$reportName, $dom, $path])) { // If emit returned true, it means the report was not supported throw new Exception\ReportNotSupported(); } // Sending back false will interupt the event chain and tell the server // we've handled this method. return false; }
/** * This method is responsible for handling the 'ACL' event. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpAcl(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $body = $request->getBodyAsString(); $dom = DAV\XMLUtil::loadDOMDocument($body); $newAcl = Property\Acl::unserialize($dom->firstChild, $this->server->propertyMap)->getPrivileges(); // Normalizing urls foreach ($newAcl as $k => $newAce) { $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']); } $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IACL) { throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method'); } $oldAcl = $this->getACL($node); $supportedPrivileges = $this->getFlatPrivilegeSet($node); /* Checking if protected principals from the existing principal set are not overwritten. */ foreach ($oldAcl as $oldAce) { if (!isset($oldAce['protected']) || !$oldAce['protected']) { continue; } $found = false; foreach ($newAcl as $newAce) { if ($newAce['privilege'] === $oldAce['privilege'] && $newAce['principal'] === $oldAce['principal'] && $newAce['protected']) { $found = true; } } if (!$found) { throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request'); } } foreach ($newAcl as $newAce) { // Do we recognize the privilege if (!isset($supportedPrivileges[$newAce['privilege']])) { throw new Exception\NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server'); } if ($supportedPrivileges[$newAce['privilege']]['abstract']) { throw new Exception\NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege'); } // Looking up the principal try { $principal = $this->server->tree->getNodeForPath($newAce['principal']); } catch (DAV\Exception\NotFound $e) { throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist'); } if (!$principal instanceof IPrincipal) { throw new Exception\NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal'); } } $node->setACL($newAcl); $response->setStatus(200); // Breaking the event chain, because we handled this method. return false; }
/** * This function handles the MKCALENDAR HTTP method, which creates * a new calendar. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpMkCalendar(RequestInterface $request, ResponseInterface $response) { $body = $request->getBodyAsString(); $path = $request->getPath(); $properties = []; if ($body) { try { $mkcalendar = $this->server->xml->expect('{urn:ietf:params:xml:ns:caldav}mkcalendar', $body); } catch (\Sabre\Xml\ParseException $e) { throw new BadRequest($e->getMessage(), null, $e); } $properties = $mkcalendar->getProperties(); } // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored // subscriptions. Before that it used MKCOL which was the correct way // to do this. // // If the body had a {DAV:}resourcetype, it means we stumbled upon this // request, and we simply use it instead of the pre-defined list. if (isset($properties['{DAV:}resourcetype'])) { $resourceType = $properties['{DAV:}resourcetype']->getValue(); } else { $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; } $this->server->createCollection($path, new MkCol($resourceType, $properties)); $this->server->httpResponse->setStatus(201); $this->server->httpResponse->setHeader('Content-Length', 0); // This breaks the method chain. return false; }
/** * POST operation on Comments collections * * @param RequestInterface $request request object * @param ResponseInterface $response response object * @return null|false */ public function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof EntityCollection) { return null; } $data = $request->getBodyAsString(); $comment = $this->createComment($node->getName(), $node->getId(), $data, $request->getHeader('Content-Type')); // update read marker for the current user/poster to avoid // having their own comments marked as unread $node->setReadMarker(null); $url = $request->getUrl() . '/' . urlencode($comment->getId()); $response->setHeader('Content-Location', $url); // created $response->setStatus(201); return false; }
/** * 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; }
/** * We intercept this to handle POST requests on calendars. * * @param RequestInterface $request * @param ResponseInterface $response * @return null|bool */ function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Only handling xml $contentType = $request->getHeader('Content-Type'); if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) { return; } // Making sure the node exists try { $node = $this->server->tree->getNodeForPath($path); } catch (DAV\Exception\NotFound $e) { return; } $requestBody = $request->getBodyAsString(); // If this request handler could not deal with this POST request, it // will return 'null' and other plugins get a chance to handle the // request. // // However, we already requested the full body. This is a problem, // because a body can only be read once. This is why we preemptively // re-populated the request body with the existing data. $request->setBody($requestBody); $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); switch ($documentType) { // Dealing with the 'share' document, which modified invitees on a // calendar. case '{' . Plugin::NS_CALENDARSERVER . '}share': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-calendar-share'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $node->updateShares($message->set, $message->remove); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; // The invite-reply document is sent when the user replies to an // invitation of a calendar share. // The invite-reply document is sent when the user replies to an // invitation of a calendar share. case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply': // This only works on the calendar-home-root node. if (!$node instanceof CalendarHome) { return; } $this->server->transactionType = 'post-invite-reply'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $url = $node->shareReply($message->href, $message->status, $message->calendarUri, $message->inReplyTo, $message->summary); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); if ($url) { $writer = $this->server->xml->getWriter($this->server->getBaseUri()); $writer->openMemory(); $writer->startDocument(); $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as'); $writer->write(new Href($url)); $writer->endElement(); $response->setHeader('Content-Type', 'application/xml'); $response->setBody($writer->outputMemory()); } // Breaking the event chain return false; case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-publish-calendar'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $node->setPublishStatus(true); // iCloud sends back the 202, so we will too. $response->setStatus(202); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-unpublish-calendar'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $node->setPublishStatus(false); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; } }
/** * POST operation on system tag collections * * @param RequestInterface $request request object * @param ResponseInterface $response response object * @return null|false */ public function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Making sure the node exists try { $node = $this->server->tree->getNodeForPath($path); } catch (NotFound $e) { return null; } if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) { $data = $request->getBodyAsString(); $tag = $this->createTag($data, $request->getHeader('Content-Type')); if ($node instanceof SystemTagsObjectMappingCollection) { // also add to collection $node->createFile($tag->getId()); $url = $request->getBaseUrl() . 'systemtags/'; } else { $url = $request->getUrl(); } if ($url[strlen($url) - 1] !== '/') { $url .= '/'; } $response->setHeader('Content-Location', $url . $tag->getId()); // created $response->setStatus(201); return false; } }
/** * We intercept this to handle POST requests on calendars. * * @param RequestInterface $request * @param ResponseInterface $response * @return null|bool */ function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Only handling xml $contentType = $request->getHeader('Content-Type'); if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) { return; } // Making sure the node exists try { $node = $this->server->tree->getNodeForPath($path); } catch (DAV\Exception\NotFound $e) { return; } $requestBody = $request->getBodyAsString(); // If this request handler could not deal with this POST request, it // will return 'null' and other plugins get a chance to handle the // request. // // However, we already requested the full body. This is a problem, // because a body can only be read once. This is why we preemptively // re-populated the request body with the existing data. $request->setBody($requestBody); $dom = DAV\XMLUtil::loadDOMDocument($requestBody); $documentType = DAV\XMLUtil::toClarkNotation($dom->firstChild); switch ($documentType) { // Dealing with the 'share' document, which modified invitees on a // calendar. case '{' . Plugin::NS_CALENDARSERVER . '}share': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-calendar-share'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $mutations = $this->parseShareRequest($dom); $node->updateShares($mutations[0], $mutations[1]); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; // The invite-reply document is sent when the user replies to an // invitation of a calendar share. // The invite-reply document is sent when the user replies to an // invitation of a calendar share. case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply': // This only works on the calendar-home-root node. if (!$node instanceof CalendarHome) { return; } $this->server->transactionType = 'post-invite-reply'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $message = $this->parseInviteReplyRequest($dom); $url = $node->shareReply($message['href'], $message['status'], $message['calendarUri'], $message['inReplyTo'], $message['summary']); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); if ($url) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $root = $dom->createElement('cs:shared-as'); foreach ($this->server->xmlNamespaces as $namespace => $prefix) { $root->setAttribute('xmlns:' . $prefix, $namespace); } $dom->appendChild($root); $href = new DAV\Property\Href($url); $href->serialize($this->server, $root); $response->setHeader('Content-Type', 'application/xml'); $response->setBody($dom->saveXML()); } // Breaking the event chain return false; case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-publish-calendar'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $node->setPublishStatus(true); // iCloud sends back the 202, so we will too. $response->setStatus(202); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableCalendar) { return; } $this->server->transactionType = 'post-unpublish-calendar'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { $acl->checkPrivileges($path, '{DAV:}write'); } $node->setPublishStatus(false); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; } }
/** * We intercept this to handle POST requests on calendars. * * @param RequestInterface $request * @param ResponseInterface $response * @return null|false */ function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Only handling xml $contentType = $request->getHeader('Content-Type'); if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) { return; } // Making sure the node exists try { $node = $this->server->tree->getNodeForPath($path); } catch (NotFound $e) { return; } // CSRF protection $this->protectAgainstCSRF(); $requestBody = $request->getBodyAsString(); // If this request handler could not deal with this POST request, it // will return 'null' and other plugins get a chance to handle the // request. // // However, we already requested the full body. This is a problem, // because a body can only be read once. This is why we preemptively // re-populated the request body with the existing data. $request->setBody($requestBody); $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); switch ($documentType) { // Dealing with the 'share' document, which modified invitees on a // calendar. case '{' . self::NS_OWNCLOUD . '}share': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableAddressBook) { return; } $this->server->transactionType = 'post-oc-addressbook-share'; // Getting ACL info $acl = $this->server->getPlugin('acl'); // If there's no ACL support, we allow everything if ($acl) { /** @var \Sabre\DAVACL\Plugin $acl */ $acl->checkPrivileges($path, '{DAV:}write'); } $node->updateShares($message->set, $message->remove); $response->setStatus(200); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); // Breaking the event chain return false; } }
/** * This function handles the MKCALENDAR HTTP method, which creates * a new calendar. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpMkCalendar(RequestInterface $request, ResponseInterface $response) { // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support // for clients matching iCal in the user agent //$ua = $this->server->httpRequest->getHeader('User-Agent'); //if (strpos($ua,'iCal/')!==false) { // throw new \Sabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.'); //} $body = $request->getBodyAsString(); $path = $request->getPath(); $properties = []; if ($body) { $dom = DAV\XMLUtil::loadDOMDocument($body); foreach ($dom->firstChild->childNodes as $child) { if (DAV\XMLUtil::toClarkNotation($child) !== '{DAV:}set') { continue; } foreach (DAV\XMLUtil::parseProperties($child, $this->server->propertyMap) as $k => $prop) { $properties[$k] = $prop; } } } // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored // subscriptions. Before that it used MKCOL which was the correct way // to do this. // // If the body had a {DAV:}resourcetype, it means we stumbled upon this // request, and we simply use it instead of the pre-defined list. if (isset($properties['{DAV:}resourcetype'])) { $resourceType = $properties['{DAV:}resourcetype']->getValue(); } else { $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; } $this->server->createCollection($path, $resourceType, $properties); $this->server->httpResponse->setStatus(201); $this->server->httpResponse->setHeader('Content-Length', 0); // This breaks the method chain. return false; }
/** * Compute an HTTP POST method. * * @param Request $request HTTP request. * @param Response $response HTTP response. * @return bool * @throws Exception\Dav\Exception */ function httpPost(Request $request, Response $response) { if (System\Collection::NAME . '/' . Node::NAME !== $request->getPath()) { return; } $payload = @json_decode($request->getBodyAsString()); if (!$payload || !isset($payload->transport) || !isset($payload->username) || !isset($payload->password)) { throw new Exception\Dav\Exception('Payload is corrupted.'); } $this->configuration->mail = new StdClass(); $this->configuration->mail->transport = $payload->transport; $this->configuration->mail->username = $payload->username; $this->configuration->mail->password = $payload->password; $this->configuration->save(); $response->setHeader('Content-Type', 'application/json'); $response->setBody(json_encode(true)); return false; }
/** * This method handles the PROPFIND method. * * It's a very lazy method, it won't bother checking the request body * for which properties were requested, and just sends back a default * set of properties. * * @param RequestInterface $request * @param ResponseInterface $hR * @param string $tempLocation * @return bool */ function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation) { if (!file_exists($tempLocation)) { return true; } $hR->setHeader('X-Sabre-Temp', 'true'); $hR->setStatus(207); $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->parsePropFindRequest($request->getBodyAsString()); $properties = ['href' => $request->getPath(), 200 => ['{DAV:}getlastmodified' => new Property\GetLastModified(filemtime($tempLocation)), '{DAV:}getcontentlength' => filesize($tempLocation), '{DAV:}resourcetype' => new Property\ResourceType(null), '{' . Server::NS_SABREDAV . '}tempFile' => true]]; $data = $this->server->generateMultiStatus([$properties]); $hR->setBody($data); return false; }