mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #1187 from dhensby/restfulservice-improvements
API Restfulservice improvements
This commit is contained in:
commit
6ff1f9050d
@ -17,6 +17,27 @@ class RestfulService extends ViewableData {
|
||||
protected $customHeaders = array();
|
||||
protected $proxy;
|
||||
protected static $default_proxy;
|
||||
protected static $default_curl_options = array();
|
||||
|
||||
/**
|
||||
* set a curl option that will be applied to all requests as default
|
||||
* {@see http://php.net/manual/en/function.curl-setopt.php#refsect1-function.curl-setopt-parameters}
|
||||
*
|
||||
* @param int $option The cURL opt Constant
|
||||
* @param mixed $value The cURL opt value
|
||||
*/
|
||||
public static function set_default_curl_option($option, $value) {
|
||||
self::$default_curl_options[$option] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* set many defauly curl options at once
|
||||
*/
|
||||
public static function set_default_curl_options($optionArray) {
|
||||
foreach ($optionArray as $option => $value) {
|
||||
self::set_default_curl_option($option, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default proxy settings for outbound RestfulService connections
|
||||
@ -114,12 +135,20 @@ class RestfulService extends ViewableData {
|
||||
|
||||
assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS')));
|
||||
|
||||
$cachedir = TEMP_FOLDER; // Default silverstripe cache
|
||||
$cache_file = md5($url); // Encoded name of cache file
|
||||
$cache_path = $cachedir."/xmlresponse_$cache_file";
|
||||
$cache_path = $this->getCachePath(array(
|
||||
$url,
|
||||
$method,
|
||||
$data,
|
||||
array_merge((array)$this->customHeaders, (array)$headers),
|
||||
array_merge(self::$default_curl_options,$curlOptions),
|
||||
$this->getBasicAuthString()
|
||||
));
|
||||
|
||||
// Check for unexpired cached feed (unless flush is set)
|
||||
if(!isset($_GET['flush']) && @file_exists($cache_path)
|
||||
//assume any cache_expire that is 0 or less means that we dont want to
|
||||
// cache
|
||||
if($this->cache_expire > 0 && !isset($_GET['flush'])
|
||||
&& @file_exists($cache_path)
|
||||
&& @filemtime($cache_path) + $this->cache_expire > time()) {
|
||||
|
||||
$store = file_get_contents($cache_path);
|
||||
@ -140,10 +169,10 @@ class RestfulService extends ViewableData {
|
||||
$store = file_get_contents($cache_path);
|
||||
$cachedResponse = unserialize($store);
|
||||
|
||||
$response->setCachedBody($cachedResponse->getBody());
|
||||
$response->setCachedResponse($cachedResponse);
|
||||
}
|
||||
else {
|
||||
$response->setCachedBody(false);
|
||||
$response->setCachedResponse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,6 +196,7 @@ class RestfulService extends ViewableData {
|
||||
$timeout = 5;
|
||||
$sapphireInfo = new SapphireInfo();
|
||||
$useragent = 'SilverStripe/' . $sapphireInfo->Version();
|
||||
$curlOptions = array_merge(self::$default_curl_options, $curlOptions);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
@ -174,6 +204,8 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
//include headers in the response
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
|
||||
// Add headers
|
||||
if($this->customHeaders) {
|
||||
@ -183,7 +215,7 @@ class RestfulService extends ViewableData {
|
||||
if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Add authentication
|
||||
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, "$this->authUsername:$this->authPassword");
|
||||
if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, $this->getBasicAuthString());
|
||||
|
||||
// Add fields to POST and PUT requests
|
||||
if($method == 'POST') {
|
||||
@ -208,26 +240,112 @@ class RestfulService extends ViewableData {
|
||||
curl_setopt_array($ch, $curlOptions);
|
||||
|
||||
// Run request
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$responseBody = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
// Problem verifying the server SSL certificate; just ignore it as it's not mandatory
|
||||
if(strpos($curlError,'14090086') !== false) {
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$responseBody = curl_exec($ch);
|
||||
$curlError = curl_error($ch);
|
||||
}
|
||||
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
|
||||
|
||||
$response = new RestfulService_Response($responseBody, $statusCode);
|
||||
$rawResponse = curl_exec($ch);
|
||||
$response = $this->extractResponse($ch, $rawResponse);
|
||||
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, $rawResponse) {
|
||||
//get the status code
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
//normalise the status code
|
||||
if($curlError !== '' || $statusCode == 0) $statusCode = 500;
|
||||
//calculate the length of the header and extract it
|
||||
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$rawHeaders = substr($rawResponse, 0, $headerLength);
|
||||
//extract the body
|
||||
$body = substr($rawResponse, $headerLength);
|
||||
//parse the headers
|
||||
$headers = $this->parseRawHeaders($rawHeaders);
|
||||
//return the response object
|
||||
return new RestfulService_Response($body, $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]);'),
|
||||
strtolower(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
|
||||
@ -418,10 +536,10 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
protected $simpleXML;
|
||||
|
||||
/**
|
||||
* @var boolean It should be populated with cached content
|
||||
* @var boolean It should be populated with cached request
|
||||
* when a request referring to this response was unsuccessful
|
||||
*/
|
||||
protected $cachedBody = false;
|
||||
protected $cachedResponse = false;
|
||||
|
||||
public function __construct($body, $statusCode = 200, $headers = null) {
|
||||
$this->setbody($body);
|
||||
@ -441,18 +559,44 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
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() {
|
||||
return $this->cachedBody;
|
||||
if ($this->cachedResponse) {
|
||||
return $this->cachedResponse->getBody();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public function setCachedBody($content) {
|
||||
Deprecation::notice('3.1', '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 setCachedBody($content) {
|
||||
$this->cachedBody = $content;
|
||||
public function setCachedResponse($response) {
|
||||
$this->cachedResponse = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -467,8 +611,8 @@ class RestfulService_Response extends SS_HTTPResponse {
|
||||
*/
|
||||
public function xpath_one($xpath) {
|
||||
$items = $this->xpath($xpath);
|
||||
return $items[0];
|
||||
if (isset($items[0])) {
|
||||
return $items[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -145,16 +145,55 @@ class RestfulServiceTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$cachedir = TEMP_FOLDER; // Default silverstripe cache
|
||||
$cache_file = md5($fullUrl); // Encoded name of cache file
|
||||
$cache_path = $cachedir."/xmlresponse_$cache_file";
|
||||
//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".
|
||||
"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',
|
||||
'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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RestfulServiceTest_Controller extends Controller implements TestOnly {
|
||||
|
Loading…
Reference in New Issue
Block a user