Passed
Push — master ( 6e9496...0e6475 )
by Pauli
04:14
created

RadioApiController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

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