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

SettingController::getScrobbleAuth()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 14
rs 9.9332
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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2025
13
 */
14
15
namespace OCA\Music\Controller;
16
17
use OCA\Music\Service\ScrobblerService;
18
use OCP\AppFramework\Controller;
19
use OCP\AppFramework\Http;
20
use OCP\AppFramework\Http\JSONResponse;
21
use OCP\IRequest;
22
use OCP\IURLGenerator;
23
use OCP\Security\ISecureRandom;
24
25
use OCA\Music\AppFramework\Core\Logger;
26
use OCA\Music\Db\AmpacheSessionMapper;
27
use OCA\Music\Db\AmpacheUserMapper;
28
use OCA\Music\Http\ErrorResponse;
29
use OCA\Music\Service\LibrarySettings;
30
use OCA\Music\Service\Scanner;
31
use OCA\Music\Utility\AppInfo;
32
use OCA\Music\Utility\StringUtil;
33
34
class SettingController extends Controller {
35
	const DEFAULT_PASSWORD_LENGTH = 10;
36
	/* Character set without look-alike characters. Similar but even more stripped set would be found
37
	 * on Nextcloud as ISecureRandom::CHAR_HUMAN_READABLE but that's not available on ownCloud. */
38
	const API_KEY_CHARSET = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
39
40
	private AmpacheSessionMapper $ampacheSessionMapper;
41
	private AmpacheUserMapper $ampacheUserMapper;
42
	private Scanner $scanner;
43
	private string $userId;
44
	private LibrarySettings $librarySettings;
45
	private ISecureRandom $secureRandom;
46
	private IURLGenerator $urlGenerator;
47
	private Logger $logger;
48
	/** @var ScrobblerService[] $scrobblerServices */
49
	private array $scrobblerServices;
50
51
	public function __construct(string $appName,
52
								IRequest $request,
53
								AmpacheSessionMapper $ampacheSessionMapper,
54
								AmpacheUserMapper $ampacheUserMapper,
55
								Scanner $scanner,
56
								?string $userId,
57
								LibrarySettings $librarySettings,
58
								ISecureRandom $secureRandom,
59
								IURLGenerator $urlGenerator,
60
								Logger $logger,
61
								array $scrobblerServices) {
62
		parent::__construct($appName, $request);
63
64
		$this->ampacheSessionMapper = $ampacheSessionMapper;
65
		$this->ampacheUserMapper = $ampacheUserMapper;
66
		$this->scanner = $scanner;
67
		$this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; the null case should happen only when the user has already logged out
68
		$this->librarySettings = $librarySettings;
69
		$this->secureRandom = $secureRandom;
70
		$this->urlGenerator = $urlGenerator;
71
		$this->logger = $logger;
72
		$this->scrobblerServices = $scrobblerServices;
73
	}
74
75
	/**
76
	 * @NoAdminRequired
77
	 * @NoCSRFRequired
78
	 * @UseSession to keep the session reserved while execution in progress
79
	 */
80
	public function userPath(string $value) : JSONResponse {
81
		$prevPath = $this->librarySettings->getPath($this->userId);
82
		$success = $this->librarySettings->setPath($this->userId, $value);
83
84
		if ($success) {
85
			$this->scanner->updatePath($prevPath, $value, $this->userId);
86
		}
87
88
		return new JSONResponse(['success' => $success]);
89
	}
90
91
	/**
92
	 * @NoAdminRequired
93
	 * @NoCSRFRequired
94
	 */
95
	public function userExcludedPaths(array $value) : JSONResponse {
96
		$success = $this->librarySettings->setExcludedPaths($this->userId, $value);
97
		return new JSONResponse(['success' => $success]);
98
	}
99
100
	/**
101
	 * @NoAdminRequired
102
	 * @NoCSRFRequired
103
	 */
104
	public function enableScanMetadata(bool $value) : JSONResponse {
105
		$this->librarySettings->setScanMetadataEnabled($this->userId, $value);
106
		return new JSONResponse(['success' => true]);
107
	}
108
109
	/**
110
	 * @NoAdminRequired
111
	 * @NoCSRFRequired
112
	 */
113
	public function ignoredArticles(array $value) : JSONResponse {
114
		$this->librarySettings->setIgnoredArticles($this->userId, $value);
115
		return new JSONResponse(['success' => true]);
116
	}
117
118
	/**
119
	 * @NoAdminRequired
120
	 * @NoCSRFRequired
121
	 */
122
	public function getAll() : JSONResponse {
123
		return new JSONResponse([
124
			'path' => $this->librarySettings->getPath($this->userId),
125
			'excludedPaths' => $this->librarySettings->getExcludedPaths($this->userId),
126
			'scanMetadata' => $this->librarySettings->getScanMetadataEnabled($this->userId),
127
			'ignoredArticles' => $this->librarySettings->getIgnoredArticles($this->userId),
128
			'ampacheUrl' => $this->getAmpacheUrl(),
129
			'subsonicUrl' => $this->getSubsonicUrl(),
130
			'ampacheKeys' => $this->ampacheUserMapper->getAll($this->userId),
131
			'appVersion' => AppInfo::getVersion(),
132
			'user' => $this->userId,
133
			'scrobblers' => $this->getScrobbleAuth()
134
		]);
135
	}
136
137
	/**
138
	 * @NoAdminRequired
139
	 * @NoCSRFRequired
140
	 */
141
	public function getUserKeys() : JSONResponse {
142
		return new JSONResponse($this->ampacheUserMapper->getAll($this->userId));
143
	}
144
145
	private function getAmpacheUrl() : string {
146
		return (string)\str_replace('/server/xml.server.php', '',
147
				$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('music.ampache.xmlApi')));
148
	}
149
150
	private function getSubsonicUrl() : string {
151
		return (string)\str_replace('/rest/dummy', '',
152
				$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute(
153
						'music.subsonic.handleRequest', ['method' => 'dummy'])));
154
	}
155
156
	private function getScrobbleAuth(): array {
157
		$services = [];
158
		foreach ($this->scrobblerServices as $scrobblerService) {
159
			$tokenRequestUrl = $scrobblerService->getTokenRequestUrl();
160
			$services[] = [
161
				'service' => $scrobblerService->getName(),
162
				'identifier' => $scrobblerService->getIdentifier(),
163
				'configured' => $tokenRequestUrl && $scrobblerService->getApiSecret(),
164
				'tokenRequestUrl' => $tokenRequestUrl,
165
				'hasSession' => $scrobblerService->getApiSession($this->userId) !== null
166
			];
167
		}
168
169
		return $services;
170
	}
171
172
	private function storeUserKey(?string $description, string $password) : ?int {
173
		$hash = \hash('sha256', $password);
174
		$description = StringUtil::truncate($description, 64); // some DB setups can't truncate automatically to column max size
175
		return $this->ampacheUserMapper->addUserKey($this->userId, $hash, $description);
176
	}
177
178
	/**
179
	 * @NoAdminRequired
180
	 */
181
	public function createUserKey(?int $length, ?string $description) : JSONResponse {
182
		if ($length === null || $length < self::DEFAULT_PASSWORD_LENGTH) {
183
			$length = self::DEFAULT_PASSWORD_LENGTH;
184
		}
185
186
		$password = $this->secureRandom->generate($length, self::API_KEY_CHARSET);
187
188
		$id = $this->storeUserKey($description, $password);
189
190
		if ($id === null) {
191
			return new ErrorResponse(Http::STATUS_INTERNAL_SERVER_ERROR, 'Error while saving the credentials');
192
		}
193
194
		return new JSONResponse(['id' => $id, 'password' => $password, 'description' => $description], Http::STATUS_CREATED);
195
	}
196
197
	/**
198
	 * The CORS-version of the key creation function is targeted for external clients. We need separate function
199
	 * because the CORS middleware blocks the normal internal access on Nextcloud versions older than 25 as well
200
	 * as on ownCloud 10.0, at least (but not on OC 10.4+).
201
	 *
202
	 * @NoAdminRequired
203
	 * @CORS
204
	 */
205
	public function createUserKeyCors(?int $length, ?string $description) : JSONResponse {
206
		return $this->createUserKey($length, $description);
207
	}
208
209
	/**
210
	 * @NoAdminRequired
211
	 * @NoCSRFRequired
212
	 */
213
	public function removeUserKey(int $id) : JSONResponse {
214
		$this->ampacheSessionMapper->revokeSessions($id);
215
		$this->ampacheUserMapper->removeUserKey($this->userId, $id);
216
		return new JSONResponse(['success' => true]);
217
	}
218
}
219