Passed
Push — feature/786_podcasts ( f026ee )
by Pauli
11:46
created

PodcastApiController::updateChannel()   B

Complexity

Conditions 10
Paths 15

Size

Total Lines 44
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 30
c 0
b 0
f 0
nc 15
nop 2
dl 0
loc 44
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 2021
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
19
use \OCP\IRequest;
20
21
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
22
use \OCA\Music\AppFramework\Core\Logger;
23
use \OCA\Music\BusinessLayer\PodcastChannelBusinessLayer;
24
use \OCA\Music\BusinessLayer\PodcastEpisodeBusinessLayer;
25
use \OCA\Music\Http\ErrorResponse;
26
use \OCA\Music\Utility\Util;
27
28
class PodcastApiController extends Controller {
29
	private $channelBusinessLayer;
30
	private $episodeBusinessLayer;
31
	private $userId;
32
	private $logger;
33
34
	public function __construct(string $appname,
35
								IRequest $request,
36
								PodcastChannelBusinessLayer $channelBusinessLayer,
37
								PodcastEpisodeBusinessLayer $episodeBusinessLayer,
38
								?string $userId,
39
								Logger $logger) {
40
		parent::__construct($appname, $request);
41
		$this->channelBusinessLayer = $channelBusinessLayer;
42
		$this->episodeBusinessLayer = $episodeBusinessLayer;
43
		$this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; the null case should happen only when the user has already logged out
44
		$this->logger = $logger;
45
	}
46
47
	/**
48
	 * lists all podcasts
49
	 *
50
	 * @NoAdminRequired
51
	 * @NoCSRFRequired
52
	 */
53
	public function getAll() {
54
		$episodes = $this->episodeBusinessLayer->findAll($this->userId);
0 ignored issues
show
Unused Code introduced by
The call to OCA\Music\BusinessLayer\...usinessLayer::findAll() has too many arguments starting with $this->userId. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

54
		/** @scrutinizer ignore-call */ 
55
  $episodes = $this->episodeBusinessLayer->findAll($this->userId);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
55
		$episodesPerChannel = [];
56
		foreach ($episodes as $episode) {
57
			$episodesPerChannel[$episode->getChannelId()][] = $episode;
58
		}
59
60
		$channels = $this->channelBusinessLayer->findAll($this->userId);
0 ignored issues
show
Unused Code introduced by
The call to OCA\Music\BusinessLayer\...usinessLayer::findAll() has too many arguments starting with $this->userId. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

60
		/** @scrutinizer ignore-call */ 
61
  $channels = $this->channelBusinessLayer->findAll($this->userId);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
61
		foreach ($channels as &$channel) {
62
			$channel->setEpisodes($episodesPerChannel[$channel->getId()] ?? []);
63
		}
64
65
		return Util::arrayMapMethod($channels, 'toApi');
66
	}
67
68
	/**
69
	 * add a followed podcast
70
	 *
71
	 * @NoAdminRequired
72
	 * @NoCSRFRequired
73
	 */
74
	public function subscribe(?string $url) {
75
		if ($url === null) {
76
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Mandatory argument 'url' not given");
77
		}
78
79
		$content = \file_get_contents($url);
80
		if ($content === false) {
81
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Invalid URL $url");
82
		}
83
84
		$xmlTree = \simplexml_load_string($content, \SimpleXMLElement::class, LIBXML_NOCDATA);
85
		if ($xmlTree === false || !$xmlTree->channel) {
86
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "The document at URL $url is not a valid podcast RSS feed");
87
		}
88
89
		try {
90
			$channel = $this->channelBusinessLayer->create($this->userId, $url, $content, $xmlTree->channel);
91
		} catch (\OCA\Music\AppFramework\Db\UniqueConstraintViolationException $ex) {
92
			return new ErrorResponse(Http::STATUS_CONFLICT, 'User already has this podcast channel subscribed');
93
		}
94
95
		$episodes = [];
96
		foreach ($xmlTree->channel->item as $episodeNode) {
97
			$episodes[] = $this->episodeBusinessLayer->addOrUpdate($this->userId, $channel->getId(), $episodeNode);
0 ignored issues
show
Bug introduced by
It seems like $episodeNode can also be of type null; however, parameter $xmlNode of OCA\Music\BusinessLayer\...essLayer::addOrUpdate() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
			$episodes[] = $this->episodeBusinessLayer->addOrUpdate($this->userId, $channel->getId(), /** @scrutinizer ignore-type */ $episodeNode);
Loading history...
98
		}
99
100
		$channel->setEpisodes($episodes);
101
		return $channel->toApi();
102
	}
103
104
	/**
105
	 * deletes a station
106
	 *
107
	 * @NoAdminRequired
108
	 * @NoCSRFRequired
109
	 */
110
	public function unsubscribe(int $id) {
111
		try {
112
			$this->channelBusinessLayer->delete($id, $this->userId); // throws if not found
113
			$this->episodeBusinessLayer->deleteByChannel($id, $this->userId); // does not throw
114
			return new JSONResponse(['success' => true]);
115
		} catch (BusinessLayerException $ex) {
116
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
117
		}
118
	}
119
120
	/**
121
	 * get a single podcast channel
122
	 *
123
	 * @NoAdminRequired
124
	 * @NoCSRFRequired
125
	 */
126
	public function get(int $id) {
127
		try {
128
			$channel = $this->channelBusinessLayer->find($id, $this->userId);
129
			$channel->setEpisodes($this->episodeBusinessLayer->findAllByChannel($id, $this->userId));
130
			return $channel->toApi();
131
		} catch (BusinessLayerException $ex) {
132
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
133
		}
134
	}
135
136
	/**
137
	 * check a single channel for updates
138
	 * @param int $id Channel ID
139
	 * @param string|null $prevHash Previous content hash known by the client. If given, the result will tell
140
	 *								if the channel content has updated from this state. If omitted, the result
141
	 *								will thell if the channel changed from its previous server-known state.
142
	 *
143
	 * @NoAdminRequired
144
	 * @NoCSRFRequired
145
	 */
146
	public function updateChannel(int $id, ?string $prevHash) {
147
		$updated = false;
148
149
		try {
150
			$channel = $this->channelBusinessLayer->find($id, $this->userId);
151
		} catch (BusinessLayerException $ex) {
152
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
153
		}
154
155
		$xmlTree = false;
156
		$content = \file_get_contents($channel->getRssUrl());
157
		if ($content === null) {
158
			$this->logger->log("Could not load RSS feed for channel {$channel->id}", 'warn');
159
		} else {
160
			$xmlTree = \simplexml_load_string($content, \SimpleXMLElement::class, LIBXML_NOCDATA);
161
		}
162
163
		if ($xmlTree === false || !$xmlTree->channel) {
164
			$this->logger->log("RSS feed for the chanenl {$channel->id} was invalid", 'warn');
165
			return new JSONResponse(['success' => false]);
166
		} else if ($this->channelBusinessLayer->updateChannel($channel, $content, $xmlTree->channel)) {
167
			// channel content has actually changed, update the episodes too
168
			$episodes = [];
169
			foreach ($xmlTree->channel->item as $episodeNode) {
170
				$episodes[] = $this->episodeBusinessLayer->addOrUpdate($this->userId, $id, $episodeNode);
0 ignored issues
show
Bug introduced by
It seems like $episodeNode can also be of type null; however, parameter $xmlNode of OCA\Music\BusinessLayer\...essLayer::addOrUpdate() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
				$episodes[] = $this->episodeBusinessLayer->addOrUpdate($this->userId, $id, /** @scrutinizer ignore-type */ $episodeNode);
Loading history...
171
			}
172
			$channel->setEpisodes($episodes);
173
			$this->episodeBusinessLayer->deleteByChannelExcluding($id, Util::extractIds($episodes), $this->userId);
174
			$updated = true;
175
		} else if ($prevHash !== null && $prevHash !== $channel->getContentHash()) {
176
			// the channel content is not new for the server but it is still new for the client
177
			$channel->setEpisodes($this->episodeBusinessLayer->findAllByChannel($id, $this->userId));
178
			$updated = true;
179
		}
180
181
		$response = [
182
			'success' => true,
183
			'updated' => $updated,
184
		];
185
		if ($updated) {
186
			$response['channel'] = $channel->toApi();
187
		}
188
189
		return new JSONResponse($response);
190
	}
191
192
	/**
193
	 * reset all the subscribed podcasts of the user
194
	 *
195
	 * @NoAdminRequired
196
	 * @NoCSRFRequired
197
	 */
198
	public function resetAll() {
199
		$this->episodeBusinessLayer->deleteAll($this->userId);
200
		$this->channelBusinessLayer->deleteAll($this->userId);
201
		return new JSONResponse(['success' => true]);
202
	}
203
}
204