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); } }
/** * This method checks the main HTTP preconditions. * * Currently these are: * * If-Match * * If-None-Match * * If-Modified-Since * * If-Unmodified-Since * * The method will return true if all preconditions are met * The method will return false, or throw an exception if preconditions * failed. If false is returned the operation should be aborted, and * the appropriate HTTP response headers are already set. * * Normally this method will throw 412 Precondition Failed for failures * related to If-None-Match, If-Match and If-Unmodified Since. It will * set the status to 304 Not Modified for If-Modified_since. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function checkPreconditions(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $node = null; $lastMod = null; $etag = null; if ($ifMatch = $request->getHeader('If-Match')) { // If-Match contains an entity tag. Only if the entity-tag // matches we are allowed to make the request succeed. // If the entity-tag is '*' we are only allowed to make the // request succeed if a resource exists at that url. try { $node = $this->tree->getNodeForPath($path); } catch (Exception\NotFound $e) { throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); } // Only need to check entity tags if they are not * if ($ifMatch !== '*') { // There can be multiple ETags $ifMatch = explode(',', $ifMatch); $haveMatch = false; foreach ($ifMatch as $ifMatchItem) { // Stripping any extra spaces $ifMatchItem = trim($ifMatchItem, ' '); $etag = $node instanceof IFile ? $node->getETag() : null; if ($etag === $ifMatchItem) { $haveMatch = true; } else { // Evolution has a bug where it sometimes prepends the " // with a \. This is our workaround. if (str_replace('\\"', '"', $ifMatchItem) === $etag) { $haveMatch = true; } } } if (!$haveMatch) { if ($etag) $response->setHeader('ETag', $etag); throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); } } } if ($ifNoneMatch = $request->getHeader('If-None-Match')) { // The If-None-Match header contains an ETag. // Only if the ETag does not match the current ETag, the request will succeed // The header can also contain *, in which case the request // will only succeed if the entity does not exist at all. $nodeExists = true; if (!$node) { try { $node = $this->tree->getNodeForPath($path); } catch (Exception\NotFound $e) { $nodeExists = false; } } if ($nodeExists) { $haveMatch = false; if ($ifNoneMatch === '*') $haveMatch = true; else { // There might be multiple ETags $ifNoneMatch = explode(',', $ifNoneMatch); $etag = $node instanceof IFile ? $node->getETag() : null; foreach ($ifNoneMatch as $ifNoneMatchItem) { // Stripping any extra spaces $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); if ($etag === $ifNoneMatchItem) $haveMatch = true; } } if ($haveMatch) { if ($etag) $response->setHeader('ETag', $etag); if ($request->getMethod() === 'GET') { $response->setStatus(304); return false; } else { throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); } } } } if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { // The If-Modified-Since header contains a date. We // will only return the entity if it has been changed since // that date. If it hasn't been changed, we return a 304 // header // Note that this header only has to be checked if there was no If-None-Match header // as per the HTTP spec. $date = HTTP\Util::parseHTTPDate($ifModifiedSince); if ($date) { if (is_null($node)) { $node = $this->tree->getNodeForPath($path); } $lastMod = $node->getLastModified(); if ($lastMod) { $lastMod = new \DateTime('@' . $lastMod); if ($lastMod <= $date) { $response->setStatus(304); $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod)); return false; } } } } if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { // The If-Unmodified-Since will allow allow the request if the // entity has not changed since the specified date. $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince); // We must only check the date if it's valid if ($date) { if (is_null($node)) { $node = $this->tree->getNodeForPath($path); } $lastMod = $node->getLastModified(); if ($lastMod) { $lastMod = new \DateTime('@' . $lastMod); if ($lastMod > $date) { throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); } } } } // Now the hardest, the If: header. The If: header can contain multiple // urls, ETags and so-called 'state tokens'. // // Examples of state tokens include lock-tokens (as defined in rfc4918) // and sync-tokens (as defined in rfc6578). // // The only proper way to deal with these, is to emit events, that a // Sync and Lock plugin can pick up. $ifConditions = $this->getIfConditions($request); foreach ($ifConditions as $kk => $ifCondition) { foreach ($ifCondition['tokens'] as $ii => $token) { $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; } } // Plugins are responsible for validating all the tokens. // If a plugin deemed a token 'valid', it will set 'validToken' to // true. $this->emit('validateTokens', [ $request, &$ifConditions ]); // Now we're going to analyze the result. // Every ifCondition needs to validate to true, so we exit as soon as // we have an invalid condition. foreach ($ifConditions as $ifCondition) { $uri = $ifCondition['uri']; $tokens = $ifCondition['tokens']; // We only need 1 valid token for the condition to succeed. foreach ($tokens as $token) { $tokenValid = $token['validToken'] || !$token['token']; $etagValid = false; if (!$token['etag']) { $etagValid = true; } // Checking the ETag, only if the token was already deamed // valid and there is one. if ($token['etag'] && $tokenValid) { // The token was valid, and there was an ETag. We must // grab the current ETag and check it. $node = $this->tree->getNodeForPath($uri); $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; } if (($tokenValid && $etagValid) ^ $token['negate']) { // Both were valid, so we can go to the next condition. continue 2; } } // If we ended here, it means there was no valid ETag + token // combination found for the current condition. This means we fail! throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If'); } return true; }
/** * This method is called before any HTTP method handler * * This method intercepts any GET, DELETE, PUT and PROPFIND calls to * filenames that are known to match the 'temporary file' regex. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function beforeMethod(RequestInterface $request, ResponseInterface $response) { if (!($tempLocation = $this->isTempFile($request->getPath()))) { return; } switch ($request->getMethod()) { case 'GET': return $this->httpGet($request, $response, $tempLocation); case 'PUT': return $this->httpPut($request, $response, $tempLocation); case 'PROPFIND': return $this->httpPropfind($request, $response, $tempLocation); case 'DELETE': return $this->httpDelete($request, $response, $tempLocation); } return; }
/** * Triggered before any method is handled * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function beforeMethod(RequestInterface $request, ResponseInterface $response) { $method = $request->getMethod(); $path = $request->getPath(); $exists = $this->server->tree->nodeExists($path); // If the node doesn't exists, none of these checks apply if (!$exists) { return; } switch ($method) { case 'GET': case 'HEAD': case 'OPTIONS': // For these 3 we only need to know if the node is readable. $this->checkPrivileges($path, '{DAV:}read'); break; case 'PUT': case 'LOCK': case 'UNLOCK': // This method requires the write-content priv if the node // already exists, and bind on the parent if the node is being // created. // The bind privilege is handled in the beforeBind event. $this->checkPrivileges($path, '{DAV:}write-content'); break; case 'PROPPATCH': $this->checkPrivileges($path, '{DAV:}write-properties'); break; case 'ACL': $this->checkPrivileges($path, '{DAV:}write-acl'); break; case 'COPY': case 'MOVE': // Copy requires read privileges on the entire source tree. // If the target exists write-content normally needs to be // checked, however, we're deleting the node beforehand and // creating a new one after, so this is handled by the // beforeUnbind event. // // The creation of the new node is handled by the beforeBind // event. // // If MOVE is used beforeUnbind will also be used to check if // the sourcenode can be deleted. $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); break; } }
/** * 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)); } }
function beforeMethod(RequestInterface $request, ResponseInterface $response) { $this->beforeMethod = $request->getMethod(); return true; }
/** * Turns a RequestInterface object into an array with settings that can be * fed to curl_setopt * * @param RequestInterface $request * @return array */ protected function createCurlSettingsArray(RequestInterface $request) { $settings = $this->curlSettings; switch ($request->getMethod()) { case 'HEAD': $settings[CURLOPT_NOBODY] = true; $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; $settings[CURLOPT_POSTFIELDS] = ''; $settings[CURLOPT_PUT] = false; break; case 'GET': $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; $settings[CURLOPT_POSTFIELDS] = ''; $settings[CURLOPT_PUT] = false; break; default: $body = $request->getBody(); if (is_resource($body)) { // This needs to be set to PUT, regardless of the actual // method used. Without it, INFILE will be ignored for some // reason. $settings[CURLOPT_PUT] = true; $settings[CURLOPT_INFILE] = $request->getBody(); } else { // For security we cast this to a string. If somehow an array could // be passed here, it would be possible for an attacker to use @ to // post local files. $settings[CURLOPT_POSTFIELDS] = (string) $body; } $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); break; } $nHeaders = []; foreach ($request->getHeaders() as $key => $values) { foreach ($values as $value) { $nHeaders[] = $key . ': ' . $value; } } $settings[CURLOPT_HTTPHEADER] = $nHeaders; $settings[CURLOPT_URL] = $request->getUrl(); // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM if (defined('CURLOPT_PROTOCOLS')) { $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM if (defined('CURLOPT_REDIR_PROTOCOLS')) { $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } return $settings; }