RadioApiController::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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