From c0de5dd11e24bf671a8482e687049d0a257c327d Mon Sep 17 00:00:00 2001 From: Simon Erkelens Date: Fri, 6 May 2016 12:20:14 +1200 Subject: [PATCH] RFC #5347 remove RestfulService from SS4. Rebuild OEmbed on using curl --- api/RestfulService.php | 656 ------------------------------- oembed/Oembed.php | 49 ++- tests/api/RestfulServiceTest.php | 487 ----------------------- 3 files changed, 39 insertions(+), 1153 deletions(-) delete mode 100644 api/RestfulService.php delete mode 100644 tests/api/RestfulServiceTest.php diff --git a/api/RestfulService.php b/api/RestfulService.php deleted file mode 100644 index 517459976..000000000 --- a/api/RestfulService.php +++ /dev/null @@ -1,656 +0,0 @@ -update('RestfulService', 'default_curl_options', array($option => $value)); - } - - /** - * set many defauly curl options at once - * - * @deprecated 4.0 Use the "RestfulService.default_curl_options" config setting instead - */ - public static function set_default_curl_options($optionArray) { - Deprecation::notice('4.0', 'Use the "RestfulService.default_curl_options" config setting instead'); - Config::inst()->update('RestfulService', 'default_curl_options', $optionArray); - } - - /** - * Sets default proxy settings for outbound RestfulService connections - * - * @param string $proxy The URL of the proxy to use. - * @param int $port Proxy port - * @param string $user The proxy auth user name - * @param string $password The proxy auth password - * @param boolean $socks Set true to use socks5 proxy instead of http - * @deprecated 4.0 Use the "RestfulService.default_curl_options" config setting instead, - * with direct reference to the CURL_* options - */ - public static function set_default_proxy($proxy, $port = 80, $user = "", $password = "", $socks = false) { - Deprecation::notice( - '4.0', - 'Use the "RestfulService.default_curl_options" config setting instead, ' - . 'with direct reference to the CURL_* options' - ); - config::inst()->update('RestfulService', 'default_proxy', array( - CURLOPT_PROXY => $proxy, - CURLOPT_PROXYUSERPWD => "{$user}:{$password}", - CURLOPT_PROXYPORT => $port, - CURLOPT_PROXYTYPE => ($socks ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP) - )); - } - - /** - * Creates a new restful service. - * @param string $base Base URL of the web service eg: api.example.com - * @param int $expiry Set the cache expiry interva. Defaults to 1 hour (3600 seconds) - */ - public function __construct($base, $expiry=3600){ - $this->baseURL = $base; - $this->cache_expire = $expiry; - parent::__construct(); - $this->proxy = $this->config()->default_proxy; - } - - /** - * Sets the Query string parameters to send a request. - * @param array $params An array passed with necessary parameters. - */ - public function setQueryString($params=NULL){ - $this->queryString = http_build_query($params,'','&'); - } - - /** - * Set proxy settings for this RestfulService instance - * - * @param string $proxy The URL of the proxy to use. - * @param int $port Proxy port - * @param string $user The proxy auth user name - * @param string $password The proxy auth password - * @param boolean $socks Set true to use socks5 proxy instead of http - */ - public function setProxy($proxy, $port = 80, $user = "", $password = "", $socks = false) { - $this->proxy = array( - CURLOPT_PROXY => $proxy, - CURLOPT_PROXYUSERPWD => "{$user}:{$password}", - CURLOPT_PROXYPORT => $port, - CURLOPT_PROXYTYPE => ($socks ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP) - ); - } - - /** - * Set basic authentication - */ - public function basicAuth($username, $password) { - $this->authUsername = $username; - $this->authPassword = $password; - } - - /** - * Set a custom HTTP header - */ - public function httpHeader($header) { - $this->customHeaders[] = $header; - } - - /** - * @deprecated since version 4.0 - */ - protected function constructURL(){ - Deprecation::notice('4.0', 'constructURL is deprecated, please use `getAbsoluteRequestURL` instead'); - return Controller::join_links($this->baseURL, '?' . $this->queryString); - } - - /** - * Makes a request to the RESTful server, and return a {@link RestfulService_Response} object for parsing of the - * result. - * - * @todo Better POST, PUT, DELETE, and HEAD support - * @todo Caching of requests - probably only GET and HEAD requestst - * @todo JSON support in RestfulService_Response - * @todo Pass the response headers to RestfulService_Response - * - * This is a replacement of {@link connect()}. - * - * @return RestfulService_Response - If curl request produces error, the returned response's status code will - * be 500 - */ - public function request($subURL = '', $method = "GET", $data = null, $headers = null, $curlOptions = array()) { - - $url = $this->getAbsoluteRequestURL($subURL); - $method = strtoupper($method); - - assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS','PATCH'))); - - $cache_path = $this->getCachePath(array( - $url, - $method, - $data, - array_merge((array)$this->customHeaders, (array)$headers), - $curlOptions + (array)$this->config()->default_curl_options, - $this->getBasicAuthString() - )); - - // Check for unexpired cached feed (unless flush is set) - //assume any cache_expire that is 0 or less means that we dont want to - // cache - if($this->cache_expire > 0 && !self::$flush - && @file_exists($cache_path) - && @filemtime($cache_path) + $this->cache_expire > time()) { - - $store = file_get_contents($cache_path); - $response = unserialize($store); - - } else { - $response = $this->curlRequest($url, $method, $data, $headers, $curlOptions); - - if(!$response->isError()) { - // Serialise response object and write to cache - $store = serialize($response); - file_put_contents($cache_path, $store); - } - else { - // In case of curl or/and http indicate error, populate response's cachedBody property - // with cached response body with the cache file exists - if (@file_exists($cache_path)) { - $store = file_get_contents($cache_path); - $cachedResponse = unserialize($store); - - $response->setCachedResponse($cachedResponse); - } - else { - $response->setCachedResponse(false); - } - } - } - - return $response; - } - - /** - * Actually performs a remote service request using curl. This is used by - * {@link RestfulService::request()}. - * - * @param string $url - * @param string $method - * @param array $data - * @param array $headers - * @param array $curlOptions - * @return RestfulService_Response - */ - public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) { - $ch = curl_init(); - $timeout = 5; - $sapphireInfo = new SapphireInfo(); - $useragent = 'SilverStripe/' . $sapphireInfo->Version(); - $curlOptions = $curlOptions + (array)$this->config()->default_curl_options; - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_USERAGENT, $useragent); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - - - // Write headers to a temporary file - $headerfd = tmpfile(); - curl_setopt($ch, CURLOPT_WRITEHEADER, $headerfd); - - // Add headers - if($this->customHeaders) { - $headers = array_merge((array)$this->customHeaders, (array)$headers); - } - - if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - // Add authentication - if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, $this->getBasicAuthString()); - - // Add fields to POST and PUT requests - if($method == 'POST' || $method == 'PATCH') { - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - } elseif($method == 'PUT') { - $put = fopen("php://temp", 'r+'); - fwrite($put, $data); - fseek($put, 0); - - curl_setopt($ch, CURLOPT_PUT, 1); - curl_setopt($ch, CURLOPT_INFILE, $put); - curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data)); - } - - // Apply proxy settings - if(is_array($this->proxy)) { - curl_setopt_array($ch, $this->proxy); - } - - // Set any custom options passed to the request() function - curl_setopt_array($ch, $curlOptions); - - // Run request - $body = curl_exec($ch); - - rewind($headerfd); - $headers = stream_get_contents($headerfd); - fclose($headerfd); - - $response = $this->extractResponse($ch, $headers, $body); - curl_close($ch); - - return $response; - } - - /** - * A function to return the auth string. This helps consistency through the - * class but also allows tests to pull it out when generating the expected - * cache keys - * - * @see {self::getCachePath()} - * @see {RestfulServiceTest::createFakeCachedResponse()} - * - * @return string The auth string to be base64 encoded - */ - protected function getBasicAuthString() { - return $this->authUsername . ':' . $this->authPassword; - } - - /** - * Generate a cache key based on any cache data sent. The cache data can be - * any type - * - * @param mixed $cacheData The cache seed for generating the key - * @param string the md5 encoded cache seed. - */ - protected function generateCacheKey($cacheData) { - return md5(var_export($cacheData, true)); - } - - /** - * Generate the cache path - * - * This is mainly so that the cache path can be generated in a consistent - * way in tests without having to hard code the cachekey generate function - * in tests - * - * @param mixed $cacheData The cache seed {@see self::generateCacheKey} - * - * @return string The path to the cache file - */ - protected function getCachePath($cacheData) { - return TEMP_FOLDER . "/xmlresponse_" . $this->generateCacheKey($cacheData); - } - - /** - * Extracts the response body and headers from a full curl response - * - * @param curl_handle $ch The curl handle for the request - * @param string $rawResponse The raw response text - * - * @return RestfulService_Response The response object - */ - protected function extractResponse($ch, $rawHeaders, $rawBody) { - //get the status code - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - //get a curl error if there is one - $curlError = curl_error($ch); - //normalise the status code - if(curl_error($ch) !== '' || $statusCode == 0) $statusCode = 500; - //parse the headers - $parts = array_filter(explode("\r\n\r\n", $rawHeaders)); - $lastHeaders = array_pop($parts); - $headers = $this->parseRawHeaders($lastHeaders); - //return the response object - return new RestfulService_Response($rawBody, $statusCode, $headers); - } - - /** - * Takes raw headers and parses them to turn them to an associative array - * - * Any header that we see more than once is turned into an array. - * - * This is meant to mimic http_parse_headers {@link http://php.net/manual/en/function.http-parse-headers.php} - * thanks to comment #77241 on that page for foundation of this - * - * @param string $rawHeaders The raw header string - * @return array The assosiative array of headers - */ - protected function parseRawHeaders($rawHeaders) { - $headers = array(); - $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeaders)); - foreach( $fields as $field ) { - if( preg_match('/([^:]+): (.+)/m', $field, $match) ) { - $match[1] = preg_replace_callback( - '/(?<=^|[\x09\x20\x2D])./', - create_function('$matches', 'return strtoupper($matches[0]);'), - trim($match[1]) - ); - if( isset($headers[$match[1]]) ) { - if (!is_array($headers[$match[1]])) { - $headers[$match[1]] = array($headers[$match[1]]); - } - $headers[$match[1]][] = $match[2]; - } else { - $headers[$match[1]] = trim($match[2]); - } - } - } - return $headers; - } - - - /** - * Returns a full request url - * @param string - */ - public function getAbsoluteRequestURL($subURL = '') { - $url = Controller::join_links($this->baseURL, $subURL, '?' . $this->queryString); - - return str_replace(' ', '%20', $url); // Encode spaces - } - - /** - * Gets attributes as an array, of a particular type of element. - * Example : - * returns id, owner,secret and sever attribute values of all such photo elements. - * @param string $xml The source xml to parse, this could be the original response received. - * @param string $collection The name of parent node which wraps the elements, if available - * @param string $element The element we need to extract the attributes. - */ - - public function getAttributes($xml, $collection=NULL, $element=NULL){ - $xml = new SimpleXMLElement($xml); - $output = new ArrayList(); - - if($collection) - $childElements = $xml->{$collection}; - if($element) - $childElements = $xml->{$collection}->{$element}; - - if($childElements){ - foreach($childElements as $child){ - $data = array(); - foreach($child->attributes() as $key => $value){ - $data["$key"] = Convert::raw2xml($value); - } - $output->push(new ArrayData($data)); - } - } - return $output; - - } - - /** - * Gets an attribute of a particular element. - * @param string $xml The source xml to parse, this could be the original response received. - * @param string $collection The name of the parent node which wraps the element, if available - * @param string $element The element we need to extract the attribute - * @param string $attr The name of the attribute - */ - - public function getAttribute($xml, $collection=NULL, $element=NULL, $attr){ - $xml = new SimpleXMLElement($xml); - $attr_value = ""; - - if($collection) - $childElements = $xml->{$collection}; - if($element) - $childElements = $xml->{$collection}->{$element}; - - if($childElements) - $attr_value = (string) $childElements[$attr]; - - return Convert::raw2xml($attr_value); - - } - - - /** - * Gets set of node values as an array. - * When you get to the depth in the hierarchy use node_child_subchild syntax to get the value. - * @param string $xml The the source xml to parse, this could be the original response received. - * @param string $collection The name of parent node which wraps the elements, if available - * @param string $element The element we need to extract the node values. - */ - - public function getValues($xml, $collection=NULL, $element=NULL){ - $xml = new SimpleXMLElement($xml); - $output = new ArrayList(); - - $childElements = $xml; - if($collection) - $childElements = $xml->{$collection}; - if($element) - $childElements = $xml->{$collection}->{$element}; - - if($childElements){ - foreach($childElements as $child){ - $data = array(); - $this->getRecurseValues($child,$data); - $output->push(new ArrayData($data)); - } - } - return $output; - } - - protected function getRecurseValues($xml,&$data,$parent=""){ - $conv_value = ""; - $child_count = 0; - foreach($xml as $key=>$value) - { - $child_count++; - $k = ($parent == "") ? (string)$key : $parent . "_" . (string)$key; - if($this->getRecurseValues($value,$data,$k) == 0){ // no childern, aka "leaf node" - $conv_value = Convert::raw2xml($value); - } - //Review the fix for similar node names overriding it's predecessor - if(array_key_exists($k, $data) == true) { - $data[$k] = $data[$k] . ",". $conv_value; - } - else { - $data[$k] = $conv_value; - } - - - } - return $child_count; - - } - - /** - * Gets a single node value. - * @param string $xml The source xml to parse, this could be the original response received. - * @param string $collection The name of parent node which wraps the elements, if available - * @param string $element The element we need to extract the node value. - */ - - public function getValue($xml, $collection=NULL, $element=NULL){ - $xml = new SimpleXMLElement($xml); - - if($collection) - $childElements = $xml->{$collection}; - if($element) - $childElements = $xml->{$collection}->{$element}; - - if($childElements) - return Convert::raw2xml($childElements); - } - - /** - * Searches for a node in document tree and returns it value. - * @param string $xml source xml to parse, this could be the original response received. - * @param string $node Node to search for - */ - public function searchValue($xml, $node=NULL){ - $xml = new SimpleXMLElement($xml); - $childElements = $xml->xpath($node); - - if($childElements) - return Convert::raw2xml($childElements[0]); - } - - /** - * Searches for a node in document tree and returns its attributes. - * @param string $xml the source xml to parse, this could be the original response received. - * @param string $node Node to search for - */ - public function searchAttributes($xml, $node=NULL){ - $xml = new SimpleXMLElement($xml); - $output = new ArrayList(); - - $childElements = $xml->xpath($node); - - if($childElements) - foreach($childElements as $child){ - $data = array(); - foreach($child->attributes() as $key => $value){ - $data["$key"] = Convert::raw2xml($value); - } - - $output->push(new ArrayData($data)); - } - - return $output; - } -} - -/** - * @package framework - * @subpackage integration - */ -class RestfulService_Response extends SS_HTTPResponse { - protected $simpleXML; - - /** - * @var boolean It should be populated with cached request - * when a request referring to this response was unsuccessful - */ - protected $cachedResponse = false; - - public function __construct($body, $statusCode = 200, $headers = null) { - $this->setbody($body); - $this->setStatusCode($statusCode); - $this->headers = $headers; - } - - public function simpleXML() { - if(!$this->simpleXML) { - try { - $this->simpleXML = new SimpleXMLElement($this->body); - } - catch(Exception $e) { - user_error("String could not be parsed as XML. " . $e, E_USER_WARNING); - } - } - return $this->simpleXML; - } - - /** - * get the cached response object. This allows you to access the cached - * eaders, not just the cached body. - * - * @return RestfulSerivice_Response The cached response object - */ - public function getCachedResponse() { - return $this->cachedResponse; - } - - /** - * @return string - */ - public function getCachedBody() { - if ($this->cachedResponse) { - return $this->cachedResponse->getBody(); - } - return false; - } - - /** - * @param string - * @deprecated since version 4.0 - */ - public function setCachedBody($content) { - Deprecation::notice('4.0', 'Setting the response body is now deprecated, set the cached request instead'); - if (!$this->cachedResponse) { - $this->cachedResponse = new RestfulService_Response($content); - } - else { - $this->cachedResponse->setBody($content); - } - } - - /** - * @param string - */ - public function setCachedResponse($response) { - $this->cachedResponse = $response; - } - - /** - * Return an array of xpath matches - */ - public function xpath($xpath) { - return $this->simpleXML()->xpath($xpath); - } - - /** - * Return the first xpath match - */ - public function xpath_one($xpath) { - $items = $this->xpath($xpath); - if (isset($items[0])) { - return $items[0]; - } - } -} diff --git a/oembed/Oembed.php b/oembed/Oembed.php index 312095cb0..f2b7f1a6c 100644 --- a/oembed/Oembed.php +++ b/oembed/Oembed.php @@ -103,13 +103,28 @@ class Oembed implements ShortcodeHandler { */ protected static function autodiscover_from_url($url) { - // Fetch the URL (cache for a week by default) - $service = new RestfulService($url, 60 * 60 * 24 * 7); - $body = $service->request(); - if (!$body || $body->isError()) { + $timeout = 5; + $sapphireInfo = new SapphireInfo(); + $useragent = 'SilverStripe/' . $sapphireInfo->Version(); + $curlRequest = curl_init(); + curl_setopt_array( + $curlRequest, + array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_USERAGENT => $useragent, + CURLOPT_CONNECTTIMEOUT => $timeout, + CURLOPT_FOLLOWLOCATION => 1, + + ) + ); + + $response = curl_exec($curlRequest); + $headers = curl_getinfo($curlRequest); + if(!$response || $headers['http_code'] !== 200) { return false; } - $body = $body->getBody(); + $body = $response; return static::autodiscover_from_body($body); } @@ -283,15 +298,29 @@ class Oembed_Result extends ViewableData { if($this->data !== false) { return; } + $timeout = 5; + $sapphireInfo = new SapphireInfo(); + $useragent = 'SilverStripe/' . $sapphireInfo->Version(); + $curlRequest = curl_init(); + curl_setopt_array( + $curlRequest, + array( + CURLOPT_URL => $this->url, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_USERAGENT => $useragent, + CURLOPT_CONNECTTIMEOUT => $timeout, + CURLOPT_FOLLOWLOCATION => 1, - // Fetch from Oembed URL (cache for a week by default) - $service = new RestfulService($this->url, 60*60*24*7); - $body = $service->request(); - if(!$body || $body->isError()) { + ) + ); + + $response = curl_exec($curlRequest); + $headers = curl_getinfo($curlRequest); + if(!$response || $headers['http_code'] !== 200) { $this->data = array(); return; } - $body = $body->getBody(); + $body = $response; $data = json_decode($body, true); if(!$data) { // if the response is no valid JSON we might have received a binary stream to an image diff --git a/tests/api/RestfulServiceTest.php b/tests/api/RestfulServiceTest.php deleted file mode 100644 index aa1ef35c1..000000000 --- a/tests/api/RestfulServiceTest.php +++ /dev/null @@ -1,487 +0,0 @@ -member_unique_identifier_field = Member::config()->unique_identifier_field; - - Member::config()->unique_identifier_field = 'Email'; - - parent::setUp(); - } - - public function tearDown() { - parent::tearDown(); - - // set old Member::config()->unique_identifier_field value - if ($this->member_unique_identifier_field) { - Member::config()->unique_identifier_field = $this->member_unique_identifier_field; - } - } - - /** - * Check we can put slashes anywhere and it works - */ - public function testGetAbsoluteURLSlashes() { - $urls = array( - '/url/', - 'url', - '/url', - 'url/', - ); - $restWithoutSlash = new RestfulService('http://example.com'); - $restWithSlash = new RestfulService('http://example.com/'); - foreach ($urls as $url) { - $url = ltrim($url, '/'); - $this->assertEquals("http://example.com/$url", $restWithoutSlash->getAbsoluteRequestURL($url)); - $this->assertEquals("http://example.com/$url", $restWithSlash->getAbsoluteRequestURL($url)); - $this->assertEquals($restWithoutSlash->getAbsoluteRequestURL($url), $restWithSlash->getAbsoluteRequestURL($url)); - } - } - - /** - * Check we can add query strings all over the shop and it's ok - */ - public function testGetAbsoluteURLQueries() { - $restWithoutSlash = new RestfulService('http://example.com?b=query2'); - $restWithSlash = new RestfulService('http://example.com/?b=query2'); - $restWithQuery = new RestfulService('http://example.com/?b=query2'); - $restWithQuery->setQueryString(array( - 'c' => 'query3', - )); - $this->assertEquals('http://example.com/url?b=query2&a=query1', $restWithoutSlash->getAbsoluteRequestURL('url?a=query1')); - $this->assertEquals('http://example.com/url?b=query2&a=query1', $restWithSlash->getAbsoluteRequestURL('url?a=query1')); - $this->assertEquals('http://example.com/url?b=query2&a=query1&c=query3', $restWithQuery->getAbsoluteRequestURL('url?a=query1')); - - $this->assertEquals('http://example.com/url?b=query2', $restWithoutSlash->getAbsoluteRequestURL('url')); - $this->assertEquals('http://example.com/url?b=query2', $restWithSlash->getAbsoluteRequestURL('url')); - $this->assertEquals('http://example.com/url?b=query2&c=query3', $restWithQuery->getAbsoluteRequestURL('url')); - - $restWithoutSlash = new RestfulService('http://example.com'); - $restWithSlash = new RestfulService('http://example.com/'); - $restWithQuery = new RestfulService('http://example.com/'); - $restWithQuery->setQueryString(array( - 'c' => 'query3', - )); - $this->assertEquals('http://example.com/url?a=query1', $restWithoutSlash->getAbsoluteRequestURL('url?a=query1')); - $this->assertEquals('http://example.com/url?a=query1', $restWithSlash->getAbsoluteRequestURL('url?a=query1')); - $this->assertEquals('http://example.com/url?a=query1&c=query3', $restWithQuery->getAbsoluteRequestURL('url?a=query1')); - - $this->assertEquals('http://example.com/url', $restWithoutSlash->getAbsoluteRequestURL('url')); - $this->assertEquals('http://example.com/url', $restWithSlash->getAbsoluteRequestURL('url')); - $this->assertEquals('http://example.com/url?c=query3', $restWithQuery->getAbsoluteRequestURL('url')); - } - - /** - * Check spaces are encoded - */ - public function testGetAbsoluteURLWithSpaces() { - $rest = new RestfulService('http://example.com'); - $this->assertEquals('http://example.com/query%20with%20spaces', $rest->getAbsoluteRequestURL('query with spaces')); - } - - public function testSpecialCharacters() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL()); - $url = 'RestfulServiceTest_Controller/'; - $params = array( - 'test1a' => 4352655636.76543, // number test - 'test1b' => '$&+,/:;=?@#%', // special char test. These should all get encoded - 'test1c' => 'And now for a string test' // string test - ); - $service->setQueryString($params); - $responseBody = $service->request($url)->getBody(); - foreach ($params as $key => $value) { - $this->assertContains("$value", $responseBody); - $this->assertContains("$value", $responseBody); - } - } - - public function testGetDataWithSetQueryString() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL()); - $url = 'RestfulServiceTest_Controller/'; - $params = array( - 'test1a' => 'val1a', - 'test1b' => 'val1b' - ); - $service->setQueryString($params); - $responseBody = $service->request($url)->getBody(); - foreach ($params as $key => $value) { - $this->assertContains("$value", $responseBody); - $this->assertContains("$value", $responseBody); - } - } - - public function testGetDataWithUrlParameters() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL()); - $url = 'RestfulServiceTest_Controller/'; - $params = array( - 'test1a' => 'val1a', - 'test1b' => 'val1b' - ); - $url .= '?' . http_build_query($params); - $responseBody = $service->request($url)->getBody(); - foreach ($params as $key => $value) { - $this->assertContains("$value", $responseBody); - $this->assertContains("$value", $responseBody); - } - } - - public function testPostData() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL(), 0); - $params = array( - 'test1a' => 'val1a', - 'test1b' => 'val1b' - ); - $responseBody = $service->request('RestfulServiceTest_Controller/', 'POST', $params)->getBody(); - foreach ($params as $key => $value) { - $this->assertContains("$value", $responseBody); - $this->assertContains("$value", $responseBody); - } - } - - public function testPutData() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL(), 0); - $data = 'testPutData'; - $responseBody = $service->request('RestfulServiceTest_Controller/', 'PUT', $data)->getBody(); - $this->assertContains("$data", $responseBody); - } - - public function testConnectionDoesntCacheWithDifferentUrl() { - $service = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL()); - $url = 'RestfulServiceTest_Controller/'; - - // First run - $params = array( - 'test1a' => 'first run', - ); - $service->setQueryString($params); - $responseBody = $service->request($url)->getBody(); - $this->assertContains("first run", $responseBody); - - // Second run - $params = array( - 'test1a' => 'second run', - ); - $service->setQueryString($params); - $responseBody = $service->request($url)->getBody(); - $this->assertContains("second run", $responseBody); - } - - /** - * @expectedException PHPUnit_Framework_Error - */ - public function testIncorrectData() { - $connection = new RestfulService(Director::absoluteBaseURL(), 0); - $test1 = $connection->request('RestfulServiceTest_Controller/invalid'); - $test1->xpath("\\fail"); - } - - public function testHttpErrorWithoutCache() { - $connection = new RestfulServiceTest_MockRestfulService(Director::absoluteBaseURL(), 0); - $response = $connection->request('RestfulServiceTest_Controller/httpErrorWithoutCache'); - - $this->assertEquals(400, $response->getStatusCode()); - $this->assertFalse($response->getCachedBody()); - $this->assertContains("HTTP Error", $response->getBody()); - - } - - public function testHttpErrorWithCache() { - $subUrl = 'RestfulServiceTest_Controller/httpErrorWithCache'; - $connection = new RestfulServiceTest_MockErrorService(Director::absoluteBaseURL(), 0); - $this->createFakeCachedResponse($connection, $subUrl); - $response = $connection->request($subUrl); - $this->assertEquals(400, $response->getStatusCode()); - $this->assertEquals("Cache response body",$response->getCachedBody()); - $this->assertContains("HTTP Error", $response->getBody()); - - } - - /** - * Simulate cached response file for testing error requests that are supposed to have cache files - * - * @todo Generate the cachepath without hardcoding the cache data - */ - private function createFakeCachedResponse($connection, $subUrl) { - $fullUrl = $connection->getAbsoluteRequestURL($subUrl); - //these are the defaul values that one would expect in the - $basicAuthStringMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getBasicAuthString'); - $basicAuthStringMethod->setAccessible(true); - $cachePathMethod = new ReflectionMethod('RestfulServiceTest_MockErrorService', 'getCachePath'); - $cachePathMethod->setAccessible(true); - $cache_path = $cachePathMethod->invokeArgs($connection, array(array( - $fullUrl, - 'GET', - null, - array(), - array(), - $basicAuthStringMethod->invoke($connection) - ))); - - $cacheResponse = new RestfulService_Response("Cache response body"); - $store = serialize($cacheResponse); - file_put_contents($cache_path, $store); - } - - public function testHttpHeaderParseing() { - $headers = "content-type: text/html; charset=UTF-8\r\n". - "Server: Funky/1.0\r\n". - "X-BB-ExampleMANycaPS: test\r\n". - "Set-Cookie: foo=bar\r\n". - "Set-Cookie: baz=quux\r\n". - "Set-Cookie: bar=foo\r\n"; - $expected = array( - 'Content-Type' => 'text/html; charset=UTF-8', - 'Server' => 'Funky/1.0', - 'X-BB-ExampleMANycaPS' => 'test', - 'Set-Cookie' => array( - 'foo=bar', - 'baz=quux', - 'bar=foo' - ) - ); - $headerFunction = new ReflectionMethod('RestfulService', 'parseRawHeaders'); - $headerFunction->setAccessible(true); - $this->assertEquals( - $expected, - $headerFunction->invoke( - new RestfulService(Director::absoluteBaseURL(),0), $headers - ) - ); - } - - public function testExtractResponseRedirectionAndProxy() { - // This is an example of real raw response for a request via a proxy that gets redirected. - $rawHeaders = - "HTTP/1.0 200 Connection established\r\n" . - "\r\n" . - "HTTP/1.1 301 Moved Permanently\r\n" . - "Server: nginx\r\n" . - "Date: Fri, 20 Sep 2013 01:53:07 GMT\r\n" . - "Content-Type: text/html\r\n" . - "Content-Length: 178\r\n" . - "Connection: keep-alive\r\n" . - "Location: https://www.foobar.org.nz/\r\n" . - "\r\n" . - "HTTP/1.0 200 Connection established\r\n" . - "\r\n" . - "HTTP/1.1 200 OK\r\n" . - "Server: nginx\r\n" . - "Date: Fri, 20 Sep 2013 01:53:08 GMT\r\n" . - "Content-Type: text/html; charset=utf-8\r\n" . - "Transfer-Encoding: chunked\r\n" . - "Connection: keep-alive\r\n" . - "X-Frame-Options: SAMEORIGIN\r\n" . - "Cache-Control: no-cache, max-age=0, must-revalidate, no-transform\r\n" . - "Vary: Accept-Encoding\r\n" . - "\r\n" - ; - - $headerFunction = new ReflectionMethod('RestfulService', 'extractResponse'); - $headerFunction->setAccessible(true); - - $ch = curl_init(); - $response = $headerFunction->invoke( - new RestfulService(Director::absoluteBaseURL(),0), - $ch, - $rawHeaders, - '' - ); - - $this->assertEquals( - $response->getHeaders(), - array( - 'Server' => "nginx", - 'Date' => "Fri, 20 Sep 2013 01:53:08 GMT", - 'Content-Type' => "text/html; charset=utf-8", - 'Transfer-Encoding' => "chunked", - 'Connection' => "keep-alive", - 'X-Frame-Options' => "SAMEORIGIN", - 'Cache-Control' => "no-cache, max-age=0, must-revalidate, no-transform", - 'Vary' => "Accept-Encoding" - ), - 'Only last header is extracted and parsed.' - ); - } - - public function testExtractResponseNoHead() { - $headerFunction = new ReflectionMethod('RestfulService', 'extractResponse'); - $headerFunction->setAccessible(true); - - $ch = curl_init(); - $response = $headerFunction->invoke( - new RestfulService(Director::absoluteBaseURL(),0), - $ch, - '', - '' - ); - - $this->assertEquals($response->getHeaders(), array(), 'Headers are correctly extracted.'); - } -} - -class RestfulServiceTest_Controller extends Controller implements TestOnly { - - private static $allowed_actions = array( - 'index', - 'httpErrorWithoutCache', - 'httpErrorWithCache' - ); - - public function init() { - $this->basicAuthEnabled = false; - - parent::init(); - } - - public function index() { - $request = ''; - foreach ($this->getRequest()->requestVars() as $key=>$value) { - $request .= "\t\t$value\n"; - } - $get = ''; - foreach ($this->getRequest()->getVars() as $key => $value) { - $get .= "\t\t$value\n"; - } - $post = ''; - foreach ($this->getRequest()->postVars() as $key => $value) { - $post .= "\t\t$value\n"; - } - $body = $this->getRequest()->getBody(); - - $out = << - - $request - $get - $post - $body - -XML; - $response = $this->getResponse(); - $response->setBody($out); - $response->addHeader('Content-type', 'text/xml'); - - return $response; - } - - public function invalid() { - $out = << - - - -XML; - header('Content-type: text/xml'); - echo $out; - } - - public function httpErrorWithoutCache() { - $out = << - - HTTP Error - -XML; - - $this->getResponse()->setBody($out); - $this->getResponse()->setStatusCode(400); - $this->getResponse()->addHeader('Content-type', 'text/xml'); - - return $this->getResponse(); - } - - /** - * The body of this method is the same as self::httpErrorWithoutCache() - * but we need it for caching since caching using request url to determine path to cache file - */ - public function httpErrorWithCache() { - return $this->httpErrorWithoutCache(); - } -} - -/** - * Mock implementation of {@link RestfulService}, which uses {@link Director::test()} - * instead of direct curl system calls. - * - * @todo Less overloading of request() - * @todo Currently only works with relative (internal) URLs - * - * @package framework - * @subpackage tests - */ -class RestfulServiceTest_MockRestfulService extends RestfulService { - - public $session = null; - - public function request($subURL = '', $method = "GET", $data = null, $headers = null, $curlOptions = array()) { - - if(!$this->session) { - $this->session = Injector::inst()->create('Session', array()); - } - - $url = $this->baseURL . $subURL; // Url for the request - - if($this->queryString) { - if(strpos($url, '?') !== false) { - $url .= '&' . $this->queryString; - } else { - $url .= '?' . $this->queryString; - } - } - $url = str_replace(' ', '%20', $url); // Encode spaces - - // Custom for mock implementation: Director::test() doesn't cope with absolute URLs - $url = Director::makeRelative($url); - - $method = strtoupper($method); - - assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS'))); - - // Add headers - if($this->customHeaders) { - $headers = array_merge((array)$this->customHeaders, (array)$headers); - } - - // Add authentication - if($this->authUsername) { - $headers[] = "Authorization: Basic " . base64_encode( - $this->authUsername.':'.$this->authPassword - ); - } - - // Custom for mock implementation: Use Director::test() - $body = null; - $postVars = null; - - if($method!='POST') $body = $data; - else $postVars = $data; - - $responseFromDirector = Director::test($url, $postVars, $this->session, $method, $body, $headers); - - $response = new RestfulService_Response( - $responseFromDirector->getBody(), - $responseFromDirector->getStatusCode() - ); - - return $response; - } -} - -/** - * A mock service that returns a 400 error for requests. - */ -class RestfulServiceTest_MockErrorService extends RestfulService { - - public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) { - return new RestfulService_Response('HTTP Error', 400); - } - -}