/** * Intercepts GET requests on addressbook urls ending with ?export. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGet(RequestInterface $request, ResponseInterface $response) { $queryParams = $request->getQueryParameters(); if (!array_key_exists('export', $queryParams)) { return; } $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IAddressBook) { return; } $this->server->transactionType = 'get-addressbook-export'; // Checking ACL, if available. if ($aclPlugin = $this->server->getPlugin('acl')) { $aclPlugin->checkPrivileges($path, '{DAV:}read'); } $nodes = $this->server->getPropertiesForPath($path, ['{' . Plugin::NS_CARDDAV . '}address-data'], 1); $format = 'text/directory'; $output = null; $filenameExtension = null; switch ($format) { case 'text/directory': $output = $this->generateVCF($nodes); $filenameExtension = '.vcf'; break; } $filename = preg_replace('/[^a-zA-Z0-9-_ ]/um', '', $node->getName()); $filename .= '-' . date('Y-m-d') . $filenameExtension; $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); $response->setHeader('Content-Type', $format); $response->setStatus(200); $response->setBody($output); // Returning false to break the event chain return false; }
/** * Intercepts GET requests on addressbook urls ending with ?photo. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool|void */ function httpGet(RequestInterface $request, ResponseInterface $response) { $queryParams = $request->getQueryParameters(); // TODO: in addition to photo we should also add logo some point in time if (!array_key_exists('photo', $queryParams)) { return true; } $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof Card) { return true; } $this->server->transactionType = 'carddav-image-export'; // Checking ACL, if available. if ($aclPlugin = $this->server->getPlugin('acl')) { /** @var \Sabre\DAVACL\Plugin $aclPlugin */ $aclPlugin->checkPrivileges($path, '{DAV:}read'); } if ($result = $this->getPhoto($node)) { $response->setHeader('Content-Type', $result['Content-Type']); $response->setStatus(200); $response->setBody($result['body']); // Returning false to break the event chain return false; } return true; }
/** * Handles POST requests for tree operations not handled in the SabreDAV parent clas * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ public function httpPOSTExtra(RequestInterface $request, ResponseInterface $response) { $contentType = $request->getHeader('Content-Type'); list($contentType) = explode(';', $contentType); if ($contentType !== 'application/x-www-form-urlencoded' && $contentType !== 'multipart/form-data') { return; } $postVars = $request->getPostData(); if (!isset($postVars['sabreActionExtra'])) { return; } $uri = $request->getPath(); switch ($postVars['sabreActionExtra']) { case 'del': if (isset($postVars['path'])) { // Using basename() because we won't allow slashes list(, $Name) = \Sabre\HTTP\URLUtil::splitPath(trim($postVars['path'])); if (!empty($Name) && $this->config->browserplugin_enable_delete === true) { $this->server->tree->delete($uri . '/' . $Name); } } break; } $response->setHeader('Location', $request->getUrl()); $response->setStatus(302); return false; }
/** * Plugin that adds a 'Content-Disposition: attachment' header to all files * delivered by SabreDAV. * @param RequestInterface $request * @param ResponseInterface $response */ function httpGet(RequestInterface $request, ResponseInterface $response) { // Only handle valid files $node = $this->tree->getNodeForPath($request->getPath()); if (!$node instanceof IFile) { return; } $response->addHeader('Content-Disposition', 'attachment'); }
/** * Intercepts GET requests on calendar urls ending with ?export. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGet(RequestInterface $request, ResponseInterface $response) { $queryParams = $request->getQueryParameters(); if (!array_key_exists('export', $queryParams)) { return; } $path = $request->getPath(); $node = $this->server->getProperties($path, ['{DAV:}resourcetype', '{DAV:}displayname', '{http://sabredav.org/ns}sync-token', '{DAV:}sync-token', '{http://apple.com/ns/ical/}calendar-color']); if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) { return; } // Marking the transactionType, for logging purposes. $this->server->transactionType = 'get-calendar-export'; $properties = $node; $start = null; $end = null; $expand = false; $componentType = false; if (isset($queryParams['start'])) { if (!ctype_digit($queryParams['start'])) { throw new BadRequest('The start= parameter must contain a unix timestamp'); } $start = DateTime::createFromFormat('U', $queryParams['start']); } if (isset($queryParams['end'])) { if (!ctype_digit($queryParams['end'])) { throw new BadRequest('The end= parameter must contain a unix timestamp'); } $end = DateTime::createFromFormat('U', $queryParams['end']); } if (isset($queryParams['expand']) && !!$queryParams['expand']) { if (!$start || !$end) { throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.'); } $expand = true; $componentType = 'VEVENT'; } if (isset($queryParams['componentType'])) { if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) { throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here'); } $componentType = $queryParams['componentType']; } $format = \Sabre\HTTP\Util::Negotiate($request->getHeader('Accept'), ['text/calendar', 'application/calendar+json']); if (isset($queryParams['accept'])) { if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') { $format = 'application/calendar+json'; } } if (!$format) { $format = 'text/calendar'; } $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response); // Returning false to break the event chain return false; }
/** * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGet(RequestInterface $request, ResponseInterface $response) { $node = $this->server->tree->getNodeForPath($request->getPath()); if ($node instanceof DAV\IFile) { return; } $subRequest = clone $request; $subRequest->setMethod('PROPFIND'); $this->server->invokeMethod($subRequest, $response); 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 (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); $dom = XMLUtil::loadDOMDocument($requestBody); $documentType = XMLUtil::toClarkNotation($dom->firstChild); switch ($documentType) { // Dealing with the 'share' document, which modified invitees on a // calendar. case '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}share': // We can only deal with IShareableCalendar objects if (!$node instanceof IShareableAddressBook) { 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; } }
public function releaseLock(RequestInterface $request) { if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { return; } try { $node = $this->server->tree->getNodeForPath($request->getPath()); } catch (NotFound $e) { return; } if ($node instanceof Node) { $node->releaseLock(ILockingProvider::LOCK_SHARED); } }
public function releaseLock(RequestInterface $request) { if ($request->getMethod() !== 'PUT') { return; } try { $node = $this->tree->getNodeForPath($request->getPath()); } catch (NotFound $e) { return; } if ($node instanceof Node) { $node->releaseLock(ILockingProvider::LOCK_SHARED); } }
/** * Intercepts GET requests on addressbook urls ending with ?export. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpGet(RequestInterface $request, ResponseInterface $response) { $queryParams = $request->getQueryParameters(); if (!array_key_exists('export', $queryParams)) { return; } $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IAddressBook) { return; } $this->server->transactionType = 'get-addressbook-export'; // Checking ACL, if available. if ($aclPlugin = $this->server->getPlugin('acl')) { $aclPlugin->checkPrivileges($path, '{DAV:}read'); } $response->setHeader('Content-Type', 'text/directory'); $response->setStatus(200); $nodes = $this->server->getPropertiesForPath($path, ['{' . Plugin::NS_CARDDAV . '}address-data'], 1); $response->setBody($this->generateVCF($nodes)); // Returning false to break the event chain return false; }
/** * Fakes a successful LOCK * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ public function fakeLockProvider(RequestInterface $request, ResponseInterface $response) { $lockInfo = new LockInfo(); $lockInfo->token = md5($request->getPath()); $lockInfo->uri = $request->getPath(); $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; $lockInfo->timeout = 1800; $body = $this->server->xml->write('{DAV:}prop', ['{DAV:}lockdiscovery' => new LockDiscovery([$lockInfo])]); $response->setBody($body); 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; }
/** * 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; } $hR->setHeader('X-Sabre-Temp', 'true'); $hR->setStatus(207); $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); $properties = ['href' => $request->getPath(), 200 => ['{DAV:}getlastmodified' => new Xml\Property\GetLastModified(filemtime($tempLocation)), '{DAV:}getcontentlength' => filesize($tempLocation), '{DAV:}resourcetype' => new Xml\Property\ResourceType(null), '{' . Server::NS_SABREDAV . '}tempFile' => true]]; $data = $this->server->generateMultiStatus([$properties]); $hR->setBody($data); 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; }
/** * Handles POST requests for tree operations. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpPOST(RequestInterface $request, ResponseInterface $response) { $contentType = $request->getHeader('Content-Type'); list($contentType) = explode(';', $contentType); if ($contentType !== 'application/x-www-form-urlencoded' && $contentType !== 'multipart/form-data') { return; } $postVars = $request->getPostData(); if (!isset($postVars['sabreAction'])) { return; } $uri = $request->getPath(); if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { switch ($postVars['sabreAction']) { case 'mkcol': if (isset($postVars['name']) && trim($postVars['name'])) { // Using basename() because we won't allow slashes list(, $folderName) = URLUtil::splitPath(trim($postVars['name'])); if (isset($postVars['resourceType'])) { $resourceType = explode(',', $postVars['resourceType']); } else { $resourceType = ['{DAV:}collection']; } $properties = []; foreach ($postVars as $varName => $varValue) { // Any _POST variable in clark notation is treated // like a property. if ($varName[0] === '{') { // PHP will convert any dots to underscores. // This leaves us with no way to differentiate // the two. // Therefore we replace the string *DOT* with a // real dot. * is not allowed in uris so we // should be good. $varName = str_replace('*DOT*', '.', $varName); $properties[$varName] = $varValue; } } $mkCol = new MkCol($resourceType, $properties); $this->server->createCollection($uri . '/' . $folderName, $mkCol); } break; // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart case 'put': if ($_FILES) { $file = current($_FILES); } else { break; } list(, $newName) = URLUtil::splitPath(trim($file['name'])); if (isset($postVars['name']) && trim($postVars['name'])) { $newName = trim($postVars['name']); } // Making sure we only have a 'basename' component list(, $newName) = URLUtil::splitPath($newName); if (is_uploaded_file($file['tmp_name'])) { $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'], 'r')); } break; // @codeCoverageIgnoreEnd } } $response->setHeader('Location', $request->getUrl()); $response->setStatus(302); return false; }
/** * Patch an uri * * The WebDAV patch request can be used to modify only a part of an * existing resource. If the resource does not exist yet and the first * offset is not 0, the request fails * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function httpPatch(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Get the node. Will throw a 404 if not found $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IFile && !$node instanceof IPatchSupport) { throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.'); } $range = $this->getHTTPUpdateRange($request); if (!$range) { throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers'); } $contentType = strtolower($request->getHeader('Content-Type')); if ($contentType != 'application/x-sabredav-partialupdate') { throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"'); } $len = $this->server->httpRequest->getHeader('Content-Length'); if (!$len) { throw new DAV\Exception\LengthRequired('A Content-Length header is required'); } switch ($range[0]) { case self::RANGE_START: // Calculate the end-range if it doesn't exist. if (!$range[2]) { $range[2] = $range[1] + $len - 1; } else { if ($range[2] < $range[1]) { throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[2] . ') is lower than the start offset (' . $range[1] . ')'); } if ($range[2] - $range[1] + 1 != $len) { throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[1] . ') and end (' . $range[2] . ') offsets'); } } break; } if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) { return; } $body = $this->server->httpRequest->getBody(); if ($node instanceof IPatchSupport) { $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null); } else { // The old interface switch ($range[0]) { case self::RANGE_APPEND: throw new DAV\Exception\NotImplemented('This node does not support the append syntax. Please upgrade it to IPatchSupport'); case self::RANGE_START: $etag = $node->putRange($body, $range[1]); break; case self::RANGE_END: throw new DAV\Exception\NotImplemented('This node does not support the end-range syntax. Please upgrade it to IPatchSupport'); break; } } $this->server->emit('afterWriteContent', [$path, $node]); $response->setHeader('Content-Length', '0'); if ($etag) { $response->setHeader('ETag', $etag); } $response->setStatus(204); // Breaks the event chain return false; }
/** * This method is created to extract information from the WebDAV HTTP 'If:' header * * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information * The function will return an array, containing structs with the following keys * * * uri - the uri the condition applies to. * * tokens - The lock token. another 2 dimensional array containing 3 elements * * Example 1: * * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) * * Would result in: * * [ * [ * 'uri' => '/request/uri', * 'tokens' => [ * [ * [ * 'negate' => false, * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', * 'etag' => "" * ] * ] * ], * ] * ] * * Example 2: * * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) * * Would result in: * * [ * [ * 'uri' => 'path', * 'tokens' => [ * [ * [ * 'negate' => true, * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', * 'etag' => '"Im An ETag"' * ], * [ * 'negate' => false, * 'token' => '', * 'etag' => '"Another ETag"' * ] * ] * ], * ], * [ * 'uri' => 'path2', * 'tokens' => [ * [ * [ * 'negate' => true, * 'token' => '', * 'etag' => '"Path2 ETag"' * ] * ] * ], * ], * ] * * @return array */ function getIfConditions(RequestInterface $request) { $header = $request->getHeader('If'); if (!$header) return []; $matches = []; $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; preg_match_all($regex, $header, $matches, PREG_SET_ORDER); $conditions = []; foreach ($matches as $match) { // If there was no uri specified in this match, and there were // already conditions parsed, we add the condition to the list of // conditions for the previous uri. if (!$match['uri'] && count($conditions)) { $conditions[count($conditions) - 1]['tokens'][] = [ 'negate' => $match['not'] ? true : false, 'token' => $match['token'], 'etag' => isset($match['etag']) ? $match['etag'] : '' ]; } else { if (!$match['uri']) { $realUri = $request->getPath(); } else { $realUri = $this->calculateUri($match['uri']); } $conditions[] = [ 'uri' => $realUri, 'tokens' => [ [ 'negate' => $match['not'] ? true : false, 'token' => $match['token'], 'etag' => isset($match['etag']) ? $match['etag'] : '' ] ], ]; } } return $conditions; }
/** * This method handles POST requests to the schedule-outbox. * * Currently, two types of requests are support: * * FREEBUSY requests from RFC 6638 * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 * * The latter is from an expired early draft of the CalDAV scheduling * extensions, but iCal depends on a feature from that spec, so we * implement it. * * @param IOutbox $outboxNode * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) { $outboxPath = $request->getPath(); // Parsing the request body try { $vObject = VObject\Reader::read($request->getBody()); } catch (VObject\ParseException $e) { throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage()); } // The incoming iCalendar object must have a METHOD property, and a // component. The combination of both determines what type of request // this is. $componentType = null; foreach ($vObject->getComponents() as $component) { if ($component->name !== 'VTIMEZONE') { $componentType = $component->name; break; } } if (is_null($componentType)) { throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); } // Validating the METHOD $method = strtoupper((string) $vObject->METHOD); if (!$method) { throw new BadRequest('A METHOD property must be specified in iTIP messages'); } // So we support one type of request: // // REQUEST with a VFREEBUSY component $acl = $this->server->getPlugin('acl'); if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy'); $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); } else { throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint'); } }
/** * Handles POST requests for tree operations. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpPOST(RequestInterface $request, ResponseInterface $response) { $contentType = $request->getHeader('Content-Type'); list($contentType) = explode(';', $contentType); if ($contentType !== 'application/x-www-form-urlencoded' && $contentType !== 'multipart/form-data') { return; } $postVars = $request->getPostData(); if (!isset($postVars['sabreAction'])) { return; } $uri = $request->getPath(); if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { switch ($postVars['sabreAction']) { case 'mkcol': if (isset($postVars['name']) && trim($postVars['name'])) { // Using basename() because we won't allow slashes list(, $folderName) = URLUtil::splitPath(trim($postVars['name'])); $this->server->createDirectory($uri . '/' . $folderName); } break; // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart case 'put': if ($_FILES) { $file = current($_FILES); } else { break; } list(, $newName) = URLUtil::splitPath(trim($file['name'])); if (isset($postVars['name']) && trim($postVars['name'])) { $newName = trim($postVars['name']); } // Making sure we only have a 'basename' component list(, $newName) = URLUtil::splitPath($newName); if (is_uploaded_file($file['tmp_name'])) { $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'], 'r')); } break; // @codeCoverageIgnoreEnd } } $response->setHeader('Location', $request->getUrl()); $response->setStatus(302); return false; }
/** * This event is triggered before the usual GET request handler. * * We use this to intercept GET calls to notification nodes, and return the * proper response. * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function httpGet(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); try { $node = $this->server->tree->getNodeForPath($path); } catch (DAV\Exception\NotFound $e) { return; } if (!$node instanceof INode) { return; } $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $root = $dom->createElement('cs:notification'); foreach ($this->server->xmlNamespaces as $namespace => $prefix) { $root->setAttribute('xmlns:' . $prefix, $namespace); } $dom->appendChild($root); $node->getNotificationType()->serializeBody($this->server, $root); $response->setHeader('Content-Type', 'application/xml'); $response->setHeader('ETag', $node->getETag()); $response->setStatus(200); $response->setBody($dom->saveXML()); // Return false to break the event chain. return false; }
/** * Fakes a successful LOCK * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ public function fakeLockProvider(RequestInterface $request, ResponseInterface $response) { $dom = new \DOMDocument('1.0', 'utf-8'); $prop = $dom->createElementNS('DAV:', 'd:prop'); $dom->appendChild($prop); $lockDiscovery = $dom->createElementNS('DAV:', 'd:lockdiscovery'); $prop->appendChild($lockDiscovery); $lockInfo = new LockInfo(); $lockInfo->token = md5($request->getPath()); $lockInfo->uri = $request->getPath(); $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; $lockInfo->timeout = 1800; $lockObj = new LockDiscovery([$lockInfo]); $lockObj->serialize($this->server, $lockDiscovery); $response->setBody($dom->saveXML()); 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); $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; } }
/** * 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(); $result = $this->server->xml->parse($request->getBody(), $request->getUrl(), $rootElementName); if ($this->server->emit('report', [$rootElementName, $result, $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; }
/** * 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; }
/** * The validateTokens event is triggered before every request. * * It's a moment where this plugin can check all the supplied lock tokens * in the If: header, and check if they are valid. * * In addition, it will also ensure that it checks any missing lokens that * must be present in the request, and reject requests without the proper * tokens. * * @param RequestInterface $request * @param mixed $conditions * @return void */ function validateTokens(RequestInterface $request, &$conditions) { // First we need to gather a list of locks that must be satisfied. $mustLocks = []; $method = $request->getMethod(); // Methods not in that list are operations that doesn't alter any // resources, and we don't need to check the lock-states for. switch ($method) { case 'DELETE': $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), true)); break; case 'MKCOL': case 'MKCALENDAR': case 'PROPPATCH': case 'PUT': case 'PATCH': $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), false)); break; case 'MOVE': $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), true)); $mustLocks = array_merge($mustLocks, $this->getLocks($this->server->calculateUri($request->getHeader('Destination')), false)); break; case 'COPY': $mustLocks = array_merge($mustLocks, $this->getLocks($this->server->calculateUri($request->getHeader('Destination')), false)); break; case 'LOCK': //Temporary measure.. figure out later why this is needed // Here we basically ignore all incoming tokens... foreach ($conditions as $ii => $condition) { foreach ($condition['tokens'] as $jj => $token) { $conditions[$ii]['tokens'][$jj]['validToken'] = true; } } return; } // It's possible that there's identical locks, because of shared // parents. We're removing the duplicates here. $tmp = []; foreach ($mustLocks as $lock) { $tmp[$lock->token] = $lock; } $mustLocks = array_values($tmp); foreach ($conditions as $kk => $condition) { foreach ($condition['tokens'] as $ii => $token) { // Lock tokens always start with opaquelocktoken: if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') { continue; } $checkToken = substr($token['token'], 16); // Looping through our list with locks. foreach ($mustLocks as $jj => $mustLock) { if ($mustLock->token == $checkToken) { // We have a match! // Removing this one from mustlocks unset($mustLocks[$jj]); // Marking the condition as valid. $conditions[$kk]['tokens'][$ii]['validToken'] = true; // Advancing to the next token continue 2; } } // If we got here, it means that there was a // lock-token, but it was not in 'mustLocks'. // // This is an edge-case, as it could mean that token // was specified with a url that was not 'required' to // check. So we're doing one extra lookup to make sure // we really don't know this token. // // This also gets triggered when the user specified a // lock-token that was expired. $oddLocks = $this->getLocks($condition['uri']); foreach ($oddLocks as $oddLock) { if ($oddLock->token === $checkToken) { // We have a hit! $conditions[$kk]['tokens'][$ii]['validToken'] = true; continue 2; } } // If we get all the way here, the lock-token was // really unknown. } } // If there's any locks left in the 'mustLocks' array, it means that // the resource was locked and we must block it. if ($mustLocks) { throw new DAV\Exception\Locked(reset($mustLocks)); } }
/** * 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 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; }
/** * 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; }
/** * 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; } }