/** * 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; }
/** * 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; } }
/** * Detects all unsupported clients and throws a \Sabre\DAV\Exception\Forbidden * exception which will result in a 403 to them. * @param RequestInterface $request * @throws \Sabre\DAV\Exception\Forbidden If the client version is not supported */ public function beforeHandler(RequestInterface $request) { $userAgent = $request->getHeader('User-Agent'); if ($userAgent === null) { return; } $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '1.7.0'); // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or // "mirall/%1" for older releases preg_match("/(?:mirall\\/)([\\d.]+)/i", $userAgent, $versionMatches); if (isset($versionMatches[1]) && version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) { throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.'); } }
/** * @param RequestInterface $request * @param ResponseInterface $response * @return array */ private function auth(RequestInterface $request, ResponseInterface $response) { if (\OC_User::handleApacheAuth() || $this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED)) || $this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) { $user = $this->userSession->getUser()->getUID(); \OC_Util::setupFS($user); $this->currentUser = $user; $this->session->close(); return [true, $this->principalPrefix . $user]; } if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) { // do not re-authenticate over ajax, use dummy auth name to prevent browser popup $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"'); $response->setStatus(401); throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); } return parent::check($request, $response); }
/** * 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)); } }
/** * @param RequestInterface $request * @param ResponseInterface $response * @return array * @throws NotAuthenticated */ private function auth(RequestInterface $request, ResponseInterface $response) { $forcedLogout = false; if (!$this->request->passesCSRFCheck() && $this->requiresCSRFCheck()) { // In case of a fail with POST we need to recheck the credentials if ($this->request->getMethod() === 'POST') { $forcedLogout = true; } else { $response->setStatus(401); throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.'); } } if ($forcedLogout) { $this->userSession->logout(); } else { if ($this->twoFactorManager->needsSecondFactor()) { throw new \Sabre\DAV\Exception\NotAuthenticated('2FA challenge not passed.'); } if (\OC_User::handleApacheAuth() || $this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED)) || $this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) { $user = $this->userSession->getUser()->getUID(); \OC_Util::setupFS($user); $this->currentUser = $user; $this->session->close(); return [true, $this->principalPrefix . $user]; } } if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) { // do not re-authenticate over ajax, use dummy auth name to prevent browser popup $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"'); $response->setStatus(401); throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); } $data = parent::check($request, $response); if ($data[0] === true) { $startPos = strrpos($data[1], '/') + 1; $user = $this->userSession->getUser()->getUID(); $data[1] = substr_replace($data[1], $user, $startPos); } return $data; }
/** * This event is triggered after GET requests. * * This is used to transform data into jCal, if this was requested. * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function httpAfterGet(RequestInterface $request, ResponseInterface $response) { if (strpos($response->getHeader('Content-Type'), 'text/vcard') === false) { return; } $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType); $newBody = $this->convertVCard($response->getBody(), $target); $response->setBody($newBody); $response->setHeader('Content-Type', $mimeType . '; charset=utf-8'); $response->setHeader('Content-Length', strlen($newBody)); }
/** * 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'); } $dom = XMLUtil::loadDOMDocument($requestBody); if (XMLUtil::toClarkNotation($dom->firstChild) !== '{DAV:}mkcol') { // We must throw 415 for unsupported mkcol bodies throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.'); } $properties = []; foreach ($dom->firstChild->childNodes as $childNode) { if (XMLUtil::toClarkNotation($childNode) !== '{DAV:}set') { continue; } $properties = array_merge($properties, XMLUtil::parseProperties($childNode, $this->server->propertyMap)); } 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']; } $result = $this->server->createCollection($path, $resourceType, $properties); 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; }
/** * 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; }
/** * 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; } }
/** * 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; }
/** * @return bool */ function httpGet(Request $request, Response $response) { if (System\Collection::NAME . '/' . Node::NAME !== $request->getPath()) { return; } $payload = ['current_version' => SABRE_KATANA_VERSION]; $extra = []; if (true === $request->hasHeader('Referer')) { $extra['referer'] = $request->getHeader('Referer'); } $updatesDotJson = Updater::getUpdateUrl(Updater::DEFAULT_UPDATE_SERVER, $extra); $versions = @file_get_contents($updatesDotJson); if (!empty($versions)) { $versions = json_decode($versions, true); $versionsToFetch = Updater::filterVersions($versions, SABRE_KATANA_VERSION, Updater::FORMAT_PHAR); $payload['next_versions'] = array_keys($versionsToFetch); } $response->setHeader('Content-Type', 'application/json'); $response->setBody(json_encode($payload)); return false; }
/** * We intercept this to handle POST requests on shared resources * * @param RequestInterface $request * @param ResponseInterface $response * @return null|bool */ function httpPost(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $contentType = $request->getHeader('Content-Type'); // We're only interested in the davsharing content type. if (strpos($contentType, 'application/davsharing+xml') === false) { return; } $message = $this->server->xml->parse($request->getBody(), $request->getUrl(), $documentType); switch ($documentType) { case '{DAV:}share-resource': $this->shareResource($path, $message->sharees); $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; default: throw new BadRequest('Unexpected document type: ' . $documentType . ' for this Content-Type'); } }
/** * We intercept this to handle POST requests on a dav resource. * * @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; } $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 IShareable) { return; } $this->server->transactionType = 'post-oc-resource-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; } }
/** * 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 event is triggered after GET requests. * * This is used to transform data into jCal, if this was requested. * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function httpAfterGet(RequestInterface $request, ResponseInterface $response) { if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) { return; } $result = HTTP\Util::negotiate($request->getHeader('Accept'), ['text/calendar', 'application/calendar+json']); if ($result !== 'application/calendar+json') { // Do nothing return; } // Transforming. $vobj = VObject\Reader::read($response->getBody()); $jsonBody = json_encode($vobj->jsonSerialize()); $response->setBody($jsonBody); $response->setHeader('Content-Type', 'application/calendar+json'); $response->setHeader('Content-Length', strlen($jsonBody)); }
/** * 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; } }
/** * 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; }
/** * 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; } }
/** * 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; }
/** * 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; }
/** * Returns the HTTP custom range update header * * This method returns null if there is no well-formed HTTP range request * header. It returns array(1) if it was an append request, array(2, * $start, $end) if it's a start and end range, lastly it's array(3, * $endoffset) if the offset was negative, and should be calculated from * the end of the file. * * Examples: * * null - invalid * [1] - append * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15 * [2,10,null] - update bytes 10 until the end of the patch body * [3,-5] - update from 5 bytes from the end of the file. * * @param RequestInterface $request * @return array|null */ function getHTTPUpdateRange(RequestInterface $request) { $range = $request->getHeader('X-Update-Range'); if (is_null($range)) { return null; } // Matching "Range: bytes=1234-5678: both numbers are optional if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) { return null; } if ($matches[1] === 'append') { return [self::RANGE_APPEND]; } elseif (strlen($matches[2]) > 0) { return [self::RANGE_START, $matches[2], $matches[3] ?: null]; } elseif ($matches[4]) { return [self::RANGE_END, $matches[4]]; } else { return null; } }
/** * This method checks the 'Schedule-Reply' header * and returns false if it's 'F', otherwise true. * * @param RequestInterface $request * @return bool */ private function scheduleReply(RequestInterface $request) { $scheduleReply = $request->getHeader('Schedule-Reply'); return $scheduleReply !== 'F'; }