Passed
Push — main ( c05ac9...4fe852 )
by smiley
01:41
created

OAuthProvider::setStreamFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
/**
3
 * Class OAuthProvider
4
 *
5
 * @created      09.07.2017
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2017 Smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\OAuth\Core;
12
13
use chillerlan\OAuth\Storage\MemoryStorage;
14
use chillerlan\HTTP\Psr17\{RequestFactory, StreamFactory, UriFactory};
15
use chillerlan\HTTP\Utils\QueryUtil;
16
use chillerlan\OAuth\OAuthOptions;
17
use chillerlan\OAuth\Storage\OAuthStorageInterface;
18
use chillerlan\Settings\SettingsContainerInterface;
19
use Psr\Http\Client\ClientInterface;
20
use Psr\Http\Message\{
21
	RequestFactoryInterface, RequestInterface, ResponseInterface,
22
	StreamFactoryInterface, StreamInterface, UriFactoryInterface
23
};
24
use Psr\Log\{LoggerAwareTrait, LoggerInterface, NullLogger};
25
use ReflectionClass;
26
use function array_merge;
27
use function in_array;
28
use function is_array;
29
use function is_string;
30
use function json_encode;
31
use function sprintf;
32
use function str_starts_with;
33
use function strtolower;
34
use const PHP_QUERY_RFC1738;
35
36
/**
37
 * Implements an abstract OAuth provider with all methods required by the OAuthInterface.
38
 * It also implements a magic getter that allows to access the properties listed below.
39
 *
40
 * @property string|null $apiDocs
41
 * @property string      $apiURL
42
 * @property string|null $applicationURL
43
 * @property string      $serviceName
44
 * @property string|null $userRevokeURL
45
 */
46
abstract class OAuthProvider implements OAuthInterface{
47
	use LoggerAwareTrait;
48
49
	protected const ALLOWED_PROPERTIES = [
50
		'apiDocs', 'apiURL', 'applicationURL', 'serviceName', 'userRevokeURL'
51
	];
52
53
	/**
54
	 * the http client instance
55
	 */
56
	protected ClientInterface $http;
57
58
	/**
59
	 * the token storage instance
60
	 */
61
	protected OAuthStorageInterface $storage;
62
63
	/**
64
	 * the options instance
65
	 */
66
	protected OAuthOptions|SettingsContainerInterface $options;
67
68
	/**
69
	 * an optional PSR-17 request factory
70
	 */
71
	protected RequestFactoryInterface $requestFactory;
72
73
	/**
74
	 * an optional PSR-17 stream factory
75
	 */
76
	protected StreamFactoryInterface  $streamFactory;
77
78
	/**
79
	 * an optional PSR-17 URI factory
80
	 */
81
	protected UriFactoryInterface $uriFactory;
82
83
	/**
84
	 * the name of the provider (class) (magic)
85
	 */
86
	protected ?string $serviceName = null;
87
88
	/**
89
	 * the authentication URL
90
	 */
91
	protected string $authURL;
92
93
	/**
94
	 * an optional link to the provider's API docs (magic)
95
	 */
96
	protected ?string $apiDocs = null;
97
98
	/**
99
	 * the API base URL (magic)
100
	 */
101
	protected ?string $apiURL = null;
102
103
	/**
104
	 * an optional URL to the provider's credential registration/application page (magic)
105
	 */
106
	protected ?string $applicationURL = null;
107
108
	/**
109
	 * an optional link to the page where a user can revoke access tokens (magic)
110
	 */
111
	protected ?string $userRevokeURL = null;
112
113
	/**
114
	 * an optional URL for application side token revocation
115
	 */
116
	protected ?string $revokeURL = null;
117
118
	/**
119
	 * the provider's access token exchange URL
120
	 */
121
	protected string $accessTokenURL;
122
123
	/**
124
	 * additional headers to use during authentication
125
	 */
126
	protected array $authHeaders = [];
127
128
	/**
129
	 * additional headers to use during API access
130
	 */
131
	protected array $apiHeaders = [];
132
133
	/**
134
	 * OAuthProvider constructor.
135
	 */
136
	public function __construct(
137
		ClientInterface $http,
138
		OAuthOptions|SettingsContainerInterface $options,
139
		LoggerInterface $logger = null
140
	){
141
		$this->http    = $http;
142
		$this->storage = new MemoryStorage;
143
		$this->options = $options;
144
		$this->logger  = $logger ?? new NullLogger;
145
146
		// i hate this, but i also hate adding 3 more params to the constructor
147
		// no, i won't use a DI container for this. don't @ me
148
		$this->requestFactory = new RequestFactory;
149
		$this->streamFactory  = new StreamFactory;
150
		$this->uriFactory     = new UriFactory;
151
		$this->serviceName    = (new ReflectionClass($this))->getShortName();
152
153
		$this->storage->setServiceName($this->serviceName);
154
	}
155
156
	/**
157
	 * Magic getter for the properties specified in self::ALLOWED_PROPERTIES
158
	 *
159
	 * @return mixed|null
160
	 */
161
	public function __get(string $name):mixed{
162
163
		if(in_array($name, $this::ALLOWED_PROPERTIES, true)){
164
			return $this->{$name};
165
		}
166
167
		return null;
168
	}
169
170
	/**
171
	 * @inheritDoc
172
	 * @codeCoverageIgnore
173
	 */
174
	public function setStorage(OAuthStorageInterface $storage):OAuthInterface{
175
		$this->storage = $storage;
176
		$this->storage->setServiceName($this->serviceName);
177
178
		return $this;
179
	}
180
181
	/**
182
	 * @inheritDoc
183
	 * @codeCoverageIgnore
184
	 */
185
	public function getStorage():OAuthStorageInterface{
186
		return $this->storage;
187
	}
188
189
	/**
190
	 * @inheritDoc
191
	 * @codeCoverageIgnore
192
	 */
193
	public function storeAccessToken(AccessToken $token):OAuthInterface{
194
		$this->storage->storeAccessToken($token, $this->serviceName);
195
196
		return $this;
197
	}
198
199
	/**
200
	 * @inheritDoc
201
	 * @codeCoverageIgnore
202
	 */
203
	public function setRequestFactory(RequestFactoryInterface $requestFactory):OAuthInterface{
204
		$this->requestFactory = $requestFactory;
205
206
		return $this;
207
	}
208
209
	/**
210
	 * @inheritDoc
211
	 * @codeCoverageIgnore
212
	 */
213
	public function setStreamFactory(StreamFactoryInterface $streamFactory):OAuthInterface{
214
		$this->streamFactory = $streamFactory;
215
216
		return $this;
217
	}
218
219
	/**
220
	 * @inheritDoc
221
	 * @codeCoverageIgnore
222
	 */
223
	public function setUriFactory(UriFactoryInterface $uriFactory):OAuthInterface{
224
		$this->uriFactory = $uriFactory;
225
226
		return $this;
227
	}
228
229
	/**
230
	 * Creates an access token with the provider set to $this->serviceName
231
	 */
232
	protected function createAccessToken():AccessToken{
233
		return new AccessToken(['provider' => $this->serviceName]);
234
	}
235
236
	/**
237
	 * @inheritDoc
238
	 */
239
	public function request(
240
		string $path,
241
		array $params = null,
242
		string $method = null,
243
		StreamInterface|array|string $body = null,
244
		array $headers = null
245
	):ResponseInterface{
246
247
		$request = $this->requestFactory
248
			->createRequest($method ?? 'GET', QueryUtil::merge($this->getRequestTarget($path), $params ?? []));
249
250
		foreach(array_merge($this->apiHeaders, $headers ?? []) as $header => $value){
251
			$request = $request->withAddedHeader($header, $value);
252
		}
253
254
		if($request->hasHeader('content-type')){
255
			$contentType = strtolower($request->getHeaderLine('content-type'));
256
257
			if(is_array($body)){
258
				if($contentType === 'application/x-www-form-urlencoded'){
259
					$body = $this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738));
260
				}
261
				elseif(in_array($contentType, ['application/json', 'application/vnd.api+json'])){
262
					$body = $this->streamFactory->createStream(json_encode($body));
263
				}
264
			}
265
			elseif(is_string($body)){
0 ignored issues
show
introduced by
The condition is_string($body) is always false.
Loading history...
266
				// we don't check if the given string matches the content type - this is the implementor's responsibility
267
				$body = $this->streamFactory->createStream($body);
268
			}
269
		}
270
271
		if($body instanceof StreamInterface){
272
			$request = $request
273
				->withBody($body)
274
				->withHeader('Content-length', (string)$body->getSize())
275
			;
276
		}
277
278
		return $this->sendRequest($request);
279
	}
280
281
	/**
282
	 * Determine the request target from the given URI (path segment or URL) with respect to $apiURL,
283
	 * anything except host and path will be ignored, scheme will always be set to "https".
284
	 * Throws if the given path is invalid or if the host of a given URL does not match $apiURL.
285
	 *
286
	 * @see \chillerlan\OAuth\Core\OAuthInterface::request()
287
	 *
288
	 * @throws \chillerlan\OAuth\Core\ProviderException
289
	 */
290
	protected function getRequestTarget(string $uri):string{
291
		$parsedURL = QueryUtil::parseUrl($uri);
292
293
		if(!isset($parsedURL['path'])){
294
			throw new ProviderException('invalid path');
295
		}
296
297
		// for some reason we were given a host name
298
		if(isset($parsedURL['host'])){
299
			$api  = QueryUtil::parseUrl($this->apiURL);
300
			$host = $api['host'] ?? null;
301
302
			// back out if it doesn't match
303
			if($parsedURL['host'] !== $host){
304
				throw new ProviderException(sprintf('given host (%s) does not match provider (%s)', $parsedURL['host'] , $host));
305
			}
306
307
			// we explicitly ignore any existing parameters here
308
			return 'https://'.$parsedURL['host'].$parsedURL['path'];
309
		}
310
311
		// $apiURL may already include a part of the path
312
		return $this->apiURL.$parsedURL['path'];
313
	}
314
315
	/**
316
	 * @inheritDoc
317
	 */
318
	public function sendRequest(RequestInterface $request):ResponseInterface{
319
320
		// get authorization only if we request the provider API
321
		if(str_starts_with((string)$request->getUri(), $this->apiURL)){
322
			$token = $this->storage->getAccessToken($this->serviceName);
323
324
			// attempt to refresh an expired token
325
			if(
326
				$this instanceof TokenRefresh
327
				&& $this->options->tokenAutoRefresh
0 ignored issues
show
Bug Best Practice introduced by
The property $tokenAutoRefresh is declared protected in chillerlan\OAuth\OAuthOptions. Since you implement __get, consider adding a @property or @property-read.
Loading history...
328
				&& ($token->isExpired() || $token->expires === $token::EOL_UNKNOWN)
329
			){
330
				$token = $this->refreshAccessToken($token);
331
			}
332
333
			$request = $this->getRequestAuthorization($request, $token);
334
		}
335
336
		return $this->http->sendRequest($request);
337
	}
338
339
	/**
340
	 * @inheritDoc
341
	 * @codeCoverageIgnore
342
	 */
343
	public function me():ResponseInterface{
344
		throw new ProviderException('not implemented');
345
	}
346
347
}
348