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

namespace Garden;

use JsonSerializable;

/**
 * A class that contains the information in an http response.
 */
class Response implements JsonSerializable {
    /// Properties ///

    /**
     * An array of cookie sets. This array is in the form:
     *
     * ```
     * array (
     *     'name' => [args for setcookie()]
     * )
     * ```
     *
     * @var array An array of cookies sets.
     */
    protected $cookies = [];

    /**
     * An array of global cookie sets.
     *
     * This array is for code the queue up cookie changes before the response has been created.
     *
     * @var array An array of cookies.
     */
    protected static $globalCookies;

    /**
     * @var Response The current response.
     */
    protected static $current;

    /**
     * @var array An array of meta data that is not related to the response data.
     */
    protected $meta = [];

    /**
     * @var array An array of response data.
     */
    protected $data = [];

    /**
     * @var string The asset that should be rendered.
     */
    protected $contentAsset;

    /**
     * @var string The default cookie domain.
     */
    public $defaultCookieDomain;

    /**
     * @var string The default cookie path.
     */
    public $defaultCookiePath;

    /**
     * @var array An array of http headers.
     */
    protected $headers = array();

    /**
     * @var array An array of global http headers.
     */
    protected static $globalHeaders;

    /**
     * @var int HTTP status code
     */
    protected $status = 200;

    /**
     * @var array HTTP response codes and messages.
     */
    protected static $messages = array(
        // Informational 1xx
        100 => 'Continue',
        101 => 'Switching Protocols',
        // Successful 2xx
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        // Redirection 3xx
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        306 => '(Unused)',
        307 => 'Temporary Redirect',
        // Client Error 4xx
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        418 => 'I\'m a teapot',
        422 => 'Unprocessable Entity',
        423 => 'Locked',
        // Server Error 5xx
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported'
    );

    /// Methods ///

    /**
     * Gets or sets the response that is currently being processed.
     *
     * @param Response|null $response Set a new response or pass null to get the current response.
     * @return Response Returns the current response.
     */
    public static function current(Response $response = null) {
        if ($response !== null) {
            self::$current = $response;
        } elseif (self::$current === null) {
            self::$current = new Response();
        }

        return self::$current;
    }

    /**
     * Create a Response from a variety of data.
     *
     * @param mixed $result The result to create the response from.
     * @return Response Returns a {@link Response} object.
     */
    public static function create($result) {
        if ($result instanceof Response) {
            return $result;
        } elseif ($result instanceof Exception\ResponseException) {
            /* @var Exception\ResponseException $result */
            return $result->getResponse();
        }

        $response = new Response();

        if ($result instanceof Exception\ClientException) {
            /* @var Exception\ClientException $cex */
            $cex = $result;
            $response->status($cex->getCode());
            $response->headers($cex->getHeaders());
            $response->data($cex->jsonSerialize());
        } elseif ($result instanceof \Exception) {
            /* @var \Exception $ex */
            $ex = $result;
            $response->status($ex->getCode());
            $response->data([
                'exception' => $ex->getMessage(),
                'code' => $ex->getCode()
            ]);
        } elseif (is_array($result)) {
            if (count($result) === 3 && isset($result[0], $result[1], $result[2])) {
                // This is a rack style response in the form [code, headers, body].
                $response->status($result[0]);
                $response->headers($result[1]);
                $response->data($result[2]);
            } elseif (array_key_exists('response', $result)) {
                $resultResponse = $result['response'];
                if (!$resultResponse) {
                    $response->data($result['body']);
                } else {
                    // This is a dispatched response.
                    $response = static::create($resultResponse);
                }

                // Set the rest of the result to the response context.
                unset($result['response']);
                $response->meta($result, true);
            } else {
                $response->data($result);
            }
        } else {
            $response->status(422);
            $response->data([
                'exception' => "Unknown result type for response.",
                'code' => $response->status()
            ]);
        }
        return $response;
    }

    /**
     * Gets or sets the content type.
     *
     * @param string|null $value The new content type or null to get the current content type.
     * @return Response|string Returns the current content type or `$this` for fluent calls.
     */
    public function contentType($value = null) {
        if ($value === null) {
            return $this->headers('Content-Type');
        }

        return $this->headers('Content-Type', $value);
    }

    /**
     * Gets or sets the asset that will be rendered in the response.
     *
     * @param string $asset Set a new value or pass `null` to get the current value.
     * @return Response|string Returns the current content asset or `$this` when settings.
     */
    public function contentAsset($asset = null) {
        if ($asset !== null) {
            $this->contentAsset = $asset;
            return $this;
        }

        return $this->contentAsset;
    }

    /**
     * Set the content type from an accept header.
     *
     * @param string $accept The value of the accept header.
     * @return Response $this Returns `$this` for fluent calls.
     */
    public function contentTypeFromAccept($accept) {
        if (!empty($this->headers['Content-Type'])) {
            return;
        }

        $accept = strtolower($accept);
        if (strpos($accept, ',') === false) {
            list($contentType) = explode(';', $accept);
        } elseif (strpos($accept, 'text/html') !== false) {
            $contentType = 'text/html';
        } elseif (strpos($accept, 'application/rss+xml' !== false)) {
            $contentType = 'application/rss+xml';
        } elseif (strpos($accept, 'text/plain')) {
            $contentType = 'text/plain';
        } else {
            $contentType = 'text/html';
        }
        $this->contentType($contentType);
        return $this;
    }

    /**
     * Translate an http code to its corresponding status message.
     *
     * @param int $statusCode The http status code.
     * @param bool $header Whether or not the result should be in a form that can be passed to {@link header}.
     * @return string Returns the status message corresponding to {@link $code}.
     */
    public static function statusMessage($statusCode, $header = false) {
        $message = val($statusCode, self::$messages, 'Unknown');

        if ($header) {
            return "HTTP/1.1 $statusCode $message";
        } else {
            return $message;
        }
    }

    /**
     * Gets or sets a cookie.
     *
     * @param string $name The name of the cookie.
     * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information.
     * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch.
     * @param string $path The path on the server in which the cookie will be available on.
     * If set to '/', the cookie will be available within the entire {@link $domain}.
     * @param string $domain The domain that the cookie is available to.
     * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
     * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol.
     * @return $this|mixed Returns the cookie settings at {@link $name} or `$this` when setting a cookie for fluent calls.
     */
    public function cookies($name, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) {
        if ($value === false) {
            return val($name, $this->cookies);
        }

        $this->cookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly];
        return $this;
    }

    /**
     * Gets or sets a global cookie.
     *
     * Global cookies are used when you want to set a cookie, but a {@link Response} has not been created yet.
     *
     * @param string $name The name of the cookie.
     * @param bool $value The value of the cookie. This value is stored on the clients computer; do not store sensitive information.
     * @param int $expires The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch.
     * @param string $path The path on the server in which the cookie will be available on.
     * If set to '/', the cookie will be available within the entire {@link $domain}.
     * @param string $domain The domain that the cookie is available to.
     * @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
     * @param bool $httponly When TRUE the cookie will be made accessible only through the HTTP protocol.
     * @return mixed|null Returns the cookie settings at {@link $name} or `null` when setting a cookie.
     */
    public static function globalCookies($name = null, $value = false, $expires = 0, $path = null, $domain = null, $secure = false, $httponly = false) {
        if (self::$globalCookies === null) {
            self::$globalCookies = [];
        }

        if ($name === null) {
            return self::$globalCookies;
        }

        if ($value === false) {
            return val($name, self::$globalCookies);
        }

        self::$globalCookies[$name] = [$value, $expires, $path, $domain, $secure, $httponly];
        return null;
    }

    /**
     * Get or set the meta data for the response.
     *
     * The meta is an array of data that is unrelated to the response data.
     *
     * @param array|null $meta Pass a new meta data value or `null` to get the current meta array.
     * @param bool $merge Whether or not to merge new data with the current data when setting.
     * @return $this|array Returns either the meta or `$this` when setting the meta data.
     */
    public function meta($meta = null, $merge = false) {
        if ($meta !== null) {
            if ($merge) {
                $this->meta = array_merge($this->meta, $meta);
            } else {
                $this->meta = $meta;
            }
            return $this;
        } else {
            return $this->meta;
        }
    }

    /**
     * Get or set the data for the response.
     *
     * @param array|null $data Pass a new data value or `null` to get the current data array.
     * @param bool $merge Whether or not to merge new data with the current data when setting.
     * @return Response|array Returns either the data or `$this` when setting the data.
     */
    public function data($data = null, $merge = false) {
        if ($data !== null) {
            if ($merge) {
                $this->data = array_merge($this->data, $data);
            } else {
                $this->data = $data;
            }
            return $this;
        } else {
            return $this->data;
        }
    }

    /**
     * Gets or sets headers.
     *
     * @param string|array $name The name of the header or an array of headers.
     * @param string|null $value A new value for the header or null to get the current header.
     * @param bool $replace Whether or not to replace the current header or append.
     * @return Response|string Returns the value of the header or `$this` for fluent calls.
     */
    public function headers($name, $value = null, $replace = true) {
        $headers = static::splitHeaders($name, $value);

        if (is_string($headers)) {
            return val($headers, $this->headers);
        }

        foreach ($headers as $name => $value) {
            if ($replace || !isset($this->headers[$name])) {
                $this->headers[$name] = $value;
            } else {
                $this->headers[$name] = array_merge((array)$this->headers, [$value]);
            }
        }
        return $this;
    }

    /**
     * Gets or sets global headers.
     *
     * The global headers exist to allow code to queue up headers before the response has been constructed.
     *
     * @param string|array|null $name The name of the header or an array of headers.
     * @param string|null $value A new value for the header or null to get the current header.
     * @param bool $replace Whether or not to replace the current header or append.
     * @return string|array Returns one of the following:
     * - string|array: Returns the current value of the header at {@link $name}.
     * - array: Returns the entire global headers array when {@link $name} is not passed.
     * - null: Returns `null` when setting a global header.
     */
    public static function globalHeaders($name = null, $value = null, $replace = true) {
        if (self::$globalHeaders === null) {
            self::$globalHeaders = [
                'P3P' => 'CP="CAO PSA OUR"'
            ];
        }

        if ($name === null) {
            return self::$globalHeaders;
        }

        $headers = static::splitHeaders($name, $value);

        if (is_string($headers)) {
            return val($headers, self::$globalHeaders);
        }

        foreach ($headers as $name => $value) {
            if ($replace || !isset(self::$globalHeaders[$name])) {
                self::$globalHeaders[$name] = $value;
            } else {
                self::$globalHeaders[$name] = array_merge((array)self::$globalHeaders, [$value]);
            }
        }
        return null;
    }

    /**
     * Split and normalize headers into a form appropriate for {@link $headers} or {@link $globalHeaders}.
     *
     * @param string|array $name The name of the header or an array of headers.
     * @param string|null $value The header value if {@link $name} is a string.
     * @return array|string Returns one of the following:
     * - array: An array of headers.
     * - string: The header name if just a name was passed.
     * @throws \InvalidArgumentException Throws an exception if {@link $name} is not a valid string or array.
     */
    protected static function splitHeaders($name, $value = null) {
        if (is_string($name)) {
            if (strpos($name, ':') !== false) {
                // The name is in the form Header: value.
                list($name, $value) = explode(':', $name, 2);
                return [static::normalizeHeader(trim($name)) => trim($value)];
            } elseif ($value !== null) {
                return [static::normalizeHeader($name) => $value];
            } else {
                return static::normalizeHeader($name);
            }
        } elseif (is_array($name)) {
            $result = [];
            foreach ($name as $key => $value) {
                if (is_numeric($key)) {
                    // $value should be a header in the form Header: value.
                    list($key, $value) = explode(':', $value, 2);
                }
                $result[static::normalizeHeader(trim($key))] = trim($value);
            }
            return $result;
        }
        throw new \InvalidArgumentException("Argument #1 to splitHeaders() was not valid.", 422);
    }

    /**
     * Normalize a header key to the proper casing.
     *
     * Example:
     *
     * ```
     * echo static::normalizeHeader('CONTENT_TYPE');
     *
     * // Content-Type
     * ```
     *
     * @param string $name The name of the header.
     * @return string Returns the normalized header name.
     */
    public static function normalizeHeader($name) {
        static $special = [
            'etag' => 'ETag', 'p3p' => 'P3P', 'www-authenticate' => 'WWW-Authenticate',
            'x-ua-compatible' => 'X-UA-Compatible'
        ];

        $name = str_replace(['-', '_'], ' ', strtolower($name));
        if (isset($special[$name])) {
            $name = $special[$name];
        } else {
            $name = str_replace(' ', '-', ucwords($name));
        }
        return $name;
    }

    /**
     * Gets/sets the http status code.
     *
     * @param int $value The new value if setting the http status code.
     * @return int The current http status code.
     * @throws \InvalidArgumentException The new status is not a valid http status number.
     */
    public function status($value = null) {
        if ($value !== null) {
            if (!isset(self::$messages[$value])) {
                $this->headers('X-Original-Status', $value);
                $value = 500;
            }
            $this->status = (int)$value;
        }
        return $this->status;
    }

    /**
     * Flush the response to the client.
     */
    public function flush() {
        $this->flushHeaders();

        echo json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    /**
     * Flush the headers to the browser.
     *
     * @param bool $global Whether or not to merge the global headers with this response.
     */
    public function flushHeaders($global = true) {
        if (headers_sent()) {
            return;
        }

        if ($global) {
            $cookies = array_replace(static::globalCookies(), $this->cookies);
            $headers = array_replace(static::globalHeaders(), $this->headers);
        } else {
            $cookies = $this->cookies;
            $headers = $this->headers;
        }

        // Set the cookies first.
        foreach ($cookies as $name => $value) {
            setcookie(
                $name,
                $value[0],
                $value[1],
                $value[2] !== null ? $value[2] : $this->defaultCookiePath,
                $value[3] !== null ? $value[3] : $this->defaultCookieDomain,
                $value[4],
                $value[5]
            );
        }

        // Set the response code.
        header(static::statusMessage($this->status, true), true, $this->status);

        $headers = array_filter($headers);

        // The content type is a special case.
        if (isset($headers['Content-Type'])) {
            $contentType = (array)$headers['Content-Type'];
            header('Content-Type: '.reset($contentType).'; charset=utf8', true);
            unset($headers['Content-Type']);
        }

        // Flush the rest of the headers.
        foreach ($headers as $name => $value) {
            foreach ((array)$value as $hvalue) {
                header("$name: $hvalue", false);
            }
        }
    }

    /**
     * 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() {
        $asset = (string)$this->contentAsset();

        if ($asset) {
            // A specific asset was specified.
            if (strpos($asset, '.') !== false) {
                list($group, $key) = explode('.', $asset, 2);
                switch ($group) {
                    case 'meta':
                        return val($key, $this->meta);
                    case 'data':
                        return val($key, $this->data);
                    default:
                        return null;
                }
            } else {
                switch ($asset) {
                    case 'data':
                        return $this->data;
                    case 'meta':
                        return $this->meta;
                    default:
                        return null;
                }
            }
        }
        return $this->data;
    }
}


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

namespace Garden\Password;

/**
 * Implements the portable PHP password hashing framework.
 *
 * The code in this class is copied from the phppass library version 0.3 located at http://www.openwall.com/phpass/.
 * Any code copied from the phppass library is copyright the original owner.
 */
class PhpassPassword implements IPassword {
    const HASH_PHPASS = 0x00;
    const HASH_BLOWFISH = 0x01;
    const HASH_EXTDES = 0x02;
    const HASH_BEST = 0x03;

    protected $itoa64;
    protected $iteration_count_log2;
    protected $portable;
    protected $random_state;

    protected $hashMethod;

    /**
     * Initializes an instance of the of the {@link PhpPass} class.
     *
     * @param int $hashMethod The hash method to use when hashing passwords.
     * @param int $iteration_count_log2 The number of times to iterate when generating the passwords.
     */
    public function __construct($hashMethod = PhpassPassword::HASH_PHPASS, $iteration_count_log2 = 8) {
        $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

        if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) {
            $iteration_count_log2 = 8;
        }
        $this->iteration_count_log2 = $iteration_count_log2;
        $this->hashMethod = $hashMethod;

        $this->random_state = microtime();
        if (function_exists('getmypid')) {
            $this->random_state .= getmypid();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function needsRehash($hash) {
        $id = substr($hash, 0, 3);
        $portable = ($id != '$P$' && $id != '$H$');

        return $portable;
    }

    /**
     * {@inheritdoc}
     */
    public function hash($password) {
        $random = '';

        if (CRYPT_BLOWFISH == 1 && ($this->hashMethod & self::HASH_BLOWFISH)) {
            $random = $this->getRandomBytes(16);
            $hash =
                crypt($password, $this->gensaltBlowfish($random));
            if (strlen($hash) == 60) {
                return $hash;
            }
        }

        if (CRYPT_EXT_DES == 1 && ($this->hashMethod & self::HASH_EXTDES)) {
            if (strlen($random) < 3) {
                $random = $this->getRandomBytes(3);
            }
            $hash =
                crypt($password, $this->gensaltExtended($random));
            if (strlen($hash) == 20) {
                return $hash;
            }
        }

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

        # Returning '*' on error is safe here, but would _not_ be safe
        # in a crypt(3)-like function used _both_ for generating new
        # hashes and for validating passwords against existing hashes.
        return '*';
    }

    /**
     * Get a string of random bytes.
     *
     * @param int $count The number of bytes to get.
     * @return string Returns a string of the generated random bytes.
     */
    protected function getRandomBytes($count) {
        $output = '';
        if (is_readable('/dev/urandom') &&
            ($fh = @fopen('/dev/urandom', 'rb'))
        ) {
            $output = fread($fh, $count);
            fclose($fh);
        }

        if (strlen($output) < $count) {
            $output = '';
            for ($i = 0; $i < $count; $i += 16) {
                $this->random_state =
                    md5(microtime().$this->random_state);
                $output .=
                    pack('H*', md5($this->random_state));
            }
            $output = substr($output, 0, $count);
        }

        return $output;
    }

    /**
     * Generate a password salt appropriate for blowfish.
     *
     * @param string $input The random input to generate the salt from.
     * @return string The generated salt.
     */
    protected function gensaltBlowfish($input) {
        # This one needs to use a different order of characters and a
        # different encoding scheme from the one in encode64() above.
        # We care because the last character in our encoded string will
        # only represent 2 bits.  While two known implementations of
        # bcrypt will happily accept and correct a salt string which
        # has the 4 unused bits set to non-zero, we do not want to take
        # chances and we also do not want to waste an additional byte
        # of entropy.
        $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

        $output = '$2a$';
        $output .= chr(ord('0') + $this->iteration_count_log2 / 10);
        $output .= chr(ord('0') + $this->iteration_count_log2 % 10);
        $output .= '$';

        $i = 0;
        do {
            $c1 = ord($input[$i++]);
            $output .= $itoa64[$c1 >> 2];
            $c1 = ($c1 & 0x03) << 4;
            if ($i >= 16) {
                $output .= $itoa64[$c1];
                break;
            }

            $c2 = ord($input[$i++]);
            $c1 |= $c2 >> 4;
            $output .= $itoa64[$c1];
            $c1 = ($c2 & 0x0f) << 2;

            $c2 = ord($input[$i++]);
            $c1 |= $c2 >> 6;
            $output .= $itoa64[$c1];
            $output .= $itoa64[$c2 & 0x3f];
        } while (1);

        return $output;
    }

    /**
     * Generate a password salt based on the input.
     *
     * @param string $input The string to generate the salt from.
     * @return string The generated salt.
     */
    private function gensaltExtended($input) {
        $count_log2 = min($this->iteration_count_log2 + 8, 24);
        # This should be odd to not reveal weak DES keys, and the
        # maximum valid value is (2**24 - 1) which is odd anyway.
        $count = (1 << $count_log2) - 1;

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

        $output .= $this->encode64($input, 3);

        return $output;
    }

    /**
     * A custom base64 encoding function.
     *
     * @param string $input The string to encode.
     * @param int $count The number of characters to encode.
     * @return string Returns the encoded string.
     */
    protected function encode64($input, $count) {
        $output = '';
        $i = 0;
        do {
            $value = ord($input[$i++]);
            $output .= $this->itoa64[$value & 0x3f];
            if ($i < $count) {
                $value |= ord($input[$i]) << 8;
            }
            $output .= $this->itoa64[($value >> 6) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            if ($i < $count) {
                $value |= ord($input[$i]) << 16;
            }
            $output .= $this->itoa64[($value >> 12) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            $output .= $this->itoa64[($value >> 18) & 0x3f];
        } while ($i < $count);

        return $output;
    }

    /**
     * A portable version of a crypt-like algorithm.
     *
     * @param string $password The plaintext password to encrypt.
     * @param string $setting The hash prefix that defines what kind of algorithm to use.
     * @return string Returns the encrypted string.
     */
    private function cryptPrivate($password, $setting) {
        $output = '*0';
        if (substr($setting, 0, 2) == $output) {
            $output = '*1';
        }

        $id = substr($setting, 0, 3);
        # We use "$P$", phpBB3 uses "$H$" for the same thing
        if ($id != '$P$' && $id != '$H$') {
            return $output;
        }

        $count_log2 = strpos($this->itoa64, $setting[3]);
        if ($count_log2 < 7 || $count_log2 > 30) {
            return $output;
        }

        $count = 1 << $count_log2;

        $salt = substr($setting, 4, 8);
        if (strlen($salt) != 8) {
            return $output;
        }

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

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

        return $output;
    }

    /**
     * Generate a password salt based on the given input string.
     *
     * @param string $input The input string to generate the salt from.
     * @return string Returns the password salt prefixed with `$P$`.
     */
    private function gensaltPrivate($input) {
        $output = '$P$';
        $output .= $this->itoa64[min($this->iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
        $output .= $this->encode64($input, 6);

        return $output;
    }

    /**
     * {@inheritdoc}
     */
    public function verify($password, $hash) {
        if (!$hash) {
            return false;
        }
        $calc_hash = $this->cryptPrivate($password, $hash);
        if ($calc_hash[0] === '*') {
            $calc_hash = crypt($password, $hash);
        }

        return $calc_hash === $hash;
    }

    /**
     * Get the current hash method.
     *
     * @return int Returns the current hash method.
     */
    public function getHashMethod() {
        return $this->hashMethod;
    }

    /**
     * Set the current hash method.
     *
     * @param int $hashMethod The new hash mathod.
     * @return PhpassPassword Returns $this for fluent calls.
     */
    public function setHashMethod($hashMethod) {
        $this->hashMethod = $hashMethod;
        return $this;
    }
}


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

namespace Garden\Db;

use PDO;

/**
 * A {@link Db} class for connecting to SQLite.
 */
class SqliteDb extends MySqlDb {
    /**
     * {@inheritdoc}
     */
    protected function alterTable($tablename, array $alterdef, array $options = []) {
        $this->alterTableMigrate($tablename, $alterdef, $options);
    }

    /**
     * Alter a table by creating a new table and copying the old table's data to it.
     *
     * @param string $tablename The table to alter.
     * @param array $alterDef The new definition.
     * @param array $options An array of options for the migration.
     */
    protected function alterTableMigrate($tablename, array $alterDef, array $options = []) {
        $currentDef = $this->getTableDef($tablename);

        // Merge the table definitions if we aren't dropping stuff.
        if (!val(Db::OPTION_DROP, $options)) {
            $tableDef = $this->mergeTableDefs($currentDef, $alterDef);
        } else {
            $tableDef = $alterDef['def'];
        }

        // Drop all of the indexes on the current table.
        foreach (val('indexes', $currentDef, []) as $indexDef) {
            if (val('type', $indexDef, Db::INDEX_IX) === Db::INDEX_IX) {
                $this->dropIndex($indexDef['name']);
            }
        }

        $tmpTablename = $tablename.'_'.time();

        // Rename the current table.
        $this->renameTable($tablename, $tmpTablename);

        // Create the new table.
        $this->createTable($tablename, $tableDef, $options);

        // Figure out the columns that we can insert.
        $columns = array_keys(array_intersect_key($tableDef['columns'], $currentDef['columns']));

        // Build the insert/select statement.
        $sql = 'insert into '.$this->backtick($this->px.$tablename)."\n".
            $this->bracketList($columns, '`')."\n".
            $this->buildSelect($tmpTablename, [], ['columns' => $columns]);

        $this->query($sql, Db::QUERY_WRITE);

        // Drop the temp table.
        $this->dropTable($tmpTablename);
    }

    /**
     * Rename a table.
     *
     * @param string $oldname The old name of the table.
     * @param string $newname The new name of the table.
     */
    protected function renameTable($oldname, $newname) {
        $renameSql = 'alter table '.
            $this->backtick($this->px.$oldname).
            ' rename to '.
            $this->backtick($this->px.$newname);
        $this->query($renameSql, Db::QUERY_WRITE);
    }

    /**
     * Merge a table def with its alter def so that no columns/indexes are lost in an alter.
     *
     * @param array $tableDef The table def.
     * @param array $alterDef The alter def.
     * @return array The new table def.
     */
    protected function mergeTableDefs(array $tableDef, array $alterDef) {
        $result = $tableDef;

        $result['columns'] = array_merge($result['columns'], $alterDef['def']['columns']);
        $result['indexes'] = array_merge($result['indexes'], $alterDef['add']['indexes']);

        return $result;
    }

    /**
     * Drop an index.
     *
     * @param string $indexName The name of the index to drop.
     */
    protected function dropIndex($indexName) {
        $sql = 'drop index if exists '.
            $this->backtick($indexName);
        $this->query($sql, Db::QUERY_DEFINE);
    }

    /**
     * {@inheritdoc}
     */
    protected function buildInsert($tablename, array $row, $quotevals = true, $options = []) {
        if (val(Db::OPTION_UPSERT, $options)) {
            throw new \Exception("Upsert is not supported.");
        } elseif (val(Db::OPTION_IGNORE, $options)) {
            $sql = 'insert or ignore into ';
        } elseif (val(Db::OPTION_REPLACE, $options)) {
            $sql = 'insert or replace into ';
        } else {
            $sql = 'insert into ';
        }
        $sql .= $this->backtick($this->px.$tablename);

        // Add the list of values.
        $sql .=
            "\n".$this->bracketList(array_keys($row), '`').
            "\nvalues".$this->bracketList($row, $quotevals ? "'" : '');

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    protected function buildLike($column, $value, $quotevals) {
        return "$column like ".$this->quoteVal($value, $quotevals)." escape '\\'";
    }

    /**
     * {@inheritdoc}
     */
    protected function buildUpdate($tablename, array $set, array $where, $quotevals = true, array $options = []) {
        $sql = 'update '.
            (val(Db::OPTION_IGNORE, $options) ? 'or ignore ' : '').
            $this->backtick($this->px.$tablename).
            "\nset\n  ";

        $parts = [];
        foreach ($set as $key => $value) {
            $parts[] = $this->backtick($key).' = '.$this->quoteVal($value, $quotevals);
        }
        $sql .= implode(",\n  ", $parts);

        if (!empty($where)) {
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND, $quotevals);
        }

        return $sql;
    }

    /**
     * Construct a column definition string from an array defintion.
     *
     * @param string $name The name of the column.
     * @param array $def The column definition.
     * @return string Returns a string representing the column definition.
     */
    protected function columnDefString($name, array $def) {
        // Auto-increments MUST be of type integer.
        if (val('autoincrement', $def)) {
            $def['type'] = 'integer';
        }

        $result = $this->backtick($name).' '.$this->columnTypeString($def['type']);

        if (val('primary', $def) && val('autoincrement', $def)) {
//            if (val('autoincrement', $def)) {
                $result .= ' primary key autoincrement';
                $def['primary'] = true;
//            }
        } elseif (isset($def['default'])) {
            $result .= ' default '.$this->quoteVal($def['default']);
        } elseif (val('required', $def)) {
            $result .= ' not null';
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    protected function createTable($tablename, array $tabledef, array $options = []) {
        $parts = [];

        // Make sure the primary key columns are defined first and in order.
        $autoinc = false;
        if (isset($tabledef['indexes']['primary'])) {
            $pkIndex = $tabledef['indexes']['primary'];
            foreach ($pkIndex['columns'] as $column) {
                $cdef = $tabledef['columns'][$column];
                $parts[] = $this->columnDefString($column, $cdef);
                $autoinc |= val('autoincrement', $cdef, false);
                unset($tabledef['columns'][$column]);
            }
        }

        foreach ($tabledef['columns'] as $name => $cdef) {
            $parts[] = $this->columnDefString($name, $cdef);
        }

        // Add the prinary key index.
        if (isset($pkIndex) && !$autoinc) {
            $parts[] = 'primary key '.$this->bracketList($pkIndex['columns'], '`');
        }

        $fullTablename = $this->backtick($this->px.$tablename);
        $sql = "create table $fullTablename (\n  ".
            implode(",\n  ", $parts).
            "\n)";

        $this->query($sql, Db::QUERY_DEFINE);

        // Add the rest of the indexes.
        foreach (val('indexes', $tabledef, []) as $index) {
            if (val('type', $index, Db::INDEX_IX) !== Db::INDEX_PK) {
                $this->createIndex($tablename, $index, $options);
            }
        }
    }

    /**
     * Create an index.
     *
     * @param string $tablename The name of the table to create the index on.
     * @param array $indexDef The index definition.
     * @param array $options Additional options for the index creation.
     */
    public function createIndex($tablename, array $indexDef, $options = []) {
        $sql = 'create '.
            (val('type', $indexDef) === Db::INDEX_UNIQUE ? 'unique ' : '').
            'index '.
            (val(Db::OPTION_IGNORE, $options) ? 'if not exists ' : '').
            $this->buildIndexName($tablename, $indexDef).
            ' on '.
            $this->backtick($this->px.$tablename).
            $this->bracketList($indexDef['columns'], '`');

        $this->query($sql, Db::QUERY_DEFINE);
    }

    /**
     * Force a value into the appropriate php type based on its Sqlite type.
     *
     * @param mixed $value The value to force.
     * @param string $type The sqlite type name.
     * @return mixed Returns $value cast to the appropriate type.
     */
    protected function forceType($value, $type) {
        $type = strtolower($type);

        if ($type === 'null') {
            return null;
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
            return force_int($value);
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
            'numeric', 'decimal(10,5)'])) {
            return floatval($value);
        } else {
            return (string)$value;
        }
    }

    /**
     * Get the columns for tables and put them in {MySqlDb::$tables}.
     *
     * @param string $tablename The table to get the columns for or blank for all columns.
     * @return array|null Returns an array of columns if {@link $tablename} is specified, or null otherwise.
     */
    protected function getColumns($tablename = '') {
        if (!$tablename) {
            $tablenames = $this->getTablenames();
            foreach ($tablenames as $tablename) {
                $this->getColumns($tablename);
            }
        }

        $cdefs = (array)$this->query('pragma table_info('.$this->quoteVal($this->px.$tablename).')');
        if (empty($cdefs)) {
            return null;
        }

        $columns = [];
        $pk = [];
        foreach ($cdefs as $cdef) {
            $column = [
                'type' => $this->columnTypeString($cdef['type']),
                'required' => force_bool($cdef['notnull']),
            ];
            if ($cdef['pk']) {
                $pk[] = $cdef['name'];
                if (strcasecmp($cdef['type'], 'integer') === 0) {
                    $column['autoincrement'] = true;
                } else {
                    $column['primary'] = true;
                }
            }
            if ($cdef['dflt_value'] !== null) {
                $column['default'] = $cdef['dflt_value'];
            }
            $columns[$cdef['name']] = $column;
        }
        $tdef = ['columns' => $columns];
        if (!empty($pk)) {
            $tdef['indexes'][Db::INDEX_PK] = [
                'columns' => $pk,
                'type' => Db::INDEX_PK
            ];
        }
        $this->tables[$tablename] = $tdef;
        return $columns;
    }

    /**
     * Get the indexes from the database.
     *
     * @param string $tablename The name of the table to get the indexes for or an empty string to get all indexes.
     * @return array|null
     */
    protected function getIndexes($tablename = '') {
        if (!$tablename) {
            $tablenames = $this->getTablenames();
            foreach ($tablenames as $tablename) {
                $this->getIndexes($tablename);
            }
        }

        $pk = valr(['indexes', Db::INDEX_PK], $this->tables[$tablename]);

        // Reset the index list for the table.
        $this->tables[$tablename]['indexes'] = [];

        if ($pk) {
            $this->tables[$tablename]['indexes'][Db::INDEX_PK] = $pk;
        }

        $indexInfos = (array)$this->query('pragma index_list('.$this->quoteVal($this->px.$tablename).')');
        foreach ($indexInfos as $row) {
            $indexName = $row['name'];
            if ($row['unique']) {
                $type = Db::INDEX_UNIQUE;
            } else {
                $type = Db::INDEX_IX;
            }

            // Query the columns in the index.
            $columns = (array)$this->query('pragma index_info('.$this->quoteVal($indexName).')');

            $index = [
                'name' => $indexName,
                'columns' => array_column($columns, 'name'),
                'type' => $type
            ];
            $this->tables[$tablename]['indexes'][] = $index;
        }

        return $this->tables[$tablename]['indexes'];
    }

    /**
     * Get the primary or secondary keys from the given rows.
     *
     * @param string $tablename The name of the table.
     * @param array $row The row to examine.
     * @param bool $quick Whether or not to quickly look for <tablename>ID for the primary key.
     * @return array|null Returns the primary keys and values from {@link $rows} or null if the primary key isn't found.
     */
    protected function getPKValue($tablename, array $row, $quick = false) {
        if ($quick && isset($row[$tablename.'ID'])) {
            return [$tablename.'ID' => $row[$tablename.'ID']];
        }

        $tdef = $this->getTableDef($tablename);
        if (isset($tdef['indexes'][Db::INDEX_PK]['columns'])) {
            $pkColumns = array_flip($tdef['indexes'][Db::INDEX_PK]['columns']);
            $cols = array_intersect_key($row, $pkColumns);
            if (count($cols) === count($pkColumns)) {
                return $cols;
            }
        }

        return null;
    }

    /**
     * Get the all of tablenames in the database.
     *
     * @return array Returns an array of table names with prefixes stripped.
     */
    protected function getTablenames() {
        // Get the table names.
        $tables = (array)$this->get(
            'sqlite_master',
            [
                'type' => 'table',
                'name' => [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
            ],
            [
                'columns' => ['name'],
                'escapeTable' => false
            ]
        );

        // Strip the table prefixes.
        $tables = array_map(function ($name) {
            return ltrim_substr($name, $this->px);
        }, array_column($tables, 'name'));

        return $tables;
    }

    /**
     * {@inheritdoc}
     */
    public function insert($tablename, array $rows, array $options = []) {
        // Sqlite doesn't support upsert so do upserts manually.
        if (val(Db::OPTION_UPSERT, $options)) {
            unset($options[Db::OPTION_UPSERT]);

            $keys = $this->getPKValue($tablename, $rows, true);
            if (!$keys) {
                throw new \Exception("Cannot upsert with no key.", 500);
            }
            // Try updating first.
            $updated = $this->update(
                $tablename,
                array_diff_key($rows, $keys),
                $keys,
                $options
            );
            if ($updated) {
                // Updated.
                if (count($keys) === 1) {
                    return array_pop($keys);
                } else {
                    return true;
                }
            }
        }

        $result = parent::insert($tablename, $rows, $options);
        return $result;
    }

    /**
     * Gets the {@link PDO} object for this connection.
     *
     * @return \PDO
     */
    public function pdo() {
        $dsn = 'sqlite:'.$this->config['path'];

        if (!isset($this->pdo)) {
            $this->pdo = new PDO($dsn, val('username', $this->config, null), val('password', $this->config, null));
        }
        return $this->pdo;
    }

    /**
     * Optionally quote a where value.
     *
     * @param mixed $value The value to quote.
     * @param bool $quote Whether or not to quote the value.
     * @return string Returns the value, optionally quoted.
     */
    public function quoteVal($value, $quote = true) {
        if ($value instanceof Literal) {
            /* @var Literal $value */
            return $value->getValue('mysql');
        } elseif (in_array(gettype($value), ['integer', 'double'])) {
            return (string)$value;
        } elseif ($value === true) {
            return '1';
        } elseif ($value === false) {
            return '0';
        } elseif ($quote) {
            return $this->pdo()->quote($value);
        } else {
            return $value;
        }
    }
}


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

namespace Garden\Password;

/**
 * An {@link IPassword} that uses the various `password_*` functions.
 */
class PhpPassword implements IPassword {
    /**
     * @var int One of the `PASSWORD_*` constants supplied to {@link password_hash()}.
     */
    protected $algorithm;

    /**
     * Initialize an instance of this class.
     *
     * @param int $algorithm The crypt password to use when hashing passwords.
     */
    public function __construct($algorithm = PASSWORD_DEFAULT) {
        $this->algorithm = $algorithm;
    }

    /**
     * Hashes a plaintext password.
     *
     * @param string $password The password to hash.
     * @return string Returns the hashed password.
     */
    public function hash($password) {
        return password_hash($password, $this->algorithm);
    }

    /**
     * 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 password_needs_rehash($hash, $this->algorithm);
    }

    /**
     * {@inheritdoc}
     */
    public function verify($password, $hash) {
        if (!$hash) {
            return false;
        }
        return password_verify((string)$password, (string)$hash);
    }
}


<?php namespace Garden;

abstract class Route {
    /// Constants ///

    const MAP_QUERY = 'query'; // map to the querystring.
    const MAP_INPUT = 'input'; // map to the input (post).
    const MAP_DATA = 'data'; // map to the querystring or input depending on the method.

    /// Properties ///

    /**
     * @var array[string] An array of allowed http methods for this route.
     */
    protected $methods;

    protected $pattern;

    /**
     * @var array An array of parameter conditions.
     */
    protected $conditions;

    /**
     * @var array An array of global parameter conditions.
     */
    protected static $globalConditions;

    /**
     * @var array An array of parameter mappings.
     */
    protected $mappings;

    /**
     * @var bool Whether or not to match the path with the file extension.
     */
    protected $matchFullPath;

    /**
     * @var array An array of global parameter mappings.
     */
    protected static $globalMappings = [
        'data' => Route::MAP_DATA,
        'query' => Route::MAP_QUERY,
        'input' => Route::MAP_INPUT
    ];

    /// Methods ///

    /**
     * Create and return a new route.
     *
     * @param string $pattern The pattern for the route.
     * @param callable|string $callback Either a callback to map the route to or a string representing
     * a format for {@link sprintf()}.
     * @return \Garden\Route Returns the new route.
     */
    public static function create($pattern, $callback) {
        if (is_callable($callback)) {
            $route = new CallbackRoute($pattern, $callback);
        } else {
            $route = new ResourceRoute($pattern, $callback);
        }
        return $route;
    }

    /**
     * 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()}.
     */
    abstract public function dispatch(Request $request, array &$args);

    /**
     * Gets or sets the route's conditions.
     *
     * @param array|null $conditions An array of conditions to set.
     * @return Route|array
     */
    public function conditions($conditions = null) {
        if ($this->conditions === null) {
            $this->conditions = [];
        }

        if (is_array($conditions)) {
            $conditions = array_change_key_case($conditions);

            $this->conditions = array_replace(
                $this->conditions,
                $conditions
            );
            return $this;
        }

        return $this->conditions;
    }

    /**
     * Gets or sets the allowed http methods for this route.
     *
     * @param array|string|null $methods Set a new set of allowed methods or pass null to get the current methods.
     * @return Route|array Returns the current methods or `$this` for fluent calls.
     */
    public function methods($methods = null) {
        if ($methods === null) {
            return $this->methods;
        }

        $this->methods = array_map('strtoupper', (array)$methods);
        return $this;
    }

    /**
     * Gets/sets the global conditions.
     *
     * @param array|null $conditions An array of conditions to set.
     * @return array The current global conditions.
     */
    public static function globalConditions($conditions = null) {
        if (self::$globalConditions === null) {
            self::$globalConditions = [];
        }

        if (is_array($conditions)) {
            $conditions = array_change_key_case($conditions);

            self::$globalConditions = array_replace(
                self::$globalConditions,
                $conditions
            );
        }

        return self::$globalConditions;
    }

    /**
     * Gets or sets the mappings array that maps parameter names to mappings.
     *
     * @param array|null $mappings An array of mappings to set.
     * @return Route|array Returns the current mappings or `$this` for fluent calls.
     */
    public function mappings($mappings = null) {
        if ($this->mappings === null) {
            $this->mappings = [];
        }

        if (is_array($mappings)) {
            $mappings = array_change_key_case($mappings);

            $this->mappings = array_replace(
                $this->mappings,
                $mappings
            );
            return $this;
        }

        return $this->mappings;
    }

    /**
     * Gets or sets the global mappings array that maps parameter names to mappings.
     *
     * @param array|null $mappings An array of mappings to set.
     * @return array Returns the current global mappings.
     */
    public static function globalMappings($mappings = null) {
        if (self::$globalMappings === null) {
            self::$globalMappings = [];
        }

        if (is_array($mappings)) {
            $mappings = array_change_key_case($mappings);

            self::$globalMappings = array_replace(
                self::$globalMappings,
                $mappings
            );
        }

        return self::$globalMappings;
    }

    /**
     * Determine whether or not a parameter is mapped to special request data.
     *
     * @param string $name The name of the parameter to check.
     * @return bool Returns true if the parameter is mapped, false otherwise.
     */
    protected function isMapped($name) {
        $name = strtolower($name);
        return isset($this->mappings[$name]) || isset(self::$globalMappings[$name]);
    }

    /**
     * Get the mapped data for a parameter.
     *
     * @param string $name The name of the parameter.
     * @param Request $request The {@link Request} to get the data from.
     * @return array|null Returns the mapped data or null if there is no data.
     */
    protected function mappedData($name, Request $request) {
        $name = strtolower($name);

        if (isset($this->mappings[$name])) {
            $mapping = $this->mappings[$name];
        } elseif (isset(self::$globalMappings[$name])) {
            $mapping = self::$globalMappings[$name];
        } else {
            return null;
        }

        switch (strtolower($mapping)) {
            case self::MAP_DATA:
                $result = $request->getData();
                break;
            case self::MAP_INPUT:
                $result = $request->getInput();
                break;
            case self::MAP_QUERY:
                $result = $request->getQuery();
                break;
            default:
                return null;
        }
        return $result;
    }

    /**
     * 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.
     */
    abstract public function matches(Request $request, Application $app);

    /**
     * Tests whether or not a route matches the allowed methods for this route.
     *
     * @param Request $request The request to test.
     * @return bool Returns `true` if the route allows the method, otherwise `false`.
     */
    protected function matchesMethods(Request $request) {
        if (empty($this->methods)) {
            return true;
        }
        return in_array($request->getMethod(), $this->methods);
    }

    /**
     * Gets or sets the route pattern.
     *
     * @param string|null $pattern The route pattern.
     * @return string Returns the pattern.
     */
    public function pattern($pattern = null) {
        if ($pattern !== null) {
            $this->pattern = '/'.ltrim($pattern, '/');
        }
        return $this->pattern;
    }

    /**
     * Get whether or not to match the path with the extension.
     *
     * @return boolean Returns **true** if the path should be matched with the file extension or **false** otherwise.
     */
    public function getMatchFullPath() {
        return $this->matchFullPath;
    }

    /**
     * Set whether or not to match the path with the extension.
     *
     * @param boolean $matchFullPath The new value for the property.
     * @return Route Returns `$this` for fluent calls.
     */
    public function setMatchFullPath($matchFullPath) {
        $this->matchFullPath = $matchFullPath;
        return $this;
    }
}


<?php

namespace Garden;

abstract class Plugin {
    /// Properties ///

    /**
     *
     * @var array The singleton instances of the plugin subclasses.
     */
    protected static $instances;

    /// Methods ///

    public static function instance() {
        $class_name = get_called_class();

        if (!isset(self::$instances[$class_name])) {
            self::$instances[$class_name] = new $class_name();
        }
        return self::$instances[$class_name];

    }
}


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

namespace Garden;

/**
 * A route that maps urls to callbacks.
 */
class CallbackRoute extends Route {
    /// Properties ///

    /**
     *
     * @var callable The callback to call on a matching pattern.
     */
    protected $callback;

    /// Methods ///

    /**
     * Initialize an instance of the {@link CallbackRoute} class.
     *
     * @param string $pattern The pattern to match to.
     * @param callable $callback The callback to call when the url matches.
     */
    public function __construct($pattern, callable $callback) {
        $this->pattern($pattern);
        $this->setCallback($callback);
    }

    /**
     * Dispatch the matched route and call its callback.
     *
     * @param Request $request The request to dispatch.
     * @param array &$args The arguments returned from {@link CallbackRoute::dispatch()}.
     * @return mixed Returns the result of the callback.
     */
    public function dispatch(Request $request, array &$args) {
        $callback = $args['callback'];
        $callback_args = reflect_args($callback, $args['args']);

        $result = call_user_func_array($callback, $callback_args);
        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function matches(Request $request, Application $app) {
        if (!$this->matchesMethods($request)) {
            return null;
        }

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

        if (preg_match($regex, $path, $matches)) {
            // This route matches so extract the args.
            $args = array();
            foreach ($matches as $key => $value) {
                if (!is_numeric($key)) {
                    $args[$key] = $value;
                }
            }
            $result = array(
                'callback' => $this->callback,
                'args' => $args,
                );
            return $result;
        } else {
            return null;
        }
    }

    /**
     * Convert a path pattern into its regex.
     *
     * @param string $pattern The route pattern to convert into a regular expression.
     * @return string Returns the regex pattern for the route.
     */
    protected function getPatternRegex($pattern) {
        $result = preg_replace_callback('`{([^}]+)}`i', function ($match) {
            if (preg_match('`(.*?)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(.*?)`', $match[1], $matches)) {
                $before = preg_quote($matches[1], '`');
                $param = $matches[2];
                $after = preg_quote($matches[3], '`');
            } else {
                throw new \Exception("Invalid route parameter: $match[1].", 500);
            }

            $param_pattern = val($param, $this->conditions, val($param, self::$globalConditions, '[^/]+?'));
            $result = "(?<$param>$before{$param_pattern}$after)";

            return $result;
        }, $pattern);

        $result = '`^'.$result.'$`i';
        return $result;
    }

    /**
     * Get the callback for the route.
     *
     * @return callable Returns the current callback.
     */
    public function getCallback() {
        return $this->callback;
    }

    /**
     * Set the callback for the route.
     *
     * @param callable $callback The new callback to set.
     * @return CallbackRoute Retuns $this for fluent calls.
     */
    public function setCallback(callable $callback) {
        $this->callback = $callback;
        return $this;
    }
}


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

namespace Garden\Db;

use PDO;

/**
 * A {@link Db} class for connecting to MySQL.
 */
class MySqlDb extends Db {
    /// Properties ///

    /**
     * @var \PDO
     */
    protected $pdo;

    protected $config;

    /// Methods ///

    /**
     * Initialize an instance of the {@link MySqlDb} class.
     *
     * @param array $config The database config.
     */
    public function __construct(array $config = []) {
        $this->config = $config;
    }

    /**
     * {@inheritdoc}
     */
    public function dropTable($tablename, array $options = []) {
        $sql = 'drop table '.
            (val(Db::OPTION_IGNORE, $options) ? 'if exists ' : '').
            $this->backtick($this->px.$tablename);
        $result = $this->query($sql, Db::QUERY_DEFINE);
        unset($this->tables[strtolower($tablename)]);

        return $result;
    }

    /**
     * Surround a field with backticks.
     *
     * @param string $field The field to backtick.
     * @return string Returns the field properly escaped and backticked.
     * @link http://www.php.net/manual/en/pdo.quote.php#112169
     */
    protected function backtick($field) {
        return '`'.str_replace('`', '``', $field).'`';
    }

    /**
     * Execute a query on the database.
     *
     * @param string $sql The sql query to execute.
     * @param string $type One of the Db::QUERY_* constants.
     *
     * Db::QUERY_READ
     * : The query reads from the database.
     *
     * Db::QUERY_WRITE
     * : The query writes to the database.
     *
     * Db::QUERY_DEFINE
     * : The query alters the structure of the datbase.
     *
     * @param array $options Additional options for the query.
     *
     * Db::OPTION_MODE
     * : Override {@link Db::$mode}.
     *
     * @return array|string|PDOStatement|int Returns the result of the query.
     *
     * array
     * : Returns an array when reading from the database and the mode is {@link Db::MODE_EXEC}.
     * string
     * : Returns the sql query when the mode is {@link Db::MODE_SQL}.
     * PDOStatement
     * : Returns a {@link \PDOStatement} when the mode is {@link Db::MODE_PDO}.
     * int
     * : Returns the number of rows affected when performing an update or an insert.
     */
    public function query($sql, $type = Db::QUERY_READ, $options = []) {
        $mode = val(Db::OPTION_MODE, $options, $this->mode);

        if ($mode & Db::MODE_ECHO) {
            echo trim($sql, "\n;").";\n\n";
        }
        if ($mode & Db::MODE_SQL) {
            return $sql;
        }

        $result = null;
        if ($mode & Db::MODE_EXEC) {
            $result = $this->pdo()->query($sql);

            if ($type == Db::QUERY_READ) {
                $result->setFetchMode(PDO::FETCH_ASSOC);
                $result = $result->fetchAll();
                $this->rowCount = count($result);
            } elseif (is_object($result) && method_exists($result, 'rowCount')) {
                $this->rowCount = $result->rowCount();
                $result = $this->rowCount;
            }
        } elseif ($mode & Db::MODE_PDO) {
            /* @var \PDOStatement $result */
            $result = $this->pdo()->prepare($sql);
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function getTableDef($tablename) {
        $table = parent::getTableDef($tablename);
        if ($table || $table === null) {
            return $table;
        }

        $ltablename = strtolower($tablename);
        $table = val($ltablename, $this->tables, []);
        if (!isset($table['columns'])) {
            $columns = $this->getColumns($tablename);
            if ($columns === null) {
                // A table with no columns does not exist.
                $this->tables[$ltablename] = ['name' => $tablename];
                return null;
            }

            $table['columns'] = $columns;
        }
        if (!isset($table['indexes'])) {
            $table['indexes'] = $this->getIndexes($tablename);
        }
        $table['name'] = $tablename;
        $this->tables[$ltablename] = $table;
        return $table;
    }

    /**
     * Get the columns for tables and put them in {MySqlDb::$tables}.
     *
     * @param string $tablename The table to get the columns for or blank for all columns.
     * @return array|null Returns an array of columns if {@link $tablename} is specified, or null otherwise.
     */
    protected function getColumns($tablename = '') {
        $ltablename = strtolower($tablename);
        /* @var \PDOStatement $stmt */
        $stmt = $this->get(
            'information_schema.COLUMNS',
            [
                'TABLE_SCHEMA' => $this->getDbName(),
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
            ],
            [
                'columns' => [
                    'TABLE_NAME',
                    'COLUMN_TYPE',
                    'IS_NULLABLE',
                    'EXTRA',
                    'COLUMN_KEY',
                    'COLUMN_DEFAULT',
                    'COLUMN_NAME'
                ],
                Db::OPTION_MODE => Db::MODE_PDO,
                'escapeTable' => false,
                'order' => ['TABLE_NAME', 'ORDINAL_POSITION']
            ]
        );

        $stmt->execute();
        $tablecolumns = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);

        foreach ($tablecolumns as $ctablename => $cdefs) {
            $ctablename = strtolower(ltrim_substr($ctablename, $this->px));
            $columns = [];

            foreach ($cdefs as $cdef) {
                $column = [
                    'type' => $this->columnTypeString($cdef['COLUMN_TYPE']),
                    'required' => !force_bool($cdef['IS_NULLABLE']),
                ];
                if ($cdef['EXTRA'] === 'auto_increment') {
                    $column['autoincrement'] = true;
                }
                if ($cdef['COLUMN_KEY'] === 'PRI') {
                    $column['primary'] = true;
                }

                if ($cdef['COLUMN_DEFAULT'] !== null) {
                    $column['default'] = $this->forceType($cdef['COLUMN_DEFAULT'], $column['type']);
                }

                $columns[$cdef['COLUMN_NAME']] = $column;
            }
            $this->tables[$ctablename]['columns'] = $columns;
        }
        if ($ltablename && isset($this->tables[$ltablename]['columns'])) {
            return $this->tables[$ltablename]['columns'];
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function get($tablename, array $where, array $options = []) {
        $sql = $this->buildSelect($tablename, $where, $options);
        $result = $this->query($sql, Db::QUERY_READ, $options);
        return $result;
    }

    /**
     * Build a sql select statement.
     *
     * @param string $table The name of the main table.
     * @param array $where The where filter.
     * @param array $options An array of additional query options.
     * @return string Returns the select statement as a string.
     * @see Db::get()
     */
    public function buildSelect($table, array $where, array $options = []) {
        $sql = '';

        // Build the select clause.
        if (isset($options['columns'])) {
            $columns = array();
            foreach ($options['columns'] as $value) {
                $columns[] = $this->backtick($value);
            }
            $sql .= 'select '.implode(', ', $columns);
        } else {
            $sql .= "select *";
        }

        // Build the from clause.
        if (val('escapeTable', $options, true)) {
            $sql .= "\nfrom ".$this->backtick($this->px.$table);
        } else {
            $sql .= "\nfrom $table";
        }

        // Build the where clause.
        $whereString = $this->buildWhere($where, Db::OP_AND);
        if ($whereString) {
            $sql .= "\nwhere ".$whereString;
        }

        // Build the order.
        if (isset($options['order'])) {
            $order = array_quick($options['order'], Db::ORDER_ASC);
            $orders = array();
            foreach ($order as $key => $value) {
                switch ($value) {
                    case Db::ORDER_ASC:
                    case Db::ORDER_DESC:
                        $orders[] = $this->backtick($key)." $value";
                        break;
                    default:
                        trigger_error("Invalid sort direction '$value' for column '$key'.", E_USER_WARNING);
                }
            }

            $sql .= "\norder by ".implode(', ', $orders);
        }

        // Build the limit, offset.
        $limit = 10;
        if (isset($options['limit'])) {
            $limit = (int)$options['limit'];
            $sql .= "\nlimit $limit";
        }

        if (isset($options['offset'])) {
            $sql .= ' offset '.((int)$options['offset']);
        } elseif (isset($options['page'])) {
            $offset = $limit * ($options['page'] - 1);
            $sql .= ' offset '.$offset;
        }

        return $sql;
    }

    /**
     * Build a where clause from a where array.
     *
     * @param array $where There where string.
     * This is an array in the form `['column' => 'value']` with more advanced options for non-equality comparisons.
     * @param string $op The logical operator to join multiple field comparisons.
     * @param bool $quotevals Whether or not to quote the where values.
     * @return string The where string.
     */
    protected function buildWhere($where, $op = Db::OP_AND, $quotevals = true) {
        static $map = array(Db::OP_GT => '>', Db::OP_GTE => '>=', Db::OP_LT => '<', Db::OP_LTE => '<=', Db::OP_LIKE => 'like');

        $result = '';
        foreach ($where as $column => $value) {
            $btcolumn = $this->backtick($column);

            if (is_array($value)) {
                if (isset($value[0])) {
                    // This is a short in syntax.
                    $value = [Db::OP_IN => $value];
                }

                foreach ($value as $vop => $rval) {
                    if ($result) {
                        $result .= "\n  $op ";
                    }

                    switch ($vop) {
                        case Db::OP_AND:
                        case Db::OP_OR:
                            $innerWhere = [$column => $rval];
                            $result .= "(\n  ".
                                $this->buildWhere($innerWhere, $vop, $quotevals).
                                "\n  )";
                            break;
                        case Db::OP_EQ:
                            if ($rval === null) {
                                $result .= "$btcolumn is null";
                            } elseif (is_array($rval)) {
                                $result .= "$btcolumn in ".$this->bracketList($rval);
                            } else {
                                $result .= "$btcolumn = ".$this->quoteVal($rval, $quotevals);
                            }
                            break;
                        case Db::OP_GT:
                        case Db::OP_GTE:
                        case Db::OP_LT:
                        case Db::OP_LTE:
                            $result .= "$btcolumn {$map[$vop]} ".$this->quoteVal($rval, $quotevals);
                            break;
                        case Db::OP_LIKE:
                            $result .= $this->buildLike($btcolumn, $rval, $quotevals);
                            break;
                        case Db::OP_IN:
                            // Quote the in values.
                            $rval = array_map(array($this->pdo, 'quote'), (array)$rval);
                            $result .= "$btcolumn in (".implode(', ', $rval).')';
                            break;
                        case Db::OP_NE:
                            if ($rval === null) {
                                $result .= "$btcolumn is not null";
                            } elseif (is_array($rval)) {
                                $result .= "$btcolumn not in ".$this->bracketList($rval);
                            } else {
                                $result .= "$btcolumn <> ".$this->quoteVal($rval, $quotevals);
                            }
                            break;
                    }
                }
            } else {
                if ($result) {
                    $result .= "\n  $op ";
                }

                // This is just an equality operator.
                if ($value === null) {
                    $result .= "$btcolumn is null";
                } else {
                    $result .= "$btcolumn = ".$this->quoteVal($value, $quotevals);
                }
            }
        }
        return $result;
    }

    /**
     * Build a like expression.
     *
     * @param string $column The column name.
     * @param mixed $value The right-hand value.
     * @param bool $quotevals Whether or not to quote the values.
     * @return string Returns the like expression.
     */
    protected function buildLike($column, $value, $quotevals) {
        return "$column like ".$this->quoteVal($value, $quotevals);
    }

    /**
     * Convert an array into a bracketed list suitable for MySQL clauses.
     *
     * @param array $row The row to expand.
     * @param string $quote The quotes to surroud the items with. There are two special cases.
     * ' (single quote)
     * : The row will be passed through {@link PDO::quote()}.
     * ` (backticks)
     * : The row will be passed through {@link MySqlDb::backtick()}.
     * @return string Returns the bracket list.
     */
    public function bracketList($row, $quote = "'") {
        switch ($quote) {
            case "'":
                $row = array_map([$this->pdo(), 'quote'], $row);
                $quote = '';
                break;
            case '`':
                $row = array_map([$this, 'backtick'], $row);
                $quote = '';
                break;
        }

        return "($quote".implode("$quote, $quote", $row)."$quote)";
    }

    /**
     * Gets the {@link PDO} object for this connection.
     *
     * @return \PDO
     */
    public function pdo() {
        $dsnParts = array_translate($this->config, ['host', 'dbname', 'port']);
        $dsn = 'mysql:'.implode_assoc(';', '=', $dsnParts);

        if (!isset($this->pdo)) {
            $this->pdo = new PDO(
                $dsn,
                val('username', $this->config, ''),
                val('password', $this->config, ''),
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8'
                ]
            );
        }
        return $this->pdo;
    }

    /**
     * Optionally quote a where value.
     *
     * @param mixed $value The value to quote.
     * @param bool $quote Whether or not to quote the value.
     * @return string Returns the value, optionally quoted.
     */
    public function quoteVal($value, $quote = true) {
        if ($value instanceof Literal) {
            /* @var Literal $value */
            return $value->getValue('mysql');
        } elseif ($quote) {
            return $this->pdo()->quote($value);
        } else {
            return $value;
        }
    }

    /**
     * Get the current database name.
     *
     * @return mixed
     */
    public function getDbName() {
        return val('dbname', $this->config);
    }

    /**
     * Parse a column type string and return it in a way that is suitible for a create/alter table statement.
     *
     * @param string $typeString The string to parse.
     * @return string Returns a canonical typestring.
     */
    protected function columnTypeString($typeString) {
        $type = null;

        if (substr($type, 0, 4) === 'enum') {
            // This is an enum which will come in as an array.
            if (preg_match_all("`'([^']+)'`", $typeString, $matches)) {
                $type = $matches[1];
            }
        } else {
            if (preg_match('`([a-z]+)\s*(?:\((\d+(?:\s*,\s*\d+)*)\))?\s*(unsigned)?`', $typeString, $matches)) {
                //         var_dump($matches);
                $str = $matches[1];
                $length = val(2, $matches);
                $unsigned = val(3, $matches);

                if (substr($str, 0, 1) == 'u') {
                    $unsigned = true;
                    $str = substr($str, 1);
                }

                // Remove the length from types without real lengths.
                if (in_array($str, array('tinyint', 'smallint', 'mediumint', 'int', 'bigint', 'float', 'double'))) {
                    $length = null;
                }

                $type = $str;
                if ($length) {
                    $length = str_replace(' ', '', $length);
                    $type .= "($length)";
                }
                if ($unsigned) {
                    $type .= ' unsigned';
                }
            }
        }

        if (!$type) {
            debug_print_backtrace();
            trigger_error("Couldn't parse type $typeString", E_USER_ERROR);
        }

        return $type;
    }

    /**
     * Get the indexes from the database.
     *
     * @param string $tablename The name of the table to get the indexes for or an empty string to get all indexes.
     * @return array|null
     */
    protected function getIndexes($tablename = '') {
        $ltablename = strtolower($tablename);
        /* @var \PDOStatement */
        $stmt = $this->get(
            'information_schema.STATISTICS',
            [
                'TABLE_SCHEMA' => $this->getDbName(),
                'TABLE_NAME' => $tablename ? $this->px.$tablename : [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
            ],
            [
                'columns' => [
                    'INDEX_NAME',
                    'TABLE_NAME',
                    'NON_UNIQUE',
                    'COLUMN_NAME'
                ],
                'escapeTable' => false,
                'order' => ['TABLE_NAME', 'INDEX_NAME', 'SEQ_IN_INDEX'],
                Db::OPTION_MODE => Db::MODE_PDO
            ]
        );

        $stmt->execute();
        $indexDefs = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_GROUP);

        foreach ($indexDefs as $indexName => $indexRows) {
            $row = reset($indexRows);
            $itablename = strtolower(ltrim_substr($row['TABLE_NAME'], $this->px));
            $index = [
                'name' => $indexName,
                'columns' => array_column($indexRows, 'COLUMN_NAME')
            ];

            if ($indexName === 'PRIMARY') {
                $index['type'] = Db::INDEX_PK;
                $this->tables[$itablename]['indexes'][Db::INDEX_PK] = $index;
            } else {
                $index['type'] = $row['NON_UNIQUE'] ? Db::INDEX_IX : Db::INDEX_UNIQUE;
                $this->tables[$itablename]['indexes'][] = $index;
            }
        }

        if ($ltablename) {
            return valr([$ltablename, 'indexes'], $this->tables, []);
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getAllTables($withDefs = false) {
        $tables = parent::getAllTables($withDefs);
        if ($tables !== null) {
            return $tables;
        }

        // Grab the tablenames first.
        if ($this->allTablesFetched & Db::FETCH_TABLENAMES) {
            $tablenames = array_keys($this->tables);
        } else {
            $tablenames = $this->getTablenames();
            $this->tables = [];
            foreach ($tablenames as $tablename) {
                $this->tables[strtolower($tablename)] = ['name' => $tablename];
            }
            $this->allTablesFetched = Db::FETCH_TABLENAMES;
        }

        if (!$withDefs) {
            return $tablenames;
        }

        $this->getColumns();
        $this->allTablesFetched |= Db::FETCH_COLUMNS;

        $this->getIndexes();
        $this->allTablesFetched |= Db::FETCH_INDEXES;

        return $this->tables;
    }

    /**
     * Get the all of tablenames in the database.
     *
     * @return array Returns an array of table names with prefixes stripped.
     */
    protected function getTablenames() {
        // Get the table names.
        $tables = (array)$this->get(
            'information_schema.TABLES',
            [
                'TABLE_SCHEMA' => $this->getDbName(),
                'TABLE_NAME' => [Db::OP_LIKE => addcslashes($this->px, '_%').'%']
            ],
            [
                'columns' => ['TABLE_NAME'],
                'escapeTable' => false
            ]
        );

        // Strip the table prefixes.
        $tables = array_map(function ($name) {
            return ltrim_substr($name, $this->px);
        }, array_column($tables, 'TABLE_NAME'));

        return $tables;
    }

    /**
     * {@inheritdoc}
     */
    public function insert($tablename, array $rows, array $options = []) {
        $sql = $this->buildInsert($tablename, $rows, true, $options);
        $this->query($sql, Db::QUERY_WRITE);
        $id = $this->pdo()->lastInsertId();
        if (is_numeric($id)) {
            return (int)$id;
        } else {
            return $id;
        }
    }

    /**
     * Build an insert statement.
     *
     * @param string $tablename The name of the table to insert to.
     * @param array $row The row to insert.
     * @param bool $quotevals Whether or not to quote the values.
     * @param array $options An array of options for the insert. See {@link Db::insert} for the options.
     * @return string Returns the the sql string of the insert statement.
     */
    protected function buildInsert($tablename, array $row, $quotevals = true, $options = []) {
        if (val(Db::OPTION_UPSERT, $options)) {
            return $this->buildUpsert($tablename, $row, $quotevals, $options);
        } elseif (val(Db::OPTION_IGNORE, $options)) {
            $sql = 'insert ignore ';
        } elseif (val(Db::OPTION_REPLACE, $options)) {
            $sql = 'replace ';
        } else {
            $sql = 'insert ';
        }
        $sql .= $this->backtick($this->px.$tablename);

        // Add the list of values.
        $sql .=
            "\n".$this->bracketList(array_keys($row), '`').
            "\nvalues".$this->bracketList($row, $quotevals ? "'" : '');

        return $sql;
    }

    /**
     * Build an upsert statement.
     *
     * An upsert statement is an insert on duplicate key statement in MySQL.
     *
     * @param string $tablename The name of the table to update.
     * @param array $row The row to insert or update.
     * @param bool $quotevals Whether or not to quote the values in the row.
     * @param array $options An array of additional query options.
     * @return string Returns the upsert statement as a string.
     */
    protected function buildUpsert($tablename, array $row, $quotevals = true, $options = []) {
        // Build the initial insert statement first.
        unset($options[Db::OPTION_UPSERT]);
        $sql = $this->buildInsert($tablename, $row, $quotevals, $options);

        // Add the duplicate key stuff.
        $updates = [];
        foreach ($row as $key => $value) {
            $updates[] = $this->backtick($key).' = values('.$this->backtick($key).')';
        }
        $sql .= "\non duplicate key update ".implode(', ', $updates);

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function load($tablename, $rows, array $options = []) {
        $count = 0;
        $first = true;
        $spec = [];
        $stmt = null;

        // Loop over the rows and insert them with the statement.
        foreach ($rows as $row) {
            if ($first) {
                // Build the insert statement from the first row.
                foreach ($row as $key => $value) {
                    $spec[$key] = $this->paramName($key);
                }

                $sql = $this->buildInsert($tablename, $spec, false, $options);
                $stmt = $this->pdo()->prepare($sql);
                $first = false;
            }

            $params = array_translate($row, $spec);
            $stmt->execute($params);
            $count += $stmt->rowCount();
        }

        return $count;
    }

    /**
     * Make a valid pdo parameter name from a string.
     *
     * This method replaces invalid placeholder characters with underscores.
     *
     * @param string $name The name to replace.
     * @return string
     */
    protected function paramName($name) {
        $result = ':'.preg_replace('`[^a-zA-Z0-9_]`', '_', $name);
        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function update($tablename, array $set, array $where, array $options = []) {
        $sql = $this->buildUpdate($tablename, $set, $where, true, $options);
        $result = $this->query($sql, Db::QUERY_WRITE);

        if ($result instanceof \PDOStatement) {
            /* @var \PDOStatement $result */
            return $result->rowCount();
        }
        return $result;
    }

    /**
     * Build a sql update statement.
     *
     * @param string $tablename The name of the table to update.
     * @param array $set An array of columns to set.
     * @param array $where The where filter.
     * @param bool $quotevals Whether or not to quote the values.
     * @param array $options Additional options for the query.
     * @return string Returns the update statement as a string.
     */
    protected function buildUpdate($tablename, array $set, array $where, $quotevals = true, array $options = []) {
        $sql = 'update '.
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
            $this->backtick($this->px.$tablename).
            "\nset\n  ";

        $parts = [];
        foreach ($set as $key => $value) {
            $parts[] = $this->backtick($key).' = '.$this->quoteVal($value, $quotevals);
        }
        $sql .= implode(",\n  ", $parts);

        if (!empty($where)) {
            $sql .= "\nwhere ".$this->buildWhere($where, Db::OP_AND, $quotevals);
        }

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function delete($tablename, array $where, array $options = []) {
        if (val(Db::OPTION_TRUNCATE, $options)) {
            if (!empty($where)) {
                throw new \InvalidArgumentException("You cannot truncate $tablename with a where filter.", 500);
            }
            $sql = 'truncate table '.$this->backtick($this->px.$tablename);
        } else {
            $sql = 'delete from '.$this->backtick($this->px.$tablename);

            if (!empty($where)) {
                $sql .= "\nwhere ".$this->buildWhere($where);
            }
        }
        return $this->query($sql, Db::QUERY_WRITE);
    }

    /**
     * {@inheritdoc}
     */
    protected function createTable($tablename, array $tabledef, array $options = []) {
        // The table doesn't exist so this is a create table.
        $parts = array();
        foreach ($tabledef['columns'] as $name => $def) {
            $parts[] = $this->columnDefString($name, $def);
        }

        foreach (val('indexes', $tabledef, []) as $index) {
            $indexDef = $this->indexDefString($tablename, $index);
            if ($indexDef) {
                $parts[] = $indexDef;
            }
        }

        $fullTablename = $this->backtick($this->px.$tablename);
        $sql = "create table $fullTablename (\n  ".
            implode(",\n  ", $parts).
            "\n)";

        if (val('collate', $options)) {
            $sql .= "\n collate {$options['collate']}";
        }

        $this->query($sql, Db::QUERY_DEFINE);
    }

    /**
     * Construct a column definition string from an array defintion.
     *
     * @param string $name The name of the column.
     * @param array $def The column definition.
     * @return string Returns a string representing the column definition.
     */
    protected function columnDefString($name, array $def) {
        $result = $this->backtick($name).' '.$this->columnTypeString($def['type']);

        if (val('required', $def)) {
            $result .= ' not null';
        }

        if (isset($def['default'])) {
            $result .= ' default '.$this->quoteVal($def['default']);
        }

        if (val('autoincrement', $def)) {
            $result .= ' auto_increment';
        }

        return $result;
    }

    /**
     * Return the SDL string that defines an index.
     *
     * @param string $tablename The name of the table that the index is on.
     * @param array $def The index defintion. This definition should have the following keys.
     *
     * columns
     * : An array of columns in the index.
     * type
     * : One of "index", "unique", or "primary".
     * @return null|string Returns the index string or null if the index is not correct.
     */
    protected function indexDefString($tablename, array $def) {
        $indexName = $this->backtick($this->buildIndexName($tablename, $def));
        switch (val('type', $def, Db::INDEX_IX)) {
            case Db::INDEX_IX:
                return "index $indexName ".$this->bracketList($def['columns'], '`');
            case Db::INDEX_UNIQUE:
                return "unique $indexName ".$this->bracketList($def['columns'], '`');
            case Db::INDEX_PK:
                return "primary key ".$this->bracketList($def['columns'], '`');
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    protected function alterTable($tablename, array $alterdef, array $options = []) {
        $columnOrders = $this->getColumnOrders($alterdef['def']['columns']);
        $parts = [];

        // Add the columns and indexes.
        foreach ($alterdef['add']['columns'] as $cname => $cdef) {
            // Figure out the order of the column.
            $pos = val($cname, $columnOrders, '');
            $parts[] = 'add '.$this->columnDefString($cname, $cdef).$pos;
        }
        foreach ($alterdef['add']['indexes'] as $ixdef) {
            $parts[] = 'add '.$this->indexDefString($tablename, $ixdef);
        }

        // Alter the columns.
        foreach ($alterdef['alter']['columns'] as $cname => $cdef) {
            $parts[] = 'modify '.$this->columnDefString($cname, $cdef);
        }

        // Drop the columns and indexes.
        foreach ($alterdef['drop']['columns'] as $cname => $_) {
            $parts[] = 'drop '.$this->backtick($cname);
        }
        foreach ($alterdef['drop']['indexes'] as $ixdef) {
            $parts[] = 'drop index '.$this->backtick($ixdef['name']);
        }

        if (empty($parts)) {
            return false;
        }

        $sql = 'alter '.
            (val(Db::OPTION_IGNORE, $options) ? 'ignore ' : '').
            'table '.$this->backtick($this->px.$tablename)."\n  ".
            implode(",\n  ", $parts);

        $result = $this->query($sql, Db::QUERY_DEFINE);
        return $result;
    }

    /**
     * Get an array of column orders so that added columns can be slotted into their correct spot.
     *
     * @param array $cdefs An array of column definitions.
     * @return array Returns an array of column orders suitable for an `alter table` statement.
     */
    protected function getColumnOrders($cdefs) {
        $orders = array_flip(array_keys($cdefs));

        $prev = ' first';
        foreach ($orders as $cname => &$value) {
            $value = $prev;
            $prev = ' after '.$this->backtick($cname);
        }
        return $orders;
    }

    /**
     * Force a value into the appropriate php type based on its Sqlite type.
     *
     * @param mixed $value The value to force.
     * @param string $type The sqlite type name.
     * @return mixed Returns $value cast to the appropriate type.
     */
    protected function forceType($value, $type) {
        $type = strtolower($type);

        if ($type === 'null') {
            return null;
        } elseif (in_array($type, ['int', 'integer', 'tinyint', 'smallint',
            'mediumint', 'bigint', 'unsigned big int', 'int2', 'int8', 'boolean'])) {
            return force_int($value);
        } elseif (in_array($type, ['real', 'double', 'double precision', 'float',
            'numeric', 'decimal(10,5)'])) {
            return floatval($value);
        } else {
            return (string)$value;
        }
    }
}


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

namespace Garden\Password;

/**
 * Implements the password hash from Simple Machines Forums (SMF).
 *
 * Note that smf salts passwords with usernames. In order to use a smf password with this class you must concatenate
 * the username to the end of the password hash in the following way:
 *
 * ```php
 * // Password hash in php.
 * $passwordHash = $sha1PasswordHash.'$'.$username;
 * ```
 *
 * ```sql
 * -- Password hash in mysql.
 * select concat(memberName, '$', password) as passwordHash
 * from smf_members
 * ```
 *
 * The smf password hash is not a good choice for a default password hashing algorithm for several reasons.
 *
 * 1. It uses the username as a salt which could change thus invalidating the hash.
 * 2. The username is not a secure field and is thus a bad choice for a password salt.
 * 3. It relies on sha1 which is susceptible to a rainbow table attack.
 */
class SmfPassword implements IPassword {

    /**
     * Hashes a plaintext password.
     *
     * Although smf uses the username as salt, this hashing algorithm generates a random salt.
     *
     * @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));
        $hash = sha1(strtolower($salt).$password);

        return $hash.'$'.$salt;
    }

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

    /**
     * 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($storedHash, $username) = $this->splitHash($hash);
        $result = (sha1(strtolower($username).$password) == $storedHash);
        return $result;
    }

    /**
     * Split a password hash and salt into its separate components.
     *
     * @param string $hash The full password hash.
     * @return array An array in the form of [$hash, $username].
     */
    protected function splitHash($hash) {
        if (strpos($hash, '$') === false) {
            return [false, false];
        } else {
            return explode('$', $hash, 2);
        }
    }
}