<?php

namespace Develpr\AlexaApp\Request;

use Illuminate\Http\Request;

class AlexaRequest extends Request implements \Develpr\AlexaApp\Contracts\AlexaRequest
{
    const CONFIRMED_STATUS = 'CONFIRMED';
    const DENIED_STATUS = 'DENIED';
    const NO_CONFIRMATION_STATUS = 'NONE';

    private $data = null;
    private $processed = false;
    private $intent = null;
    private $slots = [];
    private $promptResponse = null;
    private $confirmationStatus = null;

    protected function getData()
    {
        if (!$this->processed) {
            $this->process();
        }

        return $this->data;
    }

    public function isProcessed()
    {
        return $this->processed;
    }

    /**
     * returns the request type, i.e. IntentRequest
     *
     * @return mixed
     */
    public function getRequestType()
    {
        return array_get($this->getData(), 'request.type');
    }

    /**
     * @return string|null
     */
    public function getPromptResponseIntent()
    {
        $intent = trim($this->getSessionValue('original_prompt_intent'));

        return (strlen($intent) > 0) ? $intent : null;
    }

    /**
     * Is this request formatted as an Amazon Echo/Alexa request?
     *
     * @return bool
     */
    public function isAlexaRequest()
    {
        return !(is_null($this->getRequestType()));
    }

    /**
     * Is this a new session according to Amazon AlexaSkillsKit?
     *
     * @return bool
     */
    public function isNewSession()
    {
        return boolval(array_get($this->getData(), 'session.new'));
    }

    /**
     * Get the UserId provided in the request
     *
     * @return mixed
     */
    public function getUserId()
    {
        return array_get($this->getData(), $this->hasSession() ? 'session.user.userId' : 'context.System.user.userId');
    }

    /**
     * Get the accessToken provided in the request
     *
     * @return mixed
     */
    public function getAccessToken()
    {
        return array_get($this->getData(), $this->hasSession() ? 'session.user.accessToken' : 'context.System.user.accessToken');
    }

    /**
     * Get the unique Application Id
     *
     * @return mixed
     */
    public function getAppId()
    {
        return  array_get($this->getData(), $this->hasSession() ? 'session.application.applicationId' : 'context.System.application.applicationId');
    }

    /**
     * Checks if the request contains session informatiom
     *
     * @return bool
     */
    public function hasSession()
    {
        return array_has($this->getData(), 'session');
    }

    /**
     * Get all of the session values in an array
     *
     * @return array
     */
    public function getSession()
    {
        $sessionAttributes = array_get($this->getData(), 'session.attributes');

        if (!$sessionAttributes) {
            return [];
        }

        return $sessionAttributes;
    }

    /**
     * Get the contect in an array
     *
     * @return array
     */
    public function getContext()
    {
        $context = array_get($this->getData(), 'context');

        if (!$context) {
            return [];
        }

        return $context;
    }

    /**
     * Get the dialog state possible values are:
     * "STARTED", "IN_PROGRESS", or "COMPLETED"
     *
     * @return string|null
     */
    public function dialogState()
    {
        return array_get($this->getData(), 'request.dialogState');
    }

    /**
     * Get a particular session value by key
     *
     * @param string $key
     *
     * @return mixed|null
     */
    public function getSessionValue($key = null)
    {
        return array_key_exists($key, $this->getSession()) ? $this->getSession()[$key] : null;
    }

    /**
     * @return string|null
     */
    public function getIntent()
    {
        return array_get($this->getData(), 'request.intent.name');
    }

    /**
     * @param string     $slotKey
     * @param mixed|null $default
     *
     * @return mixed|null
     */
    public function slot($slotKey, $default = null)
    {
        if (!$this->processed) {
            $this->process();
        }

        $key_exists = (array_key_exists($slotKey, $this->slots));

        if (!$key_exists) {
            return $default;
        }

        return (array_key_exists('value', $this->slots[$slotKey])) ? $this->slots[$slotKey]['value'] : $default;
    }

    /**
     * @return array|null
     */
    public function slots()
    {
        if (!$this->processed) {
            $this->process();
        }

        return count($this->slots) ? $this->slots : null;
    }

    /**
     * Update a slot
     *
     * @param $slotName
     * @param $value
     * @param bool $confirmed
     * @param bool $denied
     *
     * @return $this
     */
    public function updateSlot($slotName, $value, $confirmed = null)
    {
        if (!$this->processed) {
            $this->process();
        }

        if (array_has($this->slots, [$slotName])) {
            $this->slots[$slotName]['value'] = $value;

            if ($confirmed) {
                $this->slots[$slotName]['confirmationStatus'] = $this::CONFIRMED_STATUS;
            } elseif (!is_null($confirmed) && !$confirmed) {
                $this->slots[$slotName]['confirmationStatus'] = $this::DENIED_STATUS;
            } else {
                $this->slots[$slotName]['confirmationStatus'] = $this::NO_CONFIRMATION_STATUS;
            }
        }

        return $this;
    }

    /**
     * @return int
     */
    public function getTimestamp()
    {
        return strtotime(array_get($this->getData(), 'request.timestamp'));
    }

    private function process()
    {
        $data = $this->getContent();
        $this->data = json_decode($data, true);
        $this->intent = array_get($this->data, 'request.intent.name');
        $this->slots = array_get($this->data, 'request.intent.slots', []);
        $this->confirmationStatus = array_get($this->data, 'request.intent.confirmationStatus', '');

        $this->processed = true;
    }

    /**
     * @param bool $promptResponse
     */
    public function setPromptResponse($promptResponse)
    {
        $this->promptResponse = $promptResponse;
    }

    /**
     * @return bool
     */
    public function isPromptResponse()
    {
        return boolval($this->promptResponse);
    }

    public function getConfirmationStatus()
    {
        if (!$this->processed) {
            $this->process();
        }

        return $this->confirmationStatus;
    }
}


<?php

namespace Mdb\PayPal\Ipn;

class Verifier
{
    const STATUS_KEYWORD_VERIFIED = 'VERIFIED';
    const STATUS_KEYWORD_INVALID = 'INVALID';

    /**
     * @var Service
     */
    private $service;

    /**
     * @param Service $service
     */
    public function __construct(Service $service)
    {
        $this->service = $service;
    }

    /**
     * @param Message $message
     *
     * @return bool
     *
     * @throws UnexpectedValueException
     */
    public function verify(Message $message)
    {
        $serviceResponse = $this->service->verifyIpnMessage($message);
        $serviceResponseBody = $serviceResponse->getBody();

        $pattern = sprintf('/(%s|%s)/', self::STATUS_KEYWORD_VERIFIED, self::STATUS_KEYWORD_INVALID);

        if (!preg_match($pattern, $serviceResponseBody)) {
            throw new \UnexpectedValueException(sprintf('Unexpected verification status encountered: %s', $serviceResponseBody));
        }

        return $serviceResponseBody === self::STATUS_KEYWORD_VERIFIED;
    }
}


<?php

namespace Develpr\AlexaApp\Http\Routing;

use Develpr\AlexaApp\Http\Routing\Matching\AlexaValidator;
use Illuminate\Routing\Matching\UriValidator;
use Illuminate\Routing\Route;
use Illuminate\Routing\RouteCompiler;

class AlexaRoute extends Route
{
    /**
     * @var string
     */
    private $routeIntent;

    /**
     * AlexaRoute constructor.
     * @param array|string   $methods
     * @param string         $uri
     * @param string         $intent
     * @param \Closure|array $action
     */
    public function __construct($methods, $uri, $intent, $action)
    {
        parent::__construct($methods, $uri, $action);

        $this->routeIntent = $intent;
    }

    /**
     * @return string
     */
    public function getRouteIntent()
    {
        return $this->routeIntent;
    }

    /**
     * Set the router instance on the route.
     *
     * @param  \Illuminate\Routing\Router $router
     *
     * @return $this
     */
    public function setRouter(\Illuminate\Routing\Router $router)
    {
        $this->router = $router;

        return $this;
    }

    /**
     * Get the route validators for the instance.
     *
     * @return array
     */
    public static function getValidators()
    {
        $validators = parent::getValidators();

        foreach ($validators as $key => $validator) {
            if ($validator instanceof UriValidator) {
                break;
            }
        }

        $validators[] = new AlexaValidator;

        return $validators;
    }

    /**
     * Before Laravel 5.4, `uri()` was `getUri()` for the time being, we'll make this
     * friendly to both pre and post 5.4 with this check
     * todo: version 5.5+ we should remove this check to clean things up and update readme
     * @return string
     */
    public function getUri()
    {
        return parent::getUri() . $this->getRouteIntent();
    }

    /**
     * Returns the URI for the request. Note that with Laravel
     *
     * @return string
     */
    public function uri() {
        return parent::uri() . $this->getRouteIntent();
    }

    /**
     * Compile the route into a Symfony CompiledRoute instance.
     *
     * @return void
     */
    protected function compileRoute()
    {
        if (version_compare(\Illuminate\Foundation\Application::VERSION, '5.5.0') < 0) {
            if ( is_callable( "parent::extractOptionalParameters" ) ) {
                return parent::compileRoute();
            }
        }

        if (! $this->compiled) {

            //todo: this is a bit ugly - we should go deeper and solve the real problem
            //This is ugly - before 5.4, we didn't use "uri()" method in the RouteCompiler (there was no
            //route compiler!), and instead we used the private uri instance variable. Which meant that
            //`uri()` and `uri` were different.
            $tempRouterIntent = $this->routeIntent;
            $this->routeIntent = "";
            $this->compiled = (new RouteCompiler($this))->compile();
            $this->routeIntent = $tempRouterIntent;
        }

        return $this->compiled;
    }
}


<?php

namespace Mdb\PayPal\Ipn\Service;

use GuzzleHttp\ClientInterface;
use Mdb\PayPal\Ipn\Exception\ServiceException;
use Mdb\PayPal\Ipn\Message;
use Mdb\PayPal\Ipn\Service;
use Mdb\PayPal\Ipn\ServiceResponse;

class GuzzleService implements Service
{
    /**
     * @var ClientInterface
     */
    private $httpClient;

    /**
     * @var string
     */
    private $serviceEndpoint;

    /**
     * @param ClientInterface $httpClient
     * @param string          $serviceEndpoint
     */
    public function __construct(ClientInterface $httpClient, $serviceEndpoint)
    {
        $this->httpClient = $httpClient;
        $this->serviceEndpoint = $serviceEndpoint;
    }

    /**
     * {@inheritdoc}
     */
    public function verifyIpnMessage(Message $message)
    {
        $requestBody = array_merge(
            ['cmd' => '_notify-validate'],
            $message->getAll()
        );

        try {
            $response = $this->httpClient->post(
                $this->serviceEndpoint,
                ['form_params' => $requestBody]
            );
        } catch (\Exception $e) {
            throw new ServiceException($e->getMessage());
        }

        return new ServiceResponse(
            (string) $response->getBody()
        );
    }
}


<?php

namespace Develpr\AlexaApp\Response\Directives\AudioPlayer;

use Develpr\AlexaApp\Response\Directives\Directive;

class Play extends Directive
{
    const DEFAULT_PLAY_BEHAVIOR = 'REPLACE_ALL';

    const TYPE = 'AudioPlayer.Play';

    private $validPlayBehaviors = ['REPLACE_ALL', 'ENQUEUE', 'REPLACE_ENQUEUED'];

    protected $url = '';

    protected $token = '';

    protected $playBehavior = '';

    protected $offsetInMilliseconds = '';

    protected $expectedPreviousToken = '';

    /**
     * Play constructor.
     *
     * @param string $url
     * @param string $token
     * @param string $playBehavior
     * @param int $offset
     * @param string $expectedPreviousToken
     */
    public function __construct($url, $token = '', $offsetInMilliseconds = 0, $playBehavior = null, $expectedPreviousToken = '')
    {
        $this->url = $url;
        $this->token = $token ?: str_random(64);
        $this->playBehavior = $playBehavior ?: self::DEFAULT_PLAY_BEHAVIOR;
        $this->offsetInMilliseconds = $offsetInMilliseconds;
        $this->expectedPreviousToken = $expectedPreviousToken;
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        $playAsArray['type'] = self::TYPE;
        $playAsArray['audioItem']['stream'] = [];

        $this->addAttributeToArray('playBehavior', $playAsArray);
        $this->addAttributeToArray('url', $playAsArray['audioItem']['stream']);
        $this->addAttributeToArray('token', $playAsArray['audioItem']['stream']);
        $this->addAttributeToArray('offsetInMilliseconds', $playAsArray['audioItem']['stream']);

        if($this->playBehavior == 'ENQUEUE')
        {
            $this->addAttributeToArray('expectedPreviousToken', $playAsArray['audioItem']['stream']);
        }

        return $playAsArray;
    }

    /**
     * @param string $url
     *
     * @return $this
     */
    public function setUrl($url)
    {
        $this->url = $url;

        return $this;
    }

    /**
     * @param string $token
     *
     * @return $this
     */
    public function setToken($token)
    {
        $this->token = $token;

        return $this;
    }

    /**
     * @param string $offsetInMilliseconds
     *
     * @return $this
     */
    public function setOffsetInMilliseconds($offsetInMilliseconds)
    {
        $this->offsetInMilliseconds = $offsetInMilliseconds;

        return $this;
    }

    /**
     * @param string $type
     *
     * @throws \Exception
     *
     * @return $this
     */
    public function setPlayBehavior($playBehavior)
    {
        if (!in_array($playBehavior, $this->validPlayBehaviors)) {
            throw new \Exception('Invalid play behavior supplied');
        }

        $this->playBehavior = $playBehavior;

        return $this;
    }

    /**
     * @param string $token
     *
     * @return $this
     */
    public function setExpectedPreviousToken($expectedPreviousToken)
    {
        if ($this->playBehavior != 'ENQUEUE') {
            throw new \Exception('ExpectedPreviousToken is only allowed for play behavior ENQUEUE');
        }

        $this->expectedPreviousToken = $expectedPreviousToken;

        return $this;
    }

    /**
     * @return string
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * @return string
     */
    public function getToken()
    {
        return $this->token;
    }

    /**
     * @return string
     */
    public function getPlayBehavior()
    {
        return $this->playBehavior;
    }

    /**
     * @return integer
     */
    public function getOffsetInMilliseconds()
    {
        return $this->offsetInMilliseconds;
    }

    /**
     * @return integer
     */
    public function getExpectedPreviousToken()
    {
        return $this->expectedPreviousToken;
    }
}


<?php

namespace Develpr\AlexaApp\Response;

use Illuminate\Contracts\Support\Arrayable;

class Card implements Arrayable
{
    const DEFAULT_CARD_TYPE = 'Simple';
    const LINK_ACCOUNT_CARD_TYPE = 'LinkAccount';
    const STANDARD_CARD_TYPE = 'Standard';
    const SIMPLE_CARD_TYPE = 'Simple';

    private $validCardTypes = ['Simple', 'LinkAccount', 'Standard'];

    //The type of card
    //@see https://developer.amazon.com/public/solutions/devices/echo/alexa-app-kit/docs/alexa-appkit-app-interface-reference
    private $type = 'Simple';

    private $title = '';

    private $subtitle = '';

    /** @var string $content Only applicable for simple card types */
    private $content = '';

    /** @var string $text Only applicable for standard card types */
    private $text = '';

    /** @var array | string $img Only applicable for standard card types */
    private $image = [];

    /**
     * Card constructor
     *
     * @param string $title
     * @param string $subtitle
     * @param string $content
     * @param string $type
     * @param string $text
     * @param array|string $image url for the image to display on the card
     */
    public function __construct(
        $title = '',
        $subtitle = '',
        $content = '',
        $type = self::DEFAULT_CARD_TYPE,
        $text = '',
        $image = []
    ) {
        $this->title = $title;
        $this->subtitle = $subtitle;
        $this->content = $content;
        $this->type = $type;
        $this->text = $text;
        $this->setImg($image);
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        $cardAsArray = [];

        //todo: as of now, only *type* is required but this seems likely to change
        $cardAsArray['type'] = $this->type;

        $this->addAttributeToArray('title', $cardAsArray);
        $this->addAttributeToArray('subtitle', $cardAsArray);

        if ($this->isSimpleCard()) {
            $this->addAttributeToArray('content', $cardAsArray);
        }

        if ($this->isStandardCard()) {
            $this->addAttributeToArray('text', $cardAsArray);
            $this->addAttributeToArray('image', $cardAsArray);
        }

        return $cardAsArray;
    }

    /**
     * @param string $attributeName
     * @param array  $outputArray
     */
    private function addAttributeToArray($attributeName, array &$outputArray)
    {
        if (!is_string($this->$attributeName) || strlen($this->$attributeName) > 0) {
            $outputArray[$attributeName] = $this->$attributeName;
        }
    }

    /**
     * Set content of the simple card
     *
     * @param string $content
     *
     * @return $this
     */
    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }

    /**
     * Set text of the standard card
     *
     * @param string $text
     *
     * @return $this
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * @param string $subtitle
     *
     * @return $this
     */
    public function setSubtitle($subtitle)
    {
        $this->subtitle = $subtitle;

        return $this;
    }

    /**
     * @param string $title
     *
     * @return $this
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * @param string $type
     *
     * @throws \Exception
     *
     * @return $this
     */
    public function setType($type)
    {
        if (!in_array($type, $this->validCardTypes)) {
            throw new \Exception('Invalid Card type supplied');
        }

        $this->type = $type;

        return $this;
    }

    /**
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * @return string
     */
    public function getSubtitle()
    {
        return $this->subtitle;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @param string | array[string] $image the image url
     *
     * @return $this
     */
    public function setImg($image)
    {
        $requiredKeys = ['smallImageUrl', 'largeImageUrl'];

        if (is_array($image) && array_has($image, $requiredKeys)) {
            $this->image  = array_only($image, $requiredKeys);
        } elseif (is_string($image)) {
            $this->image = [
                'smallImageUrl' => $image,
                'largeImageUrl' =>   $image
            ];
        }

        return $this;
    }

    /**
     * @return array
     */
    public function getImg()
    {
        return $this->$image;
    }

    /**
     * @return bool
     */
    public function isStandardCard()
    {
        return $this->type === Card::STANDARD_CARD_TYPE;
    }

    /**
     * @return bool
     */
    public function isSimpleCard()
    {
        return $this->type === Card::SIMPLE_CARD_TYPE;
    }
}


<?php

namespace Develpr\AlexaApp\Http\Middleware;

use Closure;
use Develpr\AlexaApp\Contracts\AlexaRequest;
use Develpr\AlexaApp\Contracts\CertificateProvider;
use Develpr\AlexaApp\Exceptions\InvalidAppIdException;
use Develpr\AlexaApp\Exceptions\InvalidCertificateException;
use Develpr\AlexaApp\Exceptions\InvalidRequestTimestamp;
use Develpr\AlexaApp\Exceptions\InvalidSignatureChainException;
use Illuminate\Http\Request as IlluminateRequest;

class Certificate
{
    const CERTIFICATE_URL_HEADER = "Signaturecertchainurl";
    const SIGNATURE_HEADER       = "Signature";
    const ENCRYPT_METHOD         = "sha1WithRSAEncryption";

    /**
     * @var \Develpr\AlexaApp\Contracts\AlexaRequest
     */
    private $alexaRequest;

    /**
     * @var \Develpr\AlexaApp\Contracts\CertificateProvider
     */
    private $certificateProvider;

    /**
     * @var array
     */
    private $config;

    /**
     * Certificate constructor.
     *
     * @param AlexaRequest        $alexaRequest
     * @param CertificateProvider $certificateProvider
     * @param array               $config
     */
    public function __construct(AlexaRequest $alexaRequest, CertificateProvider $certificateProvider, array $config)
    {
        $this->alexaRequest = $alexaRequest;
        $this->certificateProvider = $certificateProvider;
        $this->config = $config;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure                 $next
     *
     * @return mixed
     *
     * @throws InvalidSignatureChainException
     * @throws \Exception
     */
    public function handle($request, Closure $next)
    {
        if (!$this->alexaRequest->isAlexaRequest()) {
            return $next($request);
        }

        $this->verifyApplicationId();
        $this->checkTimestampTolerance();
        $certificateResult = $this->verifyCertificate($request);

        if ($certificateResult === 1) {
            return $next($request);
        } elseif ($certificateResult === 0) {
            throw new InvalidSignatureChainException("The request did not validate against the certificate chain.");
        } else {
            throw new \Exception("Something went wrong when validating the request and certificate.");
        }
    }

    /**
     * Get the certificate from the certificate provider
     *
     * @param IlluminateRequest $request
     *
     * @return mixed
     */
    private function getCertificate(IlluminateRequest $request)
    {
        $signatureChainUri = $request->header(self::CERTIFICATE_URL_HEADER);

        $this->validateKeychainUri($signatureChainUri);

        $certificate = $this->certificateProvider->getCertificateFromUri($signatureChainUri);

        return $certificate;
    }

    /**
     * @param string $keychainUri
     *
     * @throws InvalidCertificateException
     */
    private function validateKeychainUri($keychainUri)
    {
        $uriParts = parse_url($keychainUri);

        if (strcasecmp($uriParts['host'], array_get($this->config, 'origin.host')) !== 0) {
            throw new InvalidCertificateException("The host for the Certificate provided in the header is invalid");
        }

        if (strpos($uriParts['path'], array_get($this->config, 'origin.path')) !== 0) {
            throw new InvalidCertificateException("The URL path for the Certificate provided in the header is invalid");
        }

        if (strcasecmp($uriParts['scheme'], array_get($this->config, 'origin.scheme')) !== 0) {
            throw new InvalidCertificateException("The URL is using an unsupported scheme. Should be https");
        }

        if (array_key_exists('port', $uriParts) && $uriParts['port'] != array_get($this->config,'origin.port')) {
            throw new InvalidCertificateException("The URL is using an unsupported https port");
        }
    }

    /**
     * @param IlluminateRequest $request
     *
     * @return string
     */
    private function getDecodedSignature(IlluminateRequest $request)
    {
        $signature = $request->header(self::SIGNATURE_HEADER);
        $base64DecodedSignature = base64_decode($signature);

        return $base64DecodedSignature;
    }

    /**
     * @throws InvalidAppIdException
     */
    private function verifyApplicationId()
    {
        if (!boolval(array_get($this->config, 'verifyAppId'))) {
            return;
        }

        $possible = $this->possibleApplicationIds();

        //Somebody might use the .env files and set the applicationIds as a string instead of an array so we'll be sure
        if (!is_array($possible)) {
            $possible = array($possible);
        }

        $appId = $this->alexaRequest->getAppId();

        if (!in_array($appId, $possible)) {
            throw new InvalidAppIdException("The request's app id does not match the configured app id(s)");
        }
    }

    /**
    * @return array
    */
    private function possibleApplicationIds()
    {
        return array_get($this->config, 'applicationIds');
    }

    /**
     * @throws InvalidRequestTimestamp
     */
    private function checkTimestampTolerance()
    {
        //If the timestamp tolerance is set to 0 we'll skip the check (see config)
        if (intval(array_get($this->config, 'timestampTolerance')) === 0) {
            return;
        }

        $timestampTolerance = array_get($this->config, 'timestampTolerance');
        $timestamp = $this->alexaRequest->getTimestamp();

        if (time() - $timestamp > $timestampTolerance) {
            throw new InvalidRequestTimestamp("The request timestamp is older then configured timestamp tolerances allow");
        }
    }

    /**
     * @param \Illuminate\Http\Request $request
     *
     * @return int
     */
    private function verifyCertificate(IlluminateRequest $request)
    {
        $signature = $this->getDecodedSignature($request);
        $certificate = $this->getCertificate($request);

        //Get the request body that will be validated
        $data = $request->getContent();

        $certKey = openssl_pkey_get_public($certificate);

        // ok, let's do this thing! we're saving the world from hackers here!
        $valid = openssl_verify($data, $signature, $certKey, self::ENCRYPT_METHOD);

        return $valid;
    }
}


<?php

use ListenerBuilder\Guzzle\ArrayListenerBuilder;

class GuzzleArrayListenerBuilderContext extends FeatureContext
{
    /**
     * {@inheritdoc}
     */
    protected function getListener()
    {
        $listenerBuilder = new ArrayListenerBuilder();

        $listenerBuilder->setServiceEndpoint(
            $this->getServiceEndpoint()
        );
        $listenerBuilder->setData($this->ipnMessageData);

        return $listenerBuilder->build();
    }
}


<?php

namespace Develpr\AlexaApp\Certificate;

use Develpr\AlexaApp\Exceptions\InvalidCertificateException;

abstract class BaseCertificateProvider
{
    const ECHO_SERVICE_DOMAIN = 'echo-api.amazon.com';

    /**
     * @param mixed $certificate
     *
     * @return array
     */
    protected function parseCertificate($certificate)
    {
        return openssl_x509_parse($certificate);
    }

    /**
     * Returns whether the configured service domain is present and validpresent
     *
     * @param array $parsedCertificate
     *
     * @return bool
     */
    protected function verifyCertificateSubjectAltNamePresent(array $parsedCertificate)
    {
        return strpos(array_get($parsedCertificate, 'extensions.subjectAltName'), self::ECHO_SERVICE_DOMAIN) !== false;
    }

    /**
     * Returns whether the date is valid
     *
     * @param array $parsedCertificate
     *
     * @return bool
     */
    protected function validateCertificateDate(array $parsedCertificate)
    {
        $validFrom = array_get($parsedCertificate, 'validFrom_time_t');

        $validTo = array_get($parsedCertificate, 'validTo_time_t');

        $time = time();

        return ($validFrom <= $time && $time <= $validTo);
    }

    /**
     * Retrieve the certificate from a url
     *
     * @param string $certificateChainUri
     *
     * @return string
     */
    protected function getRemoteCertificateChain($certificateChainUri)
    {
        return file_get_contents($certificateChainUri);
    }

    /**
     * Download/retrieve the certificate chain from a given location
     *
     * @param string $certificateChainUri
     *
     * @return string|null
     */
    public function getCertificateFromUri($certificateChainUri)
    {
        $certificateChain = $this->retrieveCertificateFromStore($certificateChainUri);

        if (!is_null($certificateChain) || $certificateChain === false) {
            $parsedCertificate = $this->parseCertificate($certificateChain);
            if (!$this->validateCertificateDate($parsedCertificate) || !$this->verifyCertificateSubjectAltNamePresent($parsedCertificate)) {
                $certificateChain = $this->storeRemoteCertificate($certificateChainUri);
            }
        } else {
            $certificateChain = $this->storeRemoteCertificate($certificateChainUri);
        }

        return $certificateChain;
    }

    /**
     * @param string $certificateChainUri
     *
     * @return string|null
     *
     * @throws InvalidCertificateException
     */
    protected function storeRemoteCertificate($certificateChainUri)
    {
        $certificateContents = $this->getRemoteCertificateChain($certificateChainUri);

        $parsedCertificate = $this->parseCertificate($certificateContents);

        // If the certificate we are getting from the URL doesn't have a valid SAN present, this is not secure!
        if (!$this->verifyCertificateSubjectAltNamePresent($parsedCertificate)) {
            throw new InvalidCertificateException("The remote certificate doesn't contain a valid SANs in the signature");
        }

        $this->persistCertificate($certificateChainUri, $certificateContents);

        return $this->retrieveCertificateFromStore($certificateChainUri);
    }

    /**
     * Persist the certificate contents to data store so it's retrievable using the certificate chain uri
     *
     * @param string $certificateChainUri
     * @param string $certificateContents
     */
    protected abstract function persistCertificate($certificateChainUri, $certificateContents);

    /**
     * Retrieve the certificate give the certificate chain's uri from the data store
     *
     * @param string $certificateChainUri
     *
     * @return string|null
     */
    protected abstract function retrieveCertificateFromStore($certificateChainUri);
}


<?php

namespace Develpr\AlexaApp\Response;

use Develpr\AlexaApp\Contracts\OutputSpeech;
use Develpr\AlexaApp\Response\Directives\AudioPlayer\Play;
use Develpr\AlexaApp\Response\Directives\Directive;
use Illuminate\Contracts\Support\Jsonable;

/**
 * This class represents a valid response to be send back to Alexa.
 *
 * Class AlexaResponse
 */
class AlexaResponse implements Jsonable
{
    const ALEXA_RESPONSE_VERSION = '1.0';

    /**
     * A key-value pair of session attributes
     *
     * @var array
     */
    private $sessionAttributes = [];

    /**
     * Should the session be ended
     *
     * @var bool
     */
    private $shouldSessionEnd = true;

    /**
     * @var Speech
     */
    private $speech = null;

    /**
     * @var array
     */
    private $directives = null;

    /**
     * @var Reprompt
     */
    private $reprompt = null;

    /**
     * Does this response represent a prompt?
     *
     * @var bool
     */
    private $isPrompt = false;

    /**
     * @var Card
     */
    private $card = null;

    /**
     * @var \Develpr\AlexaApp\Alexa
     */
    private $alexa;

    /**
     * The Intent that a response should be routed to if a response comes in
     *
     * @var string|null
     */
    private $promptResponseIntent = null;

    /**
     * @param OutputSpeech|null   $speech
     * @param Card|null     $card
     * @param Reprompt|null $reprompt
     */
    public function __construct(OutputSpeech $speech = null, Card $card = null, Reprompt $reprompt = null)
    {
        $this->speech = $speech;
        $this->card = $card;
        $this->reprompt = $reprompt;

        //this... I'm not sure about this... I might at some point attach the session data
        //to the response in middleware to remove this somewhat hacky feeling dependency
        $this->alexa = app()->make('alexa');

        return $this;
    }

    /**
     * Convert the object to its JSON representation.
     *
     * @param int $options
     *
     * @return string
     */
    public function toJson($options = 0)
    {
        $alexaResponseData = $this->prepareResponseData();

        return json_encode($alexaResponseData, JSON_UNESCAPED_SLASHES);
    }

    /**
     * Set whether or not the session should be ended
     *
     * @param bool $shouldEnd
     *
     * @return $this
     */
    public function endSession($shouldEnd = true)
    {
        $this->shouldSessionEnd = $shouldEnd;

        return $this;
    }

    /**
     * Build the valid Alexa response object
     *
     * @return array
     */
    private function prepareResponseData()
    {
        $responseData = [];
        $responseData['version'] = self::ALEXA_RESPONSE_VERSION;
        $response = [];

        //Check to see if a speech, card, or reprompt object are set and if so
        //add them to the data object
        if (!is_null($this->speech) && $this->speech instanceof OutputSpeech) {
            $response = $this->speech->toArray();
        }

        if (!is_null($this->card) && $this->card instanceof Card) {
            $response['card'] = $this->card->toArray();
        }

        if (!is_null($this->reprompt) && $this->reprompt instanceof Reprompt && trim($this->reprompt->getValue()) != "") {
            $response['reprompt'] = $this->reprompt->toArray();
        }

        if (!is_null($this->directives)) {
            foreach ($this->directives as $directive) {
                $response['directives'][] = $directive->toArray();
            }
        }

        $response['shouldEndSession'] = $this->shouldSessionEnd;

        $sessionAttributes = $this->getSessionData();

        if ($sessionAttributes && count($sessionAttributes) > 0) {
            $responseData['sessionAttributes'] = $sessionAttributes;
        }

        $responseData['response'] = $response;

        return $responseData;
    }

    /**
     * @param array $sessionAttributes
     */
    public function setSessionAttributes(array $sessionAttributes)
    {
        $this->sessionAttributes = $sessionAttributes;
    }

    /**
     * @param Card $card
     *
     * @return $this
     */
    public function withCard(Card $card)
    {
        return $this->setCard($card);
    }

    /**
     * @param Reprompt $reprompt
     *
     * @return $this
     */
    public function withReprompt(Reprompt $reprompt)
    {
        return $this->setReprompt($reprompt);
    }

    /**
     * @param OutputSpeech $speech
     *
     * @return $this
     */
    public function withSpeech(OutputSpeech $speech)
    {
        return $this->setSpeech($speech);
    }

    /**
     * @param Play $play
     * @return mixed
     */
    public function withAudio(Play $play)
    {
        return $this->setAudio($play);
    }

    /**
     * @param Directive $directive
     * @return AlexaResponse
     */
    public function withDirective(Directive $directive)
    {
        return $this->setDirective($directive);
    }

    /**
     * @param Play $play
     * @return $this
     */
    public function setAudio(Play $play)
    {
        $this->directives[] = $play;

        return $this;
    }

    /**
     * @param Directive $directive
     * @return $this
     */
    public function setDirective(Directive $directive)
    {
        $this->directives[] = $directive;

        return $this;
    }

    /**
     * @param Card $card
     *
     * @return $this
     */
    public function setCard(Card $card)
    {
        $this->card = $card;

        return $this;
    }

    /**
     * @return bool
     */
    public function isPrompt()
    {
        return boolval($this->isPrompt);
    }

    /**
     * @param bool $prompt
     *
     * @return $this
     */
    public function setIsPrompt($prompt = false)
    {
        if (is_bool($prompt)) {
            $this->isPrompt = $prompt;
        }

        return $this;
    }

    /**
     * @param OutputSpeech $speech
     *
     * @return $this
     */
    public function setSpeech(OutputSpeech $speech)
    {
        $this->speech = $speech;

        return $this;
    }

    /**
     * @param Reprompt $reprompt
     *
     * @return $this
     */
    public function setReprompt(Reprompt $reprompt)
    {
        $this->reprompt = $reprompt;

        return $this;
    }

    /**
     * @param string|null $intent
     *
     * @return $this
     */
    public function setPromptResponseIntent($intent)
    {
        $this->promptResponseIntent = $intent;

        return $this;
    }

    /**
     * @param string|null $intent
     *
     * @return $this
     */
    public function sendResponseTo($intent)
    {
        return $this->setPromptResponseIntent($intent);
    }

    /**
     * @return array|mixed|null
     */
    private function getSessionData()
    {
        $data = $this->alexa->session();

        if ($this->isPrompt()) {
            $data['possible_prompt_response'] = true;

            if ($this->promptResponseIntent) {
                $data['prompt_response_intent'] = $this->promptResponseIntent;
            }

            if ($this->speech) {
                $data['original_prompt'] = $this->speech->getValue();
            }

            if ($this->alexa->requestType() === 'IntentRequest') {
                $data['original_prompt_intent'] = $this->alexa->request()->getIntent();
            }
        }

        return $data;
    }
}


<?php

namespace Develpr\AlexaApp\Http\Middleware;

use Closure;
use Develpr\AlexaApp\Contracts\AlexaRequest;
use Develpr\AlexaApp\Http\Routing\AlexaRouter;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Pipeline\Pipeline;
use Symfony\Component\HttpFoundation\Response;

class Request
{
    /**
     * Application instance.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;

    /**
     * Router instance.
     *
     * @var AlexaRouter
     */
    protected $router;

    /**
     * Array of middleware.
     *
     * @var array
     */
    protected $middleware;

    /**
     * @var \Develpr\AlexaApp\Contracts\AlexaRequest
     */
    private $alexaRequest;

    /**
     * @param Application  $app
     * @param AlexaRouter  $router
     * @param AlexaRequest $alexaRequest
     * @param array        $middleware
     */
    public function __construct(Application $app, AlexaRouter $router, AlexaRequest $alexaRequest, array $middleware)
    {
        $this->app = $app;
        $this->router = $router;
        $this->middleware = $middleware;
        $this->alexaRequest = $alexaRequest;
    }

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($this->alexaRequest->isAlexaRequest()) {
            return $this->sendRequestThroughRouter($request);
        }

        return $next($request);
    }

    /**
     * Send through our custom router
     *
     * @param $request
     *
     * @return Response
     */
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        return (new Pipeline($this->app))->send($request)->through($this->middleware)->then(function ($request) {
            return $this->router->dispatch($this->alexaRequest);
        });
    }
}


<?php

namespace Develpr\AlexaApp\Provider;

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\ServiceProvider;
use ReflectionClass;
use Route;

class LaravelServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->publishes([
            realpath(__DIR__ . '/../../config/alexa.php') => config_path('alexa.php'),
        ], 'config');

        $this->publishes([
            realpath(__DIR__ . '/../../database/2015_06_21_000000_create_alexa_devices_table.php') => database_path('/migrations/2015_06_21_000000_create_alexa_devices_table.php'),
        ], 'migrations');

        if ($this->app['config']['alexa.audio.proxy.enabled'])
        {
            Route::get($this->app['config']['alexa.audio.proxy.route'] . '/{audiofile}', function($audiofile) {
                return response(base64_decode($audiofile))
                    ->header('Content-Type', 'application/x-mpegurl');
            });
        }
    }

    /**
     * Register the service provider.
     */
    public function register()
    {
        $this->setupConfig();

        /** @var \App\Http\Kernel $kernel */
        $kernel = $this->app->make('Illuminate\Contracts\Http\Kernel');

        $this->app->instance('alexa.router.middleware', $this->gatherAppMiddleware($kernel));

        // Register our universal service provider
        $this->app->register('Develpr\AlexaApp\Provider\AlexaServiceProvider');

        $this->addRequestMiddlewareToBeginning($kernel);
    }

    protected function setupConfig()
    {
        $this->mergeConfigFrom(realpath(__DIR__.'/../../config/alexa.php'), 'alexa');
    }

    protected function addRequestMiddlewareToBeginning(Kernel $kernel)
    {
        /** @var \App\Http\Kernel $kernel */
        $kernel->prependMiddleware('Develpr\AlexaApp\Http\Middleware\Request');
    }

    /**
     * Gather the application middleware besides this one so that we can send
     * our request through them, exactly how the developer wanted.
     *
     * @param \Illuminate\Contracts\Http\Kernel $kernel
     *
     * @return array
     */
    protected function gatherAppMiddleware(Kernel $kernel)
    {
        $reflection = new ReflectionClass($kernel);

        $property = $reflection->getProperty('middleware');
        $property->setAccessible(true);

        $middleware = $property->getValue($kernel);

        if ($this->app['config']['alexa.skipCsrfCheck']) {
            $middleware = $this->unsetCsrfMiddleware($middleware);
        }

        return $middleware;
    }

    private function unsetCsrfMiddleware($allMiddleware)
    {
        foreach ($allMiddleware as $position => $aMiddleware) {
            if (strpos(strtolower($aMiddleware), 'verifycsrftoken') !== false) {
                unset($allMiddleware[$position]);
                break;
            }
        }

        return $allMiddleware;
    }
}


<?php

namespace Develpr\AlexaApp\Certificate;

use Develpr\AlexaApp\Contracts\CertificateProvider;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;

class FileCertificateProvider extends BaseCertificateProvider implements CertificateProvider
{
    /**
     * @var \Illuminate\Filesystem\Filesystem
     */
    private $filesystem;

    /**
     * @var string
     */
    private $filePath;

    /**
     * FileCertificateProvider constructor.
     *
     * @param Filesystem $filesystem
     * @param string     $filePath
     */
    public function __construct(Filesystem $filesystem, $filePath)
    {
        $this->filesystem = $filesystem;
        $this->filePath = $filePath;

        if (!$this->filesystem->isDirectory($this->filePath)) {
            $this->filesystem->makeDirectory($this->filePath);
        }
    }

    /**
     * Persist the certificate contents to data store so it's retrievable using the certificate chain uri
     *
     * @param string $certificateChainUri
     * @param string $certificateContents
     */
    protected function persistCertificate($certificateChainUri, $certificateContents)
    {
        $this->filesystem->put($this->calculateFilePath($certificateChainUri), $certificateContents);
    }

    /**
     * Retrieve the certificate give the certificate chain's uri from the datastore
     *
     * @param string $certificateChainUri
     *
     * @return string|null
     */
    protected function retrieveCertificateFromStore($certificateChainUri)
    {
        try {
            $certificateChain = $this->filesystem->get($this->calculateFilePath($certificateChainUri));
        } catch (FileNotFoundException $e) {
            $certificateChain = null;
        }

        return $certificateChain;
    }

    /**
     * Calculate the path that the certificate should be stored
     *
     * @param string $certificateChainUri
     *
     * @return string
     */
    private function calculateFilePath($certificateChainUri)
    {
        $filename = md5($certificateChainUri);

        $path = $this->filePath.$filename;

        return $path;
    }
}


<?php

namespace Develpr\AlexaApp\Response;

use Develpr\AlexaApp\Contracts\OutputSpeech;

class Speech implements OutputSpeech
{
    const DEFAULT_TYPE = 'PlainText';
    const SPEECH_TYPE_PLAINTEXT = 'PlainText';
    const SPEECH_TYPE_SSML = 'SSML';

    private $validTypes = ['PlainText', 'SSML'];
    private $value;
    private $type;

    /**
     * Speech constructor.
     *
     * @param string $value
     * @param string $type
     */
    public function __construct($value = '', $type = self::DEFAULT_TYPE)
    {
        $this->value = $value;
        $this->type = $type;
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        $textKey = ($this->getType() === 'SSML') ? 'ssml' : 'text';

        return [
            'outputSpeech' => [
                'type' => $this->getType(),
                $textKey => $this->getValue(),
            ]
        ];
    }

    /**
     * @param string $type
     *
     * @throws \Exception
     *
     * @return $this
     */
    public function setType($type)
    {
        if (!in_array($type, $this->validTypes)) {
            throw new \Exception('Invalid speech type'); //todo: should be specific exception ?  is this helpful?
        }

        $this->type = $type;

        return $this;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @param string
     *
     * @return $this
     */
    public function setValue($value)
    {
        $this->value = $value;

        return $this;
    }

    /**
     * This is here to implement the Speech contract and allow for backwards comparability
     *
     * @return string
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * @deprecated since v 0.3.0
     *
     * @param string $text
     *
     * @return $this
     */
    public function setText($text)
    {
        return $this->setValue($text);
    }

    /**
     * @deprecated since v 0.3.0
     *
     * @return string
     */
    public function getText()
    {
        return $this->getValue();
    }
}


<?php

namespace Develpr\AlexaApp\Http\Middleware;

use Closure;
use Develpr\AlexaApp\Contracts\AmazonEchoDevice;
use Illuminate\Contracts\Routing\Middleware;

class AlexaAuthentication implements Middleware
{
    /**
     * {@inheritdoc}
     */
    public function handle($request, Closure $next)
    {
        /** @var AmazonEchoDevice $device */
        $device = \Alexa::device();

        if (!$device ) {
            return response('Unauthorized.', 401);
        }

        return $next($request);
    }
}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\libs;

/*
 * This file is based on the `CapitalizationHelper` class from the Craft Entitle plugin.
 * https://github.com/experience/entitle.craft-plugin/
 *
 * The original source code is (c) Experience
 * c.f. https://github.com/experience/entitle.craft-plugin/blob/master/LICENSE.txt
 */

use Stringy\Stringy;
use topshelfcraft\wordsmith\Wordsmith;

/**
 * @author Michael Rog <michael@michaelrog.com>
 * @package Wordsmith
 * @since 3.0.0
 */
class APTitleCapitalizer
{

	/**
	 * @var string[]
	 */
	protected $standardProtectedWords;

	/**
	 * @var string[]
	 */
	protected $customProtectedWords;

	/**
	 * Constructor.
	 *
	 * @param array $customProtectedWords
	 */
	public function __construct(array $customProtectedWords = [])
	{
		// TODO: Move the reference to Wordsmith's config out of the library to remove its dependence on Wordsmith.
		$this->standardProtectedWords = Wordsmith::$plugin->getSettings()->apTitleProtectedWords;
		$this->setCustomProtectedWords($customProtectedWords);
	}

	/**
	 * Trims whitespace from the given words, and assigns the
	 * array to the "custom protected words" property.
	 *
	 * @param string[] $words
	 */
	public function setCustomProtectedWords(array $words)
	{
		$this->customProtectedWords = [];
		foreach ($words as $word) {
			$this->customProtectedWords[] = Stringy::create($word)->trim();
		}
	}

	/**
	 * Capitalizes the given string, according to AP rules.
	 *
	 * @param string $string
	 *
	 * @return string
	 */
	public function capitalize($string)
	{
		$string = $this->normalizeInput($string);
		$parts = $this->splitStringIntoParts($string);
		$parts = $this->processStringParts($parts);
		$parts = $this->processFirstSentenceWordsInParts($parts);
		$parts = $this->processLastWordInParts($parts);
		return $this->joinStringParts($parts);
	}

	/**
	 * Normalises the given string, ready for capitalisation.
	 *
	 * @param string $string
	 *
	 * @return string
	 */
	protected function normalizeInput($string)
	{
		$string = Stringy::create($string)->collapseWhitespace();
		$string = $this->normalizeInputPunctuation($string->__toString());
		return Stringy::create($string);
	}

	/**
	 * Splits the given string into an array of elements.
	 *
	 * // TODO: Make sure `splitStringIntoParts` is multi-byte safe
	 *
	 * @param string $string
	 *
	 * @return string[]
	 */
	protected function splitStringIntoParts($string)
	{
		return preg_split(
			"/([A-z]+[\-'’]{1}[A-z]+)|([A-z]+)/u",
			$string,
			null,
			PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
		);
	}

	/**
	 * Processes the given array of string parts.
	 *
	 * @param array $parts
	 *
	 * @return array
	 */
	protected function processStringParts(array $parts)
	{
		array_walk($parts, function (&$part) {
			$part = $this->isWordLike($part) ? $this->processWord($part) : $part;
		});
		return $parts;
	}

	/**
	 * Processes the first words in any sentences within the given array of
	 * parts.
	 *
	 * @param array $parts
	 *
	 * @return array
	 */
	protected function processFirstSentenceWordsInParts(array $parts)
	{
		$length = count($parts);
		$isFirst = true;
		for ($index = 0; $index < $length; $index++) {
			$part = $parts[$index];
			if ($this->isWordLike($part)) {
				if ($isFirst) {
					$parts[$index] = $this->processFirstLastWord($part);
					$isFirst = false;
				}
				continue;
			}
			if ($this->isSentenceDelimiter($part)) {
				$isFirst = true;
			}
		}
		return $parts;
	}

	/**
	 * Processes the last word-like item in the given array of parts.
	 *
	 * @param array $parts
	 *
	 * @return array
	 */
	protected function processLastWordInParts(array $parts)
	{
		$parts = array_reverse($parts);
		$length = count($parts);
		for ($index = 0; $index < $length; $index++) {
			$part = $parts[$index];
			if ($this->isWordLike($part)) {
				$parts[$index] = $this->processFirstLastWord($part);
				break;
			}
		}
		return array_reverse($parts);
	}

	/**
	 * Converts the array of string parts back into a string.
	 *
	 * @param array $parts
	 *
	 * @return string
	 */
	protected function joinStringParts(array $parts)
	{
		return implode('', $parts);
	}

	/**
	 * Ensures that punctuation characters have no leading spaces, and one
	 * trailing space.
	 *
	 * @param string $string
	 *
	 * @return string
	 */
	protected function normalizeInputPunctuation($string)
	{
		return $this->replacePattern(
			$string,
			'/\s*([,;:])\s*([A-z])/',
			'$1 $2'
		);
	}

	/**
	 * Returns a boolean indicating whether the given string looks, walks, and
	 * talks like a word.
	 *
	 * // TODO: Make `isWordLike` multi-byte safe
	 *
	 * @param string $string
	 *
	 * @return bool
	 */
	protected function isWordLike($string)
	{
		return (bool)preg_match(
			"/(^[A-z]+[\-'’]{1}[A-z]+$)|(^[A-z]+$)/u",
			$string
		);
	}

	/**
	 * Processes the given word.
	 *
	 * @param string $word
	 *
	 * @return string
	 */
	protected function processWord($word)
	{
		if ($this->isStandardProtectedWord($word)) {
			return $this->lowercaseWord($word);
		}
		if ($this->isCustomProtectedWord($word)) {
			return $word;
		}
		return $this->capitalizeWord($word);
	}

	/**
	 * Processes the first or last word in the sentence. The first and last
	 * word should always be capitalised, _unless_ it is a custom protected
	 * word.
	 *
	 * @param string $word
	 *
	 * @return string
	 */
	protected function processFirstLastWord($word)
	{
		return $this->isCustomProtectedWord($word)
			? $word
			: $this->capitalizeWord($word);
	}

	/**
	 * Returns a boolean indicating whether the given string is a sentence
	 * delimiter.
	 *
	 * @param string $string
	 *
	 * @return bool
	 */
	protected function isSentenceDelimiter($string)
	{
		return $string === '.';
	}

	/**
	 * Replaces all matches of the given regular expression pattern in the
	 * given string with the given replacement. If an error occurs, returns the
	 * original string.
	 *
	 * // TODO: Make `replacePattern` multi-byte safe
	 *
	 * @param string $string
	 * @param string $pattern
	 * @param string $replacement
	 *
	 * @return string
	 */
	protected function replacePattern($string, $pattern, $replacement)
	{
		$replaced = preg_replace($pattern, $replacement, $string);
		return is_null($replaced) ? $string : $replaced;
	}

	/**
	 * Returns a boolean indicating whether the given word is a standard
	 * protected word, which should not be capitalised.
	 *
	 * @param string $word
	 *
	 * @return bool
	 */
	protected function isStandardProtectedWord($word)
	{
		return in_array(strtolower($word), $this->standardProtectedWords);
	}

	/**
	 * Lowercases the given word.
	 *
	 * @param string $word
	 *
	 * @return Stringy
	 */
	protected function lowercaseWord($word)
	{
		return Stringy::create($word)->toLowerCase();
	}

	/**
	 * Returns a boolean indicating whether the given word is a custom
	 * protected word, which should not be modified.
	 *
	 * @param string $word
	 *
	 * @return bool
	 */
	protected function isCustomProtectedWord($word)
	{
		return in_array($word, $this->customProtectedWords);
	}

	/**
	 * Capitalises the given word.
	 *
	 * @param string $word
	 *
	 * @return Stringy
	 */
	protected function capitalizeWord($word)
	{
		return $this->lowercaseWord($word)->upperCaseFirst();
	}

}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\libs;

/**
 * FullNameParser
 *
 * Split a full name into its constituent parts
 *   - prefix/salutation (Mr. Mrs. Dr. etc)
 *   - given/first name
 *   - middle name/initial(s)
 *   - surname (last name)
 *   - surname base (last name without compounds)
 *   - surname compounds (only the compounds)
 *   - suffix (II, PhD, Jr. etc)
 *
 * Author: Josh Fraser
 *
 * Contribution from Clive Verrall www.cliveverrall.com February 2016
 *
 * Other contributions:
 * - eric willis [list of honorifics](http://notes.ericwillis.com/2009/11/common-name-prefixes-titles-and-honorifics/)
 * - `TomThak` for raising issue #16 and providing [wikepedia resource](https://cs.wikipedia.org/wiki/Akademick%C3%BD_titul)
 * - `atla5` for closing the issue.
 */
class FullNameParser
{

	/**
	 * Create the dictionary of terms for use later
	 *
	 *  - Common honorific prefixes (english)
	 *  - Common compound surname identifiers
	 *  - Common suffixes (lineage and professional)
	 * Note: longer professional titles should appear earlier in the array than shorter titles to reduce the risk of
	 * mis-identification e.g. BEng before BE Also note that case and periods are part of the matching for professional
	 * titles and therefore need to be correct, there are no case conversions
	 */
	protected $dict = array(
		'prefix' => array(
			'Mr.' => array('mr', 'mister', 'master'),
			'Mrs.' => array('mrs', 'missus', 'missis'),
			'Ms.' => array('ms', 'miss'),
			'Dr.' => array('dr'),
			'Rev.' => array("rev", "rev'd", "reverend"),
			'Fr.' => array('fr', 'father'),
			'Sr.' => array('sr', 'sister'),
			'Prof.' => array('prof', 'professor'),
			'Sir' => array('sir'),
			'Hon.' => array('honorable'),
			'Pres.' => array('president'),
			'Gov' => array('governor', 'governer'),
			'Ofc' => array('officer'),
			'Msgr' => array('monsignor'),
			'Sr.' => array('sister'),
			'Br.' => array('brother'),
			'Supt.' => array('superintendent'),
			'Rep.' => array('representatitve'),
			'Sen.' => array('senator'),
			'Amb.' => array('ambassador'),
			'Treas.' => array('treasurer'),
			'Sec.' => array('secretary'),
			'Pvt.' => array('private'),
			'Cpl.' => array('corporal'),
			'Sgt.' => array('sargent'),
			'Adm.' => array('administrative', 'administrator', 'administrater'),
			'Maj.' => array('major'),
			'Capt.' => array('captain'),
			'Cmdr.' => array('commander'),
			'Lt.' => array('lieutenant'),
			'Lt. Col.' => array('lieutenant colonel'),
			'Col.' => array('colonel'),
			'Gen.' => array('general'),
			'Bc.' => array('bachelor', 'baccalaureus'),
			'BcA.' => array('bachelor of arts', 'baccalaureus artis'),
			'ICDr.' => array('doctor of canon law', 'juris cononici doctor'),
			'Ing.' => array('engineer', 'ingenieur'),
			'Ing. sheet.' => array('architect engineer', 'intrudes upon architectus'),
			'JUDr.' => array('juris doctor utriusque', 'doctor rights'),
			'MDDr.' => array('doctor of dental medicine', 'medicinae doctor dentium'),
			'MgA.' => array('master of arts', 'magister artis'),
			'Mgr.' => array('master'),
			'MD.' => array('doctor of general medicine'),
			'DVM.' => array('doctor of veterinary medine'),
			'PaedDr.' => array('doctor of education'),
			'PharmDr.' => array('doctor of pharmacy'),
			'PhDr.' => array('doctor of philosophy'),
			'PhMr.' => array('master of pharmacy'),
			'RCDr.' => array('doctor of business studies'),
			'RNDr.' => array('doctor of science'),
			'DSc.' => array('doctor of science'),
			'RSDr.' => array('doctor of socio-political sciences'),
			'RTDr.' => array('doctor of technical sciences'),
			'ThDr.' => array('doctor of theology'),
			'Th.D.' => array('doctor of theology'),
			'ThLic.' => array('licentiate of theology'),
			'ThMgr.' => array('master of theology', 'master of divinity'),
			'Acad.' => array('academian', 'academic'),
			'ArtD.' => array('doctor of arts'),
			'DiS.' => array('certified specialist'),
			'As.' => array('assistant'),
			'Odb. As.' => array('assistant professor'),
			'Doc.' => array('associate professor'),
			' ' => array('the')
		),
		'compound' => array('da', 'de', 'del', 'della', 'dem', 'den', 'der', 'di', 'du', 'het', 'la', 'onder', 'op', 'pietro', 'st.', 'st', '\'t', 'ten', 'ter', 'van', 'vanden', 'vere', 'von'),
		'suffixes' => array(
			'line' => array('I', 'II', 'III', 'IV', 'V', '1st', '2nd', '3rd', '4th', '5th', 'Senior', 'Junior', 'Jr', 'Sr'),
			'prof' => array('AO', 'B.A.', 'M.Sc', 'BCompt', 'PhD', 'Ph.D.', 'APR', 'RPh', 'PE', 'MD', 'M.D.', 'MA', 'DMD', 'CME', 'BSc', 'Bsc', 'BSc(hons)', 'Ph.D.', 'BEng', 'M.B.A.', 'MBA', 'FAICD', 'CM', 'OBC', 'M.B.', 'ChB', 'FRCP', 'FRSC',
				'FREng', 'Esq', 'MEng', 'MSc', 'J.D.', 'JD', 'BGDipBus', 'Dip', 'Dipl.Phys', 'M.H.Sc.', 'MPA', 'B.Comm', 'B.Eng', 'B.Acc', 'FSA', 'PGDM', 'FCPA', 'RN', 'R.N.', 'MSN',
				'PCA', 'PCCRM', 'PCFP', 'PCGD', 'PCHR', 'PCM', 'PCPS', 'PCPM', 'PCSCM', 'PCSM', 'PCMM', 'PCTC', 'ACA', 'FCA', 'ACMA', 'FCMA', 'AAIA', 'FAIA', 'CCC', 'MIPA', 'FIPA', 'CIA', 'CFE', 'CISA', 'CFAP',
				'QC', 'Q.C.', 'M.Tech', 'CTA', 'C.I.M.A.', 'B.Ec',
				'CFIA', 'ICCP', 'CPS', 'CAP-OM', 'CAPTA', 'TNAOAP', 'AFA', 'AVA', 'ASA', 'CAIA', 'CBA', 'CVA', 'ICVS', 'CIIA', 'CMU', 'PFM', 'PRM', 'CFP', 'CWM', 'CCP', 'EA', 'CCMT', 'CGAP', 'CDFM', 'CFO', 'CGFM', 'CGAT', 'CGFO', 'CMFO', 'CPFO', 'CPFA',
				'BMD', 'BIET', 'P.Eng', 'PE', 'MBBS', 'MB', 'BCh', 'BAO', 'BMBS', 'MBBChir', 'MBChBa', 'MPhil', 'LL.D', 'LLD', 'D.Lit', 'DEA', 'DESS', 'DClinPsy', 'DSc', 'MRes', 'M.Res', 'Psy.D', 'Pharm.D',
				'BA(Admin)', 'BAcc', 'BACom', 'BAdmin', 'BAE', 'BAEcon', 'BA(Ed)', 'BA(FS)', 'BAgr', 'BAH', 'BAI', 'BAI(Elect)', 'BAI(Mech)', 'BALaw', 'BAO', 'BAppSc', 'BArch', 'BArchSc', 'BARelSt', 'BASc', 'BASoc', 'DDS', 'D.D.S.',
				'BASS', 'BATheol', 'BBA', 'BBLS', 'BBS', 'BBus', 'BChem', 'BCJ', 'BCL', 'BCLD(SocSc)', 'BClinSci', 'BCom', 'BCombSt', 'BCommEdCommDev', 'BComp', 'BComSc', 'BCoun', 'BD', 'BDes', 'BE', 'BEcon', 'BEcon&Fin', 'M.P.P.M.', 'MPPM',
				'BEconSci', 'BEd', 'BEng', 'BES', 'BEng(Tech)', 'BFA', 'BFin', 'BFLS', 'BFST', 'BH', 'BHealthSc', 'BHSc', 'BHy', 'BJur', 'BL', 'BLE', 'BLegSc', 'BLib', 'BLing', 'BLitt', 'BLittCelt', 'BLS', 'BMedSc', 'BMet',
				'BMid', 'BMin', 'BMS', 'BMSc', 'BMSc', 'BMS', 'BMus', 'BMusEd', 'BMusPerf', 'BN', 'BNS', 'BNurs', 'BOptom', 'BPA', 'BPharm', 'BPhil', 'TTC', 'DIP', 'Tchg', 'BEd', 'MEd', 'ACIB', 'FCIM', 'FCIS', 'FCS', 'Fcs',
				'Bachelor', 'O.C.', 'JP', 'C.Eng', 'C.P.A.', 'B.B.S.', 'MBE', 'GBE', 'KBE', 'DBE', 'CBE', 'OBE', 'MRICS', 'D.P.S.K.', 'D.P.P.J.', 'DPSK', 'DPPJ', 'B.B.A.', 'GBS', 'MIGEM', 'M.I.G.E.M.', 'FCIS',
				'BPhil(Ed)', 'BPhys', 'BPhysio', 'BPl', 'BRadiog', 'BSc', 'B.Sc', 'BScAgr', 'BSc(Dairy)', 'BSc(DomSc)', 'BScEc', 'BScEcon', 'BSc(Econ)', 'BSc(Eng)', 'BScFor', 'BSc(HealthSc)', 'BSc(Hort)', 'BBA', 'B.B.A.',
				'BSc(MCRM)', 'BSc(Med)', 'BSc(Mid)', 'BSc(Min)', 'BSc(Psych)', 'BSc(Tech)', 'BSD', 'BSocSc', 'BSS', 'BStSu', 'BTchg', 'BTCP', 'BTech', 'BTechEd', 'BTh', 'BTheol', 'BTS', 'EdB', 'LittB', 'LLB', 'MA', 'MusB', 'ScBTech',
				'CEng', 'FCA', 'CFA', 'Cfa', 'C.F.A.', 'LLB', 'LL.B', 'LLM', 'LL.M', 'CA(SA)', 'C.A.', 'CA', 'CPA', 'Solicitor', 'DMS', 'FIWO', 'CEnv', 'MICE', 'MIWEM', 'B.Com', 'BCom', 'BAcc', 'BA', 'BEc', 'MEc', 'HDip', 'B.Bus.', 'E.S.C.P.')
		),
		'vowels' => array('a', 'e', 'i', 'o', 'u')
	);

	protected $not_nicknames = array("(hons)");

	/**
	 * Parse Static entry point.
	 *
	 * @param string $name the full name you wish to parse
	 * @return array returns associative array of name parts
	 */
	public static function parse($name)
	{
		$parser = new self();
		return $parser->parse_name($name);
	}

	/**
	 * This is the primary method which calls all other methods
	 *
	 * @param string $name the full name you wish to parse
	 * @return array returns associative array of name parts
	 */
	public function parse_name($full_name)
	{

		# Remove leading/trailing whitespace
		$full_name = trim($full_name);

		// remove any words that don't add value
		// $full_name = str_replace("(Hons)", '', $full_name );
		// $full_name = str_replace("(hons)", '', $full_name );

		# Setup default vars
		extract(array('salutation' => '', 'fname' => '', 'initials' => '', 'lname' => '', 'lname_base' => '', 'lname_compound' => '', 'suffix' => ''));

		# Find all the professional suffixes possible
		$professional_suffix = $this->get_pro_suffix($full_name);

		// The position of the first professional suffix denotes the end of the name and the start of suffixes
		$first_suffix_index = mb_strlen($full_name);
		foreach ($professional_suffix as $key => $psx) {
			$start = mb_strpos($full_name, $psx);
			if ($start === FALSE) {
				echo "ASSERT ERROR, the professional suffix:" . $psx . " cannot be found in the full name:" . $full_name . "<br>";
				continue;
			}
			if ($start < $first_suffix_index) {
				$first_suffix_index = $start;
			}
		}

		// everything to the right of the first professional suffix is part of the suffix
		$suffix = mb_substr($full_name, $first_suffix_index);

		// remove the suffixes from the full_name
		$full_name = mb_substr($full_name, 0, $first_suffix_index);

		# Deal with nickname, push to array
		$has_nick = $this->get_nickname($full_name);
		if ($has_nick) {
			# Remove wrapper chars from around nickname
			$name['nickname'] = mb_substr($has_nick, 1, (mb_strlen($has_nick) - 2));
			# Remove the nickname from the full name
			$full_name = str_replace($has_nick, '', $full_name);
			# Get rid of consecutive spaces left by the removal
			$full_name = str_replace('  ', ' ', $full_name);
		}

		# Grab a list of words from the remainder of the full name
		$unfiltered_name_parts = $this->break_words($full_name);

		# Is first word a title or multiple titles consecutively?
		if (count($unfiltered_name_parts)) {
			// only start looking if there are any words left in the name to process
			while (count($unfiltered_name_parts) > 0 && $s = $this->is_salutation($unfiltered_name_parts[0])) {
				$salutation .= "$s ";
				array_shift($unfiltered_name_parts);
			}
			$salutation = trim($salutation);
			// Find if there is a line suffix, if so then move it out
			# Is last word a suffix or multiple suffixes consecutively?
			while (count($unfiltered_name_parts) > 0 && $s = $this->is_line_suffix($unfiltered_name_parts[count($unfiltered_name_parts) - 1], $full_name)) {
				if ($suffix != "") {
					$suffix = $s . ", " . $suffix;
				} else {
					$suffix .= $s;
				}
				array_pop($unfiltered_name_parts);
			}
			$suffix = trim($suffix);
		} else {
			$salutation = "";
			$suffix = "";
		}

		// Re-pack the unfiltered name parts array and exclude empty words
		$name_arr = array();
		foreach ($unfiltered_name_parts as $key => $name_part) {
			$name_part = trim($name_part);
			$name_part = rtrim($name_part, ',');
			if (mb_strlen($name_part) == '1') {
				// If any word left is of one character that is not alphabetic then it is not a real word, so remove it
				if (!$this->mb_ctype_alpha($name_part)) {
					$name_part = "";
				}
			}
			if (mb_strlen(trim($name_part))) {
				$name_arr[] = $name_part;
			}
		}
		$unfiltered_name_parts = $name_arr;

		# set the ending range after prefix/suffix trim
		$end = count($unfiltered_name_parts);

		# concat the first name
		for ($i = 0; $i < $end - 1; $i++) {
			$word = $unfiltered_name_parts[$i];
			# move on to parsing the last name if we find an indicator of a compound last name (Von, Van, etc)
			# we use $i != 0 to allow for rare cases where an indicator is actually the first name (like "Von Fabella")
			if ($this->is_compound($word) && $i != 0) {
				break;
			}
			# is it a middle initial or part of their first name?
			# if we start off with an initial, we'll call it the first name
			if ($this->is_initial($word)) {
				# is the initial the first word?
				if ($i == 0) {
					# if so, do a look-ahead to see if they go by their middle name
					# for ex: "R. Jason Smith" => "Jason Smith" & "R." is stored as an initial
					# but "R. J. Smith" => "R. Smith" and "J." is stored as an initial
					if ($this->is_initial($unfiltered_name_parts[$i + 1])) {
						$fname .= " " . mb_strtoupper($word);
					} else {
						$initials .= " " . mb_strtoupper($word);
					}
				} # otherwise, just go ahead and save the initial
				else {
					$initials .= " " . mb_strtoupper($word);
				}
			} else {
				$fname .= " " . $this->fix_case($word);
			}
		}

		if (count($unfiltered_name_parts)) {
			# check that we have more than 1 word in our string
			if ($end - 0 > 1) {
				# concat the last name and split last name in base and compound
				for ($i; $i < $end; $i++) {
					if ($this->is_compound($unfiltered_name_parts[$i])) {
						$lname_compound .= " " . $unfiltered_name_parts[$i];
					} else {
						$lname_base .= " " . $this->fix_case($unfiltered_name_parts[$i]);
					}
					$lname .= " " . $this->fix_case($unfiltered_name_parts[$i]);
				}
			} else {
				# otherwise, single word strings are assumed to be first names
				$fname = $this->fix_case($unfiltered_name_parts[$i]);
			}
		} else {
			$fname = "";
		}

		# return the various parts in an array
		$name['salutation'] = $salutation;
		$name['fname'] = trim($fname);
		$name['initials'] = trim($initials);
		$name['lname'] = trim($lname);
		$name['lname_base'] = trim($lname_base);
		$name['lname_compound'] = trim($lname_compound);
		$name['suffix'] = $suffix;
		return $name;
	}

	/**
	 * Checks for the existence of, and returns professional suffix
	 *
	 * @param string $name the name you wish to test
	 * @return mixed returns the suffix if exists, false otherwise
	 */
	public function get_pro_suffix($name)
	{

		$found_suffix_arr = array();
		foreach ($this->dict['suffixes']['prof'] as $suffix) {
			if (preg_match('/[,\s]+' . preg_quote($suffix) . '\b/i', $name, $matches)) {
				$found_suffix = trim($matches[0]);
				$found_suffix = rtrim($found_suffix, ',');
				$found_suffix = ltrim($found_suffix, ',');
				$found_suffix_arr[] = trim($found_suffix);
			}
		}
		return $found_suffix_arr;
	}

	/**
	 * Function to check name for existence of nickname based on these stipulations
	 *  - String wrapped in parentheses (string)
	 *  - String wrapped in double quotes "string"
	 *  x String wrapped in single quotes 'string'
	 *
	 *  I removed the check for strings in single quotes 'string' due to possible
	 *  conflicts with names that may include apostrophes. Arabic transliterations, for example
	 *
	 * @param string $name the name you wish to test against
	 * @return mixed returns nickname if exists, false otherwise
	 */
	protected function get_nickname($name)
	{
		if (preg_match("/[\(|\"].*?[\)|\"]/", $name, $matches)) {
			if (!in_array(mb_strtolower($matches[0]), $this->not_nicknames)) {
				return $matches[0];
			} else {
				return false;
			}
		}
		return false;
	}

	/**
	 * Breaks name into individual words
	 *
	 * @param string $name the full name you wish to parse
	 * @return array full list of words broken down by spaces
	 */
	public function break_words($name)
	{
		$temp_word_arr = explode(' ', $name);
		$final_word_arr = array();
		foreach ($temp_word_arr as $key => $word) {
			if ($word != "" && $word != ",") {
				$final_word_arr[] = $word;
			}
		}
		return $final_word_arr;
	}

	/**
	 * Checks word against list of common honorific prefixes
	 *
	 * @param string $word the single word you wish to test
	 * @return boolean
	 */
	protected function is_salutation($word)
	{
		$word = str_replace('.', '', mb_strtolower($word));
		foreach ($this->dict['prefix'] as $replace => $originals) {
			if (in_array($word, $originals)) {
				return $replace;
			}
		}
		return false;
	}

	/**
	 * Checks word against array of common lineage suffixes
	 *
	 * @param string $word the single word you wish to test
	 * @param string $name full name for context in determining edge-cases
	 * @return mixed boolean if false, string if true (returns suffix)
	 */
	protected function is_line_suffix($word, $name)
	{

		# Ignore periods and righ commas, normalize case
		$word = str_replace('.', '', mb_strtolower($word));
		$word = rtrim($word, ',');

		# Search the array for our word
		$line_match = array_search($word, array_map('mb_strtolower', $this->dict['suffixes']['line']));

		# Now test our edge cases based on lineage
		if ($line_match !== false) {
			# Store our match
			$matched_case = $this->dict['suffixes']['line'][$line_match];

			# Remove it from the array
			$temp_array = $this->dict['suffixes']['line'];
			unset($temp_array[$line_match]);

			# Make sure we're dealing with the suffix and not a surname
			if ($word == 'senior' || $word == 'junior') {

				# If name is Joshua Senior, it's pretty likely that Senior is the surname
				# However, if the name is Joshua Jones Senior, then it's likely a suffix
				if ($this->mb_str_word_count($name) < 3) {
					return false;
				}

				# If the word Junior or Senior is contained, but so is some other
				# lineage suffix, then the word is likely a surname and not a suffix
				foreach ($temp_array as $suffix) {
					if (preg_match("/\b" . $suffix . "\b/i", $name)) {
						return false;
					}
				}
			}
			return $matched_case;
		}
		return false;
	}

	public function mb_str_word_count($text)
	{
		if (empty($text)) {
			return 0;
		} else {
			return preg_match('/s+/', $text) + 1;
		}
	}

	public function mb_ctype_alpha($text)
	{
		return (bool)preg_match('/^\p{L}*$/', $text);
	}

	/**
	 * Checks our dictionary of compound indicators to see if last name is compound
	 *
	 * @param string $word the single word you wish to test
	 * @return boolean
	 */
	protected function is_compound($word)
	{
		return in_array(mb_strtolower($word), $this->dict['compound']);
	}

	# ucfirst words split by dashes or periods
	# ucfirst all upper/lower strings, but leave camelcase words alone

	/**
	 * Test string to see if it's a single letter/initial (period optional)
	 *
	 * @param string $word the single word you wish to test
	 * @return boolean
	 */
	protected function is_initial($word)
	{
		return ((mb_strlen($word) == 1) || (mb_strlen($word) == 2 && $word{1} == "."));
	}

	# helper public function for fix_case

	public function fix_case($word)
	{

		# Fix case for words split by periods (J.P.)
		if (mb_strpos($word, '.') !== false) {
			$word = $this->safe_ucfirst(".", $word);;
		}

		# Fix case for words split by hyphens (Kimura-Fay)
		if (mb_strpos($word, '-') !== false) {
			$word = $this->safe_ucfirst("-", $word);
		}

		# Special case for single letters
		if (mb_strlen($word) == 1) {
			$word = mb_strtoupper($word);
		}

		# Special case for 2-letter words
		if (mb_strlen($word) == 2) {
			# Both letters vowels (uppercase both)
			if (in_array(mb_strtolower($word{0}), $this->dict['vowels']) && in_array(mb_strtolower($word{1}), $this->dict['vowels'])) {
				$word = mb_strtoupper($word);
			}
			# Both letters consonants (uppercase both)
			if (!in_array(mb_strtolower($word{0}), $this->dict['vowels']) && !in_array(mb_strtolower($word{1}), $this->dict['vowels'])) {
				$word = mb_strtoupper($word);
			}
			# First letter is vowel, second letter consonant (uppercase first)
			if (in_array(mb_strtolower($word{0}), $this->dict['vowels']) && !in_array(mb_strtolower($word{1}), $this->dict['vowels'])) {
				$word = $this->mb_ucfirst(mb_strtolower($word));
			}
			# First letter consonant, second letter vowel or "y" (uppercase first)
			if (!in_array(mb_strtolower($word{0}), $this->dict['vowels']) && (in_array(mb_strtolower($word{1}), $this->dict['vowels']) || mb_strtolower($word{1}) == 'y')) {
				$word = $this->mb_ucfirst(mb_strtolower($word));
			}
		}

		# Fix case for words which aren't initials, but are all uppercase or lowercase
		if ((mb_strlen($word) >= 3) && ($this->mb_ctype_upper($word) || $this->mb_ctype_lower($word))) {
			$word = $this->mb_ucfirst(mb_strtolower($word));
		}

		return $word;
	}

	# helper public function for multibytes ctype_alpha

	public function safe_ucfirst($seperator, $word)
	{
		# uppercase words split by the seperator (ex. dashes or periods)
		$parts = explode($seperator, $word);
		foreach ($parts as $word) {
			$words[] = ($this->is_camel_case($word)) ? $word : $this->mb_ucfirst(mb_strtolower($word));
		}
		return implode($seperator, $words);
	}

	# helper public function for multibytes ctype_lower

	/**
	 * Checks for camelCase words such as McDonald and MacElroy
	 *
	 * @param string $word the single word you wish to test
	 * @return boolean
	 */
	protected function is_camel_case($word)
	{
		if (preg_match('/\p{L}(\p{Lu}*\p{Ll}\p{Ll}*\p{Lu}|\p{Ll}*\p{Lu}\p{Lu}*\p{Ll})\p{L}*/', $word)) {
			return true;
		}
		return false;
	}

	# helper public function for multibytes ctype_upper

	public function mb_ucfirst($string)
	{
		$strlen = mb_strlen($string);
		$firstChar = mb_substr($string, 0, 1);
		$then = mb_substr($string, 1, $strlen - 1);
		return mb_strtoupper($firstChar) . $then;
	}

	# helper public function for multibytes str_word_count

	public function mb_ctype_upper($text)
	{
		return (bool)preg_match('/^\p{Lu}*$/', $text);
	}

	# helper public function for multibytes ucfirst

	public function mb_ctype_lower($text)
	{
		return (bool)preg_match('/^\p{Ll}*$/', $text);
	}

}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\services;

use craft\base\Component;
use PHP_Typography\Fixes\Node_Fixes\Dewidow_Fix;
use PHP_Typography\Fixes\Node_Fixes\Smart_Quotes_Fix;
use PHP_Typography\Fixes\Node_Fixes\Style_Ampersands_Fix;
use PHP_Typography\Fixes\Node_Fixes\Style_Caps_Fix;
use topshelfcraft\wordsmith\Wordsmith;

/**
 * @author Michael Rog <michael@michaelrog.com>
 * @package Wordsmith
 * @since 3.0.0
 */
class TypographyService extends Component
{

	/*
     * Private properties
     * ===========================================================================
     */

	private $_typographer;
	private $_typographySettings;

	/*
     * Public methods
     * ===========================================================================
     */

	/**
	 *
	 */
	public function init()
	{
		parent::init();
	}

	/**
	 * @return \PHP_Typography\PHP_Typography
	 */
	public function getTypographer()
	{

		if (!isset($this->_typographer))
		{

			// Create a new PhpTypography instance

			$this->_typographer = new \PHP_Typography\PHP_Typography();

			// Create a new phpTypographySettings instance

			$this->_typographySettings = new \PHP_Typography\Settings();

			// Apply settings from our plugin config

			$settings = Wordsmith::$plugin->getSettings()->typographySettings;

			if (is_array($settings)) {
				foreach ($settings as $key => $value) {
					$this->_typographySettings->{$key}($value);
				}
			}

		}

		return $this->_typographer;

	}

	/**
	 * @param array $adhocSettings
	 * @return \PHP_Typography\Settings
	 */
	public function getTypographySettings($adhocSettings = [])
	{

		$settings = (clone $this->_typographySettings);

		if (is_array($adhocSettings)) {
			foreach ($adhocSettings as $key => $value) {
				$settings->{$key}($value);
			}
		}

		return $settings;

	}

	/**
	 * @param string $text
	 *
	 * @return string
	 */
	public function typogrify($text, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process($text, $this->getTypographySettings($adhocSettings));
		return $result;
	}

	/**
	 * @param string $text
	 *
	 * @return string
	 */
	public function typogrifyFeed($text, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process_feed($text, $this->getTypographySettings($adhocSettings));
		return $result;
	}

	/**
	 * @param string $text
	 *
	 * @return string
	 */
	public function smartypants($text, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process_textnodes($text, [(new Smart_Quotes_Fix()), 'apply'], $this->getTypographySettings($adhocSettings));
		return $result;
	}

	/**
	 * @param $text
	 * @param array $adhocSettings
	 * @return string
	 */
	public function widont($text, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process_textnodes($text, [(new Dewidow_Fix()), 'apply'], $this->getTypographySettings($adhocSettings));
		return $result;
	}

	/**
	 * @param $text
	 * @param $class
	 * @param array $adhocSettings
	 * @return string
	 */
	public function wrapAmps($text, $class, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process_textnodes($text, [(new Style_Ampersands_Fix($class)), 'apply'], $this->getTypographySettings($adhocSettings));
		return $result;
	}

	/**
	 * @param $text
	 * @param $class
	 * @param array $adhocSettings
	 * @return string
	 */
	public function wrapCaps($text, $class, $adhocSettings = [])
	{
		if (empty($text)) {
			return '';
		}
		$result = $this->getTypographer()->process_textnodes($text, [(new Style_Caps_Fix($class)), 'apply'], $this->getTypographySettings($adhocSettings));
		return $result;
	}

}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\services;

use craft\base\Component;
use Stringy\Stringy;
use topshelfcraft\wordsmith\libs\Emoji;
use topshelfcraft\wordsmith\Wordsmith;

/**
 * @author Michael Rog <michael@michaelrog.com>
 * @package Wordsmith
 * @since 3.0.0
 */
class EmojiService extends Component
{

	private $_emojiData;

	/**
	 * @return array
	 */
	public function getEmojiData()
	{

		if (!isset($this->_emojiData))
		{

			$data = json_decode(file_get_contents(Wordsmith::$plugin->getBasePath() . '/libs/emoji.json'));

			$this->_emojiData = array_map(
				function($entry){

					$assumedName = !empty($entry->name) ? $entry->name : (string) Stringy::create($entry->short_name)->humanize()->toUpperCase();
					$snakeName = Stringy::create($assumedName)->toLowerCase()->replace('&', 'AND')->slugify('_');
					$constantName = (string) $snakeName->toUpperCase();
					$camelName = (string) Stringy::create($assumedName)->toLowerCase()->camelize();
					// TODO: I think toLowerCase is redundant before camelize...? ^^

					return [
						'name' => $entry->name,
						'unified' => $entry->unified,
						'nonQualified' => $entry->non_qualified,
						'text' => $entry->text,
						'texts' => $entry->texts,
						'shortName' => $entry->short_name,
						'shortNames' => $entry->short_names,
						'snakeName' => (string) $snakeName,
						'constantName' => $constantName,
						'camelName' => $camelName,
						'unicode' => constant('topshelfcraft\wordsmith\libs\Emoji::'.$constantName)
					];
				},
				$data
			);

			// GC this plz.
			unset($data);

		}

		return $this->_emojiData;

	}

	/**
	 * @param string $startDelimiter
	 * @param string $endDelimiter
	 * @return array
	 */
	public function getShortNameReplacements($startDelimiter = ':', $endDelimiter = ':')
	{

		$replacements = [];

		foreach($this->getEmojiData() as $emoji)
		{
			if (!empty($emoji['shortNames']))
			{
				foreach ($emoji['shortNames'] as $shortName)
				{
					$replacements[$startDelimiter.$shortName.$endDelimiter] = $emoji['unicode'];
				}
			}
		}

		return $replacements;

	}

	/**
	 * @param string $startDelimiter
	 * @param string $endDelimiter
	 * @return array
	 */
	public function getSnakeNameReplacements($startDelimiter = ':', $endDelimiter = ':')
	{

		$replacements = [];

		foreach($this->getEmojiData() as $emoji)
		{
			if (!empty($emoji['snakeName']))
			{
				$replacements[$startDelimiter.$emoji['snakeName'].$endDelimiter] = $emoji['unicode'];
			}
		}

		return $replacements;

	}

	/**
	 * @param string $startDelimiter
	 * @param string $endDelimiter
	 * @return array
	 */
	public function getConstantNameReplacements($startDelimiter = ':', $endDelimiter = ':')
	{

		$replacements = [];

		foreach($this->getEmojiData() as $emoji)
		{
			if (!empty($emoji['constantName']))
			{
				$replacements[$startDelimiter.$emoji['constantName'].$endDelimiter] = $emoji['unicode'];
			}
		}

		return $replacements;

	}

	/**
	 * @param string $startDelimiter
	 * @param string $endDelimiter
	 * @return array
	 */
	public function getCamelNameReplacements($startDelimiter = ':', $endDelimiter = ':')
	{

		$replacements = [];

		foreach($this->getEmojiData() as $emoji)
		{
			if (!empty($emoji['camelName']))
			{
				$replacements[$startDelimiter.$emoji['camelName'].$endDelimiter] = $emoji['unicode'];
			}
		}

		return $replacements;

	}

	/**
	 * @param string $startDelimiter
	 * @param string $endDelimiter
	 * @return array
	 */
	public function getTextReplacements($startDelimiter = '', $endDelimiter = '')
	{

		$replacements = [];

		foreach($this->getEmojiData() as $emoji)
		{
			if (!empty($emoji['texts']))
			{
				foreach ($emoji['texts'] as $text)
				{
					$replacements[$startDelimiter.$text.$endDelimiter] = $emoji['unicode'];
				}
			}
		}

		return $replacements;

	}

	/**
	 * @param $str
	 * @param string $nameStartDelimiter
	 * @param string $nameEndDelimiter
	 * @param string $textStartDelimiter
	 * @param string $textEndDelimiter
	 *
	 * @return string The parsed text
	 */
	public function emojify($str, $nameStartDelimiter=':', $nameEndDelimiter=":", $textStartDelimiter='', $textEndDelimiter='')
	{

		$sets = [
			$this->getShortNameReplacements($nameStartDelimiter, $nameEndDelimiter),
			$this->getSnakeNameReplacements($nameStartDelimiter, $nameEndDelimiter),
			$this->getConstantNameReplacements($nameStartDelimiter, $nameEndDelimiter),
			$this->getCamelNameReplacements($nameStartDelimiter, $nameEndDelimiter),
			$this->getTextReplacements($textStartDelimiter, $textEndDelimiter),
		];

		foreach ($sets as $set)
		{
			$str = str_replace(array_keys($set), $set, $str);
		}

		return $str;

	}

	/**
	 * For internal use only.
	 *
	 * We use this function to reformat data from the source-of-truth JSON in order to create the code for \libs\Emoji.php
	 *
	 * @return string
	 */
	public function generateEmojiPhpConstants()
	{

		$data = json_decode(file_get_contents(Wordsmith::$plugin->getBasePath() . '/libs/emoji.json'));

		$data = array_map(
			function($entry){

				$assumedName = !empty($entry->name) ? $entry->name : (string) Stringy::create($entry->short_name)->humanize()->toUpperCase();
				$constantName = (string) Stringy::create($assumedName)->toLowerCase()->replace('&', 'AND')->slugify('_')->toUpperCase();

				$char = '';
				foreach (explode('-', $entry->unified) as $code)
				{
					$char .= '\u{' . $code . '}';
				}

				return [
					'constantName' => $constantName,
					'unicode' => $char,
				];
			},
			$data
		);

		$output = '';

		foreach ($data as $emoji)
		{
			$output .= "\n" . "const {$emoji['constantName']} = \"{$emoji['unicode']}\";";
		}

		return $output;

	}

}


<?php
/**
 * Wordsmith
 *
 * @author     Michael Rog <michael@michaelrog.com>
 * @link       https://topshelfcraft.com
 * @copyright  Copyright 2017, Top Shelf Craft (Michael Rog)
 * @see        https://github.com/topshelfcraft/Wordsmith
 */

namespace topshelfcraft\wordsmith\twigextensions;

use topshelfcraft\wordsmith\Wordsmith;

use Craft;
use Twig_Filter;
use Twig_Function;

/**
 * @author Michael Rog <michael@michaelrog.com>
 * @package Wordsmith
 * @since 3.0.0
 */
class WordsmithTwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface
{

    /*
     * Public methods
     * ===========================================================================
     */

    /**
     * Returns the name of the extension.
     *
     * @return string The extension name
     */
    public function getName()
    {
        return 'Wordsmith';
    }

    /**
     * Returns an array of Twig filters, to be used in Twig templates via:
     *
     *      {{ 'bar' | fooFilter }}
     *
     * @return array
     */
    public function getFilters()
    {

		$smith = Wordsmith::$plugin->smith;
		$prefix = Wordsmith::$plugin->getSettings()->twigPrefix;

		$filters = [];

		foreach ($smith->getMethodList() as $method => $meta)
		{
			$filters[] = new Twig_Filter($prefix.$method, [$smith, $method], $meta);
		}

		return $filters;

    }

    /**
     * Returns an array of Twig functions, used in Twig templates via:
     *
     *      {% set fizz = fooFunction('buzz') %}
     *
     * @return array
     */
    public function getFunctions()
    {

		$smith = Wordsmith::$plugin->smith;
		$prefix = Wordsmith::$plugin->getSettings()->twigPrefix;

		$functions = [];

		foreach ($smith->getMethodList() as $method => $meta)
		{
			$functions[] = new Twig_Function($prefix.$method, [$smith, $method], $meta);
		}

        return $functions;

    }

    /**
     * @inheritdoc
     */
    public function getGlobals()
    {
        return ['wordsmith' => Wordsmith::$plugin->smith];
    }

}