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

SettingController::getScrobbleAuth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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
	private ScrobblerService $scrobblerService;
49
50
	public function __construct(string $appName,
51
								IRequest $request,
52
								AmpacheSessionMapper $ampacheSessionMapper,
53
								AmpacheUserMapper $ampacheUserMapper,
54
								Scanner $scanner,
55
								?string $userId,
56
								LibrarySettings $librarySettings,
57
								ISecureRandom $secureRandom,
58
								IURLGenerator $urlGenerator,
59
								Logger $logger,
60
								ScrobblerService $scrobblerService) {
61
		parent::__construct($appName, $request);
62
63
		$this->ampacheSessionMapper = $ampacheSessionMapper;
64
		$this->ampacheUserMapper = $ampacheUserMapper;
65
		$this->scanner = $scanner;
66
		$this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; the null case should happen only when the user has already logged out
67
		$this->librarySettings = $librarySettings;
68
		$this->secureRandom = $secureRandom;
69
		$this->urlGenerator = $urlGenerator;
70
		$this->logger = $logger;
71
		$this->scrobblerService = $scrobblerService;
72
	}
73
74
	/**
75
	 * @NoAdminRequired
76
	 * @NoCSRFRequired
77
	 * @UseSession to keep the session reserved while execution in progress
78
	 */
79
	public function userPath(string $value) : JSONResponse {
80
		$prevPath = $this->librarySettings->getPath($this->userId);
81
		$success = $this->librarySettings->setPath($this->userId, $value);
82
83
		if ($success) {
84
			$this->scanner->updatePath($prevPath, $value, $this->userId);
85
		}
86
87
		return new JSONResponse(['success' => $success]);
88
	}
89
90
	/**
91
	 * @NoAdminRequired
92
	 * @NoCSRFRequired
93
	 */
94
	public function userExcludedPaths(array $value) : JSONResponse {
95
		$success = $this->librarySettings->setExcludedPaths($this->userId, $value);
96
		return new JSONResponse(['success' => $success]);
97
	}
98
99
	/**
100
	 * @NoAdminRequired
101
	 * @NoCSRFRequired
102
	 */
103
	public function enableScanMetadata(bool $value) : JSONResponse {
104
		$this->librarySettings->setScanMetadataEnabled($this->userId, $value);
105
		return new JSONResponse(['success' => true]);
106
	}
107
108
	/**
109
	 * @NoAdminRequired
110
	 * @NoCSRFRequired
111
	 */
112
	public function ignoredArticles(array $value) : JSONResponse {
113
		$this->librarySettings->setIgnoredArticles($this->userId, $value);
114
		return new JSONResponse(['success' => true]);
115
	}
116
117
	/**
118
	 * @NoAdminRequired
119
	 * @NoCSRFRequired
120
	 */
121
	public function getAll() : JSONResponse {
122
		return new JSONResponse([
123
			'path' => $this->librarySettings->getPath($this->userId),
124
			'excludedPaths' => $this->librarySettings->getExcludedPaths($this->userId),
125
			'scanMetadata' => $this->librarySettings->getScanMetadataEnabled($this->userId),
126
			'ignoredArticles' => $this->librarySettings->getIgnoredArticles($this->userId),
127
			'ampacheUrl' => $this->getAmpacheUrl(),
128
			'subsonicUrl' => $this->getSubsonicUrl(),
129
			'ampacheKeys' => $this->ampacheUserMapper->getAll($this->userId),
130
			'appVersion' => AppInfo::getVersion(),
131
			'user' => $this->userId,
132
			'scrobbler' => $this->getScrobbleAuth()
133
		]);
134
	}
135
136
	/**
137
	 * @NoAdminRequired
138
	 * @NoCSRFRequired
139
	 */
140
	public function getUserKeys() : JSONResponse {
141
		return new JSONResponse($this->ampacheUserMapper->getAll($this->userId));
142
	}
143
144
	private function getAmpacheUrl() : string {
145
		return (string)\str_replace('/server/xml.server.php', '',
146
				$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('music.ampache.xmlApi')));
147
	}
148
149
	private function getSubsonicUrl() : string {
150
		return (string)\str_replace('/rest/dummy', '',
151
				$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute(
152
						'music.subsonic.handleRequest', ['method' => 'dummy'])));
153
	}
154
155
	private function getScrobbleAuth(): array {
156
		$tokenRequestUrl = $this->scrobblerService->getTokenRequestUrl();
157
		return [
158
			'apiService' => $this->scrobblerService->getApiService(),
159
            'configured' => $tokenRequestUrl !== null,
160
            'tokenRequestUrl' => $tokenRequestUrl,
161
            'hasSession' => $this->scrobblerService->getApiSession($this->userId) !== null,
162
            'service' => $this->scrobblerService->getName()
163
		];
164
	}
165
166
	private function storeUserKey(?string $description, string $password) : ?int {
167
		$hash = \hash('sha256', $password);
168
		$description = StringUtil::truncate($description, 64); // some DB setups can't truncate automatically to column max size
169
		return $this->ampacheUserMapper->addUserKey($this->userId, $hash, $description);
170
	}
171
172
	/**
173
	 * @NoAdminRequired
174
	 */
175
	public function createUserKey(?int $length, ?string $description) : JSONResponse {
176
		if ($length === null || $length < self::DEFAULT_PASSWORD_LENGTH) {
177
			$length = self::DEFAULT_PASSWORD_LENGTH;
178
		}
179
180
		$password = $this->secureRandom->generate($length, self::API_KEY_CHARSET);
181
182
		$id = $this->storeUserKey($description, $password);
183
184
		if ($id === null) {
185
			return new ErrorResponse(Http::STATUS_INTERNAL_SERVER_ERROR, 'Error while saving the credentials');
186
		}
187
188
		return new JSONResponse(['id' => $id, 'password' => $password, 'description' => $description], Http::STATUS_CREATED);
189
	}
190
191
	/**
192
	 * The CORS-version of the key creation function is targeted for external clients. We need separate function
193
	 * because the CORS middleware blocks the normal internal access on Nextcloud versions older than 25 as well
194
	 * as on ownCloud 10.0, at least (but not on OC 10.4+).
195
	 *
196
	 * @NoAdminRequired
197
	 * @CORS
198
	 */
199
	public function createUserKeyCors(?int $length, ?string $description) : JSONResponse {
200
		return $this->createUserKey($length, $description);
201
	}
202
203
	/**
204
	 * @NoAdminRequired
205
	 * @NoCSRFRequired
206
	 */
207
	public function removeUserKey(int $id) : JSONResponse {
208
		$this->ampacheSessionMapper->revokeSessions($id);
209
		$this->ampacheUserMapper->removeUserKey($this->userId, $id);
210
		return new JSONResponse(['success' => true]);
211
	}
212
}
213