public function testGetCallbackResultException() { $methodData = ['method' => 'testCallbackException', 'cache' => ['ttl' => 0], 'header' => ['cache' => ['expires' => 0], 'status' => ['success' => 200, 'error' => 404, 'errorMessage' => '']]]; $requestBag = new RequestBag(); $requestBag->setApi('CacheTest')->setClassData(['class' => 'Webiny\\Component\\Rest\\Tests\\Mocks\\MockApiClassCallback', 'version' => '1.0'])->setMethodData($methodData)->setMethodParameters([]); $callback = new Callback($requestBag); $response = $callback->getCallbackResult()->getOutput(); $this->assertSame('There has been an error processing the request.', $response['errorReport']['message']); }
/** * Checks if user is within rate limits. * * @param RequestBag $requestBag * @param CallbackResult $cr * * @return bool * @throws \Webiny\Component\Rest\RestException */ public static function isWithinRateLimits(RequestBag $requestBag, CallbackResult $cr) { // do we have rate control in place? if (!($rateControl = $requestBag->getApiConfig()->get('RateControl', false))) { return true; // if rate control is not set, user is within his limits } // check if we should ignore rate control for this particular method if (isset($requestBag->getMethodData()['rateControl']['ignore'])) { return true; } // verify that we have a Cache service set if (!($cache = $requestBag->getApiConfig()->get('Cache', false))) { throw new RestException('Rest Rate Control requires that you have a Cache service defined under the Rest configuration.'); } // set the limit in response header $cr->attachDebugHeader('RateControl-Limit', $rateControl->Limit, true); // get current usage $cacheKey = md5('Webiny.Rest.RateLimit.' . self::httpRequest()->getClientIp()); $cacheData = self::cache($cache)->read($cacheKey); if (!$cacheData) { $cacheData = ['usage' => 0, 'penalty' => 0, 'ttl' => time() + 60 * $rateControl->Interval]; } else { $cacheData = self::unserialize($cacheData); // validate the ttl if (time() > $cacheData['ttl']) { $cacheData = ['usage' => 0, 'penalty' => 0, 'ttl' => time() + 60 * $rateControl->Interval]; } } // check if user is already in penalty if ($cacheData['penalty'] > time()) { // when in penalty the reset value, equals the penalty value $cr->attachDebugHeader('RateControl-Reset', $cacheData['penalty'] - time(), true); // and the remaining equals 0 $cr->attachDebugHeader('RateControl-Remaining', 0, true); return false; } // check if rate is reached if ($cacheData['usage'] >= $rateControl->Limit) { // set penalty for reaching the limit $cr->attachDebugHeader('RateControl-Reset', $rateControl->Penalty * 60 + time(), true); // and the remaining 0 $cr->attachDebugHeader('RateControl-Remaining', 0, true); return false; } // if limit not reached, increment the usage and save the data $cacheData['usage']++; $cr->attachDebugHeader('RateControl-Remaining', $rateControl->Limit - $cacheData['usage'], true); $cr->attachDebugHeader('RateControl-Reset', $cacheData['ttl'], true); $cacheTtl = $rateControl->Interval > $rateControl->Penalty ? $rateControl->Interval : $rateControl->Penalty; self::cache($cache)->save($cacheKey, self::serialize($cacheData), $cacheTtl * 60); return true; }
public function testPurgeResult() { $requestBag = new RequestBag(); // populate request bag and point the cache key creation to the mocked class $requestBag->setApi('CacheTest')->setMethodData(['cache' => ['ttl' => 100]])->setClassInstance(new MockCacheTestApiClass())->setClassData(['cacheKeyInterface' => true]); \Webiny\Component\Rest\Response\Cache::saveResult($requestBag, 'my result'); $result = \Webiny\Component\Rest\Response\Cache::getFromCache($requestBag); $this->assertSame('my result', $result); \Webiny\Component\Rest\Response\Cache::purgeResult($requestBag); $result = \Webiny\Component\Rest\Response\Cache::getFromCache($requestBag); $this->assertFalse($result); }
/** * Checks if current user has access to the current rest request. * * @param RequestBag $requestBag * * @return bool * @throws \Webiny\Component\Rest\RestException */ public static function hasAccess(RequestBag $requestBag) { // first we check if method requires a special access level if (!$requestBag->getApiConfig()->get('Security', false)) { return true; // no special access level required } // get the required role if (isset($requestBag->getMethodData()['role'])) { $role = $requestBag->getMethodData()['role']; } else { $role = $requestBag->getApiConfig()->get('Security.Role', 'ROLE_ANONYMOUS'); } // check if user has the access level required if ($requestBag->getClassData()['accessInterface']) { return $requestBag->getClassInstance()->hasAccess($role); } else { // get firewall name $firewallName = $requestBag->getApiConfig()->get('Security.Firewall', false); if (!$firewallName) { throw new RestException('When using Rest access rule, you must specify a Firewall in your configuration. Alternatively you can implement the AccessInterface and do the check on your own.'); } return self::security()->firewall($firewallName)->getUser()->hasRole($role); } }
/** * Computes the cache key, or gets it from the implemented interface from the api class. * * @return string Cache key. */ private function getCacheKey() { if ($this->requestBag->getClassData()['cacheKeyInterface']) { $cacheKey = $this->requestBag->getClassInstance()->getCacheKey(); } else { $url = $this->httpRequest()->getCurrentUrl(true); $cacheKey = 'path-' . $url->getPath(); $cacheKey .= 'query-' . $this->serialize($url->getQuery()); $cacheKey .= 'method-' . $this->httpRequest()->getRequestMethod(); $cacheKey .= 'post-' . $this->serialize($this->httpRequest()->getPost()->getAll()); $cacheKey .= 'payload-' . $this->serialize($this->httpRequest()->getPayload()->getAll()); $cacheKey .= 'version-' . $this->requestBag->getClassData()['version']; $cacheKey = md5($cacheKey); } return $cacheKey; }
/** * Analyzes the request and tries to match an api method. * * @param array $classData Class array form compiled cache file. * * @return CallbackResult * @throws RestException * @throws \Exception */ private function matchRequest(&$classData) { if (!is_array($classData)) { throw new RestException("Invalid class cache data."); } // build the request url upon which we will do the matching try { $url = $this->getUrl(); } catch (\Exception $e) { throw $e; } // get request method $method = $this->getMethod(); if (!in_array($method, self::$supportedRequestTypes)) { throw new RestException('Unsupported request method: "' . $method . '"'); } $callbacks = empty($classData[$method]) ? [] : $classData[$method]; // match array $matchedMethod = ['methodData' => false, 'matchedParameters' => false]; // validate that we have the ending class name in the url $classUrl = PathTransformations::classNameToUrl($this->class, $this->normalize); if (strpos($url, '/' . $classUrl . '/') !== false) { $matchedMethod = $this->matchMethod($callbacks, $url); // if method was not matched if (!$matchedMethod['methodData']) { // if no method was matched, let's try to match a default method $matchedMethod = $this->matchDefaultMethod($callbacks, $url, $classUrl); } } $methodData = isset($matchedMethod['methodData']) ? $matchedMethod['methodData'] : false; $matchedParameters = $matchedMethod['matchedParameters'] ? $matchedMethod['matchedParameters'] : []; $requestBag = new RequestBag(); $requestBag->setClassData($classData)->setMethodData($methodData)->setMethodParameters($matchedParameters)->setApi($this->api); $callback = new Callback($requestBag); return $callback->getCallbackResult(); }
public function testSetClassInstance() { $rb = new RequestBag(); $rb->setClassInstance('instance'); $this->assertSame('instance', $rb->getClassInstance()); }
/** * Processes the callback and returns an instance of CallbackResult. * * @return CallbackResult * @throws \Webiny\Component\Rest\RestException */ public function getCallbackResult() { $class = $this->requestBag->getClassData()['class']; $this->requestBag->setClassInstance(new $class()); // create CallbackResult instance $cr = new CallbackResult(); $env = 'production'; if ($this->requestBag->getApiConfig()->get('Environment', 'production') == 'development') { $cr->setEnvToDevelopment(); $env = 'development'; } // attach some metadata $cr->attachDebugHeader('Class', $class); $cr->attachDebugHeader('ClassVersion', $this->requestBag->getClassData()['version']); $cr->attachDebugHeader('Method', strtoupper($this->httpRequest()->getRequestMethod())); if (!$this->requestBag->getMethodData()) { // if no method matched the request $cr->setHeaderResponse(404); $cr->setErrorResponse('No service matched the request.'); return $cr; } // check rate limit try { $rateControl = RateControl::isWithinRateLimits($this->requestBag, $cr); if (!$rateControl) { $cr->setHeaderResponse(429); $cr->setErrorResponse('Rate control limit reached.'); return $cr; } } catch (\Exception $e) { throw new RestException('Rate control verification failed. ' . $e->getMessage()); } // verify access role try { $hasAccess = Security::hasAccess($this->requestBag); if (!$hasAccess) { $cr->setHeaderResponse(403); $cr->setErrorResponse('You don\'t have the required access level.'); $cr->attachDebugHeader('RequestedRole', $this->requestBag->getMethodData()['role']); return $cr; } } catch (\Exception $e) { throw new RestException('Access role verification failed. ' . $e->getMessage()); } // verify cache try { $cachedResult = Cache::getFromCache($this->requestBag); } catch (\Exception $e) { throw new RestException('Reading result from cache failed. ' . $e->getMessage()); } // finalize output if ($cachedResult) { $cr->setData($cachedResult); $cr->attachDebugHeader('Cache', 'HIT'); } else { try { $result = call_user_func_array([$this->requestBag->getClassInstance(), $this->requestBag->getMethodData()['method']], $this->requestBag->getMethodParameters()); // check if method has custom headers set $cr->setHeaderResponse($this->requestBag->getMethodData()['header']['status']['success']); $cr->attachDebugHeader('Cache', 'MISS'); // add result to output $cr->setData($result); // check if we need to attach the cache headers if ($this->requestBag->getMethodData()['header']['cache']['expires'] > 0) { $cr->setExpiresIn($this->requestBag->getMethodData()['header']['cache']['expires']); } // cache the result Cache::saveResult($this->requestBag, $result); } catch (RestErrorException $re) { // check if method has custom headers set $cr->setHeaderResponse($statusCode = $re->getResponseCode(), $this->requestBag->getMethodData()['header']['status']['errorMessage']); // check if a custom http response code is set $cr->setErrorResponse($re->getErrorMessage(), $re->getErrorDescription(), $re->getErrorCode()); $errors = $re->getErrors(); foreach ($errors as $e) { $cr->addErrorMessage($e); } if ($env == 'development') { $cr->addDebugMessage(['file' => $re->getFile(), 'line' => $re->getLine(), 'traces' => explode('#', $re->getTraceAsString())]); } } catch (\Exception $e) { // check if method has custom headers set $cr->setHeaderResponse($this->requestBag->getMethodData()['header']['status']['error'], $this->requestBag->getMethodData()['header']['status']['errorMessage']); $cr->setErrorResponse('There has been an error processing the request.'); if ($env == 'development') { $cr->addErrorMessage(['message' => $e->getMessage()]); $cr->addDebugMessage(['file' => $e->getFile(), 'line' => $e->getLine(), 'traces' => explode('#', $e->getTraceAsString())]); } } } return $cr; }