netglue/ZF2-Prismic-Module

View on GitHub
src/NetgluePrismic/Mvc/Controller/PrismicController.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace NetgluePrismic\Mvc\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use NetgluePrismic\Exception;
use Zend\Session\Container;
use Zend\Http\Client as HttpClient;
use Zend\Http\Header\SetCookie;
use Zend\Http\Response as HttpResponse;
use Zend\Mvc\Application;
use Prismic\Api;

class PrismicController extends AbstractActionController
{
    /**
     * @var HttpClient|null
     */
    protected $httpClient;

    /**
     * @var Container|null
     */
    protected $session;

    /**
     * @var string Webhook Secret
     */
    protected $webhookSecret;

    /**
     * Throw an exception if we cannot retrieve the client id or secret from config
     * @param  string                                &$clientId     Populated with client ID if it has been set
     * @param  string                                &$clientSecret Populated with client Secret if it has been set
     * @return void
     * @throws Exception\InvalidCredentialsException
     */
    private function requireClientIdAndSecret(&$clientId, &$clientSecret)
    {
        $clientId = $this->getClientId();
        if (empty($clientId)) {
            throw new Exception\InvalidCredentialsException('No Client ID has been provided');
        }

        $clientSecret = $this->getClientSecret();
        if (empty($clientSecret)) {
            throw new Exception\InvalidCredentialsException('No Client Secret has been provided');
        }
    }

    /**
     * Redirects to the oauth endpoint
     * @return void
     */
    public function signinAction()
    {
        // OAuth Initiation Endpoint
        $endpoint = $this->prismic()->api()->oauthInitiateEndpoint();

        // Make sure we have an ID/Secret
        $this->requireClientIdAndSecret($clientId, $clientSecret);

        // Construct URI to our callback for requesting an access token
        $callback = clone($this->getRequest()->getUri());
        $callback->setPath($this->url()->fromRoute('prismic-signin/callback'));

        // Redirect end-user to authorise
        $params = http_build_query(array(
            "client_id" => $clientId,
            "redirect_uri" => (string) $callback,
            "scope" => "master+releases"
        ));
        $url = $endpoint . '?' . $params;

        return $this->redirect()->toUrl($url);
    }

    /**
     * Given a valid code, post it to the token endpoint with the client secret to exchange it for a temporary access token
     * @return void
     */
    public function oauthCallbackAction()
    {
        // We should have 'code' in GET params:
        $code = $this->params()->fromQuery('code');
        if (empty($code)) {
            $this->getResponse()->setReasonPhrase('Bad Request');
            $this->getResponse()->setStatusCode(404);

            return;
        }

        // Make sure we have an ID/Secret
        $this->requireClientIdAndSecret($clientId, $clientSecret);

        // Construct absolute callback URL
        // I don't know why we need this...
        $callback = clone($this->getRequest()->getUri());
        $callback->setPath($this->url()->fromRoute('prismic-signin/callback'));
        $callback->setQuery('');

        $params = array(
            "grant_type" => array('authorization_code'),
            "code" => $code,
            "redirect_uri" => (string) $callback,
            "client_id" => $clientId,
            "client_secret" => $clientSecret,
        );

        // Get the token endpoint:
        $endpoint = $this->prismic()->api()->oauthTokenEndpoint();
        // Use Curl for straightforward SSL
        $client = $this->getHttpClient();
        $client->setUri($endpoint);
        $client->setParameterPost($params);
        $response = $client->send();

        if (!$response->isSuccess()) {
            $this->getResponse()->setStatusCode(404);

            return;
        }

        $data = json_decode($response->getBody());
        $accessToken = $data->access_token;
        $expires = $data->expires_in;

        // Take a minute from the expiry duration incase clocks are off
        $expires -= 60;

        $session = $this->getSessionContainer();
        $session->setAccessToken($accessToken);
        $session->setExpirationSeconds($expires, 'access_token');

        return $this->redirect()->toUrl('/');
    }

    /**
     * Sets a cookie for previewing draft documents
     * @return void
     */
    public function previewAction()
    {
        $token = $this->params()->fromQuery('token');

        /**
         * URL Decode the token
         * Yes, this does need to be doneā€¦
         */
        $token = urldecode($token);
        if (empty($token)) {
            $this->getResponse()->setReasonPhrase('Bad Request');
            $this->raise404();

            return;
        }

        /**
         * If you don't set the context ref now,
         * the link resolver will be unable to resolve unpublished URLs
         */
        $this->getContext()->setRefWithString($token);

        /**
         * If you don't set the cookie, the Prismic Preview Icon will not show up
         * at the bottom of the page
         */
        $expires = time() + (29 * 60);
        $cookie = new SetCookie(Api::PREVIEW_COOKIE, $token, $expires);
        $headers = $this->getResponse()->getHeaders();
        $headers->addHeader($cookie);

        /**
         * Figure out URL and redirect
         */
        $api = $this->prismic()->api();
        $url = $api->previewSession($token, $this->prismic()->getLinkResolver(), '/');
        return $this->redirect()->toUrl($url);
    }

    /**
     * Set the response to a 404 error
     */
    protected function raise404()
    {
        $e = $this->getEvent();
        $e->setError(Application::ERROR_CONTROLLER_CANNOT_DISPATCH);
        $response = $e->getResponse();
        if ($response instanceof HttpResponse) {
            $response->setStatusCode(404);
        }
    }

    /**
     * Return an existing client or create a new one
     * @return HttpClient
     */
    public function getHttpClient()
    {
        if (!$this->httpClient) {
            $this->httpClient = new HttpClient(null, array(
                'adapter' => 'Zend\Http\Client\Adapter\Curl'
            ));
            $this->httpClient->setMethod('POST');
        }

        return $this->httpClient;
    }

    /**
     * Override HttpClient used for authenticating to the prismic api
     * @param  HttpClient $client
     * @return self
     */
    public function setHttpClient(HttpClient $client)
    {
        $this->httpClient = $client;

        return $this;
    }

    /**
     * Return session container for storing the access token
     * @return Container
     */
    public function getSessionContainer()
    {
        if (!$this->session) {
            $services = $this->getServiceLocator();
            $this->session = $services->get('NetgluePrismic\Session\PrismicContainer');
        }

        return $this->session;
    }

    /**
     * Return the client id as configured
     * @return string|null
     */
    protected function getClientId()
    {
        $sl = $this->getServiceLocator();
        $config = $sl->get('config');
        if (isset($config['prismic']['clientId'])) {
            return $config['prismic']['clientId'];
        }

        return null;
    }

    /**
     * Return the client secret as configured
     * @return string|null
     */
    protected function getClientSecret()
    {
        $sl = $this->getServiceLocator();
        $config = $sl->get('config');
        if (isset($config['prismic']['clientSecret'])) {
            return $config['prismic']['clientSecret'];
        }

        return null;
    }

    /**
     * Get a reference to the Prismic context
     * @return \NetgluePrismic\Context
     */
    public function getContext()
    {
        return $this->getServiceLocator()->get('Prismic\Context');
    }

    /**
     * Controller action to allow the user to change the current ref
     * @return void
     */
    public function changeRefAction()
    {
        $ref = $this->params()->fromQuery('ref');
        $url = $this->params()->fromQuery('url');
        if (!empty($url)) {
            try {
                $uri = new \Zend\Uri\Uri($url);
                $redirect = (string) $uri;
            } catch (\Exception $e) {
                // Not caught because $uri is tested below:
            }
        }
        if (!isset($redirect) || !isset($ref)) {
            $this->raise404();
            return;
        }

        // Make sure ref is valid
        $ref = $this->getContext()->getRefWithString($ref);
        if (!is_object($ref)) {
            $this->raise404();
            return;
        }

        // Store the ref in the session
        $this->getSessionContainer()->setRef($ref);

        // Redirect to the determined url
        return $this->redirect()->toUrl($redirect);
    }

    /**
     * Set the expected webhook secret
     *
     * @param  string $secret
     * @return self
     */
    public function setWebhookSecret($secret)
    {
        $this->webhookSecret = $secret;

        return $this;
    }

    /**
     * Respond to a posted webhook trigger
     * @return void
     */
    public function webhookAction()
    {
        if (!$this->getRequest()->isPost()) {
            return $this->getResponse()
                ->setStatusCode(400)
                ->setContent('Expected a POST Request' . PHP_EOL);
        }

        $json = $this->getRequest()->getContent();

        if(empty($json)) {
            return $this->getResponse()
                ->setStatusCode(500)
                ->setContent('Invalid JSON Data' . PHP_EOL);
        }

        try {
            $params = \Zend\Json\Json::decode($json);
        } catch (\Zend\Json\Exception\ExceptionInterface $e) {
            return $this->getResponse()
                ->setStatusCode(500)
                ->setContent('Invalid JSON Data' . PHP_EOL);
        }

        // The JSON Payload should include the plain text secret as stdClass->secret
        if ((string) $params->secret !== (string) $this->webhookSecret) {
            return $this->getResponse()
                ->setStatusCode(401)
                ->setContent('Unauthorised' . PHP_EOL);
        }

        $this->getEventManager()->trigger(__FUNCTION__, $this, (array) $params);

        return $this->getResponse()
            ->setStatusCode(200)
            ->setContent(PHP_EOL);
    }

}