Passed
Push — main ( e868ec...9f95cc )
by smiley
12:00
created

LastFM::request()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 44
rs 8.9457
cc 6
nc 9
nop 5
1
<?php
2
/**
3
 * Class LastFM
4
 *
5
 * @created      10.04.2018
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2018 Smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\OAuth\Providers;
12
13
use chillerlan\HTTP\Utils\MessageUtil;
14
use chillerlan\HTTP\Utils\QueryUtil;
15
use chillerlan\OAuth\Core\{AccessToken, OAuthProvider, ProviderException};
16
use Psr\Http\Message\{RequestInterface, ResponseInterface, StreamInterface, UriInterface};
17
use Throwable;
18
use function array_merge;
19
use function in_array;
20
use function is_array;
21
use function ksort;
22
use function md5;
23
use function sprintf;
24
use function trigger_error;
25
use const PHP_QUERY_RFC1738;
26
27
/**
28
 * @see https://www.last.fm/api/authentication
29
 */
30
class LastFM extends OAuthProvider{
31
32
	public const PERIOD_OVERALL = 'overall';
33
	public const PERIOD_7DAY    = '7day';
34
	public const PERIOD_1MONTH  = '1month';
35
	public const PERIOD_3MONTH  = '3month';
36
	public const PERIOD_6MONTH  = '6month';
37
	public const PERIOD_12MONTH = '12month';
38
	public const PERIODS        = [
39
		self::PERIOD_OVERALL,
40
		self::PERIOD_7DAY,
41
		self::PERIOD_1MONTH,
42
		self::PERIOD_3MONTH,
43
		self::PERIOD_6MONTH,
44
		self::PERIOD_12MONTH,
45
	];
46
47
	protected string  $authURL        = 'https://www.last.fm/api/auth';
48
	protected string  $apiURL         = 'https://ws.audioscrobbler.com/2.0';
49
	protected ?string $userRevokeURL  = 'https://www.last.fm/settings/applications';
50
	protected ?string $apiDocs        = 'https://www.last.fm/api/';
51
	protected ?string $applicationURL = 'https://www.last.fm/api/account/create';
52
53
	/**
54
	 * @inheritdoc
55
	 */
56
	public function getAuthURL(array $params = null):UriInterface{
57
58
		$params = array_merge($params ?? [], [
59
			'api_key' => $this->options->key,
60
		]);
61
62
		return $this->uriFactory->createUri(QueryUtil::merge($this->authURL, $params));
63
	}
64
65
	/**
66
	 *
67
	 */
68
	protected function getSignature(array $params):string{
69
		ksort($params);
70
71
		$signature = '';
72
73
		foreach($params as $k => $v){
74
			if(!in_array($k, ['format', 'callback'])){
75
				$signature .= $k.$v;
76
			}
77
		}
78
79
		return md5($signature.$this->options->secret);
80
	}
81
82
	/**
83
	 *
84
	 */
85
	public function getAccessToken(string $session_token):AccessToken{
86
87
		$params = [
88
			'method'  => 'auth.getSession',
89
			'format'  => 'json',
90
			'api_key' => $this->options->key,
91
			'token'   => $session_token,
92
		];
93
94
		$params['api_sig'] = $this->getSignature($params);
95
96
		/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable */
97
		$request = $this->requestFactory->createRequest('GET', QueryUtil::merge($this->apiURL, $params));
98
99
		return $this->parseTokenResponse($this->http->sendRequest($request));
100
	}
101
102
	/**
103
	 * @throws \chillerlan\OAuth\Core\ProviderException
104
	 */
105
	protected function parseTokenResponse(ResponseInterface $response):AccessToken{
106
107
		try{
108
			$data = MessageUtil::decodeJSON($response, true);
109
110
			if(!$data || !is_array($data)){
111
				trigger_error('');
112
			}
113
		}
114
		catch(Throwable $e){
115
			throw new ProviderException('unable to parse token response');
116
		}
117
118
		if(isset($data['error'])){
119
			throw new ProviderException('error retrieving access token: '.$data['message']);
120
		}
121
		elseif(!isset($data['session']['key'])){
122
			throw new ProviderException('token missing');
123
		}
124
125
		$token = new AccessToken;
126
127
		$token->provider     = $this->serviceName;
128
		$token->accessToken  = $data['session']['key'];
129
		$token->expires      = AccessToken::EOL_NEVER_EXPIRES;
130
131
		unset($data['session']['key']);
132
133
		$token->extraParams = $data;
134
135
		$this->storage->storeAccessToken($token, $this->serviceName);
136
137
		return $token;
138
	}
139
140
	/**
141
	 * @inheritDoc
142
	 */
143
	public function request(
144
		string $path,
145
		array $params = null,
146
		string $method = null,
147
		StreamInterface|array|string $body = null,
148
		array $headers = null
149
	):ResponseInterface{
150
151
		if($body !== null && !is_array($body)){
0 ignored issues
show
introduced by
The condition is_array($body) is always true.
Loading history...
152
			throw new ProviderException('$body must be an array');
153
		}
154
155
		$method ??= 'GET';
156
		$params ??= [];
157
		$body   ??= [];
158
159
		$params = array_merge($params, $body, [
160
			'method'  => $path,
161
			'format'  => 'json',
162
			'api_key' => $this->options->key,
163
			'sk'      => $this->storage->getAccessToken($this->serviceName)->accessToken,
164
		]);
165
166
		$params['api_sig'] = $this->getSignature($params);
167
168
		if($method === 'POST'){
169
			$body   = $params;
170
			$params = [];
171
		}
172
173
		/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable */
174
		$request = $this->requestFactory->createRequest($method, QueryUtil::merge($this->apiURL, $params));
175
176
		foreach(array_merge($this->apiHeaders, $headers ?? []) as $header => $value){
177
			$request = $request->withAddedHeader($header, $value);
178
		}
179
180
		if($method === 'POST'){
181
			$request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded');
182
			$body    = $this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738));
183
			$request = $request->withBody($body);
184
		}
185
186
		return $this->http->sendRequest($request);
187
	}
188
189
	/**
190
	 * @inheritDoc
191
	 * @codeCoverageIgnore
192
	 */
193
	public function getRequestAuthorization(RequestInterface $request, AccessToken $token):RequestInterface{
194
		return $request;
195
	}
196
197
	/**
198
	 * @inheritDoc
199
	 */
200
	public function me():ResponseInterface{
201
		$response = $this->request('user.getInfo');
202
		$status   = $response->getStatusCode();
203
204
		if($status === 200){
205
			return $response;
206
		}
207
208
		$json = MessageUtil::decodeJSON($response);
209
210
		if(isset($json->error, $json->error_description)){
211
			throw new ProviderException($json->error_description);
212
		}
213
214
		throw new ProviderException(sprintf('user info error error HTTP/%s', $status));
215
	}
216
217
	/**
218
	 * @todo
219
	 *
220
	 * @param array $tracks
221
	 */
222
#	public function scrobble(array $tracks){}
223
224
}
225