<?php
/**
 * PEAR_REST
 *
 * PHP versions 4 and 5
 *
 * @category   pear
 * @package    PEAR
 * @author     Greg Beaver <cellog@php.net>
 * @copyright  1997-2009 The Authors
 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
 * @version    CVS: $Id: REST.php 313023 2011-07-06 19:17:11Z dufuz $
 * @link       http://pear.php.net/package/PEAR
 * @since      File available since Release 1.4.0a1
 */

/**
 * For downloading xml files
 */
require_once 'PEAR.php';
require_once 'PEAR/XMLParser.php';

/**
 * Intelligently retrieve data, following hyperlinks if necessary, and re-directing
 * as well
 * @category   pear
 * @package    PEAR
 * @author     Greg Beaver <cellog@php.net>
 * @copyright  1997-2009 The Authors
 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
 * @version    Release: 1.9.4
 * @link       http://pear.php.net/package/PEAR
 * @since      Class available since Release 1.4.0a1
 */
class PEAR_REST
{
    var $config;
    var $_options;

    function PEAR_REST(&$config, $options = array())
    {
        $this->config   = &$config;
        $this->_options = $options;
    }

    /**
     * Retrieve REST data, but always retrieve the local cache if it is available.
     *
     * This is useful for elements that should never change, such as information on a particular
     * release
     * @param string full URL to this resource
     * @param array|false contents of the accept-encoding header
     * @param boolean     if true, xml will be returned as a string, otherwise, xml will be
     *                    parsed using PEAR_XMLParser
     * @return string|array
     */
    function retrieveCacheFirst($url, $accept = false, $forcestring = false, $channel = false)
    {
        $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
            md5($url) . 'rest.cachefile';

        if (file_exists($cachefile)) {
            return unserialize(implode('', file($cachefile)));
        }

        return $this->retrieveData($url, $accept, $forcestring, $channel);
    }

    /**
     * Retrieve a remote REST resource
     * @param string full URL to this resource
     * @param array|false contents of the accept-encoding header
     * @param boolean     if true, xml will be returned as a string, otherwise, xml will be
     *                    parsed using PEAR_XMLParser
     * @return string|array
     */
    function retrieveData($url, $accept = false, $forcestring = false, $channel = false)
    {
        $cacheId = $this->getCacheId($url);
        if ($ret = $this->useLocalCache($url, $cacheId)) {
            return $ret;
        }

        $file = $trieddownload = false;
        if (!isset($this->_options['offline'])) {
            $trieddownload = true;
            $file = $this->downloadHttp($url, $cacheId ? $cacheId['lastChange'] : false, $accept, $channel);
        }

        if (PEAR::isError($file)) {
            if ($file->getCode() !== -9276) {
                return $file;
            }

            $trieddownload = false;
            $file = false; // use local copy if available on socket connect error
        }

        if (!$file) {
            $ret = $this->getCache($url);
            if (!PEAR::isError($ret) && $trieddownload) {
                // reset the age of the cache if the server says it was unmodified
                $result = $this->saveCache($url, $ret, null, true, $cacheId);
                if (PEAR::isError($result)) {
                    return PEAR::raiseError($result->getMessage());
                }
            }

            return $ret;
        }

        if (is_array($file)) {
            $headers      = $file[2];
            $lastmodified = $file[1];
            $content      = $file[0];
        } else {
            $headers      = array();
            $lastmodified = false;
            $content      = $file;
        }

        if ($forcestring) {
            $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId);
            if (PEAR::isError($result)) {
                return PEAR::raiseError($result->getMessage());
            }

            return $content;
        }

        if (isset($headers['content-type'])) {
            switch ($headers['content-type']) {
                case 'text/xml' :
                case 'application/xml' :
                case 'text/plain' :
                    if ($headers['content-type'] === 'text/plain') {
                        $check = substr($content, 0, 5);
                        if ($check !== '<?xml') {
                            break;
                        }
                    }

                    $parser = new PEAR_XMLParser;
                    PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
                    $err = $parser->parse($content);
                    PEAR::popErrorHandling();
                    if (PEAR::isError($err)) {
                        return PEAR::raiseError('Invalid xml downloaded from "' . $url . '": ' .
                            $err->getMessage());
                    }
                    $content = $parser->getData();
                case 'text/html' :
                default :
                    // use it as a string
            }
        } else {
            // assume XML
            $parser = new PEAR_XMLParser;
            $parser->parse($content);
            $content = $parser->getData();
        }

        $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId);
        if (PEAR::isError($result)) {
            return PEAR::raiseError($result->getMessage());
        }

        return $content;
    }

    function useLocalCache($url, $cacheid = null)
    {
        if ($cacheid === null) {
            $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
                md5($url) . 'rest.cacheid';
            if (!file_exists($cacheidfile)) {
                return false;
            }

            $cacheid = unserialize(implode('', file($cacheidfile)));
        }

        $cachettl = $this->config->get('cache_ttl');
        // If cache is newer than $cachettl seconds, we use the cache!
        if (time() - $cacheid['age'] < $cachettl) {
            return $this->getCache($url);
        }

        return false;
    }

    function getCacheId($url)
    {
        $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
            md5($url) . 'rest.cacheid';

        if (!file_exists($cacheidfile)) {
            return false;
        }

        $ret = unserialize(implode('', file($cacheidfile)));
        return $ret;
    }

    function getCache($url)
    {
        $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
            md5($url) . 'rest.cachefile';

        if (!file_exists($cachefile)) {
            return PEAR::raiseError('No cached content available for "' . $url . '"');
        }

        return unserialize(implode('', file($cachefile)));
    }

    /**
     * @param string full URL to REST resource
     * @param string original contents of the REST resource
     * @param array  HTTP Last-Modified and ETag headers
     * @param bool   if true, then the cache id file should be regenerated to
     *               trigger a new time-to-live value
     */
    function saveCache($url, $contents, $lastmodified, $nochange = false, $cacheid = null)
    {
        $cache_dir   = $this->config->get('cache_dir');
        $d           = $cache_dir . DIRECTORY_SEPARATOR . md5($url);
        $cacheidfile = $d . 'rest.cacheid';
        $cachefile   = $d . 'rest.cachefile';

        if (!is_dir($cache_dir)) {
            if (System::mkdir(array('-p', $cache_dir)) === false) {
              return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory and attempts to create the directory failed.");
            }
        }

        if ($cacheid === null && $nochange) {
            $cacheid = unserialize(implode('', file($cacheidfile)));
        }

        $idData = serialize(array(
            'age'        => time(),
            'lastChange' => ($nochange ? $cacheid['lastChange'] : $lastmodified),
        ));

        $result = $this->saveCacheFile($cacheidfile, $idData);
        if (PEAR::isError($result)) {
            return $result;
        } elseif ($nochange) {
            return true;
        }

        $result = $this->saveCacheFile($cachefile, serialize($contents));
        if (PEAR::isError($result)) {
            if (file_exists($cacheidfile)) {
              @unlink($cacheidfile);
            }

            return $result;
        }

        return true;
    }

    function saveCacheFile($file, $contents)
    {
        $len = strlen($contents);

        $cachefile_fp = @fopen($file, 'xb'); // x is the O_CREAT|O_EXCL mode
        if ($cachefile_fp !== false) { // create file
            if (fwrite($cachefile_fp, $contents, $len) < $len) {
                fclose($cachefile_fp);
                return PEAR::raiseError("Could not write $file.");
            }
        } else { // update file
            $cachefile_lstat = lstat($file);
            $cachefile_fp = @fopen($file, 'wb');
            if (!$cachefile_fp) {
                return PEAR::raiseError("Could not open $file for writing.");
            }

            $cachefile_fstat = fstat($cachefile_fp);
            if (
              $cachefile_lstat['mode'] == $cachefile_fstat['mode'] &&
              $cachefile_lstat['ino']  == $cachefile_fstat['ino'] &&
              $cachefile_lstat['dev']  == $cachefile_fstat['dev'] &&
              $cachefile_fstat['nlink'] === 1
            ) {
                if (fwrite($cachefile_fp, $contents, $len) < $len) {
                    fclose($cachefile_fp);
                    return PEAR::raiseError("Could not write $file.");
                }
            } else {
                fclose($cachefile_fp);
                $link = function_exists('readlink') ? readlink($file) : $file;
                return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $file . ' as it is symlinked to ' . $link . ' - Possible symlink attack');
            }
        }

        fclose($cachefile_fp);
        return true;
    }

    /**
     * Efficiently Download a file through HTTP.  Returns downloaded file as a string in-memory
     * This is best used for small files
     *
     * If an HTTP proxy has been configured (http_proxy PEAR_Config
     * setting), the proxy will be used.
     *
     * @param string  $url       the URL to download
     * @param string  $save_dir  directory to save file in
     * @param false|string|array $lastmodified header values to check against for caching
     *                           use false to return the header values from this download
     * @param false|array $accept Accept headers to send
     * @return string|array  Returns the contents of the downloaded file or a PEAR
     *                       error on failure.  If the error is caused by
     *                       socket-related errors, the error object will
     *                       have the fsockopen error code available through
     *                       getCode().  If caching is requested, then return the header
     *                       values.
     *
     * @access public
     */
    function downloadHttp($url, $lastmodified = null, $accept = false, $channel = false)
    {
        static $redirect = 0;
        // always reset , so we are clean case of error
        $wasredirect = $redirect;
        $redirect = 0;

        $info = parse_url($url);
        if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) {
            return PEAR::raiseError('Cannot download non-http URL "' . $url . '"');
        }

        if (!isset($info['host'])) {
            return PEAR::raiseError('Cannot download from non-URL "' . $url . '"');
        }

        $host   = isset($info['host']) ? $info['host'] : null;
        $port   = isset($info['port']) ? $info['port'] : null;
        $path   = isset($info['path']) ? $info['path'] : null;
        $schema = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http';

        $proxy_host = $proxy_port = $proxy_user = $proxy_pass = '';
        if ($this->config->get('http_proxy')&&
              $proxy = parse_url($this->config->get('http_proxy'))
        ) {
            $proxy_host = isset($proxy['host']) ? $proxy['host'] : null;
            if ($schema === 'https') {
                $proxy_host = 'ssl://' . $proxy_host;
            }

            $proxy_port   = isset($proxy['port']) ? $proxy['port'] : 8080;
            $proxy_user   = isset($proxy['user']) ? urldecode($proxy['user']) : null;
            $proxy_pass   = isset($proxy['pass']) ? urldecode($proxy['pass']) : null;
            $proxy_schema = (isset($proxy['scheme']) && $proxy['scheme'] == 'https') ? 'https' : 'http';
        }

        if (empty($port)) {
            $port = (isset($info['scheme']) && $info['scheme'] == 'https')  ? 443 : 80;
        }

        if (isset($proxy['host'])) {
            $request = "GET $url HTTP/1.1\r\n";
        } else {
            $request = "GET $path HTTP/1.1\r\n";
        }

        $request .= "Host: $host\r\n";
        $ifmodifiedsince = '';
        if (is_array($lastmodified)) {
            if (isset($lastmodified['Last-Modified'])) {
                $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n";
            }

            if (isset($lastmodified['ETag'])) {
                $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n";
            }
        } else {
            $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : '');
        }

        $request .= $ifmodifiedsince .
            "User-Agent: PEAR/1.9.4/PHP/" . PHP_VERSION . "\r\n";

        $username = $this->config->get('username', null, $channel);
        $password = $this->config->get('password', null, $channel);

        if ($username && $password) {
            $tmp = base64_encode("$username:$password");
            $request .= "Authorization: Basic $tmp\r\n";
        }

        if ($proxy_host != '' && $proxy_user != '') {
            $request .= 'Proxy-Authorization: Basic ' .
                base64_encode($proxy_user . ':' . $proxy_pass) . "\r\n";
        }

        if ($accept) {
            $request .= 'Accept: ' . implode(', ', $accept) . "\r\n";
        }

        $request .= "Accept-Encoding:\r\n";
        $request .= "Connection: close\r\n";
        $request .= "\r\n";

        if ($proxy_host != '') {
            $fp = @fsockopen($proxy_host, $proxy_port, $errno, $errstr, 15);
            if (!$fp) {
                return PEAR::raiseError("Connection to `$proxy_host:$proxy_port' failed: $errstr", -9276);
            }
        } else {
            if ($schema === 'https') {
                $host = 'ssl://' . $host;
            }

            $fp = @fsockopen($host, $port, $errno, $errstr);
            if (!$fp) {
                return PEAR::raiseError("Connection to `$host:$port' failed: $errstr", $errno);
            }
        }

        fwrite($fp, $request);

        $headers = array();
        $reply   = 0;
        while ($line = trim(fgets($fp, 1024))) {
            if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) {
                $headers[strtolower($matches[1])] = trim($matches[2]);
            } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) {
                $reply = (int)$matches[1];
                if ($reply == 304 && ($lastmodified || ($lastmodified === false))) {
                    return false;
                }

                if (!in_array($reply, array(200, 301, 302, 303, 305, 307))) {
                    return PEAR::raiseError("File $schema://$host:$port$path not valid (received: $line)");
                }
            }
        }

        if ($reply != 200) {
            if (!isset($headers['location'])) {
                return PEAR::raiseError("File $schema://$host:$port$path not valid (redirected but no location)");
            }

            if ($wasredirect > 4) {
                return PEAR::raiseError("File $schema://$host:$port$path not valid (redirection looped more than 5 times)");
            }

            $redirect = $wasredirect + 1;
            return $this->downloadHttp($headers['location'], $lastmodified, $accept, $channel);
        }

        $length = isset($headers['content-length']) ? $headers['content-length'] : -1;

        $data = '';
        while ($chunk = @fread($fp, 8192)) {
            $data .= $chunk;
        }
        fclose($fp);

        if ($lastmodified === false || $lastmodified) {
            if (isset($headers['etag'])) {
                $lastmodified = array('ETag' => $headers['etag']);
            }

            if (isset($headers['last-modified'])) {
                if (is_array($lastmodified)) {
                    $lastmodified['Last-Modified'] = $headers['last-modified'];
                } else {
                    $lastmodified = $headers['last-modified'];
                }
            }

            return array($data, $lastmodified, $headers);
        }

        return $data;
    }
}


<?php

namespace Reaction\Helpers;

/**
 * BaseStringHelper provides concrete implementation for [[StringHelper]].
 *
 * Borrowed from yii2 framework
 */
class StringHelper
{
    /**
     * Returns the number of bytes in the given string.
     * This method ensures the string is treated as a byte array by using `mb_strlen()`.
     * @param string $string the string being measured for length
     * @return int the number of bytes in the given string.
     */
    public static function byteLength($string)
    {
        return mb_strlen($string, '8bit');
    }

    /**
     * Returns the portion of string specified by the start and length parameters.
     * This method ensures the string is treated as a byte array by using `mb_substr()`.
     * @param string $string the input string. Must be one character or longer.
     * @param int $start the starting position
     * @param int $length the desired portion length. If not specified or `null`, there will be
     * no limit on length i.e. the output will be until the end of the string.
     * @return string the extracted part of string, or FALSE on failure or an empty string.
     * @see http://www.php.net/manual/en/function.substr.php
     */
    public static function byteSubstr($string, $start, $length = null)
    {
        return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit');
    }

    /**
     * Returns the trailing name component of a path.
     * This method is similar to the php function `basename()` except that it will
     * treat both \ and / as directory separators, independent of the operating system.
     * This method was mainly created to work on php namespaces. When working with real
     * file paths, php's `basename()` should work fine for you.
     * Note: this method is not aware of the actual filesystem, or path components such as "..".
     *
     * @param string $path A path string.
     * @param string $suffix If the name component ends in suffix this will also be cut off.
     * @return string the trailing name component of the given path.
     * @see http://www.php.net/manual/en/function.basename.php
     */
    public static function basename($path, $suffix = '')
    {
        if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) === $suffix) {
            $path = mb_substr($path, 0, -$len);
        }
        $path = rtrim(str_replace('\\', '/', $path), '/\\');
        if (($pos = mb_strrpos($path, '/')) !== false) {
            return mb_substr($path, $pos + 1);
        }

        return $path;
    }

    /**
     * Returns parent directory's path.
     * This method is similar to `dirname()` except that it will treat
     * both \ and / as directory separators, independent of the operating system.
     *
     * @param string $path A path string.
     * @return string the parent directory's path.
     * @see http://www.php.net/manual/en/function.basename.php
     */
    public static function dirname($path)
    {
        $pos = mb_strrpos(str_replace('\\', '/', $path), '/');
        if ($pos !== false) {
            return mb_substr($path, 0, $pos);
        }

        return '';
    }

    /**
     * Truncates a string to the number of characters specified.
     *
     * @param string $string The string to truncate.
     * @param int $length How many characters from original string to include into truncated string.
     * @param string $suffix String to append to the end of truncated string.
     * @param string $encoding The charset to use, defaults to charset currently used by application.
     * @param bool $asHtml Whether to treat the string being truncated as HTML and preserve proper HTML tags.
     * @return string the truncated string.
     */
    public static function truncate($string, $length, $suffix = '...', $encoding = null, $asHtml = false)
    {
        if ($encoding === null) {
            $encoding = \Reaction::$app ? \Reaction::$app->charset : 'UTF-8';
        }
        if ($asHtml) {
            return static::truncateHtml($string, $length, $suffix, $encoding);
        }

        if (mb_strlen($string, $encoding) > $length) {
            return rtrim(mb_substr($string, 0, $length, $encoding)) . $suffix;
        }

        return $string;
    }

    /**
     * Truncates a string to the number of words specified.
     *
     * @param string $string The string to truncate.
     * @param int $count How many words from original string to include into truncated string.
     * @param string $suffix String to append to the end of truncated string.
     * @param bool $asHtml Whether to treat the string being truncated as HTML and preserve proper HTML tags.
     * @return string the truncated string.
     */
    public static function truncateWords($string, $count, $suffix = '...', $asHtml = false)
    {
        if ($asHtml) {
            return static::truncateHtml($string, $count, $suffix);
        }

        $words = preg_split('/(\s+)/u', trim($string), null, PREG_SPLIT_DELIM_CAPTURE);
        if (!empty($words) && count($words) / 2 > $count) {
            return implode('', array_slice($words, 0, ($count * 2) - 1)) . $suffix;
        }

        return $string;
    }

    /**
     * Truncate a string while preserving the HTML.
     *
     * @param string $string The string to truncate
     * @param int $count
     * @param string $suffix String to append to the end of the truncated string.
     * @param string|bool $encoding
     * @return string
     */
    protected static function truncateHtml($string, $count, $suffix, $encoding = false)
    {
        $config = \HTMLPurifier_Config::create(null);
        if (\Reaction::$app !== null) {
            $config->set('Cache.SerializerPath', \Reaction::$app->getAlias('@runtime'));
        }
        $lexer = \HTMLPurifier_Lexer::create($config);
        $tokens = $lexer->tokenizeHTML($string, $config, new \HTMLPurifier_Context());
        $openTokens = [];
        $totalCount = 0;
        $depth = 0;
        $truncated = [];
        foreach ($tokens as $token) {
            if ($token instanceof \HTMLPurifier_Token_Start) { //Tag begins
                $openTokens[$depth] = $token->name;
                $truncated[] = $token;
                ++$depth;
            } elseif ($token instanceof \HTMLPurifier_Token_Text && $totalCount <= $count) { //Text
                if (false === $encoding) {
                    preg_match('/^(\s*)/um', $token->data, $prefixSpace) ?: $prefixSpace = ['', ''];
                    $token->data = $prefixSpace[1] . self::truncateWords(ltrim($token->data), $count - $totalCount, '');
                    $currentCount = self::countWords($token->data);
                } else {
                    $encoding = !is_string($encoding) && \Reaction::$app && \Reaction::$app->charset ? \Reaction::$app->charset : $encoding;
                    $token->data = self::truncate($token->data, $count - $totalCount, '', $encoding);
                    $currentCount = mb_strlen($token->data, $encoding);
                }
                $totalCount += $currentCount;
                $truncated[] = $token;
            } elseif ($token instanceof \HTMLPurifier_Token_End) { //Tag ends
                if ($token->name === $openTokens[$depth - 1]) {
                    --$depth;
                    unset($openTokens[$depth]);
                    $truncated[] = $token;
                }
            } elseif ($token instanceof \HTMLPurifier_Token_Empty) { //Self contained tags, i.e. <img/> etc.
                $truncated[] = $token;
            }
            if ($totalCount >= $count) {
                if (0 < count($openTokens)) {
                    krsort($openTokens);
                    foreach ($openTokens as $name) {
                        $truncated[] = new \HTMLPurifier_Token_End($name);
                    }
                }
                break;
            }
        }
        $context = new \HTMLPurifier_Context();
        $generator = new \HTMLPurifier_Generator($config, $context);
        return $generator->generateFromTokens($truncated) . ($totalCount >= $count ? $suffix : '');
    }

    /**
     * Check if given string starts with specified substring.
     * Binary and multibyte safe.
     *
     * @param string $string Input string
     * @param string $with Part to search inside the $string
     * @param bool   $caseSensitive Case sensitive search. Default is true. When case sensitive is enabled, $with must exactly match the starting of the string in order to get a true value.
     * @param string $encoding String encoding
     * @return bool Returns true if first input starts with second input, false otherwise
     */
    public static function startsWith($string, $with, $caseSensitive = true, $encoding = null)
    {
        if (!$bytes = static::byteLength($with)) {
            return true;
        }
        if ($caseSensitive) {
            return strncmp($string, $with, $bytes) === 0;

        }
        if (!isset($encoding)) {
            $encoding = \Reaction::$app ? \Reaction::$app->charset : 'UTF-8';
        }
        return mb_strtolower(mb_substr($string, 0, $bytes, '8bit'), $encoding) === mb_strtolower($with, $encoding);
    }

    /**
     * Check if given string ends with specified substring.
     * Binary and multibyte safe.
     *
     * @param string $string Input string to check
     * @param string $with Part to search inside of the $string.
     * @param bool $caseSensitive Case sensitive search. Default is true. When case sensitive is enabled, $with must exactly match the ending of the string in order to get a true value.
     * @param string $encoding String encoding
     * @return bool Returns true if first input ends with second input, false otherwise
     */
    public static function endsWith($string, $with, $caseSensitive = true, $encoding = null)
    {
        if (!$bytes = static::byteLength($with)) {
            return true;
        }
        if ($caseSensitive) {
            // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
            if (static::byteLength($string) < $bytes) {
                return false;
            }

            return substr_compare($string, $with, -$bytes, $bytes) === 0;
        }
        if (!isset($encoding)) {
            $encoding = \Reaction::$app ? \Reaction::$app->charset : 'UTF-8';
        }
        return mb_strtolower(mb_substr($string, -$bytes, mb_strlen($string, '8bit'), '8bit'), $encoding) === mb_strtolower($with, $encoding);
    }

    /**
     * Explodes string into array, optionally trims values and skips empty ones.
     *
     * @param string $string String to be exploded.
     * @param string $delimiter Delimiter. Default is ','.
     * @param mixed $trim Whether to trim each element. Can be:
     *   - boolean - to trim normally;
     *   - string - custom characters to trim. Will be passed as a second argument to `trim()` function.
     *   - callable - will be called for each value instead of trim. Takes the only argument - value.
     * @param bool $skipEmpty Whether to skip empty strings between delimiters. Default is false.
     * @return array
     */
    public static function explode($string, $delimiter = ',', $trim = true, $skipEmpty = false)
    {
        $result = explode($delimiter, $string);
        if ($trim) {
            if ($trim === true) {
                $trim = 'trim';
            } elseif (!is_callable($trim)) {
                $trim = function ($v) use ($trim) {
                    return trim($v, $trim);
                };
            }
            $result = array_map($trim, $result);
        }
        if ($skipEmpty) {
            // Wrapped with array_values to make array keys sequential after empty values removing
            $result = array_values(array_filter($result, function ($value) {
                return $value !== '';
            }));
        }

        return $result;
    }

    /**
     * Counts words in a string.
     *
     * @param string $string
     * @return int
     */
    public static function countWords($string)
    {
        $result = preg_split('/\s+/u', $string, null, PREG_SPLIT_NO_EMPTY);
        return !empty($result) ? count($result) : 0;
    }

    /**
     * Returns string representation of number value with replaced commas to dots, if decimal point
     * of current locale is comma.
     * @param int|float|string $value
     * @return string
     */
    public static function normalizeNumber($value)
    {
        $value = (string)$value;

        $localeInfo = localeconv();
        $decimalSeparator = isset($localeInfo['decimal_point']) ? $localeInfo['decimal_point'] : null;

        if ($decimalSeparator !== null && $decimalSeparator !== '.') {
            $value = str_replace($decimalSeparator, '.', $value);
        }

        return $value;
    }

    /**
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
     *
     * > Note: Base 64 padding `=` may be at the end of the returned string.
     * > `=` is not transparent to URL encoding.
     *
     * @see https://tools.ietf.org/html/rfc4648#page-7
     * @param string $input the string to encode.
     * @return string encoded string.
     */
    public static function base64UrlEncode($input)
    {
        return strtr(base64_encode($input), '+/', '-_');
    }

    /**
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
     *
     * @see https://tools.ietf.org/html/rfc4648#page-7
     * @param string $input encoded string.
     * @return string decoded string.
     */
    public static function base64UrlDecode($input)
    {
        return base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * Safely casts a float to string independent of the current locale.
     *
     * The decimal separator will always be `.`.
     * @param float|int $number a floating point number or integer.
     * @return string the string representation of the number.
     */
    public static function floatToString($number)
    {
        // . and , are the only decimal separators known in ICU data,
        // so its safe to call str_replace here
        return str_replace(',', '.', (string) $number);
    }

    /**
     * Checks if the passed string would match the given shell wildcard pattern.
     * This function emulates [[fnmatch()]], which may be unavailable at certain environment, using PCRE.
     * @param string $pattern the shell wildcard pattern.
     * @param string $string the tested string.
     * @param array $options options for matching. Valid options are:
     *
     * - caseSensitive: bool, whether pattern should be case sensitive. Defaults to `true`.
     * - escape: bool, whether backslash escaping is enabled. Defaults to `true`.
     * - filePath: bool, whether slashes in string only matches slashes in the given pattern. Defaults to `false`.
     *
     * @return bool whether the string matches pattern or not.
     */
    public static function matchWildcard($pattern, $string, $options = [])
    {
        if ($pattern === '*' && empty($options['filePath'])) {
            return true;
        }

        $replacements = [
            '\\\\\\\\' => '\\\\',
            '\\\\\\*' => '[*]',
            '\\\\\\?' => '[?]',
            '\*' => '.*',
            '\?' => '.',
            '\[\!' => '[^',
            '\[' => '[',
            '\]' => ']',
            '\-' => '-',
        ];

        if (isset($options['escape']) && !$options['escape']) {
            unset($replacements['\\\\\\\\']);
            unset($replacements['\\\\\\*']);
            unset($replacements['\\\\\\?']);
        }

        if (!empty($options['filePath'])) {
            $replacements['\*'] = '[^/\\\\]*';
            $replacements['\?'] = '[^/\\\\]';
        }

        $pattern = strtr(preg_quote($pattern, '#'), $replacements);
        $pattern = '#^' . $pattern . '$#us';

        if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
            $pattern .= 'i';
        }

        return preg_match($pattern, $string) === 1;
    }
}


<?php
/**
 * Create the user, then set it as acting as
 */
declare(strict_types=1);

namespace AaronSaray\LaravelTestHelpers;

use Illuminate\Contracts\Auth\Authenticatable;

/**
 * Trait CreateActingAs
 * @package AaronSaray\LaravelTestHelpers
 */
trait CreateActingAs
{
    /**
     * @var Authenticatable Current acting as user
     */
    protected $actingAs;

    /**
     * Create an acting as user, save it, and return it
     *
     * Note that this uses the config option but it can be overridden by the other option
     *
     * @param array $properties
     * @param string|null $userModel
     * @return Authenticatable
     */
    protected function createActingAs(array $properties = [], string $userModel = null): Authenticatable
    {
        $userClass = $userModel ?: config('auth.providers.users.model');

        $this->actingAs = factory($userClass)->create($properties);
        $this->actingAs($this->actingAs);
        return $this->actingAs;
    }
}


<?php

namespace Reaction\Db\Pgsql;

use Reaction\Db\Expressions\ArrayExpression;
use Reaction\Db\Expressions\ExpressionBuilderInterface;
use Reaction\Db\Expressions\ExpressionBuilderTrait;
use Reaction\Db\Expressions\ExpressionInterface;
use Reaction\Db\Expressions\JsonExpression;
use Reaction\Db\Query;

/**
 * Class ArrayExpressionBuilder builds [[ArrayExpression]] for PostgreSQL DBMS.
 */
class ArrayExpressionBuilder implements ExpressionBuilderInterface
{
    use ExpressionBuilderTrait;

    /**
     * {@inheritdoc}
     * @param ArrayExpression $expression the expression to be built
     */
    public function build($expression, array &$params = [])
    {
        $value = $expression->getValue();
        if ($value === null) {
            return 'NULL';
        }

        if ($value instanceof Query) {
            list ($sql, $params) = $this->queryBuilder->build($value, $params);
            return $this->buildSubqueryArray($sql, $expression);
        }

        $placeholders = $this->buildPlaceholders($expression, $params);

        return 'ARRAY[' . implode(', ', $placeholders) . ']' . $this->getTypehint($expression);
    }

    /**
     * Builds placeholders array out of $expression values
     * @param ArrayExpression $expression
     * @param array $params the binding parameters.
     * @return array
     */
    protected function buildPlaceholders($expression, &$params)
    {
        $value = $expression->getValue();

        $placeholders = [];
        if ($value === null || !is_array($value) && !$value instanceof \Traversable) {
            return $placeholders;
        }

        if ($expression->getDimension() > 1) {
            foreach ($value as $item) {
                $placeholders[] = $this->build($this->unnestArrayExpression($expression, $item), $params);
            }
            return $placeholders;
        }

        foreach ($value as $item) {
            if ($item instanceof Query) {
                list ($sql, $params) = $this->queryBuilder->build($item, $params);
                $placeholders[] = $this->buildSubqueryArray($sql, $expression);
                continue;
            }

            $item = $this->typecastValue($expression, $item);
            if ($item instanceof ExpressionInterface) {
                $placeholders[] = $this->queryBuilder->buildExpression($item, $params);
                continue;
            }

            $placeholders[] = $this->queryBuilder->bindParam($item, $params);
        }

        return $placeholders;
    }

    /**
     * @param ArrayExpression $expression
     * @param mixed $value
     * @return ArrayExpression
     */
    private function unnestArrayExpression(ArrayExpression $expression, $value)
    {
        $expressionClass = get_class($expression);

        return new $expressionClass($value, $expression->getType(), $expression->getDimension() - 1);
    }

    /**
     * @param ArrayExpression $expression
     * @return string the typecast expression based on [[type]].
     */
    protected function getTypehint(ArrayExpression $expression)
    {
        if ($expression->getType() === null) {
            return '';
        }

        $result = '::' . $expression->getType();
        $result .= str_repeat('[]', $expression->getDimension());

        return $result;
    }

    /**
     * Build an array expression from a subquery SQL.
     *
     * @param string $sql the subquery SQL.
     * @param ArrayExpression $expression
     * @return string the subquery array expression.
     */
    protected function buildSubqueryArray($sql, ArrayExpression $expression)
    {
        return 'ARRAY(' . $sql . ')' . $this->getTypehint($expression);
    }

    /**
     * Casts $value to use in $expression
     *
     * @param ArrayExpression $expression
     * @param mixed $value
     * @return JsonExpression
     */
    protected function typecastValue(ArrayExpression $expression, $value)
    {
        if ($value instanceof ExpressionInterface) {
            return $value;
        }

        if (in_array($expression->getType(), [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
            return new JsonExpression($value);
        }

        return $value;
    }
}


<?php

namespace Reaction\Web\Sessions;

use Reaction\Promise\ExtendedPromiseInterface;
use React\Promise\PromiseInterface;
use Reaction;
use Reaction\Base\RequestAppComponent;
use Reaction\Exceptions\InvalidArgumentException;
use Reaction\Exceptions\InvalidConfigException;

/**
 * Session provides session data management and the related configurations.
 *
 * Session is a Web application component that can be accessed via `Yii::$app->session`.
 *
 * To start the session, call [[open()]]; To complete and send out session data, call [[close()]];
 * To destroy the session, call [[destroy()]].
 *
 * Session can be used like an array to set and get session data. For example,
 *
 * ```php
 * $session = new Session;
 * $session->open();
 * $value1 = $session['name1'];  // get session variable 'name1'
 * $value2 = $session['name2'];  // get session variable 'name2'
 * foreach ($session as $name => $value) // traverse all session variables
 * $session['name3'] = $value3;  // set session variable 'name3'
 * ```
 *
 * Session can be extended to support customized session storage.
 *
 * Session also supports a special type of session data, called *flash messages*.
 * A flash message is available only in the current request and the next request.
 * After that, it will be deleted automatically. Flash messages are particularly
 * useful for displaying confirmation messages. To use flash messages, simply
 * call methods such as [[setFlash()]], [[getFlash()]].
 *
 * For more details and usage information on Session, see the [guide article on sessions](guide:runtime-sessions-cookies).
 *
 * @property array $allFlashes Flash messages (key => message or key => [message1, message2]). This property
 * is read-only.
 * @property string $cacheLimiter Current cache limiter. This property is read-only.
 * @property array $cookieParams The session cookie parameters. This property is read-only.
 * @property int $count The number of session variables. This property is read-only.
 * @property string $flash The key identifying the flash message. Note that flash messages and normal session
 * variables share the same name space. If you have a normal session variable using the same name, its value will
 * be overwritten by this method. This property is write-only.
 * @property bool $hasSessionId Whether the current request has sent the session ID.
 * @property string $id The current session ID.
 * @property bool $isActive Whether the session has started. This property is read-only.
 * @property SessionIterator $iterator An iterator for traversing the session variables. This property is
 * read-only.
 * @property string $name The current session name.
 * @property bool|null $useCookies The value indicating whether cookies should be used to store session IDs.
 * @property Reaction\Web\RequestHelper $request
 */
class Session extends RequestAppComponent implements \IteratorAggregate, \ArrayAccess, \Countable, RequestSessionInterface
{
    /**
     * @var string the name of the session variable that stores the flash message data.
     */
    public $flashParam = '__flash';
    /**
     * @var SessionHandlerInterface|array an object implementing the SessionHandlerInterface or a configuration array. If set, will be used to provide persistency instead of build-in methods.
     */
    public $handler = 'sessionHandler';
    /**
     * @var string Cookie name with session ID
     */
    public $cookieName = '_sess';
    /**
     * @var array Session data array
     */
    public $data = [];
    /** @var string[] Ignore data in this keys if it is empty (on ::isEmpty() check) */
    public $ignoredEmptyKeys = ['__flash'];
    /** @var string[] Ignore data in this keys (on ::isEmpty() check) */
    public $ignoredKeys = [];

    /**
     * @var array parameter-value pairs to override default session cookie parameters that are used for session_set_cookie_params() function
     * Array may have the following possible keys: 'lifetime', 'path', 'domain', 'secure', 'httponly'
     * @see http://www.php.net/manual/en/function.session-set-cookie-params.php
     */
    protected $_cookieParams = ['httponly' => true];
    /**
     * @var array|null is used for saving session between recreations due to session parameters update.
     */
    protected $frozenSessionData;

    /** @var string|null */
    protected $_cookieName;
    /** @var string Current session ID */
    protected $_sessionId;
    /** @var bool Flag indicates that session is active */
    protected $_isActive = false;

    /** @var array Session data before changes */
    protected $_dataInitial;
    /** @var bool */
    protected $_initialized = false;

    /**
     * Initializes the application component.
     * This method is required by IApplicationComponent and is invoked by application.
     */
    public function init()
    {
        parent::init();
        $self = $this;
        //Close session on end of request
        $this->app->once(Reaction\RequestApplicationInterface::EVENT_REQUEST_END, function() use (&$self) {
            return $self->close();
        });

        if ($this->getIsActive()) {
            Reaction::warning('Session is already started');
            $this->updateFlashCounters();
        }
    }

    /**
     * Init callback. Called by parent container/service/component on init and must return a fulfilled Promise
     * @return PromiseInterface
     */
    public function initComponent()
    {
        return $this->open()
            ->then(function() {
                $this->_initialized = true;
                return true;
            }, function($error = null) {
                Reaction::error($error);
            });
    }

    /**
     * Check that component was initialized earlier
     * @return bool
     */
    public function isInitialized()
    {
        return $this->_initialized;
    }

    /**
     * Starts the session.
     * @return ExtendedPromiseInterface
     */
    public function open()
    {
        if ($this->getIsActive()) {
            return \Reaction\Promise\resolve(true);
        }

        $this->registerSessionHandler();

        $self = $this;
        return $this->openInternal()->then(
            function() use ($self) {
                if (Reaction::isDebug()) {
                    //Reaction::info('Session started');
                }
                $self->updateFlashCounters();
            }
        );
    }

    /**
     * Open session internal method
     * @return ExtendedPromiseInterface
     */
    protected function openInternal()
    {
        $self = $this;
        if (!$this->getId()) {
            $self = $this;
            return $this->handler->createId($this->app)
                ->then(function($id = null) use ($self) {
                    $self->_isActive = true;
                    $self->setId($id);
                    $cookie = $self->createSessionCookie();
                    $self->app->response->cookies->add($cookie);
                    return true;
                });
        } else {
            $this->_isActive = true;
            return $this->readSession()->then(
                function($data) use ($self) {
                    $self->data = is_array($data) ? $data : [];
                    return true;
                },
                function($error = null) use ($self) {
                    $self->data = [];
                    return false;
                }
            )->always(function() {
                //Write initial data state for future needs
                $this->_dataInitial = $this->data;
            });
        }
    }

    /**
     * Registers session handler.
     * @throws \Reaction\Exceptions\InvalidConfigException
     */
    protected function registerSessionHandler()
    {
        if ($this->handler !== null) {
            if (!is_object($this->handler)) {
                if (is_string($this->handler) && Reaction::$app->has($this->handler)) {
                    $this->handler = Reaction::$app->get($this->handler);
                } else {
                    $this->handler = Reaction::create($this->handler);
                }
            }
            if (!$this->handler instanceof SessionHandlerInterface) {
                throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the \Reaction\Web\Sessions\SessionHandlerInterface.');
            }
        } else {
            throw new InvalidConfigException('"' . get_class($this) . '::handler" can not be empty.');
        }
    }

    /**
     * Check if session is empty, e.g. has no important data
     * @return bool
     */
    public function isEmpty() {
        $data = $this->data;
        $keys = !empty($this->ignoredEmptyKeys) ? array_fill_keys($this->ignoredEmptyKeys, 1) : [];
        $_keys = !empty($this->ignoredKeys) ? array_fill_keys($this->ignoredKeys, 0) : [];
        $keys = Reaction\Helpers\ArrayHelper::merge($keys, $_keys);
        foreach ($keys as $key => $checkEmpty) {
            if (!Reaction\Helpers\ArrayHelper::keyExists($key, $data)) {
                continue;
            }
            if (!$checkEmpty || empty($data[$key])) {
                unset($data[$key]);
            }
        }
        return empty($data);
    }

    /**
     * Ends the current session and store session data.
     * @param bool $destroy
     * @return ExtendedPromiseInterface
     */
    public function close($destroy = false)
    {
        if ($this->getIsActive()) {
            if (Reaction::isDebug()) {
                //Reaction::info('Session closed');
            }
            return $destroy || $this->isEmpty() ? $this->destroySession() : $this->writeSession();
        }
        $this->_isActive = false;
        return \Reaction\Promise\resolve(true);
    }

    /**
     * Frees all session variables and destroys all data registered to a session.
     *
     * This method has no effect when session is not [[getIsActive()|active]].
     * Make sure to call [[open()]] before calling it.
     * @see open()
     * @see isActive
     */
    public function destroy()
    {
        if ($this->getIsActive()) {
            $sessionId = $this->getId();
            return $this->close(true)
                ->otherwise(function() { return true; })
                ->then(function() use ($sessionId) {
                    $this->setId($sessionId);
                    return $this->open();
                });
        }
        return Reaction\Promise\resolve(true);
    }

    /**
     * @return bool whether the session has started
     */
    public function getIsActive()
    {
        return $this->_isActive;
    }

    private $_hasSessionId;

    /**
     * Returns a value indicating whether the current request has sent the session ID.
     * The default implementation will check cookie and $_GET using the session name.
     * If you send session ID via other ways, you may need to override this method
     * or call [[setHasSessionId()]] to explicitly set whether the session ID is sent.
     * @return bool whether the current request has sent the session ID.
     */
    public function getHasSessionId()
    {
        if ($this->_hasSessionId === null) {
            $name = $this->getName();
            $request = $this->request;
            $cookie = $request->getCookies()->getValue($name);
            if (!empty($cookie) && ini_get('session.use_cookies')) {
                $this->_hasSessionId = true;
                $this->_sessionId = $cookie;
            } elseif (!ini_get('session.use_only_cookies') && ini_get('session.use_trans_sid')) {
                $idFromRequest = $request->get($name);
                $this->_hasSessionId = $idFromRequest != '';
                $this->_sessionId = $idFromRequest != '' ? $idFromRequest : null;
            } else {
                $this->_sessionId = null;
                $this->_hasSessionId = false;
            }
        }

        return $this->_hasSessionId;
    }

    /**
     * Sets the value indicating whether the current request has sent the session ID.
     * This method is provided so that you can override the default way of determining
     * whether the session ID is sent.
     * @param bool $value whether the current request has sent the session ID.
     */
    public function setHasSessionId($value)
    {
        $this->_hasSessionId = $value;
    }

    /**
     * Gets the session ID.
     * This is a wrapper for [PHP session_id()](http://php.net/manual/en/function.session-id.php).
     * @return string the current session ID
     */
    public function getId()
    {
        if (!isset($this->_sessionId)) {
            $this->getHasSessionId();
        }
        return $this->_sessionId;
    }

    /**
     * Sets the session ID.
     * This is a wrapper for [PHP session_id()](http://php.net/manual/en/function.session-id.php).
     * @param string $value the session ID for the current session
     */
    public function setId($value)
    {
        $this->_sessionId = $value;
    }

    /**
     * Updates the current session ID with a newly generated one.
     *
     * Please refer to <http://php.net/session_regenerate_id> for more details.
     *
     * This method has no effect when session is not [[getIsActive()|active]].
     * Make sure to call [[open()]] before calling it.
     *
     * @param bool $deleteOldSession Whether to delete the old associated session file or not.
     * @see open()
     * @see isActive
     * @return \React\Promise\PromiseInterface
     */
    public function regenerateID($deleteOldSession = false)
    {
        if ($this->getIsActive()) {
            return $this->handler->regenerateId($this->id, $this->app, $deleteOldSession);
        }
        return \Reaction\Promise\reject(new Reaction\Exceptions\SessionException('Session is not active'));
    }

    /**
     * Gets the name of the current session.
     * This is a wrapper for [PHP session_name()](http://php.net/manual/en/function.session-name.php).
     * @return string the current session name
     */
    public function getName()
    {
        if (!isset($this->_cookieName)) {
            if (strlen($this->_cookieName) < 32 && $this->request->enableCookieValidation) {
                $key = $this->request->cookieValidationKey;
                $bytesLn = Reaction\Helpers\StringHelper::byteLength($key);
                $strLn = mb_strlen($key);
                $this->_cookieName = $this->cookieName . md5(crypt($bytesLn . ':' . $strLn, $key));
            }
        }
        return $this->_cookieName;
    }

    /**
     * Sets the name for the current session.
     * This is a wrapper for [PHP session_name()](http://php.net/manual/en/function.session-name.php).
     * @param string $value the session name for the current session, must be an alphanumeric string.
     * It defaults to "PHPSESSID".
     */
    public function setName($value)
    {
        $this->freeze();
        $this->cookieName = $value;
        $this->_cookieName = null;
        $this->unfreeze();
    }

    /**
     * @return array the session cookie parameters.
     * @see http://php.net/manual/en/function.session-get-cookie-params.php
     */
    public function getCookieParams()
    {
        return array_merge(session_get_cookie_params(), array_change_key_case($this->_cookieParams));
    }

    /**
     * Sets the session cookie parameters.
     * The cookie parameters passed to this method will be merged with the result
     * of `session_get_cookie_params()`.
     * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httponly`.
     * @throws InvalidArgumentException if the parameters are incomplete.
     * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php
     */
    public function setCookieParams(array $value)
    {
        $this->_cookieParams = $value;
    }

    /**
     * Returns the value indicating whether cookies should be used to store session IDs.
     * @return bool|null the value indicating whether cookies should be used to store session IDs.
     * @see setUseCookies()
     */
    public function getUseCookies()
    {
        if (ini_get('session.use_cookies') === '0') {
            return false;
        } elseif (ini_get('session.use_only_cookies') === '1') {
            return true;
        }

        return null;
    }

    /**
     * Sets the value indicating whether cookies should be used to store session IDs.
     *
     * Three states are possible:
     *
     * - true: cookies and only cookies will be used to store session IDs.
     * - false: cookies will not be used to store session IDs.
     * - null: if possible, cookies will be used to store session IDs; if not, other mechanisms will be used (e.g. GET parameter)
     *
     * @param bool|null $value the value indicating whether cookies should be used to store session IDs.
     */
    public function setUseCookies($value)
    {
        $this->freeze();
        if ($value === false) {
            ini_set('session.use_cookies', '0');
            ini_set('session.use_only_cookies', '0');
        } elseif ($value === true) {
            ini_set('session.use_cookies', '1');
            ini_set('session.use_only_cookies', '1');
        } else {
            ini_set('session.use_cookies', '1');
            ini_set('session.use_only_cookies', '0');
        }
        $this->unfreeze();
    }

    /**
     * Session read handler.
     * @internal Do not call this method directly.
     * @param string $id session ID
     * @return ExtendedPromiseInterface with array the session data
     */
    public function readSession($id = null)
    {
        $id = !isset($id) && isset($this->_sessionId) ? $this->_sessionId : $id;
        if (!isset($id)) {
            return \Reaction\Promise\reject(new Reaction\Exceptions\ErrorException('Param "$id" must be specified in "' . __METHOD__ . '"'));
        }
        return $this->handler->read($id);
    }

    /**
     * Session write handler.
     * @internal Do not call this method directly.
     * @param string $id session ID
     * @param array  $data session data
     * @return ExtendedPromiseInterface with bool whether session write is successful
     */
    public function writeSession($data = null, $id = null)
    {
        $id = !isset($id) && isset($this->_sessionId) ? $this->_sessionId : $id;
        if (!isset($id)) {
            return \Reaction\Promise\reject(new Reaction\Exceptions\ErrorException('Param "$id" must be specified in "' . __METHOD__ . '"'));
        }
        if (!isset($data)) {
            $data = $this->data;
        }
        return $this->handler->write($id, $data);
    }

    /**
     * Session destroy handler.
     * @internal Do not call this method directly.
     * @param string $id session ID
     * @return ExtendedPromiseInterface with bool whether session is destroyed successfully
     */
    public function destroySession($id = null)
    {
        $id = !isset($id) && isset($this->_sessionId) ? $this->_sessionId : $id;
        if (!isset($id)) {
            return \Reaction\Promise\reject(new Reaction\Exceptions\ErrorException('Param "$id" must be specified in "' . __METHOD__ . '"'));
        }
        return $this->handler->destroy($id, true);
    }

    /**
     * Returns an iterator for traversing the session variables.
     * This method is required by the interface [[\IteratorAggregate]].
     * @return SessionIterator an iterator for traversing the session variables.
     */
    public function getIterator()
    {
        $this->open();
        return new SessionIterator($this);
    }

    /**
     * Returns the number of items in the session.
     * @return int the number of session variables
     */
    public function getCount()
    {
        $this->open();
        return count($this->data);
    }

    /**
     * Returns the number of items in the session.
     * This method is required by [[\Countable]] interface.
     * @return int number of items in the session.
     */
    public function count()
    {
        return $this->getCount();
    }

    /**
     * Returns the session variable value with the session variable name.
     * If the session variable does not exist, the `$defaultValue` will be returned.
     * @param string $key the session variable name
     * @param mixed $defaultValue the default value to be returned when the session variable does not exist.
     * @return mixed the session variable value, or $defaultValue if the session variable does not exist.
     */
    public function get($key, $defaultValue = null)
    {
        $this->open();
        return isset($this->data[$key]) ? $this->data[$key] : $defaultValue;
    }

    /**
     * Adds a session variable.
     * If the specified name already exists, the old value will be overwritten.
     * @param string $key session variable name
     * @param mixed  $value session variable value
     */
    public function set($key, $value)
    {
        $this->open();
        $data = $this->data;
        $data[$key] = $value;
        $this->data = $data;
    }

    /**
     * Removes a session variable.
     * @param string $key the name of the session variable to be removed
     */
    public function remove($key)
    {
        $this->open();
        $data = $this->data;
        if (isset($data[$key])) {
            unset($data[$key]);
        }
        $this->data = $data;
    }

    /**
     * Removes all session variables.
     * @return PromiseInterface with bool when write process ends
     */
    public function removeAll()
    {
        $this->open();
        $data = $this->data;
        foreach (array_keys($data) as $key) {
            unset($data[$key]);
        }
        $this->data = $data;
        return $this->writeSession();
    }

    /**
     * @param mixed $key session variable name
     * @return bool whether there is the named session variable
     */
    public function has($key)
    {
        $this->open();
        return isset($this->data[$key]);
    }

    /**
     * Updates the counters for flash messages and removes outdated flash messages.
     * This method should only be called once in [[init()]].
     */
    protected function updateFlashCounters()
    {
        $counters = $this->get($this->flashParam, []);
        $data = $this->data;
        if (is_array($counters)) {
            foreach ($counters as $key => $count) {
                if ($count > 0) {
                    unset($counters[$key], $this->data[$key]);
                } elseif ($count == 0) {
                    $counters[$key]++;
                }
            }
            $data[$this->flashParam] = $counters;
        } else {
            // fix the unexpected problem that flashParam doesn't return an array
            unset($data[$this->flashParam]);
        }
        $this->data = $data;
    }

    /**
     * Returns a flash message.
     * @param string $key the key identifying the flash message
     * @param mixed $defaultValue value to be returned if the flash message does not exist.
     * @param bool $delete whether to delete this flash message right after this method is called.
     * If false, the flash message will be automatically deleted in the next request.
     * @return mixed the flash message or an array of messages if addFlash was used
     * @see setFlash()
     * @see addFlash()
     * @see hasFlash()
     * @see getAllFlashes()
     * @see removeFlash()
     */
    public function getFlash($key, $defaultValue = null, $delete = false)
    {
        $counters = $this->get($this->flashParam, []);
        if (isset($counters[$key])) {
            $value = $this->get($key, $defaultValue);
            if ($delete) {
                $this->removeFlash($key);
            } elseif ($counters[$key] < 0) {
                // mark for deletion in the next request
                $counters[$key] = 1;
                $this->data[$this->flashParam] = $counters;
            }

            return $value;
        }

        return $defaultValue;
    }

    /**
     * Returns all flash messages.
     *
     * You may use this method to display all the flash messages in a view file:
     *
     * ```php
     * <?php
     * foreach ($app->session->getAllFlashes() as $key => $message) {
     *     echo '<div class="alert alert-' . $key . '">' . $message . '</div>';
     * } ?>
     * ```
     *
     * With the above code you can use the [bootstrap alert][] classes such as `success`, `info`, `danger`
     * as the flash message key to influence the color of the div.
     *
     * Note that if you use [[addFlash()]], `$message` will be an array, and you will have to adjust the above code.
     *
     * [bootstrap alert]: http://getbootstrap.com/components/#alerts
     *
     * @param bool $delete whether to delete the flash messages right after this method is called.
     * If false, the flash messages will be automatically deleted in the next request.
     * @return array flash messages (key => message or key => [message1, message2]).
     * @see setFlash()
     * @see addFlash()
     * @see getFlash()
     * @see hasFlash()
     * @see removeFlash()
     */
    public function getAllFlashes($delete = false)
    {
        $counters = $this->get($this->flashParam, []);
        $flashes = [];
        foreach (array_keys($counters) as $key) {
            if (array_key_exists($key, $this->data)) {
                $flashes[$key] = $this->data[$key];
                if ($delete) {
                    unset($counters[$key], $this->data[$key]);
                } elseif ($counters[$key] < 0) {
                    // mark for deletion in the next request
                    $counters[$key] = 1;
                }
            } else {
                unset($counters[$key]);
            }
        }

        $this->data[$this->flashParam] = $counters;

        return $flashes;
    }

    /**
     * Sets a flash message.
     * A flash message will be automatically deleted after it is accessed in a request and the deletion will happen
     * in the next request.
     * If there is already an existing flash message with the same key, it will be overwritten by the new one.
     * @param string $key the key identifying the flash message. Note that flash messages
     * and normal session variables share the same name space. If you have a normal
     * session variable using the same name, its value will be overwritten by this method.
     * @param mixed $value flash message
     * @param bool $removeAfterAccess whether the flash message should be automatically removed only if
     * it is accessed. If false, the flash message will be automatically removed after the next request,
     * regardless if it is accessed or not. If true (default value), the flash message will remain until after
     * it is accessed.
     * @see getFlash()
     * @see addFlash()
     * @see removeFlash()
     */
    public function setFlash($key, $value = true, $removeAfterAccess = true)
    {
        $counters = $this->get($this->flashParam, []);
        $counters[$key] = $removeAfterAccess ? -1 : 0;
        $this->data[$key] = $value;
        $this->data[$this->flashParam] = $counters;
    }

    /**
     * Adds a flash message.
     * If there are existing flash messages with the same key, the new one will be appended to the existing message array.
     * @param string $key the key identifying the flash message.
     * @param mixed $value flash message
     * @param bool $removeAfterAccess whether the flash message should be automatically removed only if
     * it is accessed. If false, the flash message will be automatically removed after the next request,
     * regardless if it is accessed or not. If true (default value), the flash message will remain until after
     * it is accessed.
     * @see getFlash()
     * @see setFlash()
     * @see removeFlash()
     */
    public function addFlash($key, $value = true, $removeAfterAccess = true)
    {
        $counters = $this->get($this->flashParam, []);
        $counters[$key] = $removeAfterAccess ? -1 : 0;
        $this->data[$this->flashParam] = $counters;
        if (empty($this->data[$key])) {
            $this->data[$key] = [$value];
        } else {
            if (is_array($this->data[$key])) {
                $this->data[$key][] = $value;
            } else {
                $this->data[$key] = [$this->data[$key], $value];
            }
        }
    }

    /**
     * Removes a flash message.
     * @param string $key the key identifying the flash message. Note that flash messages
     * and normal session variables share the same name space.  If you have a normal
     * session variable using the same name, it will be removed by this method.
     * @return mixed the removed flash message. Null if the flash message does not exist.
     * @see getFlash()
     * @see setFlash()
     * @see addFlash()
     * @see removeAllFlashes()
     */
    public function removeFlash($key)
    {
        $counters = $this->get($this->flashParam, []);
        $value = isset($this->data[$key], $counters[$key]) ? $this->data[$key] : null;
        unset($counters[$key], $this->data[$key]);
        $this->data[$this->flashParam] = $counters;

        return $value;
    }

    /**
     * Removes all flash messages.
     * Note that flash messages and normal session variables share the same name space.
     * If you have a normal session variable using the same name, it will be removed
     * by this method.
     * @see getFlash()
     * @see setFlash()
     * @see addFlash()
     * @see removeFlash()
     */
    public function removeAllFlashes()
    {
        $counters = $this->get($this->flashParam, []);
        foreach (array_keys($counters) as $key) {
            unset($this->data[$key]);
        }
        unset($this->data[$this->flashParam]);
    }

    /**
     * Returns a value indicating whether there are flash messages associated with the specified key.
     * @param string $key key identifying the flash message type
     * @return bool whether any flash messages exist under specified key
     */
    public function hasFlash($key)
    {
        return $this->getFlash($key) !== null;
    }

    /**
     * This method is required by the interface [[\ArrayAccess]].
     * @param mixed $offset the offset to check on
     * @return bool
     */
    public function offsetExists($offset)
    {
        $this->open();

        return isset($this->data[$offset]);
    }

    /**
     * This method is required by the interface [[\ArrayAccess]].
     * @param int $offset the offset to retrieve element.
     * @return mixed the element at the offset, null if no element is found at the offset
     */
    public function offsetGet($offset)
    {
        $this->open();

        return isset($this->data[$offset]) ? $this->data[$offset] : null;
    }

    /**
     * This method is required by the interface [[\ArrayAccess]].
     * @param int $offset the offset to set element
     * @param mixed $item the element value
     */
    public function offsetSet($offset, $item)
    {
        $this->open();
        $this->data[$offset] = $item;
    }

    /**
     * This method is required by the interface [[\ArrayAccess]].
     * @param mixed $offset the offset to unset element
     */
    public function offsetUnset($offset)
    {
        $this->open();
        unset($this->data[$offset]);
    }

    /**
     * If session is started it's not possible to edit session ini settings. In PHP7.2+ it throws exception.
     * This function saves session data to temporary variable and stop session.
     */
    protected function freeze()
    {
        if ($this->getIsActive()) {
            if (isset($this->data)) {
                $this->frozenSessionData = $this->data;
            }
            //$this->close();
            if (Reaction::isDebug()) {
                Reaction::info('Session frozen');
            }
        }
    }

    /**
     * Starts session and restores data from temporary variable
     */
    protected function unfreeze()
    {
        if (null !== $this->frozenSessionData) {

            //$this->open();

            if ($this->getIsActive()) {
                Reaction::info('Session unfrozen');
            } else {
                $error = error_get_last();
                $message = isset($error['message']) ? $error['message'] : 'Failed to unfreeze session.';
                Reaction::error($message);
            }

            $this->data = $this->frozenSessionData;
            $this->frozenSessionData = null;
            $this->writeSession();
        }
    }

    /**
     * Create session cookie
     * @return Reaction\Web\Cookie
     */
    protected function createSessionCookie() {
        $_params = $this->getCookieParams();
        $config = [
            'class' => 'Reaction\Web\Cookie',
        ];
        foreach ($_params as $key => $value) {
            if ($key === 'httponly') {
                $key = 'httpOnly';
            } elseif ($key === 'lifetime') {
                $expireValue = intval($value);
                if (empty($expireValue)) {
                    continue;
                }
                $key = 'expire';
                $value = time() + $expireValue;
            }
            $config[$key] = $value;
        }
        $config['value'] = $this->id;
        $config['name'] = $this->getName();
        return Reaction::create($config);
    }

    /**
     * Get Request helper
     * @return Reaction\Web\RequestHelper
     */
    protected function getRequest() {
        return $this->app->reqHelper;
    }
}


<?php

/*
 * This file is part of the Valkyrja framework.
 *
 * (c) Melech Mizrachi
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Valkyrja\Config\Commands;

use Valkyrja\Console\CommandHandler;
use Valkyrja\Console\Support\ProvidesCommand;

/**
 * Class ConfigCache.
 *
 * @author Melech Mizrachi
 */
class ConfigCacheCommand extends CommandHandler
{
    use ProvidesCommand;

    /**
     * The command.
     */
    public const COMMAND           = 'config:cache';
    public const PATH              = self::COMMAND;
    public const SHORT_DESCRIPTION = 'Generate the config cache';

    /**
     * Run the command.
     *
     * @return int
     */
    public function run(): int
    {
        // If the cache file already exists, delete it
        if (file_exists(config()['cacheFilePath'])) {
            unlink(config()['cacheFilePath']);
        }

        // If the config file exists
        if (file_exists(config()['filePath'])) {
            // Re-setup the application with it
            app()->setup(require config()['filePath'], true);
        } else {
            // Otherwise just re-setup the application with default configs
            app()->setup(null, true);
        }

        $cache = config();

        // Get the results of the cache attempt
        $result = file_put_contents(
            config()['cacheFilePath'],
            '<?php return ' . var_export($cache, true) . ';',
            LOCK_EX
        );

        if ($result === false) {
            output()->writeMessage(
                'An error occurred while generating config cache.',
                true
            );

            return 1;
        }

        output()->writeMessage('Config cache generated successfully', true);

        return 0;
    }
}


<?php
namespace Li;

class Validator {
    public $rules = [];
    public $dataWrap = 'data';
    public $errors = [];
    protected $_data;
    protected $_dataType;
    protected $_customMessages;
    protected $_customAttributes;

    public static function make($data, $rules, $customMessages, $customAttributes) {
        return new Validator($data, $rules, $customMessages, $customAttributes);
    }

    public function __construct($data, $rules, $customMessages = [], $customAttributes = []) {
        $this->_data = $data;
        $this->setRules($rules);
        $this->_customMessages = $customMessages;
        $this->_customAttributes = $customAttributes;
    }

    public function setRules($rules) {
        $rules = $this->_explodeRules($rules);
        $this->rules = array_merge($this->rules, $rules);

        return $this;
    }
    public function fails() {
        return !$this->passes();
    }

    public function test() {
        return false;
    }

    public function passes() {
        foreach ($this->rules as $attribute => $rules) {
            foreach ($rules as $rule) {
                if (false === $this->_validate($attribute, $rule)) {
                    break;
                }
            }
        }

        return count($this->errors) === 0;
    }

    protected function _validate($attribute, $rule) {
        list($rule, $parameters) = $this->_parseRule($rule);

        if ($rule == '') {
            return true;
        }

        $method = '_validate' . Str::studly($rule);
        $value = $this->_getValue($attribute);

        if (!$this->$method($attribute, $value, $parameters, $this)) {
            $this->_addFailure($attribute, $rule, $parameters);
            return false;
        }

        return true;
    }

    protected function _parseRule($rules) {
        $parameters = [];

        if (strpos($rules, ':') !== false) {
            list($rules, $parameter) = explode(':', $rules, 2);
            $parameters = $this->_parseParameters($rules, $parameter);
        }
        $rules = $this->_normalizeRule($rules);

        return [$rules, $parameters];
    }

    protected function _normalizeRule($rule) {
        switch ($rule) {
            case 'int':
                return 'Integer';
            case 'bool':
                return 'Boolean';
            default:
                return $rule;
        }
    }

    protected function _explodeRules($rules) {
        foreach ($rules as $key => $rule) {
            $rules[$key] = (is_string($rule)) ? explode('|', $rule) : $rule;
        }

        return $rules;
    }

    protected function _parseParameters($rule, $parameter) {
        if (strtolower($rule) == 'regex') {
            return [$parameter];
        }

        return str_getcsv($parameter);
    }

    protected function _addFailure($attribute, $rule, $parameters) {

        list($id, $parameters) = $this->_transParameters($attribute, $rule, $parameters);

        $this->errors[$attribute] = App::app()->t($id, $parameters);
    }

    protected function _transParameters($attribute, $rule, $parameters) {
        $id = 'validation.' . $rule;

        if ($rule == 'between') {
            $id .= '.' . $this->_getType($attribute);
            $parameters = array_combine([':min', ':max'], $parameters);
        } else if ($rule == 'after') {
            $parameters = array_combine([':date'], $parameters);
        } else if ($rule == 'date_format') {
            $parameters = array_combine([':format'], $parameters);
        } else if ($rule == 'before') {
            $parameters = array_combine([':date'], $parameters);
        } else if ($rule == 'digits') {
            $parameters = array_combine([':digits'], $parameters);
        } else if ($rule == 'digits_between') {
            $parameters = array_combine([':min', ':max'], $parameters);
        } else if ($rule == 'max') {
            $id .= '.' . $this->_getType($attribute);
            $parameters = array_combine([':max'], $parameters);
        } else if ($rule == 'min') {
            $id .= '.' . $this->_getType($attribute);
            $parameters = array_combine([':min'], $parameters);
        }

        $parameters[':attribute'] = isset($this->_customAttributes[$attribute]) ? $this->_customAttributes[$attribute] : $attribute;

        return [$id, $parameters];
    }

    protected function _getType($attribute) {
        if (empty($this->_dataType[$attribute])) {

            if ($this->_hasRule($attribute, ['numeric', 'integer'])) {
                $this->_dataType[$attribute] = 'numeric';
            } else if ($this->_hasRule($attribute, ['array'])) {
                $this->_dataType[$attribute] = 'array';
            }
            $this->_dataType[$attribute] = 'string';

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

    protected function _getValue($attribute) {
        if (array_key_exists($attribute, $this->_data)) {
            return $this->_data[$attribute];
        }
    }

    protected function _requireParameterCount($count, $parameters, $rule) {
        if (count($parameters) < $count) {
            throw new Exception("Validation rule $rule requires at least $count parameters.");
        }
    }

    protected function _validateAccepted($attribute, $value) {
        $acceptable = ['yes', 'on', '1', 1, true, 'true'];

        return $this->_validateRequired($attribute, $value) && in_array($value, $acceptable, true);
    }

    protected function _validateActiveUrl($attribute, $value)
    {
        if (! is_string($value)) {
            return false;
        }

        if ($url = parse_url($value, PHP_URL_HOST)) {
            return count(dns_get_record($url, DNS_A | DNS_AAAA)) > 0;
        }

        return false;
    }

    /**
     * Validate the date is after a given date.
     *
     * @param  string  $attribute
     * @param  mixed   $value
     * @param  array   $parameters
     * @return bool
     */
    protected function _validateAfter($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'after');

        if (! is_string($value)) {
            return false;
        }

        if ($format = $this->_getDateFormat($attribute)) {
            return $this->_validateAfterWithFormat($format, $value, $parameters);
        }

        if (! ($date = strtotime($parameters[0]))) {
            return strtotime($value) > strtotime($this->_getValue($parameters[0]));
        }

        return strtotime($value) > $date;
    }

    /**
     * Validate the date is after a given date with a given format.
     *
     * @param  string  $format
     * @param  mixed   $value
     * @param  array   $parameters
     * @return bool
     */
    protected function _validateAfterWithFormat($format, $value, $parameters)
    {
        $param = $this->_getValue($parameters[0]) ?: $parameters[0];

        return $this->_checkDateTimeOrder($format, $param, $value);
    }

    protected function _checkDateTimeOrder($format, $before, $after)
    {
        $before = $this->_getDateTimeWithOptionalFormat($format, $before);

        $after = $this->_getDateTimeWithOptionalFormat($format, $after);

        return ($before && $after) && ($after > $before);
    }

    protected function _getDateTimeWithOptionalFormat($format, $value)
    {
        $date = \DateTime::createFromFormat($format, $value);

        if ($date) {
            return $date;
        }

        try {
            return new \DateTime($value);
        } catch (Exception $e) {
            //
        }
    }

    /**
     * Validate that an attribute matches a date format.
     *
     * @param  string  $attribute
     * @param  mixed   $value
     * @param  array   $parameters
     * @return bool
     */
    protected function _validateDateFormat($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'date_format');

        if (! is_string($value)) {
            return false;
        }

        $parsed = date_parse_from_format($parameters[0], $value);

        return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
    }

    /**
     * Get the date format for an attribute if it has one.
     *
     * @param  string  $attribute
     * @return string|null
     */
    protected function _getDateFormat($attribute)
    {
        if ($result = $this->_getRule($attribute, 'date_format')) {
            return $result[1][0];
        }
    }

    protected function _validateAlpha($attribute, $value)
    {
        return is_string($value) && preg_match('/^[\pL\pM]+$/u', $value);
    }

    protected function _validateAlphaDash($attribute, $value)
    {
        if (! is_string($value) && ! is_numeric($value)) {
            return false;
        }

        return preg_match('/^[\pL\pM\pN_-]+$/u', $value);
    }

    protected function _validateAlphaNum($attribute, $value)
    {
        if (! is_string($value) && ! is_numeric($value)) {
            return false;
        }

        return preg_match('/^[\pL\pM\pN]+$/u', $value);
    }

    protected function _validateArray($attribute, $value)
    {
        return is_null($value) || is_array($value);
    }

    protected function _validateBefore($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'before');

        if (! is_string($value)) {
            return false;
        }

        if ($format = $this->_getDateFormat($attribute)) {
            return $this->_validateBeforeWithFormat($format, $value, $parameters);
        }

        if (! ($date = strtotime($parameters[0]))) {
            return strtotime($value) < strtotime($this->getValue($parameters[0]));
        }

        return strtotime($value) < $date;
    }

    protected function _validateBeforeWithFormat($format, $value, $parameters)
    {
        $param = $this->_getValue($parameters[0]) ?: $parameters[0];

        return $this->_checkDateTimeOrder($format, $value, $param);
    }

    protected function _validateBetween($attribute, $value, $parameters) {
        $this->_requireParameterCount(2, $parameters, 'between');
        $size = $this->_getSize($attribute, $value);
        return $size >= $parameters[0] && $size <= $parameters[1];
    }

    protected function _validateBoolean($attribute, $value)
    {
        $acceptable = [true, false, 0, 1, '0', '1'];

        return is_null($value) || in_array($value, $acceptable, true);
    }

    protected function _validateConfirmed($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'confirmed');
        $other = $this->_getValue($parameters[0]);
        return isset($other) && $value === $other;
    }

    protected function _validateDate($attribute, $value)
    {
        if ($value instanceof DateTime) {
            return true;
        }

        if (! is_string($value) || strtotime($value) === false) {
            return false;
        }

        $date = date_parse($value);

        return checkdate($date['month'], $date['day'], $date['year']);
    }

    protected function _validateDifferent($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'different');
        $other = $this->_getValue($parameters[0]);
        return isset($other) && $value !== $other;
    }

    protected function _validateDigits($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'digits');

        return $this->_validateNumeric($attribute, $value)
            && strlen((string) $value) == $parameters[0];
    }

    protected function _validateDigitsBetween($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(2, $parameters, 'digits_between');

        $length = strlen((string) $value);

        return $this->_validateNumeric($attribute, $value)
          && $length >= $parameters[0] && $length <= $parameters[1];
    }

    protected function _validateEmail($attribute, $value)
    {
        return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
    }

    protected function _validateIn($attribute, $value, $parameters)
    {
        if (is_array($value) && $this->_hasRule($attribute, 'Array')) {
            return count(array_diff($value, $parameters)) == 0;
        }

        return ! is_array($value) && in_array((string) $value, $parameters);
    }

    protected function _validateInteger($attribute, $value)
    {
        return is_null($value) || filter_var($value, FILTER_VALIDATE_INT) !== false;
    }

    protected function _validateIp($attribute, $value)
    {
        return filter_var($value, FILTER_VALIDATE_IP) !== false;
    }

    protected function _validateJson($attribute, $value)
    {
        if (! is_scalar($value) && ! method_exists($value, '__toString')) {
            return false;
        }

        json_decode($value);

        return json_last_error() === JSON_ERROR_NONE;
    }

    protected function _validateMax($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'max');
        return $this->_getSize($attribute, $value) <= $parameters[0];
    }

    protected function _validateMin($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'min');
        return $this->_getSize($attribute, $value) >= $parameters[0];
    }

    protected function _validateNotIn($attribute, $value, $parameters)
    {
        return ! $this->_validateIn($attribute, $value, $parameters);
    }

    protected function _validateNumeric($attribute, $value, $parameters) {
        return is_null($value) || is_numeric($value);
    }

    protected function _validateRegex($attribute, $value, $parameters)
    {
        if (! is_string($value) && ! is_numeric($value)) {
            return false;
        }

        $this->_requireParameterCount(1, $parameters, 'regex');

        return preg_match($parameters[0], $value);
    }

    protected function _validateRequired($attribute, $value) {
        if (is_null($value)) {
            return false;
        } elseif (is_string($value) && trim($value) === '') {
            return false;
        }

        return true;
    }

    protected function _validateSize($attribute, $value, $parameters)
    {
        $this->_requireParameterCount(1, $parameters, 'size');

        return $this->_getSize($attribute, $value) == $parameters[0];
    }

    protected function _validateString($attribute, $value)
    {
        return is_null($value) || is_string($value);
    }

    protected function _validateTimezone($attribute, $value)
    {
        try {
            new DateTimeZone($value);
        } catch (Exception $e) {
            return false;
        }

        return true;
    }

    protected function _getSize($attribute, $value) {
        $type = $this->_getType($attribute);

        if ('numeric' == $type) {
            return $value;
        } else if ('array' == $type) {
            return count($value);
        }

        return mb_strlen($value);
    }

    protected function _hasRule($attribute, $rules) {
        return !is_null($this->_getRule($attribute, $rules));
    }

    protected function _getRule($attribute, $rules) {
        if (!array_key_exists($attribute, $this->rules)) {
            return;
        }

        $rules = (array) $rules;

        foreach ($this->rules[$attribute] as $rule) {
            list($rule, $parameters) = $this->_parseRule($rule);

            if (in_array($rule, $rules)) {
                return [$rule, $parameters];
            }
        }
    }

    /**
     * Validate that an attribute was "accepted".
     *
     * This validation rule implies the attribute is "required".
     *
     * @param  string  $attribute
     * @param  mixed   $value
     * @return bool
     */
    protected function validateAccepted($attribute, $value) {
        $acceptable = ['yes', 'on', '1', 1, true, 'true'];

        return $this->validateRequired($attribute, $value) && in_array($value, $acceptable, true);
    }

    public function getValidator() {
        echo json_encode($this->errors);
    }

    /**
     * 验证
     */
    /*
    public function validate_old($data, $rules, $message = [], $attributes = []) {
        //解析rule
        foreach ($this->rule as $key => $rule) {
            foreach ($rule as $r) {
                if ($r[0] == 'required') {
                    if (!isset($data[$key]) || $data[$key] === '') {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'email') {
                    if (!isset($data[$key]) || !$this->checkEmail($data[$key])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'number') {
                    if (!isset($data[$key]) || !is_numeric($data[$key])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'digits') {
                    if (!isset($data[$key]) && !is_numeric($data[$key]) && !is_int($data[$key] + 0)) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'date') {
                    debug($data[$key]);

                    if (!isset($data[$key]) || !$this->checkDate(($data[$key]), $r[2])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'url') {
                    if (!isset($data[$key]) || !$this->checkUrl($data[$key])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'minlength') {
                    if (!isset($data[$key]) || strlen($data[$key]) < $r[2]) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'maxlength') {
                    if (!isset($data[$key]) || strlen($data[$key]) > $r[2]) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'equalTo') {
                    if (!isset($data[$key]) || $data[$key] != $r[2]) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'rangelength') {
                    $len = strlen($data[$key]);
                    if (!isset($data[$key]) || ($len < $r[2] || $len > $r[3])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'range') {
                    if (!isset($data[$key]) || ($data[$key] < $r[2] || $data[$key] > $r[3])) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'max') {
                    if (!isset($data[$key]) || $data[$key] > $r[2]) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                } else if ($r[0] == 'min') {
                    if (!isset($data[$key]) || $data[$key] < $r[2]) {
                        $this->error[$key] = $r[1];
                        break;
                    }
                }
            }
        }

        return $this->error;
    }

    public function checkEmail($email) {
        $pattern = "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i";

        if (preg_match($pattern, $email)) {
            return true;
        }

        return false;
    }
    public function checkUrl($url) {
        $pattern = "|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i";

        if (preg_match($pattern, $url)) {
            return true;
        }

        return false;
    }

    public function checkDate($date, $format) {
        $format = str_replace('YYYY', 'Y', $format);
        $format = str_replace('YY', 'y', $format);

        $format = str_replace('MM', 'm', $format);

        $format = str_replace('DD', 'd', $format);

        $format = str_replace('HH', 'H', $format);
        $format = str_replace('mm', 'i', $format);
        $format = str_replace('ss', 's', $format);

        if (empty($format)) {
            $format = 'Y-m-d';
        }
        $dt = \DateTime::createFromFormat($format, $date);
        return $dt !== false && !array_sum($dt->getLastErrors());
    }

    public function getError() {
        $error = '';
        $errorCount = count($this->error);
        $i = 0;
        foreach ($this->error as $key => $value) {
            $error .= '"' . $this->dataWrap . '[' . $key . ']":"' . $value . '"';

            if ($i < $errorCount - 1) {
                $error .= ',';
            }
            $i++;
        }

        $js = <<< EOF
    {$this->dataWrap}_validate.showErrors({
        {$error}
    });
EOF;
        echo $js;
    }
    */
}


<?php

namespace Reaction\Db\Conditions;

use Reaction\Exceptions\InvalidArgumentException;
use Reaction\Db\Expressions\ExpressionBuilderInterface;
use Reaction\Db\Expressions\ExpressionBuilderTrait;
use Reaction\Db\Expressions\ExpressionInterface;

/**
 * Class LikeConditionBuilder builds objects of [[LikeCondition]]
 */
class LikeConditionBuilder implements ExpressionBuilderInterface
{
    use ExpressionBuilderTrait;

    /**
     * @var array map of chars to their replacements in LIKE conditions.
     * By default it's configured to escape `%`, `_` and `\` with `\`.
     */
    protected $escapingReplacements = [
        '%' => '\%',
        '_' => '\_',
        '\\' => '\\\\',
    ];
    /**
     * @var string|null character used to escape special characters in LIKE conditions.
     * By default it's assumed to be `\`.
     */
    protected $escapeCharacter;

    /**
     * Method builds the raw SQL from the $expression that will not be additionally
     * escaped or quoted.
     *
     * @param LikeCondition $expression the expression to be built.
     * @param array $params the binding parameters.
     * @return string the raw SQL that will not be additionally escaped or quoted.
     */
    public function build($expression, array &$params = [])
    {
        $operator = $expression->getOperator();
        $column = $expression->getColumn();
        $values = $expression->getValue();
        $escape = $expression->getEscapingReplacements();
        if ($escape === null || $escape === []) {
            $escape = $this->escapingReplacements;
        }

        list($andor, $not, $operator) = $this->parseOperator($operator);

        if (!is_array($values)) {
            $values = [$values];
        }

        if (empty($values)) {
            return $not ? '' : '0=1';
        }

        if (strpos($column, '(') === false) {
            $column = $this->queryBuilder->db->quoteColumnName($column);
        }

        $escapeSql = $this->getEscapeSql();
        $parts = [];
        foreach ($values as $value) {
            if ($value instanceof ExpressionInterface) {
                $phName = $this->queryBuilder->buildExpression($value, $params);
            } else {
                $phName = $this->queryBuilder->bindParam(empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'), $params);
            }
            $parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
        }

        return implode($andor, $parts);
    }

    /**
     * @return string
     */
    private function getEscapeSql()
    {
        if ($this->escapeCharacter !== null) {
            return " ESCAPE '{$this->escapeCharacter}'";
        }

        return '';
    }

    /**
     * @param string $operator
     * @return array
     */
    protected function parseOperator($operator)
    {
        if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
            throw new InvalidArgumentException("Invalid operator '$operator'.");
        }
        $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
        $not = !empty($matches[3]);
        $operator = $matches[2];

        return [$andor, $not, $operator];
    }
}