steadlane/silverstripe-cloudflare

View on GitHub
src/CloudFlare.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace SteadLane\Cloudflare;

use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Session;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\GraphQL\Controller;
use SteadLane\Cloudflare\Messages\Notifications;

/**
 * Class CloudFlare
 * @package SteadLane\Cloudflare
 */
class CloudFlare
{
    use Configurable;
    use Injectable;
    use Extensible;

    /**
     * @var string
     */
    const CF_ZONE_ID_CACHE_KEY = 'CFZoneID';

    /**
     * This will toggle to TRUE when a ZoneID has been detected thus allowing the functionality in the admin panel to
     * be available.
     *
     * @var bool
     */
    protected static $ready = false;

    /**
     * Ensures that Cloudflare authentication credentials are defined as constants
     *
     * @return bool
     */
    public function hasCFCredentials()
    {
        if (!getenv('TRAVIS') && (!defined('CLOUDFLARE_AUTH_EMAIL') || !defined('CLOUDFLARE_AUTH_KEY')) && (!Environment::getEnv('CLOUDFLARE_AUTH_EMAIL') || !Environment::getEnv('CLOUDFLARE_AUTH_KEY'))) {
            return false;
        }

        return true;
    }

    /**
     * Fetches Cloudflare Credentials from YML configuration
     *
     * @return array|bool
     */
    public function getCFCredentials()
    {
        if ($this->hasCFCredentials()) {
            if (Environment::getEnv('CLOUDFLARE_AUTH_EMAIL')) {
                return array(
                    'email' => Environment::getEnv('CLOUDFLARE_AUTH_EMAIL'),
                    'key'   => Environment::getEnv('CLOUDFLARE_AUTH_KEY')
                );
            } else {
                return array(
                    'email' => CLOUDFLARE_AUTH_EMAIL,
                    'key'   => CLOUDFLARE_AUTH_KEY
                );
            }
        }

        return false;
    }

    /**
     * Gathers the current server name, which will be used as the Cloudflare zone ID
     *
     * @return string
     */
    public function getServerName()
    {
        $serverName = '';
        if (isset($_SERVER['HTTP_HOST']) && !empty($_SERVER['HTTP_HOST'])) {
            $serverName = Convert::raw2xml($_SERVER['HTTP_HOST']); // "Fixes" #1 (what?)
        } else if (!empty($_SERVER['SERVER_NAME'])) {
            $server = Convert::raw2xml($_SERVER); // "Fixes" #1
            $serverName = $server['SERVER_NAME'];
        }

        // CI support
        if (getenv('TRAVIS')) {
            $serverName = getenv('CLOUDFLARE_DUMMY_SITE');
        }

        // Remove protocols, etc
        $replaceWith = array(
            'http://' => '',
            'https://' => ''
        );
        if (!isset($_SERVER['HTTP_HOST'])) { $replaceWith['www.']=''; } // hack!
        $serverName = str_replace(array_keys($replaceWith), array_values($replaceWith), $serverName);

        // Allow extensions to modify or replace the server name if required
        $this->extend('updateCloudFlareServerName', $serverName);

        return $serverName;
    }

    /**
     * Returns whether caching is enabled for the Cloudflare class instance
     * @return bool
     */
    public function getCacheEnabled()
    {
        return (bool)self::config()->get('cache_enabled') === true;
    }

    /**
     * Gets the CF Zone ID for the current domain.
     *
     * @return string|bool
     */
    public function fetchZoneID()
    {
        if (!$this->hasCFCredentials()) {
            return null;
        }

        if ($this->getCacheEnabled()) {
            $factory = Injector::inst()->get(CacheInterface::class . '.CloudflareCache');
            if ($factory && ($cache = $factory->get(self::CF_ZONE_ID_CACHE_KEY))) {
                $this->isReady(true);
                return $cache;
            }
        }

        $serverName = $this->getServerName();

        if ($serverName == 'localhost') {
            Notifications::handleMessage(
                _t(
                    "CloudFlare.NoLocalhost",
                    "This module does not operate under <strong>localhost</strong>." .
                    "Please ensure your website has a resolvable DNS and access the website via the domain."
                ),
                ["type"=>"error"]
            );

            return false;
        }

        $url = "https://api.cloudflare.com/client/v4/zones" .
            "?name={$serverName}" .
            "&status=active&page=1&per_page=20" .
            "&order=status&direction=desc&match=all";

        $result = $this->curlRequest($url, null, 'GET');

        $array = json_decode($result, true);

        if (!is_array($array) || !array_key_exists("result", $array) || empty($array['result'])) {
            $this->isReady(false);
            Notifications::handleMessage(
                _t(
                    "CloudFlare.ZoneIdNotFound",
                    "Unable to detect a Zone ID for <strong>{server_name}</strong> under the defined Cloudflare" .
                    " user.<br/><br/>Please create a new zone under this account to use this module on this domain.",
                    "",
                    array(
                        "server_name" => $serverName
                    )
                ),
                ["type"=>"error"]
            );

            return false;
        }

        $zoneID = $array['result'][0]['id'];

        if ($this->getCacheEnabled() && isset($factory)) {
            $factory->set(self::CF_ZONE_ID_CACHE_KEY, $zoneID);
        }

        $this->isReady(true);

        return $zoneID;
    }

    /**
     * Set or get the ready state
     *
     * @param null $state
     *
     * @return bool|null
     */
    public function isReady($state = null)
    {
        if ($state!==null) {
            self::$ready = (bool)$state;
            return $state;
        }

        $this->fetchZoneID();
        return self::$ready;
    }

    /**
     * Get or Set the Session Jar
     *
     * @return array|mixed|null|Session
     */
    public function getSessionJar()
    {
        $session = Controller::curr()->getRequest()->getSession()->get('slCloudFlare');
        if (!$session) {
            $session=array();
            Controller::curr()->getRequest()->getSession()->set('slCloudFlare',$session);
        }
        return $session;
    }

    /**
     * @param array|mixed $data
     *
     * @return $this
     */
    public function setSessionJar($data)
    {
        Controller::curr()->getRequest()->getSession()->set('slCloudFlare', $data);
        return $this;
    }

    /**
     * Returns the cURL execution timeout limit (seconds)
     *
     * @return int
     */
    public function getCurlTimeout()
    {
        return (int)self::config()->get('curl_timeout');
    }

    ///**
    // * Fetch the Cloudflare configuration
    // * @return \SilverStripe\Core\Config\Config_ForClass
    // */
    //public static function config()
    //{
    //    return Config::inst()->forClass('CloudFlare');
    //}

    /**
     * Sends our cURL requests with our custom auth headers
     *
     * @param string $url    The URL
     * @param null|string|array $data   Optional array of data to send
     * @param string $method GET, PUT, POST, DELETE etc
     *
     * @return string JSON
     */
    public function curlRequest($url, $data = null, $method = 'DELETE')
    {
        $curlTimeout = $this->getCurlTimeout();

        $curl = curl_init();

        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $curlTimeout);
        curl_setopt($curl, CURLOPT_TIMEOUT, $curlTimeout);

        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);


        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->getAuthHeaders());
        // This is intended, and was/is required by Cloudflare at one point
        curl_setopt($curl, CURLOPT_USERAGENT, $this->getUserAgent());

        if (!is_null($data)) {
            if (is_array($data)) {
                $data = json_encode($data);
            }
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }

        $result = curl_exec($curl);
        $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

        // Handle any errors
        if (false === $result) {
            self::debug("Error connecting to Cloudflare: (code=".$responseCode.")\n".curl_error($curl));
            user_error(sprintf("Error connecting to Cloudflare: (code=%s)\n%s", ((string)$responseCode), curl_error($curl)), E_USER_ERROR);
        }
        curl_close($curl);

        self::debug("CloudFlare::curlRequest() / url = [".$url."] data=".(is_null($data)?"NULL":$data)." / response code=[".$responseCode."]".($result===false?" error=[".curl_error($curl)."]":""));
        //self::debug("CloudFlare::curlRequest() / result = [".$result."]");

        return $result;
    }

    /**
     * Fake a user agent
     *
     * @return string
     */
    public function getUserAgent()
    {
        return "Mozilla/5.0 " .
        "(Macintosh; Intel Mac OS X 10_11_6) " .
        "AppleWebKit/537.36 (KHTML, like Gecko) " .
        "Chrome/53.0.2785.143 Safari/537.36";
    }

    /**
     * Get Authentication Headers
     *
     * @return array
     */
    public function getAuthHeaders()
    {
        if (getenv('TRAVIS')) {
            $auth = array(
                'email' => getenv('AUTH_EMAIL'),
                'key' => getenv('AUTH_KEY'),
            );
        } elseif (!$auth = $this->getCFCredentials()) {
            self::debug("Cloudflare API credentials have not been provided.");
            user_error("Cloudflare API credentials have not been provided.");
            //return null;
            exit;
        }

        $headers = array(
            "X-Auth-Email: {$auth['email']}",
            "X-Auth-Key: {$auth['key']}",
            "Content-Type: application/json"
        );

        $this->extend("updateCloudFlareAuthHeaders", $headers);

        return $headers;
    }

    /**
     * Appends server name to input
     *
     * @param array|string $input
     *
     * @return array|string
     */
    public function prependServerName($input)
    {
        $serverName = CloudFlare::singleton()->getServerName();

        if (is_array($input)) {

            $stack = array();
            foreach ($input as $string) {
                $stack[] = $this->prependServerName($string);
            }

            return $stack;
        }


        if (strstr($input, "http://") || strstr($input, "https://")) {
            $input = str_replace(array("http://", "https://"), "", trim($input));
        }

        if (strstr($input, $serverName)) {
            return "http://" . $input;
        }

        return "http://" . str_replace("//", "/", "{$serverName}/{$input}");
    }

    public function canUser($member)
    {
        return true;
    }

    /**
     * @param string $type         Class you want to test, this is purely based on the file naming convention
     *                             in /tests/Mock of {$type}{$isSuccessful}.json
     * @param bool   $isSuccessful Should the response be of successful nature or a failure?
     *
     * @return array
     */
    public static function getMockResponse($type, $isSuccessful) {
        $mockDir = rtrim($_SERVER['DOCUMENT_ROOT'], '/') . Director::baseURL() . "cloudflare/tests/Mock/";

        if (getenv('TRAVIS')) {
            $mockDir = '/home/travis/builds/ss' . $mockDir;
        }

        if (!is_dir($mockDir)) {
            user_error("The directory $mockDir needs to exist to get mock responses from the Cloudflare module", E_USER_ERROR);
        }

        $filename = ucfirst($type) . (($isSuccessful) ? "Success" : "Failure") . ".json";
        $path = $mockDir . $filename;

        $result = json_decode(file_get_contents($path), true);

        if (!$result || !is_array($result)) {
            user_error($filename . " contents must be a valid JSON string", E_USER_ERROR);
        }

        return $result;
    }

    /**
     * @param string $msg
     * @param Object|array|null $obj
     */
    public static function debug($msg, $obj=null)
    {
        if (CloudFlare::config()->debug && Injector::inst()->get(LoggerInterface::class)) {
            $error=$msg;
            if ($obj) {
                $error.=' '.print_r($obj,true);
            }
            Injector::inst()->get(LoggerInterface::class)->debug($error);
        }
    }
}