Passed
Pull Request — master (#1266)
by Matthew
04:02
created

ExternalScrobbler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 11
dl 0
loc 24
rs 9.9
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Matthew Wells
10
 * @copyright Matthew Wells 2025
11
 */
12
13
namespace OCA\Music\Service;
14
15
use DateTime;
16
use OCA\Music\AppFramework\Core\Logger;
17
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
18
use OCA\Music\BusinessLayer\TrackBusinessLayer;
19
use OCA\Music\Db\Track;
20
use OCP\IConfig;
21
use OCP\IURLGenerator;
22
use OCP\Security\ICrypto;
23
24
class ExternalScrobbler implements Scrobbler
25
{
26
	private IConfig $config;
27
	private Logger $logger;
28
	private IURLGenerator $urlGenerator;
29
	private TrackBusinessLayer $trackBusinessLayer;
30
	private AlbumBusinessLayer $albumBusinessLayer;
31
	private ICrypto $crypto;
32
	private string $name;
33
	private string $identifier;
34
	private string $endpoint;
35
	private string $tokenRequestUrl;
36
	private ?string $appName;
37
38
	public function __construct(
39
		IConfig $config,
40
		Logger $logger,
41
		IURLGenerator $urlGenerator,
42
		TrackBusinessLayer $trackBusinessLayer,
43
		AlbumBusinessLayer $albumBusinessLayer,
44
		ICrypto $crypto,
45
		string $name,
46
		string $identifier,
47
		string $endpoint,
48
		string $tokenRequestUrl,
49
		?string $appName = 'music'
50
	) {
51
		$this->config = $config;
52
		$this->logger = $logger;
53
		$this->urlGenerator = $urlGenerator;
54
		$this->trackBusinessLayer = $trackBusinessLayer;
55
		$this->albumBusinessLayer = $albumBusinessLayer;
56
		$this->crypto = $crypto;
57
		$this->name = $name;
58
		$this->identifier = $identifier;
59
		$this->endpoint = $endpoint;
60
		$this->tokenRequestUrl = $tokenRequestUrl;
61
		$this->appName = $appName;
62
	}
63
64
	/**
65
	 * @throws \Exception when curl initialization or session key save fails
66
	 * @throws ScrobbleServiceException when auth.getSession call fails
67
	 */
68
	public function generateSession(string $token, string $userId) : void {
69
		$ch = $this->makeCurlHandle();
70
		\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query(
71
			$this->generateMethodParams('auth.getSession', ['token' => $token])
72
		));
73
		$xml = \simplexml_load_string(\curl_exec($ch));
0 ignored issues
show
Bug introduced by
It seems like curl_exec($ch) can also be of type true; however, parameter $data of simplexml_load_string() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

73
		$xml = \simplexml_load_string(/** @scrutinizer ignore-type */ \curl_exec($ch));
Loading history...
74
75
		$status = (string)$xml['status'];
76
		if ($status !== 'ok') {
77
			throw new ScrobbleServiceException((string)$xml->error, (int)$xml->error['code']);
78
		}
79
		$sessionValue = (string)$xml->session->key;
80
81
		$this->saveApiSession($userId, $sessionValue);
82
	}
83
84
	/**
85
	 * @throws \InvalidArgumentException
86
	 */
87
	public function clearSession(?string $userId) : void {
88
		try {
89
			$this->config->deleteUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
90
		} catch (\InvalidArgumentException $e) {
91
			$this->logger->error(
92
				'Could not delete user config "' . $this->identifier . '.scrobbleSessionKey". ' . $e->getMessage()
93
			);
94
			throw $e;
95
		}
96
	}
97
98
	public function getApiSession(string $userId) : ?string {
99
		$encryptedKey = $this->config->getUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey');
100
		if (!$encryptedKey) {
101
			return null;
102
		}
103
		$key = $this->crypto->decrypt($encryptedKey, $userId . $this->config->getSystemValue('secret'));
104
		return $key;
105
	}
106
107
	public function getName() : string {
108
		return $this->name;
109
	}
110
111
	public function getIdentifier() : string {
112
		return $this->identifier;
113
	}
114
115
	public function getApiKey() : ?string {
116
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_key', null);
117
	}
118
119
	public function getApiSecret() : ?string {
120
		return $this->config->getSystemValue('music.' . $this->identifier . '_api_secret', null);
121
	}
122
123
	public function recordTrackPlayed(int $trackId, string $userId, ?\DateTime $timeOfPlay = null) : void {
124
		$timeOfPlay = $timeOfPlay ?? new \DateTime();
125
		$sessionKey = $this->getApiSession($userId);
126
		if (!$sessionKey) {
127
			return;
128
		}
129
130
		$timestamp = $timeOfPlay->getTimestamp();
131
		$scrobbleData = [
132
			'sk' => $sessionKey
133
		];
134
135
		/** @var array<Track> $tracks */
136
		$tracks = $this->trackBusinessLayer->findById([$trackId], $userId);
137
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $userId);
138
		foreach ($tracks as $i => $track) {
139
			if ($track->getAlbum()) {
140
				$albumArtist = $track->getAlbum()->getAlbumArtistName();
141
				if ($albumArtist !== $track->getArtistName()) {
142
					$scrobbleData["albumArtist[{$i}]"] = $albumArtist;
143
				}
144
			}
145
			$scrobbleData["artist[{$i}]"] = $track->getArtistName();
146
			$scrobbleData["track[{$i}]"] = $track->getTitle();
147
			$scrobbleData["timestamp[{$i}]"] = $timestamp;
148
			$scrobbleData["album[{$i}]"] = $track->getAlbumName();
149
			$scrobbleData["trackNumber[{$i}]"] = $track->getNumber();
150
		}
151
		$scrobbleData = $this->generateMethodParams('track.scrobble', $scrobbleData);
152
153
		$ch = $this->makeCurlHandle();
154
		\curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($scrobbleData));
155
		$xml = \simplexml_load_string(\curl_exec($ch));
0 ignored issues
show
Bug introduced by
It seems like curl_exec($ch) can also be of type true; however, parameter $data of simplexml_load_string() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

155
		$xml = \simplexml_load_string(/** @scrutinizer ignore-type */ \curl_exec($ch));
Loading history...
156
157
		if ((string)$xml['status'] !== 'ok') {
158
			$this->logger->warning('Failed to scrobble to ' . $this->name);
159
		}
160
	}
161
162
	public function getTokenRequestUrl(): ?string {
163
		$apiKey = $this->getApiKey();
164
		if (!$apiKey) {
165
			return null;
166
		}
167
168
		$tokenHandleUrl = $this->urlGenerator->linkToRouteAbsolute('music.scrobbler.handleToken', [
169
			'serviceIdentifier' => $this->identifier
170
		]);
171
		return "{$this->tokenRequestUrl}?api_key={$apiKey}&cb={$tokenHandleUrl}";
172
	}
173
174
	private function saveApiSession(string $userId, string $sessionValue) : void {
175
		try {
176
			$encryptedKey = $this->crypto->encrypt(
177
				$sessionValue,
178
				$userId . $this->config->getSystemValue('secret')
179
			);
180
			$this->config->setUserValue($userId, $this->appName, $this->identifier . '.scrobbleSessionKey', $encryptedKey);
181
		} catch (\Exception $e) {
182
			$this->logger->error('Encryption of scrobble session key failed');
183
			throw $e;
184
		}
185
	}
186
187
	/**
188
	 * @param array<string, string|array> $params
189
	 */
190
	private function generateSignature(array $params) : string {
191
		\ksort($params);
192
		$paramString = '';
193
		foreach ($params as $key => $value) {
194
			if (\is_array($value)) {
195
				foreach ($value as $valIdx => $valVal) {
196
					$paramString .= "{$key}[{$valIdx}]{$valVal}";
197
				}
198
			} else {
199
				$paramString .= $key . $value;
200
			}
201
		}
202
203
		$paramString .= $apiSecret = $this->getApiSecret();
0 ignored issues
show
Unused Code introduced by
The assignment to $apiSecret is dead and can be removed.
Loading history...
204
		return \md5($paramString);
205
	}
206
207
	/**
208
	 * @return array<string, string>
209
	 * @param array<string, mixed> $moreParams
210
	 * @param bool $sign
211
	 * @return array<string, mixed>
212
	 */
213
	private function generateMethodParams(string $method, array $moreParams = [], bool $sign = true) : array {
214
		$params = \array_merge($moreParams, [
215
			'method' => $method,
216
			'api_key' => $this->getApiKey()
217
		]);
218
219
		if ($sign) {
220
			$params['api_sig'] = $this->generateSignature($params);
221
		}
222
223
		return $params;
224
	}
225
226
	/**
227
	 * @return resource (in PHP8+ return \CurlHandle)
228
	 * @throws \RuntimeException when unable to initialize a cURL handle
229
	 */
230
	private function makeCurlHandle() {
231
		$ch = \curl_init();
232
		if (!$ch) {
233
			$this->logger->error('Failed to initialize a curl handle, is the php curl extension installed?');
234
			throw new \RuntimeException('Unable to initialize a curl handle');
235
		}
236
		\curl_setopt($ch, \CURLOPT_URL, $this->endpoint);
237
		\curl_setopt($ch, \CURLOPT_CONNECTTIMEOUT, 10);
238
		\curl_setopt($ch, \CURLOPT_POST, true);
239
		\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
240
		return $ch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ch also could return the type CurlHandle which is incompatible with the documented return type resource.
Loading history...
241
	}
242
}
243