<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2014 Vanilla Forums Inc.
 * @license MIT
 */

namespace Garden\Password;

/**
 * Implements tha password hashing algorithm from the Django framework.
 */
class DjangoPassword implements IPassword {
    /**
     * @var string The hash method to use when hashing passwords.
     */
    public $hashMethod;

    /**
     * Initiailize an instance of the {@link DjangoPassword} class.
     *
     * @param string $hashMethod The hasm method used to hash the passwords.
     */
    public function __construct($hashMethod = 'crypt') {
        $this->hashMethod = $hashMethod;
    }

    /**
     * Generate a random salt that is compatible with {@link crypt()}.
     *
     * @return string|null Returns the salt as a string or **null** if the crypt algorithm isn't known.
     */
    protected function generateCryptSalt() {
        if (CRYPT_BLOWFISH === 1) {
            $salt = str_replace('+', '/', base64_encode(openssl_random_pseudo_bytes(12)));
        } elseif (CRYPT_EXT_DES) {
            $count_log2 = 24; //min($this->iteration_count_log2 + 8, 24);
            # This should be odd to not reveal weak DES keys, and the
            # maximum valid value is (2**24 - 1) which is odd anyway.
            $count = (1 << $count_log2) - 1;

            $salt = '_';
            $salt .= $this->itoa64[$count & 0x3f];
            $salt .= $this->itoa64[($count >> 6) & 0x3f];
            $salt .= $this->itoa64[($count >> 12) & 0x3f];
            $salt .= $this->itoa64[($count >> 18) & 0x3f];

            $salt .= substr(base64_encode(openssl_random_pseudo_bytes(3), 0, 3));
        } else {
            $salt = null;
        }
        return $salt;
    }

    /**
     * Hashes a plaintext password.
     *
     * @param string $password The password to hash.
     * @return string Returns the hashed password.
     * @throws \Exception Throws an exception when the hash method is invalid.
     */
    public function hash($password) {
        if ($this->hashMethod === 'crypt') {
            $salt = $this->generateCryptSalt();
            try {
                $hash = crypt($password, $salt);
            } catch (\Exception $ex) {
                throw new \Exception("$salt is an invalid salt.", $ex);
            }
        } elseif (in_array($this->hashMethod, hash_algos())) {
            $salt = base64_encode(openssl_random_pseudo_bytes(12));
            $hash = hash($this->hashMethod, $salt.$password);
        } else {
            throw new \Exception("The {$this->hashMethod} hash method is invalid.", 500);
        }

        $result = $this->hashMethod.'$'.$salt.'$'.$hash;
        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function needsRehash($hash) {
        if (strpos($hash, '$') === false) {
            return true;
        } else {
            list($method,,) = explode('$', $hash, 3);
            switch (strtolower($method)) {
                case 'crypt':
                case 'sha256':
                    return false;
                default:
                    return true;
            }
        }
    }

    /**
     * Check to make sure a password matches its stored hash.
     *
     * @param string $password The password to verify.
     * @param string $hash The stored password hash.
     * @return bool Returns `true` if the password matches the stored hash.
     */
    public function verify($password, $hash) {
        if (strpos($hash, '$') === false) {
            return md5($password) == $hash;
        } else {
            list($method, $salt, $rawHash) = explode('$', $hash);

            if ($method === 'crypt') {
                return crypt($password, $salt) === $rawHash;
            } elseif (in_array($method, hash_algos())) {
                return hash($method, $salt.$password) === $rawHash;
            } else {
                return false;
            }
        }
    }
}


<?php namespace Garden;

    /**
     * Format of a porter run.
     *
     *     array('source' => array(
     *        ['source' => array('table', array(filter)),]
     *        'destination' => tablename,
     *        'columns' => array(
     *           'sourcecolumn' => array('destcolumn'
     *              [, 'type' => dbtype]
     *              [, 'sourcecolumn' => 'name']
     *              [, 'filter' => callable]
     *           )
     *        )
     *        [, 'rowfilter' => callable]
     */

/**
 *
 */
class Porter {
    /// Properties ///

    /**
     * The destination database.
     * @var Db
     */
    protected $destination;

    /**
     * @var array
     */
    protected $formats;

    /**
     * The source database.
     * @var Db
     */
    protected $source;

    /// Methods ///

    /**
     *
     * @param Db $source
     * @param Db $destination
     */
    public function __construct($source, $destination) {
        $this->source = $source;
        $this->destination = $destination;
    }

    public function run() {
        // Get all of the tables in the source database.
        if (!isset($this->formats)) {
            $this->formats = $this->getFormatsFromDb($this->source);
        } else {
            $this->fixFormats($this->formats);
        }

        // Port the data in each table.
        foreach ($this->formats as $name => $format) {
            $this->dumpTable($format);
        }
    }

    /**
     * Get the translation format from the database.
     *
     * @param Db $db
     * @return type
     */
    protected function getFormatsFromDb($db) {
        $tables = $db->tables(true);
        $formats = $this->fixFormats($tables);
        return $formats;
    }

    protected function fixFormats($data) {
        $result = array();

        foreach ($data as $table => $tdef) {
            $format = $tdef;

            // Set the source query.
            array_touch('source', $format, array($table, array()));
            array_touch('destination', $format, $format['destination']);

            // Coax the columns into the proper format.
            $columns = array();
            if (isset($tdef['columns'])) {
                foreach ($tdef['columns'] as $sourceColumn => $cdef) {
                    if (is_string($cdef)) {
                        // This is a format in the form sourcecolumns => destcolumn
                        $cformat = array($cdef);
                    } elseif (isset($cdef[0])) {
                        $cformat = array_change_key_case($cdef);
                    } elseif (isset($cdef['column'])) {
                        // This is an old porter format.
                        $cformat = array($cdef['column']) + $cdef;
                        unset($cformat['column']);
                    } else {
                        $cformat = array(val('sourcecolumn', $cdef, $sourceColumn)) + array_change_key_case($cdef);
                    }

                    array_touch('sourcecolumn', $cformat, $sourceColumn);
                    array_touch('type', $cformat, 'varchar(255)');
                    array_touch('default', $cformat, null);

                    $columns[$sourceColumn] = $cformat;
                }
            }
            $format['columns'] = $columns;
//            $format['defaultrow'] = $this->getDefaultRow($format);

            $result[$table] = $columns;
        }

        return $result;
    }

    public function dumpTable($format) {
        $table = $format['destination'];

        timerStart("dumping $table");

        // Define the table.
        timerStart("defining $table");
        $this->destination->defineTable($format);
        timerStop();

        // Loop over the data from the source.
        timerStart("loading $table");
        $rows = $this->source->get($format['source'][0], $format['source'][1], null, null, array(Db::GET_UNBUFFERED => true));

        $this->destination->loadStart($table);
        foreach ($rows as $row) {
            $trow = $this->translateRow($row, $format);
            $this->destination->loadRow($trow);
        }
        $load = $this->destination->loadFinish();
        timerStop(array_translate($load, array('count' => 'rows'))); // loading table

        timerStop(); // dumping table
    }

    /**
     * Translate a row from one row format to another.
     *
     * @param array $row The data to translate.
     * @param array $format The translation format.
     * @return array The translated row.
     */
    protected function translateRow($row, $format) {
        // Apply the row filter.
        if (isset($format['rowfilter'])) {
            call_user_func_array($format['rowfilter'], array(&$row));
        }

        $result = array();
        foreach ($format['columns'] as $key => $cdef) {
            if (array_key_exists($cdef['sourcecolumn'], $row))
                $value = $row[$cdef['sourcecolumn']];
            else
                $value = $cdef['default'];

            if (isset($cdef['filter'])) {
                $value = call_user_func($cdef['filter'], $value, $key, $row);
            }

            $result[$cdef[0]] = $value;
        }
        return $result;
    }
}


<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2014 Vanilla Forums Inc.
 * @license MIT
 */

namespace Garden\Password;

/**
 * Implements the password hashing algorithm of punBB.
 *
 * In order to use this class with passwords from a punbb database concatenate the password hashes and salts.
 *
 * ```php
 * // php
 * $hash = $password.'$'.$salt;
 * ```
 *
 * ```sql
 * -- mysql
 * select concat(u.password, '$', u.salt) as password_hash
 * from punbb_users u;
 * ```
 */
class PunbbPassword implements IPassword {

    /**
     * {@inheritdoc}
     */
    public function hash($password) {
        $salt = base64_encode(openssl_random_pseudo_bytes(12));
        return $this->hashRaw($password, $salt).'$'.$salt;
    }

    /**
     * Hashes a password with a given salt.
     *
     * @param string $password The password to hash.
     * @param string $salt The password salt.
     * @return string Returns the password hash.
     */
    protected function hashRaw($password, $salt) {
        $calc_hash = sha1($salt.sha1($password));

        return $calc_hash;
    }

    /**
     * {@inheritdoc}
     */
    public function needsRehash($hash) {
        list($stored_hash, $stored_salt) = $this->splitHash($hash);

        // Unsalted hashes should be rehashed.
        if ($stored_hash === false || $stored_salt === false) {
            return true;
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function verify($password, $hash) {
        list($stored_hash, $stored_salt) = $this->splitHash($hash);

        if (md5($password) == $stored_hash) {
            $result = true;
        } elseif (sha1($password) == $stored_hash) {
            $result = true;
        } elseif (sha1($stored_salt.sha1($password)) == $stored_hash) {
            $result = true;
        } else {
            $result = false;
        }

        return $result;
    }

    /**
     * Split the hash into its calculated hash and salt.
     *
     * @param string $hash The hash to split.
     * @return array An array in the form [$hash, $salt].
     */
    protected function splitHash($hash) {
        if (strpos($hash, '$') === false) {
            return [$hash, false];
        } else {
            $parts = explode('$', $hash, 2);
            return $parts;
        }
    }
}


<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2005 phpBB Group (Original source code)
 * @copyright 2009-2014 Vanilla Forums Inc. (Source code changes)
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 *
 * This class adapts functions from phpBB 3 (/includes/functions.php). Any source code that was taken from the
 * phpBB project is copyright 2005 phpBB Group. Any source code changes are copyright 2009-2014 Vanilla Forums Inc.
 *
 */

namespace Garden\Password;

/**
 * Implements phpBB's password hashing algorithm.
 */
class PhpbbPassword implements IPassword {
    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    /**
     * Check for a correct password.
     *
     * @param string $password The password in plain text.
     * @param string $hash The stored password hash.
     * @return bool Returns true if the password is correct, false if not.
     */
    public function verify($password, $hash) {
        if (strlen($hash) == 34) {
            return ($this->cryptPrivate($password, $hash) === $hash) ? true : false;
        }

        return (md5($password) === $hash) ? true : false;
    }

    /**
     * The crypt function/replacement.
     *
     * @param string $password The password to encrypt.
     * @param string $setting The hash prefix setting. It should start with $H$.
     * @return string The encypted password.
     */
    private function cryptPrivate($password, $setting) {
        $itoa64 = PhpbbPassword::ITOA64;
        $output = '*';

        // Check for correct hash
        if (substr($setting, 0, 3) != '$H$') {
            return $output;
        }

        $count_log2 = strpos($itoa64, $setting[3]);

        if ($count_log2 < 7 || $count_log2 > 30) {
            return $output;
        }

        $count = 1 << $count_log2;
        $salt = substr($setting, 4, 8);

        if (strlen($salt) != 8) {
            return $output;
        }

        /**
         * We're kind of forced to use MD5 here since it's the only
         * cryptographic primitive available in all versions of PHP
         * currently in use.  To implement our own low-level crypto
         * in PHP would result in much worse performance and
         * consequently in lower iteration counts and hashes that are
         * quicker to crack (by non-PHP code).
         */
        if (PHP_VERSION >= 5) {
            $hash = md5($salt.$password, true);
            do {
                $hash = md5($hash.$password, true);
            } while (--$count);
        } else {
            $hash = pack('H*', md5($salt.$password));
            do {
                $hash = pack('H*', md5($hash.$password));
            } while (--$count);
        }

        $output = substr($setting, 0, 12);
        $output .= $this->encode64($hash, 16);

        return $output;
    }

    /**
     * Encode hash.
     *
     * @param string $input The input to encode.
     * @param int $count The number of characters to encode.
     * @return string The encoded string.
     */
    protected function encode64($input, $count) {
        $itoa64 = PhpbbPassword::ITOA64;
        $output = '';
        $i = 0;

        do {
            $value = ord($input[$i++]);
            $output .= $itoa64[$value & 0x3f];

            if ($i < $count) {
                $value |= ord($input[$i]) << 8;
            }

            $output .= $itoa64[($value >> 6) & 0x3f];

            if ($i++ >= $count) {
                break;
            }

            if ($i < $count) {
                $value |= ord($input[$i]) << 16;
            }

            $output .= $itoa64[($value >> 12) & 0x3f];

            if ($i++ >= $count) {
                break;
            }

            $output .= $itoa64[($value >> 18) & 0x3f];
        } while ($i < $count);

        return $output;
    }

    /**
     * Hashes a plaintext password.
     *
     * @param string $password The password to hash.
     * @return string Returns the hashed password.
     */
    public function hash($password) {
        $random = openssl_random_pseudo_bytes(6);

        $hash = $this->cryptPrivate($password, $this->gensaltPrivate($random));
        if (strlen($hash) == 34) {
            return $hash;
        }

        return null;
    }

    /**
     * Generate a password salt based on the given input string.
     *
     * @param string $input The input string to generate the salt from.
     * @return string Returns the password salt prefixed with `$P$`.
     */
    private function gensaltPrivate($input) {
        $itoa64 = PhpbbPassword::ITOA64;

        $output = '$H$';
        $output .= $itoa64[min(8 + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
        $output .= $this->encode64($input, 6);

        return $output;
    }

    /**
     * Checks if a given password hash needs to be re-hashed to to a stronger algorithm.
     *
     * @param string $hash The hash to check.
     * @return bool Returns `true`
     */
    public function needsRehash($hash) {
        return $hash === '*';
    }
}


<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2014 Vanilla Forums Inc.
 * @license MIT
 */

namespace Garden\Password;

/**
 * Implements the password hashing algorithm of Invision Power Board (ipb).
 */
class IpbPassword implements IPassword {

    /**
     * {@inheritdoc}
     */
    public function hash($password) {
        $salt = base64_encode(openssl_random_pseudo_bytes(12));
        return $this->hashRaw($password, $salt).'$'.$salt;
    }

    /**
     * Hashes a password with a given salt.
     *
     * @param string $password The password to hash.
     * @param string $salt The password salt.
     * @return string Returns the password hash.
     */
    protected function hashRaw($password, $salt) {
        $calc_hash = md5(md5($salt).md5($password));

        return $calc_hash;
    }

    /**
     * {@inheritdoc}
     */
    public function needsRehash($hash) {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function verify($password, $hash) {
        list($stored_hash, $salt) = $this->splitHash($hash);
        $calc_hash = $this->hashRaw($password, $salt);
        return $calc_hash === $stored_hash;
    }

    /**
     * Split the hash into its calculated hash and salt.
     *
     * @param string $hash The hash to split.
     * @return array An array in the form [$hash, $salt].
     */
    protected function splitHash($hash) {
        if (strpos($hash, '$') === false) {
            return [false, false];
        } else {
            $parts = explode('$', $hash, 2);
            return $parts;
        }
    }
}


<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2014 Vanilla Forums Inc.
 * @license MIT
 */

namespace Garden;

use JsonSerializable;

/**
 * A class that contains the information in an http request.
 */
class Request implements JsonSerializable {

    /// Constants ///
    const METHOD_HEAD = 'HEAD';
    const METHOD_GET = 'GET';
    const METHOD_POST = 'POST';
    const METHOD_PUT = 'PUT';
    const METHOD_PATCH = 'PATCH';
    const METHOD_DELETE = 'DELETE';
    const METHOD_OPTIONS = 'OPTIONS';

    /// Properties ///

    /**
     *
     * @var array The data in this request.
     */
    protected $env;

    /**
     * @var Request The currently dispatched request.
     */
    protected static $current;

    /**
     * @var array The default environment for constructed requests.
     */
    protected static $defaultEnv;

    protected static $knownExtensions = [
        '.html' => 'text/html',
        '.json' => 'application/json',
        '.txt' => 'text/plain',
        '.xml' => 'application/xml'
    ];

    /**
     * @var array The global environment for the request.
     */
    protected static $globalEnv;

    /**
     * Special-case HTTP headers that are otherwise unidentifiable as HTTP headers.
     * Typically, HTTP headers in the $_SERVER array will be prefixed with
     * `HTTP_` or `X_`. These are not so we list them here for later reference.
     *
     * @var array
     */
    protected static $specialHeaders = array(
        'CONTENT_TYPE',
        'CONTENT_LENGTH',
        'PHP_AUTH_USER',
        'PHP_AUTH_PW',
        'PHP_AUTH_DIGEST',
        'AUTH_TYPE'
    );

    /// Methods ///

    /**
     * Initialize a new instance of the {@link Request} class.
     *
     * @param string $url The url of the request or blank to use the current environment.
     * @param string $method The request method.
     * @param mixed $data The request data. This is the query string for GET requests or the body for other requests.
     */
    public function __construct($url = '', $method = '', $data = null) {
        if ($url) {
            $this->env = (array)static::defaultEnvironment();
            // Instantiate the request from the url.
            $this->setUrl($url);
            if ($method) {
                $this->setMethod($method);
            }
            if (is_array($data)) {
                $this->setData($data);
            }
        } else {
            // Instantiate the request from the global environment.
            $this->env = static::globalEnvironment();
            if ($method) {
                $this->setMethod($method);
            }
            if (is_array($data)) {
                $this->setData($data);
            }
        }

        static::overrideEnvironment($this->env);
    }

    /**
     * Convert a request to a string.
     *
     * @return string Returns the url of the request.
     */
    public function __toString() {
        return $this->getUrl();
    }

    /**
     * Gets or sets the current request.
     *
     * @param Request $request Pass a request object to set the current request.
     * @return Request Returns the current request if {@link Request} is null or the previous request otherwise.
     */
    public static function current(Request $request = null) {
        if ($request !== null) {
            $bak = self::$current;
            self::$current = $request;
            return $bak;
        }
        return self::$current;
    }

    /**
     * Gets or updates the default environment.
     *
     * @param string|array|null $key Specifies a specific key in the environment array.
     * If you pass an array for this parameter then you can set the default environment.
     * @param bool $merge Whether or not to merge the new value.
     * @return array|mixed Returns the value at {@link $key} or the entire environment array.
     * @throws \InvalidArgumentException Throws an exception when {@link $key} is not valid.
     */
    public static function defaultEnvironment($key = null, $merge = false) {
        if (self::$defaultEnv === null) {
            self::$defaultEnv = array(
                'REQUEST_METHOD' => 'GET',
                'X_REWRITE' => true,
                'SCRIPT_NAME' => '',
                'PATH_INFO' => '/',
                'EXT' => '',
                'QUERY' => [],
                'SERVER_NAME' => 'localhost',
                'SERVER_PORT' => 80,
                'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
                'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
                'HTTP_USER_AGENT' => 'Garden/0.1 (Howdy stranger)',
                'REMOTE_ADDR' => '127.0.0.1',
                'URL_SCHEME' => 'http',
                'INPUT' => [],
            );
        }

        if ($key === null) {
            return self::$defaultEnv;
        } elseif (is_array($key)) {
            if ($merge) {
                self::$defaultEnv = array_merge(self::$defaultEnv, $key);
            } else {
                self::$defaultEnv = $key;
            }
            return self::$defaultEnv;
        } elseif (is_string($key)) {
            return val($key, self::$defaultEnv);
        } else {
            throw new \InvalidArgumentException("Argument #1 for Request::globalEnvironment() is invalid.", 422);
        }
    }

    /**
     * Parse request information from the current environment.
     *
     * The environment contains keys based on the Rack protocol (see http://rack.rubyforge.org/doc/SPEC.html).
     *
     * @param mixed $key The environment variable to look at.
     * - null: Return the entire environment.
     * - true: Force a re-parse of the environment and return the entire environment.
     * - string: One of the environment variables.
     * @return array|string Returns the global environment or the value at {@link $key}.
     */
    public static function globalEnvironment($key = null) {
        // Check to parse the environment.
        if ($key === true || !isset(self::$globalEnv)) {
            self::$globalEnv = static::parseServerVariables();
        }

        if ($key) {
            return val($key, self::$globalEnv);
        }
        return self::$globalEnv;
    }

    /**
     * Parse the various server variables to build the global environment.
     *
     * @return array Returns an array suitable to be used as the {@see Request::$globalEnv}.
     * @see Request::globalEnvironment().
     */
    protected static function parseServerVariables() {
        $env = static::defaultEnvironment();

        // REQUEST_METHOD.
        $env['REQUEST_METHOD'] = val('REQUEST_METHOD', $_SERVER) ?: 'CONSOLE';

        // SCRIPT_NAME: This is the root directory of the application.
        $script_name = rtrim_substr($_SERVER['SCRIPT_NAME'], 'index.php');
        $env['SCRIPT_NAME'] = rtrim($script_name, '/');

        // PATH_INFO.
        $path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';

        // Strip the extension from the path.
        list($path, $ext) = static::splitPathExt($path);
        $env['PATH_INFO'] = '/' . ltrim($path, '/');
        $env['EXT'] = $ext;

        // QUERY.
        $get = $_GET;
        $env['QUERY'] = $get;

        // SERVER_NAME.
        $host = array_select(
            ['HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'SERVER_NAME'],
            $_SERVER
        );
        list($host) = explode(':', $host, 2);
        $env['SERVER_NAME'] = $host;

        // HTTP_* headers.
        $env = array_replace($env, static::extractHeaders($_SERVER));

        // URL_SCHEME.
        $url_scheme = 'http';
        // Web server-originated SSL.
        if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') {
            $url_scheme = 'https';
        }
        $url_scheme = array_select([
            'HTTP_X_ORIGINALLY_FORWARDED_PROTO', // varnish modifies the scheme
            'HTTP_X_FORWARDED_PROTO' // load balancer-originated (and terminated) ssl
        ], $_SERVER, $url_scheme);
        $env['URL_SCHEME'] = $url_scheme;

        // SERVER_PORT.
        $server_port = (int)val('SERVER_PORT', $_SERVER, $url_scheme === 'https' ? 443 :80);
        $env['SERVER_PORT'] = $server_port;

        // INPUT: The entire input.
        // Input stream (readable one time only; not available for multipart/form-data requests)
        switch (val('CONTENT_TYPE', $env)) {
            case 'application/json':
                $input_raw = @file_get_contents('php://input');
                $input = @json_decode($input_raw, true);
                break;
            default:
                $input = $_POST;
                $input_raw = null;
                break;
        }
        $env['INPUT'] = $input;
        $env['INPUT_RAW'] = $input_raw;

        // IP Address.
        // Load balancers set a different ip address.
        $ip = array_select(
            ['HTTP_X_ORIGINALLY_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'],
            $_SERVER,
            '127.0.0.1'
        );
        $env['REMOTE_ADDR'] = force_ipv4($ip);
        return $env;
    }

    /**
     * Check for specific environment overrides.
     *
     * @param array &$env The environment to override.
     */
    protected static function overrideEnvironment(&$env) {
        $get =& $env['QUERY'];

        // Check to override the method.
        if (isset($get['x-method'])) {
            $method = strtoupper($get['x-method']);

            $getMethods = array(self::METHOD_GET, self::METHOD_HEAD, self::METHOD_OPTIONS);

            // Don't allow get style methods to be overridden to post style methods.
            if (!in_array($env['REQUEST_METHOD'], $getMethods) || in_array($method, $getMethods)) {
                static::replaceEnv($env, 'REQUEST_METHOD', $method);
            } else {
                $env['X_METHOD_BLOCKED'] = true;
            }
            unset($get['x-method']);
        }

        // Force the path and extension to lowercase.
        $path = strtolower($env['PATH_INFO']);
        if ($path !== $env['PATH_INFO']) {
            static::replaceEnv($env, 'PATH_INFO', $path);
        }

        $ext = strtolower($env['EXT']);
        if ($ext !== $env['EXT']) {
            static::replaceEnv($env, 'EXT', $ext);
        }

        // Check to override the accepts header.
        if (isset(self::$knownExtensions[$ext]) && $env['HTTP_ACCEPT'] !== 'application/internal') {
            static::replaceEnv($env, 'HTTP_ACCEPT', self::$knownExtensions[$ext]);
        }
    }

    /**
     * Get a value from the environment or the entire environment.
     *
     * @param string|null $key The key to get or null to get the entire environment.
     * @param mixed $default The default value if {@link $key} is not found.
     * @return mixed|array Returns the value at {@link $key}, {$link $default} or the entire environment array.
     * @see Request::setEnv()
     */
    public function getEnv($key = null, $default = null) {
        if ($key === null) {
            return $this->env;
        }
        return val(strtoupper($key), $this->env, $default);
    }

    /**
     * Set a value from the environment or the entire environment.
     *
     * @param string|array $key The key to set or an array to set the entire environment.
     * @param mixed $value The value to set.
     * @return Request Returns $this for fluent calls.
     * @throws \InvalidArgumentException Throws an exception when {@link $key} is invalid.
     * @see Request::getEnv()
     */
    public function setEnv($key, $value = null) {
        if (is_string($key)) {
            $this->env[strtoupper($key)] = $value;
        } elseif (is_array($key)) {
            $this->env = $key;
        } else {
            throw new \InvalidArgumentException("Argument 1 must be either a string or array.", 422);
        }
        return $this;
    }

    /**
     * Replace an environment variable with another one and back up the old one in a *_RAW key.
     *
     * @param array &$env The environment array.
     * @param string $key The environment key.
     * @param mixed $value The new environment value.
     * @return mixed Returns the old value or null if there was no old value.
     */
    public static function replaceEnv(&$env, $key, $value) {
        $key = strtoupper($key);

        $result = null;
        if (isset($env[$key])) {
            $result = $env[$key];
            $env[$key.'_RAW'] = $result;
        }
        $env[$key] = $value;
        return $result;
    }

    /**
     * Restore an environment variable that was replaced with {@link Request::replaceEnv()}.
     *
     * @param array &$env The environment array.
     * @param string $key The environment key.
     * @return mixed Returns the current environment value.
     */
    public static function restoreEnv(&$env, $key) {
        $key = strtoupper($key);

        if (isset($env[$key.'_RAW'])) {
            $env[$key] = $env[$key.'_RAW'];
            unset($env[$key.'_RAW']);
            return $env[$key];
        } elseif (isset($env[$key])) {
            return $env[$key];
        }
        return null;
    }

    /**
     * Extract the headers from an array such as $_SERVER or the request's own $env.
     *
     * @param array $arr The array to extract.
     * @return array The extracted headers.
     */
    public static function extractHeaders($arr) {
        $result = [];

        foreach ($arr as $key => $value) {
            $key = strtoupper($key);
            if (strpos($key, 'X_') === 0 || strpos($key, 'HTTP_') === 0 || in_array($key, static::$specialHeaders)) {
                if ($key === 'HTTP_CONTENT_TYPE' || $key === 'HTTP_CONTENT_LENGTH') {
                    continue;
                }
                $result[$key] = $value;
            }
        }
        return $result;
    }

    /**
     * Retrieves all message headers.
     *
     * @return array Returns an associative array of the message's headers.
     * Each key represents a header name, and each value is an array of strings.
     */
    public function getHeaders() {
        $result = [];

        foreach ($this->env as $key => $value) {
            if (stripos($key, 'HTTP_') === 0 && !str_ends($key, '_RAW')) {
                $headerKey = static::normalizeHeaderName(substr($key, 5));
                $result[$headerKey][] = $value;
            }
        }
        return $result;
    }

    /**
     * Get the hostname of the request.
     *
     * @return string Returns the host.
     */
    public function getHost() {
        return (string)$this->env['SERVER_NAME'];
    }

    /**
     * Set the hostname of the request.
     *
     * @param string $host The hostname.
     * @return Request Returns $this for fluent calls.
     */
    public function setHost($host) {
        $this->env['SERVER_NAME'] = $host;
        return $this;
    }

    /**
     * Get the host and port, but only if the port is not the standard port for the request scheme.
     *
     * @return string Returns the host and port or just the host if this is the standard port.
     * @see Request::getHost()
     * @see Request::getPort()
     */
    public function getHostAndPort() {
        $host = $this->getHost();
        $port = $this->getPort();

        // Only append the port if it is non-standard.
        if (($port == 80 && $this->getScheme() === 'http') || ($port == 443 && $this->getScheme() === 'https')) {
            $port = '';
        } else {
            $port = ':'.$port;
        }
        return $host.$port;
    }

    /**
     * Get the ip address of the request.
     *
     * @return string Returns the current ip address.
     */
    public function getIP() {
        return (string)$this->env['REMOTE_ADDR'];
    }

    /**
     * Set the ip address of the request.
     *
     * @param string $ip The new ip address.
     * @return Request Returns $this for fluent calls.
     */
    public function setIP($ip) {
        $this->env['REMOTE_ADDR'] = $ip;
        return $this;
    }

    /**
     * Gets whether or not this is a DELETE request.
     *
     * @return bool Returns true if this is a DELETE request, false otherwise.
     */
    public function isDelete() {
        return $this->getMethod() === self::METHOD_DELETE;
    }

    /**
     * Gets whether or not this is a GET request.
     *
     * @return bool Returns true if this is a GET request, false otherwise.
     */
    public function isGet() {
        return $this->getMethod() === self::METHOD_GET;
    }

    /**
     * Gets whether or not this is a HEAD request.
     *
     * @return bool Returns true if this is a HEAD request, false otherwise.
     */
    public function isHead() {
        return $this->getMethod() === self::METHOD_HEAD;
    }

    /**
     * Gets whether or not this is an OPTIONS request.
     *
     * @return bool Returns true if this is an OPTIONS request, false otherwise.
     */
    public function isOptions() {
        return $this->getMethod() === self::METHOD_OPTIONS;
    }

    /**
     * Gets whether or not this is a PATCH request.
     *
     * @return bool Returns true if this is a PATCH request, false otherwise.
     */
    public function isPatch() {
        return $this->getMethod() === self::METHOD_PATCH;
    }

    /**
     * Gets whether or not this is a POST request.
     *
     * @return bool Returns true if this is a POST request, false otherwise.
     */
    public function isPost() {
        return $this->getMethod() === self::METHOD_POST;
    }

    /**
     * Gets whether or not this is a PUT request.
     *
     * @return bool Returns true if this is a PUT request, false otherwise.
     */
    public function isPut() {
        return $this->getMethod() === self::METHOD_PUT;
    }

    /**
     * Get the http method of the request.
     *
     * @return string Returns the http method of the request.
     */
    public function getMethod() {
        return (string)$this->env['REQUEST_METHOD'];
    }

    /**
     * Set the http method of the request.
     *
     * @param string $method The new request method.
     * @return Request Returns $this for fluent calls.
     */
    public function setMethod($method) {
        $this->env['REQUEST_METHOD'] = strtoupper($method);
        return $this;
    }

    /**
     * Gets the request path.
     *
     * Note that this returns the path without the file extension.
     * If you want the full path use {@link Request::getFullPath()}.
     *
     * @return string Returns the request path.
     */
    public function getPath() {
        return (string)$this->env['PATH_INFO'];
    }

    /**
     * Sets the request path.
     *
     * @param string $path The path to set.
     * @return Request Returns $this for fluent calls.
     */
    public function setPath($path) {
        $this->env['PATH_INFO'] = (string)$path;
        return $this;
    }

    /**
     * Get the file extension of the request.
     *
     * @return string Returns the file extension.
     */
    public function getExt() {
        return (string)$this->env['EXT'];
    }

    /**
     * Sets the file extension of the request.
     *
     * @param string $ext The file extension to set.
     * @return Request Returns $this for fluent calls.
     */
    public function setExt($ext) {
        if ($ext) {
            $this->env['EXT'] = '.'.ltrim($ext, '.');
        } else {
            $this->env['EXT'] = '';
        }
        return $this;
    }

    /**
     * Get the path and file extenstion.
     *
     * @return string Returns the path and file extension.
     * @see Request::setPathExt()
     */
    public function getPathExt() {
        return $this->env['PATH_INFO'].$this->env['EXT'];
    }

    /**
     * Set the path with file extension.
     *
     * @param string $path The path to set.
     * @return Request Returns $this for fluent calls.
     * @see Request::getPathExt()
     */
    public function setPathExt($path) {
        // Strip the extension from the path.
        if (substr($path, -1) !== '/' && ($pos = strrpos($path, '.')) !== false) {
            $ext = substr($path, $pos);
            $path = substr($path, 0, $pos);
            $this->env['EXT'] = $ext;
        } else {
            $this->env['EXT'] = '';
        }
        $this->env['PATH_INFO'] = $path;
        return $this;
    }

    /**
     * Get the full path of the request.
     *
     * The full path consists of the root + path + extension.
     *
     * @return string Returns the full path.
     */
    public function getFullPath() {
        return $this->getRoot().$this->getPathExt();
    }

    /**
     * Set the full path of the request.
     *
     * The full path consists of the root + path + extension.
     * This method examins the current root to see if the root can still be used or should be removed.
     * Special care must be taken when calling this method to make sure you don't remove the root unintentionally.
     *
     * @param string $fullPath The full path to set.
     * @return Request Returns $this for fluent calls.
     */
    public function setFullPath($fullPath) {
        $fullPath = '/'.ltrim($fullPath, '/');

        // Try stripping the root out of the path first.
        $root = (string)$this->getRoot();

        if ($root &&
            strpos($fullPath, $root) === 0 &&
            (strlen($fullPath) === strlen($root) || substr($fullPath, strlen($root), 1) === '/')) {

            $this->setPathExt(substr($fullPath, strlen($root)));
        } else {
            $this->setRoot('');
            $this->setPathExt($fullPath);
        }

        return $this;
    }

    /**
     * Normalize a header field name to follow the general HTTP header `Capital-Dash-Separated` convention.
     *
     * @param string $name The header name to normalize.
     * @return string Returns the normalized header name.
     */
    public static function normalizeHeaderName($name) {
        $result = str_replace(' ', '-', ucwords(str_replace(['-', '_'], ' ', strtolower($name))));
        return $result;
    }

    /**
     * Gets the port.
     *
     * @return int Returns the port.
     */
    public function getPort() {
        return (int)$this->env['SERVER_PORT'];
    }

    /**
     * Sets the port.
     *
     * Setting the port to 80 or 443 will also set the scheme to http or https respectively.
     *
     * @param int $port The port to set.
     * @return Request Returns $this for fluent calls.
     */
    public function setPort($port) {
        $this->env['SERVER_PORT'] = $port;

        // Override the scheme for standard ports.
        if ($port === 80) {
            $this->setScheme('http');
        } elseif ($port === 443) {
            $this->setScheme('https');
        }

        return $this;
    }

    /**
     * Get an item from the query string array.
     *
     * @param string|null $key Either a string key or null to get the entire array.
     * @param mixed|null $default The default to return if {@link $key} is not found.
     * @return string|array|null Returns the query string value or the query string itself.
     * @see Request::setQuery()
     */
    public function getQuery($key = null, $default = null) {
        if ($key === null) {
            return $this->env['QUERY'];
        }
        return isset($this->env['QUERY'][$key]) ? $this->env['QUERY'][$key] : $default;
    }

    /**
     * Set an item from the query string array.
     *
     * @param string|array $key Either a string key or an array to set the entire query string.
     * @param mixed|null $value The value to set.
     * @return Request Returns $this for fluent call.
     * @throws \InvalidArgumentException Throws an exception when {@link $key is invalid}.
     * @see Request::getQuery()
     */
    public function setQuery($key, $value = null) {
        if (is_string($key)) {
            $this->env['QUERY'][$key] = $value;
        } elseif (is_array($key)) {
            $this->env['QUERY'] = $key;
        } else {
            throw new \InvalidArgumentException("Argument 1 must be a string or array.", 422);
        }
        return $this;
    }

    /**
     * Get an item from the input array.
     *
     * @param string|null $key Either a string key or null to get the entire array.
     * @param mixed|null $default The default to return if {@link $key} is not found.
     * @return string|array|null Returns the query string value or the input array itself.
     */
    public function getInput($key = null, $default = null) {
        if ($key === null) {
            return $this->env['INPUT'];
        }
        return isset($this->env['INPUT'][$key]) ? $this->env['INPUT'][$key] : $default;
    }

    /**
     * Set an item from the input array.
     *
     * @param string|array $key Either a string key or an array to set the entire input.
     * @param mixed|null $value The value to set.
     * @return Request Returns $this for fluent call.
     * @throws \InvalidArgumentException Throws an exception when {@link $key is invalid}.
     */
    public function setInput($key, $value = null) {
        if (is_string($key)) {
            $this->env['INPUT'][$key] = $value;
        } elseif (is_array($key)) {
            $this->env['INPUT'] = $key;
        } else {
            throw new \InvalidArgumentException("Argument 1 must be a string or array.", 422);
        }
        return $this;
    }

    /**
     * Gets the query on input depending on the http method.
     *
     * @param string|null $key Either a string key or null to get the entire array.
     * @param mixed|null $default The default to return if {@link $key} is not found.
     * @return mixed|array Returns the value at {@link $key} or the entire array.
     * @see Request::setData()
     * @see Request::getInput()
     * @see Request::getQuery()
     * @see Request::hasInput()
     */
    public function getData($key = null, $default = null) {
        if ($this->hasInput()) {
            return $this->getInput($key, $default);
        } else {
            return $this->getQuery($key, $default);
        }
    }

    /**
     * Sets the query on input depending on the http method.
     *
     * @param string|array $key Either a string key or an array to set the entire data.
     * @param mixed|null $value The value to set.
     * @return Request Returns $this for fluent call.
     * @see Request::getData()
     * @see Request::setInput()
     * @see Request::setQuery()
     * @see Request::hasInput()
     */
    public function setData($key, $value = null) {
        if ($this->hasInput()) {
            $this->setInput($key, $value);
        } else {
            $this->setQuery($key, $value);
        }
        return $this;
    }

    /**
     * Returns true if an http method has input (a post body).
     *
     * @param string $method The http method to test.
     * @return bool Returns true if the http method has input, false otherwise.
     */
    public function hasInput($method = '') {
        if (!$method) {
            $method = $this->getMethod();
        }

        switch (strtoupper($method)) {
            case self::METHOD_GET:
            case self::METHOD_DELETE:
            case self::METHOD_HEAD:
            case self::METHOD_OPTIONS:
                return false;
        }
        return true;
    }

    /**
     * Get the root directory (SCRIPT_NAME) of the request.
     *
     * @return Returns the root directory of the request as a string.
     * @see Request::setRoot()
     */
    public function getRoot() {
        return (string)$this->env['SCRIPT_NAME'];
    }

    /**
     * Set the root directory (SCRIPT_NAME) of the request.
     *
     * This method will modify the set root to include a leading slash if it does not have one.
     * A root of just "/" will be coerced to an empty string.
     *
     * @param string $root The new root directory.
     * @return Request Returns $this for fluent calls.
     * @see Request::getRoot()
     */
    public function setRoot($root) {
        $value = trim($root, '/');
        if ($value) {
            $value = '/'.$value;
        }
        $this->env['SCRIPT_NAME'] = $value;
        return $this;
    }

    /**
     * Get the request scheme.
     *
     * The request scheme is usually http or https.
     *
     * @return string Retuns the scheme.
     * @see Request::setScheme()
     */
    public function getScheme() {
        return (string)$this->env['URL_SCHEME'];
    }

    /**
     * Set the request scheme.
     *
     * The request scheme is usually http or https.
     *
     * @param string $scheme The new scheme to set.
     * @return Request Returns $this for fluent calls.
     * @see Request::getScheme()
     */
    public function setScheme($scheme) {
        $this->env['URL_SCHEME'] = $scheme;
        return $this;
    }

    /**
     * Get the full url of the request.
     *
     * @return string Returns the full url of the request.
     * @see Request::setUrl()
     */
    public function getUrl() {
        $query = $this->getQuery();
        return
            $this->getScheme().
            '://'.
            $this->getHostAndPort().
            $this->getRoot().
            $this->getPath().
            (!empty($query) ? '?'.http_build_query($query) : '');
    }

    /**
     * Set the full url of the request.
     *
     * @param string $url The new url.
     * @return Request $this Returns $this for fluent calls.
     * @see Request::getUrl()
     */
    public function setUrl($url) {
        // Parse the url and set the individual components.
        $url_parts = parse_url($url);

        if (isset($url_parts['scheme'])) {
            $this->setScheme($url_parts['scheme']);
        }

        if (isset($url_parts['host'])) {
            $this->setHost($url_parts['host']);
        }

        if (isset($url_parts['port'])) {
            $this->setPort($url_parts['port']);
        } elseif (isset($url_parts['scheme'])) {
            $this->setPort($this->getScheme() === 'https' ? 443 : 80);
        }

        if (isset($url_parts['path'])) {
            $this->setFullPath($url_parts['path']);
        }

        if (isset($url_parts['query'])) {
            parse_str($url_parts['query'], $query);
            if (is_array($query)) {
                $this->setQuery($query);
            }
        }
        return $this;
    }

    /**
     * Construct a url on the current site.
     *
     * @param string $path The path of the url.
     * @param mixed $domain Whether or not to include the domain. This can be one of the following.
     * - false: The domain will not be included.
     * - true: The domain will be included.
     * - http: Force http.
     * - https: Force https.
     * - //: A schemeless domain will be included.
     * - /: Just the path will be returned.
     * @return string Returns the url.
     */
    public function makeUrl($path, $domain = false) {
        if (!$path) {
            $path = $this->getPath();
        }

        // Check for a specific scheme.
        $scheme = $this->getScheme();
        if ($domain === 'http' || $domain === 'https') {
            $scheme = $domain;
            $domain = true;
        }

        if ($domain === true) {
            $prefix = $scheme.'://'.$this->getHostAndPort().$this->getRoot();
        } elseif ($domain === false) {
            $prefix = $this->getRoot();
        } elseif ($domain === '//') {
            $prefix = '//'.$this->getHostAndPort().$this->getRoot();
        } else {
            $prefix = '';
        }

        return $prefix.'/'.ltrim($path, '/');
    }

    /**
     * Split the file extension off a path.
     *
     * @param string $path The path to split.
     * @return array Returns an array in the form `['path', 'ext']`.
     */
    protected static function splitPathExt($path) {
        if (substr($path, -1) !== '/' && ($pos = strrpos($path, '.')) !== false) {
            $ext = substr($path, $pos);
            $path = substr($path, 0, $pos);
            return [$path, $ext];
        } else {
            return [$path, ''];
        }
    }

    /**
     * Specify data which should be serialized to JSON.
     *
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     */
    public function jsonSerialize() {
        return $this->env;
    }
}


<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2014 Vanilla Forums Inc.
 * @license MIT
 */

namespace Garden\Db;

/**
 * A helper class for creating database tables.
 */
class DbDef implements \JsonSerializable {
    /// Properties ///

    /**
     * @var Db The database connection to send definitions to.
     */
    protected $db;

    /**
     * @var array The columns that need to be set in the table.
     */
    protected $columns;

    /**
     * @var array Options to send to the table definitaion call.
     */
    protected $options;

    /**
     *
     * @var string The name of the currently working table.
     */
    protected $table;

    /**
     * @var array An array of indexes.
     */
    protected $indexes;

    /// Methods ///

    /**
     * Initialize an instance of the {@link DbDef} class.
     *
     * @param Db $db The database to execute against.
     */
    public function __construct($db) {
        $this->db = $db;
        $this->reset();
    }

    /**
     * Reset the internal state of this object so that it can be re-used.
     *
     * @return DbDef Returns $this for fluent calls.
     */
    public function reset() {
        $this->table = null;
        $this->columns = [];
        $this->indexes = [];
        $this->options = [];

        return $this;
    }

    /**
     * Define a column.
     *
     * @param string $name The column name.
     * @param string $type The column type.
     * @param mixed $nullDefault Whether the column is required or it's default.
     *
     * null|true
     * : The column is not required.
     * false
     * : The column is required.
     * Anything else
     * : The column is required and this is its default.
     *
     * @param string|array $index The index that the column participates in.
     * @return DbDef
     */
    public function column($name, $type, $nullDefault = false, $index = null) {
        $this->columns[$name] = $this->columnDef($type, $nullDefault);

        $index = (array)$index;
        foreach ($index as $typeStr) {
            if (strpos($typeStr, '.') === false) {
                $indexType = $typeStr;
                $suffix = '';
            } else {
                list($indexType, $suffix) = explode('.', $typeStr);
            }
            $this->index($name, $indexType, $suffix);
        }

        return $this;
    }

    /**
     * Get an array column def from a structured function call.
     *
     * @param string $type The database type of the column.
     * @param mixed $nullDefault Whether or not to allow null or the default value.
     *
     * null|true
     * : The column is not required.
     * false
     * : The column is required.
     * Anything else
     * : The column is required and this is its default.
     *
     * @return array Returns the column def as an array.
     */
    protected function columnDef($type, $nullDefault = false) {
        $column = ['type' => $type];

        if ($nullDefault === null || $nullDefault == true) {
            $column['required'] = false;
        }
        if ($nullDefault === false) {
            $column['required'] = true;
        } else {
            $column['required'] = true;
            $column['default'] = $nullDefault;
        }

        return $column;
    }

    /**
     * Define the primary key in the database.
     *
     * @param string $name The name of the column.
     * @param string $type The datatype for the column.
     * @return DbDef
     */
    public function primaryKey($name, $type = 'int') {
        $column = $this->columnDef($type, false);
        $column['autoincrement'] = true;
        $column['primary'] = true;

        $this->columns[$name] = $column;

        // Add the pk index.
        $this->index($name, Db::INDEX_PK);

        return $this;
    }

    /**
     * Execute the table def against the database.
     *
     * @param bool $reset Whether or not to reset the db def upon completion.
     * @return DbDef $this Returns $this for fluent calls.
     */
    public function exec($reset = true) {
        $this->db->setTableDef(
            $this->table,
            $this->jsonSerialize(),
            $this->options
        );

        if ($reset) {
            $this->reset();
        }

        return $this;
    }

    /**
     * Set the name of the table.
     *
     * @param string|null $name The name of the table.
     * @return DbDef|string Returns $this for fluent calls.
     */
    public function table($name = null) {
        if ($name !== null) {
            $this->table = $name;
            return $this;
        }
        return $this->table;
    }

    /**
     * Add or update an index.
     *
     * @param string|array $columns An array of columns or a single column name.
     * @param string $type One of the `Db::INDEX_*` constants.
     * @param string $suffix An index suffix to group columns together in an index.
     * @return DbDef Returns $this for fluent calls.
     */
    public function index($columns, $type, $suffix = '') {
        $type = strtolower($type);
        $columns = (array)$columns;
        $suffix = strtolower($suffix);

        // Look for a current index row.
        $currentIndex = null;
        foreach ($this->indexes as $i => $index) {
            if ($type !== $index['type']) {
                continue;
            }

            $indexSuffix = val('suffix', $index, '');

            if ($type === Db::INDEX_PK ||
                ($type === Db::INDEX_UNIQUE && $suffix == $indexSuffix) ||
                ($type === Db::INDEX_IX && $suffix && $suffix == $indexSuffix) ||
                ($type === Db::INDEX_IX && !$suffix && array_diff($index['columns'], $columns) == [])
            ) {
                $currentIndex =& $this->indexes[$i];
                break;
            }
        }

        if ($currentIndex) {
            $currentIndex['columns'] = array_unique(array_merge($currentIndex['columns'], $columns));
        } else {
            $indexDef = [
                'type' => $type,
                'columns' => $columns,
                'suffix' => $suffix
            ];
            if ($type === Db::INDEX_PK) {
                $this->indexes[Db::INDEX_PK] = $indexDef;
            } else {
                $this->indexes[] = $indexDef;
            }
        }

        return $this;
    }

    /**
     * Set an option that will be passed on to the final {@link Db::setTableDef()} call.
     *
     * @param string $key The option key.
     * @param mixed $value The option value.
     * @return DbDef Returns $this for fluent calls.
     */
    public function option($key, $value) {
        $this->options[$key] = $value;
        return $this;
    }

    /**
     * Specify data which should be serialized to JSON.
     *
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by {@link json_encode()},
     * which is a value of any type other than a resource.
     */
    public function jsonSerialize() {
        return [
            'name' => $this->table,
            'columns' => $this->columns,
            'indexes' => $this->indexes
        ];
    }

    /**
     * Get the db connection to send definitions to.
     *
     * @return Db Returns the db connection.
     * @see DbDef::setDb()
     */
    public function getDb() {
        return $this->db;
    }

    /**
     * Set the db connection to send definitions to.
     *
     * @param Db $db The new database connection.
     * @see DbDef::getDef()
     */
    public function setDb($db) {
        $this->db = $db;
    }
}


<?php

namespace WyriHaximus\React\ChildProcess\Pool;

use Evenement\EventEmitterTrait;
use React\Promise\PromiseInterface;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Message;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Rpc;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;

class Worker implements WorkerInterface
{
    use EventEmitterTrait;

    /**
     * @var Messenger
     */
    protected $messenger;

    /**
     * @var bool
     */
    protected $busy = false;

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

    /**
     * @param Rpc $rpc
     * @return PromiseInterface
     */
    public function rpc(Rpc $rpc)
    {
        $this->busy = true;
        return $this->messenger->rpc($rpc)->always(function () {
            $this->busy = false;
            $this->emit('done', [$this]);
        });
    }

    /**
     * @param Message $message
     */
    public function message(Message $message)
    {
        $this->messenger->message($message);
    }

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

    /*
     * @return PromiseInterface
     */
    public function terminate()
    {
        $this->busy = true;
        $this->emit('terminating', [$this]);
        return $this->messenger->softTerminate();
    }
}


<?php
namespace Gverschuur\RobotsTxt\Controllers;

use Illuminate\Routing\Controller;

class RobotsTxtController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Robots Controller
    |--------------------------------------------------------------------------
    |
    | Returns the correct content for a robots.txt based on the environment.
    |
    */

    /**
     * By default, the production environment allows all, and every other environment allows none
     * Custom paths can be set by publishing and editing the config file
     * @return Illuminate\Http\Response HTTP status 200 with a text/plain content type robots.txt output
     */
    public function index() {
        /**
         * Note that the config is originally loaded from this package's config file, but a config file can be published to the Laravel config dir.
         * If so, due to the nature of the config setup and Larvel's config merge, the original config gets completely overwritten.
         */
        $envs = config('robots-txt.paths');
        $robots = '';
        $env = config('app.env');

        // if no env is set, or one of the set envs cannot be matched against the current env, use the default
        if($envs === null || !array_key_exists($env, $envs)) {
            $robots = $this->defaultRobot();
        } else {
            // for each user agent, get the user agent name and the paths for the agent, appending them to the result string
            $agents = $envs[$env];
            foreach($agents as $name => $paths) {
                $robot = 'User-agent: ' . $name . PHP_EOL;

                foreach ($paths as $path) {
                    $robot .= 'Disallow: ' . $path . PHP_EOL;
                }

                // append this user agent and paths to the final output
                $robots .= $robot . PHP_EOL;
            }
        }

        // output the entire robots.txt
        return \Response::make($robots, 200, array('content-type' => 'text/plain'));
    }

    /**
     * Default 'Disallow /' for every robot
     * @return string user agent and disallow string
     */
    protected function defaultRobot() {
        return 'User-agent: *' . PHP_EOL . 'Disallow: /';
    }
}


<?php

namespace Judopay;

use Judopay\Exception\ValidationError;

/**
 * Base model class
 * @package Judopay
 **/
class Model
{
    const JUDO_ID = 'judoId';
    /**
     * Request object
     * @var Request
     **/
    protected $request;
    /**
     * Resource path (e.g. /transactions/payments)
     * @var string
     **/
    protected $resourcePath;
    /**
     * Valid API methods for this model
     * e.g. array('all', 'create')
     * @var array
     **/
    protected $validApiMethods;
    /**
     * Attributes with expected data types
     * e.g. array('yourConsumerReference' => DataType::TYPE_STRING)
     * @var array
     **/
    protected $attributes = array();
    /**
     * Attributes with values
     * e.g. array('yourConsumerReference' => '123456')
     * @var array
     **/
    protected $attributeValues = array();
    /**
     * Attributes that must be present before submission to the API
     * e.g. array('yourConsumerReference', 'judoId')
     * @var array
     **/
    protected $requiredAttributes = array();

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Retrieve a list of records
     * @param  int    $offset   The start point in the sorted list of records from which the results set will start
     * @param  int    $pageSize The number of records to display per page
     * @param  string $sort     Determines how the list is sorted (time-descending or time-ascending)
     * @return array API response
     **/
    public function all($offset = 0, $pageSize = 10, $sort = 'time-descending')
    {
        $this->checkApiMethodIsSupported(__FUNCTION__);
        $pagingOptions = array(
            'offset'   => $offset,
            'pageSize' => $pageSize,
            'sort'     => $sort,
        );

        // Ressource Path with options
        $uri = $this->resourcePath.'?'.http_build_query($pagingOptions);

        $response = $this->request->get(
            $uri
            // No attributes
        );

        return $response->json();
    }

    /**
     * Retrieve a specific record
     * @param  int $resourceId
     * @return array API response
     * @author
     **/
    public function find($resourceId)
    {
        $this->checkApiMethodIsSupported(__FUNCTION__);

        return $this->request
            ->get($this->resourcePath.'/'.(int)$resourceId)
            ->json();
    }

    /**
     * Create a new record
     * @return array API response
     **/
    public function create()
    {
        $this->checkApiMethodIsSupported(__FUNCTION__);
        $this->checkJudoId();
        $this->checkRequiredAttributes($this->attributeValues);

        $response = $this->request->post(
            $this->resourcePath,
            $this->attributeValues
        );

        return $response->json();
    }

    /**
     * Validate card payment details without making a payment
     * @return array $response
     **/
    public function validate()
    {
        $this->checkApiMethodIsSupported(__FUNCTION__);
        $this->checkJudoId();
        $this->checkRequiredAttributes($this->attributeValues);

        $validateResourcePath = $this->resourcePath.'/validate';

        $response = $this->request->post(
            $validateResourcePath,
            $this->attributeValues
        );

        return $response->json();
    }

    /**
     * Get a single attribute value
     * @param string $attribute Attribute name
     * @return string Attribute value
     **/
    public function getAttributeValue($attribute)
    {
        if (!array_key_exists($attribute, $this->attributeValues)) {
            return null;
        }

        return $this->attributeValues[$attribute];
    }

    /**
     * Get all attribute values
     * @return array Attribute values
     **/
    public function getAttributeValues()
    {
        return $this->attributeValues;
    }

    /**
     * Set attribute values
     * @param array $values Attribute values
     * @return void
     **/
    public function setAttributeValues($values)
    {
        foreach ($values as $key => $value) {
            // Does the attribute exist?
            if (!array_key_exists($key, $this->attributes)) {
                continue;
            }

            // Coerce to the right type if required
            $targetDataType = $this->attributes[$key];
            $this->attributeValues[$key] = DataType::coerce(
                $targetDataType,
                $value
            );
        }
    }

    /**
     * Check if the specified method name is supported by this model
     * @param string $methodName Method name
     * @return void
     **/
    protected function checkApiMethodIsSupported($methodName)
    {
        if (empty($this->validApiMethods)
            || !in_array($methodName, $this->validApiMethods)
        ) {
            throw new ValidationError('API method is not supported');
        }
    }

    /**
     * Check if request data contains all of the required attributes to create a new record
     * @param array $data Request data
     * @return bool
     */
    protected function checkRequiredAttributes($data)
    {
        $existingAttributes = array_keys($data);
        $errors = array();
        foreach ($this->requiredAttributes as $requiredAttribute) {
            if (!in_array($requiredAttribute, $existingAttributes)
                || $data[$requiredAttribute] === ''
                || $data[$requiredAttribute] === null
            ) {
                $errors[] = $requiredAttribute.' is missing or empty';
            }
        }

        if (count($errors) > 0) {
            throw new ValidationError('Missing required fields', $errors);
        }

        return true;
    }

    /**
     * If a Judo ID is not set, use the value from configuration
     * @return void
     **/
    protected function checkJudoId()
    {
        $judoId = $this->getAttributeValue(static::JUDO_ID);
        if (!empty($judoId)) {
            return;
        }

        $configuration = $this->request->getConfiguration();
        $this->attributeValues[static::JUDO_ID] = $configuration->get(static::JUDO_ID);
    }
}


<?php

namespace Judopay\Exception;

use GuzzleHttp\Message\ResponseInterface;
use Judopay\Helper\ArrayHelper;

class ApiException extends \RuntimeException
{
    const MESSAGE = 'JudoPay ApiException (status code %d, error code %d, category %d) %s%s';
    const CATEGORY_UNKNOWN = 0;
    const CATEGORY_REQUEST = 1;
    const CATEGORY_MODEL = 2;
    const CATEGORY_CONFIG = 3;
    const CATEGORY_PROCESSING = 4;
    const CATEGORY_EXCEPTION = 5;
    /** @var FieldError[] */
    protected $fieldErrors = array();
    /** @var int */
    protected $statusCode;
    /** @var int */
    protected $category;

    /**
     * Factory method
     * @param ResponseInterface $response
     * @return static
     */
    public static function factory(ResponseInterface $response)
    {
        $parsedBody = $response->json();

        $category = ArrayHelper::get(
            $parsedBody,
            'category',
            static::CATEGORY_UNKNOWN
        );

        $message = ArrayHelper::get($parsedBody, 'message', get_called_class());

        $errorCode = ArrayHelper::get($parsedBody, 'code', 0);

        $fieldErrors = array();

        if (isset($parsedBody['details']['receiptId'])) {
            //Convert receiptId to a non scientific notation
            $fReceiptId = sprintf('%f', $parsedBody['details']['receiptId']);
            $iReceiptId = rtrim($fReceiptId, '0');
            //De-deduplicate format
            $message .= ' Duplicate receipt id: '. $iReceiptId;
        } elseif (isset($parsedBody['details']) && is_array($parsedBody['details'])) {
            //Regular 'model errors' format
            foreach ($parsedBody['details'] as $rawFieldError) {
                $fieldErrors[] = new FieldError(
                    ArrayHelper::get($rawFieldError, 'message', ''),
                    ArrayHelper::get($rawFieldError, 'code', 0),
                    ArrayHelper::get($rawFieldError, 'fieldName', ''),
                    ArrayHelper::get($rawFieldError, 'detail')
                );
            }
        }

        $statusCode = $response->getStatusCode();

        return new static(
            $message,
            $errorCode,
            $statusCode,
            $category,
            $fieldErrors
        );
    }

    /**
     * ApiException constructor.
     * @param string       $message     User message
     * @param int          $code        Error code
     * @param int          $statusCode  HTTP status code
     * @param int          $category    Error category
     * @param FieldError[] $fieldErrors Fields errors
     */
    public function __construct(
        $message,
        $code = 0,
        $statusCode = 0,
        $category = self::CATEGORY_UNKNOWN,
        $fieldErrors = array()
    ) {
        $this->message = $message;
        $this->code = $code;
        $this->statusCode = $statusCode;
        $this->category = $category;
        $this->fieldErrors = $fieldErrors;
    }

    /**
     * Summary of the error
     * @return string
     */
    public function getSummary()
    {
        return sprintf(
            static::MESSAGE,
            $this->getHttpStatusCode(),
            $this->getCode(),
            $this->getCategory(),
            $this->getMessage(),
            $this->getDetailsSummary()
        );
    }

    /**
     * @inheritdoc
     */
    public function __toString()
    {
        return $this->getSummary();
    }

    /**
     * HTTP status code of error
     * @return int
     */
    public function getHttpStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * Error category
     * @return int
     */
    public function getCategory()
    {
        return $this->category;
    }

    protected function getDetailsSummary()
    {
        return empty($this->fieldErrors)
            ? ""
            : ' Fields errors: '.PHP_EOL.join(PHP_EOL, $this->fieldErrors);
    }

    /**
     * @return FieldError[]
     */
    public function getFieldErrors()
    {
        return $this->fieldErrors;
    }
}


<?php

use Judopay\Configuration;
use Pimple\Container;

/**
 * Main Judopay wrapper
 * Handle initial configuration and build service objects
 * @package      Judopay
 */
class Judopay
{
    const SDK_VERSION = '2.2.0';
    const API_VERSION = '5.5.0';
    /**
     * Pimple DI container
     * @var object Pimple\Container
     **/
    protected $container;

    /**
     * @param array $settings Configuration settings (e.g. [judoId] => '123-456')
     **/
    public function __construct($settings = null)
    {
        // Create new DI container
        $this->container = new Container();

        // Create config object
        $this->container['configuration'] = function () use ($settings) {
            return new Configuration($settings);
        };

        // Create request factory
        $this->container['request'] = $this->container->factory(
            function ($c) {
                /** @var Configuration $configuration */
                $configuration = $c['configuration'];
                $request = new \Judopay\Request($configuration);
                $request->setClient(new \Judopay\Client());

                return $request;
            }
        );
    }

    /**
     * Get an object from the DI container
     * @param string Object name
     * @return object
     **/
    public function get($objName)
    {
        return $this->container[$objName];
    }

    /**
     * Build a new model object
     * @param string $modelName
     * @return object
     */
    public function getModel($modelName)
    {
        // If the model is already defined in the container, just return it
        if (isset($this->container[$modelName])) {
            return $this->get($modelName);
        }

        // Set up the model in the DI container
        $request = $this->get('request');
        $this->container[$modelName] = $this->container->factory(
            function () use ($modelName, $request) {
                $modelClassName = '\Judopay\Model\\'.ucfirst($modelName);

                return new $modelClassName($request);
            }
        );

        return $this->get($modelName);
    }
}


<?php

namespace Judopay\Exception;

class ValidationError extends \RuntimeException
{
    /** @var array */
    protected $modelErrors;

    /**
     * ValidationError constructor.
     * @param string $message
     * @param array  $modelErrors
     */
    public function __construct($message, $modelErrors = array())
    {
        $this->message = $message;
        $this->modelErrors = $modelErrors;
    }

    /**
     * Returns string representation of error
     * @return string
     */
    public function getSummary()
    {
        // As a sensible default, use the class name
        $message = get_class($this);

        // Append model errors summary if applicable
        $modelErrorSummary = $this->getModelErrorSummary();
        if (!empty($modelErrorSummary)) {
            $message .= ' ('.$modelErrorSummary.')';
        }

        return $message;
    }

    /** @inheritdoc */
    public function __toString()
    {
        return $this->getSummary();
    }

    /**
     * Array of model errors
     * @return array
     */
    public function getModelErrors()
    {
        return $this->modelErrors;
    }

    /**
     * String representation of Model errors
     * @return null|string
     */
    protected function getModelErrorSummary()
    {
        if (!is_array($this->modelErrors)) {
            return null;
        }

        return join('; ', $this->modelErrors);
    }
}


<?php

namespace Judopay;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Message\FutureResponse;
use GuzzleHttp\Message\Request as GuzzleRequest;
use GuzzleHttp\Ring\Future\FutureArray;
use Judopay\Exception\ApiException;

class Request
{
    /** @var Configuration */
    protected $configuration;
    /** @var  Client */
    protected $client;

    public function __construct(Configuration $configuration)
    {
        $this->configuration = $configuration;
    }

    public function setClient(Client $client)
    {
        $this->client = $client;

        // Set SSL connection
        $this->client->setDefaultOption(
            'verify',
            __DIR__.'/../../cert/digicert_sha256_ca.pem'
        );
    }

    /**
     * Make a GET request to the specified resource path
     * @param string $resourcePath
     * @throws ApiException
     * @return FutureArray|FutureResponse
     */
    public function get($resourcePath)
    {
        $endpointUrl = $this->configuration->get('endpointUrl');

        $request = $this->client->createRequest(
            'GET',
            $endpointUrl.'/'.$resourcePath,
            [
                'json' => null
            ]
        );

        return $this->send($request);
    }

    /**
     * Make a POST request to the specified resource path
     * @param string $resourcePath
     * @param array  $data
     * @return FutureArray|FutureResponse
     */
    public function post($resourcePath, $data)
    {
        $endpointUrl = $this->configuration->get('endpointUrl');

        $request = $this->client->createRequest(
            'POST',
            $endpointUrl.'/'.$resourcePath,
            [
                'json' => $data
            ]
        );

        return $this->send($request);
    }

    public function setRequestHeaders(GuzzleRequest $request)
    {
        $request->setHeader('api-version', $this->configuration->get('apiVersion'));
        $request->setHeader('Accept', 'application/json; charset=utf-8');
        $request->setHeader('Content-Type', 'application/json');
        $request->setHeader('User-Agent', $this->configuration->get('userAgent'));
    }

    public function setRequestAuthentication(GuzzleRequest $request)
    {
        $this->configuration->validate();
        $oauthAccessToken = $this->configuration->get('oauthAccessToken');

        // Do we have an oAuth2 access token?
        if (!empty($oauthAccessToken)) {
            $request->setHeader('Authorization', 'Bearer ' . $oauthAccessToken);
        } else {
            // Otherwise, use basic authentication
            $basicAuth =  $this->configuration->get('apiToken'). ":" . $this->configuration->get('apiSecret');
            $request->setHeader('Authorization', 'Basic ' . base64_encode($basicAuth));
        }
    }

    /**
     * Configuration getter
     * @return Configuration
     */
    public function getConfiguration()
    {
        return $this->configuration;
    }

    /**
     * @param GuzzleRequest $guzzleRequest
     * @throws ApiException
     * @return FutureArray|FutureResponse
     */
    protected function send(GuzzleRequest $guzzleRequest)
    {
        $this->setRequestHeaders($guzzleRequest);
        $this->setRequestAuthentication($guzzleRequest);

        try {
            $guzzleResponse = $this->client->send($guzzleRequest);
        } catch (BadResponseException $e) {
            // Guzzle throws an exception when it encounters a 4xx or 5xx error
            // Rethrow the exception so we can raise our custom exception classes
            throw ApiException::factory($e->getResponse());
        }

        return $guzzleResponse;
    }
}


<?php

namespace spec\Judopay\Model;

use Judopay\Model\CardPayment;
use Tests\Builders\CardPaymentBuilder;

class CardPaymentSpec extends ModelObjectBehavior
{
    public function it_is_initializable()
    {
        $this->shouldHaveType('Judopay\Model\CardPayment');
    }

    public function it_should_create_a_new_payment()
    {
        $this->beConstructedWith(
            $this->concoctRequest('card_payments/create.json')
        );

        $modelBuilder = new CardPaymentBuilder();
        /** @var CardPayment|CardPaymentSpec $this */
        $this->setAttributeValues(
            $modelBuilder->compile()->getAttributeValues()
        );
        $output = $this->create();

        $output->shouldBeArray();
        $output['result']->shouldEqual('Success');
    }

    public function it_should_raise_an_error_when_required_fields_are_missing()
    {
        $this->beConstructedWith(
            $this->concoctRequest('card_payments/create.json')
        );
        $this->shouldThrow('\Judopay\Exception\ValidationError')->during(
            'create'
        );
    }

    public function it_should_use_the_configured_judo_id_if_one_is_not_provided()
    {
        $this->beConstructedWith(
            $this->concoctRequest('card_payments/create.json')
        );

        $modelBuilder = new CardPaymentBuilder();

        // Set an empty Judo ID to make sure the config value is used
        $modelBuilder->unsetAttribute('judoId');
        /** @var CardPayment|CardPaymentSpec $this */
        $this->setAttributeValues(
            $modelBuilder->compile()->getAttributeValues()
        );
        $this->create();

        $this->getAttributeValue('judoId')->shouldEqual('123-456');
    }

    // Generic model methods
    public function it_coerces_attributes_into_the_correct_data_type()
    {
        $input = array(
            'yourPaymentMetaData' => (object)array('val' => 'an unexpected string'),
            'judoId'              => 'judo123',
            'amount'              => '123.23',
        );

        $expectedOutput = array(
            'yourPaymentMetaData' => (object)array('val' => 'an unexpected string'),
            'judoId'              => 'judo123',
            'amount'              => 123.23,
        );

        /** @var CardPayment|CardPaymentSpec $this */
        $this->setAttributeValues($input);
        $this->getAttributeValues()->shouldBeLike($expectedOutput);
    }

    public function it_should_baulk_at_very_unusual_float_values()
    {
        $input = array(
            'amount' => '123.23GBP',
        );

        $this->shouldThrow('Judopay\Exception\ValidationError')
            ->during('setAttributeValues', array($input));
    }
}


<?php
namespace Judopay\Model\Inner;

use Judopay\Exception\ValidationError;
use Judopay\Helper\ArrayHelper;

abstract class TransmittedField
{
    const ERROR_MESSAGE_INVALID_JSON = 'Can\'t decode %s object from JSON';
    const ERROR_MESSAGE_CORRUPTED_OBJECT
        = 'You passed wrong value to the %1$s. Please pass array, json-encoded string or %1$s object';
    /** @var  string Field name and wrapper */
    static protected $fieldName;
    /** @var array Field data */
    protected $data = array();
    /** @var array Attributes that must be present in field */
    protected $requiredAttributes = array();

    /**
     * Factory method
     * @param array|string|static|\StdClass $value field data
     * @return static
     */
    public static function factory($value)
    {
        // if user passes same object
        if ($value instanceof static) {
            return $value;
        }

        // if user passes json string
        if (is_string($value) && (($value = json_decode($value, true)) == false)) {
            throw new ValidationError(
                sprintf(static::ERROR_MESSAGE_INVALID_JSON, get_called_class())
            );
        }

        //if user passes StdClass
        if (is_object($value)) {
            $value = (array)$value;
        }

        // check if we have array at last
        if (!is_array($value)) {
            throw new ValidationError(
                sprintf(static::ERROR_MESSAGE_CORRUPTED_OBJECT, get_called_class())
            );
        }

        //Check if we have wrapper above object
        if (isset($value[static::$fieldName])) {
            $value = $value[static::$fieldName];
        }

        return new static($value);
    }

    /**
     * TransmittedField constructor.
     * @param array $data
     */
    public function __construct(array $data = array())
    {
        $this->data = $data;
    }

    /**
     * Converts field into JSON serializable object
     * @return \StdClass
     */
    public function toObject()
    {
        $this->validate();

        //Needed due to recursive objects structure
        return json_decode(json_encode($this->data));
    }

    /**
     * Check if object contains all of the required attributes
     * @throws ValidationError if some fields are missing
     * @return bool
     */
    protected function validate()
    {
        $errors = array();
        foreach ($this->requiredAttributes as $requiredAttribute) {
            if (!ArrayHelper::keyExists($this->data, $requiredAttribute)) {
                $errors[] = $requiredAttribute.' is missing or empty';
            }
        }

        if (count($errors) > 0) {
            throw new ValidationError(get_called_class().' object misses required fields', $errors);
        }

        return true;
    }
}


<?php

namespace Judopay\Helper;

/**
 * Class ArrayHelper
 * @package Judopay\Helper
 */
class ArrayHelper
{
    /**
     * Returns value from array by index or default
     * @param array $array
     * @param mixed $index
     * @param mixed $default
     * @return mixed array value or default
     */
    public static function get(array $array, $index, $default = null)
    {
        return isset($array[$index])
            ? $array[$index]
            : $default;
    }

    /**
     * Checks for existence of an array element key.
     * @param array  $array Array or object to check index
     * @param string $key   Key of the array element, specified in a dot format.
     *                      e.g. if the key is `x.y.z`, then the returned value would be `$array['x']['y']['z']`
     * @return bool
     */
    public static function keyExists(array $array, $key)
    {
        if (($pos = strrpos($key, '.')) !== false
            && ($subKey = substr($key, 0, $pos)) !== false
            && static::keyExists($array, $subKey)
            && static::keyExists($array[$subKey], substr($key, $pos + 1))
        ) {
            return true;
        }

        return (isset($array[$key]) || array_key_exists($key, $array));
    }
}


<?php namespace Kamaln7\Toastr;

use Illuminate\Support\ServiceProvider;

class ToastrServiceProvider extends ServiceProvider {

	/**
	 * Indicates if loading of the provider is deferred.
	 *
	 * @var bool
	 */
	protected $defer = false;

	/**
	 * Bootstrap the application events.
	 *
	 * @return void
	 */
	public function boot()
	{
		$this->package('kamaln7/toastr');
	}

	/**
	 * Register the service provider.
	 *
	 * @return void
	 */
	public function register()
	{
        $this->app['toastr'] = $this->app->share(function($app) {
            return new Toastr($app['session'], $app['config']);
        });
	}

	/**
	 * Get the services provided by the provider.
	 *
	 * @return array
	 */
	public function provides()
	{
		return array('toastr');
	}

}


<?php namespace Kamaln7\Toastr;

use Illuminate\Config\Repository;

class Toastr {

    /**
     * Added notifications
     *
     * @var array
     */
    protected $notifications = array();

    /**
     * Illuminate Session
     *
     * @var \Illuminate\Session\SessionManager
     */
    protected $session;

    /**
     * Toastr config
     *
     * @var Illuminate\Config\Repository
     */
    protected $config;

    /**
     * Constructor
     *
     * @param \Illuminate\Session\SessionManager $sessio
     * @param  Illuminate\Config\Repository  $config
     * @return void
     */
    public function __construct(\Illuminate\Session\SessionManager $session, Repository $config) {
        $this->session = $session;
        $this->config = $config;
    }

    /**
     * Render the notifications' script tag
     *
     * @param bool $flashed Whether to get the
     *
     * @return string
     */
    public function render() {
        $notifications = $this->session->get('toastr::notifications');
        if(!$notifications) $notifications = array();

        $output = '<script type="text/javascript">';
        $lastConfig = [];
        foreach($notifications as $notification) {

            $config = $this->config->get('toastr::options');

            if(count($notification['options']) > 0) {
                // Merge user supplied options with default options
                $config = array_merge($config, $notification['options']);
            }

            // Config persists between toasts
            if($config != $lastConfig) {
                $output .= 'toastr.options = ' . json_encode($config) . ';';
                $lastConfig = $config;
            }

            // Toastr output
            $output .= 'toastr.' . $notification['type'] . "('" . str_replace("'", "\\'", htmlentities($notification['message'])) . "'" . (isset($notification['title']) ? ", '" . str_replace("'", "\\'", htmlentities($notification['title'])) . "'" : null) . ');';
        }
        $output .= '</script>';

        return $output;
    }

    /**
     * Add a notification
     *
     * @param string $type Could be error, info, success, or warning.
     * @param string $message The notification's message
     * @param string $title The notification's title
     *
     * @return bool Returns whether the notification was successfully added or
     * not.
     */
    public function add($type, $message, $title = null,$options = array()) {
        $allowedTypes = array('error', 'info', 'success', 'warning');
        if(!in_array($type, $allowedTypes)) return false;

        $this->notifications[] = array(
            'type' => $type,
            'title' => $title,
            'message' => $message,
            'options' => $options
        );

        $this->session->flash('toastr::notifications', $this->notifications);
    }

    /**
     * Shortcut for adding an info notification
     *
     * @param string $message The notification's message
     * @param string $title The notification's title
     */
    public function info($message, $title = null, $options = array()) {
        $this->add('info', $message, $title, $options);
    }

    /**
     * Shortcut for adding an error notification
     *
     * @param string $message The notification's message
     * @param string $title The notification's title
     */
    public function error($message, $title = null, $options = array()) {
        $this->add('error', $message, $title, $options);
    }

    /**
     * Shortcut for adding a warning notification
     *
     * @param string $message The notification's message
     * @param string $title The notification's title
     */
    public function warning($message, $title = null, $options = array()) {
        $this->add('warning', $message, $title, $options);
    }

    /**
     * Shortcut for adding a success notification
     *
     * @param string $message The notification's message
     * @param string $title The notification's title
     */
    public function success($message, $title = null, $options = array()) {
        $this->add('success', $message, $title, $options);
    }

    /**
     * Clear all notifications
     */
    public function clear() {
        $this->notifications = array();
    }

}


<?php

namespace Stuart;

use Stuart\converters\JsonToJob;

class Job
{
    private $id;
    private $transportType;
    private $assignmentCode;
    private $status;
    private $pickups = array();
    private $dropOffs = array();
    private $deliveries = array();
    private $distance;
    private $duration;

    /**
     * @var Pricing
     */
    private $pricing;

    /**
     * @param $address
     * @return \Stuart\Pickup
     */
    public function addPickup($address)
    {
        $pickup = new Pickup();
        $pickup->setAddress($address);
        $this->pickups[] = $pickup;
        return $pickup;
    }

    /**
     * @param $address
     * @return DropOff
     */
    public function addDropOff($address)
    {
        $dropOff = new DropOff();
        $dropOff->setAddress($address);
        $this->dropOffs[] = $dropOff;
        return $dropOff;
    }

    /**
     * This method allows you to create deliveries from two locations. It's only used by the
     * JsonToJob converter, you cannot create you own route.
     *
     * @param Location $origin
     * @param Location $destination
     * @return Delivery
     *
     * @see JsonToJob
     */
    public function link($origin, $destination)
    {
        $delivery = new Delivery($origin, $destination);
        $this->deliveries[] = $delivery;
        return $delivery;
    }

    /**
     * @return Pickup[]
     */
    public function getPickups()
    {
        return $this->pickups;
    }

    /**
     * @return DropOff[]
     */
    public function getDropOffs()
    {
        return $this->dropOffs;
    }

    /**
     * @return Delivery[]
     */
    public function getDeliveries()
    {
        return $this->deliveries;
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return mixed
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param mixed $status
     */
    public function setStatus($status)
    {
        $this->status = $status;
    }

    /**
     * @return mixed
     */
    public function getTransportType()
    {
        return $this->transportType;
    }

    /**
     * @param mixed $transportType
     */
    public function setTransportType($transportType)
    {
        $this->transportType = $transportType;
    }

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

    /**
     * @param string $assignmentCode
     */
    public function setAssignmentCode($assignmentCode)
    {
        $this->assignmentCode = $assignmentCode;
    }

    /**
     * Total Job distance, in kilometer.
     *
     * @return float
     */
    public function getDistance()
    {
        return $this->distance;
    }

    /**
     * @param float $distance
     */
    public function setDistance($distance)
    {
        $this->distance = $distance;
    }

    /**
     * Total Job duration estimation, in minute.
     *
     * @return int
     */
    public function getDuration()
    {
        return $this->duration;
    }

    /**
     * @param int $duration
     */
    public function setDuration($duration)
    {
        $this->duration = $duration;
    }

    /**
     * @return Pricing
     */
    public function getPricing()
    {
        return $this->pricing;
    }

    /**
     * @param Pricing $pricing
     */
    public function setPricing($pricing)
    {
        $this->pricing = $pricing;
    }
}


<?php

namespace Stuart\Converters;

use Stuart\Job;
use Stuart\Location;

class JobToJson
{
    /**
     * Converts a Job into a Stuart Job as JSON.
     *
     * @param Job $job
     * @return string
     */
    public static function convert($job)
    {
        $result = array(
            'job' => array()
        );

        if ($job->getTransportType() !== null) {
            $result['job']['transport_type'] = $job->getTransportType();
        }

        if ($job->getAssignmentCode() !== null) {
            $result['job']['assignment_code'] = $job->getAssignmentCode();
        }

        if (count($job->getPickups()) === 1 && $job->getPickups()[0]->getPickupAt() !== null) {
            $result['job']['pickup_at'] = $job->getPickups()[0]->getPickupAt()->format(JsonToJob::$STUART_DATE_FORMAT);
        }

        if (count($job->getDropoffs()) === 1 && $job->getDropoffs()[0]->getDropoffAt() !== null) {
            $result['job']['dropoff_at'] = $job->getDropoffs()[0]->getDropoffAt()->format(JsonToJob::$STUART_DATE_FORMAT);
        }

        $pickups = array();
        foreach ($job->getPickups() as $pickup) {
            $pickups[] = JobToJson::locationAsArray($pickup);
        }

        $dropOffs = array();
        foreach ($job->getDropOffs() as $dropOff) {
            $dropOffs[] = array_merge(JobToJson::locationAsArray($dropOff), array(
                'package_type' => $dropOff->getPackageType(),
                'package_description' => $dropOff->getPackageDescription(),
                'client_reference' => $dropOff->getClientReference()
            ));
        }

        $result['job']['pickups'] = $pickups;

        $result['job']['dropoffs'] = $dropOffs;

        return json_encode($result);
    }

    /**
     * @param Location $location
     * @return array
     */
    private static function locationAsArray($location)
    {
        return array(
            'address' => $location->getAddress(),
            'comment' => $location->getComment(),
            'contact' => array(
                'firstname' => $location->getFirstName(),
                'lastname' => $location->getLastName(),
                'phone' => $location->getPhone(),
                'email' => $location->getEmail(),
                'company' => $location->getCompany()
            )
        );
    }
}