View source
<?php
namespace Drupal\fastly;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\fastly\Form\PurgeOptionsForm;
use Drupal\fastly\Services\Webhook;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class Api {
use StringTranslationTrait;
const FASTLY_MAX_HEADER_KEY_SIZE = 256;
protected $apiKey;
protected $serviceId;
protected $baseUrl;
protected $logger;
protected $connectTimeout;
private $purgeMethod;
protected $state;
protected $webhook;
protected $cacheTagsHash;
protected $messenger;
public function __construct(ConfigFactoryInterface $config_factory, $host, ClientInterface $http_client, LoggerInterface $logger, State $state, $connectTimeout, Webhook $webhook, RequestStack $requestStack, CacheTagsHash $cache_tags_hash, Messenger $messenger) {
$config = $config_factory
->get('fastly.settings');
$this->apiKey = getenv('FASTLY_API_TOKEN') ?: $config
->get('api_key');
$this->serviceId = getenv('FASTLY_API_SERVICE') ?: $config
->get('service_id');
$this->purgeMethod = $config
->get('purge_method') ?: PurgeOptionsForm::FASTLY_INSTANT_PURGE;
$this->purgeLogging = $config
->get('purge_logging');
$this->connectTimeout = $connectTimeout;
$this->host = $host;
$this->httpClient = $http_client;
$this->logger = $logger;
$this->state = $state;
$this->webhook = $webhook;
$this->baseUrl = $requestStack
->getCurrentRequest()
->getHost();
$this->cacheTagsHash = $cache_tags_hash;
$this->messenger = $messenger;
}
public function getApiKey() {
return $this->apiKey;
}
public function setApiKey($api_key) {
$this->apiKey = $api_key;
}
public function setServiceId($service_id) {
$this->serviceId = $service_id;
}
public function getToken() {
$response = $this
->query('/tokens/self');
return $this
->json($response);
}
public function getServices() {
$response = $this
->query('/service');
return $this
->json($response);
}
public function getCurrentUser() {
$response = $this
->query('/current_user');
return $this
->json($response);
}
public function validatePurgeToken() {
try {
$token = $this
->getToken();
if (!empty($token->scopes)) {
$potentially_valid_purge_scopes = 'global';
$valid_purge_scopes = [
'purge_all',
'purge_select',
];
if (array_intersect($valid_purge_scopes, $token->scopes) === $valid_purge_scopes) {
return TRUE;
}
elseif (in_array($potentially_valid_purge_scopes, $token->scopes, TRUE)) {
try {
$current_user = $this
->getCurrentUser();
if (!empty($current_user->role)) {
if ($current_user->role === 'engineer' || $current_user->role === 'superuser') {
return TRUE;
}
elseif ($current_user->role === 'billing' || $current_user->role === 'user') {
return FALSE;
}
else {
return FALSE;
}
}
else {
return FALSE;
}
} catch (\Exception $e) {
return FALSE;
}
}
else {
return FALSE;
}
}
else {
return FALSE;
}
} catch (\Exception $e) {
return FALSE;
}
}
public function validateTokenServiceAccess() {
if (empty($this->serviceId)) {
return FALSE;
}
$token = $this
->getToken();
if (isset($token->services) && empty($token->services)) {
return TRUE;
}
elseif (in_array($this->serviceId, $token->services, TRUE)) {
return TRUE;
}
else {
return FALSE;
}
}
public function validatePurgeCredentials() {
if (empty($this->apiKey) || empty($this->serviceId)) {
return FALSE;
}
if ($this
->validatePurgeToken() && $this
->validateTokenServiceAccess()) {
return TRUE;
}
return FALSE;
}
public function purgeAll($siteOnly = TRUE) {
if ($siteOnly) {
$siteId = $this->cacheTagsHash
->getSiteId();
$siteIdHash = $this->cacheTagsHash
->hashInput($siteId);
return $this
->purgeKeys([
$siteIdHash,
]);
}
else {
if ($this->state
->getPurgeCredentialsState()) {
try {
$response = $this
->query('service/' . $this->serviceId . '/purge_all', [], 'POST');
$result = $this
->json($response);
if ($result->status === 'ok') {
if ($this->purgeLogging) {
$this->logger
->info('Successfully purged all on Fastly.');
}
$this->webhook
->sendWebHook($this
->t("Successfully purged / invalidated all content on @base_url.", [
'@base_url' => $this->baseUrl,
]), "purge_all");
return TRUE;
}
else {
$this->logger
->critical('Unable to purge all on Fastly. Response status: %status.', [
'%status' => $result['status'],
]);
}
} catch (RequestException $e) {
$this->logger
->critical($e
->getMessage());
}
}
return FALSE;
}
}
public function purgeUrl($url = '') {
if (strpos($url, 'http') === FALSE && strpos($url, 'https') === FALSE) {
return FALSE;
}
if (!UrlHelper::isValid($url, TRUE)) {
return FALSE;
}
if (strpos($url, ' ') !== FALSE) {
return FALSE;
}
if ($this->state
->getPurgeCredentialsState()) {
try {
$response = $this
->query('purge/' . $url, [], 'POST');
$result = $this
->json($response);
if ($result->status === 'ok') {
if ($this->purgeLogging) {
$this->logger
->info('Successfully purged URL %url. Purge Method: %purge_method.', [
'%url' => $url,
'%purge_method' => $this->purgeMethod,
]);
}
return TRUE;
}
else {
$this->logger
->critical('Unable to purge URL %url from Fastly. Purge Method: %purge_method.', [
'%url' => $url,
'%purge_method' => $this->purgeMethod,
]);
}
} catch (RequestException $e) {
$this->logger
->critical($e
->getMessage());
}
}
return FALSE;
}
public function purgeKeys(array $keys = []) {
if ($this->state
->getPurgeCredentialsState()) {
try {
$num = count($keys);
if ($num >= self::FASTLY_MAX_HEADER_KEY_SIZE) {
$parts = $num / self::FASTLY_MAX_HEADER_KEY_SIZE;
$additional = $parts > (int) $parts ? 1 : 0;
$parts = (int) $parts + (int) $additional;
$chunks = ceil($num / $parts);
$collection = array_chunk($keys, $chunks);
}
else {
$collection = [
$keys,
];
}
foreach ($collection as $keysChunk) {
$response = $this
->query('service/' . $this->serviceId . '/purge', [], 'POST', [
"Surrogate-Key" => implode(" ", $keysChunk),
]);
$result = $this
->json($response);
if (!empty($result)) {
$message = $this
->t('Successfully purged following key(s) *@keys* on @base_url. Purge Method: @purge_method', [
'@keys' => implode(" ", $keysChunk),
'@base_url' => $this->baseUrl,
'@purge_method' => $this->purgeMethod,
]);
$this->webhook
->sendWebHook($message, 'purge_keys');
if ($this->purgeLogging) {
$this->logger
->info('Successfully purged following key(s) %key. Purge Method: %purge_method.', [
'%key' => implode(" ", $keysChunk),
'%purge_method' => $this->purgeMethod,
]);
}
return TRUE;
}
else {
$message = $this
->t('Unable to purge following key(s) * @keys. Purge Method: @purge_method', [
'@keys' => implode(" ", $keysChunk),
'@purge_method' => $this->purgeMethod,
]);
$this->webhook
->sendWebHook($message, 'purge_keys');
$this->logger
->critical('Unable to purge following key(s) %key from Fastly. Purge Method: %purge_method.', [
'%key' => implode(" ", $keysChunk),
'%purge_method' => $this->purgeMethod,
]);
}
}
} catch (RequestException $e) {
$this->logger
->critical($e
->getMessage());
}
}
return FALSE;
}
protected function query($uri, array $data = [], $method = 'GET', array $headers = []) {
$data['connect_timeout'] = $this->connectTimeout;
try {
if (empty($data['headers'])) {
$data['headers'] = $headers;
$data['headers']['Accept'] = 'application/json';
$data['headers']['Fastly-Key'] = $this->apiKey;
if ($this->purgeMethod == PurgeOptionsForm::FASTLY_SOFT_PURGE) {
$data['headers']['Fastly-Soft-Purge'] = 1;
}
}
switch (strtoupper($method)) {
case 'GET':
$uri = ltrim($uri, "/");
return $this->httpClient
->request($method, $this->host . $uri, $data);
case 'POST':
return $this->httpClient
->post($this->host . $uri, $data);
case 'PURGE':
return $this->httpClient
->request($method, $uri, $data);
default:
throw new \Exception('Method :method is not valid for Fastly service.', [
':method' => $method,
]);
}
} catch (\Exception $e) {
$this->messenger
->addError($e
->getMessage());
$this->logger
->critical($e
->getMessage());
}
return new Response();
}
protected function vQuery($uri, array $data = [], $method = 'GET', array $headers = []) {
try {
if (empty($data['headers'])) {
$data['headers'] = $headers;
$data['headers']['Accept'] = 'application/json';
$data['headers']['Fastly-Key'] = $this->apiKey;
if ($this->purgeMethod == PurgeOptionsForm::FASTLY_SOFT_PURGE) {
$data['headers']['Fastly-Soft-Purge'] = 1;
}
$data['headers']['http_errors'] = TRUE;
}
$uri = ltrim($uri, '/');
$uri = $this->host . $uri;
$uri = rtrim($uri, '/');
switch (strtoupper($method)) {
case 'GET':
case 'POST':
case 'PURGE':
case 'PATCH':
case 'PUT':
case 'DELETE':
$data["http_errors"] = FALSE;
return $this->httpClient
->request($method, $uri, $data);
default:
throw new \Exception('Method :method is not valid for Fastly service.', [
':method' => $method,
]);
}
} catch (\Exception $e) {
$this->logger
->critical($e
->getMessage());
}
return new Response();
}
public function json(ResponseInterface $response) {
return json_decode($response
->getBody());
}
public function vclQuery($uri, array $data = [], $method = 'GET', array $headers = []) {
if (empty($data['headers'])) {
$data['headers'] = $headers;
$data['headers']['Accept'] = 'application/json';
$data['headers']['Fastly-Key'] = $this->apiKey;
}
if ($method == "POST" || $method == "PUT") {
$data['form_params'] = $data;
}
return $this
->vQuery($uri, $data, $method, $headers);
}
public function testFastlyApiConnection() {
if (empty($this->host) || empty($this->serviceId) || empty($this->apiKey)) {
return [
'status' => FALSE,
'message' => $this
->t('Please enter credentials first'),
];
}
$url = '/service/' . $this->serviceId;
$headers = [
'Fastly-Key' => $this->apiKey,
'Accept' => 'application/json',
];
try {
$message = '';
$response = $this
->vclQuery($url, [], "GET", $headers);
if ($response
->getStatusCode() == "200") {
$status = TRUE;
$response_body = json_decode($response
->getBody());
if (!empty($response_body->name)) {
$args = [
'%service_name' => $response_body->name,
];
$message = $this
->t('Connection Successful on service %service_name', $args);
}
}
else {
$status = FALSE;
$response_body = json_decode($response
->getBody());
if (!empty($response_body->name)) {
$args = [
'%name]' => $response_body->name,
'@status' => $response
->getStatusCode(),
];
$message = $this
->t('Connection not Successful on service %name - @status', $args);
$this->logger
->critical($message);
}
else {
$args = [
'@status' => $response
->getStatusCode(),
];
$message = $this
->t('Connection not Successful on service - status : @status', $args);
$this->logger
->critical($message);
}
}
return [
'status' => $status,
'message' => $message,
];
} catch (Exception $e) {
return [
'status' => FALSE,
'message' => $e
->getMessage(),
];
}
}
public function getDetails($serviceId) {
$response = $this
->query('/service/' . $serviceId . '/details');
return $this
->json($response);
}
public function ioEnabled($serviceId) {
$response = $this
->getDetails($serviceId);
if ($response instanceof \stdClass) {
if (property_exists($response, 'active_version') && property_exists($response->active_version, 'io_settings')) {
return TRUE;
}
}
return FALSE;
}
}