Completed
Push — 2.0-dev ( 5df328...6a45b1 )
by Michael
09:06
created

Client::getOption()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Part of the Joomla Framework OAuth1 Package
4
 *
5
 * @copyright  Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
6
 * @license    GNU General Public License version 2 or later; see LICENSE
7
 */
8
9
namespace Joomla\OAuth1;
10
11
use Joomla\Http\Http;
12
use Joomla\Input\Input;
13
use Joomla\Application\AbstractWebApplication;
14
15
/**
16
 * Joomla Framework class for interacting with an OAuth 1.0 and 1.0a server.
17
 *
18
 * @since  1.0
19
 */
20
abstract class Client
21
{
22
	/**
23
	 * Options for the Client object.
24
	 *
25
	 * @var    array|\ArrayAccess
26
	 * @since  1.0
27
	 */
28
	protected $options;
29
30
	/**
31
	 * Contains access token key, secret and verifier.
32
	 *
33
	 * @var    array
34
	 * @since  1.0
35
	 */
36
	protected $token = [];
37
38
	/**
39
	 * The HTTP client object to use in sending HTTP requests.
40
	 *
41
	 * @var    Http
42
	 * @since  1.0
43
	 */
44
	protected $client;
45
46
	/**
47
	 * The input object to use in retrieving GET/POST data.
48
	 *
49
	 * @var    Input
50
	 * @since  1.0
51
	 */
52
	protected $input;
53
54
	/**
55
	 * The application object to send HTTP headers for redirects.
56
	 *
57
	 * @var    AbstractWebApplication
58
	 * @since  1.0
59
	 */
60
	protected $application;
61
62
	/**
63
	 * Selects which version of OAuth to use: 1.0 or 1.0a.
64
	 *
65
	 * @var    string
66
	 * @since  1.0
67
	 */
68
	protected $version;
69
70
	/**
71
	 * Constructor.
72
	 *
73
	 * @param   AbstractWebApplication  $application  The application object
74
	 * @param   Http                    $client       The HTTP client object.
75
	 * @param   Input                   $input        The input object
76
	 * @param   array|\ArrayAccess      $options      OAuth1 Client options.
77
	 * @param   string                  $version      Specify the OAuth version. By default we are using 1.0a.
78
	 *
79
	 * @since   1.0
80
	 */
81
	public function __construct(AbstractWebApplication $application, Http $client = null, Input $input = null, $options = [], $version = '1.0a')
82
	{
83
		if (!is_array($options) && !($options instanceof \ArrayAccess))
84
		{
85
			throw new \InvalidArgumentException(
86
				'The options param must be an array or implement the ArrayAccess interface.'
87
			);
88
		}
89
90
		$this->application = $application;
91
		$this->client      = $client instanceof Http ? $client : HttpFactory::getHttp($options);
92
		$this->input       = $input instanceof Input ? $input : $application->input;
93
		$this->options     = $options;
94
		$this->version     = $version;
95
	}
96
97
	/**
98
	 * Method to form the oauth flow.
99
	 *
100
	 * @return  string  The access token.
101
	 *
102
	 * @since   1.0
103
	 * @throws  \DomainException
104
	 */
105
	public function authenticate()
106
	{
107
		// Already got some credentials stored?
108
		if ($this->token)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->token 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...
109
		{
110
			$response = $this->verifyCredentials();
111
112
			if ($response)
0 ignored issues
show
Bug Best Practice introduced by
The expression $response 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...
113
			{
114
				return $this->token;
115
			}
116
117
			$this->token = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $token.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
118
		}
119
120
		// Check for callback.
121
		if (strcmp($this->version, '1.0a') === 0)
122
		{
123
			$verifier = $this->input->get('oauth_verifier');
124
		}
125
		else
126
		{
127
			$verifier = $this->input->get('oauth_token');
128
		}
129
130
		if (!empty($verifier))
131
		{
132
			$session = $this->application->getSession();
133
134
			// Get token form session.
135
			$this->token = [
136
				'key'    => $session->get('oauth_token.key'),
137
				'secret' => $session->get('oauth_token.secret')
138
			];
139
140
			// Verify the returned request token.
141
			if (strcmp($this->token['key'], $this->input->get('oauth_token')) !== 0)
142
			{
143
				throw new \DomainException('Bad session!');
144
			}
145
146
			// Set token verifier for 1.0a.
147
			if (strcmp($this->version, '1.0a') === 0)
148
			{
149
				$this->token['verifier'] = $this->input->get('oauth_verifier');
150
			}
151
152
			// Generate access token.
153
			$this->generateAccessToken();
154
155
			// Return the access token.
156
			return $this->token;
157
		}
158
159
		// Generate a request token.
160
		$this->generateRequestToken();
161
162
		// Authenticate the user and authorise the app.
163
		$this->authorise();
164
	}
165
166
	/**
167
	 * Method used to get a request token.
168
	 *
169
	 * @return  void
170
	 *
171
	 * @since   1.0
172
	 * @throws  \DomainException
173
	 */
174
	private function generateRequestToken()
175
	{
176
		$parameters = [];
177
178
		// Set the callback URL.
179
		if ($this->getOption('callback'))
180
		{
181
			$parameters['oauth_callback'] = $this->getOption('callback');
182
		}
183
184
		// Make an OAuth request for the Request Token.
185
		$response = $this->oauthRequest($this->getOption('requestTokenURL'), 'POST', $parameters);
186
187
		parse_str($response->body, $params);
188
189
		if (strcmp($this->version, '1.0a') === 0 && strcmp($params['oauth_callback_confirmed'], 'true') !== 0)
190
		{
191
			throw new \DomainException('Bad request token!');
192
		}
193
194
		// Save the request token.
195
		$this->token = ['key' => $params['oauth_token'], 'secret' => $params['oauth_token_secret']];
196
197
		// Save the request token in session
198
		$session = $this->application->getSession();
199
		$session->set('oauth_token.key', $this->token['key']);
200
		$session->set('oauth_token.secret', $this->token['secret']);
201
	}
202
203
	/**
204
	 * Method used to authorise the application.
205
	 *
206
	 * @return  void
207
	 *
208
	 * @since   1.0
209
	 */
210
	private function authorise()
211
	{
212
		$url = $this->getOption('authoriseURL') . '?oauth_token=' . $this->token['key'];
213
214
		if ($this->getOption('scope'))
215
		{
216
			$scope = is_array($this->getOption('scope')) ? implode(' ', $this->getOption('scope')) : $this->getOption('scope');
217
			$url .= '&scope=' . urlencode($scope);
218
		}
219
220
		if ($this->getOption('sendheaders'))
221
		{
222
			$this->application->redirect($url);
223
		}
224
	}
225
226
	/**
227
	 * Method used to get an access token.
228
	 *
229
	 * @return  void
230
	 *
231
	 * @since   1.0
232
	 */
233
	private function generateAccessToken()
234
	{
235
		// Set the parameters.
236
		$parameters = [
237
			'oauth_token' => $this->token['key']
238
		];
239
240
		if (strcmp($this->version, '1.0a') === 0)
241
		{
242
			$parameters = array_merge($parameters, ['oauth_verifier' => $this->token['verifier']]);
243
		}
244
245
		// Make an OAuth request for the Access Token.
246
		$response = $this->oauthRequest($this->getOption('accessTokenURL'), 'POST', $parameters);
247
248
		parse_str($response->body, $params);
249
250
		// Save the access token.
251
		$this->token = ['key' => $params['oauth_token'], 'secret' => $params['oauth_token_secret']];
252
	}
253
254
	/**
255
	 * Method used to make an OAuth request.
256
	 *
257
	 * @param   string  $url         The request URL.
258
	 * @param   string  $method      The request method.
259
	 * @param   array   $parameters  Array containing request parameters.
260
	 * @param   mixed   $data        The POST request data.
261
	 * @param   array   $headers     An array of name-value pairs to include in the header of the request
262
	 *
263
	 * @return  \Joomla\Http\Response
264
	 *
265
	 * @since   1.0
266
	 * @throws  \DomainException
267
	 */
268
	public function oauthRequest($url, $method, $parameters, $data = [], $headers = [])
269
	{
270
		// Set the parameters.
271
		$defaults = [
272
			'oauth_consumer_key'     => $this->getOption('consumer_key'),
273
			'oauth_signature_method' => 'HMAC-SHA1',
274
			'oauth_version'          => '1.0',
275
			'oauth_nonce'            => $this->generateNonce(),
276
			'oauth_timestamp'        => time()
277
		];
278
279
		$parameters = array_merge($parameters, $defaults);
280
281
		// Do not encode multipart parameters. Do not include $data in the signature if $data is not array.
282
		if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart/form-data') !== false || !is_array($data))
283
		{
284
			$oauth_headers = $parameters;
285
		}
286
		else
287
		{
288
			// Use all parameters for the signature.
289
			$oauth_headers = array_merge($parameters, $data);
290
		}
291
292
		// Sign the request.
293
		$oauth_headers = $this->signRequest($url, $method, $oauth_headers);
294
295
		// Get parameters for the Authorisation header.
296
		if (is_array($data))
297
		{
298
			$oauth_headers = array_diff_key($oauth_headers, $data);
299
		}
300
301
		// Send the request.
302
		switch ($method)
303
		{
304
			case 'GET':
305
				$url      = $this->toUrl($url, $data);
306
				$response = $this->client->get($url, ['Authorization' => $this->createHeader($oauth_headers)]);
307
				break;
308
309
			case 'POST':
310 View Code Duplication
			case 'PUT':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
311
				$headers  = array_merge($headers, ['Authorization' => $this->createHeader($oauth_headers)]);
312
				$response = $this->client->{strtolower($method)}($url, $data, $headers);
313
				break;
314
315 View Code Duplication
			case 'DELETE':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
316
				$headers  = array_merge($headers, ['Authorization' => $this->createHeader($oauth_headers)]);
317
				$response = $this->client->delete($url, $headers);
318
				break;
319
		}
320
321
		// Validate the response code.
322
		$this->validateResponse($url, $response);
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
323
324
		return $response;
325
	}
326
327
	/**
328
	 * Method to validate a response.
329
	 *
330
	 * @param   string    $url       The request URL.
331
	 * @param   Response  $response  The response to validate.
332
	 *
333
	 * @return  void
334
	 *
335
	 * @since   1.0
336
	 * @throws  \DomainException
337
	 */
338
	abstract public function validateResponse($url, $response);
339
340
	/**
341
	 * Method used to create the header for the POST request.
342
	 *
343
	 * @param   array $parameters Array containing request parameters.
344
	 *
345
	 * @return  string  The header.
346
	 *
347
	 * @since   1.0
348
	 */
349
	private function createHeader($parameters)
350
	{
351
		$header = 'OAuth ';
352
353
		foreach ($parameters as $key => $value)
354
		{
355
			if (!strcmp($header, 'OAuth '))
356
			{
357
				$header .= $key . '="' . $this->safeEncode($value) . '"';
358
			}
359
			else
360
			{
361
				$header .= ', ' . $key . '="' . $value . '"';
362
			}
363
		}
364
365
		return $header;
366
	}
367
368
	/**
369
	 * Method to create the URL formed string with the parameters.
370
	 *
371
	 * @param   string  $url         The request URL.
372
	 * @param   array   $parameters  Array containing request parameters.
373
	 *
374
	 * @return  string  The formed URL.
375
	 *
376
	 * @since   1.0
377
	 */
378
	public function toUrl($url, $parameters)
379
	{
380
		foreach ($parameters as $key => $value)
381
		{
382
			if (is_array($value))
383
			{
384
				foreach ($value as $k => $v)
385
				{
386
					if (strpos($url, '?') === false)
387
					{
388
						$url .= '?';
389
					}
390
					else
391
					{
392
						$url .= '&';
393
					}
394
395
					$url .= $key . '=' . $v;
396
				}
397
			}
398
			else
399
			{
400
				if (strpos($value, ' ') !== false)
401
				{
402
					$value = $this->safeEncode($value);
403
				}
404
405
				if (strpos($url, '?') === false)
406
				{
407
					$url .= '?';
408
				}
409
				else
410
				{
411
					$url .= '&';
412
				}
413
414
				$url .= $key . '=' . $value;
415
			}
416
		}
417
418
		return $url;
419
	}
420
421
	/**
422
	 * Method used to sign requests.
423
	 *
424
	 * @param   string  $url         The URL to sign.
425
	 * @param   string  $method      The request method.
426
	 * @param   array   $parameters  Array containing request parameters.
427
	 *
428
	 * @return  array  The array containing the request parameters, including signature.
429
	 *
430
	 * @since   1.0
431
	 */
432
	private function signRequest($url, $method, $parameters)
433
	{
434
		// Create the signature base string.
435
		$base = $this->baseString($url, $method, $parameters);
436
437
		$parameters['oauth_signature'] = $this->safeEncode(
438
			base64_encode(
439
				hash_hmac('sha1', $base, $this->prepareSigningKey(), true)
440
			)
441
		);
442
443
		return $parameters;
444
	}
445
446
	/**
447
	 * Prepare the signature base string.
448
	 *
449
	 * @param   string  $url         The URL to sign.
450
	 * @param   string  $method      The request method.
451
	 * @param   array   $parameters  Array containing request parameters.
452
	 *
453
	 * @return  string  The base string.
454
	 *
455
	 * @since   1.0
456
	 */
457
	private function baseString($url, $method, $parameters)
458
	{
459
		// Sort the parameters alphabetically
460
		uksort($parameters, 'strcmp');
461
462
		// Encode parameters.
463
		foreach ($parameters as $key => $value)
464
		{
465
			$key = $this->safeEncode($key);
466
467
			if (is_array($value))
468
			{
469
				foreach ($value as $k => $v)
470
				{
471
					$v    = $this->safeEncode($v);
472
					$kv[] = "{$key}={$v}";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$kv was never initialized. Although not strictly required by PHP, it is generally a good practice to add $kv = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
473
				}
474
			}
475
			else
476
			{
477
				$value = $this->safeEncode($value);
478
				$kv[]  = "{$key}={$value}";
0 ignored issues
show
Bug introduced by
The variable $kv does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
479
			}
480
		}
481
482
		// Form the parameter string.
483
		$params = implode('&', $kv);
484
485
		// Signature base string elements.
486
		$base = [
487
			$method,
488
			$url,
489
			$params
490
		];
491
492
		// Return the base string.
493
		return implode('&', $this->safeEncode($base));
494
	}
495
496
	/**
497
	 * Encodes the string or array passed in a way compatible with OAuth.
498
	 * If an array is passed each array value will will be encoded.
499
	 *
500
	 * @param   mixed  $data  The scalar or array to encode.
501
	 *
502
	 * @return  string  $data encoded in a way compatible with OAuth.
503
	 *
504
	 * @since   1.0
505
	 */
506
	public function safeEncode($data)
507
	{
508
		if (is_array($data))
509
		{
510
			return array_map([$this, 'safeEncode'], $data);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array_map(array($... 'safeEncode'), $data); (array) is incompatible with the return type documented by Joomla\OAuth1\Client::safeEncode of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
511
		}
512
513
		if (is_scalar($data))
514
		{
515
			return str_ireplace(
516
				['+', '%7E'],
517
				[' ', '~'],
518
				rawurlencode($data)
519
			);
520
		}
521
522
		return '';
523
	}
524
525
	/**
526
	 * Method used to generate the current nonce.
527
	 *
528
	 * @return  string  The current nonce.
529
	 *
530
	 * @since   1.0
531
	 */
532
	public static function generateNonce()
533
	{
534
		// The md5s look nicer than numbers.
535
		return md5(microtime() . mt_rand());
536
	}
537
538
	/**
539
	 * Prepares the OAuth signing key.
540
	 *
541
	 * @return  string  The prepared signing key.
542
	 *
543
	 * @since   1.0
544
	 */
545
	private function prepareSigningKey()
546
	{
547
		return $this->safeEncode($this->getOption('consumer_secret')) . '&' . $this->safeEncode(($this->token) ? $this->token['secret'] : '');
548
	}
549
550
	/**
551
	 * Returns an HTTP 200 OK response code and a representation of the requesting user if authentication was successful;
552
	 * returns a 401 status code and an error message if not.
553
	 *
554
	 * @return  array  The decoded JSON response
555
	 *
556
	 * @since   1.0
557
	 */
558
	abstract public function verifyCredentials();
559
560
	/**
561
	 * Get an option from the OAuth1 Client instance.
562
	 *
563
	 * @param   string  $key      The name of the option to get
564
	 * @param   mixed   $default  Optional default value if the option does not exist
565
	 *
566
	 * @return  mixed  The option value
567
	 *
568
	 * @since   1.0
569
	 */
570
	public function getOption($key, $default = null)
571
	{
572
		return isset($this->options[$key]) ? $this->options[$key] : $default;
573
	}
574
575
	/**
576
	 * Set an option for the OAuth1 Client instance.
577
	 *
578
	 * @param   string  $key    The name of the option to set
579
	 * @param   mixed   $value  The option value to set
580
	 *
581
	 * @return  $this
582
	 *
583
	 * @since   1.0
584
	 */
585
	public function setOption($key, $value)
586
	{
587
		$this->options[$key] = $value;
588
589
		return $this;
590
	}
591
592
	/**
593
	 * Get the oauth token key or secret.
594
	 *
595
	 * @return  array  The oauth token key and secret.
596
	 *
597
	 * @since   1.0
598
	 */
599
	public function getToken()
600
	{
601
		return $this->token;
602
	}
603
604
	/**
605
	 * Set the oauth token.
606
	 *
607
	 * @param   array  $token  The access token key and secret.
608
	 *
609
	 * @return  $this
610
	 *
611
	 * @since   1.0
612
	 */
613
	public function setToken($token)
614
	{
615
		$this->token = $token;
616
617
		return $this;
618
	}
619
}
620