<?php
/**
 * Socket-based adapter for HTTP_Request2
 *
 * PHP version 5
 *
 * LICENSE
 *
 * This source file is subject to BSD 3-Clause License that is bundled
 * with this package in the file LICENSE and available at the URL
 * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
 *
 * @category  HTTP
 * @package   HTTP_Request2
 * @author    Alexey Borzov <avb@php.net>
 * @copyright 2008-2014 Alexey Borzov <avb@php.net>
 * @license   http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
 * @link      http://pear.php.net/package/HTTP_Request2
 */

/** Base class for HTTP_Request2 adapters */
require_once 'HTTP/Request2/Adapter.php';

/** Socket wrapper class */
require_once 'HTTP/Request2/SocketWrapper.php';

/**
 * Socket-based adapter for HTTP_Request2
 *
 * This adapter uses only PHP sockets and will work on almost any PHP
 * environment. Code is based on original HTTP_Request PEAR package.
 *
 * @category HTTP
 * @package  HTTP_Request2
 * @author   Alexey Borzov <avb@php.net>
 * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
 * @version  Release: 2.2.1
 * @link     http://pear.php.net/package/HTTP_Request2
 */
class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
{
    /**
     * Regular expression for 'token' rule from RFC 2616
     */
    const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';

    /**
     * Regular expression for 'quoted-string' rule from RFC 2616
     */
    const REGEXP_QUOTED_STRING = '"(?>[^"\\\\]+|\\\\.)*"';

    /**
     * Connected sockets, needed for Keep-Alive support
     * @var  array
     * @see  connect()
     */
    protected static $sockets = array();

    /**
     * Data for digest authentication scheme
     *
     * The keys for the array are URL prefixes.
     *
     * The values are associative arrays with data (realm, nonce, nonce-count,
     * opaque...) needed for digest authentication. Stored here to prevent making
     * duplicate requests to digest-protected resources after we have already
     * received the challenge.
     *
     * @var  array
     */
    protected static $challenges = array();

    /**
     * Connected socket
     * @var  HTTP_Request2_SocketWrapper
     * @see  connect()
     */
    protected $socket;

    /**
     * Challenge used for server digest authentication
     * @var  array
     */
    protected $serverChallenge;

    /**
     * Challenge used for proxy digest authentication
     * @var  array
     */
    protected $proxyChallenge;

    /**
     * Remaining length of the current chunk, when reading chunked response
     * @var  integer
     * @see  readChunked()
     */
    protected $chunkLength = 0;

    /**
     * Remaining amount of redirections to follow
     *
     * Starts at 'max_redirects' configuration parameter and is reduced on each
     * subsequent redirect. An Exception will be thrown once it reaches zero.
     *
     * @var  integer
     */
    protected $redirectCountdown = null;

    /**
     * Whether to wait for "100 Continue" response before sending request body
     * @var bool
     */
    protected $expect100Continue = false;

    /**
     * Sends request to the remote server and returns its response
     *
     * @param HTTP_Request2 $request HTTP request message
     *
     * @return   HTTP_Request2_Response
     * @throws   HTTP_Request2_Exception
     */
    public function sendRequest(HTTP_Request2 $request)
    {
        $this->request = $request;

        try {
            $keepAlive = $this->connect();
            $headers   = $this->prepareHeaders();
            $this->socket->write($headers);
            // provide request headers to the observer, see request #7633
            $this->request->setLastEvent('sentHeaders', $headers);

            if (!$this->expect100Continue) {
                $this->writeBody();
                $response = $this->readResponse();

            } else {
                $response = $this->readResponse();
                if (!$response || 100 == $response->getStatus()) {
                    $this->expect100Continue = false;
                    // either got "100 Continue" or timed out -> send body
                    $this->writeBody();
                    $response = $this->readResponse();
                }
            }

            if ($jar = $request->getCookieJar()) {
                $jar->addCookiesFromResponse($response, $request->getUrl());
            }

            if (!$this->canKeepAlive($keepAlive, $response)) {
                $this->disconnect();
            }

            if ($this->shouldUseProxyDigestAuth($response)) {
                return $this->sendRequest($request);
            }
            if ($this->shouldUseServerDigestAuth($response)) {
                return $this->sendRequest($request);
            }
            if ($authInfo = $response->getHeader('authentication-info')) {
                $this->updateChallenge($this->serverChallenge, $authInfo);
            }
            if ($proxyInfo = $response->getHeader('proxy-authentication-info')) {
                $this->updateChallenge($this->proxyChallenge, $proxyInfo);
            }

        } catch (Exception $e) {
            $this->disconnect();
        }

        unset($this->request, $this->requestBody);

        if (!empty($e)) {
            $this->redirectCountdown = null;
            throw $e;
        }

        if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {
            $this->redirectCountdown = null;
            return $response;
        } else {
            return $this->handleRedirect($request, $response);
        }
    }

    /**
     * Connects to the remote server
     *
     * @return   bool    whether the connection can be persistent
     * @throws   HTTP_Request2_Exception
     */
    protected function connect()
    {
        $secure  = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');
        $tunnel  = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
        $headers = $this->request->getHeaders();
        $reqHost = $this->request->getUrl()->getHost();
        if (!($reqPort = $this->request->getUrl()->getPort())) {
            $reqPort = $secure? 443: 80;
        }

        $httpProxy = $socksProxy = false;
        if (!($host = $this->request->getConfig('proxy_host'))) {
            $host = $reqHost;
            $port = $reqPort;
        } else {
            if (!($port = $this->request->getConfig('proxy_port'))) {
                throw new HTTP_Request2_LogicException(
                    'Proxy port not provided',
                    HTTP_Request2_Exception::MISSING_VALUE
                );
            }
            if ('http' == ($type = $this->request->getConfig('proxy_type'))) {
                $httpProxy = true;
            } elseif ('socks5' == $type) {
                $socksProxy = true;
            } else {
                throw new HTTP_Request2_NotImplementedException(
                    "Proxy type '{$type}' is not supported"
                );
            }
        }

        if ($tunnel && !$httpProxy) {
            throw new HTTP_Request2_LogicException(
                "Trying to perform CONNECT request without proxy",
                HTTP_Request2_Exception::MISSING_VALUE
            );
        }
        if ($secure && !in_array('ssl', stream_get_transports())) {
            throw new HTTP_Request2_LogicException(
                'Need OpenSSL support for https:// requests',
                HTTP_Request2_Exception::MISCONFIGURATION
            );
        }

        // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
        // connection token to a proxy server...
        if ($httpProxy && !$secure && !empty($headers['connection'])
            && 'Keep-Alive' == $headers['connection']
        ) {
            $this->request->setHeader('connection');
        }

        $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&
                      empty($headers['connection'])) ||
                     (!empty($headers['connection']) &&
                      'Keep-Alive' == $headers['connection']);

        $options = array();
        if ($ip = $this->request->getConfig('local_ip')) {
            $options['socket'] = array(
                'bindto' => (false === strpos($ip, ':') ? $ip : '[' . $ip . ']') . ':0'
            );
        }
        if ($secure || $tunnel) {
            $options['ssl'] = array();
            foreach ($this->request->getConfig() as $name => $value) {
                if ('ssl_' == substr($name, 0, 4) && null !== $value) {
                    if ('ssl_verify_host' == $name) {
                        if ($value) {
                            $options['ssl']['CN_match'] = $reqHost;
                        }
                    } else {
                        $options['ssl'][substr($name, 4)] = $value;
                    }
                }
            }
            ksort($options['ssl']);
        }

        // Use global request timeout if given, see feature requests #5735, #8964
        if ($timeout = $this->request->getConfig('timeout')) {
            $deadline = time() + $timeout;
        } else {
            $deadline = null;
        }

        // Changing SSL context options after connection is established does *not*
        // work, we need a new connection if options change
        $remote    = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://')
                     . $host . ':' . $port;
        $socketKey = $remote . (
                        ($secure && $httpProxy || $socksProxy)
                        ? "->{$reqHost}:{$reqPort}" : ''
                     ) . (empty($options)? '': ':' . serialize($options));
        unset($this->socket);

        // We use persistent connections and have a connected socket?
        // Ensure that the socket is still connected, see bug #16149
        if ($keepAlive && !empty(self::$sockets[$socketKey])
            && !self::$sockets[$socketKey]->eof()
        ) {
            $this->socket =& self::$sockets[$socketKey];

        } else {
            if ($socksProxy) {
                require_once 'HTTP/Request2/SOCKS5.php';

                $this->socket = new HTTP_Request2_SOCKS5(
                    $remote, $this->request->getConfig('connect_timeout'),
                    $options, $this->request->getConfig('proxy_user'),
                    $this->request->getConfig('proxy_password')
                );
                // handle request timeouts ASAP
                $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));
                $this->socket->connect($reqHost, $reqPort);
                if (!$secure) {
                    $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}";
                } else {
                    $this->socket->enableCrypto();
                    $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";
                }

            } elseif ($secure && $httpProxy && !$tunnel) {
                $this->establishTunnel();
                $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";

            } else {
                $this->socket = new HTTP_Request2_SocketWrapper(
                    $remote, $this->request->getConfig('connect_timeout'), $options
                );
            }
            $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo);
            self::$sockets[$socketKey] =& $this->socket;
        }
        $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));
        return $keepAlive;
    }

    /**
     * Establishes a tunnel to a secure remote server via HTTP CONNECT request
     *
     * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP
     * sees that we are connected to a proxy server (duh!) rather than the server
     * that presents its certificate.
     *
     * @link     http://tools.ietf.org/html/rfc2817#section-5.2
     * @throws   HTTP_Request2_Exception
     */
    protected function establishTunnel()
    {
        $donor   = new self;
        $connect = new HTTP_Request2(
            $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT,
            array_merge($this->request->getConfig(), array('adapter' => $donor))
        );
        $response = $connect->send();
        // Need any successful (2XX) response
        if (200 > $response->getStatus() || 300 <= $response->getStatus()) {
            throw new HTTP_Request2_ConnectionException(
                'Failed to connect via HTTPS proxy. Proxy response: ' .
                $response->getStatus() . ' ' . $response->getReasonPhrase()
            );
        }
        $this->socket = $donor->socket;
        $this->socket->enableCrypto();
    }

    /**
     * Checks whether current connection may be reused or should be closed
     *
     * @param boolean                $requestKeepAlive whether connection could
     *                               be persistent in the first place
     * @param HTTP_Request2_Response $response         response object to check
     *
     * @return   boolean
     */
    protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)
    {
        // Do not close socket on successful CONNECT request
        if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()
            && 200 <= $response->getStatus() && 300 > $response->getStatus()
        ) {
            return true;
        }

        $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))
                       || null !== $response->getHeader('content-length')
                       // no body possible for such responses, see also request #17031
                       || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
                       || in_array($response->getStatus(), array(204, 304));
        $persistent  = 'keep-alive' == strtolower($response->getHeader('connection')) ||
                       (null === $response->getHeader('connection') &&
                        '1.1' == $response->getVersion());
        return $requestKeepAlive && $lengthKnown && $persistent;
    }

    /**
     * Disconnects from the remote server
     */
    protected function disconnect()
    {
        if (!empty($this->socket)) {
            $this->socket = null;
            $this->request->setLastEvent('disconnect');
        }
    }

    /**
     * Handles HTTP redirection
     *
     * This method will throw an Exception if redirect to a non-HTTP(S) location
     * is attempted, also if number of redirects performed already is equal to
     * 'max_redirects' configuration parameter.
     *
     * @param HTTP_Request2          $request  Original request
     * @param HTTP_Request2_Response $response Response containing redirect
     *
     * @return   HTTP_Request2_Response      Response from a new location
     * @throws   HTTP_Request2_Exception
     */
    protected function handleRedirect(
        HTTP_Request2 $request, HTTP_Request2_Response $response
    ) {
        if (is_null($this->redirectCountdown)) {
            $this->redirectCountdown = $request->getConfig('max_redirects');
        }
        if (0 == $this->redirectCountdown) {
            $this->redirectCountdown = null;
            // Copying cURL behaviour
            throw new HTTP_Request2_MessageException(
                'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',
                HTTP_Request2_Exception::TOO_MANY_REDIRECTS
            );
        }
        $redirectUrl = new Net_URL2(
            $response->getHeader('location'),
            array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))
        );
        // refuse non-HTTP redirect
        if ($redirectUrl->isAbsolute()
            && !in_array($redirectUrl->getScheme(), array('http', 'https'))
        ) {
            $this->redirectCountdown = null;
            throw new HTTP_Request2_MessageException(
                'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),
                HTTP_Request2_Exception::NON_HTTP_REDIRECT
            );
        }
        // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),
        // but in practice it is often not
        if (!$redirectUrl->isAbsolute()) {
            $redirectUrl = $request->getUrl()->resolve($redirectUrl);
        }
        $redirect = clone $request;
        $redirect->setUrl($redirectUrl);
        if (303 == $response->getStatus()
            || (!$request->getConfig('strict_redirects')
                && in_array($response->getStatus(), array(301, 302)))
        ) {
            $redirect->setMethod(HTTP_Request2::METHOD_GET);
            $redirect->setBody('');
        }

        if (0 < $this->redirectCountdown) {
            $this->redirectCountdown--;
        }
        return $this->sendRequest($redirect);
    }

    /**
     * Checks whether another request should be performed with server digest auth
     *
     * Several conditions should be satisfied for it to return true:
     *   - response status should be 401
     *   - auth credentials should be set in the request object
     *   - response should contain WWW-Authenticate header with digest challenge
     *   - there is either no challenge stored for this URL or new challenge
     *     contains stale=true parameter (in other case we probably just failed
     *     due to invalid username / password)
     *
     * The method stores challenge values in $challenges static property
     *
     * @param HTTP_Request2_Response $response response to check
     *
     * @return   boolean whether another request should be performed
     * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters
     */
    protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)
    {
        // no sense repeating a request if we don't have credentials
        if (401 != $response->getStatus() || !$this->request->getAuth()) {
            return false;
        }
        if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) {
            return false;
        }

        $url    = $this->request->getUrl();
        $scheme = $url->getScheme();
        $host   = $scheme . '://' . $url->getHost();
        if ($port = $url->getPort()) {
            if ((0 == strcasecmp($scheme, 'http') && 80 != $port)
                || (0 == strcasecmp($scheme, 'https') && 443 != $port)
            ) {
                $host .= ':' . $port;
            }
        }

        if (!empty($challenge['domain'])) {
            $prefixes = array();
            foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) {
                // don't bother with different servers
                if ('/' == substr($prefix, 0, 1)) {
                    $prefixes[] = $host . $prefix;
                }
            }
        }
        if (empty($prefixes)) {
            $prefixes = array($host . '/');
        }

        $ret = true;
        foreach ($prefixes as $prefix) {
            if (!empty(self::$challenges[$prefix])
                && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
            ) {
                // probably credentials are invalid
                $ret = false;
            }
            self::$challenges[$prefix] =& $challenge;
        }
        return $ret;
    }

    /**
     * Checks whether another request should be performed with proxy digest auth
     *
     * Several conditions should be satisfied for it to return true:
     *   - response status should be 407
     *   - proxy auth credentials should be set in the request object
     *   - response should contain Proxy-Authenticate header with digest challenge
     *   - there is either no challenge stored for this proxy or new challenge
     *     contains stale=true parameter (in other case we probably just failed
     *     due to invalid username / password)
     *
     * The method stores challenge values in $challenges static property
     *
     * @param HTTP_Request2_Response $response response to check
     *
     * @return   boolean whether another request should be performed
     * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters
     */
    protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)
    {
        if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) {
            return false;
        }
        if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) {
            return false;
        }

        $key = 'proxy://' . $this->request->getConfig('proxy_host') .
               ':' . $this->request->getConfig('proxy_port');

        if (!empty(self::$challenges[$key])
            && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
        ) {
            $ret = false;
        } else {
            $ret = true;
        }
        self::$challenges[$key] = $challenge;
        return $ret;
    }

    /**
     * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value
     *
     * There is a problem with implementation of RFC 2617: several of the parameters
     * are defined as quoted-string there and thus may contain backslash escaped
     * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as
     * just value of quoted-string X without surrounding quotes, it doesn't speak
     * about removing backslash escaping.
     *
     * Now realm parameter is user-defined and human-readable, strange things
     * happen when it contains quotes:
     *   - Apache allows quotes in realm, but apparently uses realm value without
     *     backslashes for digest computation
     *   - Squid allows (manually escaped) quotes there, but it is impossible to
     *     authorize with either escaped or unescaped quotes used in digest,
     *     probably it can't parse the response (?)
     *   - Both IE and Firefox display realm value with backslashes in
     *     the password popup and apparently use the same value for digest
     *
     * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in
     * quoted-string handling, unfortunately that means failure to authorize
     * sometimes
     *
     * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header
     *
     * @return   mixed   associative array with challenge parameters, false if
     *                   no challenge is present in header value
     * @throws   HTTP_Request2_NotImplementedException in case of unsupported challenge parameters
     */
    protected function parseDigestChallenge($headerValue)
    {
        $authParam   = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
                       self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')';
        $challenge   = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";
        if (!preg_match($challenge, $headerValue, $matches)) {
            return false;
        }

        preg_match_all('!' . $authParam . '!', $matches[0], $params);
        $paramsAry   = array();
        $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale',
                             'algorithm', 'qop');
        for ($i = 0; $i < count($params[0]); $i++) {
            // section 3.2.1: Any unrecognized directive MUST be ignored.
            if (in_array($params[1][$i], $knownParams)) {
                if ('"' == substr($params[2][$i], 0, 1)) {
                    $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
                } else {
                    $paramsAry[$params[1][$i]] = $params[2][$i];
                }
            }
        }
        // we only support qop=auth
        if (!empty($paramsAry['qop'])
            && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))
        ) {
            throw new HTTP_Request2_NotImplementedException(
                "Only 'auth' qop is currently supported in digest authentication, " .
                "server requested '{$paramsAry['qop']}'"
            );
        }
        // we only support algorithm=MD5
        if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {
            throw new HTTP_Request2_NotImplementedException(
                "Only 'MD5' algorithm is currently supported in digest authentication, " .
                "server requested '{$paramsAry['algorithm']}'"
            );
        }

        return $paramsAry;
    }

    /**
     * Parses [Proxy-]Authentication-Info header value and updates challenge
     *
     * @param array  &$challenge  challenge to update
     * @param string $headerValue value of [Proxy-]Authentication-Info header
     *
     * @todo     validate server rspauth response
     */
    protected function updateChallenge(&$challenge, $headerValue)
    {
        $authParam   = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
                       self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!';
        $paramsAry   = array();

        preg_match_all($authParam, $headerValue, $params);
        for ($i = 0; $i < count($params[0]); $i++) {
            if ('"' == substr($params[2][$i], 0, 1)) {
                $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
            } else {
                $paramsAry[$params[1][$i]] = $params[2][$i];
            }
        }
        // for now, just update the nonce value
        if (!empty($paramsAry['nextnonce'])) {
            $challenge['nonce'] = $paramsAry['nextnonce'];
            $challenge['nc']    = 1;
        }
    }

    /**
     * Creates a value for [Proxy-]Authorization header when using digest authentication
     *
     * @param string $user       user name
     * @param string $password   password
     * @param string $url        request URL
     * @param array  &$challenge digest challenge parameters
     *
     * @return   string  value of [Proxy-]Authorization request header
     * @link     http://tools.ietf.org/html/rfc2617#section-3.2.2
     */
    protected function createDigestResponse($user, $password, $url, &$challenge)
    {
        if (false !== ($q = strpos($url, '?'))
            && $this->request->getConfig('digest_compat_ie')
        ) {
            $url = substr($url, 0, $q);
        }

        $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);
        $a2 = md5($this->request->getMethod() . ':' . $url);

        if (empty($challenge['qop'])) {
            $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
        } else {
            $challenge['cnonce'] = 'Req2.' . rand();
            if (empty($challenge['nc'])) {
                $challenge['nc'] = 1;
            }
            $nc     = sprintf('%08x', $challenge['nc']++);
            $digest = md5(
                $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' .
                $challenge['cnonce'] . ':auth:' . $a2
            );
        }
        return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' .
               'realm="' . $challenge['realm'] . '", ' .
               'nonce="' . $challenge['nonce'] . '", ' .
               'uri="' . $url . '", ' .
               'response="' . $digest . '"' .
               (!empty($challenge['opaque'])?
                ', opaque="' . $challenge['opaque'] . '"':
                '') .
               (!empty($challenge['qop'])?
                ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"':
                '');
    }

    /**
     * Adds 'Authorization' header (if needed) to request headers array
     *
     * @param array  &$headers    request headers
     * @param string $requestHost request host (needed for digest authentication)
     * @param string $requestUrl  request URL (needed for digest authentication)
     *
     * @throws   HTTP_Request2_NotImplementedException
     */
    protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)
    {
        if (!($auth = $this->request->getAuth())) {
            return;
        }
        switch ($auth['scheme']) {
        case HTTP_Request2::AUTH_BASIC:
            $headers['authorization'] = 'Basic ' . base64_encode(
                $auth['user'] . ':' . $auth['password']
            );
            break;

        case HTTP_Request2::AUTH_DIGEST:
            unset($this->serverChallenge);
            $fullUrl = ('/' == $requestUrl[0])?
                       $this->request->getUrl()->getScheme() . '://' .
                        $requestHost . $requestUrl:
                       $requestUrl;
            foreach (array_keys(self::$challenges) as $key) {
                if ($key == substr($fullUrl, 0, strlen($key))) {
                    $headers['authorization'] = $this->createDigestResponse(
                        $auth['user'], $auth['password'],
                        $requestUrl, self::$challenges[$key]
                    );
                    $this->serverChallenge =& self::$challenges[$key];
                    break;
                }
            }
            break;

        default:
            throw new HTTP_Request2_NotImplementedException(
                "Unknown HTTP authentication scheme '{$auth['scheme']}'"
            );
        }
    }

    /**
     * Adds 'Proxy-Authorization' header (if needed) to request headers array
     *
     * @param array  &$headers   request headers
     * @param string $requestUrl request URL (needed for digest authentication)
     *
     * @throws   HTTP_Request2_NotImplementedException
     */
    protected function addProxyAuthorizationHeader(&$headers, $requestUrl)
    {
        if (!$this->request->getConfig('proxy_host')
            || !($user = $this->request->getConfig('proxy_user'))
            || (0 == strcasecmp('https', $this->request->getUrl()->getScheme())
                && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod())
        ) {
            return;
        }

        $password = $this->request->getConfig('proxy_password');
        switch ($this->request->getConfig('proxy_auth_scheme')) {
        case HTTP_Request2::AUTH_BASIC:
            $headers['proxy-authorization'] = 'Basic ' . base64_encode(
                $user . ':' . $password
            );
            break;

        case HTTP_Request2::AUTH_DIGEST:
            unset($this->proxyChallenge);
            $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') .
                        ':' . $this->request->getConfig('proxy_port');
            if (!empty(self::$challenges[$proxyUrl])) {
                $headers['proxy-authorization'] = $this->createDigestResponse(
                    $user, $password,
                    $requestUrl, self::$challenges[$proxyUrl]
                );
                $this->proxyChallenge =& self::$challenges[$proxyUrl];
            }
            break;

        default:
            throw new HTTP_Request2_NotImplementedException(
                "Unknown HTTP authentication scheme '" .
                $this->request->getConfig('proxy_auth_scheme') . "'"
            );
        }
    }

    /**
     * Creates the string with the Request-Line and request headers
     *
     * @return   string
     * @throws   HTTP_Request2_Exception
     */
    protected function prepareHeaders()
    {
        $headers = $this->request->getHeaders();
        $url     = $this->request->getUrl();
        $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
        $host    = $url->getHost();

        $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80;
        if (($port = $url->getPort()) && $port != $defaultPort || $connect) {
            $host .= ':' . (empty($port)? $defaultPort: $port);
        }
        // Do not overwrite explicitly set 'Host' header, see bug #16146
        if (!isset($headers['host'])) {
            $headers['host'] = $host;
        }

        if ($connect) {
            $requestUrl = $host;

        } else {
            if (!$this->request->getConfig('proxy_host')
                || 'http' != $this->request->getConfig('proxy_type')
                || 0 == strcasecmp($url->getScheme(), 'https')
            ) {
                $requestUrl = '';
            } else {
                $requestUrl = $url->getScheme() . '://' . $host;
            }
            $path        = $url->getPath();
            $query       = $url->getQuery();
            $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query);
        }

        if ('1.1' == $this->request->getConfig('protocol_version')
            && extension_loaded('zlib') && !isset($headers['accept-encoding'])
        ) {
            $headers['accept-encoding'] = 'gzip, deflate';
        }
        if (($jar = $this->request->getCookieJar())
            && ($cookies = $jar->getMatching($this->request->getUrl(), true))
        ) {
            $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
        }

        $this->addAuthorizationHeader($headers, $host, $requestUrl);
        $this->addProxyAuthorizationHeader($headers, $requestUrl);
        $this->calculateRequestLength($headers);
        if ('1.1' == $this->request->getConfig('protocol_version')) {
            $this->updateExpectHeader($headers);
        } else {
            $this->expect100Continue = false;
        }

        $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .
                      $this->request->getConfig('protocol_version') . "\r\n";
        foreach ($headers as $name => $value) {
            $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
            $headersStr   .= $canonicalName . ': ' . $value . "\r\n";
        }
        return $headersStr . "\r\n";
    }

    /**
     * Adds or removes 'Expect: 100-continue' header from request headers
     *
     * Also sets the $expect100Continue property. Parsing of existing header
     * is somewhat needed due to its complex structure and due to the
     * requirement in section 8.2.3 of RFC 2616:
     * > A client MUST NOT send an Expect request-header field (section
     * > 14.20) with the "100-continue" expectation if it does not intend
     * > to send a request body.
     *
     * @param array &$headers Array of headers prepared for the request
     *
     * @throws HTTP_Request2_LogicException
     * @link http://pear.php.net/bugs/bug.php?id=19233
     * @link http://tools.ietf.org/html/rfc2616#section-8.2.3
     */
    protected function updateExpectHeader(&$headers)
    {
        $this->expect100Continue = false;
        $expectations = array();
        if (isset($headers['expect'])) {
            if ('' === $headers['expect']) {
                // empty 'Expect' header is technically invalid, so just get rid of it
                unset($headers['expect']);
                return;
            }
            // build regexp to parse the value of existing Expect header
            $expectParam     = ';\s*' . self::REGEXP_TOKEN . '(?:\s*=\s*(?:'
                               . self::REGEXP_TOKEN . '|'
                               . self::REGEXP_QUOTED_STRING . '))?\s*';
            $expectExtension = self::REGEXP_TOKEN . '(?:\s*=\s*(?:'
                               . self::REGEXP_TOKEN . '|'
                               . self::REGEXP_QUOTED_STRING . ')\s*(?:'
                               . $expectParam . ')*)?';
            $expectItem      = '!(100-continue|' . $expectExtension . ')!A';

            $pos    = 0;
            $length = strlen($headers['expect']);

            while ($pos < $length) {
                $pos += strspn($headers['expect'], " \t", $pos);
                if (',' === substr($headers['expect'], $pos, 1)) {
                    $pos++;
                    continue;

                } elseif (!preg_match($expectItem, $headers['expect'], $m, 0, $pos)) {
                    throw new HTTP_Request2_LogicException(
                        "Cannot parse value '{$headers['expect']}' of Expect header",
                        HTTP_Request2_Exception::INVALID_ARGUMENT
                    );

                } else {
                    $pos += strlen($m[0]);
                    if (strcasecmp('100-continue', $m[0])) {
                        $expectations[]  = $m[0];
                    }
                }
            }
        }

        if (1024 < $this->contentLength) {
            $expectations[] = '100-continue';
            $this->expect100Continue = true;
        }

        if (empty($expectations)) {
            unset($headers['expect']);
        } else {
            $headers['expect'] = implode(',', $expectations);
        }
    }

    /**
     * Sends the request body
     *
     * @throws   HTTP_Request2_MessageException
     */
    protected function writeBody()
    {
        if (in_array($this->request->getMethod(), self::$bodyDisallowed)
            || 0 == $this->contentLength
        ) {
            return;
        }

        $position   = 0;
        $bufferSize = $this->request->getConfig('buffer_size');
        $headers    = $this->request->getHeaders();
        $chunked    = isset($headers['transfer-encoding']);
        while ($position < $this->contentLength) {
            if (is_string($this->requestBody)) {
                $str = substr($this->requestBody, $position, $bufferSize);
            } elseif (is_resource($this->requestBody)) {
                $str = fread($this->requestBody, $bufferSize);
            } else {
                $str = $this->requestBody->read($bufferSize);
            }
            if (!$chunked) {
                $this->socket->write($str);
            } else {
                $this->socket->write(dechex(strlen($str)) . "\r\n{$str}\r\n");
            }
            // Provide the length of written string to the observer, request #7630
            $this->request->setLastEvent('sentBodyPart', strlen($str));
            $position += strlen($str);
        }

        // write zero-length chunk
        if ($chunked) {
            $this->socket->write("0\r\n\r\n");
        }
        $this->request->setLastEvent('sentBody', $this->contentLength);
    }

    /**
     * Reads the remote server's response
     *
     * @return   HTTP_Request2_Response
     * @throws   HTTP_Request2_Exception
     */
    protected function readResponse()
    {
        $bufferSize = $this->request->getConfig('buffer_size');
        // http://tools.ietf.org/html/rfc2616#section-8.2.3
        // ...the client SHOULD NOT wait for an indefinite period before sending the request body
        $timeout    = $this->expect100Continue ? 1 : null;

        do {
            try {
                $response = new HTTP_Request2_Response(
                    $this->socket->readLine($bufferSize, $timeout), true, $this->request->getUrl()
                );
                do {
                    $headerLine = $this->socket->readLine($bufferSize);
                    $response->parseHeaderLine($headerLine);
                } while ('' != $headerLine);

            } catch (HTTP_Request2_MessageException $e) {
                if (HTTP_Request2_Exception::TIMEOUT === $e->getCode()
                    && $this->expect100Continue
                ) {
                    return null;
                }
                throw $e;
            }
            if ($this->expect100Continue && 100 == $response->getStatus()) {
                return $response;
            }
        } while (in_array($response->getStatus(), array(100, 101)));

        $this->request->setLastEvent('receivedHeaders', $response);

        // No body possible in such responses
        if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
            || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()
                && 200 <= $response->getStatus() && 300 > $response->getStatus())
            || in_array($response->getStatus(), array(204, 304))
        ) {
            return $response;
        }

        $chunked = 'chunked' == $response->getHeader('transfer-encoding');
        $length  = $response->getHeader('content-length');
        $hasBody = false;
        if ($chunked || null === $length || 0 < intval($length)) {
            // RFC 2616, section 4.4:
            // 3. ... If a message is received with both a
            // Transfer-Encoding header field and a Content-Length header field,
            // the latter MUST be ignored.
            $toRead = ($chunked || null === $length)? null: $length;
            $this->chunkLength = 0;

            while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) {
                if ($chunked) {
                    $data = $this->readChunked($bufferSize);
                } elseif (is_null($toRead)) {
                    $data = $this->socket->read($bufferSize);
                } else {
                    $data    = $this->socket->read(min($toRead, $bufferSize));
                    $toRead -= strlen($data);
                }
                if ('' == $data && (!$this->chunkLength || $this->socket->eof())) {
                    break;
                }

                $hasBody = true;
                if ($this->request->getConfig('store_body')) {
                    $response->appendBody($data);
                }
                if (!in_array($response->getHeader('content-encoding'), array('identity', null))) {
                    $this->request->setLastEvent('receivedEncodedBodyPart', $data);
                } else {
                    $this->request->setLastEvent('receivedBodyPart', $data);
                }
            }
        }

        if ($hasBody) {
            $this->request->setLastEvent('receivedBody', $response);
        }
        return $response;
    }

    /**
     * Reads a part of response body encoded with chunked Transfer-Encoding
     *
     * @param int $bufferSize buffer size to use for reading
     *
     * @return   string
     * @throws   HTTP_Request2_MessageException
     */
    protected function readChunked($bufferSize)
    {
        // at start of the next chunk?
        if (0 == $this->chunkLength) {
            $line = $this->socket->readLine($bufferSize);
            if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
                throw new HTTP_Request2_MessageException(
                    "Cannot decode chunked response, invalid chunk length '{$line}'",
                    HTTP_Request2_Exception::DECODE_ERROR
                );
            } else {
                $this->chunkLength = hexdec($matches[1]);
                // Chunk with zero length indicates the end
                if (0 == $this->chunkLength) {
                    $this->socket->readLine($bufferSize);
                    return '';
                }
            }
        }
        $data = $this->socket->read(min($this->chunkLength, $bufferSize));
        $this->chunkLength -= strlen($data);
        if (0 == $this->chunkLength) {
            $this->socket->readLine($bufferSize); // Trailing CRLF
        }
        return $data;
    }
}

?>


<?php
namespace hathoora\grid;

use hathoora\container,
    hathoora\database\dbAdapter;

class grid extends container
{
   /**
     * Render grid given an array of data\table
     *
     * @param array $arrGridData
     * @param bool $return or echo
    * @return string
    */
    public static function renderGrid(&$arrGridData, $return = false)
    {
        $table_id = isset($arrGridData['table']['id']) ? $arrGridData['table']['id'] : 'htgrid_'. time() . rand(1,99999);
        $table_class = isset($arrGridData['table']['class']) ? $arrGridData['table']['class'] : null;
        $grid = $noResultsTableClass = $hasNoResults = null;

        $hathooraGridJS = '/_assets/_hathoora/hathooraGrid.js';
        if (!empty($arrGridData['javascript']['gridPluginURL']))
            $hathooraGridJS = $arrGridData['javascript']['gridPluginURL'];

        $gridHeader = $gridPager = $gridTable = $gridPager = null;
        $htmlAppend = isset($arrGridData['html']['append']) ? $arrGridData['html']['append'] : null;
        $table_id = $arrGridData['table']['id'];
        $onGridReadyCallBack = !empty($arrGridData['javascript']['onGridReadyCallBack']) ? $arrGridData['javascript']['onGridReadyCallBack'] : 'null';
        $ajaxCalls = isset($arrGridData['javascript']['ajax']) ? $arrGridData['javascript']['ajax'] : true;

        // pagination?
        if (empty($arrGridData['table']['pagination']))
            $arrGridData['table']['pagination'] = array('\hathoora\grid\pagination', 'simple');
        else if (is_string($arrGridData['table']['pagination']))
            $arrGridData['table']['pagination'] = array('\hathoora\grid\pagination', $arrGridData['table']['pagination']);

        if (isset($arrGridData['table']['template']))
            require($arrGridData['table']['template']);
        // load default template
        else
            require(__DIR__ .'/template.php');

        $gridPager = trim($gridPager);

        if (!empty($arrGridData['debug']))
            $htmlAppend .= '

' . $arrGridData['queryTotal'] . '

' . $arrGridData['queryRow'] . '

'. print_r($arrGridData, true) .'
';

        // when not ajax call
        if (empty($arrGridData['pajax']))
        {
            $grid = $gridHeader .'

';

            if ($ajaxCalls)
            {
                $grid .= '
                 ';
            }

            $grid .=
                (!empty($arrGridData['table']['options']['topPager']) ? '
'. $gridPager .'
' : null ) .
                $gridTable .
                (!$hasNoResults && (isset($arrGridData['table']['options']['bottomPager']) && $arrGridData['table']['options']['bottomPager'] !== false && $gridPager) ? '
 '. $gridPager .'
' : null) .
                $htmlAppend .'

';
        }
        // is ajx call
        else
        {
            if ($ajaxCalls)
            {
                $grid = '
                ';
            }

            $grid .=
            ( !empty($arrGridData['table']['options']['topPager']) ? '
'. $gridPager .'
' : null ) .
            $gridTable .
            (!$hasNoResults && (isset($arrGridData['table']['options']['bottomPager']) && $arrGridData['table']['options']['bottomPager'] !== false && $gridPager) ? '
 '. $gridPager .'
' : null) .
            $htmlAppend;
        }

        return $grid;
    }

    /**
     * Run a sql statement
     *
     * @param array $arrParams that have
     * $arrParams that have:
     *  dsn: which dsn to use
     *  sort: the field to sort on
     *  order: the order asc|desc
     *  start: the start of limit
     *
     * @todo complete this
     * @param bool $render
     * @return array|mixed
     */
    public static function sqlRun($arrParams, $render = false)
    {
        $dsn = !empty($arrParams['dsn']) ? $arrParams['dsn'] : self::getDSN();

        // debug
        $queryTotalDebug = !empty($arrParams['queryTotalDebug']) ? $arrParams['queryTotalDebug'] : false;
        $queryRowDebug = !empty($arrParams['queryRowDebug']) ? $arrParams['queryRowDebug'] : false;
        // stores results
        $arrGridData = array('total' => null, 'rows' => array());
        // query query
        $queryTotal = !empty($arrParams['queryTotal']) ? $arrParams['queryTotal'] : null;
        $skipTotal = !empty($arrParams['skipTotal']) ? $arrParams['skipTotal'] : false;
        $totalQueryHasResults = false;
        // rows query and related variables
        $queryRow =  !empty($arrParams['queryRow']) ? $arrParams['queryRow'] : null;
        $sort = !empty($arrParams['sort']) ? $arrParams['sort'] : null;
        $order = !empty($arrParams['order']) ? $arrParams['order'] : null;
        $start = !empty($arrParams['start']) ? $arrParams['start'] : 0;
        $page = !empty($arrParams['page']) ? (int) $arrParams['page'] : 1;
        $limit = !empty($arrParams['limit']) ? (int) $arrParams['limit'] : 100;
        $orderBy = $limitClause = null;
        // don't run any sql and go with the flow
        $noSQL = isset($arrParams['noSQL']) ? $arrParams['noSQL'] : false;

        // set them again so we don't get undefined variable notices
        $arrParams['sort'] = $sort;
        $arrParams['order'] = $order;
        $arrParams['start'] = $start;
        $arrParams['limit'] = $limit;

        // queryRowOuterSelect wraps row query which helps in sorting derived fields
        $queryRowOuterSelect = !empty($arrParams['queryRowOuterSelect']) ? $arrParams['queryRowOuterSelect'] : false;

        // an array containing function to call for every row in result to which we pass $row as referrence
        // rowFunctions must be an array of arrays that can be input to call_user_func_array()
        $rowFunctions = !empty($arrParams['rowFunctions']) ? $arrParams['rowFunctions'] : null;

        // field to use as index when creating results set
        $primaryKey = !empty($arrParams['primaryKey']) ? $arrParams['primaryKey'] : null;

        // similiar to rowFunctions an array of functions to call at the end of row loops
        // to which we pass the entire $arrGridData['rows'] as referrence
        $dataFunctions = !empty($arrParams['dataFunctions']) ? $arrParams['dataFunctions'] : null;

        // sort
        if ($sort && $order)
        {
            $orderBy = $dsn->escape($sort) . ' ' . $dsn->escape($order);
            $orderBy = 'ORDER BY '. $orderBy;
        }

        // limit
        if (!$start)
        {
            $start = ($page - 1) * $limit;
            if ($start < 1) $start = 0;
        }
        if ($limit)
            $limitClause = 'LIMIT '. $dsn->escape($start) .', '. $dsn->escape($limit);

        // get total results
        if ($noSQL != true && $skipTotal == false && $queryTotal)
        {
            if ($queryTotalDebug)
                echo 'Total Query:
' . nl2br($queryTotal);
            try
            {
                $stmt = $dsn->query($queryTotal);
                if ($stmt && $stmt->rowCount())
                {
                    $totalQueryHasResults = true;
                    $row = $stmt->fetchArray();
                    $arrGridData['total'] = array_pop($row);
                }
            }
            catch (\Exception $e)
            {
                // fail silently
            }
        }

        // get the rows
        if ($noSQL != true && $queryRow &&
           ($skipTotal || (!$skipTotal && $totalQueryHasResults)))
        {
            // outer select?
            if ($queryRowOuterSelect)
            {
                // we need to remove a.field from the order, it will messup
                $orderBy = preg_replace('/ORDER BY (.+?)\./i', 'ORDER BY ', $orderBy);

                $query = 'SELECT o.*  FROM ( '. $queryRow .') o '. $orderBy .' '. $limitClause;
            }
            else
                $query = $queryRow . ' '. $orderBy .' '. $limitClause;

            if ($queryRowDebug)
                echo 'Row Query:
' . nl2br($query);

            try
            {
                $stmt = $dsn->query($query);
                if ($stmt && $stmt->rowCount())
                {
                    while ($row = $stmt->fetchArray())
                    {
                        // any row functions
                        if (is_array($rowFunctions))
                        {
                            foreach ($rowFunctions as $rowFunction)
                            {
                                if (is_callable($rowFunction))
                                    call_user_func_array($rowFunction, array(&$row));
                            }
                        }

                        if ($primaryKey && isset($row[$primaryKey]))
                            $arrGridData['rows'][$row[$primaryKey]] = $row;
                        else
                            $arrGridData['rows'][] = $row;
                    }

                    // any data functions
                    if (is_array($dataFunctions))
                    {
                        foreach($dataFunctions as $dataFunction)
                        {
                            $arrDataFunctionExtraParams = null;
                            if (count($dataFunction) > 2)
                                $arrDataFunctionExtraParams = array_pop($dataFunction);
                            if (is_callable($dataFunction))
                            {
                                $args = array();
                                $args[0] =& $arrGridData['rows'];
                                if (is_array($arrDataFunctionExtraParams))
                                    $args = array_merge($args, $arrDataFunctionExtraParams);

                                call_user_func_array($dataFunction, $args);
                            }
                        }
                    }
                }
            }
            catch (\Exception $e)
            {
                // fail silently
            }
        }

        if (!$render)
        {
            // skip totals?
            if ($skipTotal && count($arrGridData))
            {
                unset($arrGridData['total']);
                $arrGridData = $arrGridData['rows'];
            }
            return $arrGridData;
        }
        else
        {
            // merge arrGridData into arrParams
            $arrParams['data']['total'] =& $arrGridData['total'];
            $arrParams['data']['rows'] =& $arrGridData['rows'];
            return self::renderGrid($arrParams, true);
        }
    }

    /**
     * Builds sql select
     */
    public static function sqlBuildSelect($arrSelect, $addSelect = false)
    {
        $select = null;

        if (is_array($arrSelect))
        {
            foreach ($arrSelect as $f => $v)
            {
                if ($select)  $select .= ', ';
                if (preg_match('/^(.+?):(literal)$/i', $f, $arrMatch))
                    $select .= $v;
                else
                    $select .= self::getDSN()->escape($v);
            }
        }

        if ($addSelect && $select)
            $addSelect = ' SELECT ' . $addSelect;

        return $select;
    }

    /**
     * Builds sql where criteria
     *
     * @param array $arrWhere
     * @param bool $whereClause when true we will add where clause
     */
    public static function sqlBuildWhere($arrWhere, $whereClause = true)
    {
        $where = null;
        if (is_array($arrWhere))
        {
            if ($whereClause)
                $where = ' WHERE 1 ';
            foreach ($arrWhere as $f => $v)
            {
                if ($where)
                    $where .= ' AND ';

                $field = $f;

                // special suff?
                if (preg_match('/^(.+?):(literal|int|string)$/i', $f, $arrMatch))
                {
                    $type = array_pop($arrMatch);
                    $f =  self::getDSN()->escape(array_pop($arrMatch));

                    // literal - becareful with these!!!!
                    if ($type == 'literal')
                    {
                        // make sure this literal was not passed in the URL (GET!!)
                        if (!empty($_GET['where'][$field]))
                            unset($_GET['where'][$field]);
                        else if (!empty($_POST['where'][$field]))
                            unset($_POST['where'][$field]);
                        else
                        {
                            $where .= $v;
                        }
                    }
                    else if ($type == 'int')
                        $where .= $f .' IN ('. self::getDSN()->escape($v) .') ';
                    else if ($type == 'string')
                        $where .= $f . self::sqlWhereSmartLike($v);

                    else
                        $where .= $f .' = "'. self::getDSN()->escape($v) .'" ';
                }
                else
                    $where .= $f .' = "' . self::getDSN()->escape($v) . '" ';
            }
        }

        return $where;
    }

    /**
     * Replaces \* with %
     * Would return ' LIKE "%when $v has wildcard" '
     * Else would return ' = "$v"
     */
    public static function sqlWhereSmartLike($v)
    {
        $match = null;

        $v = trim($v);
        if (preg_match('/\*/', $v))
            $match = ' LIKE "'. self::getDSN()->escape(str_replace('*', '%', $v)) . '" ';
        else
            $match = ' = "'. self::getDSN()->escape($v) . '" ';

        return $match;
    }

    /**
     * Builds sql join criters
     *
     * @param array $arrJoin
     *  ex: array('users' => 'INNER JOIN users u ON (u.user_id = f.user_id))
     */
    public static function sqlBuildJoin($arrJoin)
    {
        $joins = null;
        if (is_array($arrJoin))
        {
            foreach ($arrJoin as $v)
            {
                $joins .= $v . " \n";
            }
            $joins = " \n" . $joins;
        }

        return  $joins;
    }

    /**
     * Builds group by statement..
     *
     * @param array $arrGroup
     *  ex:
     *      array('user_id' => 'user_id'),
     *      array('user_id' => 'user_id', 'date' => 'date'),
     * @param bool $addGroup syntax
     */
    public static function sqlBuildGroupBy($arrGroup, $addGroup = false)
    {
        $groupBy = $group = null;

        if (is_array($arrGroup))
        {
            $group = '';
            foreach ($arrGroup as $k)
            {
                if ($group)  $group .= ', ';
                $group .= self::getDSN()->escape($k);
            }
        }

        if ($addGroup && $group)
            $groupBy = ' GROUP BY ' . $group;

        return $groupBy;
    }

    /**
     * This function prepare the grid array by setting limit, sorting, selected fields, row criteria etc..
     * This function also makes sure that a user cannot user a property (sort, search, select etc..) when it is
     * not specified in the $arrGridPrepare
     *
     * Once the data has been processed it is then ready to be handed off to sqlRun() function
     *
     * @param array $arrGridPrepare
     * @param array $arrFormData (which is usually via GET or POST)
     */
    public static function prepare(&$arrGridPrepare, $arrFormData)
    {
        #0 add meta info
        $arrGridPrepare['meta'] = array();

        #1 assign a unqiue id to every table, this unique id is used when making ajax calls
        if (!empty($arrGridPrepare['table']['id']))
            $table_id = $arrGridPrepare['table']['id'];
        else if (!empty($arrFormData['hathooraGrid_id']))
            $table_id = $arrGridPrepare['table']['id'];
        else
        {
            $table_id = 'hathooraGrid_' . rand(9,99999999);
            $arrGridPrepare['table']['id'] = $table_id;
        }

        #2a figure out the limit (for SQL)
        if (!empty($arrGridPrepare['table']['limit']['fixed']))
            $arrGridPrepare['limit'] = $arrGridPrepare['table']['limit']['fixed'];
        else
        {
            // limit
            if (!empty($arrFormData['limit']))
                $arrGridPrepare['limit'] = (int) $arrFormData['limit'];

            // default limit
            else if (!empty($arrGridPrepare['table']['limit']['default']) && empty($arrFormData['limit']))
                $arrGridPrepare['limit'] = $arrGridPrepare['table']['limit']['default'];
        }

        // max limit
        if (!empty($arrGridPrepare['table']['limit']['max']) && !empty($arrGridPrepare['limit']) && $arrGridPrepare['limit'] > $arrGridPrepare['table']['limit']['max'])
            $arrGridPrepare['limit'] = $arrGridPrepare['table']['limit']['max'];

        // cap limit when not able to assign a limit
        if (empty($arrGridPrepare['limit']))
            $arrGridPrepare['limit'] = 100;

        #2b figure out page (for SQL & pagination)
        if (empty($arrGridPrepare['page']))
        {
            $page = !empty($arrFormData['page']) ? (int) $arrFormData['page'] : 1;
            $arrGridPrepare['page'] = $page;
        }

        #2c figure out page (for SQL & pagination)
        if (empty($arrGridPrepare['start']))
        {
            $start = ($arrGridPrepare['page'] - 1) * $arrGridPrepare['limit'];
            if ($start < 1) $start = 0;
            $arrGridPrepare['start'] = $start;
        }

        #3 figure out the columns we need to display in table
        $tableFieldsAvailable = isset($arrGridPrepare['table']['fields']['available']) ? $arrGridPrepare['table']['fields']['available'] : null;
        if (is_array($tableFieldsAvailable))
        {
            // @todo check file before loading..
            #sqlLoadClassTest(true);
            $callable = is_callable($tableFieldsAvailable);
            #sqlLoadClassTest(false);

            if ($callable)
            {
                #3a figure out which columns are actually available for selection
                $arrFieldsAvailable = call_user_func_array($tableFieldsAvailable, array());

                // any jail?
                $arrFieldsPossibleJail = isset($arrGridPrepare['table']['fields']['jail']) ? $arrGridPrepare['table']['fields']['jail'] : null;
                $arrFieldsJail = array(); // fill it with confirm jailed fields
                if (is_array($arrFieldsAvailable) && is_array($arrFieldsPossibleJail))
                {
                    foreach ($arrFieldsPossibleJail as $field)
                    {
                        if (isset($arrFieldsAvailable[$field]))
                            $arrFieldsJail[$field] =& $arrFieldsAvailable[$field];
                    }
                }

                // these are the fields that are available for selection
                if  (count($arrFieldsJail))
                    $arrFields =& $arrFieldsJail;
                else
                    $arrFields =& $arrFieldsAvailable;

                #3b prepare the column array which is used by renderTable to draw table
                if (is_array($arrFields))
                {
                    $arrColumnsPossibleType = 'indexed';
                    // dynamic columns that are passed by the ajax call?
                    if (!empty($arrGridPrepare['table']['fields']['dynamic']) && !empty($arrFormData['columns']) && is_array($arrFormData['columns']))
                    {
                        $arrColumnsPossible = $arrFormData['columns'];
                        $arrColumnsPossibleType = 'associative';
                    }
                    // default columns specified
                    else if (!empty($arrGridPrepare['table']['fields']['default']) && count($arrGridPrepare['table']['fields']['default']))
                        $arrColumnsPossible = $arrGridPrepare['table']['fields']['default'];

                    // if still not able to figure out possible columns then use $arrFields
                    if (empty($arrColumnsPossible) || !count($arrColumnsPossible))
                    {
                        $arrColumnsPossible = $arrFields;
                        $arrColumnsPossibleType = 'associative';
                    }

                    #3c now figure out among the possible columns which ones are actually defined and can be used
                    if (is_array($arrColumnsPossible))
                    {
                        foreach($arrColumnsPossible as $field => $vfield)
                        {
                            // be careful because can have associative or indexed array based on how it was prepared
                            if ($arrColumnsPossibleType == 'indexed')
                                $field = $vfield;

                            if (isset($arrFields[$field]) && is_array($arrFields[$field]))
                            {
                                $arrField =& $arrFields[$field];

                                // name of the field?
                                if (!empty($arrField['name']))
                                {
                                    $cleanName = $arrField['name'];

                                    # @todo only when CSV is output..
                                    #if ($arrGridPrepare['table']['output'] != 'html')
                                    #    $cleanName = self::cleanString($arrField['name']);

                                    if ($cleanName)
                                        $arrField['name'] = $cleanName;
                                }

                                if (empty($arrField['name']))
                                    $arrField['name'] = $field;

                                if (!array_key_exists('canDel', $arrField))
                                    $arrField['canDel'] = true;

                                // set the default value of dbField
                                if (!array_key_exists('dbField', $arrField))
                                    $arrField['dbField'] = $field;

                                // any dependencies?
                                if (isset($arrField['dependency']) && is_array($arrField['dependency']))
                                    self::prepareDependencyHandler('column', $field, $arrField, $arrGridPrepare, $arrFormData);
                                if (!empty($arrField['dbField']))
                                    $arrGridPrepare['selectField'][$arrField['dbField']] = $arrField['dbField'];

                                $arrGridPrepare['table']['columns'][$field] =& $arrField;
                                $arrGridPrepare['table']['columnsJS'][$field] = array(
                                                                                'field' => $field,
                                                                                'name' => isset($arrField['name']) ? $arrField['name'] : $field,
                                                                                'sort' => isset($arrField['sort']) ? $arrField['sort'] : 0,
                                                                                );
                            }
                        }
                    }
                }
            }
        }

        #4 sort & order
        if (!empty($arrFormData['sort']))
        {
            // is the field even marked sortable?
            if (!empty($arrFields[$arrFormData['sort']]['sort']))
            {
                // field has dbField?
                if (!empty($arrFields[$arrFormData['sort']]['dbField']))
                    $arrGridPrepare['sort'] = self::removeNand($arrFields[$arrFormData['sort']]['dbField']);
                else
                    $arrGridPrepare['sort'] = self::removeNand($arrFormData['sort']);

                if (!empty($arrFormData['order']))
                    $arrGridPrepare['order'] = $arrFormData['order'];
            }
        }

        // default sort & order
        if (empty($arrGridPrepare['sort']) && !empty($arrGridPrepare['table']['sort']['default']))
            $arrGridPrepare['sort'] = $arrGridPrepare['table']['sort']['default'];

        if (empty($arrGridPrepare['order']) && !empty($arrGridPrepare['table']['order']['default']))
            $arrGridPrepare['order'] = $arrGridPrepare['table']['order']['default'];

        if (empty($arrGridPrepare['sort']) && !empty($arrGridPrepare['order']))
            $arrGridPrepare['order'] = 'desc';

        #5 output
        if (empty($arrGridPrepare['table']['output']) && !empty($arrFormData['output']))
            $arrGridPrepare['table']['output'] = $arrFormData['output'];
        if (empty($arrGridPrepare['table']['output']))
            $arrGridPrepare['table']['output'] = 'html';

        #6a search fields
        // fields a user can search on
        $arrSearchableFields = array();

        // get the columns that are searchable, first from the ['table']['column']['fields']
        if (!empty($arrFields) && is_array($arrFields))
        {
            foreach ($arrFields as $_field => $_arrField)
            {
                if (!empty($_arrField['search']) && is_array($_arrField['search']))
                {
                    if (!empty($_arrField['search']['type']))
                    {
                        if (!empty($_arrField['search']['operations']))
                        {
                            $_operations = explode(',', $_arrField['search']['operations']);
                            $_searchArr = array();
                            foreach($_operations as $_operation)
                            {
                                $_operation = trim($_operation);
                                $_searchArr[$_operation] = $_operation;
                            }

                            $arrSearchableFields[$_field] = $_searchArr;
                            $arrGridPrepare['table']['columnsJS'][$field]['search'] = $_searchArr;
                        }
                    }
                }
            }
        }

        #6 where criteria
        $arrWherePossible = null;
        if (!empty($arrFormData['where']) && is_array($arrFormData['where']))
            $arrWherePossible =& $arrFormData['where'];
        $arrWhere = array(); // fields that are searched

        if (is_array($arrWherePossible) && count($arrSearchableFields))
        {
            // loop over where criteria and make sure the fields are searchable so we can prepare where criteria
            foreach($arrWherePossible as $where => $value)
            {
                if (preg_match('/[a-zA-z0-9_\.]+(|:)/i', $where, $arrMatch))
                {
                    $field = $arrMatch[0];

                    // is field even searchable?
                    if (!empty($arrSearchableFields[$field]) && is_array($arrSearchableFields[$field]))
                    {
                        $arrField = $arrFields[$field];
                        if (!empty($arrField['dependency']) && is_array($arrField['dependency']))
                            self::prepareDependencyHandler('where', $field, $arrField, $arrGridPrepare, $arrFormData);

                        $arrWhere[$where] = $value;
                    }
                }
            }
        }

        // make rowWhere the same as totalWhere
        if (count($arrWhere))
            $arrGridPrepare['whereTotal'] = $arrGridPrepare['whereRow'] = $arrWhere;

        // ajax URL
        if (empty($arrGridPrepare['table']['sort']['url']))
            $arrGridPrepare['table']['sort']['url'] = $_SERVER['REQUEST_URI'];

        // cleanup duplicate for duplicate params
        // @http://stackoverflow.com/questions/2613063/remove-duplicate-from-string-in-php
        if ($arrGridPrepare['table']['sort']['url'])
        {
            $url = parse_url($arrGridPrepare['table']['sort']['url'], PHP_URL_PATH);
            $params = parse_url($arrGridPrepare['table']['sort']['url'], PHP_URL_QUERY);
            if ($params)
            {
                parse_str($params, $paramsArr);
                $params = http_build_query($paramsArr);
            }

            $arrGridPrepare['table']['sort']['url'] = $url . ($params ? '?'. $params : null);
        }

        // ajax requesy?
        if (!array_key_exists('pajax', $arrGridPrepare) && container::getRequest()->isAjax())
            $arrGridPrepare['pajax'] = true;
    }

    /**
     * Handles dependecies when certain fields are selected or searched
     *
     * @param string $handleType column, where
     * @param string $field current field
     * @param array $arrField details about field
     * @param array $arrGridPrepare
     * @param array $arrFormData the form|GET|POST
     */
    public static function prepareDependencyHandler($handleType, $field, &$arrField, &$arrGridPrepare, &$arrFormData)
    {
        // select fields?
        if (isset($arrField['dependency']['selectField']) && is_array($arrField['dependency']['selectField']))
        {
            foreach($arrField['dependency']['selectField'] as $f => $v)
            {
                $arrGridPrepare['selectField'][$f] = $v;
            }
        }

        // total joins
        if (isset($arrField['dependency']['joinTotal']) && is_array($arrField['dependency']['joinTotal']))
        {
            foreach($arrField['dependency']['joinTotal'] as $f => $v)
            {
                $arrGridPrepare['joinTotal'][$f] = $v;
            }
        }

        // row joins
        if (isset($arrField['dependency']['joinRow']) && is_array($arrField['dependency']['joinRow']))
        {
            foreach($arrField['dependency']['joinRow'] as $f => $v)
            {
                $arrGridPrepare['joinRow'][$f] = $v;
            }
        }

        // rowFunctions
        if (isset($arrField['dependency']['rowFunctions']) && is_array($arrField['dependency']['rowFunctions']))
        {
            foreach ($arrField['dependency']['rowFunctions'] as $f => $v)
            {
                $arrGridPrepare['rowFunctions'][$f] = $v;
            }
        }

        // total groups
        if (isset($arrField['dependency']['groupTotal']) && is_array($arrField['dependency']['groupTotal']))
        {
            foreach ($arrField['dependency']['groupTotal'] as $f => $v)
            {
                $arrGridPrepare['groupTotal'][$f] = $v;
            }
        }

        // row groups
        if (isset($arrField['dependency']['groupRow']) && is_array($arrField['dependency']['groupRow']))
        {
            foreach ($arrField['dependency']['groupRow'] as $f => $v)
            {
                $arrGridPrepare['groupRow'][$f] = $v;
            }
        }

        // dataFunctions
        if (isset($arrField['dependency']['dataFunctions']) && is_array($arrField['dependency']['dataFunctions']))
        {
            foreach ($arrField['dependency']['dataFunctions'] as $f => $v)
            {
                $arrGridPrepare['dataFunctions'][$f] = $v;
            }
        }
    }

    /**
     * Helper function that parse $data['rows'] and returns pk ids or other mapping ids
     *
     * @param array $arrRows
     * @param string $mapField $arrRows[$mapField] should have thie
     * @param string $dontMapWhenFieldExists we will skip all rows that have $arrRows[$dontMapWhenFieldExists]
     * @param object $db to escape against
     *
     * @return array containing
     *      arrMapping: mapping of keys from $arrRows and $mapField from $arrRows
     *      arrIds: if mapField == null then its keys of $arrRows, else it is the mapping b/w $mapFieldID and key
     *      strIds: is the db escaped, comma seperated, double quoted string ready to be used for sql
     */
    public static function getDataFunctionIds(&$arrRows, $mapField = null, $dontMapWhenFieldExists = null, $db = null)
    {
        $arrMapping = array();
        $arrIds = array();
        $strIds = null;

        if (is_array($arrRows) && ($db = self::getDSN()))
        {
            foreach($arrRows as $id => $arrRow)
            {
                if ($mapField)
                {
                    if (isset($arrRow[$mapField]))
                    {
                        if (isset($dontMapWhenFieldExists) && isset($arrRow[$dontMapWhenFieldExists]))
                            continue;

                        $map_id = $arrRow[$mapField];
                        $arrMapping[$map_id][$id] = $id;
                    }
                }
                else
                    $map_id = $id;

                if (isset($map_id) && !isset($arrIds[$map_id]))
                {
                    if ($strIds) $strIds .= ', ';
                    $strIds .= "'". $db->escape($map_id) ."'";
                    $arrIds[$map_id] = $map_id;
                }
            }
        }

        return array(
                        'arrMapping' => $arrMapping,
                        'arrIds' => $arrIds,
                        'strIds' => $strIds
                    );
    }

    /**
     * Helper function that merge mapping ids to arrRows
     *
     * @param array $arrRows
     * @param array $arrResults sql results from $arrIds/$strIds usually. The key of this array is the $mapField from getDataFunctionIds()
     * @param array $arrMapping from getDataFunctionIds()
     */
    public static function getDataFunctionMergeMapping(&$arrRows, &$arrResults, &$arrMapping, $arrRowsAdditonalKey = null)
    {
        if (is_array($arrResults))
        {
            foreach($arrResults as $mapId => $arrResult)
            {
                if (isset($arrMapping[$mapId]) && is_array($arrMapping[$mapId]))
                {
                    foreach($arrMapping[$mapId] as $rowId)
                    {
                        if (isset($arrRows[$rowId]))
                        {
                            if (isset($arrRowsAdditonalKey))
                                $arrRows[$rowId][$arrRowsAdditonalKey] = $arrResult;
                            else
                                $arrRows[$rowId] = array_merge($arrResult, $arrRows[$rowId]);
                        }
                    }
                }
            }
        }
    }

    /**
     * used to remove HTML|nonsense chars from THEAD TH of render table for csv output
     */
    public static function cleanString($string)
    {
        $string = str_replace(array(' ', '
', '
'), ' ', $string);
        $string = strip_tags($string);
        $string = str_replace(' ', ' ', $string);

        return $string;
    }

    /**
     * Toggles between two values
     *
     * @param mixed $v current value
     * @param mixed $a option a
     * @param mixed $b option b
     */
    public static function toggleValue($v, $a, $b)
    {
        if ($v == $a) return $b;
        return $a;
    }

    /**
     * Returns the value of token
     *
     * @param string $token that we want to get the value for ex: {{name}}
     * @param array $arrTokens that has a key of token ex: 'name' = 'xyz'
     * @return string
     */
    public static function deTokenize($token, &$arrTokens)
    {
        return \hathoora\helper\stringHelper::deTokenize($token, $arrTokens);
    }

    /**
     * Remove non alpha, non numeric and non dots from a string
     */
    public static function removeNand($input)
    {
        return preg_replace('/[^a-zA-Z0-9-_,\.\s]/', '', $input);
    }

    /**
     * Helper function for getting db connection
     *
     * @param string $dsn_name defined in the config
     * @param bool $reBuild when true
     * @return hathoora\database\db class
     */
    public static function getDSN($dsn_name = 'default', $reBuild = false)
    {
        return dbAdapter::getConnection($dsn_name, $reBuild);
    }
}


<?php

namespace Pagination\Util;

use Silex\Application;

class Paginator
{
    /**
     * @var Application
     */
    private $app;

    /**
     * @var array
     */
    private $options;

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

    /**
     * @param \Doctrine\ORM\QueryBuilder|array $query
     * @param array                      $options
     *
     * @throws \Pagination\Exception\UnknownAdapterException
     *
     * @return array
     */
    public function pagination($query, $options = [])
    {
        $this->options = array_replace($this->app['paginator.options'], $options);

        switch (true) {
            case $query instanceof \Doctrine\ORM\QueryBuilder:
                $adapter = new \Pagination\Adapter\PaginationORMAdapter();
                break;
            case is_array($query):
                $adapter = new \Pagination\Adapter\PaginationArrayAdapter();
                break;
            default:
                throw new \Pagination\Exception\UnknownAdapterException();
        }

        $current = 1;
        if ($this->app['request_stack']->getCurrentRequest()) {
            $current = $this->app['request_stack']->getCurrentRequest()->get('page', 1);
        }
        $counter = $adapter->getCounter($query);

        $items = ceil($counter / $this->options['items_per_page']);

        return [
            'first'      => 1,
            'prev'       => $current > 1 ? $current - 1 : null,
            'current'    => $current,
            'items'      => $adapter->getItems($query, $current, $this->options['items_per_page']),
            'pages'      => $this->getPages($current, $items),
            'next'       => $current < $items ? $current + 1 : null,
            'last'       => $items,
            'total'      => $counter,
            'total_page' => $items,
            'options'    => $this->options,
        ];
    }

    /**
     * @param int $current
     * @param int $items
     *
     * @return array
     */
    public function getPages($current, $items)
    {
        $start = $current - $this->options['offset_page'] > 1 ? $current - $this->options['offset_page'] : 1;
        $end = $current + $this->options['offset_page'] < $items ? $current + $this->options['offset_page'] : $items;

        $pages = [];
        for ($i = $start; $i <= $end; $i++) {
            $pages[] = $i;
        }
        if ($start > 1) {
            $end_min = 1 + $this->options['offset_page'] >= $start ? $start - 1 : 1 + $this->options['offset_page'];
            if ($start - $end_min > 1) {
                array_unshift($pages, null);
            }
            for ($i = $end_min; $i >= 1; $i--) {
                array_unshift($pages, $i);
            }
        }
        if ($items - $end >= 1) {
            if ($items - $this->options['offset_page'] > $end + 1) {
                $pages[] = null;
            }
            $end_max = $items - $this->options['offset_page'] <= $end ? $end + 1 : $items - $this->options['offset_page'];
            for ($i = $end_max; $i <= $items; $i++) {
                $pages[] = $i;
            }
        }

        return $pages;
    }
}


<?php

namespace Think\Model;

use Think\Model;

/**
 * MongoModel模型类
 * 实现了ODM和ActiveRecords模式
 */
class MongoModel extends Model
{
    // 主键类型
    const TYPE_OBJECT = 1;
    const TYPE_INT = 2;
    const TYPE_STRING = 3;

    // 主键名称
    protected $pk = '_id';
    // _id 类型 1 Object 采用MongoId对象 2 Int 整形 支持自动增长 3 String 字符串Hash
    protected $_idType = self::TYPE_OBJECT;
    // 主键是否自增
    protected $_autoinc = true;
    // Mongo默认关闭字段检测 可以动态追加字段
    protected $autoCheckFields = false;
    // 链操作方法列表
    protected $methods = ['table', 'order', 'auto', 'filter', 'validate'];

    /**
     * 利用__call方法实现一些特殊的Model方法

     * @param string $method 方法名称
     * @param array $args 调用参数
     * @return mixed
     */
    public function __call($method, $args)
    {
        if (in_array(strtolower($method), $this->methods, true)) {
            // 连贯操作的实现
            $this->options[strtolower($method)] = $args[0];

            return $this;
        } elseif (strtolower(substr($method, 0, 5)) == 'getby') {
            // 根据某个字段获取记录
            $field = parse_name(substr($method, 5));
            $where[$field] = $args[0];

            return $this->where($where)->find();
        } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
            // 根据某个字段获取记录的某个值
            $name = parse_name(substr($method, 10));
            $where[$name] = $args[0];

            return $this->where($where)->getField($args[1]);
        } else {
            E(__CLASS__.':'.$method.L('_METHOD_NOT_EXIST_'));

            return;
        }
    }

    /**
     * 获取字段信息并缓存 主键和自增信息直接配置

     * @return void
     */
    public function flush()
    {
        // 缓存不存在则查询数据表信息
        $fields = $this->db->getFields();
        if (!$fields) { // 暂时没有数据无法获取字段信息 下次查询
            return false;
        }
        $this->fields = array_keys($fields);
        foreach ($fields as $key => $val) {
            // 记录字段类型
            $type[$key] = $val['type'];
        }
        // 记录字段类型信息
        if (C('DB_FIELDTYPE_CHECK')) {
            $this->fields['_type'] = $type;
        }

        // 2008-3-7 增加缓存开关控制
        if (C('DB_FIELDS_CACHE')) {
            // 永久缓存数据表信息
            $db = $this->dbName ? $this->dbName : C('DB_NAME');
            F('_fields/'.$db.'.'.$this->name, $this->fields);
        }
    }

    // 写入数据前的回调方法 包括新增和更新
    protected function _before_write(&$data)
    {
        $pk = $this->getPk();
        // 根据主键类型处理主键数据
        if (isset($data[$pk]) && $this->_idType == self::TYPE_OBJECT) {
            $data[$pk] = new \MongoId($data[$pk]);
        }
    }

    /**
     * count统计 配合where连贯操作

     * @return integer
     */
    public function count()
    {
        // 分析表达式
        $options = $this->_parseOptions();

        return $this->db->count($options);
    }

    /**
     * 获取唯一值

     * @return array | false
     */
    public function distinct($field, $where = [])
    {
        // 分析表达式
        $this->options = $this->_parseOptions();
        $this->options['where'] = array_merge((array)$this->options['where'], $where);

        $command = [
            "distinct" => $this->options['table'],
            "key"      => $field,
            "query"    => $this->options['where'],
        ];

        $result = $this->command($command);

        return isset($result['values']) ? $result['values'] : false;
    }

    /**
     * 获取下一ID 用于自动增长型

     * @param string $pk 字段名 默认为主键
     * @return mixed
     */
    public function getMongoNextId($pk = '')
    {
        if (empty($pk)) {
            $pk = $this->getPk();
        }

        return $this->db->getMongoNextId($pk);
    }

    /**
     * 新增数据

     * @param mixed $data 数据
     * @param array $options 表达式
     * @param boolean $replace 是否replace
     * @return mixed
     */
    public function add($data = '', $options = [], $replace = false)
    {
        if (empty($data)) {
            // 没有传递数据，获取当前数据对象的值
            if (!empty($this->data)) {
                $data = $this->data;
                // 重置数据
                $this->data = [];
            } else {
                $this->error = L('_DATA_TYPE_INVALID_');

                return false;
            }
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        // 数据处理
        $data = $this->_facade($data);
        if (false === $this->_before_insert($data, $options)) {
            return false;
        }
        // 写入数据到数据库
        $result = $this->db->insert($data, $options, $replace);
        if (false !== $result) {
            $this->_after_insert($data, $options);
            if (isset($data[$this->getPk()])) {
                return $data[$this->getPk()];
            }
        }

        return $result;
    }

    // 插入数据前的回调方法
    protected function _before_insert(&$data, $options)
    {
        // 写入数据到数据库
        if ($this->_autoinc && $this->_idType == self::TYPE_INT) { // 主键自动增长
            $pk = $this->getPk();
            if (!isset($data[$pk])) {
                $data[$pk] = $this->db->getMongoNextId($pk);
            }
        }
    }

    public function clear()
    {
        return $this->db->clear();
    }

    // 查询成功后的回调方法
    protected function _after_select(&$resultSet, $options)
    {
        array_walk($resultSet, [$this, 'checkMongoId']);
    }

    /**
     * 获取MongoId

     * @param array $result 返回数据
     * @return array
     */
    protected function checkMongoId(&$result)
    {
        if (is_object($result['_id'])) {
            $result['_id'] = $result['_id']->__toString();
        }

        return $result;
    }

    // 表达式过滤回调方法
    protected function _options_filter(&$options)
    {
        $id = $this->getPk();
        if (isset($options['where'][$id]) && is_scalar($options['where'][$id]) && $this->_idType == self::TYPE_OBJECT) {
            $options['where'][$id] = new \MongoId($options['where'][$id]);
        }
    }

    /**
     * 查询数据

     * @param mixed $options 表达式参数
     * @return mixed
     */
    public function find($options = [])
    {
        if (is_numeric($options) || is_string($options)) {
            $id = $this->getPk();
            $where[$id] = $options;
            $options = [];
            $options['where'] = $where;
        }
        // 分析表达式
        $options = $this->_parseOptions($options);
        $result = $this->db->find($options);
        if (false === $result) {
            return false;
        }
        if (empty($result)) {// 查询结果为空
            return null;
        } else {
            $this->checkMongoId($result);
        }
        $this->data = $result;
        $this->_after_find($this->data, $options);

        return $this->data;
    }

    /**
     * 字段值增长

     * @param string $field 字段名
     * @param integer $step 增长值
     * @return boolean
     */
    public function setInc($field, $step = 1)
    {
        return $this->setField($field, ['inc', $step]);
    }

    /**
     * 字段值减少

     * @param string $field 字段名
     * @param integer $step 减少值
     * @return boolean
     */
    public function setDec($field, $step = 1)
    {
        return $this->setField($field, ['inc', '-'.$step]);
    }

    /**
     * 获取一条记录的某个字段值

     * @param string $field 字段名
     * @param string $spea 字段数据间隔符号
     * @return mixed
     */
    public function getField($field, $sepa = null)
    {
        $options['field'] = $field;
        $options = $this->_parseOptions($options);
        if (strpos($field, ',')) { // 多字段
            if (is_numeric($sepa)) {// 限定数量
                $options['limit'] = $sepa;
                $sepa = null;// 重置为null 返回数组
            }
            $resultSet = $this->db->select($options);
            if (!empty($resultSet)) {
                $_field = explode(',', $field);
                $field = array_keys($resultSet[0]);
                $key = array_shift($field);
                $key2 = array_shift($field);
                $cols = [];
                $count = count($_field);
                foreach ($resultSet as $result) {
                    $name = $result[$key];
                    if (2 == $count) {
                        $cols[$name] = $result[$key2];
                    } else {
                        $cols[$name] = is_null($sepa) ? $result : implode($sepa, $result);
                    }
                }

                return $cols;
            }
        } else {
            // 返回数据个数
            if (true !== $sepa) {// 当sepa指定为true的时候 返回所有数据
                $options['limit'] = is_numeric($sepa) ? $sepa : 1;
            }            // 查找符合的记录
            $result = $this->db->select($options);
            if (!empty($result)) {
                if (1 == $options['limit']) {
                    $result = reset($result);

                    return $result[$field];
                }
                foreach ($result as $val) {
                    $array[] = $val[$field];
                }

                return $array;
            }
        }

        return null;
    }

    /**
     * 执行Mongo指令

     * @param array $command 指令
     * @return mixed
     */
    public function command($command, $options = [])
    {
        $options = $this->_parseOptions($options);

        return $this->db->command($command, $options);
    }

    /**
     * 执行MongoCode

     * @param string $code MongoCode
     * @param array $args 参数
     * @return mixed
     */
    public function mongoCode($code, $args = [])
    {
        return $this->db->execute($code, $args);
    }

    // 数据库切换后回调方法
    protected function _after_db()
    {
        // 切换Collection
        $this->db->switchCollection($this->getTableName(), $this->dbName ? $this->dbName : C('db_name'));
    }

    /**
     * 得到完整的数据表名 Mongo表名不带dbName

     * @return string
     */
    public function getTableName()
    {
        if (empty($this->trueTableName)) {
            $tableName = !empty($this->tablePrefix) ? $this->tablePrefix : '';
            if (!empty($this->tableName)) {
                $tableName .= $this->tableName;
            } else {
                $tableName .= parse_name($this->name);
            }
            $this->trueTableName = strtolower($tableName);
        }

        return $this->trueTableName;
    }

    /**
     * 分组查询

     * @return string
     */
    public function group($key, $init, $reduce, $option = [])
    {
        $option = $this->_parseOptions($option);

        //合并查询条件
        if (isset($option['where'])) {
            $option['condition'] = array_merge((array)$option['condition'], $option['where']);
        }

        return $this->db->group($key, $init, $reduce, $option);
    }

    /**
     * 返回Mongo运行错误信息

     * @return json
     */
    public function getLastError()
    {
        return $this->db->command(['getLastError' => 1]);
    }

    /**
     * 返回指定集合的统计信息，包括数据大小、已分配的存储空间和索引的大小

     * @return json
     */
    public function status()
    {
        $option = $this->_parseOptions();

        return $this->db->command(['collStats' => $option['table']]);
    }

    /**
     * 取得当前数据库的对象

     * @return object
     */
    public function getDB()
    {
        return $this->db->getDB();
    }

    /**
     * 取得集合对象，可以进行创建索引等查询

     * @return object
     */
    public function getCollection()
    {
        return $this->db->getCollection();
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: coffee
 * Date: 2017/8/17
 * Time: 上午12:58
 */

namespace SimpleShop\Brand\Repositories;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use SimpleShop\Repositories\Contracts\RepositoryInterface as Repository;
use SimpleShop\Repositories\Criteria\Criteria;

class Search extends Criteria
{
    /**
     * @var array
     */
    private $search;

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

    /**
     * @param  Model|Builder $model
     * @param Repository     $repository
     *
     * @return mixed
     */
    public function apply($model, Repository $repository)
    {
        if (isset($this->search['name'])) {
            $model = $model->where('name', 'like', "%{$this->search['name']}%");
        }

        return $model;
    }
}


<?php namespace Ordercloud\Entities\Products;

class UnlockableProductExtraSet extends ProductExtraSet
{
    /**
     * @var array|int[]
     */
    private $unlockingOptionIds;

    /**
     * @param ProductExtraSet $extraSet
     * @param array|int[]     $unlockingOptionIds
     */
    public function __construct(ProductExtraSet $extraSet, array $unlockingOptionIds)
    {
        parent::__construct(
            $extraSet->getId(),
            $extraSet->getName(),
            $extraSet->getDisplayName(),
            $extraSet->getDescription(),
            $extraSet->isEnabled(),
            $extraSet->getExtras()
        );
        $this->unlockingOptionIds = $unlockingOptionIds;
    }

    /**
     * @param Product $product
     *
     * @return array|static[]
     */
    public static function createFromProduct(Product $product)
    {
        $linkedSets = [];
        $productOptionSets = $product->getOptionSets();

        while ($set = array_shift($productOptionSets)) {
            foreach ($set->getOptions() as $option) {
                $productOptionSets = array_merge($productOptionSets, $option->getUnlockOptionSets());
                foreach ($option->getUnlockExtraSets() as $unlockExtraSet) {
                    $linkedSets[$unlockExtraSet->getId()]['set'] = $unlockExtraSet;
                    $linkedSets[$unlockExtraSet->getId()]['options'][] = $option->getId();
                }
            }
        }

        $linkedSetInstances = [];

        foreach ($linkedSets as $linkedSet) {
            $linkedSetInstances[] = new static($linkedSet['set'], $linkedSet['options']);
        }

        return $linkedSetInstances;
    }

    /**
     * Returns the IDs of the options
     * that unlocks this ExtraSet.
     *
     * @return array|int[]
     */
    public function getUnlockingOptionIds()
    {
        return $this->unlockingOptionIds;
    }
}


<?php

namespace Kcs\FunctionMock\Prediction;

use Kcs\FunctionMock\Exception\Call\NoCallsException;
use Kcs\FunctionMock\FunctionProphecy;
use Prophecy\Argument\ArgumentsWildcard;
use Prophecy\Argument\Token\AnyValuesToken;
use Prophecy\Call\Call;
use Prophecy\Util\StringUtil;

class CallPrediction implements PredictionInterface
{
    private $util;

    /**
     * Initializes prediction.
     *
     * @param StringUtil $util
     */
    public function __construct(StringUtil $util = null)
    {
        $this->util = $util ?: new StringUtil();
    }

    /**
     * Tests that there was at least one call.
     *
     * @param Call[]         $calls
     * @param FunctionProphecy $prophecy
     *
     * @throws NoCallsException
     */
    public function check(array $calls, FunctionProphecy $prophecy)
    {
        if (count($calls)) {
            return;
        }

        $methodCalls = $prophecy->getNamespace()->findCalls($prophecy->getName(), new ArgumentsWildcard([new AnyValuesToken()]));

        if (count($methodCalls)) {
            throw new NoCallsException(sprintf(
                "No calls have been made that match:\n".
                "  %s(%s)\n".
                "but expected at least one.\n".
                "Recorded `%s(...)` calls:\n%s",

                $prophecy->getName(),
                $prophecy->getArgumentsWildcard(),
                $prophecy->getName(),
                $this->util->stringifyCalls($methodCalls)
            ));
        }

        throw new NoCallsException(sprintf(
            "No calls have been made that match:\n".
            "  %s(%s)\n".
            'but expected at least one.',

            $prophecy->getName(),
            $prophecy->getArgumentsWildcard()
        ));
    }
}