Passed
Push — master ( 23afda...b1ae99 )
by Pauli
03:08
created

CoverApiController::setClientCaching()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
c 1
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 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\PodcastChannel;
31
use OCA\Music\Http\ErrorResponse;
32
use OCA\Music\Http\FileResponse;
33
use OCA\Music\Utility\CoverHelper;
34
use OCA\Music\Utility\HttpUtil;
35
36
class CoverApiController extends Controller {
37
38
	private IURLGenerator $urlGenerator;
39
	private IRootFolder $rootFolder;
40
	private ArtistBusinessLayer $artistBusinessLayer;
41
	private AlbumBusinessLayer $albumBusinessLayer;
42
	private PodcastChannelBusinessLayer $podcastChannelBusinessLayer;
43
	private CoverHelper $coverHelper;
44
	private ?string $userId;
45
	private Logger $logger;
46
47
	public function __construct(string $appname,
48
								IRequest $request,
49
								IURLGenerator $urlGenerator,
50
								IRootFolder $rootFolder,
51
								ArtistBusinessLayer $artistBusinessLayer,
52
								AlbumBusinessLayer $albumBusinessLayer,
53
								PodcastChannelBusinessLayer $podcastChannelBusinessLayer,
54
								CoverHelper $coverHelper,
55
								?string $userId, // null if this gets called after the user has logged out or on a public page
56
								Logger $logger) {
57
		parent::__construct($appname, $request);
58
		$this->urlGenerator = $urlGenerator;
59
		$this->rootFolder = $rootFolder;
60
		$this->artistBusinessLayer = $artistBusinessLayer;
61
		$this->albumBusinessLayer = $albumBusinessLayer;
62
		$this->podcastChannelBusinessLayer = $podcastChannelBusinessLayer;
63
		$this->coverHelper = $coverHelper;
64
		$this->userId = $userId;
65
		$this->logger = $logger;
66
	}
67
68
	/**
69
	 * @PublicPage
70
	 * @NoCSRFRequired
71
	 */
72
	public function albumCover(int $albumId, $originalSize, $coverToken) {
73
		try {
74
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
75
			$album = $this->albumBusinessLayer->find($albumId, $userId);
76
			return $this->cover($album, $userId, $originalSize);
77
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
78
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
79
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
80
		}
81
	}
82
83
	/**
84
	 * @PublicPage
85
	 * @NoCSRFRequired
86
	 */
87
	public function artistCover(int $artistId, $originalSize, $coverToken) {
88
		try {
89
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
90
			$artist = $this->artistBusinessLayer->find($artistId, $userId);
91
			return $this->cover($artist, $userId, $originalSize);
92
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
93
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
94
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
95
		}
96
	}
97
98
	/**
99
	 * @PublicPage
100
	 * @NoCSRFRequired
101
	 */
102
	public function podcastCover(int $channelId, $originalSize, $coverToken) {
103
		try {
104
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
105
			$channel = $this->podcastChannelBusinessLayer->find($channelId, $userId);
106
			return $this->cover($channel, $userId, $originalSize);
107
		} catch (BusinessLayerException | \OutOfBoundsException $ex) {
108
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
109
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
110
		}
111
	}
112
113
	/**
114
	 * @PublicPage
115
	 * @NoCSRFRequired
116
	 */
117
	public function cachedCover(string $hash, ?string $coverToken) {
118
		try {
119
			$userId = $this->userId ?? $this->coverHelper->getUserForAccessToken($coverToken);
120
			$coverData = $this->coverHelper->getCoverFromCache($hash, $userId);
121
			if ($coverData === null) {
122
				throw new \OutOfBoundsException("Cover with hash $hash not found");
123
			}
124
			$response =  new FileResponse($coverData);
125
			// instruct also the client-side to cache the result, this is safe
126
			// as the resource URI contains the image hash
127
			HttpUtil::setClientCachingDays($response, 365);
128
			return $response;
129
		} catch (\OutOfBoundsException $ex) {
130
			$this->logger->log("Failed to get the requested cover: $ex", 'debug');
131
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
132
		}
133
	}
134
135
	/**
136
	 * @param Artist|Album|PodcastChannel $entity
137
	 */
138
	private function cover($entity, string $userId, $originalSize) {
139
		$originalSize = \filter_var($originalSize, FILTER_VALIDATE_BOOLEAN);
140
		$userFolder = $this->rootFolder->getUserFolder($userId);
141
142
		if ($originalSize) {
143
			// cover requested in original size, without scaling or cropping
144
			$cover = $this->coverHelper->getCover($entity, $userId, $userFolder, CoverHelper::DO_NOT_CROP_OR_SCALE);
145
			if ($cover !== null) {
146
				return new FileResponse($cover);
147
			} else {
148
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
149
			}
150
		} else {
151
			$coverAndHash = $this->coverHelper->getCoverAndHash($entity, $userId, $userFolder);
152
153
			if ($coverAndHash['hash'] !== null && $this->userId !== null) {
154
				// Cover is in cache. Return a redirection response so that the client
155
				// will fetch the content through a cacheable route.
156
				// The redirection is not used in case this is a call from the Firefox mediaSession API with not
157
				// logged in user.
158
				$link = $this->urlGenerator->linkToRoute('music.coverApi.cachedCover', ['hash' => $coverAndHash['hash']]);
159
				return new RedirectResponse($link);
160
			} elseif ($coverAndHash['data'] !== null) {
161
				return new FileResponse($coverAndHash['data']);
162
			} else {
163
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
164
			}
165
		}
166
	}
167
}
168