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

namespace Garden;

use Garden\Exception\ValidationException;

/**
 * A class for defining and validating data schemas.
 */
class Schema implements \JsonSerializable {
    /// Properties ///
    protected $schema = [];

    protected static $types = [
//        '@' => 'file',
        'a' => 'array',
        'o' => 'object',
        '=' => 'base64',
        'i' => 'integer',
        's' => 'string',
        'f' => 'float',
        'b' => 'boolean',
        'ts' => 'timestamp',
        'dt' => 'datetime'
    ];

    /**
     * @var array An array of callbacks that will custom validate the schema.
     */
    protected $validators = [];

    /// Methods ///

    /**
     * Initialize an instance of a new {@link Schema} class.
     *
     * @param array $schema The array schema to validate against.
     */
    public function __construct($schema = []) {
        $this->schema = static::parseSchema($schema);
    }

    /**
     * Create a new schema and return it.
     *
     * @param array $schema The schema array.
     * @return Schema Returns the newly created and parsed schema.
     */
    public static function create($schema = []) {
        $new = new Schema($schema);
        return $new;
    }

    /**
     * Parse a schema in short form into a full schema array.
     *
     * @param array $arr The array to parse into a schema.
     * @return array The full schema array.
     * @throws \InvalidArgumentException Throws an exception when an item in the schema is invalid.
     */
    public static function parseSchema(array $arr) {
        $result = [];

        foreach ($arr as $key => $value) {
            if (is_int($key)) {
                if (is_string($value)) {
                    // This is a short param value.
                    $param = static::parseShortParam($value);
                    $name = $param['name'];
                    $result[$name] = $param;
                } else {
                    throw new \InvalidArgumentException("Schema at position $key is not a valid param.", 500);
                }
            } else {
                // The parameter is defined in the key.
                $param = static::parseShortParam($key, $value);
                $name = $param['name'];

                if (is_array($value)) {
                    // The value describes a bit more about the schema.
                    switch ($param['type']) {
                        case 'array':
                            if (isset($value['items'])) {
                                // The value includes array schema information.
                                $param = array_replace($param, $value);
                            } elseif (isset($value['type'])) {
                                // The value is a long-form schema.
                                $param['items'] = $value;
                            } else {
                                // The value is another shorthand schema.
                                $param['items'] = [
                                    'type' => 'object',
                                    'required' => true,
                                    'properties' => static::parseSchema($value)
                                ];
                            }
                            break;
                        case 'object':
                            // The value is a schema of the object.
                            if (isset($value['properties'])) {
                                $param['properties'] = static::parseSchema($value['properties']);
                            } else {
                                $param['properties'] = static::parseSchema($value);
                            }
                            break;
                        default:
                            $param = array_replace($param, $value);
                            break;
                    }
                } elseif (is_string($value)) {
                    if ($param['type'] === 'array') {
                        // Check to see if the value is the item type in the array.
                        if (isset(self::$types[$value])) {
                            $arrType = self::$types[$value];
                        } elseif (($index = array_search($value, self::$types)) !== false) {
                            $arrType = self::$types[$index];
                        }

                        if (isset($arrType)) {
                            $param['items'] = ['type' => $arrType, 'required' => true];
                        } else {
                            $param['description'] = $value;
                        }
                    } else {
                        // The value is the schema description.
                        $param['description'] = $value;
                    }
                }

                $result[$name] = $param;
            }
        }

        return $result;
    }

    /**
     * Parse a short parameter string into a full array parameter.
     *
     * @param string $str The short parameter string to parse.
     * @param array $other An array of other information that might help resolve ambiguity.
     * @return array Returns an array in the form [name, [param]].
     * @throws \InvalidArgumentException Throws an exception if the short param is not in the correct format.
     */
    public static function parseShortParam($str, $other = []) {
        // Is the parameter optional?
        if (str_ends($str, '?')) {
            $required = false;
            $str = substr($str, 0, -1);
        } else {
            $required = true;
        }

        // Check for a type.
        $parts = explode(':', $str);

        if (count($parts) === 1) {
            if (isset($other['type'])) {
                $type = $other['type'];
            } else {
                $type = 'string';
            }
            $name = $parts[0];
        } else {
            $name = $parts[1];

            if (isset(self::$types[$parts[0]])) {
                $type = self::$types[$parts[0]];
            } else {
                throw new \InvalidArgumentException("Invalid type {$parts[1]} for field $name.", 500);
            }
        }

        $result = ['name' => $name, 'type' => $type, 'required' => $required];

        return $result;
    }

    /**
     * Add a custom validator to to validate the schema.
     *
     * @param string $fieldname The name of the field to validate, if any.
     * @param callable $callback The callback to validate with.
     * @return Schema Returns `$this` for fluent calls.
     */
    public function addValidator($fieldname, callable $callback) {
        $this->validators[$fieldname][] = $callback;
        return $this;
    }

    /**
     * Require one of a given set of fields in the schema.
     *
     * @param array $fieldnames The field names to require.
     * @param int $count The count of required items.
     * @return Schema Returns `$this` for fluent calls.
     */
    public function requireOneOf(array $fieldnames, $count = 1) {
        $result = $this->addValidator('*', function ($data, Validation $validation) use ($fieldnames, $count) {
            $hasCount = 0;
            $flattened = [];

            foreach ($fieldnames as $name) {
                $flattened = array_merge($flattened, (array)$name);

                if (is_array($name)) {
                    // This is an array of required names. They all must match.
                    $hasCountInner = 0;
                    foreach ($name as $nameInner) {
                        if (isset($data[$nameInner]) && $data[$nameInner]) {
                            $hasCountInner++;
                        } else {
                            break;
                        }
                    }
                    if ($hasCountInner >= count($name)) {
                        $hasCount++;
                    }
                } elseif (isset($data[$name]) && $data[$name]) {
                    $hasCount++;
                }

                if ($hasCount >= $count) {
                    return true;
                }
            }

            $messageFields = array_map(function ($v) {
                if (is_array($v)) {
                    return '('.implode(', ', $v).')';
                }
                return $v;
            }, $fieldnames);

            if ($count === 1) {
                $message = sprintft('One of %s are required.', implode(', ', $messageFields));
            } else {
                $message = sprintft('%1$s of %2$s are required.', $count, implode(', ', $messageFields));
            }

            $validation->addError('missing_field', $flattened, [
                'message' => $message
            ]);
            return false;
        });

        return $result;
    }

    /**
     * Validate data against the schema.
     *
     * @param array &$data The data to validate.
     * @param Validation &$validation This argument will be filled with the validation result.
     * @return bool Returns true if the data is valid, false otherwise.
     * @throws ValidationException Throws an exception when the data does not validate against the schema.
     */
    public function validate(array &$data, Validation &$validation = null) {
        if (!$this->isValidInternal($data, $this->schema, $validation, '')) {
            if ($validation === null) {
                // Although this should never be null, scrutinizer complains that it might be.
                $validation = new Validation();
            }

            throw new ValidationException($validation);
        }
        return $this;
    }

    /**
     * Validate data against the schema and return the result.
     *
     * @param array &$data The data to validate.
     * @param Validation &$validation This argument will be filled with the validation result.
     * @return bool Returns true if the data is valid. False otherwise.
     */
    public function isValid(array &$data, Validation &$validation = null) {
        return $this->isValidInternal($data, $this->schema, $validation, '');
    }

    /**
     * Validate data against the schema and return the result.
     *
     * @param array &$data The data to validate.
     * @param array $schema The schema array to validate against.
     * @param Validation &$validation This argument will be filled with the validation result.
     * @param string $path The path to the current path for nested objects.
     * @return bool Returns true if the data is valid. False otherwise.
     */
    protected function isValidInternal(array &$data, array $schema, Validation &$validation = null, $path = '') {
        if ($validation === null) {
            $validation = new Validation();
        }

        // Loop through the schema fields and validate each one.
        foreach ($schema as $name => $field) {
            // Prepend the path the field label.
            if ($path) {
                $field['path'] = $path.array_select(['path', 'name'], $field);
            }

            if (array_key_exists($name, $data)) {
                $this->validateField($data[$name], $field, $validation);
            } elseif (val('required', $field)) {
                $validation->addError('missing_field', array_select(['path', 'name'], $field));
            }
        }

        // Validate the global validators.
        if ($path == '' && isset($this->validators['*'])) {
            foreach ($this->validators['*'] as $callback) {
                call_user_func($callback, $data, $validation);
            }
        }

        return $validation->isValid();
    }

    /**
     * Validate a field.
     *
     * @param mixed &$value The value to validate.
     * @param array $field Parameters on the field.
     * @param Validation $validation A validation object to add errors to.
     * @throws \InvalidArgumentException Throws an exception when there is something wrong in the {@link $params}.
     * @internal param string $fieldname The name of the field to validate.
     * @return bool Returns true if the field is valid, false otherwise.
     */
    protected function validateField(&$value, array $field, Validation $validation) {
        $path = array_select(['path', 'name'], $field);
        $type = val('type', $field, '');
        $valid = true;

        // Check required first.
        // A value that isn't passed should fail the required test, but short circuit the other ones.
        $validRequired = $this->validateRequired($value, $field, $validation);
        if ($validRequired !== null) {
            return $validRequired;
        }

        // Validate the field's type.
        $validType = true;
        switch ($type) {
            case 'boolean':
                $validType &= $this->validateBoolean($value, $field, $validation);
                break;
            case 'integer':
                $validType &= $this->validateInteger($value, $field, $validation);
                break;
            case 'float':
                $validType &= $this->validateFloat($value, $field, $validation);
                break;
            case 'string':
                $validType &= $this->validateString($value, $field, $validation);
                break;
            case 'timestamp':
                $validType &= $this->validateTimestamp($value, $field, $validation);
                break;
            case 'datetime':
                $validType &= $this->validateDatetime($value, $field, $validation);
                break;
            case 'base64':
                $validType &= $this->validateBase64($value, $field, $validation);
                break;
            case 'array':
                $validType &= $this->validateArray($value, $field, $validation);
                break;
            case 'object':
                $validType &= $this->validateObject($value, $field, $validation);
                break;
            case '':
                // No type was specified so we are valid.
                $validType = true;
                break;
            default:
                throw new \InvalidArgumentException("Unrecognized type $type.", 500);
        }
        if (!$validType) {
            $valid = false;
            $validation->addError(
                'invalid_type',
                $path,
                [
                    'type' => $type,
                    'message' => sprintft('%1$s is not a valid %2$s.', $path, $type),
                    'status' => 422
                ]
            );
        }

        // Validate a custom field validator.
        $validatorName = val('validatorName', $field, $path);
        if (isset($this->validators[$validatorName])) {
            foreach ($this->validators[$validatorName] as $callback) {
                call_user_func_array($callback, [&$value, $field, $validation]);
            }
        }

        return $valid;
    }

    /**
     * Validate an array.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateArray(&$value, array $field, Validation $validation) {
        $validType = true;

        if (!is_array($value) || (count($value) > 0 && !array_key_exists(0, $value))) {
            $validType = false;
        } else {
            // Cast the items into a proper numeric array.
            $value = array_values($value);

            if (isset($field['items'])) {
                // Validate each of the types.
                $path = array_select(['path', 'name'], $field);
                $itemField = $field['items'];
                $itemField['validatorName'] = array_select(['validatorName', 'path', 'name'], $field).'.items';
                foreach ($value as $i => &$item) {
                    $itemField['path'] = "$path.$i";
                    $this->validateField($item, $itemField, $validation);
                }
            }
        }
        return $validType;
    }

    /**
     * Validate a base64 string.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateBase64(&$value, array $field, Validation $validation) {
        if (!is_string($value)) {
            $validType = false;
        } else {
            if (!preg_match('`^[a-zA-Z0-9/+]*={0,2}$`', $value)) {
                $validType = false;
            } else {
                $decoded = @base64_decode($value);
                if ($decoded === false) {
                    $validType = false;
                } else {
                    $value = $decoded;
                    $validType = true;
                }
            }
        }
        return $validType;
    }

    /**
     * Validate a boolean value.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateBoolean(&$value, array $field, Validation $validation) {
        if (is_bool($value)) {
            $validType = true;
        } else {
            $bools = [
                '0' => false, 'false' => false, 'no' => false, 'off' => false,
                '1' => true,  'true'  => true, 'yes' => true,  'on'  => true
            ];
            if ((is_string($value) || is_numeric($value)) && isset($bools[$value])) {
                $value = $bools[$value];
                $validType = true;
            } else {
                $validType = false;
            }
        }
        return $validType;
    }

    /**
     * Validate a date time.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateDatetime(&$value, array $field, Validation $validation) {
        $validType = true;
        if ($value instanceof \DateTime) {
            $validType = true;
        } elseif (is_string($value)) {
            try {
                $dt = new \DateTime($value);
                if ($dt) {
                    $value = $dt;
                } else {
                    $validType = false;
                }
            } catch (\Exception $ex) {
                $validType = false;
            }
        } elseif (is_numeric($value) && $value > 0) {
            $value = new \DateTime('@'.(string)round($value));
            $validType = true;
        } else {
            $validType = false;
        }
        return $validType;
    }

    /**
     * Validate a float.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateFloat(&$value, array $field, Validation $validation) {
        if (is_float($value)) {
            $validType = true;
        } elseif (is_numeric($value)) {
            $value = (float)$value;
            $validType = true;
        } else {
            $validType = false;
        }
        return $validType;
    }

    /**
     * Validate and integer.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateInteger(&$value, array $field, Validation $validation) {
        if (is_int($value)) {
            $validType = true;
        } elseif (is_numeric($value)) {
            $value = (int)$value;
            $validType = true;
        } else {
            $validType = false;
        }
        return $validType;
    }

    /**
     * Validate an object.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateObject(&$value, array $field, Validation $validation) {
        if (!is_array($value) || isset($value[0])) {
            return false;
        } elseif (isset($field['properties'])) {
            $path = array_select(['path', 'name'], $field);
            // Validate the data against the internal schema.
            $this->isValidInternal($value, $field['properties'], $validation, $path.'.');
        }
        return true;
    }

    /**
     * Validate a required field.
     *
     * @param mixed &$value The field value.
     * @param array $field The field definition.
     * @param Validation $validation A {@link Validation} object to collect errors.
     * @return bool|null Returns one of the following:
     * - null: The field is not required.
     * - true: The field is required and {@link $value} is not empty.
     * - false: The field is required and {@link $value} is empty.
     */
    protected function validateRequired(&$value, array $field, Validation $validation) {
        $required = val('required', $field, false);
        $type = $field['type'];

        if ($value === '' || $value === null) {
            if (!$required) {
                $value = null;
                return true;
            }

            switch ($type) {
                case 'boolean':
                    $value = false;
                    return true;
                case 'string':
                    if (val('minLength', $field, 1) == 0) {
                        $value = '';
                        return true;
                    }
            }
            $validation->addError('missing_field', $field);
            return false;
        }
        return null;
    }

    /**
     * Validate a string.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateString(&$value, array $field, Validation $validation) {
        if (is_string($value)) {
            $validType = true;
        } elseif (is_numeric($value)) {
            $value = (string)$value;
            $validType = true;
        } else {
            $validType = false;
        }
        return $validType;
    }

    /**
     * Validate a unix timestamp.
     *
     * @param mixed &$value The value to validate.
     * @param array $field The field definition.
     * @param Validation $validation The validation results to add.
     * @return bool Returns true if {@link $value} is valid or false otherwise.
     */
    protected function validateTimestamp(&$value, array $field, Validation $validation) {
        $validType = true;
        if (is_numeric($value)) {
            $value = (int)$value;
        } elseif (is_string($value) && $ts = strtotime($value)) {
            $value = $ts;
        } else {
            $validType = false;
        }
        return $validType;
    }

    /**
     * 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->schema;
    }
}


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

namespace Garden\Exception;

use Garden\Validation;

/**
 * An exception that was built from a {@link Validation} object.
 *
 * The validation object collects errors and is mutable. Once it's ready to be thrown as an exception it gets converted
 * to an instance of the immutable {@link ValidationException} class.
 */
class ValidationException extends ClientException {
    protected $validation;

    /**
     * Initialize an instance of the {@link ValidationException} class.
     *
     * @param Validation $validation The {@link Validation} object for the exception.
     */
    public function __construct(Validation $validation) {
        $this->validation = $validation;
        parent::__construct($validation->getMessage(), (int)$validation->status());
    }

    /**
     * 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() {
        $errors = $this->validation->getErrorsFlat();
        if (count($errors) === 1) {
            $message = Validation::errorMessage($errors[0]);
        } else {
            $message = t('Validation failed.');
        }

        $result = [
            'message' => $message,
            'status' => $this->getCode(),
            'errors' => $errors
        ];
        return $result;
    }
}


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

namespace Garden;

/**
 * Contains functionality that allows addons to enhance or change an application's functionality.
 *
 * An addon can do the following.
 *
 * 1. Any classes that the addon defines in its root, /controllers, /library, /models, and /modules
 *    directories are made available.
 * 2. The addon can contain a bootstrap.php which will be included at the app startup.
 * 3. If the addon declares any classes ending in *Plugin then those plugins will automatically
 *    bind their event handlers. (also *Hooks)
 */
class Addons {
    /// Constants ///
    const K_BOOTSTRAP = 'bootstrap'; // bootstrap path key
    const K_CLASSES = 'classes';
    const K_DIR = 'dir';
    const K_INFO = 'info'; // addon info key

    /// Properties ///

    /**
     * @var array An array that maps addon keys to full addon information.
     */
    protected static $all;

    /**
     * @var string The base directory where all of the addons are found.
     */
    protected static $baseDir;

    /**
     * @var array An array that maps class names to their fully namespaced class names.
     */
//    protected static $basenameMap;

    /**
     * @var array|null An array that maps class names to file paths.
     */
    protected static $classMap;

    /**
     * @var array An array that maps addon keys to full addon information for enabled addons.
     */
    protected static $enabled;

    /**
     * @var array An array of enabled addon keys.
     */
    protected static $enabledKeys;

    /**
     * @var bool Signals that the addon framework is in a shared environment and shouldn't use the enabled cache.
     */
    public static $sharedEnvironment;

    /// Methods ///

    /**
     * Get all of the available addons or a single addon from the available list.
     *
     * @param string $addon_key If you supply an addon key then only that addon will be returned.
     * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon.
     * @return array Returns the addon with the given key or all available addons if no key is passed.
     */
    public static function all($addon_key = null, $key = null) {
        if (self::$all === null) {
            self::$all = static::cacheGet('addons-all', array(get_class(), 'scanAddons'));
        }

        // The array should be built now return the addon.
        if ($addon_key === null) {
            return self::$all;
        } else {
            $addon = val(strtolower($addon_key), self::$all);
            if ($addon && $key) {
                return val($key, $addon);
            } elseif ($addon) {
                return $addon;
            } else {
                return null;
            }
        }
    }

    /**
     * An autoloader that will autoload a class based on which addons are enabled.
     *
     * @param string $classname The name of the class to load.
     */
    public static function autoload($classname) {
        list($fullClass, $path) = static::classMap($classname);
        if ($path) {
            require $path;
        }
    }

    /**
     * Gets/sets the base directory for addons.
     *
     * @param string $value Pass a value to set the new base directory.
     * @return string Returns the base directory for addons.
     */
    public static function baseDir($value = null) {
        if ($value !== null) {
            self::$baseDir = rtrim($value, '/');
        } elseif (self::$baseDir === null) {
            self::$baseDir = PATH_ROOT.'/addons';
        }
    }

    /**
     * Start up the addon framework.
     *
     * @param array $enabled_addons An array of enabled addons.
     */
    public static function bootstrap($enabled_addons = null) {
        // Load the addons from the config if they aren't passed in.
        if (!is_array($enabled_addons)) {
            $enabled_addons = config('addons', array());
        }
        // Reformat the enabled array into the form: array('addon_key' => 'addon_key')
        $enabled_keys = array_keys(array_change_key_case(array_filter($enabled_addons)));
        $enabled_keys = array_combine($enabled_keys, $enabled_keys);
        self::$enabledKeys = $enabled_keys;
        self::$classMap = null; // invalidate so it will rebuild

        // Enable the addon autoloader.
        spl_autoload_register(array(get_class(), 'autoload'), true, true);

        // Bind all of the addon plugin events now.
        foreach (self::enabled() as $addon) {
            if (!isset($addon[self::K_CLASSES])) {
                continue;
            }

            foreach ($addon[self::K_CLASSES] as $class_name => $class_path) {
                if (str_ends($class_name, 'plugin')) {
                    Event::bindClass($class_name);
                } elseif (str_ends($class_name, 'hooks')) {
                    // Vanilla 2 used hooks files for themes and applications.
                    $basename = ucfirst(rtrim_substr($class_name, 'hooks'));
                    deprecated($basename.'Hooks', $basename.'Plugin');
                    Event::bindClass($class_name);
                }
            }
        }

        Event::bind('bootstrap', function () {
            // Start each of the enabled addons.
            foreach (self::enabled() as $key => $value) {
                static::startAddon($key);
            }
        });
    }

    /**
     * Get the cached file or hydrate the cache with a callback.
     *
     * @param string $key The cache key to get.
     * @param callable $cache_cb The function to run when hydrating the cache.
     * @return array Returns the cached array.
     */
    protected static function cacheGet($key, callable $cache_cb) {
        // Salt the cache with the root path so that it will invalidate if the app is moved.
        $salt = substr(md5(static::baseDir()), 0, 10);

        $cache_path = PATH_ROOT."/cache/$key-$salt.json.php";
        if (file_exists($cache_path)) {
            $result = array_load($cache_path);
            return $result;
        } else {
            $result = $cache_cb();
            array_save($result, $cache_path);
        }
        return $result;
    }

    /**
     * A an array that maps class names to physical paths.
     *
     * @param string $classname An optional class name to get the path of.
     * @return array Returns an array in the form `[fullClassname, classPath]`.
     * If no {@link $classname} is passed then the entire class map is returned.
     * @throws \Exception Throws an exception if the class map is corrupt.
     */
    public static function classMap($classname = null) {
        if (self::$classMap === null) {
            // Loop through the enabled addons and grab their classes.
            $class_map = array();
            foreach (static::enabled() as $addon) {
                if (isset($addon[self::K_CLASSES])) {
                    $class_map = array_replace($class_map, $addon[self::K_CLASSES]);
                }
            }
            self::$classMap = $class_map;
        }

        // Now that the class map has been built return the result.
        if ($classname !== null) {
            if (strpos($classname, '\\') === false) {
                $basename = strtolower($classname);
            } else {
                $basename = strtolower(trim(strrchr($classname, '\\'), '\\'));
            }

            $row = val($basename, self::$classMap);

            if ($row === null) {
                return ['', ''];
            } elseif (is_string($row)) {
                return [$classname, $row];
            } elseif (is_array($row)) {
                return  $row;
            } else {
                return ['', ''];
            }
        } else {
            return self::$classMap;
        }
    }

    /**
     * Get all of the enabled addons or a single addon from the enabled list.
     *
     * @param string $addon_key If you supply an addon key then only that addon will be returned.
     * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon.
     * @return array Returns the addon with the given key or all enabled addons if no key is passed.
     * @throws \Exception Throws an exception if {@link Addons::bootstrap()} hasn't been called yet.
     */
    public static function enabled($addon_key = null, $key = null) {
        // Lazy build the enabled array.
        if (self::$enabled === null) {
            // Make sure the enabled addons have been added first.
            if (self::$enabledKeys === null) {
                throw new \Exception("Addons::boostrap() must be called before Addons::enabled() can be called.", 500);
            }

            if (self::$all !== null || self::$sharedEnvironment) {
                // Build the enabled array by filtering the all array.
                self::$enabled = array();
                foreach (self::all() as $key => $row) {
                    if (isset($key, self::$enabledKeys)) {
                        self::$enabled[$key] = $row;
                    }
                }
            } else {
                // Build the enabled array by walking the addons.
                self::$enabled = static::cacheGet('addons-enabled', function () {
                    return static::scanAddons(null, self::$enabledKeys);
                });
            }
        }

        // The array should be built now return the addon.
        if ($addon_key === null) {
            return self::$enabled;
        } else {
            $addon = val(strtolower($addon_key), self::$enabled);
            if ($addon && $key) {
                return val($key, $addon);
            } elseif ($addon) {
                return $addon;
            } else {
                return null;
            }
        }
    }

    /**
     * Return the info array for an addon.
     *
     * @param string $addon_key The addon key.
     * @return array|null Returns the addon's info array or null if the addon wasn't found.
     */
    public static function info($addon_key) {
        $addon_key = strtolower($addon_key);

        // Check the enabled array first so that we don't load all addons if we don't have to.
        if (isset(self::$enabledKeys[$addon_key])) {
            return static::enabled($addon_key, self::K_INFO);
        } else {
            return static::all($addon_key, self::K_INFO);
        }
    }

    /**
     * Scan an addon directory for information.
     *
     * @param string $dir The addon directory to scan.
     * @param array &$addons The addons array.
     * @param array $enabled An array of enabled addons or null to scan all addons.
     * @return array Returns an array in the form [addonKey, addonInfo].
     */
    protected static function scanAddonRecursive($dir, &$addons, $enabled = null) {
        $dir = rtrim($dir, '/');
        $addonKey = strtolower(basename($dir));

        // Scan the addon if it is enabled.
        if ($enabled === null || in_array($addonKey, $enabled)) {
            list($addonKey, $addon) = static::scanAddon($dir);
        } else {
            $addon = null;
        }

        // Add the addon to the collection array if one was supplied.
        if ($addon !== null) {
            $addons[$addonKey] = $addon;
        }

        // Recurse.
        $addon_subdirs = array('/addons');
        foreach ($addon_subdirs as $addon_subdir) {
            if (is_dir($dir.$addon_subdir)) {
                static::scanAddons($dir.$addon_subdir, $enabled, $addons);
            }
        }

        return array($addonKey, $addon);
    }

    /**
     * Scan an individual addon directory and return the information about that addon.
     *
     * @param string $dir The path to the addon.
     * @return array An array in the form of `[$addon_key, $addon_row]` or `[$addon_key, null]` if the directory doesn't
     * represent an addon.
     */
    protected static function scanAddon($dir) {
        $dir = rtrim($dir, '/');
        $addon_key = strtolower(basename($dir));

        // Look for the addon info array.
        $info_path = $dir.'/addon.json';
        $info = false;
        if (file_exists($info_path)) {
            $info = json_decode(file_get_contents($info_path), true);
        }
        if (!$info) {
            $info = array();
        }
        array_touch('name', $info, $addon_key);
        array_touch('version', $info, '0.0');

        // Look for the bootstrap.
        $bootstrap = $dir.'/bootstrap.php';
        if (!file_exists($dir.'/bootstrap.php')) {
            $bootstrap = null;
        }

        // Scan the appropriate subdirectories  for classes.
        $subdirs = array('', '/library', '/controllers', '/models', '/modules', '/settings');
        $classes = array();
        foreach ($subdirs as $subdir) {
            // Get all of the php files in the subdirectory.
            $paths = glob($dir.$subdir.'/*.php');
            foreach ($paths as $path) {
                $decls = static::scanFile($path);
                foreach ($decls as $namespace_row) {
                    if (isset($namespace_row['namespace']) && $namespace_row) {
                        $namespace = rtrim($namespace_row['namespace'], '\\').'\\';
                        $namespace_classes = $namespace_row['classes'];
                    } else {
                        $namespace = '';
                        $namespace_classes = $namespace_row;
                    }

                    foreach ($namespace_classes as $class_row) {
                        $classes[strtolower($class_row['name'])] = [$namespace.$class_row['name'], $path];
                    }
                }
            }
        }

        $addon = array(
            self::K_BOOTSTRAP => $bootstrap,
            self::K_CLASSES => $classes,
            self::K_DIR => $dir,
            self::K_INFO => $info
        );

        return array($addon_key, $addon);
    }

    /**
     * Scan a directory for addons.
     *
     * @param string $dir The directory to scan.
     * @param array $enabled An array of enabled addons in the form `[addonKey => enabled, ...]`.
     * @param array &$addons The addons will fill this array.
     * @return array Returns all of the addons.
     */
    protected static function scanAddons($dir = null, $enabled = null, &$addons = null) {
        if (!$dir) {
            $dir = static::$baseDir;
        }
        if ($addons === null) {
            $addons = array();
        }

        /* @var \DirectoryIterator */
        foreach (new \DirectoryIterator($dir) as $subdir) {
            if ($subdir->isDir() && !$subdir->isDot()) {
//                echo $subdir->getPathname().$subdir->isDir().$subdir->isDot().'<br />';
                static::scanAddonRecursive($subdir->getPathname(), $addons, $enabled);
            }
        }
        return $addons;
    }

    /**
     * Looks what classes and namespaces are defined in a file and returns the first found.
     *
     * @param string $file Path to file.
     * @return array Returns an empty array if no classes are found or an array with namespaces and
     * classes found in the file.
     * @see http://stackoverflow.com/a/11114724/1984219
     */
    protected static function scanFile($file) {
        $classes = $nsPos = $final = array();
        $foundNamespace = false;
        $ii = 0;

        if (!file_exists($file)) {
            return array();
        }

        $er = error_reporting();
        error_reporting(E_ALL ^ E_NOTICE);

        $php_code = file_get_contents($file);
        $tokens = token_get_all($php_code);
        $count = count($tokens);

        for ($i = 0; $i < $count; $i++) {
            if (!$foundNamespace && $tokens[$i][0] == T_NAMESPACE) {
                $nsPos[$ii]['start'] = $i;
                $foundNamespace = true;
            } elseif ($foundNamespace && ($tokens[$i] == ';' || $tokens[$i] == '{')) {
                $nsPos[$ii]['end'] = $i;
                $ii++;
                $foundNamespace = false;
            } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_CLASS && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
                if ($i - 4 >= 0 && $tokens[$i - 4][0] == T_ABSTRACT) {
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'ABSTRACT CLASS');
                } else {
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'CLASS');
                }
            } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_INTERFACE && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
                $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'INTERFACE');
            }
        }
        error_reporting($er);
        if (empty($classes)) {
            return [];
        }

        if (!empty($nsPos)) {
            foreach ($nsPos as $k => $p) {
                $ns = '';
                for ($i = $p['start'] + 1; $i < $p['end']; $i++) {
                    $ns .= $tokens[$i][1];
                }

                $ns = trim($ns);
                $final[$k] = array('namespace' => $ns, 'classes' => $classes[$k + 1]);
            }
            $classes = $final;
        }
        return $classes;
    }

    /**
     * Start an addon.
     *
     * This function does the following:
     *
     * 1. Make the addon available in the autoloader.
     * 2. Run the addon's bootstrap.php if it exists.
     *
     * @param string $addon_key The key of the addon to enable.
     * @return bool Returns true if the addon was enabled. False otherwise.
     */
    public static function startAddon($addon_key) {
        $addon = static::enabled($addon_key);
        if (!$addon) {
            return false;
        }

        // Run the class' bootstrap.
        if ($bootstrap_path = val(self::K_BOOTSTRAP, $addon)) {
            include_once $bootstrap_path;
        }
        return true;
    }
}


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

namespace Garden\Exception;

use Garden\Response;

/**
 * Represents a 400 series exception.
 */
class ClientException extends \Exception implements \JsonSerializable {
    protected $context;

    /**
     * Initialize an instance of the {@link ClientException} class.
     *
     * The 4xx class of status code is intended for cases in which the client seems to have erred.
     * When constructing a client exception you can pass additional information on the {@link $context} parameter
     * to aid in rendering.
     *
     * - Keys beginning with **HTTP_** will be added as headers.
     * - **description** will give the exception a longer description.
     *
     * @param string $message The error message.
     * @param int $code The http error code.
     * @param array $context An array of context variables that can be used to render a more detailed response.
     */
    public function __construct($message = '', $code = 400, array $context = []) {
        parent::__construct($message, $code);
        $this->context = $context;
    }

    /**
     * Gets the response headers that were set on the exception.
     *
     * @return array Returns the headers for this exception.
     */
    public function getHeaders() {
        $result = [];
        foreach ($this->context as $key => $value) {
            if (stripos($key, 'http_') === 0) {
                $key = Response::normalizeHeader(ltrim_substr($key, 'http_'));
                $result[$key] = $value;
            }
        }
        return $result;
    }

    /**
     * Gets a longer description for the exception.
     *
     * @return string Returns the description of the exception or an empty string if there isn't one.
     */
    public function getDescription() {
        return val('description', $this->context, '');
    }

    /**
     * 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() {
        $result = [
            'message' => $this->getMessage(),
            'status' => $this->getCode()
        ];
        if ($this->getDescription()) {
            $result['description'] = $this->getDescription();
        }
        return $result;
    }
}


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

namespace Garden;

/**
 * Contains methods for binding and firing to events.
 *
 * Addons can create callbacks that bind to events which are calle througout the code to allow
 * extension of the application and framework.
 */
class Event {
    /// Constants ///

    const PRIORITY_LOW = 1000;
    const PRIORITY_NORMAL = 100;
    const PRIORITY_HIGH = 10;

    /// Properties ///

    /**
     * All of the event handlers that have been registered.
     * @var array An array of event handlers.
     */
    protected static $handlers = array();

    /**
     * All of the event handlers that still need to be sorted by priority.
     * @var array An array of event handler names that need to be sorted.
     */
    protected static $toSort = array();

    /// Methods ///

    /**
     * Call a callback with an array of parameters, checking for events to be called with the callback.
     *
     * This method is similar to {@link call_user_func_array()} but it will fire events that can happen
     * before and/or after the callback and can call an event instead of the callback itself to override it.
     *
     * In order to use events with this method they must be bound with a particular naming convention.
     *
     * **Modify a function**
     *
     * - functionname_before
     * - functionname
     * - functionname_after
     *
     * **Modify a method call**
     *
     * - classname_methodname_before
     * - classname_methodname
     * - classname_methodname_after
     *
     * @param callable $callback The {@link callable} to be called.
     * @param array $args The arguments to be passed to the callback, as an indexed array.
     * @return mixed Returns the return value of the callback.
     */
    public static function callUserFuncArray($callback, $args = []) {
        // Figure out the event name from the callback.
        $event_name = static::getEventname($callback);
        if (!$event_name) {
            return call_user_func_array($callback, $args);
        }

        // The events could have different args because the event handler can take the object as the first parameter.
        $event_args = $args;
        // If the callback is an object then it gets passed as the first argument.
        if (is_array($callback) && is_object($callback[0])) {
            array_unshift($event_args, $callback[0]);
        }

        // Fire before events.
        self::fireArray($event_name.'_before', $event_args);

        // Call the function.
        if (static::hasHandler($event_name)) {
            // The callback was overridden so fire it.
            $result = static::fireArray($event_name, $event_args);
        } else {
            // The callback was not overridden so just call the passed callback.
            $result = call_user_func_array($callback, $args);
        }

        // Fire after events.
        self::fireArray($event_name.'_after', $event_args);

        return $result;
    }

    /**
     * Bind an event handler to an event.
     *
     * @param string $event The naame of the event to bind to.
     * @param callback $callback The callback of the event.
     * @param int $priority The priority of the event.
     */
    public static function bind($event, $callback, $priority = Event::PRIORITY_NORMAL) {
        $event = strtolower($event);
        self::$handlers[$event][$priority][] = $callback;
        self::$toSort[$event] = true;
    }

    /**
     * Bind a class' declared event handlers.
     *
     * Plugin classes declare event handlers in the following way:
     *
     * ```
     * // Bind to a normal event.
     * public function eventname_handler($arg1, $arg2, ...) { ... }
     *
     * // Add/override a method called with Event::callUserFuncArray().
     * public function ClassName_methodName($sender, $arg1, $arg2) { ... }
     * public function ClassName_methodName_create($sender, $arg1, $arg2) { ... } // deprecated
     *
     * // Call the handler before or after a method called with Event::callUserFuncArray().
     * public function ClassName_methodName_before($sender, $arg1, $arg2) { ... }
     * public function ClassName_methodName_after($sender, $arg1, $arg2) { ... }
     * ```
     *
     * @param mixed $class The class name or an object instance.
     * @param int $priority The priority of the event.
     * @throws \InvalidArgumentException Throws an exception when binding to a class name with no `instance()` method.
     */
    public static function bindClass($class, $priority = Event::PRIORITY_NORMAL) {
        $method_names = get_class_methods($class);

        // Grab an instance of the class so there is something to bind to.
        if (is_string($class)) {
            if (method_exists($class, 'instance')) {
                // TODO: Make the instance lazy load.
                $instance = call_user_func(array($class, 'instance'));
            } else {
                throw new \InvalidArgumentException('Event::bindClass(): The class for argument #1 must have an instance() method or be passed as an object.', 422);
            }
        } else {
            $instance = $class;
        }

        foreach ($method_names as $method_name) {
            if (strpos($method_name, '_') === false) {
                continue;
            }

            $parts = explode('_', strtolower($method_name));
            switch (end($parts)) {
                case 'handler':
                case 'create':
                case 'override':
                    array_pop($parts);
                    $event_name = implode('_', $parts);
                    break;
                case 'before':
                case 'after':
                default:
                    $event_name = implode('_', $parts);
                    break;
            }
            // Bind the event if we have one.
            if ($event_name) {
                static::bind($event_name, array($instance, $method_name), $priority);
            }
        }
    }

    /**
     * Dumps all of the bound handlers.
     *
     * This method is meant for debugging.
     *
     * @return array Returns an array of all handlers indexed by event name.
     */
    public static function dumpHandlers() {
        $result = [];

        foreach (self::$handlers as $event_name => $nested) {
            $handlers = call_user_func_array('array_merge', static::getHandlers($event_name));
            $result[$event_name] = array_map('format_callback', $handlers);
        }

        return $result;
    }

    /**
     * Fire an event.
     *
     * @param string $event The name of the event.
     * @return mixed Returns the result of the last event handler.
     */
    public static function fire($event) {
        $handlers = self::getHandlers($event);
        if (!$handlers) {
            return null;
        }

        // Grab the handlers and call them.
        $args = array_slice(func_get_args(), 1);
        $result = null;
        foreach ($handlers as $callbacks) {
            foreach ($callbacks as $callback) {
                $result = call_user_func_array($callback, $args);
            }
        }
        return $result;
    }

    /**
     * Fire an event with an array of arguments.
     *
     * This method is to {@link Event::fire()} as {@link call_user_func_array()} is to {@link call_user_funct()}.
     * The main purpose though is to allow you to have event handlers that can take references.
     *
     * @param string $event The name of the event.
     * @param array $args The arguments for the event handlers.
     * @return mixed Returns the result of the last event handler.
     */
    public static function fireArray($event, $args = []) {
        $handlers = self::getHandlers($event);
        if (!$handlers) {
            return null;
        }

        // Grab the handlers and call them.
        $result = null;
        foreach ($handlers as $callbacks) {
            foreach ($callbacks as $callback) {
                $result = call_user_func_array($callback, $args);
            }
        }
        return $result;
    }

    /**
     * Chain several event handlers together.
     *
     * This method will fire the first handler and pass its result as the first argument
     * to the next event handler and so on. A chained event handler can have more than one parameter,
     * but must have at least one parameter.
     *
     * @param string $event The name of the event to fire.
     * @param mixed $value The value to pass into the filter.
     * @return mixed The result of the chained event or `$value` if there were no handlers.
     */
    public static function fireFilter($event, $value) {
        $handlers = self::getHandlers($event);
        if (!$handlers) {
            return $value;
        }

        $args = array_slice(func_get_args(), 1);
        foreach ($handlers as $callbacks) {
            foreach ($callbacks as $callback) {
                $value = call_user_func_array($callback, $args);
                $args[0] = $value;
            }
        }
        return $value;
    }

    /**
     * Fire an event meant to override another function or method.
     * The event handler should return true if it wants to say it is overridding the method.
     *
     * @param string $event The name of the event to fire.
     * @return bool Whether or not the event has been overridden.
     */
//   public static function fireOverride($event) {
//      $handlers = self::getHandlers($event);
//      if ($handlers === false)
//         return $value;
//
//      $args = array_slice(func_get_args(), 1);
//      foreach ($handlers as $callbacks) {
//         foreach ($callbacks as $callback) {
//            $overridden = call_user_func_array($callback, $args);
//            if ($overridden)
//               return true;
//         }
//      }
//      return false;
//   }

    /**
     * Checks if a function exists or there is a replacement event for it.
     *
     * @param string $function_name The function name.
     * @param bool $only_events Whether or not to only check events.
     * @return boolean Returns `true` if the function given by `function_name` has been defined, `false` otherwise.
     * @see http://ca1.php.net/manual/en/function.function-exists.php
     */
    public static function functionExists($function_name, $only_events = false) {
        if (!$only_events && function_exists($function_name)) {
            return true;
        } else {
            return static::hasHandler($function_name);
        }
    }

    /**
     * Get the event name for a callback.
     *
     * @param string|array|callable $callback The callback or an array in the form of a callback.
     * @return string The name of the callback.
     */
    protected static function getEventname($callback) {
        if (is_string($callback)) {
            return strtolower($callback);
        } elseif (is_array($callback)) {
            if (is_string($callback[0])) {
                $classname = $callback[0];
            } else {
                $classname = get_class($callback[0]);
            }
            $eventclass = trim(strrchr($classname, '\\'), '\\');
            if (!$eventclass) {
                $eventclass = $classname;
            }
            return strtolower($eventclass.'_'.$callback[1]);
        }
        return '';
    }

    /**
     * Get all of the handlers bound to an event.
     *
     * @param string $name The name of the event.
     * @return array Returns the handlers that are watching {@link $name}.
     */
    public static function getHandlers($name) {
        $name = strtolower($name);

        if (!isset(self::$handlers[$name])) {
            return [];
        }

        // See if the handlers need to be sorted.
        if (isset(self::$toSort[$name])) {
            ksort(self::$handlers[$name]);
            unset(self::$toSort[$name]);
        }

        return self::$handlers[$name];
    }

    /**
     * Checks if an event has a handler.
     *
     * @param string $event The name of the event.
     * @return bool Returns `true` if the event has at least one handler, `false` otherwise.
     */
    public static function hasHandler($event) {
        $event = strtolower($event);
        return array_key_exists($event, self::$handlers) && !empty(self::$handlers[$event]);
    }

    /**
     * Checks if a class method exists or there is a replacement event for it.
     *
     * @param mixed $object An object instance or a class name.
     * @param string $method_name The method name.
     * @param bool $only_events Whether or not to only check events.
     * @return boolean Returns `true` if the method given by method_name has been defined for the given object,
     * `false` otherwise.
     * @see http://ca1.php.net/manual/en/function.method-exists.php
     */
    public static function methodExists($object, $method_name, $only_events = false) {
        if (!$only_events && method_exists($object, $method_name)) {
            return true;
        } else {
            // Check to see if there is an event bound to the method.
            $event_name = self::getEventname([$object, $method_name]);
            return static::hasHandler($event_name);
        }
    }

    /**
     * Clear all of the event handlers.
     *
     * This method resets the event object to its original state.
     */
    public static function reset() {
        self::$handlers = [];
        self::$toSort = [];
    }
}


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

namespace Garden\Password;

/**
 * Implements the password hashing algorithm of Joomla.
 */
class JoomlaPassword 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($password.$salt);

        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\Db;

/**
 * Represents an string that won't be escaped in queries.
 */
class Literal {
    /// Properties ///

    /**
     * @var array An array that maps driver names to literals.
     */
    protected $driverValues = [];

    /// Methods ///

    /**
     * Initialize an instance of the {@link Literal} class.
     *
     * @param string|array $value Either a string default value or an array in the form
     * `['driver' => 'literal']` to specify different literals for different database drivers.
     */
    public function __construct($value) {
        if (is_string($value)) {
            $this->driverValues['default'] = $value;
        } elseif (is_array($value)) {
            foreach ($value as $key => $value) {
                $this->driverValues[$this->normalizeKey($key)] = $value;
            }
        }
    }

    /**
     * Get the literal value.
     *
     * @param string $driver The name of the database driver to fetch the literal for.
     * @return string Returns the value for the specific driver, the default literal, or "null" if there is no default.
     */
    public function getValue($driver = 'default') {
        $driver = $this->normalizeKey($driver);

        if (isset($this->driverValues[$driver])) {
            return $this->driverValues[$driver];
        } elseif (isset($this->driverValues['default'])) {
            return $this->driverValues['default'];
        } else {
            return 'null';
        }
    }

    /**
     * Set the literal value.
     *
     * @param string $value The new value to set.
     * @param string $driver The name of the database driver to set the value for.
     * @return Literal Returns $this for fluent calls.
     */
    public function setValue($value, $driver = 'default') {
        $driver = $this->normalizeKey($driver);
        $this->driverValues[$driver] = $value;
        return $this;
    }

    /**
     * Normalize the driver name for the drivers array.
     *
     * @param string $key The name of the driver.
     * @return string Returns the driver name normalized.
     */
    protected function normalizeKey($key) {
        return rtrim_substr(strtolower(basename($key)), 'db');
    }

    /**
     * Create and return a {@link Literal} object.
     *
     * @param string|array $value The literal value(s) as passed to {@link Literal::__construct()}.
     * @return Literal Thew new literal value.
     */
    public static function value($value) {
        $literal = new Literal($value);
        return $literal;
    }

    /**
     * Creat and return a {@link Literal} object that will query the current unix timesatmp.
     *
     * @return Literal Returns the timestamp expression.
     */
    public static function timestamp() {
        $literal = new Literal([
            'mysql' => 'unix_timestamp()',
            'sqlite' => "date('now', 'unixepoch')",
            'posgresql' => 'extract(epoch from now())',
            'default' => time()
        ]);
        return $literal;
    }
}


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

namespace Garden\Password;

/**
 * Implements the vBulletin password hash algorithms.
 *
 * VBulletin itself stores password salts in a separate database column. In order to use a vBulletin password with this
 * class you must concatenate the salt to the end of the password hash. The vBulletin password hashing algorithm is a
 * poor choice for your passwords because it uses md5 which is very insecure.
 *
 * We recommend using this class to validate existing vBulletin passwords, but then rehashing them with a different
 * algorithm.
 */
class VbulletinPassword implements IPassword {

    /**
     * Hashes a plaintext password.
     *
     * @param string $password The password to hash.
     * @return string Returns the hashed password.
     */
    public function hash($password) {
        $salt = base64_encode(openssl_random_pseudo_bytes(12));
        return $this->hashRaw($password, $salt).$salt;
    }

    protected function hashRaw($password, $salt) {
        $hash = md5(md5($password).$salt);
        return $hash;
    }

    /**
     * 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 strlen($hash) < 37;
    }

    /**
     * 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) {
        list($stored_hash, $salt) = $this->splitSalt($hash);
        $calculated_hash = $this->hashRaw($password, $salt);
        $result = $calculated_hash === $stored_hash;
        return $result;
    }

    /**
     * Split the salt and hash from a single hash.
     *
     * @param string $hash The hash to split.
     * @return array An array in the form `[$hash, $salt]`.
     */
    public function splitSalt($hash) {
        // The hash is in the form: <32 char hash><salt>.
        $salt_length = strlen($hash) - 32;
        $salt = trim(substr($hash, -$salt_length, $salt_length));
        $stored_hash = substr($hash, 0, strlen($hash) - $salt_length);

        return [$stored_hash, $salt];
    }
}


<?php

namespace Garden;

/**
 * Application configuration management.
 *
 * This class provides access to the application configuration information through one or more config files.
 * You can load/save config files in several formats. The file extension of the file determines what format will the file will use.
 * The following file formats are supported.
 *
 * - javascript object notation (json): .json or .json.php
 * - php source code: .php
 * - php serialized arrays: .ser or .ser.php
 * - yaml: .yml or .yml.php
 *
 * When using config files we recommend always using the .*.php extension so that the file cannot be read through its url.
 *
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009 Vanilla Forums Inc.
 * @license LGPL-2.1
 * @package Vanilla
 * @since 1.0
 */
class Config {
    /// Properties ///

    /**
     * @var array The config data.
     */
    protected static $data = [];

    /**
     * @var string The default path to load/save to.
     */
    protected static $defaultPath;

    /// Methods ///

    /**
     * Get or set the default path.
     *
     * @param string $value Pass a value to set a new default path.
     * @return string Returns the current default path.
     */
    public static function defaultPath($value = '') {
        if ($value) {
            self::$defaultPath = $value;
        } elseif (!self::$defaultPath) {
            self::$defaultPath = PATH_ROOT.'/conf/config.json.php';
        }
        return self::$defaultPath;
    }

    /**
     * Return all of the config data.
     *
     * @return array Returns an array of config data.
     */
    public static function data() {
        return self::$data;
    }

    /**
     * Get a setting from the config.
     *
     * @param string $key The config key.
     * @param mixed $default The default value if the config file doesn't exist.
     * @return mixed The value at {@link $key} or {@link $default} if the key isn't found.
     * @see \config()
     */
    public static function get($key, $default = null) {
        if (array_key_exists($key, self::$data)) {
            return self::$data[$key];
        } else {
            return $default;
        }
    }

    /**
     * Encode an array in ini format.
     * The resulting array will work with parse_ini_file() and parse_ini_string().
     *
     * @param array $data A flat, associative array of data.
     * @return string The data in ini format.
     */
//    public static function iniEncode($data) {
//        ksort($data, SORT_NATURAL | SORT_FLAG_CASE);
//
//        $result = '';
//
//        $lastSection = null;
//
//        foreach ($data as $key => $value) {
//            $section = trim(strstr($key, '.', true), '.');
//
//            if ($section !== $lastSection) {
//                if ($section) {
//                    $result .= "\n[$section]\n";
//                }
//                $lastSection = $section;
//            }
//
//            $result .= $key . ' = ';
//
//            if (is_bool($value)) {
//                $str = $value ? 'true' : 'false';
//            } elseif (is_numeric($value)) {
//                $str = $value;
//            } elseif (is_string($value)) {
//                $str = '"' . addcslashes($value, "\"") . '"';
//            }
//            $result .= $str . "\n";
//        }
//
//        return $result;
//    }

    /**
     * Load configuration data from a file.
     *
     * @param string $path An optional path to load the file from.
     * @param string $path If true the config will be put under the current config, not over it.
     * @param string $php_var The name of the php variable to load from if using the php file type.
     */
    public static function load($path = '', $underlay = false, $php_var = 'config') {
        if (!$path) {
            $path = self::$defaultPath;
        }

        $loaded = array_load($path, $php_var);

        if (empty($loaded)) {
            return;
        }

        if (!is_array(self::$data)) {
            self::$data = [];
        }

        if ($underlay) {
            self::$data = array_replace($loaded, self::$data);
        } else {
            self::$data = array_replace(self::$data, $loaded);
        }
    }

    /**
     * Save data to the config file.
     *
     * @param array $data The config data to save.
     * @param string $path An optional path to save the data to.
     * @param string $php_var The name of the php variable to load from if using the php file type.
     * @return bool Returns true if the save was successful or false otherwise.
     * @throws \InvalidArgumentException Throws an exception when the saved data isn't an array.
     */
    public static function save($data, $path = null, $php_var = 'config') {
        if (!is_array($data)) {
            throw new \InvalidArgumentException('Config::save(): Argument #1 is not an array.', 400);
        }

        if (!$path) {
            $path = static::defaultPath();
        }

        // Load the current config information so we know what to replace.
        $config = array_load($path, $php_var);
        // Merge the new config into the current config.
        $config = array_replace($config, $data);
        // Remove null config values.
        $config = array_filter($config, function ($value) {
            return $value !== null;
        });

        ksort($config, SORT_NATURAL | SORT_FLAG_CASE);

        $result = array_save($config, $path, $php_var);
        return $result;
    }
}


<?php

namespace Garden;

use Garden\Exception\NotFoundException;

class Application {
    /// Properties ///
    protected static $instances;

    /**
     * @var Request The current request.
     */
    public $request;

    /**
     *
     * @var Response The current response.
     */
    public $response;

    /**
     * @var array An array of route objects.
     */
    protected $routes;

    /// Methods ///

    public function __construct($name = 'default') {
        $this->routes = array();

        self::$instances[$name] = $this;
    }

    public static function instance($name = 'default') {
        if (!isset(self::$instances[$name])) {
            self::$instances[$name] = new Application($name);
        }
        return self::$instances[$name];
    }

    /**
     * Get all of the matched routes for a request.
     *
     * @param Request $request The {@link Request} to match against.
     * @return array An array of arrays corresponding to matching routes and their args.
     */
    public function matchRoutes(Request $request) {
        $result = array();

        foreach ($this->routes as $route) {
            $matches = $route->matches($request, $this);
            if ($matches) {
                $result[] = array($route, $matches);
            }
        }
        return $result;
    }

    /**
     * Add a new route.
     *
     * @param string|Route $pathOrRoute The path to the route or the {@link Route} object itself.
     * @param callable|string|null $callback Either a callback to map the route to or a string representing
     * a format for {@link sprintf()}.
     * @return Route Returns the route that was added.
     * @throws \InvalidArgumentException Throws an exceptio if {@link $path} isn't a string or {@link Route}.
     */
    public function route($pathOrRoute, $callback = null) {
        if (is_object($pathOrRoute) && $pathOrRoute instanceof Route) {
            $route = $pathOrRoute;
        } elseif (is_string($pathOrRoute) && $callback !== null) {
            $route = Route::create($pathOrRoute, $callback);
        } else {
            throw new \InvalidArgumentException("Argument #1 must be either a Garden\\Route or a string.", 500);
        }
        $this->routes[] = $route;
        return $route;
    }

    /**
     * Route to a GET request.
     *
     * @param string $pattern The url pattern to match.
     * @param callable $callback The callback to execute on the route.
     * @return CallbackRoute Returns the new route.
     */
    public function get($pattern, callable $callback) {
        return $this->route($pattern, $callback)->methods('GET');
    }

    /**
     * Route to a POST request.
     *
     * @param string $pattern The url pattern to match.
     * @param callable $callback The callback to execute on the route.
     * @return CallbackRoute Returns the new route.
     */
    public function post($pattern, callable $callback) {
        return $this->route($pattern, $callback)->methods('POST');
    }

    /**
     * Route to a PUT request.
     *
     * @param string $pattern The url pattern to match.
     * @param callable $callback The callback to execute on the route.
     * @return CallbackRoute Returns the new route.
     */
    public function put($pattern, callable $callback) {
        return $this->route($pattern, $callback)->methods('PUT');
    }

    /**
     * Route to a PATCH request.
     *
     * @param string $pattern The url pattern to match.
     * @param callable $callback The callback to execute on the route.
     * @return CallbackRoute Returns the new route.
     */
    public function patch($pattern, callable $callback) {
        return $this->route($pattern, $callback)->methods('PATCH');
    }

    /**
     * Route to a DELETE request.
     *
     * @param string $pattern The url pattern to match.
     * @param callable $callback The callback to execute on the route.
     * @return CallbackRoute Returns the new route.
     */
    public function delete($pattern, callable $callback) {
        return $this->route($pattern, $callback)->methods('DELETE');
    }

    /**
     * Run the application against a {@link Request}.
     *
     * @param Request|null $request A {@link Request} to run the application against or null to run against a request
     * on the current environment.
     * @return mixed Returns a response appropriate to the request's ACCEPT header.
     */
    public function run(Request $request = null) {
        if ($request === null) {
            $request = new Request();
        }
        $this->request = $request;
        $requestBak = Request::current($request);

        // Grab all of the matched routes.
        $routes = $this->matchRoutes($this->request);

        // Try all of the matched routes in turn.
        $dispatched = false;
        $result = null;
        try {
            foreach ($routes as $route_args) {
                list($route, $args) = $route_args;

                try {
                    // Dispatch the first matched route.
                    ob_start();
                    $response = $route->dispatch($request, $args);
                    $body = ob_get_clean();

                    $result = [
                        'routing' => $args,
                        'response' => $response,
                        'body' => $body
                    ];

                    // Once a route has been successfully dispatched we break and don't dispatch anymore.
                    $dispatched = true;
                    break;
                } catch (Exception\Pass $pex) {
                    ob_end_clean();
                    // If the route throws a pass then continue on to the next route.
                    continue;
                } catch (\Exception $ex) {
                    ob_end_clean();
                    throw $ex;
                }
            }

            if (!$dispatched) {
                throw new NotFoundException();
            }
        } catch (\Exception $ex) {
            $result = $ex;
        }

        $result = $this->finalize($result);
        Request::current($requestBak);

        return $result;
    }

    /**
     * Finalize the result from a dispatch.
     *
     * @param mixed $result The result of the dispatch.
     * @return mixed Returns relevant debug data or processes the response.
     * @throws \Exception Throws an exception when finalizing internal content types and the result is an exception.
     */
    protected function finalize($result) {
        $response = Response::create($result);
        $response->meta(['request' => $this->request], true);
        $response->contentTypeFromAccept($this->request->getEnv('HTTP_ACCEPT'));
        $response->contentAsset($this->request->getEnv('HTTP_X_ASSET'));

        $contentType = $response->contentType();

        if ($this->request->getMethod() === Request::METHOD_HEAD) {
            $response->flushHeaders();
            return null;
        }

        // Check for known response types.
        switch ($contentType) {
            case 'application/internal':
                if ($result instanceof \Exception) {
                    throw $result;
                }

                if ($response->contentAsset() === 'response') {
                    return $response;
                } else {
                    return $response->jsonSerialize();
                }
                // No break because everything returns.
            case 'application/json':
                $response->flushHeaders();
                echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
                break;
            default:
                $data = $response->data();
                if (is_string($data)) {
                    $response->flushHeaders();
                    echo $data;
                } else {
                    $response->status(415);
                    $response->flushHeaders();
                    echo "Unsupported response type: $contentType";
                }
                break;
        }
        return null;
    }
}


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

namespace Garden;

use Garden\Exception\NotFoundException;
use Garden\Exception\MethodNotAllowedException;

/**
 * Maps paths to controllers that act as RESTful resources.
 *
 * The following are examples of urls that will map using this resource route.
 * - METHOD /controller/:id -> method
 * - METHOD /controller/:id/action -> methodAction
 * - GET /controller -> index
 * - METHOD /controller/action -> methodAction
 */
class ResourceRoute extends Route {
    protected $controllerPattern = '%sApiController';

    /**
     * @var array An array of controller method names that can't be dispatched to by name.
     */
    public static $specialActions = ['delete', 'get', 'index', 'initialize', 'options', 'patch', 'post'];

    /**
     * Initialize an instance of the {@link ResourceRoute} class.
     *
     * @param string $root The url root of that this route will map to.
     * @param string $controllerPattern A pattern suitable for {@link sprintf} that will map
     * a path to a controller object name.
     */
    public function __construct($root = '', $controllerPattern = null) {
        $this->pattern($root);

        if ($controllerPattern !== null) {
            $this->controllerPattern = $controllerPattern;
        }
    }

    /**
     * Dispatch the route.
     *
     * @param Request $request The current request we are dispatching against.
     * @param array &$args The args to pass to the dispatch.
     * These are the arguments returned from {@link Route::matches()}.
     * @return mixed Returns the result from the controller method.
     * @throws NotFoundException Throws a 404 when the path doesn't map to a controller action.
     * @throws MethodNotAllowedException Throws a 405 when the http method does not map to a controller action,
     * but other methods do.
     */
    public function dispatch(Request $request, array &$args) {
        $controller = new $args['controller']();
        $method = strtolower($args['method']);
        $pathArgs = $args['pathArgs'];

        // See if the initialize method can take any of the parameters.
        $initialize = false;
        $initArgs = [];
        $initParams = [];
        $actionIndex = 0;
        if (method_exists($controller, 'initialize')) {
            $initialize = true;
            $initMethod = new \ReflectionMethod($controller, 'initialize');

            // Walk through the initialize() arguments and supply all of the ones that are required.
            foreach ($initMethod->getParameters() as $initParam) {
                $i = $initParam->getPosition();
                $initArgs[$i] = null;
                $initParams[$i] = $initParam->getName();

                if ($initParam->isDefaultValueAvailable()) {
                    $initArgs[$i] = $initParam->getDefaultValue();
                } elseif (!isset($pathArgs[$i])) {
                    throw new NotFoundException('Page', "Missing argument $i for {$args['controller']}::initialize().");
                } elseif ($this->failsCondition($initParams[$i], $pathArgs[$i])) {
                    // This isn't a valid value for a required parameter.
                    throw new NotFoundException('Page', "Invalid argument '{$pathArgs[$i]}' for {$initParams[$i]}.");
                } else {
                    $initArgs[$i] = $pathArgs[$i];
                    $actionIndex = $i + 1;
                }
            }
        }

        $action = '';
        $initComplete = false;
        for ($i = $actionIndex; $i < count($pathArgs); $i++) {
            $pathArg = $pathArgs[$i];

            $action = $this->actionExists($controller, $pathArg, $method, true);
            if ($action) {
                // We found an action and can move on to the next step.
                $actionIndex = $i + 1;
                break;
            } else {
                // This is a method argument. See whether to add it to the initialize method or not.
                if (!$initComplete && $actionIndex < count($initArgs)) {
                    // Make sure the argument is valid.
                    if ($this->failsCondition($initParams[$actionIndex], $pathArg)) {
                        // The argument doesn't validate against its condition so this can't be an init argument.
                        $initComplete = true;
                    } else {
                        // The argument can be added to initialize().
                        $initArgs[$actionIndex] = $pathArg;
                        $actionIndex++;
                    }
                }
            }
        }

        if (!$action) {
            // There is no specific action at this point so we have to check for a resource action.
            if ($actionIndex === 0) {
                $actions = ['get' => 'index', 'post' => 'post', 'options' => 'options'];
            } else {
                $actions = ['get' => 'get', 'patch' => 'patch', 'put' => 'put', 'delete' => 'delete'];
            }

            if (!isset($actions[$method])) {
                if ($actionIndex < count($pathArgs)) {
                    // There are more path args left to go then just throw a 404.
                    throw new NotFoundException();
                } else {
                    // The http method isn't allowed.
                    $allowed = array_keys($actions);
                    throw new MethodNotAllowedException($method, $allowed);
                }
            }

            $action = $actions[$method];
            if (!$this->actionExists($controller, $action)) {
                // If there are more path args left to go then just throw a 404.
                if ($actionIndex < count($pathArgs)) {
                    throw new NotFoundException();
                }

                // Check to see what actions are allowed.
                unset($actions[$method]);
                $allowed = [];
                foreach ($actions as $otherMethod => $otherAction) {
                    if ($this->actionExists($controller, $otherAction)) {
                        $allowed[] = strtoupper($otherMethod);
                    }
                }

                if (!empty($allowed)) {
                    // Other methods are allowed. Show them.
                    throw new MethodNotAllowedException($method, $allowed);
                } else {
                    // The action does not exist at all.
                    throw new NotFoundException();
                }
            }
        }

        // Make sure the number of action arguments match the action method.
        $actionMethod = new \ReflectionMethod($controller, $action);
        $action = $actionMethod->getName(); // make correct case.
        $actionParams = $actionMethod->getParameters();
        $actionArgs = array_slice($pathArgs, $actionIndex);

        if (count($actionArgs) > count($actionParams)) {
            // Double check to see if the first argument might be a method, but one that isn't allowed.
            $allowed = $this->allowedMethods($controller, $actionArgs[0]);
            if (count($allowed) > 0) {
                // At least one method was allowed for this action so throw an exception.
                throw new MethodNotAllowedException($method, $allowed);
            }

            // Too many arguments were passed.
            throw new NotFoundException();
        }
        // Fill in missing default parameters.
        foreach ($actionParams as $param) {
            $i = $param->getPosition();
            $paramName = $param->getName();

            if ($this->isMapped($paramName)) {
                // The parameter is mapped to a specific request item.
                array_splice($actionArgs, $i, 0, [$this->mappedData($paramName, $request)]);
            } elseif (!isset($actionArgs[$i]) || !$actionArgs[$i]) {
                if ($param->isDefaultValueAvailable()) {
                    $actionArgs[$i] = $param->getDefaultValue();
                } else {
                    throw new NotFoundException('Page', "Missing argument $i for {$args['controller']}::$action().");
                }
            } elseif ($this->failsCondition($paramName, $actionArgs[$i])) {
                throw new NotFoundException('Page', "Invalid argument '{$actionArgs[$i]}' for {$paramName}.");
            }
        }

        $args = array_replace($args, [
            'init' => $initialize,
            'initArgs' => $initArgs,
            'action' => $action,
            'actionArgs' => $actionArgs
        ]);

        if ($initialize) {
            Event::callUserFuncArray([$controller, 'initialize'], $initArgs);
        }

        $result = Event::callUserFuncArray([$controller, $action], $actionArgs);
        return $result;
    }

    /**
     * Tests whether or not a string is a valid identifier.
     *
     * @param string $str The string to test.
     * @return bool Returns true if {@link $str} can be used as an identifier.
     */
    protected static function isIdentifier($str) {
        if (preg_match('`[_a-zA-Z][_a-zA-Z0-9]{0,30}`i', $str)) {
            return true;
        }
        return false;
    }

    /**
     * Tests whether a controller action exists.
     *
     * @param object $object The controller object that the method should be on.
     * @param string $action The name of the action.
     * @param string $method The http method.
     * @param bool $special Whether or not to blacklist the special methods.
     * @return string Returns the name of the action method or an empty string if it doesn't exist.
     */
    protected function actionExists($object, $action, $method = '', $special = false) {
        if ($special && in_array($action, self::$specialActions)) {
            return '';
        }

        // Short circuit on a badly named action.
        if (!$this->isIdentifier($action)) {
            return '';
        }

        if ($method && $method !== $action) {
            $calledAction = $method.$action;
            if (Event::methodExists($object, $calledAction)) {
                return $calledAction;
            }
        }
        $calledAction = $action;
        if (Event::methodExists($object, $calledAction)) {
            return $calledAction;
        }
        return '';
    }

    /**
     * Find the allowed http methods on a controller object.
     *
     * @param object $object The object to test.
     * @param string $action The action to test.
     * @return array Returns an array of allowed http methods.
     */
    protected function allowedMethods($object, $action) {
        $allMethods = [
            Request::METHOD_GET, Request::METHOD_POST, Request::METHOD_DELETE,
            Request::METHOD_PATCH, Request::METHOD_PUT,
            Request::METHOD_HEAD, Request::METHOD_OPTIONS
        ];

        // Special actions should not be considered.
        if (in_array($action, self::$specialActions)) {
            return [];
        }

        if (Event::methodExists($object, $action)) {
            // The controller has the named action and thus supports all methods.
            return $allMethods;
        }

        // Loop through all the methods and check to see if they exist in the form $method.$action.
        $allowed = [];
        foreach ($allMethods as $method) {
            if (Event::methodExists($object, $method.$action)) {
                $allowed[] = $method;
            }
        }
        return $allowed;
    }

    /**
     * Try matching a route to a request.
     *
     * @param Request $request The request to match the route with.
     * @param Application $app The application instantiating the route.
     * @return array|null Whether or not the route matches the request.
     * If the route matches an array of args is returned, otherwise the function returns null.
     */
    public function matches(Request $request, Application $app) {
        if (!$this->matchesMethods($request)) {
            return null;
        }

        if ($this->getMatchFullPath()) {
            $path = $request->getFullPath();
        } else {
            $path = $request->getPath();
        }

        // If this route is off of a root then check that first.
        if ($root = $this->pattern()) {
            if (stripos($path, $root) === 0) {
                // Strip the root off the path that we are examining.
                $path = substr($path, strlen($root));
            } else {
                return null;
            }
        }

        $pathParts = explode('/', trim($path, '/'));

        $controller = array_shift($pathParts);
        if (!$controller) {
            return null;
        }

        // Check to see if a class exists with the desired controller name.
        // If a controller is found then it is responsible for the route, regardless of any other parameters.
        $basename = sprintf($this->controllerPattern, ucfirst($controller));
        if (class_exists('\Garden\Addons', false)) {
            list($classname) = Addons::classMap($basename);

            // TODO: Optimize this second check.
            if (!$classname && class_exists($basename)) {
                $classname = $basename;
            }
        } elseif (class_exists($basename)) {
            $classname = $basename;
        } else {
            $classname = '';
        }

        if (!$classname) {
            return null;
        }

        $result = array(
            'controller' => $classname,
            'method' => $request->getMethod(),
            'path' => $path,
            'pathArgs' => $pathParts,
            'query' => $request->getQuery()
        );
        return $result;
    }

    /**
     * Tests whether an argument fails against a condition.
     *
     * @param string $name The name of the parameter.
     * @param string $value The value of the argument.
     * @return bool|null Returns one of the following:
     * - true: The condition fails.
     * - false: The condition passes.
     * - null: There is no condition.
     */
    protected function failsCondition($name, $value) {
        $name = strtolower($name);
        if (isset($this->conditions[$name])) {
            $regex = $this->conditions[$name];
            return !preg_match("`^$regex$`", $value);
        }

        if (isset(self::$globalConditions[$name])) {
            $regex = self::$globalConditions[$name];
            return !preg_match("`^$regex$`", $value);
        }

        return null;
    }
}