diff --git a/cake/libs/http_socket.php b/cake/libs/http_socket.php index 143e92f68..d4eae2d1f 100644 --- a/cake/libs/http_socket.php +++ b/cake/libs/http_socket.php @@ -41,417 +41,691 @@ class HttpSocket extends CakeSocket { * @var string */ var $description = 'HTTP-based DataSource Interface'; + +/** + * When one activates the $quirksMode by setting it to true, all checks meant to enforce RFC 2616 (HTTP/1.1 specs) + * will be disabled and additional measures to deal with non-standard responses will be enabled. + * + * @var boolean + */ + var $quirksMode = false; + /** * The default values to use for a request * * @var array */ var $request = array( - 'method' => 'GET', - 'uri' => array( - 'scheme' => 'http', - 'user' => null, - 'password' => null, + 'method' => 'GET', + 'uri' => array( + 'scheme' => 'http', 'host' => null, 'port' => 80, - 'path' => '/', - 'query' => null - ), - 'auth' => array( - 'method' => 'basic' - , 'user' => null - , 'password' => null - ), - 'version' => '1.1', - 'body' => '', - 'requestLine' => null, - 'header' => array( - 'Connection' => 'close', 'User-Agent' => 'CakePHP' - ), + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => null, + 'fragment' => null + ), + 'auth' => array( + 'method' => 'basic', + 'user' => null, + 'pass' => null + ), + 'version' => '1.1', + 'body' => '', + 'line' => null, + 'header' => array( + 'Connection' => 'close', + 'User-Agent' => 'CakePHP' + ), 'raw' => null - ); -/** - * The default strucutre for storing the response - * - * @var unknown_type - */ - var $response = array( - 'raw' => '', - 'rawHeader' => '', - 'rawBody' => '', - 'statusLine' => '', - 'code' => '', - 'header' => array(), - 'body' => '' ); - - var $config = array(); +/** +* The default structure for storing the response +* +* @var array +*/ + var $response = array( + 'raw' => array( + 'status-line' => null, + 'header' => null, + 'body' => null, + 'response' => null + ), + 'status' => array( + 'http-version' => null, + 'code' => null, + 'reason-phrase' => null + ), + 'header' => array(), + 'body' => '' + ); /** - * Base configuration settings for the socket connection + * Default configuration settings for the HttpSocket * * @var array */ - var $_baseConfig = array( - 'persistent' => false, - 'host' => 'localhost', - 'port' => 80, - 'scheme' => 'http', - 'timeout' => 30, - 'authMethod' => 'basic', - 'user' => null, - 'password' => null, + var $config = array( + 'persistent' => false, + 'host' => 'localhost', + 'protocol' => 'tcp', + 'port' => 80, + 'timeout' => 30, + 'request' => array( + 'uri' => array( + 'scheme' => 'http', + 'host' => 'localhost', + 'port' => 80 + ), + 'auth' => array( + 'method' => 'basic', + 'user' => null, + 'pass' => null + ) + ) ); -/** - * The line break type to use for building the header. According to "RFC 2616 - Hypertext Transfer - * Protocol -- HTTP/1.1", clients MUST accept CRLF, LF and CR if used consistently. - * - * @var string - */ - var $headerSeparator = "\r\n"; -/** - * Called when creating a new instance of this object - * - * @param array $config Socket configuration, which will be merged with the base configuration - */ - function __construct($config = array()) { - parent::__construct(); +/** + * Enter description here... + * + * @var unknown_type + */ + var $lineBreak = "\r\n"; + + function __construct($config = array()) { if (is_string($config)) { - $uri = $this->parseURI($config); - - $config = array_intersect_key($uri, $this->_baseConfig); + $this->configUri($config); + } elseif (is_array($config)) { + $this->config = Set::merge($this->config, $config); } - $this->config = am($this->_baseConfig, $config); + parent::__construct($this->config); } - - function parseURI($uri = null, $overwrite = array()) { - if (is_array($uri)) { - return $uri; + + function request($request = array()) { + $this->reset(false); + + if (is_string($request)) { + $request = array('uri' => $request); + } elseif (!is_array($request)) { + return false; + } + + if (isset($request['host'])) { + $host = $request['host']; + unset($request['host']); + } + + if (is_string($request['uri']) && !preg_match('/^.+:\/\/|\*|^\//', $request['uri'])) { + $request['uri'] = 'http://'.$request['uri']; + } + + $request['uri'] = $this->parseUri($request['uri']); + $this->request = Set::merge($this->request, $this->config['request'], $request); + $this->configUri($this->request['uri']); + + if (isset($host)) { + $this->config['host'] = $host; + } + + if (is_array($this->request['header'])) { + $this->request['header'] = $this->parseHeader($this->request['header']); + $this->request['header'] = am(array('Host' => $this->request['uri']['host']), $this->request['header']); + } + + if (is_array($this->request['body'])) { + $this->request['body'] = $this->httpSerialize($this->request['body']); + } + + if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) { + $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) { + $this->request['header']['Content-Length'] = strlen($this->request['body']); + } + + $this->request['header'] = $this->buildHeader($this->request['header']); + + if (empty($this->request['line'])) { + $this->request['line'] = $this->buildRequestLine($this->request); + } + + if ($this->quirksMode === false && $this->request['line'] === false) { + return $this->response = false; + } + + if ($this->request['line'] !== false) { + $this->request['raw'] = $this->request['line']; + } + + if ($this->request['header'] !== false) { + $this->request['raw'] .= $this->request['header']; + } + + $this->request['raw'] .= "\r\n"; + $this->request['raw'] .= $this->request['body']; + $this->write($this->request['raw']); + + $response = null; + while ($data = $this->read()) { + $response .= $data; + } + + $this->response = $this->parseResponse($response); + return $this->response['body']; + } + + function get($uri = null, $query = array(), $request = array()) { + if (!empty($query)) { + $uri =$this->parseUri($uri); + if (isset($uri['query'])) { + $uri['query'] = am($uri['query'], $query); + } else { + $uri['query'] = $query; + } + $uri = $this->buildUri($uri); } - if (empty($uri)) { - $uri = $this->config['host']; - } elseif (strpos($uri, '/') === 0) { - $uri = $this->config['scheme'].'://'.$this->config['host'].$uri; - } - - /* - $Validation =& new Validation(); - if (!$Validation->url($uri)) { + $request = Set::merge(array('method' => 'GET', 'uri' => $uri), $request); + return $this->request($request); + } + + function post($uri = null, $data = array(), $request = array()) { + $request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request); + return $this->request($request); + } + + function put($uri = null, $data = array(), $request = array()) { + $request = Set::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request); + return $this->request($request); + } + + function delete($uri = null, $data = array(), $request = array()) { + $request = Set::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request); + return $this->request($request); + } + + function parseResponse($message) { + if (is_array($message)) { + return $message; + } elseif (!is_string($message)) { return false; } - */ - - if (!is_array($uri)) { - $uri = parse_url($uri); + + static $responseTemplate; + if (empty($responseTemplate)) { + $classVars = get_class_vars(__CLASS__); + $responseTemplate = $classVars['response']; } - - $uri = am($uri, $overwrite); - - if (!isset($uri['scheme']) || !in_array($uri['scheme'], array('http', 'https'))) { - return false; - } - - if (isset($uri['query']) && is_string($uri['query'])) { - $items = explode('&', $uri['query']); - $query = array(); - - foreach ($items as $item) { - if (isset($item[1]) && !empty($item[0])) { - list($key, $value) = explode('=', $item); - $query[urldecode($key)] = urldecode($value); - } + + $response = $responseTemplate; + + if (preg_match("/(.+\r\n)(.*)(?<=\r\n)\r\n(.+)\$/DUs", $message, $match)) { + list($response['raw']['response'], $response['raw']['status-line'], $response['raw']['header'], $response['raw']['body']) = $match; + + if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $response['raw']['status-line'], $match)) { + $response['status']['http-version'] = $match[1]; + $response['status']['code'] = (int)$match[2]; + $response['status']['reason-phrase'] = $match[3]; } - - $uri['query'] = $query; + + $response['header'] = $this->parseHeader($response['raw']['header']); + $decoded = $this->decodeBody($response['raw']['body'], @$response['header']['Transfer-Encoding']); + $response['body'] = $decoded['body']; + if (!empty($decoded['header'])) { + $response['header'] = $this->parseHeader($this->buildHeader($response['header']).$this->buildHeader($decoded['header'])); + } + + } else { + return false; } - - return $uri; + + foreach ($response['raw'] as $field => $val) { + if ($val === '') { + $response['raw'][$field] = null; + } + } + + return $response; + } +/** + * Generic function to decode a $body with a given $encoding. Returns either an array with the keys + * 'body' and 'header' or false on failure. + * + * @param string $body A string continaing the body to decode + * @param mixed $encoding Can be false in case no encoding is being used + * @return mixed Array or false + */ + function decodeBody($body, $encoding = 'chunked') { + if (!is_string($body)) { + return false; + } + if (empty($encoding)) { + return array('body' => $body, 'header' => false); + } + $decodeMethod = 'decode'.Inflector::camelize(r('-', '_', $encoding)).'Body'; + + if (!is_callable(array(&$this, $decodeMethod))) { + if (!$this->quirksMode) { + trigger_error('HttpSocket::decodeBody - Unkown encoding: "'.h($encoding).'". Activate quirks mode to surpress error.', E_USER_WARNING); + } + return array('body' => $body, 'header' => false); + } + return $this->{$decodeMethod}($body); + } +/** + * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as + * a result. + * + * @param string $body A string continaing the chunked body to decode + * @return mixed Array or false + */ + function decodeChunkedBody($body) { + if (!is_string($body)) { + return false; + } + + $decodedBody = null; + $chunkLength = null; + + while ($chunkLength !== 0) { + if (!preg_match("/^([0-9a-f]+)(?:;(.+)=(.+))?\r\n/iU", $body, $match)) { + if (!$this->quirksMode) { + trigger_error('HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', E_USER_WARNING); + return false; + } + break; + } + + @list($chunkSize, $hexLength, $chunkExtensionName, $chunkExtensionValue) = $match; + $body = substr($body, strlen($chunkSize)); + $chunkLength = hexdec($hexLength); + $chunk = substr($body, 0, $chunkLength); + if (!empty($chunkExtensionName)) { + /** + * @todo See if there are popular chunk extensions we should implement + */ + } + $decodedBody .= $chunk; + if ($chunkLength !== 0) { + $body = substr($body, $chunkLength+strlen("\r\n")); + } + } + + $entityHeader = false; + if (!empty($body)) { + $entityHeader = $this->parseHeader($body); + } + return array('body' => $decodedBody, 'header' => $entityHeader); + } +/** + * Enter description here... + * + * @param unknown_type $uri + * @return unknown + */ + function configUri($uri = null) { + if (empty($uri)) { + return false; + } + + if (is_array($uri)) { + $uri = $this->parseUri($uri); + } else { + $uri = $this->parseUri($uri, true); + } + + if (!isset($uri['host'])) { + return false; + } + + $config = array( + 'request' => array( + 'uri' => array_intersect_key($uri, $this->config['request']['uri']), + 'auth' => array_intersect_key($uri, $this->config['request']['auth']) + ) + ); + $this->config = Set::merge($this->config, $config); + $this->config = Set::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config)); + return $this->config; } /** * Takes a $uri array and turns it into a fully qualified URL string * * @param array $uri A $uri array, or uses $this->config if left empty - * @param string $uriTemplate The URI template/format to use + * @param string $uriTemplate The Uri template/format to use * @return string A fully qualified URL formated according to $uriTemplate */ - function getURI($uri = array(), $uriTemplate = '%scheme://%username:%password@%host:%port/%path?%query') { + function buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') { + if (is_string($uri)) { + $uri = array('host' => $uri); + } + $uri = $this->parseUri($uri, true); + + if (!is_array($uri) || empty($uri)) { + return false; + } + $uri['path'] = preg_replace('/^\//', null, $uri['path']); - $uri['query'] = $this->serialize($uri['query']); + $uri['query'] = $this->httpSerialize($uri['query']); + $stripIfEmpty = array( + 'query' => '?%query', + 'fragment' => '#%fragment', + 'user' => '%user:%pass@' + ); - if (empty($uri['query'])) { - $uriTemplate = str_replace('?%query', null, $uriTemplate); + foreach ($stripIfEmpty as $key => $strip) { + if (empty($uri[$key])) { + $uriTemplate = str_replace($strip, null, $uriTemplate); + } } - if (!isset($uri['username']) || empty($uri['username'])) { - $uriTemplate = str_replace('%username:%password@', null, $uriTemplate); - } $defaultPorts = array('http' => 80, 'https' => 443); - - if ($defaultPorts[$uri['scheme']] == $uri['port']) { + if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) { $uriTemplate = str_replace(':%port', null, $uriTemplate); } + foreach ($uri as $property => $value) { $uriTemplate = str_replace('%'.$property, $value, $uriTemplate); } + + if ($uriTemplate === '/*') { + $uriTemplate = '*'; + } return $uriTemplate; } /** - * Determine the status of, and ability to connect to the current host + * Enter description here... * - * @todo Ping the current host If $this->path is non-empty and != '/', query the path for a non-404 response - * @return boolean Success - */ - function isConnected() { - return true; - } -/** - * Returns the results of a Http request for the contents of a $path (possibly including a query) relative - * to the current config['host'] using some optional $options. - * - * @return array An array structure containing HTTP headers and response body - */ - function request($request = array()) { - $this->reset(false); - - $baseRequest = $this->request; - $this->request = am($baseRequest, $request); - - $this->request['uri'] = am($baseRequest['uri'], $this->parseURI($this->request['uri'])); - - $configMap = array( - 'uri' => array( - 'host' => 'host', - 'scheme' => 'scheme', - 'port' => 'port' - ), - 'auth' => array( - 'authMethod' => 'method', - 'user' => 'user', - 'password' => 'password' - ) - ); - - foreach ($configMap as $type => $mappings) { - foreach ($mappings as $configKey => $requestKey) { - if (empty($this->request[$type][$requestKey])) { - $this->request[$type][$requestKey] = $this->config[$configKey]; - } - $this->config[$configKey] = $this->request[$type][$requestKey]; - } - } - - $this->request = $this->generateRequest($this->request); - $this->connect(); - $this->write($this->request['raw']); - - $rawResponse = null; - - while ($package = $this->read()) { - $rawResponse = $rawResponse.$package; - }; - - $this->response = $this->parseResponse($rawResponse); - return $this->response['body']; - } - - function parseResponse($rawResponse) { - $response = $this->response; - $response['raw'] = $rawResponse; - - $headerEnd = strpos($rawResponse, str_repeat($this->headerSeparator, 2)); - $response['rawHeader'] = substr($rawResponse, 0, $headerEnd); - - $headerParts = explode($this->headerSeparator, $response['rawHeader']); - - $response['statusLine'] = array_shift($headerParts); - - if (preg_match('/HTTP\/[1]\.[01] ([0-9]{3}) .+/', $response['statusLine'], $match)) { - $response['code'] = $match[1]; - } - - foreach ($headerParts as $headerPart) { - list($key, $value) = preg_split('/\: ?/', $headerPart, 2); - $response['header'][$key] = $value; - } - - $response['rawBody'] = substr($rawResponse, $headerEnd + strlen($this->headerSeparator)*2); - - $encoding = $this->getHeader('Transfer-Encoding'); - $response['body'] = $this->decodeBody($response['rawBody'], $encoding); - - return $response; - } - - function decodeBody($rawBody, $encoding = 'chunked') { - return $rawBody; - } -/** - * Returns the header with a given $name respecting the $matchCase flag. - * - * @param unknown_type $name - * @param unknown_type $matchCase + * @param unknown_type $uri + * @param unknown_type $base * @return unknown */ - function getHeader($name = null, $matchCase = false) { - if ($name === null) { - return $this->responseHeader; + function parseUri($uri = null, $base = array()) { + $uriBase = array( + 'scheme' => array('http', 'https'), + 'host' => null, + 'port' => array(80, 443), + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => null, + 'fragment' => null + ); + + if (is_string($uri)) { + $uri = parse_url($uri); } - if (isset($this->responseHeader[$name])) { - return $this->responseHeader[$name]; - } - if ($matchCase == true) { + if (!is_array($uri) || empty($uri)) { return false; } - foreach ($this->response['header'] as $key => $val) { - if (low($key) == low($name)) { - return $val; - } + if ($base === true) { + $base = $uriBase; } - return false; - } - -/** - * Request a URL using the GET method - * - * @param array $options - * @return An array structure containing HTTP headers and response body - */ - function get($uri = null, $query = array()) { - $request = array('method' => 'GET'); - if (is_array($uri)) { - $request = am($request, $uri); - } else { - $overwrite = array(); - if (!empty($query)) { - $overwrite['query'] = $query; - } - - $uri = $this->parseURI($uri, $overwrite); - if (!empty($uri)) { - $request['uri'] = $uri; + if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) { + if (isset($uri['scheme']) && !isset($uri['port'])) { + $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])]; + } elseif (isset($uri['port']) && !isset($uri['scheme'])) { + $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])]; } } - return $this->request($request); + + if (is_array($base) && !empty($base)) { + $uri = am($base, $uri); + } + + if (isset($uri['scheme']) && is_array($uri['scheme'])) { + $uri['scheme'] = array_shift($uri['scheme']); + } + if (isset($uri['port']) && is_array($uri['port'])) { + $uri['port'] = array_shift($uri['port']); + } + + if (array_key_exists('query', $uri)) { + $uri['query'] = $this->parseQuery($uri['query']); + } + + if (!array_intersect_key($uriBase, $uri)) { + return false; + } + return $uri; } /** - * Request a URL using the POST method - * - * @param array $options - * @return An array structure containing HTTP headers and response body + * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given query string and turns it into an array and + * supports nesting by using the php bracket syntax. So this menas you can parse queries like: + * + * - ?key[subKey]=value + * - ?key[]=value1&key[]=value2 + * + * A leading '?' mark in $query is optional and does not effect the outcome of this function. For the complete capabilities of this implementation + * take a look at HttpSocketTest::testParseQuery() + * + * @param mixed $query A query string to parse into an array or an array to return directly "as is" + * @return array The $query parsed into a possibly multi-level array. If an empty $query is given, an empty array is returned. */ - function post($uri = null, $body = array()) { - $request = array('method' => 'POST'); - - if (is_array($uri)) { - $request = am($request, $uri); - } else { - $uri = $this->parseURI($uri); - - if (!empty($uri)) { - $request['uri'] = am($this->request['uri'], $uri); - } - $request['body'] = $body; + function parseQuery($query) { + if (is_array($query)) { + return $query; } - return $this->request($request); - } -/** - * Request a URL using the PUT method - * - * @param array $options - * @return An array structure containing HTTP headers and response body - */ - function put() { - $options['method'] = 'PUT'; - return $this->request($uri, $options); - } -/** - * Request a URL using the DELETE method - * - * @param array $options - * @return An array structure containing HTTP headers and response body - */ - function delete() { - $options['method'] = 'DELETE'; - return $this->request($uri, $options); - } + $parsedQuery = array(); - function generateRequest($request) { - if (empty($request['requestLine'])) { - $request['requestLine'] = $request['method'].' '.$this->getURI($request['uri'], '/%path?%query').' HTTP/1.1'; - } - - - if (!empty($request['body'])) { - if (is_array($request['body'])) { - $request['body'] = $this->serialize($request['body']); - } - - if (!isset($request['header']['Content-Type'])) { - $request['header']['Content-Type'] = 'application/x-www-form-urlencoded'; + if (is_string($query) && !empty($query)) { + $query = preg_replace('/^\?/', '', $query); + $items = explode('&', $query); + foreach ($items as $item) { + if (strpos($item, '=') !== false) { + list($key, $value) = explode('=', $item); + } else { + $key = $item; + $value = null; + } + + $key = urldecode($key); + $value = urldecode($value); + + if ($value === '') { + $value = null; + } elseif (ctype_digit($value) == true) { + $value = (int)$value; + } + + if(preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) { + $subKeys = $matches[1]; + $rootKey = substr($key, 0, strpos($key, '[')); + if (!empty($rootKey)) { + array_unshift($subKeys, $rootKey); + } + $queryNode =& $parsedQuery; + + foreach ($subKeys as $subKey) { + if (!is_array($queryNode)) { + $queryNode = array(); + } + + if ($subKey === '') { + $queryNode[] = array(); + end($queryNode); + $subKey = key($queryNode); + } + $queryNode =& $queryNode[$subKey]; + } + $queryNode = $value; + } else { + $parsedQuery[$key] = $value; + } } } - $request['header'] = $this->buildHeader($request); - - $request['raw'] = $request['requestLine'].$this->headerSeparator; - $request['raw'] .= $this->serializeHeader($request['header']); - $request['raw'] .= $request['body']; - - return $request; + return $parsedQuery; } /** - * Takes an array of items and serializes them for a GET/POST request + * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs. * - * @param array $items An associative array of items to serialize - * @return string A string ready to be sent via HTTP - * @todo Implement http_build_query for php5 and an alternative solution for php4, see http://us2.php.net/http_build_query + * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET. + * @param string $versionToken The version token to use, defaults to HTTP/1.1 + * @return unknown */ - function serialize($items) { - return substr(Router::queryString($items), 1); + function buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') { + $asteriskMethods = array('OPTIONS'); + + if (is_string($request)) { + $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match); + if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) { + trigger_error('HttpSocket::buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.', E_USER_WARNING); + return false; + } + return $request; + } elseif (!is_array($request)) { + return false; + } elseif (!array_key_exists('uri', $request)) { + return false; + } + + $request['uri'] = $this->parseUri($request['uri']); + $request = am(array('method' => 'GET'), $request); + $request['uri'] = $this->buildUri($request['uri'], '/%path?%query'); + + if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) { + trigger_error('HttpSocket::buildRequestLine - The "*" asterisk character is only allowed for the following methods: '.join(',', $asteriskMethods).'. Activate quirks mode to work outside of HTTP/1.1 specs.', E_USER_WARNING); + return false; + } + return $request['method'].' '.$request['uri'].' '.$versionToken.$this->lineBreak; } - function serializeHeader($headerParts) { - foreach ($headerParts as $key => $value) { - $header[] = $key.': '.$value; + function httpSerialize($data = array()) { + if (is_string($data)) { + return $data; + } + if (empty($data) || !is_array($data)) { + return false; + } + return substr(Router::queryString($data), 1); + } +/** + * Enter description here... + * + * @param unknown_type $header + * @return unknown + */ + function buildHeader($header) { + if (is_string($header)) { + return $header; + } elseif (!is_array($header)) { + return false; } - return join($this->headerSeparator, $header).str_repeat($this->headerSeparator, 2); - } - function buildHeader($request) { + $returnHeader = ''; + foreach ($header as $field => $contents) { + if (is_array($contents)) { + $contents = join(',', $contents); + } + $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $contents); + $field = $this->escapeToken($field); + + $returnHeader .= $field.': '.$contents.$this->lineBreak; + } + return $returnHeader; + } + + function parseHeader($header) { + if (is_array($header)) { + foreach ($header as $field => $value) { + unset($header[$field]); + $field = low($field); + preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE); + + foreach ($offsets[0] as $offset) { + $field = substr_replace($field, up($offset[0]), $offset[1], 1); + } + $header[$field] = $value; + } + return $header; + } elseif (!is_string($header)) { + return false; + } + + preg_match_all("/(.+):(.+)(?:(?lineBreak."|\$)/Uis", $header, $matches, PREG_SET_ORDER); + $header = array(); - $headerMap = array( - 'uri.host' => 'Host' - ); - - foreach ($headerMap as $fromPath => $to) { - $header[$to] = Set::extract($request, $fromPath); + foreach ($matches as $match) { + list(, $field, $value) = $match; + + $value = trim($value); + $value = preg_replace("/[\t ]\r\n/", "\r\n", $value); + + $field = $this->unescapeToken($field); + + $field = low($field); + preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE); + foreach ($offsets[0] as $offset) { + $field = substr_replace($field, up($offset[0]), $offset[1], 1); + } + + if (!isset($header[$field])) { + $header[$field] = $value; + } else { + $header[$field] .= ','.$value; + } } - if (!empty($request['body']) && !isset($request['header']['Content-Length'])) { - $header['Content-Length'] = strlen($request['body']); - } - return am($header, $request['header']); + return $header; + } + + function unescapeToken($token) { + $regex = '/"(['.join('', $this->__tokenEscapeChars()).'])"/'; + $token = preg_replace($regex, '\\1', $token); + return $token; } /** - * Resets the state of this socket (automatically called before any request) + * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs) * + * @param string $token + * @return string + */ + function escapeToken($token) { + $regex = '/(['.join('', $this->__tokenEscapeChars()).'])/'; + $token = preg_replace($regex, '"\\1"', $token); + return $token; + } + + function __tokenEscapeChars($hex = true) { + $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " "); + for ($i = 0; $i <= 31; $i++) { + $escape[] = chr($i); + } + $escape[] = chr(127); + + if ($hex == false) { + return $escape; + } + $regexChars = ''; + foreach ($escape as $key => $char) { + $escape[$key] = '\\x'.str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT); + } + return $escape; + } +/** + * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got executed) or does the same thing partially for the + * request and the response property only. + * + * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reseted + * @return boolean True on success */ function reset($full = true) { - static $classVars = array() ; - - if (empty($classVars)) { - $classVars = get_class_vars(__CLASS__); + static $initalState = array() ; + if (empty($initalState)) { + $initalState = get_class_vars(__CLASS__); } - + if ($full == false) { - $this->request = $classVars['request']; - $this->response = $classVars['response']; + $this->request = $initalState['request']; + $this->response = $initalState['response']; return true; } - foreach ($classVars as $var => $defaultVal) { - $this->{$var} = $defaultVal; + foreach ($initalState as $property => $value) { + $this->{$property} = $value; } return true; } diff --git a/cake/libs/socket.php b/cake/libs/socket.php index 9d499d053..7204b2a1a 100644 --- a/cake/libs/socket.php +++ b/cake/libs/socket.php @@ -96,8 +96,6 @@ class CakeSocket extends Object { if (!is_numeric($this->config['protocol'])) { $this->config['protocol'] = getprotobyname($this->config['protocol']); } - - return $this->connect(); } /** * Connect the socket to the given host and port. @@ -232,8 +230,12 @@ class CakeSocket extends Object { * @return boolean Success */ function disconnect() { - $this->connected = !@fclose($this->connection); - + if (!is_resource($this->connection)) { + $this->connected = false; + return true; + } + $this->connected = !fclose($this->connection); + if (!$this->connected) { $this->connection = null; } diff --git a/cake/tests/cases/libs/http_socket.test.php b/cake/tests/cases/libs/http_socket.test.php new file mode 100755 index 000000000..c8a62cf76 --- /dev/null +++ b/cake/tests/cases/libs/http_socket.test.php @@ -0,0 +1,985 @@ +Socket =& new TestHttpSocket(); + $this->RequestSocket =& new TestHttpSocketRequests(); + } + +/** + * We use this function to clean up after the test case was executed + * + */ + function tearDown() { + // Unset the HttpSocket instance we used to free memory + unset($this->Socket, $this->RequestSocket); + } + +/** + * Test that HttpSocket::__construct does what one would expect it to do + * + */ + function testConstruct() { + // Reset our Socket instance + $this->Socket->reset(); + + $baseConfig = $this->Socket->config; + + // Expect our HttpSocket to *not* connect automatically + $this->Socket->expectNever('connect'); + + // Test that passing an array causes it to be merged over our config property + $this->Socket->__construct(array('host' => 'foo-bar')); + $baseConfig['host'] = 'foo-bar'; + $baseConfig['protocol'] = getprotobyname($baseConfig['protocol']); + $this->assertIdentical($this->Socket->config, $baseConfig); + + // Reset our Socket instance + $this->Socket->reset(); + $baseConfig = $this->Socket->config; + + // Test constructing from a simple url + $this->Socket->__construct('http://www.cakephp.org:23/'); + $baseConfig['host'] = 'www.cakephp.org'; + $baseConfig['request']['uri']['host'] = 'www.cakephp.org'; + $baseConfig['port'] = 23; + $baseConfig['request']['uri']['port'] = 23; + $baseConfig['protocol'] = getprotobyname($baseConfig['protocol']); + $this->assertIdentical($this->Socket->config, $baseConfig); + } + +/** + * Test that HttpSocket::configUri works properly with different types of arguments + * + */ + function testConfigUri() { + // Reset our Socket instance + $this->Socket->reset(); + + // Test that the configuration is picked up from a Uri correctly + $r = $this->Socket->configUri('https://bob:secret@www.cakephp.org:23/?query=foo'); + $expected = array( + 'persistent' => false, + 'host' => 'www.cakephp.org', + 'protocol' => 'tcp', + 'port' => 23, + 'timeout' => 30, + 'request' => array( + 'uri' => array( + 'scheme' => 'https' + , 'host' => 'www.cakephp.org' + , 'port' => 23 + ), + 'auth' => array( + 'method' => 'basic' + , 'user' => 'bob' + , 'pass' => 'secret' + ) + ) + ); + $this->assertIdentical($this->Socket->config, $expected); + $this->assertIdentical($r, $expected); + + // Test that passing in an array causes to only overwrite the values set in it + $r = $this->Socket->configUri(array('host' => 'www.foo-bar.org')); + $expected['host'] = 'www.foo-bar.org'; + $expected['request']['uri']['host'] = 'www.foo-bar.org'; + $this->assertIdentical($this->Socket->config, $expected); + $this->assertIdentical($r, $expected); + + // Test that a new uri overwrites things completely + $r = $this->Socket->configUri('http://www.foo.com'); + $expected = array( + 'persistent' => false, + 'host' => 'www.foo.com', + 'protocol' => 'tcp', + 'port' => 80, + 'timeout' => 30, + 'request' => array( + 'uri' => array( + 'scheme' => 'http' + , 'host' => 'www.foo.com' + , 'port' => 80 + ), + 'auth' => array( + 'method' => 'basic' + , 'user' => null + , 'pass' => null + ) + ) + ); + $this->assertIdentical($this->Socket->config, $expected); + $this->assertIdentical($r, $expected); + + // Check that a path-only is not accepted by this function and returns false + $r = $this->Socket->configUri('/this-is-fuck'); + $this->assertIdentical($this->Socket->config, $expected); + $this->assertIdentical($r, false); + + // Test that passing in a non-string, non-array does not work + $r = $this->Socket->configUri(false); + $this->assertIdentical($this->Socket->config, $expected); + $this->assertIdentical($r, false); + } + +/** + * Tests that HttpSocket::request (the heart of the HttpSocket) is working properly. + * + */ + function testRequest() { + // Reset our Socket instance + $this->Socket->reset(); + + // Test that passing in an invalid $request argument causes the function to return false + $response = $this->Socket->request(true); + $this->assertIdentical($response, false); + + // Test that a couple of different $request parameters for the same request are all interpreted correctly + $requests = array( + 'http://www.cakephp.org/?foo=bar', + array('uri' => 'http://www.cakephp.org/?foo=bar'), + array( + 'uri' => array( + 'host' => 'www.cakephp.org' + , 'query' => '?foo=bar' + ) + ), + 'www.cakephp.org/?foo=bar' + ); + foreach ($requests as $request) { + $this->Socket->reset(); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->config['host'], 'www.cakephp.org'); + $this->assertIdentical($this->Socket->config['request']['uri']['host'], 'www.cakephp.org'); + $this->assertIdentical($this->Socket->request['uri']['host'], 'www.cakephp.org'); + $this->assertIdentical($this->Socket->request['uri']['query'], array('foo' => 'bar')); + } + + // Test that we can specify a different host to connect to regardless of the URI host + $this->Socket->reset(); + $request = array('host' => '192.168.0.1', 'uri' => 'http://www.cakephp.org/?foo=bar'); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['uri']['host'], 'www.cakephp.org'); + $this->assertIdentical($this->Socket->config['request']['uri']['host'], 'www.cakephp.org'); + $this->assertIdentical($this->Socket->config['host'], '192.168.0.1'); + + // Test that headers are built for us and escaping is happening + $this->Socket->reset(); + $baseHeader = $this->Socket->buildHeader($this->Socket->request['header']); + $request = array('header' => array('Foo@woo' => 'bar-value'), 'uri' => 'http://www.cakephp.org/?foo=bar'); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['header'], "Host: www.cakephp.org\r\n".$baseHeader."Foo\"@\"woo: bar-value\r\n"); + + // Test that headers are treated case insensitively + $this->Socket->reset(); + $baseHeader = $this->Socket->buildHeader($this->Socket->request['header']); + $request = array('header' => array('Foo@woo' => 'bar-value', 'host' => 'foo.com'), 'uri' => 'http://www.cakephp.org/?foo=bar'); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['header'], "Host: foo.com\r\n".$baseHeader."Foo\"@\"woo: bar-value\r\n"); + + $this->Socket->reset(); + $request = array('header' => "Foo: bar\r\n", 'uri' => 'http://www.cakephp.org/?foo=bar'); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['header'], "Foo: bar\r\n"); + + $this->Socket->reset(); + $request = array('header' => "Foo: bar\r\n", 'uri' => 'http://www.cakephp.org/search?q=http_socket#ignore-me'); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['line'], "GET /search?q=http_socket HTTP/1.1\r\n"); + $this->assertIdentical($this->Socket->request['header'], "Foo: bar\r\n"); + + $this->Socket->reset(); + $request = array('method' => 'POST', 'uri' => 'http://www.cakephp.org/posts/add', 'body' => array('name' => 'HttpSocket-is-released', 'date' => 'today')); + $response = $this->Socket->request($request); + $this->assertIdentical($this->Socket->request['body'], "name=HttpSocket-is-released&date=today"); + + $request = array('uri' => '*', 'method' => 'GET'); + $this->expectError(new PatternExpectation('/activate quirks mode/i')); + $response = $this->Socket->request($request); + $this->assertIdentical($response, false); + $this->assertIdentical($this->Socket->response, false); + + $request = array('uri' => 'htpp://www.cakephp.org/'); + $this->Socket->setReturnValue('connect', true); + $this->Socket->setReturnValue('read', false); + $this->Socket->_mock->_call_counts['read'] = 0; + $number = rand(0, 9999999); + $serverResponse = "HTTP/1.x 200 OK\r\nDate: Mon, 16 Apr 2007 04:14:16 GMT\r\nServer: CakeHttp Server\r\nContent-Type: text/html\r\n\r\n
It's good to be html
" + ) + , 'expectations' => array( + 'status.http-version' => 'HTTP/1.x', + 'status.code' => 200, + 'status.reason-phrase' => 'OK', + 'header' => $this->Socket->parseHeader("Date: Mon, 16 Apr 2007 04:14:16 GMT\r\nServer: CakeHttp Server\r\n"), + 'body' => "It's good to be html
" + ) + ), + 'no-header' => array( + 'response' => array( + 'status-line' => "HTTP/1.x 404 OK\r\n", + 'header' => null, + ) + , 'expectations' => array( + 'status.code' => 404, + 'header' => array() + ) + ), + 'chunked' => array( + 'response' => array( + 'header' => "Transfer-Encoding: chunked\r\n", + 'body' => "19\r\nThis is a chunked message\r\n0\r\n" + ), + 'expectations' => array( + 'body' => "This is a chunked message", + 'header' => $this->Socket->parseHeader("Transfer-Encoding: chunked\r\n") + ) + ), + 'enitity-header' => array( + 'response' => array( + 'body' => "19\r\nThis is a chunked message\r\n0\r\nFoo: Bar\r\n" + ), + 'expectations' => array( + 'header' => $this->Socket->parseHeader("Transfer-Encoding: chunked\r\nFoo: Bar\r\n") + ) + ), + 'enitity-header-combine' => array( + 'response' => array( + 'header' => "Transfer-Encoding: chunked\r\nFoo: Foobar\r\n" + ), + 'expectations' => array( + 'header' => $this->Socket->parseHeader("Transfer-Encoding: chunked\r\nFoo: Foobar\r\nFoo: Bar\r\n") + ) + ) + ); + + $testResponse = array(); + $expectations = array(); + + foreach ($tests as $name => $test) { + + $testResponse = am($testResponse, $test['response']); + $testResponse['response'] = $testResponse['status-line'].$testResponse['header']."\r\n".$testResponse['body']; + $r = $this->Socket->parseResponse($testResponse['response']); + $expectations = am($expectations, $test['expectations']); + + foreach ($expectations as $property => $expectedVal) { + $val = Set::extract($r, $property); + $this->assertIdentical($val, $expectedVal, 'Test "'.$name.'": response.'.$property.' - %s'); + } + + foreach (array('status-line', 'header', 'body', 'response') as $field) { + $this->assertIdentical($r['raw'][$field], $testResponse[$field], 'Test response.raw.'.$field.': %s'); + } + } + } + +/** + * Enter description here... + * + */ + function testDecodeBody() { + $this->Socket->reset(); + + $r = $this->Socket->decodeBody(true); + $this->assertIdentical($r, false); + + $r = $this->Socket->decodeBody('Foobar', false); + $this->assertIdentical($r, array('body' => 'Foobar', 'header' => false)); + + $encodings = array( + 'chunked' => array( + 'encoded' => "19\r\nThis is a chunked message\r\n0\r\n", + 'decoded' => array('body' => "This is a chunked message", 'header' => false) + ), + 'foo-coded' => array( + 'encoded' => '!Foobar!', + 'decoded' => array('body' => '!Foobar!', 'header' => false), + 'error' => new PatternExpectation('/unkown encoding: "foo-coded"/i') + ) + ); + + foreach ($encodings as $encoding => $sample) { + if (isset($sample['error'])) { + $this->expectError($sample['error']); + } + + $r = $this->Socket->decodeBody($sample['encoded'], $encoding); + $this->assertIdentical($r, $sample['decoded']); + + if (isset($sample['error'])) { + $this->Socket->quirksMode = true; + $r = $this->Socket->decodeBody($sample['encoded'], $encoding); + $this->assertIdentical($r, $sample['decoded']); + $this->Socket->quirksMode = false; + } + } + } + +/** + * Enter description here... + * + */ + function testDecodeChunkedBody() { + $this->Socket->reset(); + + $r = $this->Socket->decodeChunkedBody(true); + $this->assertIdentical($r, false); + + $encoded = "19\r\nThis is a chunked message\r\n0\r\n"; + $decoded = "This is a chunked message"; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], false); + + $encoded = "19\r\nThis is a chunked message\r\nE\r\n\nThat is cool\n\r\n0\r\n"; + $decoded = "This is a chunked message\nThat is cool\n"; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], false); + + $encoded = "19\r\nThis is a chunked message\r\nE;foo-chunk=5\r\n\nThat is cool\n\r\n0\r\n"; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], false); + + $encoded = "19\r\nThis is a chunked message\r\nE\r\n\nThat is cool\n\r\n0\r\nfoo-header: bar\r\ncake: PHP\r\n\r\n"; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], array('Foo-Header' => 'bar', 'Cake' => 'PHP')); + + $encoded = "19\r\nThis is a chunked message\r\nE\r\n\nThat is cool\n\r\n"; + $this->expectError(new PatternExpectation('/activate quirks mode/i')); + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r, false); + + $this->Socket->quirksMode = true; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], false); + + $encoded = "19\r\nThis is a chunked message\r\nE\r\n\nThat is cool\n\r\nfoo-header: bar\r\ncake: PHP\r\n\r\n"; + $r = $this->Socket->decodeChunkedBody($encoded); + $this->assertIdentical($r['body'], $decoded); + $this->assertIdentical($r['header'], array('Foo-Header' => 'bar', 'Cake' => 'PHP')); + } + + function testBuildRequestLine() { + $this->Socket->reset(); + + $this->expectError(new PatternExpectation('/activate quirks mode/i')); + $r = $this->Socket->buildRequestLine('Foo'); + $this->assertIdentical($r, false); + + $this->Socket->quirksMode = true; + $r = $this->Socket->buildRequestLine('Foo'); + $this->assertIdentical($r, 'Foo'); + $this->Socket->quirksMode = false; + + $r = $this->Socket->buildRequestLine(true); + $this->assertIdentical($r, false); + + $r = $this->Socket->buildRequestLine(array('foo' => 'bar', 'method' => 'foo')); + $this->assertIdentical($r, false); + + $r = $this->Socket->buildRequestLine(array('method' => 'GET', 'uri' => 'http://www.cakephp.org/search?q=socket')); + $this->assertIdentical($r, "GET /search?q=socket HTTP/1.1\r\n"); + + $request = array( + 'method' => 'GET', + 'uri' => array( + 'path' => '/search', + 'query' => array('q' => 'socket') + ) + ); + $r = $this->Socket->buildRequestLine($request); + $this->assertIdentical($r, "GET /search?q=socket HTTP/1.1\r\n"); + + unset($request['method']); + $r = $this->Socket->buildRequestLine($request); + $this->assertIdentical($r, "GET /search?q=socket HTTP/1.1\r\n"); + + $r = $this->Socket->buildRequestLine($request, 'CAKE-HTTP/0.1'); + $this->assertIdentical($r, "GET /search?q=socket CAKE-HTTP/0.1\r\n"); + + $request = array('method' => 'OPTIONS', 'uri' => '*'); + $r = $this->Socket->buildRequestLine($request); + $this->assertIdentical($r, "OPTIONS * HTTP/1.1\r\n"); + + $request['method'] = 'GET'; + $this->expectError(new PatternExpectation('/activate quirks mode/i')); + $r = $this->Socket->buildRequestLine($request); + $this->assertIdentical($r, false); + + $this->expectError(new PatternExpectation('/activate quirks mode/i')); + $r = $this->Socket->buildRequestLine("GET * HTTP/1.1\r\n"); + $this->assertIdentical($r, false); + + $this->Socket->quirksMode = true; + $r = $this->Socket->buildRequestLine($request); + $this->assertIdentical($r, "GET * HTTP/1.1\r\n"); + + $r = $this->Socket->buildRequestLine("GET * HTTP/1.1\r\n"); + $this->assertIdentical($r, "GET * HTTP/1.1\r\n"); + } + +/** + * Asserts that HttpSocket::parseUri is working properly + * + */ + function testParseUri() { + $this->Socket->reset(); + + $uri = $this->Socket->parseUri(array('invalid' => 'uri-string')); + $this->assertIdentical($uri, false); + + $uri = $this->Socket->parseUri(array('invalid' => 'uri-string'), array('host' => 'somehost')); + $this->assertIdentical($uri, array('host' => 'somehost', 'invalid' => 'uri-string')); + + $uri = $this->Socket->parseUri(false); + $this->assertIdentical($uri, false); + + $uri = $this->Socket->parseUri('/my-cool-path'); + $this->assertIdentical($uri, array('path' => '/my-cool-path')); + + $uri = $this->Socket->parseUri('http://bob:foo123@www.cakephp.org:40/search?q=dessert#results'); + $this->assertIdentical($uri, array( + 'scheme' => 'http', + 'host' => 'www.cakephp.org', + 'port' => 40, + 'user' => 'bob', + 'pass' => 'foo123', + 'path' => '/search', + 'query' => array('q' => 'dessert'), + 'fragment' => 'results' + )); + + $uri = $this->Socket->parseUri('http://www.cakephp.org/'); + $this->assertIdentical($uri, array( + 'scheme' => 'http', + 'host' => 'www.cakephp.org', + 'path' => '/', + )); + + $uri = $this->Socket->parseUri('http://www.cakephp.org', true); + $this->assertIdentical($uri, array( + 'scheme' => 'http', + 'host' => 'www.cakephp.org', + 'port' => 80, + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => array(), + 'fragment' => null + )); + + $uri = $this->Socket->parseUri('https://www.cakephp.org', true); + $this->assertIdentical($uri, array( + 'scheme' => 'https', + 'host' => 'www.cakephp.org', + 'port' => 443, + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => array(), + 'fragment' => null + )); + + $uri = $this->Socket->parseUri('www.cakephp.org:443/query?foo', true); + $this->assertIdentical($uri, array( + 'scheme' => 'https', + 'host' => 'www.cakephp.org', + 'port' => 443, + 'user' => null, + 'pass' => null, + 'path' => '/query', + 'query' => array('foo' => null), + 'fragment' => null + )); + + $uri = $this->Socket->parseUri('http://www.cakephp.org', array('host' => 'piephp.org', 'user' => 'bob', 'fragment' => 'results')); + $this->assertIdentical($uri, array( + 'host' => 'www.cakephp.org', + 'user' => 'bob', + 'fragment' => 'results', + 'scheme' => 'http' + )); + + $uri = $this->Socket->parseUri('https://www.cakephp.org', array('scheme' => 'http', 'port' => 23)); + $this->assertIdentical($uri, array( + 'scheme' => 'https', + 'port' => 23, + 'host' => 'www.cakephp.org' + )); + + $uri = $this->Socket->parseUri('www.cakephp.org:59', array('scheme' => array('http', 'https'), 'port' => 80)); + $this->assertIdentical($uri, array( + 'scheme' => 'http', + 'port' => 59, + 'host' => 'www.cakephp.org' + )); + } + +/** + * Tests that HttpSocket::buildUri can turn all kinds of uri arrays (and strings) into fully or partially qualified URI's + * + */ + function testBuildUri() { + $this->Socket->reset(); + + $r = $this->Socket->buildUri(true); + $this->assertIdentical($r, false); + + $r = $this->Socket->buildUri('foo.com'); + $this->assertIdentical($r, 'http://foo.com/'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org')); + $this->assertIdentical($r, 'http://www.cakephp.org/'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'scheme' => 'https')); + $this->assertIdentical($r, 'https://www.cakephp.org/'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'port' => 23)); + $this->assertIdentical($r, 'http://www.cakephp.org:23/'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'scheme' => 'https', 'port' => 79)); + $this->assertIdentical($r, 'https://www.cakephp.org:79/'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'path' => 'foo')); + $this->assertIdentical($r, 'http://www.cakephp.org/foo'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'path' => '/foo')); + $this->assertIdentical($r, 'http://www.cakephp.org/foo'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'path' => '/search', 'query' => array('q' => 'HttpSocket'))); + $this->assertIdentical($r, 'http://www.cakephp.org/search?q=HttpSocket'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'fragment' => 'bar')); + $this->assertIdentical($r, 'http://www.cakephp.org/#bar'); + + $r = $this->Socket->buildUri(array( + 'scheme' => 'https', + 'host' => 'www.cakephp.org', + 'port' => 25, + 'user' => 'bob', + 'pass' => 'secret', + 'path' => '/cool', + 'query' => array('foo' => 'bar'), + 'fragment' => 'comment' + )); + $this->assertIdentical($r, 'https://bob:secret@www.cakephp.org:25/cool?foo=bar#comment'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org', 'fragment' => 'bar'), '%fragment?%host'); + $this->assertIdentical($r, 'bar?www.cakephp.org'); + + $r = $this->Socket->buildUri(array('host' => 'www.cakephp.org'), '%fragment???%host'); + $this->assertIdentical($r, '???www.cakephp.org'); + + $r = $this->Socket->buildUri(array('path' => '*'), '/%path?%query'); + $this->assertIdentical($r, '*'); + + $r = $this->Socket->buildUri(array('scheme' => 'foo', 'host' => 'www.cakephp.org')); + $this->assertIdentical($r, 'foo://www.cakephp.org:80/'); + } + +/** + * Asserts that HttpSocket::parseQuery is working properly + * + */ + function testParseQuery() { + $this->Socket->reset(); + + $query = $this->Socket->parseQuery(array('framework' => 'cakephp')); + $this->assertIdentical($query, array('framework' => 'cakephp')); + + $query = $this->Socket->parseQuery(''); + $this->assertIdentical($query, array()); + + $query = $this->Socket->parseQuery('framework=cakephp'); + $this->assertIdentical($query, array('framework' => 'cakephp')); + + $query = $this->Socket->parseQuery('?framework=cakephp'); + $this->assertIdentical($query, array('framework' => 'cakephp')); + + $query = $this->Socket->parseQuery('a&b&c'); + $this->assertIdentical($query, array('a' => null, 'b' => null, 'c' => null)); + + $query = $this->Socket->parseQuery('value=12345'); + $this->assertIdentical($query, array('value' => 12345)); + + $query = $this->Socket->parseQuery('a[0]=foo&a[1]=bar&a[2]=cake'); + $this->assertIdentical($query, array('a' => array(0 => 'foo', 1 => 'bar', 2 => 'cake'))); + + $query = $this->Socket->parseQuery('a[]=foo&a[]=bar&a[]=cake'); + $this->assertIdentical($query, array('a' => array(0 => 'foo', 1 => 'bar', 2 => 'cake'))); + + $query = $this->Socket->parseQuery('a]][[=foo&[]=bar&]]][]=cake'); + $this->assertIdentical($query, array('a]][[' => 'foo', 0 => 'bar', ']]]' => array('cake'))); + + $query = $this->Socket->parseQuery('a[][]=foo&a[][]=bar&a[][]=cake'); + $expectedQuery = array( + 'a' => array( + 0 => array( + 0 => 'foo' + ), + 1 => array( + 0 => 'bar' + ), + array( + 0 => 'cake' + ) + ) + ); + $this->assertIdentical($query, $expectedQuery); + + $query = $this->Socket->parseQuery('a[][]=foo&a[bar]=php&a[][]=bar&a[][]=cake'); + $expectedQuery = array( + 'a' => array( + 0 => array( + 0 => 'foo' + ), + 'bar' => 'php', + 1 => array( + 0 => 'bar' + ), + array( + 0 => 'cake' + ) + ) + ); + $this->assertIdentical($query, $expectedQuery); + + $query = $this->Socket->parseQuery('user[]=jim&user[3]=tom&user[]=bob'); + $expectedQuery = array( + 'user' => array( + 0 => 'jim', + 3 => 'tom', + 4 => 'bob' + ) + ); + $this->assertIdentical($query, $expectedQuery); + + $queryStr = 'user[0]=foo&user[0][items][]=foo&user[0][items][]=bar&user[][name]=jim&user[1][items][personal][]=book&user[1][items][personal][]=pen&user[1][items][]=ball&user[count]=2&empty'; + $query = $this->Socket->parseQuery($queryStr); + $expectedQuery = array( + 'user' => array( + 0 => array( + 'items' => array( + 'foo', + 'bar' + ) + ), + 1 => array( + 'name' => 'jim', + 'items' => array( + 'personal' => array( + 'book' + , 'pen' + ), + 'ball' + ) + ), + 'count' => 2 + ), + 'empty' => null + ); + $this->assertIdentical($query, $expectedQuery); + } + +/** + * Tests that HttpSocket::buildHeader can turn a given $header array into a proper header string according to + * HTTP 1.1 specs. + * + */ + function testBuildHeader() { + $this->Socket->reset(); + + $r = $this->Socket->buildHeader(true); + $this->assertIdentical($r, false); + + $r = $this->Socket->buildHeader('My raw header'); + $this->assertIdentical($r, 'My raw header'); + + $r = $this->Socket->buildHeader(array('Host' => 'www.cakephp.org')); + $this->assertIdentical($r, "Host: www.cakephp.org\r\n"); + + $r = $this->Socket->buildHeader(array('Host' => 'www.cakephp.org', 'Connection' => 'Close')); + $this->assertIdentical($r, "Host: www.cakephp.org\r\nConnection: Close\r\n"); + + $r = $this->Socket->buildHeader(array('People' => array('Bob', 'Jim', 'John'))); + $this->assertIdentical($r, "People: Bob,Jim,John\r\n"); + + $r = $this->Socket->buildHeader(array('Multi-Line-Field' => "This is my\r\nMulti Line field")); + $this->assertIdentical($r, "Multi-Line-Field: This is my\r\n Multi Line field\r\n"); + + $r = $this->Socket->buildHeader(array('Multi-Line-Field' => "This is my\r\n Multi Line field")); + $this->assertIdentical($r, "Multi-Line-Field: This is my\r\n Multi Line field\r\n"); + + $r = $this->Socket->buildHeader(array('Multi-Line-Field' => "This is my\r\n\tMulti Line field")); + $this->assertIdentical($r, "Multi-Line-Field: This is my\r\n\tMulti Line field\r\n"); + + $r = $this->Socket->buildHeader(array('Test@Field' => "My value")); + $this->assertIdentical($r, "Test\"@\"Field: My value\r\n"); + + } + +/** + * Test that HttpSocket::parseHeader can take apart a given (and valid) $header string and turn it into an array. + * + */ + function testParseHeader() { + $this->Socket->reset(); + + $r = $this->Socket->parseHeader(array('foo' => 'Bar', 'fOO-bAr' => 'quux')); + $this->assertIdentical($r, array('Foo' => 'Bar', 'Foo-Bar' => 'quux')); + + $r = $this->Socket->parseHeader(true); + $this->assertIdentical($r, false); + + $header = "Host: cakephp.org\t\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'Host' => 'cakephp.org' + ); + $this->assertIdentical($r, $expected); + + $header = "Date:Sat, 07 Apr 2007 10:10:25 GMT\r\nX-Powered-By: PHP/5.1.2\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'Date' => 'Sat, 07 Apr 2007 10:10:25 GMT' + , 'X-Powered-By' => 'PHP/5.1.2' + ); + $this->assertIdentical($r, $expected); + + $header = "people: Jim,John\r\nfoo-LAND: Bar\r\ncAKe-PHP: rocks\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'People' => 'Jim,John' + , 'Foo-Land' => 'Bar' + , 'Cake-Php' => 'rocks' + ); + $this->assertIdentical($r, $expected); + + $header = "People: Jim,John,Tim\r\nPeople: Lisa,Tina,Chelsea\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'People' => 'Jim,John,Tim,Lisa,Tina,Chelsea' + ); + $this->assertIdentical($r, $expected); + + $header = "Multi-Line: I am a \r\nmulti line\t\r\nfield value.\r\nSingle-Line: I am not\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'Multi-Line' => "I am a\r\nmulti line\r\nfield value." + , 'Single-Line' => 'I am not' + ); + $this->assertIdentical($r, $expected); + + $header = "Esc\"@\"ped: value\r\n"; + $r = $this->Socket->parseHeader($header); + $expected = array( + 'Esc@ped' => 'value' + ); + $this->assertIdentical($r, $expected); + } + +/** + * Tests that HttpSocket::__tokenEscapeChars() returns the right characters. + * + */ + function testTokenEscapeChars() { + $this->Socket->reset(); + + $expected = array('\x22','\x28','\x29','\x3c','\x3e','\x40','\x2c','\x3b','\x3a','\x5c','\x2f','\x5b','\x5d','\x3f','\x3d','\x7b', + '\x7d','\x20','\x00','\x01','\x02','\x03','\x04','\x05','\x06','\x07','\x08','\x09','\x0a','\x0b','\x0c','\x0d', + '\x0e','\x0f','\x10','\x11','\x12','\x13','\x14','\x15','\x16','\x17','\x18','\x19','\x1a','\x1b','\x1c','\x1d', + '\x1e','\x1f','\x7f'); + $r = $this->Socket->__tokenEscapeChars(); + $this->assertEqual($r, $expected); + + foreach ($expected as $key => $char) { + $expected[$key] = chr(hexdec(substr($char, 2))); + } + + $r = $this->Socket->__tokenEscapeChars(false); + $this->assertEqual($r, $expected); + } + +/** + * Test that HttpSocket::escapeToken is escaping all characters as descriped in RFC 2616 (HTTP 1.1 specs) + * + */ + function testEscapeToken() { + $this->Socket->reset(); + + $this->assertIdentical($this->Socket->escapeToken('Foo'), 'Foo'); + + $escape = $this->Socket->__tokenEscapeChars(false); + foreach ($escape as $char) { + $token = 'My-special-'.$char.'-Token'; + $escapedToken = $this->Socket->escapeToken($token); + $expectedToken = 'My-special-"'.$char.'"-Token'; + + $this->assertIdentical($escapedToken, $expectedToken, 'Test token escaping for ASCII '.ord($char)); + } + + $token = 'Extreme-:Token- -"@-test'; + $escapedToken = $this->Socket->escapeToken($token); + $expectedToken = 'Extreme-":"Token-" "-""""@"-test'; + $this->assertIdentical($expectedToken, $escapedToken); + } + +/** + * Test that escaped token strings are properly unescaped by HttpSocket::unescapeToken + * + */ + function testUnescapeToken() { + $this->Socket->reset(); + + $this->assertIdentical($this->Socket->unescapeToken('Foo'), 'Foo'); + + $escape = $this->Socket->__tokenEscapeChars(false); + foreach ($escape as $char) { + $token = 'My-special-"'.$char.'"-Token'; + $unescapedToken = $this->Socket->unescapeToken($token); + $expectedToken = 'My-special-'.$char.'-Token'; + + $this->assertIdentical($unescapedToken, $expectedToken, 'Test token unescaping for ASCII '.ord($char)); + } + + $token = 'Extreme-":"Token-" "-""""@"-test'; + $escapedToken = $this->Socket->unescapeToken($token); + $expectedToken = 'Extreme-:Token- -"@-test'; + $this->assertIdentical($expectedToken, $escapedToken); + } + +/** + * This tests asserts HttpSocket::reset() resets a HttpSocket instance to it's initial state (before Object::__construct + * got executed) + * + */ + function testReset() { + $this->Socket->reset(); + + $initialState = get_class_vars('HttpSocket'); + foreach ($initialState as $property => $value) { + $this->Socket->{$property} = 'Overwritten'; + } + + $return = $this->Socket->reset(); + + foreach ($initialState as $property => $value) { + $this->assertIdentical($this->Socket->{$property}, $value); + } + + $this->assertIdentical($return, true); + } + +/** + * This tests asserts HttpSocket::reset(false) resets certain HttpSocket properties to their initial state (before + * Object::__construct got executed). + * + */ + function testPartialReset() { + $this->Socket->reset(); + + $partialResetProperties = array('request', 'response'); + $initialState = get_class_vars('HttpSocket'); + + foreach ($initialState as $property => $value) { + $this->Socket->{$property} = 'Overwritten'; + } + + $return = $this->Socket->reset(false); + + foreach ($initialState as $property => $originalValue) { + if (in_array($property, $partialResetProperties)) { + $this->assertIdentical($this->Socket->{$property}, $originalValue); + } else { + $this->assertIdentical($this->Socket->{$property}, 'Overwritten'); + } + } + $this->assertIdentical($return, true); + } +} + +?> \ No newline at end of file diff --git a/cake/tests/cases/libs/socket.test.php b/cake/tests/cases/libs/socket.test.php index 40f9e9bfe..e5943fba4 100644 --- a/cake/tests/cases/libs/socket.test.php +++ b/cake/tests/cases/libs/socket.test.php @@ -40,7 +40,7 @@ class SocketTest extends UnitTestCase { } function testSocketConnection() { - $this->assertTrue($this->socket->connected); + $this->assertFalse($this->socket->connected); $this->socket->disconnect(); $this->assertFalse($this->socket->connected); $this->socket->connect(); diff --git a/cake/tests/lib/cake_test_case.php b/cake/tests/lib/cake_test_case.php index 72e80beae..ba5a0cd5f 100644 --- a/cake/tests/lib/cake_test_case.php +++ b/cake/tests/lib/cake_test_case.php @@ -428,7 +428,7 @@ class CakeTestCase extends UnitTestCase { $config = $db->config; $config['prefix'] .= 'test_suite_'; - // Set up db connection + // Set up db connection ConnectionManager::create('test_suite', $config); // Get db connection