Passed
Pull Request — master (#875)
by Pauli
04:32 queued 02:24
created

PodcastChannelBusinessLayer::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 4
dl 0
loc 11
rs 10
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\BusinessLayer;
14
15
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
16
use \OCA\Music\AppFramework\Core\Logger;
17
18
use \OCA\Music\Db\BaseMapper;
19
use \OCA\Music\Db\PodcastChannelMapper;
20
use \OCA\Music\Db\PodcastChannel;
21
22
use \OCA\Music\Utility\Util;
23
24
25
/**
26
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
27
 * @method PodcastChannel find(int $channelId, string $userId)
28
 * @method PodcastChannel[] findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null, ?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null)
29
 * @method PodcastChannel[] findAllByName(string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null, ?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null)
30
 */
31
class PodcastChannelBusinessLayer extends BusinessLayer {
32
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
33
	private $logger;
34
35
	public function __construct(PodcastChannelMapper $mapper, Logger $logger) {
36
		parent::__construct($mapper);
37
		$this->mapper = $mapper;
38
		$this->logger = $logger;
39
	}
40
41
	public function create(string $userId, string $rssUrl, string $rssContent, \SimpleXMLElement $xmlNode) : PodcastChannel {
42
		$channel = new PodcastChannel();
43
		self::parseChannelDataFromXml($xmlNode, $channel);
44
45
		$channel->setUserId( $userId );
46
		$channel->setRssUrl( Util::truncate($rssUrl, 2048) );
47
		$channel->setRssHash( \hash('md5', $rssUrl) );
48
		$channel->setContentHash( self::calculateContentHash($rssContent) );
49
		$channel->setUpdateChecked( \date(BaseMapper::SQL_DATE_FORMAT) );
50
51
		return $this->mapper->insert($channel);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mapper->insert($channel) returns the type OCP\AppFramework\Db\Entity which includes types incompatible with the type-hinted return OCA\Music\Db\PodcastChannel.
Loading history...
52
	}
53
54
	/**
55
	 * @param PodcastChannel $channel Input/output parameter for the channel
56
	 * @param string $rssContent Raw content of the RSS feed
57
	 * @param \SimpleXMLElement $xmlNode <channel> node parsed from the RSS feed
58
	 * @return boolean true if the new content differed from the previously cached content
59
	 */
60
	public function updateChannel(PodcastChannel &$channel, string $rssContent, \SimpleXMLElement $xmlNode) {
61
		$contentChanged = false;
62
		$contentHash = self::calculateContentHash($rssContent);
63
64
		if ($channel->getContentHash() !== $contentHash) {
65
			$contentChanged = true;
66
			self::parseChannelDataFromXml($xmlNode, $channel);
67
			$channel->setContentHash($contentHash);
68
		}
69
		$channel->setUpdateChecked( \date(BaseMapper::SQL_DATE_FORMAT) );
70
71
		$this->update($channel);
72
		return $contentChanged;
73
	}
74
75
	private static function calculateContentHash(string $rssContent) : string {
76
		// Exclude the tag <lastBuildDate> from the calculation. This is because many podcast feeds update that
77
		// very often, e.g. every 15 minutes, even when nothing else has changed. Including such a volatile field
78
		// on the hash would cause a lot of unnecessary updating of the database contents.
79
		$ctx = \hash_init('md5');
80
81
		$head = \strstr($rssContent, '<lastBuildDate>', true);
82
		$tail = ($head === false) ? false : \strstr($rssContent, '</lastBuildDate>', false);
83
84
		if ($tail === false) {
85
			// tag not found, just calculate the hash from the whole content
86
			\hash_update($ctx, $rssContent);
87
		} else {
88
			\hash_update($ctx, $head);
89
			\hash_update($ctx, $tail);
90
		}
91
92
		return \hash_final($ctx);
93
	}
94
95
	private static function parseChannelDataFromXml(\SimpleXMLElement $xmlNode, PodcastChannel &$channel) : void {
96
		$itunesNodes = $xmlNode->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
97
98
		if ($xmlNode->pubDate) {
99
			$channel->setPublished( \date(BaseMapper::SQL_DATE_FORMAT, \strtotime((string)($xmlNode->pubDate))) );
100
		} else {
101
			$channel->setPublished(null);
102
		}
103
		$channel->setTitle( Util::truncate((string)$xmlNode->title, 256) );
104
		$channel->setLinkUrl( Util::truncate((string)$xmlNode->link, 2048) );
105
		$channel->setLanguage( Util::truncate((string)$xmlNode->language, 32) );
106
		$channel->setCopyright( Util::truncate((string)$xmlNode->copyright, 256) );
107
		$channel->setAuthor( Util::truncate((string)($xmlNode->author ?: $itunesNodes->author), 256) );
108
		$channel->setDescription( (string)($xmlNode->description ?: $itunesNodes->summary) );
109
		$channel->setImageUrl( (string)$xmlNode->image->url );
110
		$channel->setCategory( \implode(', ', \array_map(function ($category) {
111
			return $category->attributes()['text'];
112
		}, \iterator_to_array($itunesNodes->category, false))) );
113
	}
114
115
}
116