Passed
Push — master ( 041459...b27899 )
by Pauli
03:55
created

LibrarySettings   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 161
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 72
dl 0
loc 161
rs 10
c 0
b 0
f 0
wmc 30

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getFolder() 0 3 1
A pathIsExcluded() 0 16 4
A getPath() 0 3 2
A setIgnoredArticles() 0 2 1
A getHomePath() 0 2 1
A setScanMetadataEnabled() 0 2 2
A pathBelongsToMusicLibrary() 0 6 2
A getAbsoluteLibPath() 0 2 1
A pathMatchesPattern() 0 23 5
A normalizePath() 0 5 1
A __construct() 0 9 1
A setPath() 0 18 4
A getIgnoredArticles() 0 4 1
A getExcludedPaths() 0 7 2
A setExcludedPaths() 0 4 1
A getScanMetadataEnabled() 0 3 1
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 2019 - 2025
11
 */
12
13
namespace OCA\Music\Service;
14
15
use OCA\Music\AppFramework\Core\Logger;
16
use OCA\Music\Utility\FilesUtil;
17
use OCA\Music\Utility\LocalCacheTrait;
18
use OCA\Music\Utility\StringUtil;
19
20
use OCP\Files\Folder;
21
use OCP\Files\IRootFolder;
22
use OCP\IConfig;
23
24
/**
25
 * Manage the user-specific music folder setting
26
 */
27
class LibrarySettings {
28
	/**
29
	 * Caching the values is useful when the same value is used tens of thousands of times within a
30
	 * single request, like when checking the scan status of a huge music library.
31
	 * @phpstan-use LocalCacheTrait<mixed>
32
	 */
33
	use LocalCacheTrait;
34
35
	private string $appName;
36
	private IConfig $configManager;
37
	private IRootFolder $rootFolder;
38
	private Logger $logger;
39
40
	public function __construct(
41
			string $appName,
42
			IConfig $configManager,
43
			IRootFolder $rootFolder,
44
			Logger $logger) {
45
		$this->appName = $appName;
46
		$this->configManager = $configManager;
47
		$this->rootFolder = $rootFolder;
48
		$this->logger = $logger;
49
	}
50
51
	public function setScanMetadataEnabled(string $userId, bool $enabled) : void {
52
		$this->configManager->setUserValue($userId, $this->appName, 'scan_metadata', $enabled ? '1' : '0');
53
	}
54
55
	public function getScanMetadataEnabled(string $userId) : bool {
56
		$value = $this->configManager->getUserValue($userId, $this->appName, 'scan_metadata', 1);
57
		return ((int)$value > 0);
58
	}
59
60
	public function setIgnoredArticles(string $userId, array $articles) : void {
61
		$this->configManager->setUserValue($userId, $this->appName, 'ignored_articles', \json_encode($articles));
62
	}
63
64
	public function getIgnoredArticles(string $userId) : array {
65
		$default = '["The", "El", "La", "Los", "Las", "Le", "Les"]';
66
		$value = $this->configManager->getUserValue($userId, $this->appName, 'ignored_articles', $default);
67
		return \json_decode($value);
68
	}
69
70
	public function setPath(string $userId, string $path) : bool {
71
		$success = false;
72
73
		$userHome = $this->rootFolder->getUserFolder($userId);
74
		$element = $userHome->get($path);
75
		if ($element instanceof \OCP\Files\Folder) {
76
			if ($path[0] !== '/') {
77
				$path = '/' . $path;
78
			}
79
			if ($path[\strlen($path)-1] !== '/') {
80
				$path .= '/';
81
			}
82
			$this->configManager->setUserValue($userId, $this->appName, 'path', $path);
83
			$success = true;
84
		}
85
86
		$this->invalidateCache($userId);
87
		return $success;
88
	}
89
90
	public function getPath(string $userId) : string {
91
		$path = $this->configManager->getUserValue($userId, $this->appName, 'path');
92
		return $path ?: '/';
93
	}
94
95
	/**
96
	 * @param string[] $paths
97
	 */
98
	public function setExcludedPaths(string $userId, array $paths) : bool {
99
		$this->configManager->setUserValue($userId, $this->appName, 'excluded_paths', \json_encode($paths));
100
		$this->invalidateCache($userId);
101
		return true;
102
	}
103
104
	/**
105
	 * @return string[]
106
	 */
107
	public function getExcludedPaths(string $userId) : array {
108
		return $this->cachedGet($userId, 'excluded_paths', function() use ($userId) {
109
			$paths = $this->configManager->getUserValue($userId, $this->appName, 'excluded_paths');
110
			if (empty($paths)) {
111
				return [];
112
			} else {
113
				return \json_decode($paths);
114
			}
115
		});
116
	}
117
118
	public function getFolder(string $userId) : Folder {
119
		return $this->cachedGet($userId, 'music_folder', fn() => FilesUtil::getFolderFromRelativePath(
120
			$this->rootFolder->getUserFolder($userId), $this->getPath($userId)));
121
	}
122
123
	public function pathBelongsToMusicLibrary(string $filePath, string $userId) : bool {
124
		$filePath = self::normalizePath($filePath);
125
		$musicPath = $this->getAbsoluteLibPath($userId);
126
127
		return StringUtil::startsWith($filePath, $musicPath)
128
			&& !$this->pathIsExcluded($filePath, $musicPath, $userId);
129
	}
130
131
	private function getAbsoluteLibPath(string $userId) : string {
132
		return $this->cachedGet($userId, 'music_folder_abs_path', fn() => self::normalizePath($this->getFolder($userId)->getPath()));
133
	}
134
135
	private function getHomePath(string $userId) : string {
136
		return $this->cachedGet($userId, 'home_path', fn() => $this->rootFolder->getUserFolder($userId)->getPath());
137
	}
138
139
	private function pathIsExcluded(string $filePath, string $musicPath, string $userId) : bool {
140
		$userRootPath = $this->getHomePath($userId);
141
		$excludedPaths = $this->getExcludedPaths($userId);
142
143
		foreach ($excludedPaths as $excludedPath) {
144
			if (StringUtil::startsWith($excludedPath, '/')) {
145
				$excludedPath = $userRootPath . $excludedPath;
146
			} else {
147
				$excludedPath = $musicPath . '/' . $excludedPath;
148
			}
149
			if (self::pathMatchesPattern($filePath, $excludedPath)) {
150
				return true;
151
			}
152
		}
153
154
		return false;
155
	}
156
157
	private static function pathMatchesPattern(string $path, string $pattern) : bool {
158
		// normalize the pattern so that there is no trailing '/'
159
		$pattern = \rtrim($pattern, '/');
160
161
		if (\strpos($pattern, '*') === false && \strpos($pattern, '?') === false) {
162
			// no wildcards, beginning of the path should match the pattern exactly
163
			// and the next character after the matching part (if any) should be '/'
164
			$patternLen = \strlen($pattern);
165
			return StringUtil::startsWith($path, $pattern)
166
				&& (\strlen($path) === $patternLen || $path[$patternLen] === '/');
167
		} else {
168
			// some wildcard characters in the pattern, convert the pattern into regex:
169
			// - '?' matches exactly one arbitrary character except the directory separator '/'
170
			// - '*' matches zero or more arbitrary characters except the directory separator '/'
171
			// - '**' matches zero or more arbitrary characters including directory separator '/'
172
			$pattern = \preg_quote($pattern, '/');				// escape regex meta characters
173
			$pattern = \str_replace('\*\*', '.*', $pattern);	// convert ** to its regex equivalent
174
			$pattern = \str_replace('\*', '[^\/]*', $pattern);	// convert * to its regex equivalent
175
			$pattern = \str_replace('\?', '[^\/]', $pattern);	// convert ? to its regex equivalent
176
			$pattern = $pattern . '(\/.*)?$';					// after given pattern, there should be '/' or nothing
177
			$pattern = '/' . $pattern . '/';
178
179
			return (\preg_match($pattern, $path) === 1);
180
		}
181
	}
182
183
	private static function normalizePath(string $path) : string {
184
		// The file system may create paths where there are two consecutive
185
		// path separator characters (/). This was seen with an external local
186
		// folder on NC13, but may apply to other cases, too. Normalize such paths.
187
		return \str_replace('//', '/', $path);
188
	}
189
}
190