Passed
Pull Request — master (#451)
by korelstar
02:57
created

NoteUtil::isNote()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
namespace OCA\Notes\Service;
4
5
use OCP\IDBConnection;
6
use OCP\IL10N;
7
use OCP\ILogger;
8
use OCP\Files\IRootFolder;
9
use OCP\Files\FileInfo;
10
use OCP\Files\File;
11
use OCP\Files\Folder;
12
13
class NoteUtil {
14
15
	private $db;
16
	private $l10n;
17
	private $root;
18
	private $logger;
19
	private $appName;
20
21
	/**
22
	 * @param IDBConnection $db
23
	 * @param IRootFolder $root
24
	 * @param IL10N $l10n
25
	 * @param ILogger $logger
26
	 * @param String $appName
27
	 */
28
	public function __construct(
29
		IDBConnection $db,
30
		IRootFolder $root,
31
		IL10N $l10n,
32
		ILogger $logger,
33
		$appName
34
	) {
35
		$this->db = $db;
36
		$this->root = $root;
37
		$this->l10n = $l10n;
38
		$this->logger = $logger;
39
		$this->appName = $appName;
40
	}
41
42
	/**
43
	 * gather note files in given directory and all subdirectories
44
	 */
45
	public function gatherNoteFiles(Folder $folder) : array {
46
		$notes = [];
47
		$nodes = $folder->getDirectoryListing();
48
		foreach ($nodes as $node) {
49
			if ($node->getType() === FileInfo::TYPE_FOLDER && $node instanceof Folder) {
50
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
51
				continue;
52
			}
53
			if ($this->isNote($node)) {
54
				$notes[] = $node;
55
			}
56
		}
57
		return $notes;
58
	}
59
60
61
	/**
62
	 * test if file is a note
63
	 */
64
	public function isNote(FileInfo $file) : bool {
65
		$allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
66
		$ext = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION));
67
		return $file->getType() === 'file' && in_array($ext, $allowedExtensions);
68
	}
69
70
	public function moveNote(Folder $notesFolder, File $file, $category, string $title) {
71
		$id = $file->getId();
72
		$currentFilePath = $this->root->getFullPath($file->getPath());
73
		$currentBasePath = pathinfo($currentFilePath, PATHINFO_DIRNAME);
74
		$fileSuffix = '.' . pathinfo($file->getName(), PATHINFO_EXTENSION);
75
76
		// detect (new) folder path based on category name
77
		if ($category===null) {
78
			$basePath = $currentBasePath;
79
		} else {
80
			$basePath = $notesFolder->getPath();
81
			if (!empty($category)) {
82
				// sanitise path
83
				$cats = explode('/', $category);
84
				$cats = array_map([$this, 'sanitisePath'], $cats);
85
				$cats = array_filter($cats, function ($str) {
86
					return !empty($str);
87
				});
88
				$basePath .= '/'.implode('/', $cats);
89
			}
90
		}
91
		$folder = $this->getOrCreateFolder($basePath);
92
93
		// assemble new file path
94
		$newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileSuffix, $id);
95
96
		// if the current path is not the new path, the file has to be renamed
97
		if ($currentFilePath !== $newFilePath) {
98
			$file->move($newFilePath);
99
		}
100
		if ($currentBasePath !== $basePath) {
101
			FileInfo $fileBasePath = $this->root->get($currentBasePath);
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_VARIABLE on line 101 at column 12
Loading history...
102
			if($fileBasePath instanceof Folder)
103
			$this->deleteEmptyFolder($notesFolder, $fileBasePath);
104
		}
105
	}
106
107
	/**
108
	 * get path of file and the title.txt and check if they are the same
109
	 * file. If not the title needs to be renamed
110
	 *
111
	 * @param Folder $folder a folder to the notes directory
112
	 * @param string $title the filename which should be used
113
	 * @param string $suffix the suffix (incl. dot) which should be used
114
	 * @param int $id the id of the note for which the title should be generated
115
	 * used to see if the file itself has the title and not a different file for
116
	 * checking for filename collisions
117
	 * @return string the resolved filename to prevent overwriting different
118
	 * files with the same title
119
	 */
120
	public function generateFileName(Folder $folder, string $title, string $suffix, int $id) : string {
121
		$path = $title . $suffix;
122
123
		// if file does not exist, that name has not been taken. Similar we don't
124
		// need to handle file collisions if it is the filename did not change
125
		if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
126
			return $path;
127
		} else {
128
			// increments name (2) to name (3)
129
			$match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
130
			if ($match) {
131
				$newId = ((int) $matches['id']) + 1;
132
				$newTitle = preg_replace(
133
					'/(.*)\s\((\d+)\)$/u',
134
					'$1 (' . $newId . ')',
135
					$title
136
				);
137
			} else {
138
				$newTitle = $title . ' (2)';
139
			}
140
			return $this->generateFileName($folder, $newTitle, $suffix, $id);
141
		}
142
	}
143
144
	public function getSafeTitleFromContent(string $content) : string {
145
		// prepare content: remove markdown characters and empty spaces
146
		$content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item
147
		$content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline
148
		$content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline
149
		$content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis
150
151
		// sanitize: prevent directory traversal, illegal characters and unintended file names
152
		$content = $this->sanitisePath($content);
153
154
		// generate title from the first line of the content
155
		$splitContent = preg_split("/\R/u", $content, 2);
156
		$title = trim($splitContent[0]);
157
158
		// using a maximum of 100 chars should be enough
159
		$title = mb_substr($title, 0, 100, "UTF-8");
160
161
		// ensure that title is not empty
162
		if (empty($title)) {
163
			$title = $this->l10n->t('New note');
164
		}
165
166
		return $title;
167
	}
168
169
	/** removes characters that are illegal in a file or folder name on some operating systems */
170
	public function sanitisePath(string $str) : string {
171
		// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
172
		// prevents also directory traversal by eliminiating slashes
173
		// see also \OC\Files\Storage\Common::verifyPosixPath(...)
174
		$str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
175
176
		// if mysql doesn't support 4byte UTF-8, then remove those characters
177
		// see \OC\Files\Storage\Common::verifyPath(...)
178
		if (!$this->db->supports4ByteText()) {
179
			$str = preg_replace('%(?:
180
                \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
181
              | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
182
              | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
183
              )%xs', '', $str);
184
		}
185
186
		// prevent file to be hidden
187
		$str = preg_replace("/^[\. ]+/mu", "", $str);
188
		return trim($str);
189
	}
190
191
	/**
192
	 * Finds a folder and creates it if non-existent
193
	 * @param string $path path to the folder
194
	 * @return Folder
195
	 */
196
	public function getOrCreateFolder(string $path) : Folder {
197
		if ($this->root->nodeExists($path)) {
198
			$folder = $this->root->get($path);
199
		} else {
200
			$folder = $this->root->newFolder($path);
201
		}
202
		if (!($folder instanceof Folder)) {
203
			throw new NotesFolderException($path.' is not a folder');
204
		}
205
		return $folder;
206
	}
207
208
	/*
209
	 * Delete a folder and it's parent(s) if it's/they're empty
210
	 * @param Folder root folder for notes
211
	 * @param Folder folder to delete
212
	 */
213
	public function deleteEmptyFolder(Folder $notesFolder, Folder $folder) {
214
		$content = $folder->getDirectoryListing();
215
		$isEmpty = !count($content);
216
		$isNotesFolder = $folder->getPath()===$notesFolder->getPath();
217
		if ($isEmpty && !$isNotesFolder) {
218
			$this->logger->info('Deleting empty category folder '.$folder->getPath(), ['app' => $this->appName]);
219
			$parent = $folder->getParent();
220
			$folder->delete();
221
			$this->deleteEmptyFolder($notesFolder, $parent);
222
		}
223
	}
224
225
	/**
226
	 * Checks if there is enough space left on storage. Throws an Exception if storage is not sufficient.
227
	 * @param File file that needs storage
228
	 * @throws InsufficientStorageException
229
	 */
230
	public function ensureSufficientStorage(Folder $folder, $requiredBytes) : void {
231
		$availableBytes = $folder->getFreeSpace();
232
		if ($availableBytes >= 0 && $availableBytes < $requiredBytes) {
233
			$this->logger->error('Insufficient storage in '.$folder->getPath().': available are '.$availableBytes.'; required are '.$requiredBytes, ['app' => $this->appName]);
234
			throw new InsufficientStorageException($requiredBytes.' are required in '.$folder->getPath());
235
		}
236
	}
237
}
238