<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Model;

use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\HTTP\Adapter\CurlFactory;
use Magento\Framework\Cache\InvalidateLogger;
use Fastly\Cdn\Helper\Data;
use Fastly\Cdn\Helper\Vcl;
use Psr\Log\LoggerInterface;
use Magento\Backend\Model\Auth\Session\Proxy;
use Magento\Framework\App\State;

/**
 * Class Api
 *
 * @package Fastly\Cdn\Model
 */
class Api
{
    const FASTLY_HEADER_AUTH   = 'Fastly-Key';
    const FASTLY_HEADER_TOKEN  = 'X-Purge-Token';
    const FASTLY_HEADER_SOFT_PURGE = 'Fastly-Soft-Purge';
    const PURGE_TIMEOUT        = 10;
    const PURGE_TOKEN_LIFETIME = 30;
    const FASTLY_MAX_HEADER_KEY_SIZE = 256;

    /**
     * @var Config
     */
    private $config;
    /**
     * @var CurlFactory
     */
    private $curlFactory;
    /**
     * @var InvalidateLogger
     */
    private $logger;
    /**
     * @var Data
     */
    private $helper;
    /**
     * @var LoggerInterface
     */
    private $log;
    /**
     * @var bool Purge all flag
     */
    private $purged = false;
    /**
     * @var Vcl
     */
    private $vcl;
    /**
     * @var Proxy
     */
    private $authSession;
    /**
     * @var State
     */
    private $state;

    private $resultJson;

    /**
     * Api constructor.
     *
     * @param Config $config
     * @param CurlFactory $curlFactory
     * @param InvalidateLogger $logger
     * @param Data $helper
     * @param LoggerInterface $log
     * @param Vcl $vcl
     * @param Proxy $authSession
     * @param State $state
     */
    public function __construct(
        Config $config,
        CurlFactory $curlFactory,
        InvalidateLogger $logger,
        Data $helper,
        LoggerInterface $log,
        Vcl $vcl,
        Proxy $authSession,
        State $state
    ) {
        $this->config = $config;
        $this->curlFactory = $curlFactory;
        $this->logger = $logger;
        $this->helper = $helper;
        $this->log = $log;
        $this->vcl = $vcl;
        $this->authSession = $authSession;
        $this->state = $state;
    }

    /**
     * Returns the Fastly API service uri
     *
     * @return string
     */
    private function _getApiServiceUri()
    {
        $uri = $this->config->getApiEndpoint()
            . 'service/'
            . $this->config->getServiceId()
            . '/';

        return $uri;
    }

    /**
     * Historical API stats
     *
     * @return string
     */
    private function _getHistoricalEndpoint()
    {
        $uri = $this->config->getApiEndpoint() . 'stats/service/' . $this->config->getServiceId();

        return $uri;
    }

    private function _getWafEndpoint()
    {
        $uri = $this->config->getApiEndpoint() . 'wafs/';

        return $uri;
    }

    /**
     * Purge a single URL
     *
     * @param $url
     * @return \Magento\Framework\Controller\Result\Json
     * @throws \Zend_Uri_Exception
     */
    public function cleanUrl($url)
    {
        $result = $this->_purge($url, 'PURGE', 'PURGE');

        if ($result['status']) {
            $this->logger->execute($url);
        }

        if ($this->config->areWebHooksEnabled() && $this->config->canPublishKeyUrlChanges()) {
            $this->sendWebHook('*clean by URL for* ' . $url);
        }

        return $result;
    }

    /**
     * Purge Fastly by a given surrogate key
     *
     * @param $keys
     * @return bool|\Magento\Framework\Controller\Result\Json
     * @throws \Zend_Uri_Exception
     */
    public function cleanBySurrogateKey($keys)
    {
        $type = 'clean by key on ';
        $uri = $this->_getApiServiceUri() . 'purge';
        $num = count($keys);
        $result = false;
        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 $keys) {
            $payload = json_encode(['surrogate_keys' => $keys]);
            $result = $this->_purge($uri, null, \Zend_Http_Client::POST, $payload);
            if ($result['status']) {
                foreach ($keys as $key) {
                    $this->logger->execute('surrogate key: ' . $key);
                }
            }

            $canPublishKeyUrlChanges = $this->config->canPublishKeyUrlChanges();
            $canPublishPurgeChanges = $this->config->canPublishPurgeChanges();

            if ($this->config->areWebHooksEnabled() && ($canPublishKeyUrlChanges || $canPublishPurgeChanges)) {
                $status = $result['status'] ? '' : 'FAILED ';
                $this->sendWebHook($status . '*clean by key on ' . join(" ", $keys) . '*');

                $canPublishPurgeByKeyDebugBacktrace = $this->config->canPublishPurgeByKeyDebugBacktrace();
                $canPublishPurgeDebugBacktrace = $this->config->canPublishPurgeDebugBacktrace();

                if ($canPublishPurgeByKeyDebugBacktrace == false && $canPublishPurgeDebugBacktrace == false) {
                    return $result['status'];
                }

                $this->stackTrace($type . join(" ", $keys));
            }
        }

        return $result['status'];
    }

    /**
     * Purge all of Fastly's CDN content. Can be called only once per request
     *
     * @return bool|\Magento\Framework\Controller\Result\Json
     * @throws \Zend_Uri_Exception
     */
    public function cleanAll()
    {
        // Check if purge has been requested on this request
        if ($this->purged == true) {
            return true;
        }
        $this->purged = true;

        $type = 'clean/purge all';
        $uri = $this->_getApiServiceUri() . 'purge_all';
        $result = $this->_purge($uri, null);
        if ($result['status']) {
            $this->logger->execute('clean all items');
        }

        $canPublishPurgeAllChanges = $this->config->canPublishPurgeAllChanges();
        $canPublishPurgeChanges = $this->config->canPublishPurgeChanges();

        if ($this->config->areWebHooksEnabled() && ($canPublishPurgeAllChanges || $canPublishPurgeChanges)) {
            $this->sendWebHook('*initiated clean/purge all*');

            $canPublishPurgeAllDebugBacktrace = $this->config->canPublishPurgeAllDebugBacktrace();
            $canPublishPurgeDebugBacktrace = $this->config->canPublishPurgeDebugBacktrace();

            if ($canPublishPurgeAllDebugBacktrace == false && $canPublishPurgeDebugBacktrace == false) {
                return $result['status'];
            }

            $this->stackTrace($type);
        }

        return $result['status'];
    }

    /**
     * Send purge request via Fastly API
     *
     * @param $uri
     * @param $type
     * @param string $method
     * @param null $payload
     * @return \Magento\Framework\Controller\Result\Json
     * @throws \Zend_Uri_Exception
     */
    private function _purge($uri, $type, $method = \Zend_Http_Client::POST, $payload = null)
    {

        if ($method == 'PURGE') {
            // create purge token
            $expiration   = time() + self::PURGE_TOKEN_LIFETIME;

            $zendUri = \Zend_Uri::factory($uri);
            $path = $zendUri->getPath();
            $stringToSign = $path . $expiration;
            $signature    = hash_hmac('sha1', $stringToSign, $this->config->getServiceId());
            $token        = $expiration . '_' . urlencode($signature);
            $headers = [
                self::FASTLY_HEADER_TOKEN . ': ' . $token
            ];
        } else {
            // set headers
            $headers = [
                self::FASTLY_HEADER_AUTH  . ': ' . $this->config->getApiKey()
            ];
        }

        // soft purge if needed
        if ($this->config->canUseSoftPurge()) {
            array_push(
                $headers,
                self::FASTLY_HEADER_SOFT_PURGE . ': 1'
            );
        }
        $result['status'] = true;
        try {
            $client = $this->curlFactory->create();
            $client->setConfig(['timeout' => self::PURGE_TIMEOUT]);
            if ($method == 'PURGE') {
                $client->addOption(CURLOPT_CUSTOMREQUEST, 'PURGE');
            }
            $client->write($method, $uri, '1.1', $headers, $payload);
            $responseBody = $client->read();
            $responseCode = \Zend_Http_Response::extractCode($responseBody);
            $responseMessage = \Zend_Http_Response::extractMessage($responseBody);
            $client->close();

            // check response
            if ($responseCode == '429') {
                throw new LocalizedException(__($responseMessage));
            } elseif ($responseCode != '200') {
                throw new LocalizedException(__($responseCode . ': ' . $responseMessage));
            }
        } catch (\Exception $e) {
            $this->logger->critical($e->getMessage(), $uri);
            $result['status'] = false;
            $result['msg'] = $e->getMessage();
        }

        if (empty($type)) {
            return $result;
        }

        if ($this->config->areWebHooksEnabled() && $this->config->canPublishPurgeChanges()) {
            $this->sendWebHook('*initiated ' . $type .'*');

            if ($this->config->canPublishPurgeDebugBacktrace() == false) {
                return $result;
            }

            $this->stackTrace($type);
        }

        return $result;
    }

    /**
     * Get the logged in customer details
     *
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getCustomerInfo()
    {
        $uri = $this->config->getApiEndpoint() . 'current_customer';
        $result = $this->_fetch($uri);

        return $result;
    }

    /**
     * List detailed information on a specified service
     *
     * @param bool $test
     * @param null $serviceId
     * @param null $apiKey
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function checkServiceDetails($test = false, $serviceId = null, $apiKey = null)
    {
        if (!$test) {
            $uri = rtrim($this->_getApiServiceUri(), '/');
            $result = $this->_fetch($uri);
        } else {
            $uri = $this->config->getApiEndpoint() . 'service/' . $serviceId;
            $result = $this->_fetch($uri, \Zend_Http_Client::GET, null, true, $apiKey);
        }

        if (!$result) {
            throw new LocalizedException(__('Failed to check Service details.'));
        }

        return $result;
    }

    /**
     * Clone the current configuration into a new version.
     *
     * @param $curVersion
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function cloneVersion($curVersion)
    {
        $url = $this->_getApiServiceUri() . 'version/'.$curVersion.'/clone';
        $result = $this->_fetch($url, \Zend_Http_Client::PUT);

        if (!$result) {
            throw new LocalizedException(__('Failed to clone active version.'));
        }

        return $result;
    }

    /**
     * Add comment to the specified version
     *
     * @param $version
     * @param $comment
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function addComment($version, $comment)
    {
        $url = $this->_getApiServiceUri() . 'version/' . $version;
        $result = $this->_fetch($url, \Zend_Http_Client::PUT, $comment);

        return $result;
    }

    /**
     * Upload a VCL for a particular service and version
     *
     * @param array $vcl
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function uploadVcl($version, $vcl)
    {
        $url = $this->_getApiServiceUri() . 'version/' .$version. '/vcl';
        $result = $this->_fetch($url, 'POST', $vcl);

        return $result;
    }

    /**
     * Set the specified VCL as the main
     *
     * @param $version
     * @param string $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function setVclAsMain($version, $name)
    {
        $url = $this->_getApiServiceUri() . 'version/' .$version. '/vcl/' .$name. '/main';
        $result = $this->_fetch($url, 'PUT');

        return $result;
    }

    /**
     * Validate the version for a particular service and version.
     *
     * @param $version
     * @throws LocalizedException
     */
    public function validateServiceVersion($version)
    {
        $url = $this->_getApiServiceUri() . 'version/' .$version. '/validate';
        $result = $this->_fetch($url, 'GET');

        if ($result->status == 'error') {
            throw new LocalizedException(__('Failed to validate service version: ' . $result->msg));
        }
    }

    /**
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function containerValidateServiceVersion($version)
    {
        $url = $this->_getApiServiceUri() . 'version/' .$version. '/validate';
        $result = $this->_fetch($url, 'GET');

        return $result;
    }

    /**
     * Activate the current version.
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function activateVersion($version)
    {
        $url = $this->_getApiServiceUri() . 'version/' .$version. '/activate';
        $result = $this->_fetch($url, 'PUT');

        return $result;
    }

    /**
     * Creating and updating regular VCL snippets
     *
     * @param $version
     * @param array $snippet
     * @throws LocalizedException
     * @throws \Exception
     * @throws \Magento\Framework\Exception\FileSystemException
     */
    public function uploadSnippet($version, array $snippet)
    {
        // Perform replacements vcl template replacements
        if (isset($snippet['content'])) {
            $adminUrl = $this->vcl->getAdminFrontName();
            $adminPathTimeout = $this->config->getAdminPathTimeout();
            $ignoredUrlParameters = $this->config->getIgnoredUrlParameters();

            $ignoredUrlParameterPieces = explode(",", $ignoredUrlParameters);
            $filterIgnoredUrlParameterPieces = array_filter(array_map('trim', $ignoredUrlParameterPieces));
            $queryParameters = implode('|', $filterIgnoredUrlParameterPieces);

            $snippet['content'] = str_replace('####ADMIN_PATH####', $adminUrl, $snippet['content']);
            $snippet['content'] = str_replace('####ADMIN_PATH_TIMEOUT####', $adminPathTimeout, $snippet['content']);
            $snippet['content'] = str_replace('####QUERY_PARAMETERS####', $queryParameters, $snippet['content']);
        }

        $checkIfExists = $this->hasSnippet($version, $snippet['name']);
        $url = $this->_getApiServiceUri(). 'version/' .$version. '/snippet';
        if (!$checkIfExists) {
            $verb = \Zend_Http_Client::POST;
        } else {
            $verb = \Zend_Http_Client::PUT;
            $url .= '/'.$snippet['name'];
            unset($snippet['name'], $snippet['type'], $snippet['dynamic'], $snippet['priority']);
        }

        $result = $this->_fetch($url, $verb, $snippet);

        if (!$result) {
            throw new LocalizedException(__('Failed to upload the Snippet file.'));
        }
    }

    /**
     * Fetching an individual regular VCL Snippet
     *
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getSnippet($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/snippet/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Update a dynamic snippet
     *
     * @param array $snippet
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function updateSnippet(array $snippet)
    {
        $url = $this->_getApiServiceUri(). 'snippet' . '/'.$snippet['name'];
        $result = $this->_fetch($url, \Zend_Http_Client::PUT, $snippet);

        return $result;
    }

    /**
     * Performs a lookup to determine if VCL snippet exists
     *
     * @param string    $version    Fastly version
     * @param string    $name   VCL snippet name
     *
     * @return bool
     * @throws LocalizedException
     */
    public function hasSnippet($version, $name)
    {
        $url = $this->_getApiServiceUri() . 'version/' . $version . '/snippet/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET, '', false, null, false);

        if ($result == false) {
            return false;
        }

        return true;
    }

    /**
     * Deleting an individual regular VCL Snippet
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function removeSnippet($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/snippet/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * Creates a new condition
     * @param $version
     * @param array $condition
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createCondition($version, array $condition)
    {
        $checkIfExists = $this->getCondition($version, $condition['name']);
        $url = $this->_getApiServiceUri(). 'version/' .$version. '/condition';
        if (!$checkIfExists) {
            $verb = \Zend_Http_Client::POST;
        } else {
            $verb = \Zend_Http_Client::PUT;
            $url .= '/'.$condition['name'];
        }

        $result = $this->_fetch($url, $verb, $condition);

        if (!$result) {
            throw new LocalizedException(__('Failed to create a REQUEST condition.'));
        }

        return $result;
    }

    /**
     * Gets the specified condition.
     *
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getCondition($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/condition/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Creates a new header
     *
     * @param $version
     * @param $condition
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createHeader($version, array $condition)
    {
        $checkIfExists = $this->getHeader($version, $condition['name']);
        $url = $this->_getApiServiceUri(). 'version/' .$version. '/header';

        if ($checkIfExists === false) {
            $verb = \Zend_Http_Client::POST;
        } else {
            $verb = \Zend_Http_Client::PUT;
            $url .= '/'.$condition['name'];
        }

        $result = $this->_fetch($url, $verb, $condition);

        return $result;
    }

    /**
     * Gets the specified header.
     *
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getHeader($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/header/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Creates a new Response Object.
     *
     * @param $version
     * @param array $response
     * @return bool $result
     * @throws LocalizedException
     */
    public function createResponse($version, array $response)
    {
        $checkIfExists = $this->getResponse($version, $response['name']);
        $url = $this->_getApiServiceUri(). 'version/' .$version. '/response_object';
        if (!$checkIfExists) {
            $verb = \Zend_Http_Client::POST;
        } else {
            $verb = \Zend_Http_Client::PUT;
            $url .= '/'.$response['name'];
        }

        $result = $this->_fetch($url, $verb, $response);

        return $result;
    }

    /**
     * Gets the specified Response Object.
     *
     * @param string $version
     * @param string $name
     * @return bool|mixed $result
     * @throws LocalizedException
     */
    public function getResponse($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/response_object/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Creates a new Request Settings object.
     * @param $version
     * @param $request
     * @throws LocalizedException
     */
    public function createRequest($version, $request)
    {
        $checkIfExists = $this->getRequest($version, $request['name']);
        $url = $this->_getApiServiceUri(). 'version/' .$version. '/request_settings';
        if (!$checkIfExists) {
            $verb = \Zend_Http_Client::POST;
        } else {
            $verb = \Zend_Http_Client::PUT;
            $url .= '/'.$request['name'];
        }

        $result = $this->_fetch($url, $verb, $request);

        if (!$result) {
            throw new LocalizedException(__('Failed to create the REQUEST object.'));
        }
    }

    /**
     * Retrieves a specific Request settings object.
     *
     * @param string    $version    Fastly version
     * @param string    $name   Request name
     *
     * @return bool
     * @throws LocalizedException
     */
    public function getRequest($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/request_settings/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::GET, '', false, null, false);

        return $result;
    }

    /**
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getAllConditions($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/condition';
        $result = $this->_fetch($url, \Zend_Http_Client::GET, '', false, null, false);

        return $result;
    }

    /**
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getAllDomains($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/domain';
        $result = $this->_fetch($url, \Zend_Http_Client::GET, '', false, null, false);

        return $result;
    }

    /**
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function deleteDomain($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/domain/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * @param $version
     * @param $data
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createDomain($version, $data)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/domain';
        $result = $this->_fetch($url, \Zend_Http_Client::POST, $data);

        return $result;
    }

    /**
     * Removes the specified Request Settings object.
     * @param $version
     * @param $name
     * @throws LocalizedException
     */
    public function deleteRequest($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/request_settings/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        if (!$result) {
            throw new LocalizedException(__('Failed to delete the REQUEST object.'));
        }
    }

    /**
     * List all backends for a particular service and version.
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getBackends($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version. '/backend';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Configure Backend settings
     *
     * @param $params
     * @param $version
     * @param $old_name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function configureBackend($params, $version, $old_name)
    {
        $url = $this->_getApiServiceUri()
            . 'version/'
            . $version
            . '/backend/'
            . str_replace(' ', '%20', $old_name);

        $result = $this->_fetch(
            $url,
            \Zend_Http_Client::PUT,
            $params
        );

        return $result;
    }

    /**
     * Send message to Slack channel
     *
     * @param $message
     */
    public function sendWebHook($message)
    {
        $url = $this->config->getIncomingWebhookURL();
        $messagePrefix = $this->config->getWebhookMessagePrefix();
        $currentUsername = 'System';
        try {
            if ($this->state->getAreaCode() == 'adminhtml') {
                $getUser = $this->authSession->getUser();
                if (!empty($getUser)) {
                    $currentUsername = $getUser->getUserName();
                }
            }
        } catch (\Exception $e) {
            $this->log->log(100, 'Failed to retrieve Area Code');
        }

        $storeName = $this->helper->getStoreName();
        $storeUrl = $this->helper->getStoreUrl();

        $text =  $messagePrefix.' user='.$currentUsername.' '.$message.' on <'.$storeUrl.'|Store> | '.$storeName;

        $headers = [
            'Content-type: application/json'
        ];

        $body = json_encode([
            "text"  =>  $text,
            "username" => "fastly-magento-bot",
            "icon_emoji"=> ":airplane:"
        ]);

        $client = $this->curlFactory->create();
        $client->addOption(CURLOPT_CONNECTTIMEOUT, 2);
        $client->addOption(CURLOPT_TIMEOUT, 3);
        $client->write(\Zend_Http_Client::POST, $url, '1.1', $headers, $body);
        $response = $client->read();
        $responseCode = \Zend_Http_Response::extractCode($response);

        if ($responseCode != 200) {
            $this->log->log(100, 'Failed to send message to the following Webhook: '.$url);
        }

        $client->close();
    }

    /**
     * Create named dictionary for a particular service and version.
     *
     * @param $version
     * @param $params
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createDictionary($version, $params)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/dictionary';
        $result = $this->_fetch($url, \Zend_Http_Client::POST, $params);

        return $result;
    }

    /**
     * Delete named dictionary for a particular service and version.
     *
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function deleteDictionary($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/dictionary/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * Get dictionary item list
     *
     * @param $dictionaryId
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function dictionaryItemsList($dictionaryId)
    {
        $url = $this->_getApiServiceUri(). 'dictionary/'.$dictionaryId.'/items';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Fetches dictionary by name
     *
     * @param $version
     * @param $dictionaryName
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getSingleDictionary($version, $dictionaryName)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/dictionary/' . $dictionaryName;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Get auth dictionary
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getAuthDictionary($version)
    {
        $name = Config::AUTH_DICTIONARY_NAME;
        $dictionary = $this->getSingleDictionary($version, $name);

        return $dictionary;
    }

    /**
     * Check if authentication dictionary is populated
     *
     * @param $version
     * @throws LocalizedException
     */
    public function checkAuthDictionaryPopulation($version)
    {
        $dictionary = $this->getAuthDictionary($version);

        if ((is_array($dictionary) && empty($dictionary)) || !isset($dictionary->id)) {
            throw new LocalizedException(__('You must add users in order to enable Basic Authentication.'));
        }

        $authItems = $this->dictionaryItemsList($dictionary->id);

        if (is_array($authItems) && empty($authItems)) {
            throw new LocalizedException(__('You must add users in order to enable Basic Authentication.'));
        }
    }

    /**
     * Create dictionary items
     *
     * @param $dictionaryId
     * @param $params
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createDictionaryItems($dictionaryId, $params)
    {
        $url = $this->_getApiServiceUri().'dictionary/'.$dictionaryId.'/items';
        $result = $this->_fetch($url, \Zend_Http_Client::PATCH, $params);

        return $result;
    }

    /**
     * List all dictionaries for the version of the service.
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getDictionaries($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/dictionary';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Delete single Dictionary item
     *
     * @param $dictionaryId
     * @param $itemKey
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function deleteDictionaryItem($dictionaryId, $itemKey)
    {
        $url = $this->_getApiServiceUri(). 'dictionary/'. $dictionaryId . '/item/' . urlencode($itemKey);
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * Upsert single Dictionary item
     *
     * @param $dictionaryId
     * @param $itemKey
     * @param $itemValue
     * @throws LocalizedException
     */
    public function upsertDictionaryItem($dictionaryId, $itemKey, $itemValue)
    {
        $body = ['item_value' => $itemValue];
        $url = $this->_getApiServiceUri(). 'dictionary/'. $dictionaryId . '/item/' . urlencode($itemKey);
        $result = $this->_fetch($url, \Zend_Http_Client::PUT, $body);

        if (!$result) {
            throw new LocalizedException(__('Failed to create Dictionary item.'));
        }
    }

    /**
     * Get ACL container info
     * @param $version
     * @param $acl
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getSingleAcl($version, $acl)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/acl/' . $acl;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Create named ACL for a particular service and version.
     *
     * @param $version
     * @param $params
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function createAcl($version, $params)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/acl';
        $result = $this->_fetch($url, \Zend_Http_Client::POST, $params);

        return $result;
    }

    /**
     * Fetch ACL list for particular service and version
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getAcls($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/acl';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Delete named ACL for a particular service and version.
     *
     * @param $version
     * @param $name
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function deleteAcl($version, $name)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/acl/' . $name;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * Fetch ACL entry list for particular ACL
     *
     * @param $aclId
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function aclItemsList($aclId)
    {
        $url = $this->_getApiServiceUri() . 'acl/'. $aclId . '/entries';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Upsert single ACL entry
     *
     * @param $aclId
     * @param $itemValue
     * @param $negated
     * @param string $comment
     * @param bool $subnet
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function upsertAclItem($aclId, $itemValue, $negated, $comment = 'Added by Magento Module', $subnet = false)
    {
        $body = [
            'ip' => $itemValue,
            'negated' => $negated,
            'comment' => $comment
        ];

        if ($subnet) {
            $body['subnet'] = $subnet;
        }

        $url = $this->_getApiServiceUri(). 'acl/'. $aclId . '/entry';
        $result = $this->_fetch($url, \Zend_Http_Client::POST, $body);

        return $result;
    }

    /**
     * Delete single ACL entry from specific ACL
     *
     * @param $aclId
     * @param $aclItemId
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function deleteAclItem($aclId, $aclItemId)
    {
        $url = $this->_getApiServiceUri(). 'acl/'. $aclId . '/entry/' . $aclItemId;
        $result = $this->_fetch($url, \Zend_Http_Client::DELETE);

        return $result;
    }

    /**
     * Update single ACL entry
     *
     * @param $aclId
     * @param $aclItemId
     * @param $itemValue
     * @param $negated
     * @param string $comment
     * @param bool $subnet
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function updateAclItem($aclId, $aclItemId, $itemValue, $negated, $comment = '', $subnet = false)
    {
        $body = [
            'ip' => $itemValue,
            'negated' => $negated,
            'comment' => $comment
        ];

        if ($subnet) {
            $body['subnet'] = $subnet;
        }

        $url = $this->_getApiServiceUri(). 'acl/'. $aclId . '/entry/' . $aclItemId;
        $result = $this->_fetch($url, \Zend_Http_Client::PATCH, json_encode($body));

        return $result;
    }

    /**
     * Query for historic stats
     *
     * @param array $parameters
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function queryHistoricStats(array $parameters)
    {
        $uri = $this->_getHistoricalEndpoint()
            . '?region='.$parameters['region']
            . '&from='.$parameters['from']
            . '&to='.$parameters['to']
            . '&by='
            . $parameters['sample_rate'];

        $result = $this->_fetch($uri);

        return $result;
    }

    /**
     * Check if image optimization is enabled for the Fastly service
     *
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function checkImageOptimizationStatus()
    {
        $url = $this->_getApiServiceUri(). 'dynamic_io_settings';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Get the image optimization default config options
     *
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getImageOptimizationDefaultConfigOptions($version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/io_settings';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Configure the image optimization default config options
     *
     * @param $params
     * @param $version
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function configureImageOptimizationDefaultConfigOptions($params, $version)
    {
        $url = $this->_getApiServiceUri(). 'version/'. $version . '/io_settings';
        $result = $this->_fetch($url, \Zend_Http_Client::PATCH, $params);

        return $result;
    }

    /**
     * Retrieve Fastly service details
     *
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getServiceDetails()
    {
        $url = $this->_getApiServiceUri() . 'details';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Retrieve Web Application Firewall settings
     *
     * @param $id
     * @return bool|mixed
     * @throws LocalizedException
     */
    public function getWafSettings($id)
    {
        $url = $this->_getWafEndpoint() . $id;
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    public function getOwaspSettings($id)
    {
        $url = $this->_getApiServiceUri() . 'wafs/' . $id . '/owasp';
        $result = $this->_fetch($url, \Zend_Http_Client::GET);

        return $result;
    }

    /**
     * Wrapper for API calls towards Fastly service
     *
     * @param string    $uri    API Endpoint
     * @param string    $method HTTP Method for request
     * @param mixed[]|string    $body   Content
     * @param bool  $test   Use $testApiKey for request
     * @param string    $testApiKey API key to be tested
     * @param bool  $logError   When set to false, prevents writing failed requests to log
     *
     * @return bool|mixed   Returns false on failiure
     * @throws LocalizedException
     */
    private function _fetch(
        $uri,
        $method = \Zend_Http_Client::GET,
        $body = '',
        $test = false,
        $testApiKey = null,
        $logError = true
    ) {
        $apiKey = ($test == true) ? $testApiKey : $this->config->getApiKey();

        // Correctly format $body string
        if (is_array($body) == true) {
            $body = http_build_query($body);
        }

        // Client headers
        $headers = [
            self::FASTLY_HEADER_AUTH  . ': ' . $apiKey,
            'Accept: application/json'
        ];

        // Client options
        $options = [];

        // Request method specific header & option changes
        switch ($method) {
            case \Zend_Http_Client::DELETE:
                $options[CURLOPT_CUSTOMREQUEST] = \Zend_Http_Client::DELETE;
                break;
            case \Zend_Http_Client::PUT:
                $headers[] = 'Content-Type: application/x-www-form-urlencoded';
                $options[CURLOPT_CUSTOMREQUEST] = \Zend_Http_Client::PUT;

                if ($body != '') {
                    $options[CURLOPT_POSTFIELDS] = $body;
                }

                break;
            case \Zend_Http_Client::PATCH:
                $options[CURLOPT_CUSTOMREQUEST] = \Zend_Http_Client::PATCH;
                $headers[] = 'Content-Type: text/json';

                if ($body != '') {
                    $options[CURLOPT_POSTFIELDS] = $body;
                }

                break;
        }

        /** @var \Magento\Framework\HTTP\Adapter\Curl $client */
        $client = $this->curlFactory->create();

        // Execute request
        $client->setOptions($options);
        $client->write($method, $uri, '1.1', $headers, $body);
        $response = $client->read();
        $client->close();

        // Parse response
        $responseBody = \Zend_Http_Response::extractBody($response);
        $responseCode = \Zend_Http_Response::extractCode($response);
        $responseMessage = \Zend_Http_Response::extractMessage($response);

        // Return error based on response code
        if ($responseCode == '429') {
            throw new LocalizedException(__($responseMessage));
        } elseif ($responseCode != '200') {
            if ($logError == true) {
                $this->logger->critical('Return status ' . $responseCode, $uri);
            }

            return false;
        }

        return json_decode($responseBody);
    }

    private function stackTrace($type)
    {
        $stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        $trace = [];
        foreach ($stackTrace as $row => $data) {
            if (!array_key_exists('file', $data) || !array_key_exists('line', $data)) {
                $trace[] = "# <unknown>";
            } else {
                $trace[] = "#{$row} {$data['file']}:{$data['line']} -> {$data['function']}()";
            }
        }

        $this->sendWebHook('*'. $type .' backtrace:*```' .  implode("\n", $trace) . '```');
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Model;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\FrontController;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Framework\App\RequestInterface as Request;
use Magento\Framework\App\ResponseInterface as Response;
use Magento\Framework\HTTP\Header;
use Magento\Framework\Filesystem;
use Magento\Framework\App\Filesystem\DirectoryList;

/**
 * Class FrontControllerPlugin
 *
 * @package Fastly\Cdn\Model
 */
class FrontControllerPlugin
{
    /** @var string Cache tag for storing rate limit data */
    const FASTLY_CACHE_TAG = 'fastly_rate_limit_';
    /** @var string Cache tag for storing crawler rate limit data */
    const FASTLY_CRAWLER_TAG = 'fastly_crawler_protection_';

    /**
     * @var CacheInterface
     */
    private $cache;

    /**
     * @var DateTime
     */
    private $coreDate;

    /**
     * @var \Magento\PageCache\Model\Config
     */
    private $config;

    /**
     * @var \Magento\Framework\App\RequestInterface
     */
    private $request;

    /**
     * @var \Magento\Framework\App\ResponseInterface
     */
    private $response;

    /**
     * @var Header
     */
    private $httpHeader;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * FrontControllerPlugin constructor.
     * @param Request $request
     * @param Config $config
     * @param CacheInterface $cache
     * @param DateTime $coreDate
     * @param Response $response
     * @param Header $httpHeader
     * @param Filesystem $filesystem
     */
    public function __construct(
        Request $request,
        Config $config,
        CacheInterface $cache,
        DateTime $coreDate,
        Response $response,
        Header $httpHeader,
        Filesystem $filesystem
    ) {
        $this->request = $request;
        $this->response = $response;
        $this->httpHeader = $httpHeader;
        $this->config = $config;
        $this->cache = $cache;
        $this->coreDate = $coreDate;
        $this->filesystem = $filesystem;
    }

    /**
     * Check if request is limited
     * @param FrontController $subject
     * @param callable $proceed
     * @param mixed ...$args
     * @return \Magento\Framework\App\Response\Http|\Magento\Framework\App\ResponseInterface
     */
    public function aroundDispatch(FrontController $subject, callable $proceed, ...$args) // @codingStandardsIgnoreLine - unused parameter
    {
        $isRateLimitingEnabled = $this->config->isRateLimitingEnabled();
        $isCrawlerProtectionEnabled = $this->config->isCrawlerProtectionEnabled();

        if (!$isRateLimitingEnabled && !$isCrawlerProtectionEnabled) {
            return $proceed(...$args);
        }

        $path = strtolower($this->request->getPathInfo());

        if ($isRateLimitingEnabled && $this->sensitivePathProtection($path)) {
            return $this->response;
        }

        if ($isCrawlerProtectionEnabled && $this->crawlerProtection($path)) {
            return $this->response;
        }

        return $proceed(...$args);
    }

    /**
     * @param $path
     * @return bool
     */
    private function sensitivePathProtection($path)
    {
        $limitedPaths = json_decode($this->config->getRateLimitPaths());
        if (!$limitedPaths) {
            $limitedPaths = [];
        }

        $limit = false;
        foreach ($limitedPaths as $key => $value) {
            if (preg_match('{' . $value->path . '}i', $path) == 1) {
                $limit = true;
            }
        }

        if ($limit) {
            $rateLimitingLimit = $this->config->getRateLimitingLimit();
            $rateLimitingTtl = $this->config->getRateLimitingTtl();
            $ip = $this->request->getServerValue('HTTP_FASTLY_CLIENT_IP') ?? $this->request->getClientIp();
            $tag = self::FASTLY_CACHE_TAG . $ip;
            $data = json_decode($this->cache->load($tag), true);

            return $this->processData($data, $tag, $rateLimitingTtl, $rateLimitingLimit, "path");
        }

        return false;
    }

    /**
     * @param $path
     * @return bool
     */
    private function crawlerProtection($path)
    {
        $ip = $this->request->getServerValue('HTTP_FASTLY_CLIENT_IP') ?? $this->request->getClientIp();

        if ($this->config->isExemptGoodBotsEnabled()) {
            if ($this->verifyBots($ip)) {
                return false;
            }
        }

        if ($this->readMaintenanceIp($ip)) {
            return false;
        }

        $pattern = '{^/(pub|var)/(static|view_preprocessed)/}';

        if (preg_match($pattern, $path) == 1) {
            return false;
        }

        $crawlerRateLimitingLimit = $this->config->getCrawlerRateLimitingLimit();
        $crawlerRateLimitingTtl = $this->config->getCrawlerRateLimitingTtl();
        $tag = self::FASTLY_CRAWLER_TAG . $ip;
        $data = json_decode($this->cache->load($tag), true);

        return $this->processData($data, $tag, $crawlerRateLimitingTtl, $crawlerRateLimitingLimit, "crawler");
    }

    /**
     * @param $data
     * @param $tag
     * @param $ttl
     * @param $limit
     * @param $limitingType - path or crawler
     * @return bool
     */
    private function processData($data, $tag, $ttl, $limit, $limitingType = "path")
    {
        if (empty($data)) {
            $date = $this->coreDate->timestamp();
            $data = json_encode([
                'usage' => 1,
                'date'  => $date
            ]);
            $this->cache->save($data, $tag, [], $ttl);
        } else {
            $usage = $data['usage'] ?? 0;
            $date = $data['date'] ?? null;
            $newDate = $this->coreDate->timestamp();
            $dateDiff = ($newDate - $date);

            if ($dateDiff >= $ttl) {
                $data = json_encode([
                    'usage' => 1,
                    'date'  => $newDate
                ]);
                $this->cache->save($data, $tag, [], $ttl);
                return false;
            }

            if ($usage >= $limit) {
                $this->response->setStatusHeader(429, null, 'API limit exceeded');
                if ($limitingType == "path") {
                    $this->response->setHeader('Surrogate-Control', 'max-age=' . $ttl);
                }
                if ($limitingType == "crawler") {
                    $this->response->setHeader('Fastly-Vary', 'Fastly-Client-IP');
                }
                $this->response->setBody('
Request limit exceeded
');
                $this->response->setNoCacheHeaders();
                return true;
            } else {
                $usage++;
                $data['usage'] = $usage;
                $this->cache->save(json_encode($data), $tag, []);
            }
        }
        return false;
    }

    /**
     * @param $haystack
     * @param $needle
     * @return bool
     */
    private function endsWith($haystack, $needle)
    {
        $length = strlen($needle);
        if ($length == 0) {
            return false;
        }
        return (substr($haystack, -$length) === $needle);
    }

    /**
     * @param $ip
     * @return bool
     */
    private function verifyBots($ip)
    {
        $userAgent = strtolower($this->httpHeader->getHttpUserAgent());
        $goodBots = [
            'googlebot' => [
                'googlebot.com',
                'google.com'
            ],
            'msnbot'    => [
                'search.msn.com'
            ],
            'bingbot'   => [
                'search.msn.com'
            ]
        ];

        // for each good bot
        foreach ($goodBots as $botName => $botDomains) {
            // check if the user agent is a bot
            if (strpos($userAgent, $botName) !== false) {
                // get domain from ip value
                $domain = gethostbyaddr($ip);
                // for each verified bot domain
                foreach ($botDomains as $botDomain) {
                    // check if the verified and retrieved domains match
                    $endsWith = $this->endsWith($domain, $botDomain);
                    // if the domain is verified
                    if ($endsWith !== false) {
                        // confirm IP address
                        $addr = gethostbyname($domain);
                        if ($ip == $addr) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    private function readMaintenanceIp($ip)
    {
        $flagDir = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);

        if ($flagDir->isExist('.maintenance.ip')) {
            $temp = $flagDir->readFile('.maintenance.ip');
            $tempList = explode(',', trim($temp));
            foreach ($tempList as $key => $value) {
                if (!empty($value) && trim($value) == $ip) {
                    return true;
                }
            }
        }
        return false;
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\ImageOptimization;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Request\Http;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Fastly\Cdn\Model\Api;
use Magento\Framework\Controller\ResultInterface;

/**
 * Class CheckFastlyIoSetting
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\ImageOptimization
 */
class CheckFastlyIoSetting extends Action
{
    /**
     * @var Http
     */
    private $request;
    /**
     * @var JsonFactory
     */
    private $resultJson;
    /**
     * @var Api
     */
    private $api;

    /**
     * GetBackends constructor.
     *
     * @param Context $context
     * @param Http $request
     * @param JsonFactory $resultJsonFactory
     * @param Api $api
     */
    public function __construct(
        Context $context,
        Http $request,
        JsonFactory $resultJsonFactory,
        Api $api
    ) {
        $this->request = $request;
        $this->resultJson = $resultJsonFactory;
        $this->api = $api;
        parent::__construct($context);
    }

    /**
     * Get Fastly service image optimization status
     *
     * @return $this|ResponseInterface|ResultInterface
     */
    public function execute()
    {
        $result = $this->resultJson->create();
        try {
            $req = $this->api->checkImageOptimizationStatus();

            if (!$req) {
                return $result->setData([
                    'status'    => false,
                    'msg'       => 'Failed to check image optimization status.'
                ]);
            }

            return $result->setData([
                'status'        => true,
                'req_setting'   => $req
            ]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Purge;

use Fastly\Cdn\Model\PurgeCache;
use Fastly\Cdn\Model\Config;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Exception\LocalizedException;

/**
 * Class ContentType
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Purge
 */
class ContentType extends Action
{
    /**
     * @var PurgeCache
     */
    private $purgeCache;
    /**
     * @var Config
     */
    private $config;

    /**
     * ContentType constructor.
     *
     * @param Context $context
     * @param PurgeCache $purgeCache
     * @param Config $config
     */
    public function __construct(
        Context $context,
        PurgeCache $purgeCache,
        Config $config
    ) {
        parent::__construct($context);
        $this->purgeCache = $purgeCache;
        $this->config = $config;
    }

    /**
     * Purge by content type
     *
     * @return \Magento\Framework\App\ResponseInterface
     * @throws \Exception
     */
    public function execute()
    {
        try {
            if ($this->config->getType() == Config::FASTLY && $this->config->isEnabled()) {
                // check if content type is given
                $contentType = $this->getRequest()->getParam('content_types', false);
                if (!$contentType) {
                    throw new LocalizedException(__('Invalid content type "'.$contentType.'".'));
                }

                $result = $this->purgeCache->sendPurgeRequest([$contentType]);

                if ($result) {
                    $this->getMessageManager()->addSuccessMessage(__('The Fastly CDN has been cleaned.'));
                } else {
                    $this->getMessageManager()->addErrorMessage(
                        __('The purge request was not processed successfully.')
                    );
                }
            }
        } catch (\Exception $e) {
            $this->getMessageManager()->addErrorMessage(
                __('An error occurred while clearing the Fastly CDN: ') . $e->getMessage()
            );
        }
        return $this->_redirect('*/cache/index');
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Console\Command;

use Magento\Framework\App\Cache\Manager;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Storage\WriterInterface;
use Magento\Framework\App\ProductMetadataInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Class SerializeToJson
 *
 * @package Fastly\Cdn\Console\Command
 */
class SerializeToJson extends Command
{
    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var \Magento\Framework\Serialize\Serializer\Json
     */
    private $productMetadata;

    /**
     * @var WriterInterface
     */
    private $configWriter;

    /**
     * @var Manager
     */
    private $cacheManager;

    /**
     * @inheritdoc
     */
    protected function configure() // @codingStandardsIgnoreLine - required by parent class
    {
        $this->setName('fastly:format:serializetojson')
            ->setDescription('Converts Module Serialized data to Json format');
    }

    /**
     * SerializeToJson constructor.
     *
     * @param ScopeConfigInterface $scopeConfig
     * @param WriterInterface $configWriter
     * @param ProductMetadataInterface $productMetadata
     * @param Manager $cacheManager
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        WriterInterface $configWriter,
        ProductMetadataInterface $productMetadata,
        Manager $cacheManager
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->configWriter = $configWriter;
        $this->productMetadata = $productMetadata;
        $this->cacheManager = $cacheManager;

        parent::__construct();
    }

    /**
     * Converts Fastly serialized data to JSON format
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int|null|void
     */
    protected function execute(InputInterface $input, OutputInterface $output) // @codingStandardsIgnoreLine - required by parent class
    {

        $configPaths = [
            'geoip_country_mapping' => \Fastly\Cdn\Model\Config::XML_FASTLY_GEOIP_COUNTRY_MAPPING
        ];

        foreach ($configPaths as $path) {
            $magVer = $this->productMetadata->getVersion();
            if (version_compare($magVer, '2.2', '<')) {
                $output->writeln(
                    'Warning : This function is used for converting serialized data to JSON'
                        . ' (recommended for Magento versions above 2.2)'
                );
            }

            $oldData = $this->scopeConfig->getValue($path);

            try {
                $oldData = unserialize($oldData); // @codingStandardsIgnoreLine - used for conversion of old Magento format to json_decode
            } catch (\Exception $e) {
                $oldData = false;
            }

            if ($oldData === false) {
                $output->writeln(
                    'Invalid serialization format, unable to unserialize config data : ' . $path
                );

                return;
            }
            $oldData = (is_array($oldData)) ? $oldData : [];

            $newData = json_encode($oldData);
            if (false === $newData) {
                throw new \InvalidArgumentException('Unable to encode data.');
            }

            $this->configWriter->save($path, $newData); // @codingStandardsIgnoreLine - currently best way to resolve this
            $this->cacheManager->clean([\Magento\Framework\App\Cache\Type\Config::TYPE_IDENTIFIER]);

            $output->writeln('Config Cache Flushed');
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Model\Product;

use Fastly\Cdn\Model\Config;
use Magento\Catalog\Model\Product\Image as ImageModel;
use Magento\PageCache\Model\Config as PageCacheConfig;
use Magento\Catalog\Helper\Image as ImageHelper;

/**
 * Class Image
 *
 * @package Fastly\Cdn\Model\Product
 */
class Image extends ImageModel
{
    /**
     * @var array
     */
    private $fastlyParameters = [];
    /**
     * @var bool
     */
    private $isFastlyEnabled = null;
    /**
     * @var null
     */
    private $isForceLossyEnabled = null;
    /**
     * @var null
     */
    private $lossyParam = null;
    /**
     * @var null
     */
    private $lossyUrl = null;
    /**
     * @var null
     */
    private $fastlyUrl = null;

    /**
     * On/Off switch based on config value
     *
     * @return bool
     */
    private function isFastlyImageOptimizationEnabled()
    {
        if ($this->isFastlyEnabled !== null) {
            return $this->isFastlyEnabled;
        }

        $this->isFastlyEnabled = true;

        if ($this->_scopeConfig->isSetFlag(Config::XML_FASTLY_IMAGE_OPTIMIZATIONS) == false) {
            $this->isFastlyEnabled = false;
        }

        if ($this->_scopeConfig->getValue(PageCacheConfig::XML_PAGECACHE_TYPE) !== Config::FASTLY) {
            $this->isFastlyEnabled = false;
        }

        return $this->isFastlyEnabled;
    }

    /**
     * @return bool|null
     */
    public function isForceLossyEnabled()
    {
        if ($this->isForceLossyEnabled !== null) {
            return $this->isForceLossyEnabled;
        }

        $this->isForceLossyEnabled = true;

        if (empty($this->_scopeConfig->isSetFlag(Config::XML_FASTLY_FORCE_LOSSY))) {
            $this->isForceLossyEnabled = false;
        }

        if ($this->_scopeConfig->getValue(PageCacheConfig::XML_PAGECACHE_TYPE) !== Config::FASTLY) {
            $this->isForceLossyEnabled = false;
        }

        return $this->isForceLossyEnabled;
    }

    /**
     * Wrapper for original rotate()
     *
     * @param int $angle
     * @return \Magento\Catalog\Model\Product\Image
     */
    public function rotate($angle)
    {
        if ($this->isFastlyImageOptimizationEnabled() == false) {
            return parent::rotate($angle);
        }

        return $this->fastlyRotate($angle);
    }

    /**
     * Fastly implementation of rotation
     *
     * @param int $angle
     * @return \Magento\Catalog\Model\Product\Image
     */
    private function fastlyRotate($angle)
    {
        $angle = (int) $angle;

        $orient = null;
        if ($angle == 90) {
            $orient = 'r';
        }

        if ($angle == -90 || $angle == 270) {
            $orient = 'l';
        }

        if ($angle == 180) {
            $orient = 3;
        }

        if ($orient !== null) {
            $this->fastlyParameters['orient'] = $orient;
        }

        return $this;
    }

    /**
     * Wrapper for original resize()
     *
     * @see \Magento\Framework\Image\Adapter\AbstractAdapter
     * @return \Magento\Catalog\Model\Product\Image
     */
    public function resize()
    {
        if ($this->isFastlyImageOptimizationEnabled() == false) {
            return parent::resize();
        }

        return $this->fastlyResize();
    }

    /**
     * Fastly implementation of resize
     *
     * @see \Magento\Framework\Image\Adapter\AbstractAdapter
     * @return \Magento\Catalog\Model\Product\Image
     */
    private function fastlyResize()
    {
        if ($this->getWidth() === null && $this->getHeight() === null) {
            return $this;
        }

        $this->adjustSize();

        return $this;
    }

    private function adjustSize()
    {
        $originalImage = $this->_mediaDirectory->getAbsolutePath($this->getBaseFile());
        $originalSize = getimagesize($originalImage);

        if ($this->getWidth() > $originalSize[0]) {
            $this->setWidth($originalSize[0]);
        }

        if ($this->getHeight() > $originalSize[1]) {
            $this->setHeight($originalSize[1]);
        }

        $this->fastlyParameters['width'] = $this->_width;
        $this->fastlyParameters['height'] = $this->_height;

        if ($this->_scopeConfig->isSetFlag(Config::XML_FASTLY_IMAGE_OPTIMIZATION_CANVAS) == true) {
            // Make sure Fastly delivers the specified size, even with letterboxing or pillarboxing.
            // We'll use aspect ratio canvas to avoid issues when using dpr
            $this->fastlyParameters['canvas'] = "{$this->_width}:{$this->_height}";
        }
    }

    /**
     * Return resized product image information
     *
     * @return array
     */
    public function getResizedImageInfo()
    {
        if ($this->isFastlyImageOptimizationEnabled() == false) {
            return parent::getResizedImageInfo();
        }

        // Return image data
        if ($this->getBaseFile() !== null) {
            return [
                0 => $this->getWidth(),
                1 => $this->getHeight()
            ];
        }

        // No image, parse the placeholder
        $asset = $this->_assetRepo->createAsset(
            "Magento_Catalog::images/product/placeholder/{$this->getDestinationSubdir()}.jpg"
        );
        $img = $asset->getSourceFile();
        $imageInfo = getimagesize($img);

        $this->setWidth($imageInfo[0]);
        $this->setHeight($imageInfo[1]);

        return $imageInfo;
    }

    /**
     * Wrapper for original saveFile()
     *
     * @return \Magento\Catalog\Model\Product\Image
     */
    public function saveFile()
    {
        if ($this->isFastlyImageOptimizationEnabled() == false) {
            return parent::saveFile();
        }

        return $this;
    }

    /**
     * Wrapper for original getUrl()
     *
     * @return string
     */
    public function getUrl()
    {
        // returns original url if IO and force lossy are disabled
        if ($this->isFastlyImageOptimizationEnabled() == false && $this->isForceLossyEnabled() == false) {
            return parent::getUrl();
        }
        // retrieves force lossy url or param if force lossy is enabled
        if ($this->isForceLossyEnabled() != false) {
            $this->getForceLossyUrl();
        }
        // retrieves Fastly url if force lossy url is not set
        if (!$this->lossyUrl) {
            $this->getFastlyUrl();
        }
        // returns url with set parameters
        if ($this->lossyParam) {
            return $this->fastlyUrl . $this->lossyParam;
        } elseif ($this->lossyUrl) {
            return $this->lossyUrl;
        } else {
            return $this->fastlyUrl;
        }
    }

    /**
     * Creates a force lossy url param or url + param depending if IO is disabled or enabled
     */
    public function getForceLossyUrl()
    {
        $baseFile = $this->getBaseFile();
        $extension = pathinfo($baseFile, PATHINFO_EXTENSION); // @codingStandardsIgnoreLine
        $url = $this->getBaseFileUrl($baseFile);
        if ($extension == 'png' || $extension == 'bmp') {
            if ($this->isFastlyImageOptimizationEnabled() == false) {
                $this->lossyUrl = $url . '?format=jpeg';
            } else {
                $this->lossyParam = '&format=jpeg';
            }
        }
    }

    /**
     * Creates a url with fastly parameters
     */
    public function getFastlyUrl()
    {
        $baseFile = $this->getBaseFile();
        $url = $this->getBaseFileUrl($baseFile);

        $imageQuality = $this->_scopeConfig->getValue(Config::XML_FASTLY_IMAGE_OPTIMIZATION_IMAGE_QUALITY);

        $this->setQuality($imageQuality);

        $this->fastlyParameters['quality'] = $this->_quality;

        if ($this->_scopeConfig->isSetFlag(Config::XML_FASTLY_IMAGE_OPTIMIZATION_BG_COLOR) == true) {
            $this->fastlyParameters['bg-color'] = implode(',', $this->_backgroundColor);
        }
        if ($this->_keepAspectRatio == true) {
            $this->fastlyParameters['fit'] = 'bounds';
        }
        $this->fastlyUrl = $url . '?' . $this->compileFastlyParameters();
    }

    public function getBaseFileUrl($baseFile)
    {
        if ($baseFile === null || $this->isBaseFilePlaceholder()) {
            $imageHelper = \Magento\Framework\App\ObjectManager::getInstance()->get(ImageHelper::class);
            $url = $imageHelper->getDefaultPlaceholderUrl($this->getDestinationSubdir());
        } else {
            $url = $this->_storeManager->getStore()->getBaseUrl(
                \Magento\Framework\UrlInterface::URL_TYPE_MEDIA
            );
            $url .= $baseFile;
        }

        return $url;
    }

    /**
     * Compiles the fastly GET parameters
     *
     * @return string
     */
    private function compileFastlyParameters()
    {
        if (isset($this->fastlyParameters['width']) == false) {
            $this->fastlyParameters['height'] = $this->_height;
            $this->fastlyParameters['width'] = $this->_width;
        }

        $params = [];
        foreach ($this->fastlyParameters as $key => $value) {
            $params[] = $key . '=' . $value;
        }

        return implode('&', $params);
    }

    /**
     * Wrapper for original isCached()
     *
     * @return bool
     */
    public function isCached()
    {
        if ($this->isFastlyImageOptimizationEnabled() == false) {
            return parent::isCached();
        }

        return false;
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\ImageOptimization;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Request\Http;
use Magento\Framework\Controller\Result\JsonFactory;
use Fastly\Cdn\Model\Api;
use Fastly\Cdn\Helper\Vcl;
use Fastly\Cdn\Model\Config;

/**
 * Class IoDefaultConfigOptions
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\ImageOptimization
 */
class IoDefaultConfigOptions extends Action
{
    /**
     * @var Http
     */
    private $request;
    /**
     * @var JsonFactory
     */
    private $resultJson;
    /**
     * @var Api
     */
    private $api;
    /**
     * @var Vcl
     */
    private $vcl;
    /**
     * @var Config
     */
    private $config;

    /**
     * IoDefaultConfigOptions constructor.
     *
     * @param Context $context
     * @param Http $request
     * @param JsonFactory $resultJsonFactory
     * @param Api $api
     * @param Vcl $vcl
     * @param Config $config
     */
    public function __construct(
        Context $context,
        Http $request,
        JsonFactory $resultJsonFactory,
        Api $api,
        Vcl $vcl,
        Config $config
    ) {
        $this->request = $request;
        $this->resultJson = $resultJsonFactory;
        $this->api = $api;
        $this->vcl = $vcl;
        $this->config = $config;
        parent::__construct($context);
    }

    /**
     * Upload snippet with updated IO default config options
     *
     * @return $this|\Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        $result = $this->resultJson->create();
        try {
            $activate_flag = $this->getRequest()->getParam('activate_flag');
            $activeVersion = $this->getRequest()->getParam('active_version');
            $formData = $this->getRequest()->getParams();
            if (in_array("", $formData)) {
                return $result->setData([
                    'status'    => false,
                    'msg'       => 'Please fill in the required fields.'
                ]);
            }
            $service = $this->api->checkServiceDetails();
            $this->vcl->checkCurrentVersionActive($service->versions, $activeVersion);
            $currActiveVersion = $this->vcl->getCurrentVersion($service->versions);
            $clone = $this->api->cloneVersion($currActiveVersion);
            $id = $service->id . '-' . $clone->number . '-imageopto';

            $params = json_encode([
                'data' => [
                    'id' => $id,
                    'type' => 'io_settings',
                    'attributes' => [
                        'webp'          => $this->getRequest()->getParam('webp'),
                        'webp_quality'  => $this->getRequest()->getParam('webp_quality'),
                        'jpeg_type'     => $this->getRequest()->getParam('jpeg_type'),
                        'jpeg_quality'  => $this->getRequest()->getParam('jpeg_quality'),
                        'upscale'       => $this->getRequest()->getParam('upscale'),
                        'resize_filter' => $this->getRequest()->getParam('resize_filter')
                    ]
                ]
            ]);

            $configureIo = $this->api->configureImageOptimizationDefaultConfigOptions($params, $clone->number);

            if (!$configureIo) {
                return $result->setData([
                    'status'    => false,
                    'msg'       => 'Failed to update image optimization default config options.'
                ]);
            }

            $this->api->validateServiceVersion($clone->number);

            if ($activate_flag === 'true') {
                $this->api->activateVersion($clone->number);
            }
            if ($this->config->areWebHooksEnabled() && $this->config->canPublishConfigChanges()) {
                $this->api->sendWebHook('*Image optimization default config options have been updated*');
            }

            $comment = ['comment' => 'Magento Module updated the Image Optimization Default Configuration'];
            $this->api->addComment($clone->number, $comment);

            return $result->setData([
                'status'            => true,
                'active_version'    => $clone->number
            ]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\App\ResponseInterface as Response;
use Fastly\Cdn\Model\Config as FastlyConfig;

/**
 * Class MarkEsiBlock
 *
 * @package Fastly\Cdn\Observer
 */
class MarkEsiBlock implements ObserverInterface
{
    /**
     * @var FastlyConfig
     */
    private $fastlyConfig;
    /**
     * @var Response
     */
    private $response;

    /**
     * MarkEsiPage constructor.
     * @param Response $response
     * @param FastlyConfig $fastlyConfig
     */
    public function __construct(
        Response $response,
        FastlyConfig $fastlyConfig
    ) {
        $this->fastlyConfig = $fastlyConfig;
        $this->response = $response;
    }

    /**
     * Set x-esi header on ESI response request
     * If omitted, causes issues with embedded esi tags
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     */
    public function execute(Observer $observer) // @codingStandardsIgnoreLine - required, but not needed
    {
        if ($this->fastlyConfig->isFastlyEnabled() != true) {
            return;
        }

        // If not set, causes issues with embedded ESI
        $this->response->setHeader("x-esi", "1");
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Blocking;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Request\Http;
use Magento\Framework\Controller\Result\JsonFactory;
use Fastly\Cdn\Model\Config;
use Fastly\Cdn\Model\Api;
use Fastly\Cdn\Helper\Vcl;
use Magento\Framework\Exception\LocalizedException;

/**
 * Class Blocking
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Blocking
 */
class Blocking extends Action
{
    /**
     * @var Http
     */
    private $request;
    /**
     * @var JsonFactory
     */
    private $resultJson;
    /**
     * @var Config
     */
    private $config;
    /**
     * @var Api
     */
    private $api;
    /**
     * @var Vcl
     */
    private $vcl;

    /**
     * Blocking constructor.
     *
     * @param Context $context
     * @param Http $request
     * @param JsonFactory $resultJsonFactory
     * @param Config $config
     * @param Api $api
     * @param Vcl $vcl
     */
    public function __construct(
        Context $context,
        Http $request,
        JsonFactory $resultJsonFactory,
        Config $config,
        Api $api,
        Vcl $vcl
    ) {
        $this->request = $request;
        $this->resultJson = $resultJsonFactory;
        $this->config = $config;
        $this->api = $api;
        $this->vcl = $vcl;
        parent::__construct($context);
    }

    /**
     * Upload Blocking snippets
     *
     * @return $this|\Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        $result = $this->resultJson->create();
        try {
            $activeVersion = $this->getRequest()->getParam('active_version');
            $activateVcl = $this->getRequest()->getParam('activate_flag');
            $service = $this->api->checkServiceDetails();
            $this->vcl->checkCurrentVersionActive($service->versions, $activeVersion);
            $currActiveVersion = $this->vcl->getCurrentVersion($service->versions);
            $clone = $this->api->cloneVersion($currActiveVersion);
            $checkIfSettingExists = $this->api->hasSnippet($activeVersion, Config::BLOCKING_SETTING_NAME);

            $snippet = $this->config->getVclSnippets(
                Config::VCL_BLOCKING_PATH,
                Config::VCL_BLOCKING_SNIPPET
            );

            $country_codes = $this->prepareCountryCodes($this->config->getBlockByCountry());
            $acls = $this->prepareAcls($this->config->getBlockByAcl());

            $blockedItems = $country_codes . $acls;
            $strippedBlockedItems = substr($blockedItems, 0, strrpos($blockedItems, '||', -1));

            if (!$checkIfSettingExists) {
                foreach ($snippet as $key => $value) {
                    $name = Config::FASTLY_MAGENTO_MODULE . '_blocking_' . $key;

                    if ($this->api->hasSnippet($clone->number, $name) == true) {
                        $this->api->removeSnippet($clone->number, $name);
                        continue;
                    }

                    if ($strippedBlockedItems === '') {
                        $value = '';
                    } else {
                        $strippedBlockedItems = $this->config->processBlockedItems($strippedBlockedItems);
                        $value = str_replace('####BLOCKED_ITEMS####', $strippedBlockedItems, $value);
                    }

                    $snippetData = [
                        'name'      => $name,
                        'type'      => $key,
                        'dynamic'   => 1,
                        'priority'  => 5,
                        'content'   => $value
                    ];

                    $this->api->uploadSnippet($clone->number, $snippetData);
                }
            } else {
                foreach ($snippet as $key => $value) {
                    $name = Config::FASTLY_MAGENTO_MODULE . '_blocking_' . $key;

                    if ($this->api->hasSnippet($clone->number, $name) == true) {
                        $this->api->removeSnippet($clone->number, $name);
                    }
                }
            }

            $this->api->validateServiceVersion($clone->number);

            if ($activateVcl === 'true') {
                $this->api->activateVersion($clone->number);
            }

            $this->sendWebhook($checkIfSettingExists, $clone);

            $comment = ['comment' => 'Magento Module turned ON Blocking'];
            if ($checkIfSettingExists) {
                $comment = ['comment' => 'Magento Module turned OFF Blocking'];
            }
            $this->api->addComment($clone->number, $comment);

            return $result->setData([
                'status' => true
            ]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }

    /**
     * Prepares ACL VCL snippets
     *
     * @param $blockedAcls
     * @return string
     */
    private function prepareAcls($blockedAcls)
    {
        $result = '';

        if ($blockedAcls != null) {
            $blockedAclsPieces = explode(",", $blockedAcls);
            foreach ($blockedAclsPieces as $acl) {
                $result .= ' req.http.Fastly-Client-Ip ~ ' . $acl . ' ||';
            }
        }

        return $result;
    }

    /**
     * Prepares blocked countries VCL snippet
     *
     * @param $blockedCountries
     * @return string
     */
    private function prepareCountryCodes($blockedCountries)
    {
        $result = '';

        if ($blockedCountries != null) {
            $blockedCountriesPieces = explode(",", $blockedCountries);
            foreach ($blockedCountriesPieces as $code) {
                $result .= ' client.geo.country_code == "' . $code . '" ||';
            }
        }

        return $result;
    }

    private function sendWebhook($checkIfSettingExists, $clone)
    {
        if ($this->config->areWebHooksEnabled() && $this->config->canPublishConfigChanges()) {
            if ($checkIfSettingExists) {
                $this->api->sendWebHook('*Blocking has been turned OFF in Fastly version ' . $clone->number . '*');
            } else {
                $this->api->sendWebHook('*Blocking has been turned ON in Fastly version ' . $clone->number . '*');
            }
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Model\PageCache;

use \Magento\PageCache\Model\Config;

/**
 * Model is responsible for replacing default vcl template
 * file configuration with user-defined from configuration
 *
 * @author     Magento Core Team <core@magentocommerce.com>
 */

/**
 * Class ConfigPlugin
 *
 * @package Fastly\Cdn\Model\PageCache
 */
class ConfigPlugin
{
    /**
     * Return cache type "Varnish" if Fastly is configured.
     *
     * @param Config $config
     * @param string $result
     *
     * @return int
     */
    public function afterGetType(Config $config, $result)
    {
        if (!($config instanceof \Fastly\Cdn\Model\Config)) {
            if ($result == \Fastly\Cdn\Model\Config::FASTLY) {
                return Config::VARNISH;
            }
        }
        return $result;
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Advanced;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Request\Http;
use Magento\Framework\Controller\Result\JsonFactory;
use Fastly\Cdn\Model\Config;
use Fastly\Cdn\Model\Api;
use Fastly\Cdn\Helper\Vcl;

/**
 * Class ForceTls
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Advanced
 */
class ForceTls extends Action
{
    /**
     * @var Http
     */
    private $request;
    /**
     * @var JsonFactory
     */
    private $resultJson;
    /**
     * @var Config
     */
    private $config;
    /**
     * @var Api
     */
    private $api;
    /**
     * @var Vcl
     */
    private $vcl;

    /**
     * ForceTls constructor.
     *
     * @param Context $context
     * @param Http $request
     * @param JsonFactory $resultJsonFactory
     * @param Config $config
     * @param Api $api
     * @param Vcl $vcl
     */
    public function __construct(
        Context $context,
        Http $request,
        JsonFactory $resultJsonFactory,
        Config $config,
        Api $api,
        Vcl $vcl
    ) {
        $this->request = $request;
        $this->resultJson = $resultJsonFactory;
        $this->config = $config;
        $this->api = $api;
        $this->vcl = $vcl;

        parent::__construct($context);
    }

    /**
     * Upload VCL snippets
     *
     * @return $this|\Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        $result = $this->resultJson->create();
        try {
            $activeVersion = $this->getRequest()->getParam('active_version');
            $activateVcl = $this->getRequest()->getParam('activate_flag');
            $service = $this->api->checkServiceDetails();
            $this->vcl->checkCurrentVersionActive($service->versions, $activeVersion);
            $currActiveVersion = $this->vcl->getCurrentVersion($service->versions);
            $clone = $this->api->cloneVersion($currActiveVersion);
            $checkIfSettingExists = $this->api->hasSnippet($activeVersion, Config::FORCE_TLS_SETTING_NAME);
            $snippets = $this->config->getVclSnippets(Config::FORCE_TLS_PATH);

            if (!$checkIfSettingExists) {
                // Add force TLS snippet
                foreach ($snippets as $key => $value) {
                    $snippetData = [
                        'name'      => Config::FASTLY_MAGENTO_MODULE . '_force_tls_' . $key,
                        'type'      => $key,
                        'dynamic'   => "0",
                        'priority'  => 10,
                        'content'   => $value
                    ];
                    $this->api->uploadSnippet($clone->number, $snippetData);
                }
            } else {
                // Remove force TLS snippet
                foreach ($snippets as $key => $value) {
                    $name = Config::FASTLY_MAGENTO_MODULE.'_force_tls_'.$key;
                    $this->api->removeSnippet($clone->number, $name);
                }
            }

            $this->api->validateServiceVersion($clone->number);

            if ($activateVcl === 'true') {
                $this->api->activateVersion($clone->number);
            }

            if ($this->config->areWebHooksEnabled() && $this->config->canPublishConfigChanges()) {
                if ($checkIfSettingExists) {
                    $this->api->sendWebHook('*Force TLS has been turned OFF in Fastly version '. $clone->number . '*');
                } else {
                    $this->api->sendWebHook('*Force TLS has been turned ON in Fastly version '. $clone->number . '*');
                }
            }

            $comment = ['comment' => 'Magento Module turned ON Force TLS'];
            if ($checkIfSettingExists) {
                $comment = ['comment' => 'Magento Module turned OFF Force TLS'];
            }
            $this->api->addComment($clone->number, $comment);

            return $result->setData(['status' => true]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Configuration;

use Fastly\Cdn\Model\Config;
use Fastly\Cdn\Model\Api;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Fastly\Cdn\Helper\Vcl;
use Magento\Framework\Controller\Result\JsonFactory;

/**
 * Class CustomerInfo
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\Configuration
 */
class CustomerInfo extends Action
{
    /**
     * @var Api
     */
    private $api;
    /**
     * @var Config
     */
    private $config;
    /**
     * @var Vcl
     */
    private $vcl;
    /**
     * @var JsonFactory
     */
    private $resultJsonFactory;

    /**
     * ServiceInfo constructor.
     *
     * CustomerInfo constructor.
     * @param Context $context
     * @param Config $config
     * @param Api $api
     * @param JsonFactory $resultJsonFactory
     * @param Vcl $vcl
     */
    public function __construct(
        Context $context,
        Config $config,
        Api $api,
        JsonFactory $resultJsonFactory,
        Vcl $vcl
    ) {
        $this->api = $api;
        $this->config = $config;
        $this->resultJsonFactory = $resultJsonFactory;
        $this->vcl = $vcl;

        parent::__construct($context);
    }

    /**
     * Checking service details
     *
     * @return \Magento\Framework\Controller\Result\Json
     */
    public function execute()
    {
        $result = $this->resultJsonFactory->create();
        try {
            $customer = $this->api->getCustomerInfo();

            if (!$customer) {
                return $result->setData(['status' => false]);
            }

            return $result->setData([
                'status'    => true,
                'customer'  => $customer
            ]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }
}


<?php
/**
 * Fastly CDN for Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Fastly CDN for Magento End User License Agreement
 * that is bundled with this package in the file LICENSE_FASTLY_CDN.txt.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Fastly CDN to newer
 * versions in the future. If you wish to customize this module for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Fastly
 * @package     Fastly_Cdn
 * @copyright   Copyright (c) 2016 Fastly, Inc. (http://www.fastly.com)
 * @license     BSD, see LICENSE_FASTLY_CDN.txt
 */
namespace Fastly\Cdn\Controller\Adminhtml\FastlyCdn\BasicAuthentication;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Request\Http;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Fastly\Cdn\Model\Config;
use Fastly\Cdn\Model\Api;
use Fastly\Cdn\Helper\Vcl;
use Magento\Framework\Controller\ResultInterface;

/**
 * Class EnableAuth
 *
 * @package Fastly\Cdn\Controller\Adminhtml\FastlyCdn\BasicAuthentication
 */
class EnableAuth extends Action
{
    /**
     * @var Http
     */
    private $request;
    /**
     * @var JsonFactory
     */
    private $resultJson;
    /**
     * @var Config
     */
    private $config;
    /**
     * @var Api
     */
    private $api;
    /**
     * @var Vcl
     */
    private $vcl;

    /**
     * ForceTls constructor.
     *
     * @param Context $context
     * @param Http $request
     * @param JsonFactory $resultJsonFactory
     * @param Config $config
     * @param Api $api
     * @param Vcl $vcl
     */
    public function __construct(
        Context $context,
        Http $request,
        JsonFactory $resultJsonFactory,
        Config $config,
        Api $api,
        Vcl $vcl
    ) {
        $this->request = $request;
        $this->resultJson = $resultJsonFactory;
        $this->config = $config;
        $this->api = $api;
        $this->vcl = $vcl;

        parent::__construct($context);
    }

    /**
     * Upload Auth VCL snippets
     *
     * @return $this|ResponseInterface|ResultInterface
     */
    public function execute()
    {
        $result = $this->resultJson->create();
        try {
            $activeVersion = $this->getRequest()->getParam('active_version');
            $activateVcl = $this->getRequest()->getParam('activate_flag');
            $service = $this->api->checkServiceDetails();
            $enabled = false;
            $this->vcl->checkCurrentVersionActive($service->versions, $activeVersion);
            $currActiveVersion = $this->vcl->getCurrentVersion($service->versions);
            $vclPath = Config::VCL_AUTH_SNIPPET_PATH;
            $snippets = $this->config->getVclSnippets($vclPath);

            // Check if snippets exist
            $status = true;
            foreach ($snippets as $key => $value) {
                $name = Config::FASTLY_MAGENTO_MODULE.'_basic_auth_'.$key;
                $status = $this->api->getSnippet($activeVersion, $name);

                if (!$status) {
                    break;
                }
            }

            if (!$status) {
                // Check if Auth has entries
                $this->api->checkAuthDictionaryPopulation($activeVersion);
                $clone = $this->api->cloneVersion($currActiveVersion);

                // Insert snippet
                foreach ($snippets as $key => $value) {
                    $snippetData = [
                        'name'      => Config::FASTLY_MAGENTO_MODULE.'_basic_auth_'.$key,
                        'type'      => $key,
                        'dynamic'   => "0",
                        'content'   => $value,
                        'priority'  => 10
                    ];
                    $this->api->uploadSnippet($clone->number, $snippetData);
                }

                $enabled = true;
            } else {
                $clone = $this->api->cloneVersion($currActiveVersion);

                // Remove snippets
                foreach ($snippets as $key => $value) {
                    $name = Config::FASTLY_MAGENTO_MODULE.'_basic_auth_'.$key;
                    $this->api->removeSnippet($clone->number, $name);
                }
            }

            $this->api->validateServiceVersion($clone->number);

            if ($activateVcl === 'true') {
                $this->api->activateVersion($clone->number);
            }

            $this->sendWebhook($enabled, $clone);

            $comment = ['comment' => 'Magento Module turned ON Basic Authentication'];
            if (!$enabled) {
                $comment = ['comment' => 'Magento Module turned OFF Basic Authentication'];
            }
            $this->api->addComment($clone->number, $comment);

            return $result->setData(['status' => true]);
        } catch (\Exception $e) {
            return $result->setData([
                'status'    => false,
                'msg'       => $e->getMessage()
            ]);
        }
    }

    private function sendWebhook($enabled, $clone)
    {
        if ($this->config->areWebHooksEnabled() && $this->config->canPublishConfigChanges()) {
            if (!$enabled) {
                $this->api->sendWebHook(
                    '*Basic Authentication has been turned OFF in Fastly version ' . $clone->number . '*'
                );
            } else {
                $this->api->sendWebHook(
                    '*Basic Authentication has been turned ON in Fastly version '. $clone->number . '*'
                );
            }
        }
    }
}