Passed
Push — master ( 7b42d0...d80bc5 )
by Pauli
04:15 queued 21s
created

RadioApiController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 10
c 1
b 0
f 0
nc 1
nop 11
dl 0
loc 21
rs 9.9332

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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2020 - 2025
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use OCP\AppFramework\Controller;
16
use OCP\AppFramework\Http;
17
use OCP\AppFramework\Http\JSONResponse;
18
use OCP\AppFramework\Http\RedirectResponse;
19
use OCP\AppFramework\Http\Response;
20
21
use OCP\Files\Folder;
22
use OCP\IConfig;
23
use OCP\IRequest;
24
use OCP\IURLGenerator;
25
26
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
27
use OCA\Music\AppFramework\Core\Logger;
28
use OCA\Music\AppFramework\Utility\FileExistsException;
29
use OCA\Music\BusinessLayer\RadioStationBusinessLayer;
30
use OCA\Music\Http\ErrorResponse;
31
use OCA\Music\Http\FileResponse;
32
use OCA\Music\Http\RelayStreamResponse;
33
use OCA\Music\Service\PlaylistFileService;
34
use OCA\Music\Service\RadioService;
35
use OCA\Music\Service\StreamTokenService;
36
use OCA\Music\Utility\HttpUtil;
37
38
class RadioApiController extends Controller {
39
	private IConfig $config;
40
	private IURLGenerator $urlGenerator;
41
	private RadioStationBusinessLayer $businessLayer;
42
	private RadioService $service;
43
	private StreamTokenService $tokenService;
44
	private PlaylistFileService $playlistFileService;
45
	private string $userId;
46
	private ?Folder $userFolder;
47
	private Logger $logger;
48
49
	public function __construct(string $appname,
50
								IRequest $request,
51
								IConfig $config,
52
								IURLGenerator $urlGenerator,
53
								RadioStationBusinessLayer $businessLayer,
54
								RadioService $service,
55
								StreamTokenService $tokenService,
56
								PlaylistFileService $playlistFileService,
57
								?string $userId,
58
								?Folder $userFolder,
59
								Logger $logger) {
60
		parent::__construct($appname, $request);
61
		$this->config = $config;
62
		$this->urlGenerator = $urlGenerator;
63
		$this->businessLayer = $businessLayer;
64
		$this->service = $service;
65
		$this->tokenService = $tokenService;
66
		$this->playlistFileService = $playlistFileService;
67
		$this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; may be null when resolveStreamUrl used on public share
68
		$this->userFolder = $userFolder;
69
		$this->logger = $logger;
70
	}
71
72
	/**
73
	 * lists all radio stations
74
	 *
75
	 * @NoAdminRequired
76
	 * @NoCSRFRequired
77
	 */
78
	public function getAll() : JSONResponse {
79
		$stations = $this->businessLayer->findAll($this->userId);
80
		return new JSONResponse(
81
			\array_map(fn($s) => $s->toApi(), $stations)
82
		);
83
	}
84
85
	/**
86
	 * creates a station
87
	 *
88
	 * @NoAdminRequired
89
	 * @NoCSRFRequired
90
	 */
91
	public function create(?string $name, ?string $streamUrl, ?string $homeUrl) : JSONResponse {
92
		if ($streamUrl === null) {
93
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Mandatory argument 'streamUrl' not given");
94
		}
95
		
96
		try {
97
			$station = $this->businessLayer->create($this->userId, $name, $streamUrl, $homeUrl);
98
			return new JSONResponse($station->toApi());
99
		} catch (\DomainException $ex) {
100
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, $ex->getMessage());
101
		}
102
	}
103
104
	/**
105
	 * deletes a station
106
	 *
107
	 * @NoAdminRequired
108
	 * @NoCSRFRequired
109
	 */
110
	public function delete(int $id) : JSONResponse {
111
		try {
112
			$this->businessLayer->delete($id, $this->userId);
113
			return new JSONResponse([]);
114
		} catch (BusinessLayerException $ex) {
115
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
116
		}
117
	}
118
119
	/**
120
	 * get a single radio station
121
	 *
122
	 * @NoAdminRequired
123
	 * @NoCSRFRequired
124
	 */
125
	public function get(int $id) : JSONResponse {
126
		try {
127
			$station = $this->businessLayer->find($id, $this->userId);
128
			return new JSONResponse($station->toApi());
129
		} catch (BusinessLayerException $ex) {
130
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
131
		}
132
	}
133
134
	/**
135
	 * update a station
136
	 *
137
	 * @NoAdminRequired
138
	 * @NoCSRFRequired
139
	 */
140
	public function update(int $id, ?string $name = null, ?string $streamUrl = null, ?string $homeUrl = null) : JSONResponse {
141
		if ($name === null && $streamUrl === null && $homeUrl === null) {
142
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "at least one of the args ['name', 'streamUrl', 'homeUrl'] must be given");
143
		}
144
145
		try {
146
			$station = $this->businessLayer->updateStation($id, $this->userId, $name, $streamUrl, $homeUrl);
147
			return new JSONResponse($station->toApi());
148
		} catch (BusinessLayerException $ex) {
149
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
150
		} catch (\DomainException $ex) {
151
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, $ex->getMessage());
152
		}
153
	}
154
155
	/**
156
	 * export all radio stations to a file
157
	 *
158
	 * @param string $name target file name
159
	 * @param string $path parent folder path
160
	 * @param string $oncollision action to take on file name collision,
161
	 *								supported values:
162
	 *								- 'overwrite' The existing file will be overwritten
163
	 *								- 'keepboth' The new file is named with a suffix to make it unique
164
	 *								- 'abort' (default) The operation will fail
165
	 *
166
	 * @NoAdminRequired
167
	 * @NoCSRFRequired
168
	 */
169
	public function exportAllToFile(string $name, string $path, string $oncollision='abort') : JSONResponse {
170
		if ($this->userFolder === null) {
171
			// This shouldn't get actually run. The folder may be null in case the user has already logged out.
172
			// But in that case, the framework should block the execution before it reaches here.
173
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'no valid user folder got');
174
		}
175
		try {
176
			$exportedFilePath = $this->playlistFileService->exportRadioStationsToFile(
177
					$this->userId, $this->userFolder, $path, $name, $oncollision);
178
			return new JSONResponse(['wrote_to_file' => $exportedFilePath]);
179
		} catch (\OCP\Files\NotFoundException $ex) {
180
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found');
181
		} catch (FileExistsException $ex) {
182
			return new ErrorResponse(Http::STATUS_CONFLICT, 'file already exists', ['path' => $ex->getPath(), 'suggested_name' => $ex->getAltName()]);
183
		} catch (\OCP\Files\NotPermittedException $ex) {
184
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file');
185
		}
186
	}
187
188
	/**
189
	 * import radio stations from a file
190
	 * @param string $filePath path of the file to import
191
	 *
192
	 * @NoAdminRequired
193
	 * @NoCSRFRequired
194
	 */
195
	public function importFromFile(string $filePath) : JSONResponse {
196
		if ($this->userFolder === null) {
197
			// This shouldn't get actually run. The folder may be null in case the user has already logged out.
198
			// But in that case, the framework should block the execution before it reaches here.
199
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'no valid user folder got');
200
		}
201
		try {
202
			$result = $this->playlistFileService->importRadioStationsFromFile($this->userId, $this->userFolder, $filePath);
203
			$result['stations'] = \array_map(fn($s) => $s->toApi(), $result['stations']);
204
			return new JSONResponse($result);
205
		} catch (\OCP\Files\NotFoundException $ex) {
206
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
207
		} catch (\UnexpectedValueException $ex) {
208
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
209
		}
210
	}
211
212
	/**
213
	 * reset all the radio stations of the user
214
	 *
215
	 * @NoAdminRequired
216
	 * @NoCSRFRequired
217
	 */
218
	public function resetAll() : JSONResponse {
219
		$this->businessLayer->deleteAll($this->userId);
220
		return new JSONResponse(['success' => true]);
221
	}
222
223
	/**
224
	* get metadata for a channel
225
	*
226
	* @NoAdminRequired
227
	* @NoCSRFRequired
228
	*/
229
	public function getChannelInfo(int $id, ?string $type=null) : JSONResponse {
230
		try {
231
			$station = $this->businessLayer->find($id, $this->userId);
232
			$streamUrl = $station->getStreamUrl();
233
234
			switch ($type) {
235
				case 'icy':
236
					$metadata = $this->service->readIcyMetadata($streamUrl, 3, 5);
237
					break;
238
				case 'shoutcast-v1':
239
					$metadata = $this->service->readShoutcastV1Metadata($streamUrl);
240
					break;
241
				case 'shoutcast-v2':
242
					$metadata = $this->service->readShoutcastV2Metadata($streamUrl);
243
					break;
244
				case 'icecast':
245
					$metadata = $this->service->readIcecastMetadata($streamUrl);
246
					break;
247
				default:
248
					$metadata = $this->service->readIcyMetadata($streamUrl, 3, 5)
249
							?? $this->service->readShoutcastV2Metadata($streamUrl)
250
							?? $this->service->readIcecastMetadata($streamUrl)
251
							?? $this->service->readShoutcastV1Metadata($streamUrl);
252
					break;
253
			}
254
255
			return new JSONResponse($metadata);
256
		} catch (BusinessLayerException $ex) {
257
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
258
		}
259
	}
260
261
	/**
262
	 * get stream URL for a radio station
263
	 *
264
	 * @NoAdminRequired
265
	 * @NoCSRFRequired
266
	 */
267
	public function stationStreamUrl(int $id) : JSONResponse {
268
		try {
269
			$station = $this->businessLayer->find($id, $this->userId);
270
			$streamUrl = $station->getStreamUrl();
271
			$resolved = $this->service->resolveStreamUrl($streamUrl);
272
			$relayEnabled = $this->streamRelayEnabled();
273
			if ($relayEnabled && !$resolved['hls']) {
274
				$resolved['url'] = $this->urlGenerator->linkToRoute('music.radioApi.stationStream', ['id' => $id]);
275
			}
276
			return new JSONResponse($resolved);
277
		} catch (BusinessLayerException $ex) {
278
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
279
		}
280
	}
281
282
	/**
283
	 * get audio stream for a radio station
284
	 *
285
	 * @NoAdminRequired
286
	 * @NoCSRFRequired
287
	 */
288
	public function stationStream(int $id) : Response {
289
		try {
290
			$station = $this->businessLayer->find($id, $this->userId);
291
			$streamUrl = $station->getStreamUrl();
292
			$resolved = $this->service->resolveStreamUrl($streamUrl);
293
			if ($this->streamRelayEnabled()) {
294
				return new RelayStreamResponse($resolved['url']);
295
			} else {
296
				return new RedirectResponse($resolved['url']);
297
			}
298
		} catch (BusinessLayerException $ex) {
299
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
300
		}
301
	}
302
303
	/**
304
	 * get the actual stream URL from the given public URL
305
	 *
306
	 * Available without login since no user data is handled and this may be used on link-shared folder.
307
	 *
308
	 * @PublicPage
309
	 * @NoCSRFRequired
310
	 */
311
	public function resolveStreamUrl(string $url, ?string $token) : JSONResponse {
312
		$url = \rawurldecode($url);
313
314
		if ($token === null) {
315
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'a security token must be passed');
316
		} elseif (!$this->tokenService->urlTokenIsValid($url, \rawurldecode($token))) {
317
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'the security token is invalid');
318
		} else {
319
			$resolved = $this->service->resolveStreamUrl($url);
320
			$relayEnabled = $this->streamRelayEnabled();
321
			if ($relayEnabled && !$resolved['hls']) {
322
				$token = $this->tokenService->tokenForUrl($resolved['url']);
323
				$resolved['url'] = $this->urlGenerator->linkToRoute('music.radioApi.streamFromUrl',
324
					['url' => \rawurlencode($resolved['url']), 'token' => \rawurlencode($token)]);
325
			}
326
			return new JSONResponse($resolved);
327
		}
328
	}
329
330
	/**
331
	 * create a relayed stream for the given URL if relaying enabled; otherwise just redirect to the URL
332
	 * 
333
	 * @PublicPage
334
	 * @NoCSRFRequired
335
	 */
336
	public function streamFromUrl(string $url, ?string $token) : Response {
337
		$url = \rawurldecode($url);
338
339
		if ($token === null) {
340
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'a security token must be passed');
341
		} elseif (!$this->tokenService->urlTokenIsValid($url, \rawurldecode($token))) {
342
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'the security token is invalid');
343
		} elseif ($this->streamRelayEnabled()) {
344
			return new RelayStreamResponse($url);
345
		} else {
346
			return new RedirectResponse($url);
347
		}
348
	}
349
350
	/**
351
	 * get manifest of a HLS stream
352
	 *
353
	 * This fetches the manifest file from the given URL and returns a modified version of it.
354
	 * The front-end can't easily stream directly from the original source because of the Content-Security-Policy.
355
	 *
356
	 * @PublicPage
357
	 * @NoCSRFRequired
358
	 */
359
	public function hlsManifest(string $url, ?string $token) : Response {
360
		$url = \rawurldecode($url);
361
362
		if (!$this->hlsEnabled()) {
363
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'the cloud admin has disabled HLS streaming');
364
		} elseif ($token === null) {
365
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'a security token must be passed');
366
		} elseif (!$this->tokenService->urlTokenIsValid($url, \rawurldecode($token))) {
367
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'the security token is invalid');
368
		} else {
369
			list('content' => $content, 'status_code' => $status, 'content_type' => $contentType)
370
				= $this->service->getHlsManifest($url);
371
372
			return new FileResponse([
373
				'content' => $content,
374
				'mimetype' => $contentType
375
			], $status);
376
		}
377
	}
378
379
	/**
380
	 * get one segment of a HLS stream
381
	 *
382
	 * The segment is fetched from the given URL and relayed as such to the client.
383
	 *
384
	 * @PublicPage
385
	 * @NoCSRFRequired
386
	 */
387
	public function hlsSegment(string $url, ?string $token) : Response {
388
		$url = \rawurldecode($url);
389
390
		if (!$this->hlsEnabled()) {
391
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'the cloud admin has disabled HLS streaming');
392
		} elseif ($token === null) {
393
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'a security token must be passed');
394
		} elseif (!$this->tokenService->urlTokenIsValid($url, \rawurldecode($token))) {
395
			return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'the security token is invalid');
396
		} else {
397
			list('content' => $content, 'status_code' => $status, 'content_type' => $contentType)
398
				= HttpUtil::loadFromUrl($url);
399
400
			return new FileResponse([
401
				'content' => $content,
402
				'mimetype' => $contentType ?? 'application/octet-stream'
403
			], $status);
404
		}
405
	}
406
407
	private function hlsEnabled() : bool {
408
		$enabled = (bool)$this->config->getSystemValue('music.enable_radio_hls', true);
409
		if ($this->userId === '') {
410
			$enabled = (bool)$this->config->getSystemValue('music.enable_radio_hls_on_share', $enabled);
411
		}
412
		return $enabled;
413
	}
414
415
	private function streamRelayEnabled() : bool {
416
		$enabled = (bool)$this->config->getSystemValue('music.relay_radio_stream', true);
417
		if ($this->userId === '') {
418
			$enabled = (bool)$this->config->getSystemValue('music.relay_radio_stream_on_share', $enabled);
419
		}
420
		return $enabled;
421
	}
422
}
423