Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

RestfulService::getValues()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 8
nop 3
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
1
<?php
2
/**
3
 * RestfulService class allows you to consume various RESTful APIs.
4
 * Through this you could connect and aggregate data of various web services.
5
 * For more info visit wiki documentation - http://doc.silverstripe.org/doku.php?id=restfulservice
6
 *
7
 * @package framework
8
 * @subpackage integration
9
 */
10
class RestfulService extends ViewableData implements Flushable {
11
12
	protected $baseURL;
13
	protected $queryString;
14
	protected $errorTag;
15
	protected $checkErrors;
16
	protected $cache_expire;
17
	protected $authUsername, $authPassword;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
18
	protected $customHeaders = array();
19
	protected $proxy;
20
21
	/**
22
	 * @config
23
	 * @var array
24
	 */
25
	private static $default_proxy;
26
27
	/**
28
	 * @config
29
	 * @var array
30
	 */
31
	private static $default_curl_options = array();
32
33
	/**
34
	 * @config
35
	 * @var bool Flushes caches if set to true. This is set by {@link flush()}
36
	 */
37
	private static $flush = false;
38
39
	/**
40
	 * Triggered early in the request when someone requests a flush.
41
	 */
42
	public static function flush() {
43
		self::$flush = true;
44
	}
45
46
	/**
47
	 * set a curl option that will be applied to all requests as default
48
	 * {@see http://php.net/manual/en/function.curl-setopt.php#refsect1-function.curl-setopt-parameters}
49
	 *
50
	 * @deprecated 4.0 Use the "RestfulService.default_curl_options" config setting instead
51
	 * @param int $option The cURL opt Constant
52
	 * @param mixed $value The cURL opt value
53
	 */
54
	public static function set_default_curl_option($option, $value) {
55
		Deprecation::notice('4.0', 'Use the "RestfulService.default_curl_options" config setting instead');
56
		Config::inst()->update('RestfulService', 'default_curl_options', array($option => $value));
57
	}
58
59
	/**
60
	 * set many defauly curl options at once
61
	 *
62
	 * @deprecated 4.0 Use the "RestfulService.default_curl_options" config setting instead
63
	 */
64
	public static function set_default_curl_options($optionArray) {
65
		Deprecation::notice('4.0', 'Use the "RestfulService.default_curl_options" config setting instead');
66
		Config::inst()->update('RestfulService', 'default_curl_options', $optionArray);
67
	}
68
69
	/**
70
	 * Sets default proxy settings for outbound RestfulService connections
71
	 *
72
	 * @param string $proxy The URL of the proxy to use.
73
	 * @param int $port Proxy port
74
	 * @param string $user The proxy auth user name
75
	 * @param string $password The proxy auth password
76
	 * @param boolean $socks Set true to use socks5 proxy instead of http
77
	 * @deprecated 4.0 Use the "RestfulService.default_curl_options" config setting instead,
78
	 *             with direct reference to the CURL_* options
79
	 */
80
	public static function set_default_proxy($proxy, $port = 80, $user = "", $password = "", $socks = false) {
81
		Deprecation::notice(
82
			'4.0',
83
			'Use the "RestfulService.default_curl_options" config setting instead, '
84
				. 'with direct reference to the CURL_* options'
85
		);
86
		config::inst()->update('RestfulService', 'default_proxy', array(
87
			CURLOPT_PROXY => $proxy,
88
			CURLOPT_PROXYUSERPWD => "{$user}:{$password}",
89
			CURLOPT_PROXYPORT => $port,
90
			CURLOPT_PROXYTYPE => ($socks ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP)
91
		));
92
	}
93
94
	/**
95
 	* Creates a new restful service.
96
 	* @param string $base Base URL of the web service eg: api.example.com
97
 	* @param int $expiry Set the cache expiry interva. Defaults to 1 hour (3600 seconds)
98
 	*/
99
	public function __construct($base, $expiry=3600){
100
		$this->baseURL = $base;
101
		$this->cache_expire = $expiry;
102
		parent::__construct();
103
		$this->proxy = $this->config()->default_proxy;
0 ignored issues
show
Documentation introduced by
The property default_proxy does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
104
	}
105
106
	/**
107
 	* Sets the Query string parameters to send a request.
108
 	* @param array $params An array passed with necessary parameters.
109
 	*/
110
	public function setQueryString($params=NULL){
111
		$this->queryString = http_build_query($params,'','&');
112
	}
113
114
	/**
115
	 * Set proxy settings for this RestfulService instance
116
	 *
117
	 * @param string $proxy The URL of the proxy to use.
118
	 * @param int $port Proxy port
119
	 * @param string $user The proxy auth user name
120
	 * @param string $password The proxy auth password
121
	 * @param boolean $socks Set true to use socks5 proxy instead of http
122
	 */
123
	public function setProxy($proxy, $port = 80, $user = "", $password = "", $socks = false) {
124
		$this->proxy = array(
125
			CURLOPT_PROXY => $proxy,
126
			CURLOPT_PROXYUSERPWD => "{$user}:{$password}",
127
			CURLOPT_PROXYPORT => $port,
128
			CURLOPT_PROXYTYPE => ($socks ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP)
129
		);
130
	}
131
132
	/**
133
	 * Set basic authentication
134
	 */
135
	public function basicAuth($username, $password) {
136
		$this->authUsername = $username;
137
		$this->authPassword = $password;
138
	}
139
140
	/**
141
	 * Set a custom HTTP header
142
	 */
143
	public function httpHeader($header) {
144
		$this->customHeaders[] = $header;
145
	}
146
147
	/**
148
	 * @deprecated since version 4.0
149
	 */
150
	protected function constructURL(){
151
		Deprecation::notice('4.0', 'constructURL is deprecated, please use `getAbsoluteRequestURL` instead');
152
		return Controller::join_links($this->baseURL, '?' . $this->queryString);
153
	}
154
155
	/**
156
	 * Makes a request to the RESTful server, and return a {@link RestfulService_Response} object for parsing of the
157
	 * result.
158
	 *
159
	 * @todo Better POST, PUT, DELETE, and HEAD support
160
	 * @todo Caching of requests - probably only GET and HEAD requestst
161
	 * @todo JSON support in RestfulService_Response
162
	 * @todo Pass the response headers to RestfulService_Response
163
	 *
164
	 * This is a replacement of {@link connect()}.
165
	 *
166
	 * @return RestfulService_Response - If curl request produces error, the returned response's status code will
167
	 *                                   be 500
168
	 */
169
	public function request($subURL = '', $method = "GET", $data = null, $headers = null, $curlOptions = array()) {
170
171
		$url = $this->getAbsoluteRequestURL($subURL);
172
		$method = strtoupper($method);
173
174
		assert(in_array($method, array('GET','POST','PUT','DELETE','HEAD','OPTIONS','PATCH')));
175
176
		$cache_path = $this->getCachePath(array(
177
			$url,
178
			$method,
179
			$data,
180
			array_merge((array)$this->customHeaders, (array)$headers),
181
			$curlOptions + (array)$this->config()->default_curl_options,
182
			$this->getBasicAuthString()
183
		));
184
185
		// Check for unexpired cached feed (unless flush is set)
186
		//assume any cache_expire that is 0 or less means that we dont want to
187
		// cache
188
		if($this->cache_expire > 0 && !self::$flush
189
				&& @file_exists($cache_path)
190
				&& @filemtime($cache_path) + $this->cache_expire > time()) {
191
192
			$store = file_get_contents($cache_path);
193
			$response = unserialize($store);
194
195
		} else {
196
			$response = $this->curlRequest($url, $method, $data, $headers, $curlOptions);
197
198
			if(!$response->isError()) {
199
				// Serialise response object and write to cache
200
				$store = serialize($response);
201
				file_put_contents($cache_path, $store);
202
			}
203
			else {
204
				// In case of curl or/and http indicate error, populate response's cachedBody property
205
				// with cached response body with the cache file exists
206
				if (@file_exists($cache_path)) {
207
					$store = file_get_contents($cache_path);
208
					$cachedResponse = unserialize($store);
209
210
					$response->setCachedResponse($cachedResponse);
211
				}
212
				else {
213
					$response->setCachedResponse(false);
214
				}
215
			}
216
		}
217
218
		return $response;
219
	}
220
221
	/**
222
	 * Actually performs a remote service request using curl. This is used by
223
	 * {@link RestfulService::request()}.
224
	 *
225
	 * @param  string $url
226
	 * @param  string $method
227
	 * @param  array $data
228
	 * @param  array $headers
229
	 * @param  array $curlOptions
230
	 * @return RestfulService_Response
231
	 */
232
	public function curlRequest($url, $method, $data = null, $headers = null, $curlOptions = array()) {
233
		$ch        = curl_init();
234
		$timeout   = 5;
235
		$sapphireInfo = new SapphireInfo();
236
		$useragent = 'SilverStripe/' . $sapphireInfo->Version();
237
		$curlOptions = $curlOptions + (array)$this->config()->default_curl_options;
238
239
		curl_setopt($ch, CURLOPT_URL, $url);
240
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
241
		curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
242
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
243
		if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
244
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
245
246
247
		// Write headers to a temporary file
248
		$headerfd = tmpfile();
249
		curl_setopt($ch, CURLOPT_WRITEHEADER, $headerfd);
250
251
		// Add headers
252
		if($this->customHeaders) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customHeaders of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
253
			$headers = array_merge((array)$this->customHeaders, (array)$headers);
254
		}
255
256
		if($headers) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
257
258
		// Add authentication
259
		if($this->authUsername) curl_setopt($ch, CURLOPT_USERPWD, $this->getBasicAuthString());
260
261
		// Add fields to POST and PUT requests
262
		if($method == 'POST' || $method == 'PATCH') {
263
			curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
264
		} elseif($method == 'PUT') {
265
			$put = fopen("php://temp", 'r+');
266
			fwrite($put, $data);
267
			fseek($put, 0);
268
269
			curl_setopt($ch, CURLOPT_PUT, 1);
270
			curl_setopt($ch, CURLOPT_INFILE, $put);
271
			curl_setopt($ch, CURLOPT_INFILESIZE, strlen($data));
272
		}
273
274
		// Apply proxy settings
275
		if(is_array($this->proxy)) {
276
			curl_setopt_array($ch, $this->proxy);
277
		}
278
279
		// Set any custom options passed to the request() function
280
		curl_setopt_array($ch, $curlOptions);
281
282
		// Run request
283
		$body = curl_exec($ch);
284
285
		rewind($headerfd);
286
		$headers = stream_get_contents($headerfd);
287
		fclose($headerfd);
288
289
		$response = $this->extractResponse($ch, $headers, $body);
290
		curl_close($ch);
291
292
		return $response;
293
	}
294
295
	/**
296
	 * A function to return the auth string. This helps consistency through the
297
	 * class but also allows tests to pull it out when generating the expected
298
	 * cache keys
299
	 *
300
	 * @see {self::getCachePath()}
301
	 * @see {RestfulServiceTest::createFakeCachedResponse()}
302
	 *
303
	 * @return string The auth string to be base64 encoded
304
	 */
305
	protected function getBasicAuthString() {
306
		return $this->authUsername . ':' . $this->authPassword;
307
	}
308
309
	/**
310
	 * Generate a cache key based on any cache data sent. The cache data can be
311
	 * any type
312
	 *
313
	 * @param mixed $cacheData The cache seed for generating the key
314
	 * @param string the md5 encoded cache seed.
315
	 */
316
	protected function generateCacheKey($cacheData) {
317
		return md5(var_export($cacheData, true));
318
	}
319
320
	/**
321
	 * Generate the cache path
322
	 *
323
	 * This is mainly so that the cache path can be generated in a consistent
324
	 * way in tests without having to hard code the cachekey generate function
325
	 * in tests
326
	 *
327
	 * @param mixed $cacheData The cache seed {@see self::generateCacheKey}
328
	 *
329
	 * @return string The path to the cache file
330
	 */
331
	protected function getCachePath($cacheData) {
332
		return TEMP_FOLDER . "/xmlresponse_" . $this->generateCacheKey($cacheData);
333
	}
334
335
	/**
336
	 * Extracts the response body and headers from a full curl response
337
	 *
338
	 * @param curl_handle $ch The curl handle for the request
339
	 * @param string $rawResponse The raw response text
0 ignored issues
show
Bug introduced by
There is no parameter named $rawResponse. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
340
	 *
341
	 * @return RestfulService_Response The response object
342
	 */
343
	protected function extractResponse($ch, $rawHeaders, $rawBody) {
344
		//get the status code
345
		$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
346
		//get a curl error if there is one
347
		$curlError = curl_error($ch);
348
		//normalise the status code
349
		if(curl_error($ch) !== '' || $statusCode == 0) $statusCode = 500;
350
		//parse the headers
351
		$parts = array_filter(explode("\r\n\r\n", $rawHeaders));
352
		$lastHeaders = array_pop($parts);
353
		$headers = $this->parseRawHeaders($lastHeaders);
354
		//return the response object
355
		return new RestfulService_Response($rawBody, $statusCode, $headers);
356
	}
357
358
	/**
359
	 * Takes raw headers and parses them to turn them to an associative array
360
	 *
361
	 * Any header that we see more than once is turned into an array.
362
	 *
363
	 * This is meant to mimic http_parse_headers {@link http://php.net/manual/en/function.http-parse-headers.php}
364
	 * thanks to comment #77241 on that page for foundation of this
365
	 *
366
	 * @param string $rawHeaders The raw header string
367
	 * @return array The assosiative array of headers
368
	 */
369
	protected function parseRawHeaders($rawHeaders) {
370
		$headers = array();
371
		$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeaders));
372
		foreach( $fields as $field ) {
373
			if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
374
				$match[1] = preg_replace_callback(
375
					'/(?<=^|[\x09\x20\x2D])./',
376
					function($matches) {
377
					    return strtoupper($matches[0]);
378
                    },
379
					trim($match[1])
380
				);
381
				if( isset($headers[$match[1]]) ) {
382
					if (!is_array($headers[$match[1]])) {
383
						$headers[$match[1]] = array($headers[$match[1]]);
384
					}
385
					$headers[$match[1]][] = $match[2];
386
				} else {
387
					$headers[$match[1]] = trim($match[2]);
388
				}
389
			}
390
		}
391
		return $headers;
392
	}
393
394
395
	/**
396
	 * Returns a full request url
397
	 * @param string
398
	 */
399
	public function getAbsoluteRequestURL($subURL = '') {
400
		$url = Controller::join_links($this->baseURL, $subURL, '?' . $this->queryString);
401
402
		return str_replace(' ', '%20', $url); // Encode spaces
403
	}
404
405
	/**
406
 	* Gets attributes as an array, of a particular type of element.
407
 	* Example : <photo id="2636" owner="123" secret="ab128" server="2">
408
 	* returns id, owner,secret and sever attribute values of all such photo elements.
409
 	* @param string $xml The source xml to parse, this could be the original response received.
410
 	* @param string $collection The name of parent node which wraps the elements, if available
411
 	* @param string $element The element we need to extract the attributes.
412
 	*/
413
414
	public function getAttributes($xml, $collection=NULL, $element=NULL){
415
		$xml = new SimpleXMLElement($xml);
416
		$output = new ArrayList();
417
418
		if($collection)
0 ignored issues
show
Bug Best Practice introduced by
The expression $collection of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
419
			$childElements = $xml->{$collection};
420
		if($element)
0 ignored issues
show
Bug Best Practice introduced by
The expression $element of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
421
			$childElements = $xml->{$collection}->{$element};
422
423
		if(isset($childElements) && $childElements){
424
			foreach($childElements as $child){
425
				$data = array();
426
				foreach($child->attributes() as $key => $value){
427
					$data["$key"] = Convert::raw2xml($value);
428
				}
429
				$output->push(new ArrayData($data));
430
			}
431
		}
432
		return $output;
433
434
	}
435
436
	/**
437
 	* Gets an attribute of a particular element.
438
 	* @param string $xml The source xml to parse, this could be the original response received.
439
 	* @param string $collection The name of the parent node which wraps the element, if available
440
 	* @param string $element The element we need to extract the attribute
441
 	* @param string $attr The name of the attribute
442
 	*/
443
444
	public function getAttribute($xml, $collection=NULL, $element=NULL, $attr){
445
		$xml = new SimpleXMLElement($xml);
446
		$attr_value = "";
447
448
		if($collection)
0 ignored issues
show
Bug Best Practice introduced by
The expression $collection of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
449
			$childElements = $xml->{$collection};
450
		if($element)
0 ignored issues
show
Bug Best Practice introduced by
The expression $element of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
451
			$childElements = $xml->{$collection}->{$element};
452
453
		if(isset($childElements[$attr]))
454
			$attr_value = (string) $childElements[$attr];
455
456
		return Convert::raw2xml($attr_value);
457
458
	}
459
460
461
	/**
462
 	* Gets set of node values as an array.
463
 	* When you get to the depth in the hierarchy use node_child_subchild syntax to get the value.
464
 	* @param string $xml The the source xml to parse, this could be the original response received.
465
 	* @param string $collection The name of parent node which wraps the elements, if available
466
 	* @param string $element The element we need to extract the node values.
467
 	*/
468
469
	public function getValues($xml, $collection=NULL, $element=NULL){
470
		$xml = new SimpleXMLElement($xml);
471
		$output = new ArrayList();
472
473
			$childElements = $xml;
474
		if($collection)
0 ignored issues
show
Bug Best Practice introduced by
The expression $collection of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
475
			$childElements = $xml->{$collection};
476
		if($element)
0 ignored issues
show
Bug Best Practice introduced by
The expression $element of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
477
			$childElements = $xml->{$collection}->{$element};
478
479
		if(isset($childElements) && $childElements){
480
			foreach($childElements as $child){
481
				$data = array();
482
				$this->getRecurseValues($child,$data);
483
				$output->push(new ArrayData($data));
484
			}
485
		}
486
		return $output;
487
	}
488
489
	protected function getRecurseValues($xml,&$data,$parent=""){
490
		$conv_value = "";
491
		$child_count = 0;
492
		foreach($xml as $key=>$value)
493
		{
494
			$child_count++;
495
			$k = ($parent == "") ? (string)$key : $parent . "_" . (string)$key;
496
			if($this->getRecurseValues($value,$data,$k) == 0){  // no childern, aka "leaf node"
497
				$conv_value = Convert::raw2xml($value);
498
			}
499
			//Review the fix for similar node names overriding it's predecessor
500
			if(array_key_exists($k, $data) == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
501
				$data[$k] = $data[$k] . ",". $conv_value;
502
			}
503
			else {
504
				$data[$k] = $conv_value;
505
			}
506
507
508
		}
509
		return $child_count;
510
511
	}
512
513
	/**
514
 	* Gets a single node value.
515
 	* @param string $xml The source xml to parse, this could be the original response received.
516
 	* @param string $collection The name of parent node which wraps the elements, if available
517
 	* @param string $element The element we need to extract the node value.
518
 	*/
519
520
	public function getValue($xml, $collection=NULL, $element=NULL){
521
		$xml = new SimpleXMLElement($xml);
522
523
		if($collection)
0 ignored issues
show
Bug Best Practice introduced by
The expression $collection of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
524
			$childElements = $xml->{$collection};
525
		if($element)
0 ignored issues
show
Bug Best Practice introduced by
The expression $element of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
526
			$childElements = $xml->{$collection}->{$element};
527
528
		if(isset($childElements) && $childElements)
529
			return Convert::raw2xml($childElements);
530
	}
531
532
	/**
533
 	* Searches for a node in document tree and returns it value.
534
 	* @param string $xml source xml to parse, this could be the original response received.
535
 	* @param string $node Node to search for
536
 	*/
537
	public function searchValue($xml, $node=NULL){
538
		$xml = new SimpleXMLElement($xml);
539
		$childElements = $xml->xpath($node);
540
541
		if($childElements)
0 ignored issues
show
Bug Best Practice introduced by
The expression $childElements of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
542
			return Convert::raw2xml($childElements[0]);
543
	}
544
545
	/**
546
 	* Searches for a node in document tree and returns its attributes.
547
 	* @param string $xml the source xml to parse, this could be the original response received.
548
 	* @param string $node Node to search for
549
 	*/
550
	public function searchAttributes($xml, $node=NULL){
551
		$xml = new SimpleXMLElement($xml);
552
		$output = new ArrayList();
553
554
		$childElements = $xml->xpath($node);
555
556
		if($childElements)
0 ignored issues
show
Bug Best Practice introduced by
The expression $childElements of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
557
		foreach($childElements as $child){
558
		$data = array();
559
			foreach($child->attributes() as $key => $value){
560
				$data["$key"] = Convert::raw2xml($value);
561
			}
562
563
			$output->push(new ArrayData($data));
564
		}
565
566
		return $output;
567
	}
568
}
569
570
/**
571
 * @package framework
572
 * @subpackage integration
573
 */
574
class RestfulService_Response extends SS_HTTPResponse {
575
	protected $simpleXML;
576
577
	/**
578
	 * @var RestfulService_Response|false It should be populated with cached request
579
	 * when a request referring to this response was unsuccessful
580
	 */
581
	protected $cachedResponse = false;
582
583
	public function __construct($body, $statusCode = 200, $headers = null) {
584
		$this->setbody($body);
585
		$this->setStatusCode($statusCode);
586
		$this->headers = $headers;
587
	}
588
589
	public function simpleXML() {
590
		if(!$this->simpleXML) {
591
			try {
592
				$this->simpleXML = new SimpleXMLElement($this->body);
593
			}
594
			catch(Exception $e) {
595
				user_error("String could not be parsed as XML. " . $e, E_USER_WARNING);
596
			}
597
		}
598
		return $this->simpleXML;
599
	}
600
601
	/**
602
	 * get the cached response object. This allows you to access the cached
603
	 * eaders, not just the cached body.
604
	 *
605
	 * @return RestfulService_Response|false The cached response object
606
	 */
607
	public function getCachedResponse() {
608
		return $this->cachedResponse;
609
	}
610
611
	/**
612
	 * @return string|false
613
	 */
614
	public function getCachedBody() {
615
		if ($this->cachedResponse) {
616
			return $this->cachedResponse->getBody();
617
		}
618
		return false;
619
	}
620
621
	/**
622
	 * @param string
623
	 * @deprecated since version 4.0
624
	 */
625
	public function setCachedBody($content) {
626
		Deprecation::notice('4.0', 'Setting the response body is now deprecated, set the cached request instead');
627
		if (!$this->cachedResponse) {
628
			$this->cachedResponse = new RestfulService_Response($content);
629
		}
630
		else {
631
			$this->cachedResponse->setBody($content);
632
		}
633
	}
634
635
	/**
636
	 * @param string
637
	 */
638
	public function setCachedResponse($response) {
639
		$this->cachedResponse = $response;
640
	}
641
642
	/**
643
	 * Return an array of xpath matches
644
	 */
645
	public function xpath($xpath) {
646
		return $this->simpleXML()->xpath($xpath);
647
	}
648
649
	/**
650
	 * Return the first xpath match
651
	 */
652
	public function xpath_one($xpath) {
653
		$items = $this->xpath($xpath);
654
		if (isset($items[0])) {
655
			return $items[0];
656
		}
657
	}
658
}
659