/** * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request * * @param string $method * @return bool */ public function httpGetInterceptor($method) { if ($method != 'GET') { return true; } $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); if ($node instanceof Sabre_DAV_IFile) { return true; } $this->server->httpPropFind(); return false; }
/** * This function handles the calendar-query REPORT * * This report is used by clients to request calendar objects based on * complex conditions. * * @param DOMNode $dom * @return void */ public function calendarQueryReport($dom) { $requestedProperties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); $filterNode = $dom->getElementsByTagNameNS('urn:ietf:params:xml:ns:caldav', 'filter'); if ($filterNode->length !== 1) { throw new Sabre_DAV_Exception_BadRequest('The calendar-query report must have a filter element'); } $filters = Sabre_CalDAV_XMLUtil::parseCalendarQueryFilters($filterNode->item(0)); $requestedCalendarData = true; if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { // We always retrieve calendar-data, as we need it for filtering. $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; // If calendar-data wasn't explicitly requested, we need to remove // it after processing. $requestedCalendarData = false; } // These are the list of nodes that potentially match the requirement $candidateNodes = $this->server->getPropertiesForPath($this->server->getRequestUri(), $requestedProperties, $this->server->getHTTPDepth(0)); $verifiedNodes = array(); foreach ($candidateNodes as $node) { // If the node didn't have a calendar-data property, it must not be a calendar object if (!isset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { continue; } if ($this->validateFilters($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'], $filters)) { if (!$requestedCalendarData) { unset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); } $verifiedNodes[] = $node; } } $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->sendBody($this->server->generateMultiStatus($verifiedNodes)); }
protected function principalPropertySearchReport($dom) { $searchableProperties = array('{DAV:}displayname' => 'display name'); list($searchProperties, $requestedProperties) = $this->parsePrincipalPropertySearchReportRequest($dom); $uri = $this->server->getRequestUri(); $result = array(); $lookupResults = $this->server->getPropertiesForPath($uri, array_keys($searchProperties), 1); // The first item in the results is the parent, so we get rid of it. array_shift($lookupResults); $matches = array(); foreach ($lookupResults as $lookupResult) { foreach ($searchProperties as $searchProperty => $searchValue) { if (!isset($searchableProperties[$searchProperty])) { throw new Sabre_DAV_Exception_BadRequest('Searching for ' . $searchProperty . ' is not supported'); } if (isset($lookupResult[200][$searchProperty]) && mb_stripos($lookupResult[200][$searchProperty], $searchValue, 0, 'UTF-8') !== false) { $matches[] = $lookupResult['href']; } } } $matchProperties = array(); foreach ($matches as $match) { list($result) = $this->server->getPropertiesForPath($match, $requestedProperties, 0); $matchProperties[] = $result; } $xml = $this->server->generateMultiStatus($matchProperties); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->sendBody($xml); }
/** * Handles POST requests for tree operations * * This method is not yet used. * * @param string $method * @return bool */ public function httpPOSTHandler($method) { if ($method != 'POST') { return true; } if (isset($_POST['action'])) { switch ($_POST['action']) { case 'mkcol': if (isset($_POST['name']) && trim($_POST['name'])) { // Using basename() because we won't allow slashes $folderName = trim(basename($_POST['name'])); $this->server->createDirectory($this->server->getRequestUri() . '/' . $folderName); } break; case 'put': if ($_FILES) { $file = current($_FILES); } else { break; } $newName = basename($file['name']); if (isset($_POST['name']) && trim($_POST['name'])) { $newName = trim(basename($_POST['name'])); } if (is_uploaded_file($file['tmp_name'])) { $parent = $this->server->tree->getNodeForPath(trim($this->server->getRequestUri(), '/')); $parent->createFile($newName, fopen($file['tmp_name'], 'r')); } } } $this->server->httpResponse->setHeader('Location', $this->server->httpRequest->getUri()); return false; }
/** * principalPropertySearchReport * * This method is responsible for handing the * {DAV:}principal-property-search report. This report can be used for * clients to search for groups of principals, based on the value of one * or more properties. * * @param DOMDocument $dom * @return void */ protected function principalPropertySearchReport(DOMDocument $dom) { list($searchProperties, $requestedProperties, $applyToPrincipalCollectionSet) = $this->parsePrincipalPropertySearchReportRequest($dom); $uri = null; if (!$applyToPrincipalCollectionSet) { $uri = $this->server->getRequestUri(); } $result = $this->principalSearch($searchProperties, $requestedProperties, $uri); $xml = $this->server->generateMultiStatus($result); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->sendBody($xml); }
/** * 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 string $tempLocation * @return void */ public function httpPropfind($tempLocation) { if (!file_exists($tempLocation)) { return true; } $hR = $this->server->httpResponse; $hR->setHeader('X-Sabre-Temp', 'true'); $hR->sendStatus(207); $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); $requestedProps = $this->server->parsePropFindRequest($this->server->httpRequest->getBody(true)); $properties = array('href' => $this->server->getRequestUri(), 200 => array('{DAV:}getlastmodified' => new Sabre_DAV_Property_GetLastModified(filemtime($tempLocation)), '{DAV:}getcontentlength' => filesize($tempLocation), '{DAV:}resourcetype' => new Sabre_DAV_Property_ResourceType(null), '{http://www.rooftopsolutions.nl/NS/sabredav}tempFile' => true)); $data = $this->server->generateMultiStatus(array($properties)); $hR->sendBody($data); return false; }
/** * This method is responsible for parsing the request and generating the * response for the CALDAV:free-busy-query REPORT. * * @param DOMNode $dom * @return void */ protected function freeBusyQueryReport(DOMNode $dom) { $start = null; $end = null; foreach ($dom->firstChild->childNodes as $childNode) { $clark = Sabre_DAV_XMLUtil::toClarkNotation($childNode); if ($clark == '{' . self::NS_CALDAV . '}time-range') { $start = $childNode->getAttribute('start'); $end = $childNode->getAttribute('end'); break; } } if ($start) { $start = Sabre_VObject_DateTimeParser::parseDateTime($start); } if ($end) { $end = Sabre_VObject_DateTimeParser::parseDateTime($end); } if (!$start && !$end) { throw new Sabre_DAV_Exception_BadRequest('The freebusy report must have a time-range filter'); } $acl = $this->server->getPlugin('acl'); if (!$acl) { throw new Sabre_DAV_Exception('The ACL plugin must be loaded for free-busy queries to work'); } $uri = $this->server->getRequestUri(); $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy'); $calendar = $this->server->tree->getNodeForPath($uri); if (!$calendar instanceof Sabre_CalDAV_ICalendar) { throw new Sabre_DAV_Exception_NotImplemented('The free-busy-query REPORT is only implemented on calendars'); } $objects = array_map(function ($child) { $obj = $child->get(); if (is_resource($obj)) { $obj = stream_get_contents($obj); } return $obj; }, $calendar->getChildren()); $generator = new Sabre_VObject_FreeBusyGenerator(); $generator->setObjects($objects); $generator->setTimeRange($start, $end); $result = $generator->getResult(); $result = $result->serialize(); $this->server->httpResponse->sendStatus(200); $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); $this->server->httpResponse->setHeader('Content-Length', strlen($result)); $this->server->httpResponse->sendBody($result); }
/** * This method is responsible for parsing the request and generating the * response for the CALDAV:free-busy-query REPORT. * * @param DOMNode $dom * @return void */ protected function freeBusyQueryReport(DOMNode $dom) { $start = null; $end = null; foreach ($dom->firstChild->childNodes as $childNode) { $clark = Sabre_DAV_XMLUtil::toClarkNotation($childNode); if ($clark == '{' . self::NS_CALDAV . '}time-range') { $start = $childNode->getAttribute('start'); $end = $childNode->getAttribute('end'); break; } } if ($start) { $start = VObject\DateTimeParser::parseDateTime($start); } if ($end) { $end = VObject\DateTimeParser::parseDateTime($end); } if (!$start && !$end) { throw new Sabre_DAV_Exception_BadRequest('The freebusy report must have a time-range filter'); } $acl = $this->server->getPlugin('acl'); if (!$acl) { throw new Sabre_DAV_Exception('The ACL plugin must be loaded for free-busy queries to work'); } $uri = $this->server->getRequestUri(); $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy'); $calendar = $this->server->tree->getNodeForPath($uri); if (!$calendar instanceof Sabre_CalDAV_ICalendar) { throw new Sabre_DAV_Exception_NotImplemented('The free-busy-query REPORT is only implemented on calendars'); } // Doing a calendar-query first, to make sure we get the most // performance. $urls = $calendar->calendarQuery(array('name' => 'VCALENDAR', 'comp-filters' => array(array('name' => 'VEVENT', 'comp-filters' => array(), 'prop-filters' => array(), 'is-not-defined' => false, 'time-range' => array('start' => $start, 'end' => $end))), 'prop-filters' => array(), 'is-not-defined' => false, 'time-range' => null)); $objects = array_map(function ($url) use($calendar) { $obj = $calendar->getChild($url)->get(); return $obj; }, $urls); $generator = new VObject\FreeBusyGenerator(); $generator->setObjects($objects); $generator->setTimeRange($start, $end); $result = $generator->getResult(); $result = $result->serialize(); $this->server->httpResponse->sendStatus(200); $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); $this->server->httpResponse->setHeader('Content-Length', strlen($result)); $this->server->httpResponse->sendBody($result); }
/** * principalPropertySearchReport * * This method is reponsible for handing the * {DAV:}principal-property-search report. This report can be used for * clients to search for groups of principals, based on the value of one * or more properties. * * @param DOMDocument $dom * @return void */ protected function principalPropertySearchReport(DOMDocument $dom) { $searchableProperties = array('{DAV:}displayname' => 'display name'); list($searchProperties, $requestedProperties, $applyToPrincipalCollectionSet) = $this->parsePrincipalPropertySearchReportRequest($dom); $result = array(); if ($applyToPrincipalCollectionSet) { $uris = array(); } else { $uris = array($this->server->getRequestUri()); } $lookupResults = array(); foreach ($uris as $uri) { $p = array_keys($searchProperties); $p[] = '{DAV:}resourcetype'; $r = $this->server->getPropertiesForPath($uri, $p, 1); // The first item in the results is the parent, so we get rid of it. array_shift($r); $lookupResults = array_merge($lookupResults, $r); } $matches = array(); foreach ($lookupResults as $lookupResult) { // We're only looking for principals if (!isset($lookupResult[200]['{DAV:}resourcetype']) || !$lookupResult[200]['{DAV:}resourcetype'] instanceof Sabre_DAV_Property_ResourceType || !$lookupResult[200]['{DAV:}resourcetype']->is('{DAV:}principal')) { continue; } foreach ($searchProperties as $searchProperty => $searchValue) { if (!isset($searchableProperties[$searchProperty])) { // If a property is not 'searchable', the spec dictates // this is not a match. continue; } if (isset($lookupResult[200][$searchProperty]) && mb_stripos($lookupResult[200][$searchProperty], $searchValue, 0, 'UTF-8') !== false) { $matches[] = $lookupResult['href']; } } } $matchProperties = array(); foreach ($matches as $match) { list($result) = $this->server->getPropertiesForPath($match, $requestedProperties, 0); $matchProperties[] = $result; } $xml = $this->server->generateMultiStatus($matchProperties); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->sendBody($xml); }
/** * 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 Sabre_CardDAV_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 Sabre_CardDAV_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'])); }
/** * This method allows the exception to return any extra HTTP response headers. * * The headers must be returned as an array. * * @return array */ public function getHTTPHeaders(Sabre_DAV_Server $server) { $methods = $server->getAllowedMethods($server->getRequestUri()); return array('Allow' => strtoupper(implode(', ', $methods))); }
/** * validateLock should be called when a write operation is about to happen * It will check if the requested url is locked, and see if the correct lock tokens are passed * * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri * @param mixed $lastLock This variable will be populated with the last checked lock object (Sabre_DAV_Locks_LockInfo) * @return bool */ protected function validateLock($urls = null, &$lastLock = null) { if (is_null($urls)) { $urls = array($this->server->getRequestUri()); } elseif (is_string($urls)) { $urls = array($urls); } elseif (!is_array($urls)) { throw new Sabre_DAV_Exception('The urls parameter should either be null, a string or an array'); } $conditions = $this->getIfConditions(); // We're going to loop through the urls and make sure all lock conditions are satisfied foreach ($urls as $url) { $locks = $this->getLocks($url); // If there were no conditions, but there were locks, we fail if (!$conditions && $locks) { reset($locks); $lastLock = current($locks); return false; } // If there were no locks or conditions, we go to the next url if (!$locks && !$conditions) { continue; } foreach ($conditions as $condition) { $conditionUri = $condition['uri'] ? $this->server->calculateUri($condition['uri']) : ''; // If the condition has a url, and it isn't part of the affected url at all, check the next condition if ($conditionUri && strpos($url, $conditionUri) !== 0) { continue; } // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken // At least 1 condition has to be satisfied foreach ($condition['tokens'] as $conditionToken) { $etagValid = true; $lockValid = true; // key 2 can contain an etag if ($conditionToken[2]) { $uri = $conditionUri ? $conditionUri : $this->server->getRequestUri(); $node = $this->server->tree->getNodeForPath($uri); $etagValid = $node->getETag() == $conditionToken[2]; } // key 1 can contain a lock token if ($conditionToken[1]) { $lockValid = false; // Match all the locks foreach ($locks as $lockIndex => $lock) { $lockToken = 'opaquelocktoken:' . $lock->token; // Checking NOT if (!$conditionToken[0] && $lockToken != $conditionToken[1]) { // Condition valid, onto the next $lockValid = true; break; } if ($conditionToken[0] && $lockToken == $conditionToken[1]) { $lastLock = $lock; // Condition valid and lock matched unset($locks[$lockIndex]); $lockValid = true; break; } } } // If, after checking both etags and locks they are stil valid, // we can continue with the next condition. if ($etagValid && $lockValid) { continue 2; } } // No conditions matched, so we fail throw new Sabre_DAV_Exception_PreconditionFailed('The tokens provided in the if header did not match', 'If'); } // Conditions were met, we'll also need to check if all the locks are gone if (count($locks)) { reset($locks); // There's still locks, we fail $lastLock = current($locks); return false; } } // We got here, this means every condition was satisfied return true; }