Passed
Pull Request — master (#381)
by korelstar
02:03
created

NoteUtil::generateFileName()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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