Passed
Push — master ( 54eb8d...5f3e02 )
by Pauli
04:51
created

CoverApiController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
nc 1
nop 10
dl 0
loc 21
rs 9.9666
c 1
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 2024, 2025
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use OCP\AppFramework\Controller;
16
use OCP\AppFramework\Http;
17
use OCP\AppFramework\Http\RedirectResponse;
18
use OCP\AppFramework\Http\Response;
19
use OCP\Files\IRootFolder;
20
use OCP\IRequest;
21
use OCP\IURLGenerator;
22
23
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
24
use OCA\Music\AppFramework\Core\Logger;
25
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
26
use OCA\Music\BusinessLayer\ArtistBusinessLayer;
27
use OCA\Music\BusinessLayer\PodcastChannelBusinessLayer;
28
use OCA\Music\Db\Album;
29
use OCA\Music\Db\Artist;
30
use OCA\Music\Db\Entity;
31
use OCA\Music\Db\PodcastChannel;
32
use OCA\Music\Http\ErrorResponse;
33
use OCA\Music\Http\FileResponse;
34
use OCA\Music\Service\CoverService;
35
use OCA\Music\Utility\HttpUtil;
36
use OCA\Music\Utility\StringUtil;
37
38
class CoverApiController extends Controller {
39
40
	private IURLGenerator $urlGenerator;
41
	private IRootFolder $rootFolder;
42
	private ArtistBusinessLayer $artistBusinessLayer;
43
	private AlbumBusinessLayer $albumBusinessLayer;
44
	private PodcastChannelBusinessLayer $podcastChannelBusinessLayer;
45
	private CoverService $coverService;
46
	private ?string $userId;
47
	private Logger $logger;
48
49
	public function __construct(
50
			string $appName,
51
			IRequest $request,
52
			IURLGenerator $urlGenerator,
53
			IRootFolder $rootFolder,
54
			ArtistBusinessLayer $artistBusinessLayer,
55
			AlbumBusinessLayer $albumBusinessLayer,
56
			PodcastChannelBusinessLayer $podcastChannelBusinessLayer,
57
			CoverService $coverService,
58
			?string $userId, // null if this gets called after the user has logged out or on a public page
59
			Logger $logger
60
	) {
61
		parent::__construct($appName, $request);
62
		$this->urlGenerator = $urlGenerator;
63
		$this->rootFolder = $rootFolder;
64
		$this->artistBusinessLayer = $artistBusinessLayer;
65
		$this->albumBusinessLayer = $albumBusinessLayer;
66
		$this->podcastChannelBusinessLayer = $podcastChannelBusinessLayer;
67
		$this->coverService = $coverService;
68
		$this->userId = $userId;
69
		$this->logger = $logger;
70
	}
71
72
	/**
73
	 * @PublicPage
74
	 * @NoCSRFRequired
75
	 */
76
	public function externalCover(?string $url) : Response {
77
		$allowedDomains = ['lastfm.freetls.fastly.net']; // domain used by Last.fm for the album art
78
79
		if (empty($url)) {
80
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, 'Required argument "url" missing');
81
		} elseif (!\in_array(\parse_url($url, PHP_URL_HOST), $allowedDomains)) {
82
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'Only whitelisted host names are allowed');
83
		} else {
84
			['content' => $content, 'content_type' => $contentType] = HttpUtil::loadFromUrl($url);
85
			if ($content === false || !StringUtil::startsWith($contentType, 'image/')) {
86
				return new ErrorResponse(Http::STATUS_NOT_FOUND, 'Failed to fetch image from given URL');
87
			} else {
88
				return new FileResponse(['content' => $content, 'mimetype' => $contentType]);
89
			}
90
		}
91
	}
92
93
	/**
94
	 * @PublicPage
95
	 * @NoCSRFRequired
96
	 */
97
	public function albumCover(int $albumId, ?string $originalSize, ?string $coverToken) : Response {
98
		try {
99
			$userId = $this->userId ?? $this->coverService->getUserForAccessToken($coverToken);
100
			$album = $this->albumBusinessLayer->find($albumId, $userId);
101
			return $this->cover($album, $userId, $originalSize);
102
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
103
			$this->logger->debug("Failed to get the requested cover: $ex");
104
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
105
		}
106
	}
107
108
	/**
109
	 * @PublicPage
110
	 * @NoCSRFRequired
111
	 */
112
	public function artistCover(int $artistId, ?string $originalSize, ?string $coverToken) : Response {
113
		try {
114
			$userId = $this->userId ?? $this->coverService->getUserForAccessToken($coverToken);
115
			$artist = $this->artistBusinessLayer->find($artistId, $userId);
116
			return $this->cover($artist, $userId, $originalSize);
117
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
118
			$this->logger->debug("Failed to get the requested cover: $ex");
119
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
120
		}
121
	}
122
123
	/**
124
	 * @PublicPage
125
	 * @NoCSRFRequired
126
	 */
127
	public function podcastCover(int $channelId, ?string $originalSize, ?string $coverToken) : Response {
128
		try {
129
			$userId = $this->userId ?? $this->coverService->getUserForAccessToken($coverToken);
130
			$channel = $this->podcastChannelBusinessLayer->find($channelId, $userId);
131
			return $this->cover($channel, $userId, $originalSize);
132
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
133
			$this->logger->debug("Failed to get the requested cover: $ex");
134
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
135
		}
136
	}
137
138
	/**
139
	 * @PublicPage
140
	 * @NoCSRFRequired
141
	 */
142
	public function cachedCover(string $hash, ?string $coverToken) : Response {
143
		try {
144
			$userId = $this->userId ?? $this->coverService->getUserForAccessToken($coverToken);
145
			$coverData = $this->coverService->getCoverFromCache($hash, $userId);
146
			if ($coverData === null) {
147
				throw new \OutOfBoundsException("Cover with hash $hash not found");
148
			}
149
			$response =  new FileResponse($coverData);
150
			// instruct also the client-side to cache the result, this is safe
151
			// as the resource URI contains the image hash
152
			HttpUtil::setClientCachingDays($response, 365);
153
			return $response;
154
		} catch (\OutOfBoundsException $ex) {
155
			$this->logger->debug("Failed to get the requested cover: $ex");
156
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
157
		}
158
	}
159
160
	/**
161
	 * @param Artist|Album|PodcastChannel $entity
162
	 * @param string|int|bool|null $originalSize
163
	 */
164
	private function cover(Entity $entity, string $userId, /*mixed*/ $originalSize) : Response {
165
		$originalSize = \filter_var($originalSize, FILTER_VALIDATE_BOOLEAN);
166
		$userFolder = $this->rootFolder->getUserFolder($userId);
167
168
		if ($originalSize) {
169
			// cover requested in original size, without scaling or cropping
170
			$cover = $this->coverService->getCover($entity, $userId, $userFolder, CoverService::DO_NOT_CROP_OR_SCALE);
171
			if ($cover !== null) {
172
				return new FileResponse($cover);
173
			} else {
174
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
175
			}
176
		} else {
177
			$coverAndHash = $this->coverService->getCoverAndHash($entity, $userId, $userFolder);
178
179
			if ($coverAndHash['hash'] !== null && $this->userId !== null) {
180
				// Cover is in cache. Return a redirection response so that the client
181
				// will fetch the content through a cacheable route.
182
				// The redirection is not used in case this is a call from the Firefox mediaSession API with not
183
				// logged in user.
184
				$link = $this->urlGenerator->linkToRoute('music.coverApi.cachedCover', ['hash' => $coverAndHash['hash']]);
185
				return new RedirectResponse($link);
186
			} elseif ($coverAndHash['data'] !== null) {
187
				return new FileResponse($coverAndHash['data']);
188
			} else {
189
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
190
			}
191
		}
192
	}
193
}
194