<?php

namespace Zumba\GastonJS\Browser;

/**
 * Trait BrowserPageElementTrait
 * @package Zumba\GastonJS\Browser
 */
trait BrowserPageElementTrait {
  /**
   * Find elements given a method and a selector
   * @param $method
   * @param $selector
   * @return array
   */
  public function find($method, $selector) {
    $result = $this->command('find', $method, $selector);
    $found["page_id"] = $result["page_id"];
    foreach ($result["ids"] as $id) {
      $found["ids"][] = $id;
    }
    return $found;
  }

  /**
   * Find elements within a page, method and selector
   * @param $pageId
   * @param $elementId
   * @param $method
   * @param $selector
   * @return mixed
   */
  public function findWithin($pageId, $elementId, $method, $selector) {
    return $this->command('find_within', $pageId, $elementId, $method, $selector);
  }

  /**
   * @param $pageId
   * @param $elementId
   * @return mixed
   */
  public function getParents($pageId, $elementId) {
    return $this->command('parents', $pageId, $elementId);
  }

  /**
   * Returns the text of a given page and element
   * @param $pageId
   * @param $elementId
   * @return mixed
   */
  public function allText($pageId, $elementId) {
    return $this->command('all_text', $pageId, $elementId);
  }

  /**
   * Returns the inner or outer html of the given page and element
   * @param $pageId
   * @param $elementId
   * @param $type
   * @return mixed
   * @throws \Zumba\GastonJS\Exception\BrowserError
   * @throws \Exception
   */
  public function allHtml($pageId, $elementId, $type = "inner") {
    return $this->command('all_html', $pageId, $elementId, $type);
  }

  /**
   * Returns ONLY the visible text of a given page and element
   * @param $pageId
   * @param $elementId
   * @return mixed
   */
  public function visibleText($pageId, $elementId) {
    return $this->command('visible_text', $pageId, $elementId);
  }

  /**
   * Deletes the text of a given page and element
   * @param $pageId
   * @param $elementId
   * @return mixed
   */
  public function deleteText($pageId, $elementId) {
    return $this->command('delete_text', $pageId, $elementId);
  }

  /**
   * Gets the tag name of a given element and page
   * @param $pageId
   * @param $elementId
   * @return string
   */
  public function tagName($pageId, $elementId) {
    return strtolower($this->command('tag_name', $pageId, $elementId));
  }

  /**
   * Check if two elements are the same on a give
   * @param $pageId
   * @param $firstId
   * @param $secondId
   * @return bool
   */
  public function equals($pageId, $firstId, $secondId) {
    return $this->command('equals', $pageId, $firstId, $secondId);
  }

  /**
   * Returns the attributes of an element in a given page
   * @param $pageId
   * @param $elementId
   * @return mixed
   */
  public function attributes($pageId, $elementId) {
    return $this->command('attributes', $pageId, $elementId);
  }

  /**
   * Returns the attribute of an element by name in a given page
   * @param $pageId
   * @param $elementId
   * @param $name
   * @return mixed
   */
  public function attribute($pageId, $elementId, $name) {
    return $this->command('attribute', $pageId, $elementId, $name);
  }

  /**
   * Set an attribute to the given element in the given page
   * @param $pageId
   * @param $elementId
   * @param $name
   * @param $value
   * @return mixed
   * @throws \Zumba\GastonJS\Exception\BrowserError
   * @throws \Exception
   */
  public function setAttribute($pageId, $elementId, $name, $value) {
    return $this->command('set_attribute', $pageId, $elementId, $name, $value);
  }

  /**
   * Remove an attribute for a given page and element
   * @param $pageId
   * @param $elementId
   * @param $name
   * @return mixed
   * @throws \Zumba\GastonJS\Exception\BrowserError
   * @throws \Exception
   */
  public function removeAttribute($pageId, $elementId, $name) {
    return $this->command('remove_attribute', $pageId, $elementId, $name);
  }

  /**
   * Checks if an element is visible or not
   * @param $pageId
   * @param $elementId
   * @return boolean
   */
  public function isVisible($pageId, $elementId) {
    return $this->command("visible", $pageId, $elementId);
  }

  /**
   * Sends the order to execute a key event on a given element
   * @param $pageId
   * @param $elementId
   * @param $keyEvent
   * @param $key
   * @param $modifier
   * @return mixed
   */
  public function keyEvent($pageId, $elementId, $keyEvent, $key, $modifier) {
    return $this->command("key_event", $pageId, $elementId, $keyEvent, $key, $modifier);
  }

  /**
   * Sends the command to select and option given a value
   * @param      $pageId
   * @param      $elementId
   * @param      $value
   * @param bool $multiple
   * @return mixed
   */
  public function selectOption($pageId, $elementId, $value, $multiple = false) {
    return $this->command("select_option", $pageId, $elementId, $value, $multiple);
  }

}


<?php

namespace Zumba\GastonJS\Browser;

/**
 * Trait BrowserConfigurationTrait
 * @package Zumba\GastonJS\Browser
 */
trait BrowserConfigurationTrait {
  /**
   * Set whether to fail or not on javascript errors found on the page
   * @param bool $enabled
   * @return bool
   */
  public function jsErrors($enabled = true) {
    return $this->command('set_js_errors', $enabled);
  }

  /**
   * Set a blacklist of urls that we are not supposed to load
   * @param array $blackList
   * @return bool
   */
  public function urlBlacklist($blackList) {
    return $this->command('set_url_blacklist', $blackList);
  }

  /**
   * Set the debug mode on the browser
   * @param bool $enable
   * @return bool
   */
  public function debug($enable = false) {
    $this->debug = $enable;
    return $this->command('set_debug', $this->debug);
  }

  /**
   * Set the timeout after which any resource requested will stop
   * trying and proceed with other parts of the page
   * @param int $resourceTimeout
   * @return bool
   */
  public function resourceTimeout($resourceTimeout) {
    return $this->command('set_resource_timeout', $resourceTimeout);
  }

  /**
   * Sets or unsets web proxy.
   *
   * @param string|false $proxy proxy url formatted as '(http|socks5)://[username:password@]host:port', or false to unset
   * @return bool
   * @throws \UnexpectedValueException when the proxy url is invalid
   */
  public function setProxy($proxy)
  {
    $args = array('set_proxy');
    if ($proxy !== false)
    {
      if (preg_match('~^(http|socks5)://(?:([^:@/]*?):([^:@/]*?)@)?([^:@/]+):(\d+)$~', $proxy, $components))
      {
        array_push($args, $components[4], intval($components[5], 10), $components[1]);
        if (strlen($components[2]) || strlen($components[3]))
        {
          array_push($args, urldecode($components[2]), urldecode($components[3]));
        }
      }
      else
      {
        throw new \UnexpectedValueException('Invalid proxy url ' . $proxy);
      }
    }
    return call_user_func_array(array($this, 'command'), $args);
  }
}


<?php

namespace Zumba\GastonJS\Browser;

use Zumba\GastonJS\NetworkTraffic\Request;

/**
 * Trait BrowserNetworkTrait
 * @package Zumba\GastonJS\Browser
 */
trait BrowserNetworkTrait {
  /**
   * Get all the network traffic that the page have created
   * @return array
   */
  public function networkTraffic() {
    $networkTraffic = $this->command('network_traffic');
    $requestTraffic = array();

    if (count($networkTraffic) === 0) {
      return null;
    }

    foreach ($networkTraffic as $traffic) {
      $requestTraffic[] = new Request($traffic["request"], $traffic["responseParts"]);
    }

    return $requestTraffic;
  }

  /**
   * Clear the network traffic data stored on the phantomjs code
   * @return mixed
   */
  public function clearNetworkTraffic() {
    return $this->command('clear_network_traffic');
  }

}


<?php

namespace Zumba\GastonJS\Browser;

use Zumba\GastonJS\Cookie;

/**
 * Trait BrowserCookieTrait
 * @package Zumba\GastonJS\Browser
 */
trait BrowserCookieTrait {
  /**
   * Gets the cookies on the browser
   *
   * @return Cookie[]
   */
  public function cookies() {
    $cookies = $this->command('cookies');
    $objCookies = array();
    foreach ($cookies as $cookie) {
      $objCookies[$cookie["name"]] = new Cookie($cookie);
    }
    return $objCookies;
  }

  /**
   * Sets a cookie on the browser, expires times is set in seconds
   * @param $cookie
   * @return mixed
   */
  public function setCookie($cookie) {
    //TODO: add error control when the cookie array is not valid
    if (isset($cookie["expires"])) {
      $cookie["expires"] = intval($cookie["expires"]) * 1000;
    }
    $cookie['value'] = urlencode($cookie['value']);
    return $this->command('set_cookie', $cookie);
  }

  /**
   * Deletes a cookie on the browser if exists
   * @param $cookieName
   * @return bool
   */
  public function removeCookie($cookieName) {
    return $this->command('remove_cookie', $cookieName);
  }

  /**
   * Clear all the cookies
   * @return bool
   */
  public function clearCookies() {
    return $this->command('clear_cookies');
  }

  /**
   * Enables or disables the cookies con phantomjs
   * @param bool $enabled
   * @return bool
   */
  public function cookiesEnabled($enabled = true) {
    return $this->command('cookies_enabled', $enabled);
  }
}


<?php

namespace Zumba\GastonJS\Exception;

/**
 * Class JavascriptError
 * @package Zumba\GastonJS\Exception
 */
class JavascriptError extends ClientError {

  /**
   * @param array $response
   */
  public function __construct($response) {
    parent::__construct($response);
    $this->message = $this->message();
  }

  /**
   * Get the javascript errors found during the use of the phantomjs
   * @return array
   */
  public function javascriptErrors() {
    $jsErrors = array();
    $errors = $this->response["error"]["args"][0];
    foreach ($errors as $error) {
      $jsErrors[] = new JSErrorItem($error["message"], $error["stack"]);
    }
    return $jsErrors;
  }

  /**
   * Returns the javascript errors found
   * @return string
   */
  public function message() {
    $error = "One or more errors were raised in the Javascript code on the page.
            If you don't care about these errors, you can ignore them by
            setting js_errors: false in your Poltergeist configuration (see documentation for details).";
    //TODO: add javascript errors
    $jsErrors = $this->javascriptErrors();
    foreach($jsErrors as $jsError){
      $error = "$error\n$jsError";
    }
    return $error;
  }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Trees\Compilers;

use Aphiria\Routing\Matchers\Rules\IRuleFactory;
use Aphiria\Routing\Matchers\Rules\RuleFactory;
use Aphiria\Routing\Matchers\Rules\RuleFactoryRegistrant;
use Aphiria\Routing\Matchers\Trees\LiteralTrieNode;
use Aphiria\Routing\Matchers\Trees\RootTrieNode;
use Aphiria\Routing\Matchers\Trees\RouteVariable;
use Aphiria\Routing\Matchers\Trees\TrieNode;
use Aphiria\Routing\Matchers\Trees\VariableTrieNode;
use Aphiria\Routing\Route;
use Aphiria\Routing\UriTemplates\Parsers\AstNode;
use Aphiria\Routing\UriTemplates\Parsers\AstNodeTypes;
use Aphiria\Routing\UriTemplates\Parsers\IUriTemplateParser;
use Aphiria\Routing\UriTemplates\Parsers\Lexers\IUriTemplateLexer;
use Aphiria\Routing\UriTemplates\Parsers\Lexers\UriTemplateLexer;
use Aphiria\Routing\UriTemplates\Parsers\UriTemplateParser;
use InvalidArgumentException;

/**
 * Defines a compiler for a trie
 */
final class TrieCompiler implements ITrieCompiler
{
    /** @var IRuleFactory The factory that will create rules */
    private $ruleFactory;
    /** @var IUriTemplateParser The URI template parser */
    private $uriTemplateParser;
    /** @var IUriTemplateLexer The URI template lexer */
    private $uriTemplateLexer;

    /**
     * @param IRuleFactory|null $ruleFactory The factory that will create rules
     * @param IUriTemplateParser|null $uriTemplateParser The URI template parser
     * @param IUriTemplateLexer|null $uriTemplateLexer The URI template lexer
     */
    public function __construct(
        IRuleFactory $ruleFactory = null,
        IUriTemplateParser $uriTemplateParser = null,
        IUriTemplateLexer $uriTemplateLexer = null
    ) {
        if ($ruleFactory === null) {
            $this->ruleFactory = new RuleFactory();
            (new RuleFactoryRegistrant)->registerRuleFactories($this->ruleFactory);
        } else {
            $this->ruleFactory = $ruleFactory;
        }

        $this->uriTemplateParser = $uriTemplateParser ?? new UriTemplateParser();
        $this->uriTemplateLexer = $uriTemplateLexer ?? new UriTemplateLexer();
    }

    /**
     * @inheritdoc
     */
    public function compile(Route $route): TrieNode
    {
        $ast = $this->uriTemplateParser->parse($this->uriTemplateLexer->lex((string)$route->uriTemplate));
        $trie = new RootTrieNode();
        $hostTrie = null;

        foreach ($ast->children as $childAstNode) {
            switch ($childAstNode->type) {
                case AstNodeTypes::HOST:
                    $hostTrie = new RootTrieNode();
                    $this->compileNode(true, $hostTrie, $childAstNode, $route, null);
                    break;
                case AstNodeTypes::PATH:
                    $this->compileNode(false, $trie, $childAstNode, $route, $hostTrie);
                    break;
            }
        }

        return $trie;
    }

    /**
     * Creates a new node
     *
     * @param bool $isCompilingHostTrie Whether or not we're compiling a host trie
     * @param TrieNode $currTrieNode The current node
     * @param AstNode $ast The current AST node
     * @param Route $route The current route
     * @param TrieNode|null $hostTrie The host trie if one should be included, otherwise null
     */
    private function compileNode(
        bool $isCompilingHostTrie,
        TrieNode $currTrieNode,
        AstNode $ast,
        Route $route,
        ?TrieNode $hostTrie
    ): void {
        $astChildren = $ast->children;
        $numAstChildren = \count($astChildren);
        $isEndpoint = false;
        $segmentContainsVariable = false;
        $segmentBuffer = [];

        foreach ($isCompilingHostTrie ? \array_reverse($astChildren) : $astChildren as $i => $childAstNode) {
            /**
             * This isn't an endpoint if we're compiling a path trie which has a host trie
             * This is an endpoint if it's the last node or the last non-optional node
             */
            $isEndpoint = $isEndpoint
                || (
                    ($isCompilingHostTrie || $hostTrie === null)
                    && (
                        $i === $numAstChildren - 1
                        || $childAstNode->type === AstNodeTypes::OPTIONAL_ROUTE_PART
                        || ($astChildren[$i + 1]->type === AstNodeTypes::OPTIONAL_ROUTE_PART)
                    )
                );

            switch ($childAstNode->type) {
                case AstNodeTypes::SEGMENT_DELIMITER:
                    // Checking if this is an endpoint handles the case of a route at the root path
                    if ($isEndpoint || \count($segmentBuffer) > 0) {
                        $newTrieNode = self::createTrieNode(
                            $segmentBuffer,
                            $segmentContainsVariable,
                            $isEndpoint,
                            $route,
                            $hostTrie
                        );
                        $currTrieNode->addChild($newTrieNode);
                        $currTrieNode = $newTrieNode;
                    }

                    break;
                case AstNodeTypes::OPTIONAL_ROUTE_PART:
                    // Handles flushing 'foo' in the case of 'foo[/bar]'
                    if (\count($segmentBuffer) > 0) {
                        $newTrieNode = self::createTrieNode(
                            $segmentBuffer,
                            $segmentContainsVariable,
                            $isEndpoint,
                            $route,
                            $hostTrie
                        );
                        $currTrieNode->addChild($newTrieNode);
                        $currTrieNode = $newTrieNode;
                    }

                    $this->compileNode($isCompilingHostTrie, $currTrieNode, $childAstNode, $route, $hostTrie);
                    break;
                case AstNodeTypes::TEXT:
                    $segmentBuffer[] = $childAstNode->value;
                    break;
                case AstNodeTypes::VARIABLE:
                    $segmentContainsVariable = true;
                    $segmentBuffer[] = $this->compileVariableNode($childAstNode);
                    break;
                default:
                    throw new InvalidArgumentException("Unexpected node type {$childAstNode->type}");
            }
        }

        // Check if we need to flush the buffer
        if (\count($segmentBuffer) > 0) {
            $currTrieNode->addChild(
                self::createTrieNode($segmentBuffer, $segmentContainsVariable, $isEndpoint, $route, $hostTrie)
            );
        }
    }

    /**
     * Compiles a variable node
     *
     * @param AstNode $astNode The variable node to compile
     * @return RouteVariable The created route variable
     */
    private function compileVariableNode(AstNode $astNode): RouteVariable
    {
        $rules = [];

        foreach ($astNode->children as $childAstNode) {
            if ($childAstNode->type !== AstNodeTypes::VARIABLE_RULE) {
                throw new InvalidArgumentException("Unexpected node type {$childAstNode->type}");
            }

            $ruleParams = $childAstNode->hasChildren() ? $childAstNode->children[0]->value : [];
            $rules[] = $this->ruleFactory->createRule($childAstNode->value, $ruleParams);
        }

        return new RouteVariable($astNode->value, $rules);
    }

    /**
     * Creates a trie node
     *
     * @param string[]|RouteVariable[] $segmentBuffer The current buffer of parts (eg text or RouteVariables)
     * @param bool $segmentContainsVariable Whether or not the segment contains a variable
     * @param bool $isEndpoint Whether or not this node is an endpoint
     * @param Route $route The current route
     * @param TrieNode|null $hostTrie The host trie
     * @return TrieNode The created node
     */
    private static function createTrieNode(
        array &$segmentBuffer,
        bool &$segmentContainsVariable,
        bool $isEndpoint,
        Route $route,
        ?TrieNode $hostTrie
    ): TrieNode {
        $routes = $isEndpoint ? $route : [];

        if ($segmentContainsVariable) {
            $node = new VariableTrieNode($segmentBuffer, [], $routes, $hostTrie);
        } else {
            $node = new LiteralTrieNode(\implode('', $segmentBuffer), [], $routes, $hostTrie);
        }

        // Clear the buffer data
        $segmentBuffer = [];
        $segmentContainsVariable = false;

        return $node;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\UriTemplates\Parsers\Lexers;

use InvalidArgumentException;

/**
 * Defines the lexer for URI templates
 */
final class UriTemplateLexer implements IUriTemplateLexer
{
    /** @const The list of punctuation characters */
    private const PUNCTUATION = '.()[],/';
    /** @const The regex for finding a number */
    private const NUMBER_REGEX = '/\d+(?:\.\d+)?/A';
    /** @const The regex for finding a quoted string */
    private const QUOTED_STRING_REGEX = '/\s*"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'\s*/A';
    /** @const The regex for finding a variable name and default value */
    private const VARIABLE_NAME_REGEX = '/:[a-zA-Z_][\w]*/A';
    /** @const The maximum length of a variable name */
    private const VARIABLE_NAME_MAX_LENGTH = 32;

    /**
     * @inheritdoc
     */
    public function lex(string $uriTemplate): TokenStream
    {
        $cursor = 0;
        $templateLength = \mb_strlen($uriTemplate);
        $tokens = [];
        $textBuffer = '';

        while ($cursor < $templateLength) {
            $matches = [];

            if ($uriTemplate[$cursor] === ' ') {
                $cursor++;
            } elseif (\strpos(self::PUNCTUATION, $uriTemplate[$cursor]) !== false) {
                self::flushTextBuffer($textBuffer, $tokens);
                self::lexPunctuation($uriTemplate[$cursor], $tokens, $cursor);
            } elseif (\preg_match(self::VARIABLE_NAME_REGEX, $uriTemplate, $matches, 0, $cursor) === 1) {
                self::flushTextBuffer($textBuffer, $tokens);
                self::lexVariableName($matches[0], $tokens, $cursor);
            } elseif (\preg_match(self::NUMBER_REGEX, $uriTemplate, $matches, 0, $cursor) === 1) {
                self::flushTextBuffer($textBuffer, $tokens);
                self::lexNumber($matches[0], $tokens, $cursor);
            } elseif (\preg_match(self::QUOTED_STRING_REGEX, $uriTemplate, $matches, 0, $cursor) === 1) {
                self::flushTextBuffer($textBuffer, $tokens);
                self::lexQuotedString($matches[0], $tokens, $cursor);
            } else {
                self::lexTextChar($uriTemplate[$cursor], $textBuffer, $cursor);
            }
        }

        // In case there's anything left in the buffer, flush it
        self::flushTextBuffer($textBuffer, $tokens);

        return new TokenStream($tokens);
    }

    /**
     * Flushes any text from the buffer
     *
     * @param string $textBuffer The current text buffer
     * @param Token[] $tokens The list of tokens to add to
     */
    private static function flushTextBuffer(string &$textBuffer, array &$tokens): void
    {
        if ($textBuffer !== '') {
            $tokens[] = new Token(TokenTypes::T_TEXT, $textBuffer);
            $textBuffer = '';
        }
    }

    /**
     * Lexes a number lexeme
     *
     * @param string $number The lexeme to add
     * @param Token[] $tokens The list of tokens to add to
     * @param int $cursor The current cursor
     */
    private static function lexNumber(string $number, array &$tokens, int &$cursor): void
    {
        $floatVal = (float)$number;
        $intVal = (int)$number;

        // Determine if this was a float or not
        if ($floatVal && $intVal != $floatVal) {
            $tokens[] = new Token(TokenTypes::T_NUMBER, $floatVal);
        } else {
            $tokens[] = new Token(TokenTypes::T_NUMBER, $intVal);
        }

        $cursor += \mb_strlen($number);
    }

    /**
     * Lexes a punctuation lexeme
     *
     * @param string $punctuation The lexeme to add
     * @param Token[] $tokens The list of tokens to add to
     * @param int $cursor The current cursor
     */
    private static function lexPunctuation(string $punctuation, array &$tokens, int &$cursor): void
    {
        $tokens[] = new Token(TokenTypes::T_PUNCTUATION, $punctuation);
        $cursor++;
    }

    /**
     * Lexes a quoted string lexeme
     *
     * @param string $quotedString The lexeme to add
     * @param Token[] $tokens The list of tokens to add to
     * @param int $cursor The current cursor
     */
    private static function lexQuotedString(string $quotedString, array &$tokens, int &$cursor): void
    {
        $tokens[] = new Token(TokenTypes::T_QUOTED_STRING, \stripcslashes(\substr(\trim($quotedString), 1, -1)));
        $cursor += \mb_strlen($quotedString);
    }

    /**
     * Lexes a text character lexeme
     *
     * @param string $char The lexeme to add
     * @param string $textBuffer The text buffer to add to
     * @param int $cursor The current cursor
     */
    private static function lexTextChar(string $char, string &$textBuffer, int &$cursor): void
    {
        $textBuffer .= $char;
        $cursor++;
    }

    /**
     * Lexes a variable name lexeme
     *
     * @param string $variableName The lexeme to add
     * @param Token[] $tokens The list of tokens to add to
     * @param int $cursor The current cursor
     */
    private static function lexVariableName(string $variableName, array &$tokens, int &$cursor): void
    {
        // Remove the colon before the variable name
        $trimmedVariableName = \substr($variableName, 1);

        if (\mb_strlen($trimmedVariableName) > self::VARIABLE_NAME_MAX_LENGTH) {
            throw new InvalidArgumentException("Variable name \"$trimmedVariableName\" exceeds the max length limit");
        }

        $tokens[] = new Token(TokenTypes::T_VARIABLE, $trimmedVariableName);
        // We have to advance the cursor the length of the untrimmed variable name
        $cursor += \mb_strlen($variableName);
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing;

/**
 * Defines a list of routes that can be used by a router
 */
final class RouteCollection
{
    /** @var array The list of methods to their various routes */
    private $routes = [];
    /** @var Route[] The mapping of route names to routes */
    private $namedRoutes = [];

    /**
     * @param Route[] $routes The initial list of routes
     */
    public function __construct(array $routes = [])
    {
        $this->addMany($routes);
    }

    /**
     * Performs a deep clone of the routes
     */
    public function __clone()
    {
        foreach ($this->routes as $index => $route) {
            $this->routes[$index] = clone $route;
        }

        foreach ($this->namedRoutes as $name => $route) {
            $this->namedRoutes[$name] = clone $route;
        }
    }

    /**
     * Adds a route to the collection
     *
     * @param Route $route The route to add
     */
    public function add(Route $route): void
    {
        $this->routes[] = $route;

        if ($route->name !== null) {
            $this->namedRoutes[$route->name] =& $route;
        }
    }

    /**
     * Adds a list of routes to the collection
     *
     * @param Route[] $routes The routes to add
     */
    public function addMany(array $routes): void
    {
        foreach ($routes as $route) {
            $this->add($route);
        }
    }

    /**
     * Gets all the routes
     *
     * @return Route[][] The list of routes
     */
    public function getAll(): array
    {
        return $this->routes;
    }

    /**
     * Gets the route with the input name
     *
     * @param string $name The name to search for
     * @return Route|null The route with the input name if one existed, otherwise null
     */
    public function getNamedRoute(string $name): ?Route
    {
        if (!isset($this->namedRoutes[$name])) {
            return null;
        }

        return $this->namedRoutes[$name];
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Trees;

use Aphiria\Routing\IRouteFactory;
use Aphiria\Routing\Matchers\Trees\Caching\ITrieCache;
use Aphiria\Routing\Matchers\Trees\Compilers\ITrieCompiler;
use Aphiria\Routing\Matchers\Trees\Compilers\TrieCompiler;

/**
 * Defines a factory for tries
 */
final class TrieFactory
{
    /**
     * The factory that will create the routes
     * The benefit of using a factory instead of the already-instantiated routes is that we can defer the potentially
     * slow/costly overhead of creating those routes when there's a cache hit
     *
     * @var IRouteFactory
     */
    private $routeFactory;
    /** @var ITrieCache|null The cache for tries, or null if not using a cache */
    private $trieCache;
    /** @var ITrieCompiler The trie compiler */
    private $trieCompiler;

    /**
     * @param IRouteFactory $routeFactory The factory that will create the routes when not found in cache
     * @param ITrieCache|null $trieCache The cache for tries, or null if not using a cache
     * @param ITrieCompiler|null $trieCompiler The trie compiler
     */
    public function __construct(
        IRouteFactory $routeFactory,
        ITrieCache $trieCache = null,
        ITrieCompiler $trieCompiler = null
    ) {
        $this->routeFactory = $routeFactory;
        $this->trieCache = $trieCache;
        $this->trieCompiler = $trieCompiler ?? new TrieCompiler();
    }

    /**
     * Creates a trie
     *
     * @return TrieNode The trie
     */
    public function createTrie(): TrieNode
    {
        if ($this->trieCache !== null && ($trie = $this->trieCache->get()) !== null) {
            return $trie;
        }

        // Need to generate the trie
        $trie = new RootTrieNode();

        foreach ($this->routeFactory->createRoutes()->getAll() as $route) {
            foreach ($this->trieCompiler->compile($route)->getAllChildren() as $childNode) {
                $trie->addChild($childNode);
            }
        }

        // Save this to cache for next time
        if ($this->trieCache !== null) {
            $this->trieCache->set($trie);
        }

        return $trie;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Requests;

/**
 * Defines a parser that reads the request headers from the $_SERVER super global
 */
final class RequestHeaderParser
{
    /** @var array These headers do not have the HTTP_ prefix */
    private static $specialCaseHeaders = [
        'AUTH_TYPE' => true,
        'CONTENT_LENGTH' => true,
        'CONTENT_TYPE' => true,
        'PHP_AUTH_DIGEST' => true,
        'PHP_AUTH_PW' => true,
        'PHP_AUTH_TYPE' => true,
        'PHP_AUTH_USER' => true
    ];

    /**
     * Parses headers from the $_SERVER super global
     *
     * @param array $server The $_SERVER super global
     * @return array The mapping of header names => values
     */
    public function parseHeaders(array $server): array
    {
        $headers = [];

        foreach ($server as $key => $value) {
            $uppercasedKey = \strtoupper($key);

            if (isset(self::$specialCaseHeaders[$uppercasedKey]) || \strpos($uppercasedKey, 'HTTP_') === 0) {
                $value = (array)$value;
                $headers[self::normalizeName($key)] = $value;
            }
        }

        return $headers;
    }

    /**
     * Normalizes a name
     *
     * @param string $name The name to normalize
     * @return string The normalized name
     */
    private static function normalizeName($name): string
    {
        $dashedName = \str_replace('_', '-', $name);

        if (\stripos($dashedName, 'HTTP-') === 0) {
            $dashedName = \substr($dashedName, 5);
        }

        return $dashedName;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\UriTemplates\Parsers\Lexers;

use InvalidArgumentException;

/**
 * Defines a token stream
 */
final class TokenStream
{
    /** @var int The length of the stream */
    public $length;
    /** @var Token[] The list of tokens */
    private $tokens;
    /** @var int The current cursor */
    private $cursor = 0;

    /**
     * @param Token[] $tokens The list of tokens
     */
    public function __construct(array $tokens)
    {
        $this->tokens = $tokens;
        $this->length = \count($tokens);
    }

    /**
     * Tests the current token to see if it matches the input type, and optionally the input value, and throws an
     * exception if the token did not match
     *
     * @param string $type The type to check for
     * @param mixed $value The optional value to match against
     * @param string|null $message The exception message to use, otherwise a default one is generated
     *      Any '%s' in the message is first populated with the expected type, and then with the expected value
     * @throws InvalidArgumentException Thrown if the current token didn't match the expected type and value
     */
    public function expect(string $type, $value = null, string $message = null): void
    {
        if ($this->test($type, $value)) {
            return;
        }

        $currentToken = $this->getCurrent();

        if ($message === null) {
            // Let's create a default message
            $formattedMessage = \sprintf(
                'Expected token type %s%s',
                $type,
                $value === null ? '' : " with value \"$value\""
            );

            if ($currentToken === null) {
                $formattedMessage .= ', got end of stream';
            } else {
                $formattedMessage .= \sprintf(
                    ', got %s with value \"%s\"',
                    $currentToken->type,
                    $currentToken->value
                );
            }
        } else {
            $formattedMessage = \sprintf(
                $message,
                $currentToken === null ? 'T_EOF' : $currentToken->type,
                $currentToken === null ? 'end of stream' : $currentToken->value
            );
        }

        throw new InvalidArgumentException($formattedMessage);
    }

    /**
     * Gets the current token
     *
     * @return Token|null The current token
     */
    public function getCurrent(): ?Token
    {
        return \count($this->tokens) > $this->cursor ? $this->tokens[$this->cursor] : null;
    }

    /**
     * Gets the next token, if there is one
     *
     * @return Token|null The next token, if there is one, otherwise false
     */
    public function next(): ?Token
    {
        return \count($this->tokens) > ++$this->cursor ? $this->tokens[$this->cursor] : null;
    }

    /**
     * Gets the next token if the current one matches the input type, and optionally performs a value check
     *
     * @param string $type The type to check for
     * @param mixed $value The optional value to match against
     * @return bool True if the current token is of the input type, otherwise false
     */
    public function nextIfType(string $type, $value = null): bool
    {
        $currentToken = $this->getCurrent();
        $typeMatches = $currentToken !== null && $currentToken->type === $type;

        if ($typeMatches && ($value === null || $currentToken->value === $value)) {
            $this->next();

            return true;
        }

        return false;
    }

    /**
     * Peeks ahead at the next token
     *
     * @param int $lookahead The number of tokens to look ahead
     * @return Token|null The token
     */
    public function peek(int $lookahead = 1): ?Token
    {
        if ($this->cursor + $lookahead >= \count($this->tokens)) {
            return null;
        }

        return $this->tokens[$this->cursor + $lookahead];
    }

    /**
     * Tests the current token to see if it matches the input type, and optionally the input value
     *
     * @param string $type The type to check for
     * @param mixed $value The optional value to match against
     * @return bool True if the current token is of the input type, otherwise false
     */
    public function test(string $type, $value = null): bool
    {
        $currentToken = $this->getCurrent();
        $typeMatches = $currentToken !== null && $currentToken->type === $type;

        return $typeMatches && ($value === null || $currentToken->value === $value);
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\UriTemplates\Parsers;

/**
 * Defines an abstract syntax tree node
 */
final class AstNode
{
    /** @var string The node type */
    public $type;
    /** @var mixed|null The value of the node */
    public $value;
    /** @var AstNode|null The parent node */
    public $parent;
    /** @var AstNode[] The child nodes */
    public $children = [];

    /**
     * @param string $type The node type
     * @param mixed $value The value of the node
     */
    public function __construct(string $type, $value = null)
    {
        $this->type = $type;
        $this->value = $value;
    }

    /**
     * Adds a child to this node
     *
     * @param AstNode $node The child to add
     * @return AstNode Returns this for chaining
     */
    public function addChild(AstNode $node): AstNode
    {
        $node->parent = $this;
        $this->children[] = $node;

        return $this;
    }

    /**
     * Gets whether or not the node has children
     *
     * @return bool True if the node has children, otherwise false
     */
    public function hasChildren(): bool
    {
        return \count($this->children) > 0;
    }

    /**
     * Gets whether or not this node is the root
     *
     * @return bool True if this is a root node, otherwise false
     */
    public function isRoot(): bool
    {
        return $this->parent === null;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Trees;

use Aphiria\Routing\Route;
use InvalidArgumentException;

/**
 * Defines the base class for trie nodes to extend
 */
abstract class TrieNode
{
    /** @var TrieNode|null The host trie, if there is one */
    public $hostTrie;
    /** @var Route[] The list of routes for this node, if there are any */
    public $routes;
    /** @var VariableTrieNode[] The child variable nodes */
    public $variableChildren = [];
    /** @var LiteralTrieNode[] The mapping of literal child node values to child nodes */
    public $literalChildrenByValue = [];

    /**
     * @param TrieNode[] $children The list of children
     * @param Route[]|Route $routes The list of routes for this segment if there are any
     * @param TrieNode|null $hostTrie The host trie, if there is one
     * @throws InvalidArgumentException Thrown if the routes are not the expected type
     */
    protected function __construct(array $children, $routes, ?TrieNode $hostTrie)
    {
        if (\is_array($routes)) {
            $this->routes = $routes;
        } elseif ($routes instanceof Route) {
            $this->routes = [$routes];
        } else {
            throw new InvalidArgumentException('Routes must be a route or an array of routes');
        }

        foreach ($children as $child) {
            $this->addChild($child);
        }

        $this->hostTrie = $hostTrie;
    }

    /**
     * Adds a child node and recursively merges all its children, too
     *
     * @param $childNode $node The node to add
     * @return self For chaining
     * @throws InvalidArgumentException Thrown if the node was an invalid type
     */
    public function addChild(TrieNode $childNode): self
    {
        if ($childNode instanceof LiteralTrieNode) {
            $this->addLiteralChildNode($childNode);
        } elseif ($childNode instanceof VariableTrieNode) {
            $this->addVariableChildNode($childNode);
        } else {
            throw new InvalidArgumentException('Unexpected trie node type ' . \get_class($childNode));
        }

        return $this;
    }

    /**
     * Gets all the child nodes
     *
     * @return TrieNode[] The child nodes
     */
    public function getAllChildren(): array
    {
        $children = [];

        foreach ($this->literalChildrenByValue as $childNode) {
            $children[] = $childNode;
        }

        foreach ($this->variableChildren as $childNode) {
            $children[] = $childNode;
        }

        return $children;
    }

    /**
     * Adds a literal child node
     *
     * @param LiteralTrieNode $childNode The child node to add
     */
    private function addLiteralChildNode(LiteralTrieNode $childNode): void
    {
        // Stringify the value in case it's a number and we don't want PHP getting confused
        $valueAsString = \strtolower((string)$childNode->value);

        if (isset($this->literalChildrenByValue[$valueAsString])) {
            // A literal child already exists with this value, so merge the routes and add all its children
            $matchingChildNode = $this->literalChildrenByValue[$valueAsString];
            $matchingChildNode->routes = \array_merge(
                $matchingChildNode->routes,
                $childNode->routes
            );

            foreach ($childNode->getAllChildren() as $grandChildNode) {
                $matchingChildNode->addChild($grandChildNode);
            }
        } else {
            $this->literalChildrenByValue[$valueAsString] = $childNode;
        }
    }

    /**
     * Adds a variable child node
     *
     * @param VariableTrieNode $childNode The child node to add
     */
    private function addVariableChildNode(VariableTrieNode $childNode): void
    {
        // Try to find a variable child whose parts match the input child's parts
        // If we find one, then we merge its routes and add all its children
        $matchingChildNode = null;

        foreach ($this->variableChildren as $variableChildNode) {
            // Purposely doing a loose check here because we don't care about reference equality
            if ($variableChildNode->parts == $childNode->parts) {
                $matchingChildNode = $variableChildNode;
                $variableChildNode->routes = \array_merge($variableChildNode->routes, $childNode->routes);
                break;
            }
        }

        if ($matchingChildNode === null) {
            $this->variableChildren[] = $childNode;
        } else {
            foreach ($childNode->getAllChildren() as $grandChildNode) {
                $matchingChildNode->addChild($grandChildNode);
            }
        }
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Builders;

use Aphiria\Routing\Route;
use Aphiria\Routing\UriTemplates\UriTemplate;
use Closure;

/**
 * Defines the route builder registry
 */
final class RouteBuilderRegistry
{
    /** @var RouteBuilder[] The list of registered route builders */
    private $routeBuilders = [];
    /** @var RouteGroupOptions[] The stack of route group options */
    private $groupOptionsStack = [];

    /**
     * Builds all the route builders in the registry
     *
     * @return Route[] The list of routes built by this registry
     */
    public function buildAll(): array
    {
        $builtRoutes = [];

        foreach ($this->routeBuilders as $routeBuilder) {
            $builtRoutes[] = $routeBuilder->build();
        }

        return $builtRoutes;
    }

    /**
     * Creates a group of routes that share similar options
     *
     * @param RouteGroupOptions $groupOptions The list of options shared by all routes in the group
     * @param Closure $callback The callback that accepts an instance of this class
     */
    public function group(RouteGroupOptions $groupOptions, Closure $callback): void
    {
        $this->groupOptionsStack[] = $groupOptions;
        $callback($this);
        \array_pop($this->groupOptionsStack);
    }

    /**
     * Creates a route builder with some values already set
     *
     * @param array|string $httpMethods The HTTP method or list of methods the route uses
     * @param string $pathTemplate The path template
     * @param string|null $hostTemplate The host template
     * @param bool $isHttpsOnly Whether or not the route is HTTPS-only
     * @return RouteBuilder The configured route builder
     */
    public function map(
        $httpMethods,
        string $pathTemplate,
        string $hostTemplate = null,
        bool $isHttpsOnly = false
    ): RouteBuilder {
        $this->applyGroupRouteTemplates($pathTemplate, $hostTemplate, $isHttpsOnly);
        $routeBuilder = new RouteBuilder(
            (array)$httpMethods,
            new UriTemplate($pathTemplate, $hostTemplate, $isHttpsOnly)
        );
        $this->applyGroupConstraints($routeBuilder);
        $this->applyGroupMiddleware($routeBuilder);
        $this->applyGroupAttributes($routeBuilder);
        $this->routeBuilders[] = $routeBuilder;

        return $routeBuilder;
    }

    /**
     * Applies a group's attributes to the input route builder
     *
     * @param RouteBuilder $routeBuilder The route builder to bind attributes to
     */
    private function applyGroupAttributes(RouteBuilder $routeBuilder): void
    {
        $groupAttributes = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupAttributes = \array_merge($groupAttributes, $groupOptions->attributes);
        }

        $routeBuilder->withManyAttributes($groupAttributes);
    }

    /**
     * Applies a group's constraints to the input route builder
     *
     * @param RouteBuilder $routeBuilder The route builder to bind constraints to
     */
    private function applyGroupConstraints(RouteBuilder $routeBuilder): void
    {
        $groupConstraintBindings = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupConstraintBindings = \array_merge($groupConstraintBindings, $groupOptions->constraints);
        }

        $routeBuilder->withManyConstraints($groupConstraintBindings);
    }

    /**
     * Applies a group's middleware to the input route builder
     *
     * @param RouteBuilder $routeBuilder The route builder to bind middleware to
     */
    private function applyGroupMiddleware(RouteBuilder $routeBuilder): void
    {
        $groupMiddlewareBindings = [];

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupMiddlewareBindings = \array_merge($groupMiddlewareBindings, $groupOptions->middlewareBindings);
        }

        $routeBuilder->withManyMiddleware($groupMiddlewareBindings);
    }

    /**
     * Applies all the group options to a route
     *
     * @param string $pathTemplate The path template to apply settings to
     * @param string|null $hostTemplate The host template to apply settings to
     * @param bool $isHttpsOnly Whether or not the group is HTTPS-only
     */
    private function applyGroupRouteTemplates(
        string &$pathTemplate,
        string &$hostTemplate = null,
        bool &$isHttpsOnly = false
    ): void {
        $groupPathTemplate = '';
        $groupHostTemplate = '';
        $groupIsHttpsOnly = false;

        foreach ($this->groupOptionsStack as $groupOptions) {
            $groupPathTemplate .= $groupOptions->pathTemplate;
            $groupHostTemplate = $groupOptions->hostTemplate . $groupHostTemplate;
            $groupIsHttpsOnly = $groupIsHttpsOnly || $groupOptions->isHttpsOnly;
        }

        $pathTemplate = $groupPathTemplate . $pathTemplate;
        $hostTemplate = ($hostTemplate ?? '') . $groupHostTemplate;
        $isHttpsOnly = $isHttpsOnly || $groupIsHttpsOnly;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Rules;

/**
 * Defines the rule factory registrant that registers all the built-in rules' factories
 */
final class RuleFactoryRegistrant
{
    /**
     * Registers the built-in rule factories
     *
     * @param IRuleFactory The rule factory to register new factories to
     * @return IRuleFactory The rule factory with all the registered factories (for chaining)
     */
    public function registerRuleFactories(IRuleFactory $ruleFactory): IRuleFactory
    {
        $ruleFactory->registerRuleFactory(AlphaRule::getSlug(), function () {
            return new AlphaRule();
        });
        $ruleFactory->registerRuleFactory(AlphanumericRule::getSlug(), function () {
            return new AlphanumericRule();
        });
        $ruleFactory->registerRuleFactory(BetweenRule::getSlug(), function ($min, $max, bool $isInclusive = true) {
            return new BetweenRule($min, $max, $isInclusive);
        });
        $ruleFactory->registerRuleFactory(DateRule::getSlug(), function ($formats) {
            return new DateRule($formats);
        });
        $ruleFactory->registerRuleFactory(InRule::getSlug(), function (array $acceptableValues) {
            return new InRule($acceptableValues);
        });
        $ruleFactory->registerRuleFactory(IntegerRule::getSlug(), function () {
            return new IntegerRule();
        });
        $ruleFactory->registerRuleFactory(NotInRule::getSlug(), function (array $unacceptableValues) {
            return new NotInRule($unacceptableValues);
        });
        $ruleFactory->registerRuleFactory(NumericRule::getSlug(), function () {
            return new NumericRule();
        });
        $ruleFactory->registerRuleFactory(RegexRule::getSlug(), function (string $regex) {
            return new RegexRule($regex);
        });
        $ruleFactory->registerRuleFactory(UuidV4Rule::getSlug(), function () {
            return new UuidV4Rule();
        });

        return $ruleFactory;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Trees;

use Aphiria\Routing\Matchers\Constraints\HttpMethodRouteConstraint;
use Aphiria\Routing\Matchers\IRouteMatcher;
use Aphiria\Routing\Matchers\MatchedRouteCandidate;
use Aphiria\Routing\Matchers\RouteMatchingResult;

/**
 * Defines the route matcher that uses a trie structure for matching
 */
final class TrieRouteMatcher implements IRouteMatcher
{
    /** @var TrieNode The root node */
    private $rootNode;

    /**
     * @param TrieNode $rootNode The root node
     */
    public function __construct(TrieNode $rootNode)
    {
        $this->rootNode = $rootNode;
    }

    /**
     * @inheritdoc
     */
    public function matchRoute(string $httpMethod, string $host, string $path, array $headers = []): RouteMatchingResult
    {
        $hostSegments = $host === [] ? '' : \array_reverse(\explode('.', $host));
        $pathSegments = \explode('/', \trim($path, '/'));
        $routeVars = [];
        $allowedMethods = [];

        foreach (self::getMatchCandidates($this->rootNode, $pathSegments, 0, $hostSegments, $routeVars) as $candidate) {
            foreach ($candidate->route->constraints as $constraint) {
                // If any constraints fail, collect the allowed methods and go on to the next candidate
                if (!$constraint->passes($candidate, $httpMethod, $host, $path, $headers)) {
                    if ($constraint instanceof HttpMethodRouteConstraint) {
                        $allowedMethods = \array_merge($allowedMethods, $constraint->getAllowedMethods());
                    }

                    continue 2;
                }
            }

            return new RouteMatchingResult($candidate->route, $candidate->routeVariables, []);
        }

        return new RouteMatchingResult(null, [], \array_unique($allowedMethods));
    }

    /**
     * Gets the list of matching route candidates for a particular node
     *
     * This method uses generators that, given the order of the code, will return literal segments before variables
     * ones. Callers of this method will only attempt to descend the trie either on the first time or if previous
     * match candidates didn't meet constraints, hence saving us unnecessary descents down the trie.
     *
     * @param TrieNode $node The current node
     * @param array $segments The list of URI segments to match
     * @param int $segmentIter The current index of segments
     * @param array $hostSegments The list of URI host segments, which will be traversed if there's a host trie
     * @param array $routeVars The mapping of route variable names to values
     * @return iterable|MatchedRouteCandidate[] The list of matched route candidates
     */
    private static function getMatchCandidates(
        TrieNode $node,
        array $segments,
        int $segmentIter,
        array $hostSegments,
        array &$routeVars
    ): iterable {
        // Base case.  We iterate to 1 past the past segments there are n + 1 levels of nodes due to the root node.
        if ($segmentIter === \count($segments)) {
            if ($node->hostTrie === null) {
                foreach ($node->routes as $route) {
                    yield new MatchedRouteCandidate($route, $routeVars);
                }
            } else {
                // We have to traverse the host trie now
                $routeVarsCopy = $routeVars;
                yield from self::getMatchCandidates($node->hostTrie, $hostSegments, 0, $hostSegments, $routeVarsCopy);
            }

            return;
        }

        $segment = $segments[$segmentIter];

        // Check for a literal segment match, and recursively check its descendants
        if (($childNode = ($node->literalChildrenByValue[\strtolower($segment)] ?? null)) !== null) {
            $routeVarsCopy = $routeVars;
            yield from self::getMatchCandidates($childNode, $segments, $segmentIter + 1, $hostSegments, $routeVarsCopy);
        }

        // If a variable child is a match, check its descendants
        foreach ($node->variableChildren as $childNode) {
            $routeVarsCopy = $routeVars;

            if ($childNode->isMatch($segment, $routeVarsCopy)) {
                yield from self::getMatchCandidates(
                    $childNode,
                    $segments,
                    $segmentIter + 1,
                    $hostSegments,
                    $routeVarsCopy
                );
            }
        }
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Builders;

use Aphiria\Routing\ClosureRouteAction;
use Aphiria\Routing\Matchers\Constraints\HttpMethodRouteConstraint;
use Aphiria\Routing\Matchers\Constraints\IRouteConstraint;
use Aphiria\Routing\MethodRouteAction;
use Aphiria\Routing\Middleware\MiddlewareBinding;
use Aphiria\Routing\Route;
use Aphiria\Routing\RouteAction;
use Aphiria\Routing\UriTemplates\UriTemplate;
use Closure;
use InvalidArgumentException;
use LogicException;

/**
 * Defines the route builder
 */
class RouteBuilder
{
    /** @var RouteAction The action the route takes */
    private $action;
    /** @var UriTemplate The URI template */
    private $uriTemplate;
    /** @var array The mapping of custom route attribute names => values */
    private $attributes = [];
    /** @var MiddlewareBinding[] The list of middleware bindings on this route */
    private $middlewareBindings = [];
    /** @var string|null The name of this route */
    private $name;
    /** @var IRouteConstraint[] The list of constraints */
    private $constraints = [];

    /**
     * @param array $httpMethods The list of HTTP methods the route matches on
     * @param UriTemplate $uriTemplate The URI template the route matches on
     */
    public function __construct(array $httpMethods, UriTemplate $uriTemplate)
    {
        $this->constraints[] = new HttpMethodRouteConstraint($httpMethods);
        $this->uriTemplate = $uriTemplate;
    }

    /**
     * Builds a route object from all the settings in this builder
     *
     * @return Route The built route
     * @throws LogicException Thrown if no controller was specified
     */
    public function build(): Route
    {
        if ($this->action === null) {
            throw new LogicException('No controller specified for route');
        }

        return new Route(
            $this->uriTemplate,
            $this->action,
            $this->constraints,
            $this->middlewareBindings,
            $this->name,
            $this->attributes
        );
    }

    /**
     * Binds the controller the route uses to be a closure
     *
     * @param Closure $controller The closure the route uses
     * @return self For chaining
     */
    public function toClosure(Closure $controller): self
    {
        $this->action = new ClosureRouteAction($controller);

        return $this;
    }

    /**
     * Binds the controller the route uses to be a method
     *
     * @param string $controllerClassName The name of the class the route goes to
     * @param string $controllerMethodName The name of the method the route goes to
     * @return self For chaining
     */
    public function toMethod(string $controllerClassName, string $controllerMethodName): self
    {
        $this->action = new MethodRouteAction($controllerClassName, $controllerMethodName);

        return $this;
    }

    /**
     * Binds a custom attribute to the route
     * This is useful for custom route constraint matching
     *
     * @param string $name The name of the attribute
     * @param mixed $value The value of the attribute
     * @return self For chaining
     */
    public function withAttribute(string $name, $value): self
    {
        $this->attributes[$name] = $value;

        return $this;
    }

    /**
     * Binds a constraint to this route
     *
     * @param IRouteConstraint $constraint The constraint to add
     * @return self For chaining
     */
    public function withConstraint(IRouteConstraint $constraint): self
    {
        $this->constraints[] = $constraint;

        return $this;
    }

    /**
     * Binds many custom attributes to the route
     * This is useful for custom route constraint matching
     *
     * @param array $attributes The mapping of custom attribute names => values
     * @return self For chaining
     */
    public function withManyAttributes(array $attributes): self
    {
        $this->attributes = \array_merge($this->attributes, $attributes);

        return $this;
    }

    /**
     * Binds constraints to this route
     *
     * @param IRouteConstraint[] $constraints The constraints to add
     * @return self For chaining
     */
    public function withManyConstraints(array $constraints): self
    {
        $this->constraints = \array_merge($this->constraints, $constraints);

        return $this;
    }

    /**
     * Binds many middleware bindings to the route
     *
     * @param MiddlewareBinding[]|string[] $middlewareBindings The list of middleware bindings to add, or a single
     *      class name without properties
     * @return self For chaining
     * @throws InvalidArgumentException Thrown if the middleware bindings are not the correct type
     */
    public function withManyMiddleware(array $middlewareBindings): self
    {
        foreach ($middlewareBindings as $middlewareBinding) {
            if (\is_string($middlewareBinding)) {
                $this->middlewareBindings[] = new MiddlewareBinding($middlewareBinding);
            } elseif ($middlewareBinding instanceof MiddlewareBinding) {
                $this->middlewareBindings[] = $middlewareBinding;
            } else {
                throw new InvalidArgumentException(
                    'Middleware binding must either be a string or an instance of ' . MiddlewareBinding::class
                );
            }
        }

        return $this;
    }

    /**
     * Binds a single middleware class to the route
     *
     * @param string $middlewareClassName The name of the middleware class to bind
     * @param array $middlewareProperties Any properties this method relies on
     * @return self For chaining
     */
    public function withMiddleware(string $middlewareClassName, array $middlewareProperties = []): self
    {
        $this->middlewareBindings[] = new MiddlewareBinding($middlewareClassName, $middlewareProperties);

        return $this;
    }

    /**
     * Binds a name to the route
     *
     * @param string $name The name of the route
     * @return self For chaining
     */
    public function withName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\UriTemplates\Parsers;

use Aphiria\Routing\UriTemplates\Parsers\Lexers\TokenStream;
use Aphiria\Routing\UriTemplates\Parsers\Lexers\TokenTypes;
use InvalidArgumentException;

/**
 * Defines the URI template parser
 */
final class UriTemplateParser implements IUriTemplateParser
{
    /**
     * @inheritdoc
     */
    public function parse(TokenStream $tokens): AstNode
    {
        $ast = new AstNode(AstNodeTypes::ROOT, null);

        // Determine whether or not there's a host, and if so, parse it and the path
        $hostExists = false;
        $lookaheadBuffer = [];

        while (($token = $tokens->getCurrent()) !== null) {
            if ($token->type === TokenTypes::T_PUNCTUATION && $token->value === '/') {
                if ($hostExists) {
                    $hostNode = new AstNode(AstNodeTypes::HOST, null);
                    $this->parseTokens(new TokenStream($lookaheadBuffer), $hostNode);
                    $ast->addChild($hostNode);
                }

                $pathNode = new AstNode(AstNodeTypes::PATH, null);
                $this->parseTokens($tokens, $pathNode);
                $ast->addChild($pathNode);

                break;
            }

            // Anything besides an opening '[' prior to a '/' is considered the host
            if ($token->type !== TokenTypes::T_PUNCTUATION && $token->value !== '[') {
                $hostExists = true;
            }

            $lookaheadBuffer[] = $token;
            $tokens->next();
        }

        return $ast;
    }

    /**
     * Parses a punctuation token
     *
     * @param TokenStream $tokens The stream of tokens to parse
     * @param AstNode $currNode The abstract syntax tree to add nodes to
     * @param bool $parsingPath Whether or not we're parsing the path (otherwise we're parsing the host)
     * @throws InvalidArgumentException Thrown if an unexpected punctuation token was found
     */
    private function parsePunctuation(TokenStream $tokens, AstNode &$currNode, bool $parsingPath): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        switch ($token->value) {
            case '/':
                if (!$parsingPath) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\" in host");
                }

                $currNode->addChild(new AstNode(AstNodeTypes::SEGMENT_DELIMITER, $token->value));
                $tokens->next();
                break;
            case '.':
                // Periods in paths are to be treated as just text
                $nodeType = $parsingPath ? AstNodeTypes::TEXT : AstNodeTypes::SEGMENT_DELIMITER;
                $currNode->addChild(new AstNode($nodeType, $token->value));
                $tokens->next();
                break;
            case '[':
                $parentNodeType = $currNode->type;

                if (
                    $parentNodeType !== AstNodeTypes::HOST
                    && $parentNodeType !== AstNodeTypes::PATH
                    && $parentNodeType !== AstNodeTypes::OPTIONAL_ROUTE_PART
                ) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\"");
                }

                $optionalRoutePartNode = new AstNode(AstNodeTypes::OPTIONAL_ROUTE_PART, $token->value);
                $currNode->addChild($optionalRoutePartNode);
                $currNode = $optionalRoutePartNode;
                $tokens->next();

                if ($parsingPath) {
                    $tokens->expect(
                        TokenTypes::T_PUNCTUATION,
                        '/',
                        'Expected optional path part to start with \'/\', got %s'
                    );
                }

                break;
            case ']':
                if ($currNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                    // Just treat this as normal text
                    $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                    $tokens->next();
                    break;
                }

                if (!$parsingPath) {
                    /**
                     * Optional parts in hosts must end with '.', eg [foo.[bar.]]example.com
                     * So, make sure that the previous non-optional route part ends with '.'
                     */
                    $isValid = false;

                    for ($i = \count($currNode->children) - 1;$i >= 0;$i--) {
                        $childNode = $currNode->children[$i];

                        if ($childNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                            if ($childNode->type === AstNodeTypes::SEGMENT_DELIMITER) {
                                $isValid = true;
                            }

                            break;
                        }
                    }

                    if (!$isValid) {
                        throw new InvalidArgumentException('Expected optional host part to end with \'.\'');
                    }
                }

                // End this optional route part
                $currNode = $currNode->parent;
                $tokens->next();
                break;
            default:
                // Since we handle punctuation inside of variables elsewhere, we'll just treat this as text
                $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                $tokens->next();
                break;
        }
    }

    /**
     * Parses a text token
     *
     * @param TokenStream $tokens The stream of tokens to parse
     * @param AstNode $currNode The abstract syntax tree to add nodes to
     */
    private function parseText(TokenStream $tokens, AstNode $currNode): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
        $tokens->next();
    }

    /**
     * Parses a stream of tokens and adds it to an abstract syntax tree
     *
     * @param TokenStream $tokens The tokens to parse
     * @param AstNode $ast The abstract syntax tree to add to
     */
    private function parseTokens(TokenStream $tokens, AstNode $ast): void
    {
        $parsingPath = $ast->type === AstNodeTypes::PATH;
        $currNode = $ast;

        while (($token = $tokens->getCurrent()) !== null) {
            switch ($token->type) {
                case TokenTypes::T_TEXT:
                    $this->parseText($tokens, $currNode);
                    break;
                case TokenTypes::T_NUMBER:
                    // Since we handle a number inside of variables elsewhere, we'll just treat this as text
                    $this->parseText($tokens, $currNode);
                    break;
                case TokenTypes::T_VARIABLE:
                    $this->parseVariable($tokens, $currNode);
                    break;
                case TokenTypes::T_PUNCTUATION:
                    $this->parsePunctuation($tokens, $currNode, $parsingPath);
                    break;
                case TokenTypes::T_QUOTED_STRING:
                    // Since we handle a quoted string inside of variables elsewhere, we'll just treat this as text
                    $this->parseText($tokens, $currNode);
                    break;
            }
        }
    }

    /**
     * Parses a stream of variable tokens
     *
     * @param TokenStream $tokens The stream of tokens to parse
     * @param AstNode $currNode The abstract syntax tree to add nodes to
     */
    private function parseVariable(TokenStream $tokens, AstNode &$currNode): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $variableNode = new AstNode(AstNodeTypes::VARIABLE, $token->value);
        $currNode->addChild($variableNode);
        $currNode = $variableNode;
        $tokens->next();

        // Check for the beginning of a rule list
        if ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, '(')) {
            // Parse all variable rules
            do {
                $this->parseVariableRule($tokens, $variableNode);
            } while ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, ','));

            $tokens->expect(TokenTypes::T_PUNCTUATION, ')', 'Expected closing parenthesis after rules, got %s');
            $tokens->next();
        }

        if ($tokens->test(TokenTypes::T_VARIABLE)) {
            throw new InvalidArgumentException('Cannot have consecutive variables without a delimiter');
        }

        $currNode = $currNode->parent;
    }

    /**
     * Parses a single variable rule
     *
     * @param TokenStream $tokens The stream of tokens to parse
     * @param AstNode $currNode The variable node to add nodes to
     */
    private function parseVariableRule(TokenStream $tokens, AstNode $currNode): void
    {
        // Expect a rule name
        $tokens->expect(TokenTypes::T_TEXT, null, 'Expected rule name, got %s');

        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        $variableRuleNode = new AstNode(AstNodeTypes::VARIABLE_RULE, $token->value);
        $tokens->next();

        // Check for a parameter list for this rule
        if ($tokens->nextIfType(TokenTypes::T_PUNCTUATION, '(')) {
            $parameters = [];
            $currentToken = $tokens->getCurrent();

            while ($currentToken !== null && !$tokens->test(TokenTypes::T_PUNCTUATION, ')')) {
                if (!$tokens->test(TokenTypes::T_PUNCTUATION, ',')) {
                    $parameters[] = $currentToken->value;
                }

                $currentToken = $tokens->next();
            }

            $variableRuleNode->addChild(new AstNode(AstNodeTypes::VARIABLE_RULE_PARAMETERS, $parameters));
            $tokens->expect(
                TokenTypes::T_PUNCTUATION,
                ')',
                'Expected closing parenthesis after rule parameters, got %s'
            );
            $tokens->next();
        }

        $currNode->addChild($variableRuleNode);
    }
}


<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2019 David Young
 * @license   https://github.com/aphiria/router/blob/master/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Routing\Matchers\Trees;

use Aphiria\Routing\Route;
use InvalidArgumentException;

/**
 * Defines a trie node that contains a variable value
 */
final class VariableTrieNode extends TrieNode
{
    /** @var string[]|RouteVariable[] The parts that make up this node */
    public $parts;
    /** @var bool Whether or not this node contains just a single variable part (for performance reasons) */
    private $onlyContainsVariable;
    /** @var string The regex to use for matching */
    private $regex;

    /**
     * @param string[]|RouteVariable[]|string|RouteVariable $parts The parts that make up this segment
     * @param TrieNode[] $children The list of children
     * @param Route[] $routes The list of routes contained by this segment
     * @param TrieNode|null $hostTrie The host trie, if there is one
     * @throws InvalidArgumentException Thrown if the parts are empty
     */
    public function __construct($parts, array $children, $routes = [], TrieNode $hostTrie = null)
    {
        parent::__construct($children, $routes, $hostTrie);

        $this->parts = \is_array($parts) ? $parts : [$parts];

        if (\count($this->parts) === 0) {
            throw new InvalidArgumentException('Must have at least one variable part');
        }

        // If this segment is composed of only a single variable (which is the norm in REST APIs), optimize for it
        if (\count($this->parts) === 1 && $this->parts[0] instanceof RouteVariable) {
            $this->onlyContainsVariable = true;
        } else {
            // This must contain at least two  parts (variable or literal parts)
            $this->onlyContainsVariable = false;
            $this->regex = '';

            foreach ($this->parts as $part) {
                if ($part instanceof RouteVariable) {
                    $this->regex .= '(?<' . $part->name . '>.*)';
                } else {
                    $this->regex .= \preg_quote($part, '#');
                }
            }

            $this->regex = '#^' . $this->regex . '$#i';
        }
    }

    /**
     * Gets whether or not a segment matches this node
     *
     * @param string $segmentValue The segment value to match against
     * @param array $routeVariables The route variables found on a successful match
     * @return bool True if the input segment value matches this node, otherwise false
     */
    public function isMatch(string $segmentValue, array &$routeVariables): bool
    {
        if ($this->onlyContainsVariable) {
            foreach ($this->parts[0]->rules as $rule) {
                if (!$rule->passes($segmentValue)) {
                    return false;
                }
            }

            $routeVariables[$this->parts[0]->name] = $segmentValue;

            return true;
        }

        $matches = [];

        if (\preg_match($this->regex, $segmentValue, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
            return false;
        }

        // Don't change the actual array until we're sure all the rules pass
        $routeVariablesCopy = $routeVariables;

        foreach ($this->parts as $part) {
            if ($part instanceof RouteVariable) {
                foreach ($part->rules as $rule) {
                    if (!$rule->passes($matches[$part->name])) {
                        return false;
                    }
                }

                $routeVariablesCopy[$part->name] = $matches[$part->name];
            }
        }

        $routeVariables = $routeVariablesCopy;

        return true;
    }
}


<?php

namespace jumper423\decaptcha\core;

use Exception;

/**
 * Class DeCaptchaAbstract.
 */
abstract class DeCaptchaAbstract implements DeCaptchaInterface
{
    const RESPONSE_TYPE_STRING = 1;
    const RESPONSE_TYPE_JSON = 2;

    const ACTION_FIELDS = 1;
    const ACTION_URI = 2;
    const ACTION_METHOD = 3;
    const ACTION_JSON = 4;

    const ACTION_METHOD_POST = 1;
    const ACTION_METHOD_GET = 2;

    const DECODE_FORMAT = 1;
    const DECODE_ACTION = 2;
    const DECODE_SEPARATOR = 3;
    const DECODE_PARAMS = 4;
    const DECODE_PARAM_SETTING_MARKER = 5;

    const PARAM_FIELD_TYPE_STRING = 1;
    const PARAM_FIELD_TYPE_INTEGER = 2;
    const PARAM_FIELD_TYPE_MIX = 3;
    const PARAM_FIELD_TYPE_OBJECT = 4;
    const PARAM_FIELD_TYPE_BOOLEAN = 5;
    const PARAM_FIELD_TYPE_FLOAT = 6;

    const PARAM_SLUG_DEFAULT = 1;
    const PARAM_SLUG_TYPE = 2;
    const PARAM_SLUG_REQUIRE = 3;
    const PARAM_SLUG_SPEC = 4;
    const PARAM_SLUG_VARIABLE = 5;
    const PARAM_SLUG_CODING = 6;
    const PARAM_SLUG_NOTWIKI = 7;
    const PARAM_SLUG_ENUM = 8;
    const PARAM_SLUG_WIKI = 9;

    const PARAM_SLUG_CODING_BASE64 = 1;

    const PARAM_SPEC_API_KEY = -1;
    const PARAM_SPEC_FILE = -2;
    const PARAM_SPEC_CAPTCHA = -3;
    const PARAM_SPEC_CODE = -4;

    /**
     * Сервис на который будем загружать капчу.
     *
     * @var string
     */
    protected $host;
    protected $scheme = 'http';
    protected $errorLang = DeCaptchaErrors::LANG_EN;
    protected $lastRunTime = null;
    /** @var DeCaptchaErrors */
    protected $errorObject;
    protected $causeAnError = false;

    protected $limit = [];
    protected $paramsSpec = [];
    protected $params = [];
    protected $limitSettings = [];
    protected $decodeSettings = [];
    protected $actions = [];
    protected $paramsNames = [];

    protected function resetLimits()
    {
        foreach ($this->limitSettings as $action => $value) {
            $this->limit[$action] = $value;
        }
    }

    /**
     * @param $action
     *
     * @return bool
     */
    protected function limitHasNotYetEnded($action)
    {
        return $this->limit[$action]-- > 0;
    }

    /**
     * @param $action
     * @param $data
     *
     * @throws DeCaptchaErrors
     *
     * @return array
     */
    protected function decodeResponse($action, $data)
    {
        if (!array_key_exists($action, $this->decodeSettings[static::DECODE_ACTION])) {
            throw new DeCaptchaErrors('нет action');
        }
        $decodeSetting = $this->decodeSettings[static::DECODE_ACTION][$action];
        $decodeFormat = array_key_exists(static::DECODE_FORMAT, $decodeSetting) ?
            $decodeSetting[static::DECODE_FORMAT] :
            $this->decodeSettings[static::DECODE_FORMAT];
        $values = [];
        switch ($decodeFormat) {
            case static::RESPONSE_TYPE_STRING:
                foreach (explode($decodeSetting[static::DECODE_SEPARATOR], $data) as $key => $value) {
                    foreach ($decodeSetting[static::DECODE_PARAMS] as $param => $paramSetting) {
                        if ($key === $paramSetting[static::DECODE_PARAM_SETTING_MARKER]) {
                            $values[$param] = $value;
                        }
                    }
                }
                break;
            case static::RESPONSE_TYPE_JSON:
                foreach (json_decode($data, true) as $key => $value) {
                    foreach ($decodeSetting[static::DECODE_PARAMS] as $param => $paramSetting) {
                        if (count(explode('.', $paramSetting[static::DECODE_PARAM_SETTING_MARKER])) > 1) {
                            if ($key === explode('.', $paramSetting[static::DECODE_PARAM_SETTING_MARKER])[0]) {
                                if (array_key_exists(explode('.', $paramSetting[static::DECODE_PARAM_SETTING_MARKER])[1], $value)) {
                                    $values[$param] = $value[explode('.', $paramSetting[static::DECODE_PARAM_SETTING_MARKER])[1]];
                                }
                            }
                        }
                        if ($key === $paramSetting[static::DECODE_PARAM_SETTING_MARKER]) {
                            $values[$param] = $value;
                        }
                    }
                }
                break;
        }

        return $values;
    }

    /**
     * @param $errorLang
     */
    public function setErrorLang($errorLang)
    {
        $this->errorLang = $errorLang;
    }

    /**
     * Узнаём путь до файла
     * Если передана ссылка, то скачиваем и кладём во временную директорию.
     *
     * @param string $fileName
     *
     * @throws Exception
     *
     * @return string
     */
    protected function getFilePath($fileName)
    {
        if (strpos($fileName, 'http://') !== false || strpos($fileName, 'https://') !== false) {
            try {
                $current = file_get_contents($fileName);
            } catch (\Exception $e) {
                throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_FILE_IS_NOT_LOADED, $fileName, $this->errorLang);
            }
            $path = tempnam(sys_get_temp_dir(), 'captcha');
            file_put_contents($path, $current);

            return $path;
        }
        if (file_exists($fileName)) {
            return $fileName;
        }

        throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_FILE_NOT_FOUND, $fileName, $this->errorLang);
    }

    /**
     * @param $action
     *
     * @return string
     */
    protected function getActionUrl($action)
    {
        return $this->getBaseUrl().$this->actions[$action][static::ACTION_URI];
    }

    /**
     * @return string
     */
    protected function getBaseUrl()
    {
        return "{$this->scheme}://{$this->host}/";
    }

    /**
     * @param $params
     */
    public function setParams($params)
    {
        if (is_array($params)) {
            foreach ($params as $param => $value) {
                $this->params[$param] = $value;
            }
        }
    }

    /**
     * @param $param
     * @param $value
     */
    public function setParam($param, $value)
    {
        $this->params[$param] = $value;
    }

    /**
     * @param $param
     * @param $spec
     * @param $coding
     *
     * @return \CURLFile|mixed|null|string
     */
    public function getParamSpec($param, $spec = null, $coding = null)
    {
        if (is_null($spec)) {
            $spec = $param;
        }
        if (!array_key_exists($param, $this->params) || is_null($this->params[$param])) {
            if (!array_key_exists($spec, $this->params) || is_null($this->params[$spec])) {
                return null;
            }
            $param = $spec;
        }
        switch ($spec) {
            case static::PARAM_SPEC_FILE:
                switch ($coding) {
                    case static::PARAM_SLUG_CODING_BASE64:
                        return base64_encode(file_get_contents($this->params[$param]));
                }

                return (version_compare(PHP_VERSION, '5.5.0') >= 0) ? new \CURLFile($this->getFilePath($this->params[$param])) : '@'.$this->getFilePath($this->params[$param]);
            case static::PARAM_SPEC_API_KEY:
                return is_callable($this->params[$param]) ? $this->params[$param]() : $this->params[$param];
            case static::PARAM_SPEC_CAPTCHA:
                return (int) $this->params[$param];
            case static::PARAM_SPEC_CODE:
                return (string) $this->params[$param];
        }

        return null;
    }

    /**
     * @param $action
     * @param $field
     *
     * @throws DeCaptchaErrors
     *
     * @return array
     */
    protected function getParams($action, $field = null)
    {
        if (empty($this->actions[$action])) {
            return [];
        }
        $fields = $this->actions[$action][static::ACTION_FIELDS];
        if (!is_null($field)) {
            $fields = $fields[$field][static::ACTION_FIELDS];
        }
        $params = [];
        foreach ($fields as $field => $settings) {
            $value = null;
            if (array_key_exists(self::PARAM_SLUG_DEFAULT, $settings)) {
                $value = $settings[self::PARAM_SLUG_DEFAULT];
            }
            if (array_key_exists($field, $this->params) && (!array_key_exists(self::PARAM_SLUG_VARIABLE, $settings) ^ (array_key_exists(self::PARAM_SLUG_VARIABLE, $settings) && $settings[self::PARAM_SLUG_VARIABLE] === false))) {
                $value = $this->params[$field];
            }
            if (array_key_exists(self::PARAM_SLUG_SPEC, $settings)) {
                $value = $this->getParamSpec($field, $settings[self::PARAM_SLUG_SPEC], array_key_exists(self::PARAM_SLUG_CODING, $settings) ? $settings[self::PARAM_SLUG_CODING] : null);
            }
            if (is_null($value)) {
                if (array_key_exists(self::PARAM_SLUG_REQUIRE, $settings) && $settings[self::PARAM_SLUG_REQUIRE] === true) {
                    throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_PARAM_REQUIRE, array_key_exists($field, $this->paramsNames) ? $this->paramsNames[$field] : $field, $this->errorLang);
                }
                continue;
            }
            if (array_key_exists($field, $this->paramsNames)) {
                switch ($settings[self::PARAM_SLUG_TYPE]) {
                    case self::PARAM_FIELD_TYPE_FLOAT:
                        $value = (float) $value;
                        break;
                    case self::PARAM_FIELD_TYPE_INTEGER:
                        $value = (int) $value;
                        break;
                    case self::PARAM_FIELD_TYPE_STRING:
                        $value = (string) $value;
                        break;
                    case self::PARAM_FIELD_TYPE_BOOLEAN:
                        $value = (bool) $value;
                        break;
                    case self::PARAM_FIELD_TYPE_OBJECT:
                        $value = $this->getParams($action, $field);
                        break;
                }
                if (array_key_exists(self::PARAM_SLUG_ENUM, $settings) && !in_array($value, $settings[static::PARAM_SLUG_ENUM])) {
                    throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_PARAM_ENUM, (array_key_exists($field, $this->paramsNames) ? $this->paramsNames[$field] : $field).' = '.$value, $this->errorLang);
                }
                $params[$this->paramsNames[$field]] = $value;
            }
        }

        return $params;
    }

    /**
     * @param string $action
     *
     * @return string
     */
    protected function getResponse($action)
    {
        return $this->curlResponse(
            $this->getActionUrl($action),
            $this->getParams($action),
            array_key_exists(static::ACTION_METHOD, $this->actions[$action]) && $this->actions[$action][static::ACTION_METHOD] === static::ACTION_METHOD_POST,
            array_key_exists(static::ACTION_JSON, $this->actions[$action]) && $this->actions[$action][static::ACTION_JSON] === true
        );
    }

    /**
     * Задержка выполнения.
     *
     * @param int           $delay    Количество секунд
     * @param \Closure|null $callback
     *
     * @return mixed
     */
    protected function executionDelayed($delay = 0, $callback = null)
    {
        $time = microtime(true);
        $timePassed = $time - $this->lastRunTime;
        if ($timePassed < $delay) {
            usleep(($delay - $timePassed) * 1000000);
        }
        $this->lastRunTime = microtime(true);

        return $callback instanceof \Closure ? $callback($this) : $callback;
    }

    /**
     * @param string $url
     * @param array  $data
     * @param bool   $isPost
     * @param bool   $isJson
     *
     * @throws DeCaptchaErrors
     *
     * @return string
     */
    protected function curlResponse($url, $data, $isPost = true, $isJson = false)
    {
        $curl = curl_init();
        if ($isJson) {
            $data = json_encode($data);
        } elseif (!$isPost) {
            $uri = [];
            foreach ($data as $key => $value) {
                $uri[] = "$key=$value";
            }
            $url .= '?'.implode('&', $uri);
        }
        curl_setopt($curl, CURLOPT_URL, $url);
        if (!$isJson && version_compare(PHP_VERSION, '5.5.0') >= 0 && version_compare(PHP_VERSION, '7.0') < 0 && defined('CURLOPT_SAFE_UPLOAD')) {
            curl_setopt($curl, CURLOPT_SAFE_UPLOAD, false);
        }
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_ENCODING, 'gzip,deflate');
        curl_setopt($curl, CURLOPT_TIMEOUT, 30);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30);
        curl_setopt($curl, CURLOPT_POST, $isPost);
        if ($isPost) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        if ($isJson) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, [
                'Content-Type: application/json; charset=utf-8',
                'Accept: application/json',
                'Content-Length: '.strlen($data),
            ]);
        }
        $result = curl_exec($curl);
        if (curl_errno($curl)) {
            throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_CURL, curl_error($curl), $this->errorLang);
        }
        curl_close($curl);

        return $result;
    }

    abstract public function getCode();

    abstract public function getError();
}


<?php

namespace jumper423\decaptcha\core;

/**
 * Распознавание капчи.
 *
 * Class DeCaptchaBase
 */
class DeCaptchaBase extends DeCaptchaAbstract implements DeCaptchaInterface
{
    const ACTION_RECOGNIZE = 1;
    const ACTION_UNIVERSAL = 2;
    const ACTION_UNIVERSAL_WITH_CAPTCHA = 3;

    const ACTION_FIELD_METHOD = 1;
    const ACTION_FIELD_KEY = 2;
    const ACTION_FIELD_FILE = 3;
    const ACTION_FIELD_PHRASE = 4;
    const ACTION_FIELD_REGSENSE = 5;
    const ACTION_FIELD_NUMERIC = 6;
    const ACTION_FIELD_MIN_LEN = 7;
    const ACTION_FIELD_MAX_LEN = 8;
    const ACTION_FIELD_LANGUAGE = 9;
    const ACTION_FIELD_SOFT_ID = 10;
    const ACTION_FIELD_CAPTCHA_ID = 11;
    const ACTION_FIELD_ACTION = 12;
    const ACTION_FIELD_QUESTION = 13;
    const ACTION_FIELD_CALC = 14;
    const ACTION_FIELD_HEADER_ACAO = 15;
    const ACTION_FIELD_INSTRUCTIONS = 16;
    const ACTION_FIELD_PINGBACK = 17;
    const ACTION_FIELD_TASK = 18;
    const ACTION_FIELD_IS_RUSSIAN = 19;
    const ACTION_FIELD_LABEL = 20;
    const ACTION_FIELD_GOOGLEKEY = 21;
    const ACTION_FIELD_RECAPTCHA = 22;
    const ACTION_FIELD_PROXY = 22;
    const ACTION_FIELD_PROXYTYPE = 23;
    const ACTION_FIELD_PAGEURL = 24;
    const ACTION_FIELD_GOOGLETOKEN = 25;
    const ACTION_FIELD_PROXYLOGIN = 26;
    const ACTION_FIELD_PROXYPASS = 27;
    const ACTION_FIELD_USERAGENT = 28;
    const ACTION_FIELD_COOKIES = 29;
    const ACTION_FIELD_PROXYPORT = 30;
    const ACTION_FIELD_COORDINATE = 31;
    const ACTION_FIELD_INVISIBLE = 32;
    const ACTION_FIELD_SSC_USER_ID = 33;
    const ACTION_FIELD_SSC_SESSION_ID = 34;
    const ACTION_FIELD_SSC_WEB_SERVER_SIGN = 35;
    const ACTION_FIELD_SSC_WEB_SERVER_SIGN2 = 36;
    const ACTION_FIELD_NOJS = 37;
    const ACTION_FIELD_PUBLICKEY = 38;
    const ACTION_FIELD_LANG = 39;
    const ACTION_FIELD_VERSION = 40;
    const ACTION_FIELD_MIN_SCORE = 41;
    const ACTION_FIELD_GT = 42;
    const ACTION_FIELD_CHALLENGE = 43;
    const ACTION_FIELD_API_SERVER = 44;

    const RESPONSE_RECOGNIZE_OK = 'OK';
    const RESPONSE_RECOGNIZE_REPEAT = 'ERROR_NO_SLOT_AVAILABLE';
    const RESPONSE_GET_OK = 'OK';
    const RESPONSE_GET_REPEAT = 'CAPCHA_NOT_READY';

    const SLEEP_RECOGNIZE = 5;
    const SLEEP_GET = 2;
    const SLEEP_BETWEEN = 5;

    const DECODE_ACTION_RECOGNIZE = 1;
    const DECODE_ACTION_GET = 2;
    const DECODE_ACTION_UNIVERSAL = 3;

    const DECODE_PARAM_RESPONSE = 1;
    const DECODE_PARAM_CAPTCHA = 2;
    const DECODE_PARAM_CODE = 3;

    public $actions = [
        self::ACTION_RECOGNIZE              => [],
        self::ACTION_UNIVERSAL              => [],
        self::ACTION_UNIVERSAL_WITH_CAPTCHA => [],
    ];

    protected $decodeSettings = [
        self::DECODE_FORMAT => self::RESPONSE_TYPE_STRING,
        self::DECODE_ACTION => [
            self::DECODE_ACTION_RECOGNIZE => [],
            self::DECODE_ACTION_GET       => [],
        ],
    ];

    protected $limitSettings = [
        self::ACTION_RECOGNIZE              => 5,
        self::ACTION_UNIVERSAL_WITH_CAPTCHA => 40,
    ];

    /**
     * @var DeCaptchaWiki
     */
    protected $wiki;

    /**
     * DeCaptchaBase constructor.
     *
     * @param $params
     */
    public function __construct($params)
    {
        $this->setParams($params);
        $this->wiki = new DeCaptchaWiki($this);
        $this->init();
    }

    public function init()
    {
    }

    /**
     * @param $filePath
     * @param array $additionally
     *
     * @throws DeCaptchaErrors
     *
     * @return bool
     */
    public function recognize($filePath, $additionally = [])
    {
        try {
            $this->resetLimits();
            if ($filePath) {
                $additionally[static::ACTION_FIELD_FILE] = $filePath;
            }
            $this->setParams($additionally);

            return $this->requestRecognize() && $this->requestCode();
        } catch (DeCaptchaErrors $e) {
            if ($this->causeAnError) {
                throw $e;
            }
            $this->errorObject = $e;

            return false;
        }
    }

    /**
     * Запуск распознавания капчи.
     *
     * @deprecated
     *
     * @param string $filePath Путь до файла или ссылка на него
     *
     * @return bool
     */
    public function run($filePath)
    {
        return $this->recognize($filePath);
    }

    /**
     * Универсальная отправка повторяющихся запросов.
     *
     * @param int      $action
     * @param int      $decodeAction
     * @param int      $setParam
     * @param int      $decodeSerParam
     * @param int      $ok
     * @param int      $sleep
     * @param int      $repeat
     * @param int|null $error
     *
     * @throws DeCaptchaErrors
     *
     * @return bool
     */
    protected function requestRepeat($action, $decodeAction, $setParam, $decodeSerParam, $ok, $sleep, $repeat, $error = null)
    {
        if (is_null($error)) {
            $error = static::DECODE_PARAM_RESPONSE;
        }
        while ($this->limitHasNotYetEnded($action)) {
            $this->executionDelayed($sleep);
            $response = $this->getResponse($action);
            $dataRecognize = $this->decodeResponse($decodeAction, $response);
            if ($dataRecognize[static::DECODE_PARAM_RESPONSE] === $ok && !empty($dataRecognize[$decodeSerParam])) {
                $this->setParam($setParam, $dataRecognize[$decodeSerParam]);
                $this->executionDelayed(static::SLEEP_BETWEEN);

                return true;
            } elseif ($dataRecognize[static::DECODE_PARAM_RESPONSE] === $repeat) {
                continue;
            }

            throw new DeCaptchaErrors($dataRecognize[$error], null, $this->errorLang);
        }

        throw new DeCaptchaErrors(DeCaptchaErrors::ERROR_LIMIT, null, $this->errorLang);
    }

    /**
     * Универсальная отправка.
     *
     * @param string $action
     *
     * @return array
     */
    protected function requestUniversal($action)
    {
        $this->setParam(static::ACTION_FIELD_ACTION, $action);
        $response = $this->getResponse(static::ACTION_UNIVERSAL);

        return $this->decodeResponse(static::DECODE_ACTION_UNIVERSAL, $response);
    }

    /**
     * @throws DeCaptchaErrors
     *
     * @return bool
     */
    protected function requestRecognize()
    {
        return $this->requestRepeat(static::ACTION_RECOGNIZE, static::DECODE_ACTION_RECOGNIZE, static::PARAM_SPEC_CAPTCHA, static::DECODE_PARAM_CAPTCHA, static::RESPONSE_RECOGNIZE_OK, static::SLEEP_RECOGNIZE, static::RESPONSE_RECOGNIZE_REPEAT);
    }

    /**
     * @throws DeCaptchaErrors
     *
     * @return bool
     */
    protected function requestCode()
    {
        return $this->requestRepeat(static::ACTION_UNIVERSAL_WITH_CAPTCHA, static::DECODE_ACTION_GET, static::PARAM_SPEC_CODE, static::DECODE_PARAM_CODE, static::RESPONSE_GET_OK, static::SLEEP_GET, static::RESPONSE_GET_REPEAT);
    }

    /**
     * @return array|string
     */
    public function getCode()
    {
        return $this->getParamSpec(static::PARAM_SPEC_CODE);
    }

    /**
     * @return string
     */
    public function getError()
    {
        return $this->errorObject->getMessage();
    }

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

    public function getWiki($lang = 'en')
    {
        $this->wiki->setLang($lang);

        return $this->wiki;
    }
}