-
Notifications
You must be signed in to change notification settings - Fork 152
Expand file tree
/
Copy pathNoteUtil.php
More file actions
301 lines (263 loc) · 9.71 KB
/
NoteUtil.php
File metadata and controls
301 lines (263 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Notes\Service;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IDBConnection;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
class NoteUtil {
private const MAX_TITLE_LENGTH = 100;
public Util $util;
private IDBConnection $db;
private IRootFolder $root;
private TagService $tagService;
private IManager $shareManager;
private IUserSession $userSession;
private SettingsService $settingsService;
public function __construct(
Util $util,
IRootFolder $root,
IDBConnection $db,
TagService $tagService,
IManager $shareManager,
IUserSession $userSession,
SettingsService $settingsService,
) {
$this->util = $util;
$this->root = $root;
$this->db = $db;
$this->tagService = $tagService;
$this->shareManager = $shareManager;
$this->userSession = $userSession;
$this->settingsService = $settingsService;
}
public function getRoot() : IRootFolder {
return $this->root;
}
public function getPathForUser(File $file) {
$userFolder = $this->root->getUserFolder($this->userSession->getUser()->getUID());
return $userFolder->getRelativePath($file->getPath());
}
public function getTagService() : TagService {
return $this->tagService;
}
public function getCategoryFolder(Folder $notesFolder, string $category) {
$path = $notesFolder->getPath();
// sanitise path
$cats = explode('/', $category);
$cats = array_map([$this, 'sanitisePath'], $cats);
$cats = array_filter($cats, function ($str) {
return $str !== '';
});
$path .= '/' . implode('/', $cats);
return $this->getOrCreateFolder($path);
}
/**
* get path of file and the title.txt and check if they are the same
* file. If not the title needs to be renamed
*
* @param Folder $folder a folder to the notes directory
* @param string $title the filename which should be used
* @param string $suffix the suffix (incl. dot) which should be used
* @param int $id the id of the note for which the title should be generated
* used to see if the file itself has the title and not a different file for
* checking for filename collisions
* @return string the resolved filename to prevent overwriting different
* files with the same title
*/
public function generateFileName(Folder $folder, string $title, string $suffix, int $id) : string {
$title = $this->getSafeTitle($title);
$filename = $title . $suffix;
// if file does not exist, that name has not been taken. Similar we don't
// need to handle file collisions if it is the filename did not change
if (!$folder->nodeExists($filename) || $folder->get($filename)->getId() === $id) {
return $filename;
} else {
// increments name (2) to name (3)
$match = preg_match('/\s\((?P<id>\d+)\)$/u', $title, $matches);
if ($match) {
$newId = ((int)$matches['id']) + 1;
$baseTitle = preg_replace('/\s\(\d+\)$/u', '', $title);
$idSuffix = ' (' . $newId . ')';
} else {
$baseTitle = $title;
$idSuffix = ' (2)';
}
// make sure there's enough room for the ID suffix before appending or it will be
// trimmed by getSafeTitle() and could cause infinite recursion
$newTitle = mb_substr($baseTitle, 0, self::MAX_TITLE_LENGTH - mb_strlen($idSuffix), 'UTF-8') . $idSuffix;
return $this->generateFileName($folder, $newTitle, $suffix, $id);
}
}
public function getSafeTitle(string $content) : string {
// sanitize: prevent directory traversal, illegal characters and unintended file names
$content = $this->sanitisePath($content);
// generate title from the first line of the content
$splitContent = preg_split("/\R/u", $content, 2);
$title = trim($splitContent[0]);
// replace (Unicode) white-space with normal space
$title = preg_replace('/\s/u', ' ', $title);
// using a maximum of 100 chars should be enough
$title = mb_substr($title, 0, self::MAX_TITLE_LENGTH, 'UTF-8');
// ensure that title is not empty
if (empty($title)) {
$title = $this->util->l10n->t('New note');
}
return $title;
}
/** removes characters that are illegal in a file or folder name on some operating systems */
private function sanitisePath(string $str) : string {
// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
// prevents also directory traversal by eliminiating slashes
// see also \OC\Files\Storage\Common::verifyPosixPath(...)
$str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
// if mysql doesn't support 4byte UTF-8, then remove those characters
// see \OC\Files\Storage\Common::verifyPath(...)
if (!$this->db->supports4ByteText()) {
$str = preg_replace('%(?:
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)%xs', '', $str);
}
// prevent file to be hidden
$str = preg_replace('/^[\.\s]+/mu', '', $str);
return trim($str);
}
public function stripMarkdown(string $str) : string {
// prepare content: remove markdown characters and empty spaces
$str = preg_replace("/^\s*[*+-]\s+/mu", '', $str); // list item
$str = preg_replace("/^#+\s+(.*?)\s*#*$/mu", '$1', $str); // headline
$str = preg_replace('/^(=+|-+)$/mu', '', $str); // separate line for headline
$str = preg_replace("/(\*+|_+)(.*?)\\1/mu", '$2', $str); // emphasis
return $str;
}
/**
* Finds a folder and creates it if non-existent
* @param string $path path to the folder
* @return Folder
*/
public function getOrCreateFolder(string $path, bool $create = true) : Folder {
$folder = null;
if ($this->root->nodeExists($path)) {
$folder = $this->root->get($path);
} elseif ($create) {
$folder = $this->root->newFolder($path);
}
if (!($folder instanceof Folder)) {
throw new NotesFolderException($path . ' is not a folder');
}
return $folder;
}
public function getNotesFolderUserPath(string $userId): ?string {
/** @psalm-suppress MissingDependency */
$userFolder = $this->getRoot()->getUserFolder($userId);
try {
$nodesFolder = $this->getOrCreateNotesFolder($userId, false);
} catch (NotesFolderException $e) {
$this->util->logger->debug("Failed to get notes folder for user $userId: " . $e->getMessage());
return null;
}
return $userFolder->getRelativePath($nodesFolder->getPath());
}
public function getOrCreateNotesFolder(string $userId, bool $create = true) : Folder {
$userFolder = $this->getRoot()->getUserFolder($userId);
$notesPath = $this->settingsService->getValueString($userId, 'notesPath');
$allowShared = $notesPath !== $this->settingsService->getDefaultNotesPath($userId);
$folder = null;
$updateNotesPath = false;
if ($userFolder->nodeExists($notesPath)) {
$folder = $userFolder->get($notesPath);
if (!$allowShared && $folder->isShared()) {
$notesPath = $userFolder->getNonExistingName($notesPath);
$folder = $userFolder->newFolder($notesPath);
$updateNotesPath = true;
}
} elseif ($create) {
$folder = $userFolder->newFolder($notesPath);
$updateNotesPath = true;
}
if (!($folder instanceof Folder)) {
throw new NotesFolderException($notesPath . ' is not a folder');
}
if ($updateNotesPath) {
$this->settingsService->set($userId, [
'notesPath' => $notesPath,
], true);
}
return $folder;
}
/*
* Delete a folder and it's parent(s) if it's/they're empty
* @param Folder $folder folder to delete
* @param Folder $notesFolder root notes folder
*/
public function deleteEmptyFolder(Folder $folder, Folder $notesFolder) : void {
$content = $folder->getDirectoryListing();
$isEmpty = !count($content);
$isNotesFolder = $folder->getPath() === $notesFolder->getPath();
if ($isEmpty && !$isNotesFolder) {
$this->util->logger->debug('Deleting empty category folder ' . $folder->getPath());
$parent = $folder->getParent();
$folder->delete();
$this->deleteEmptyFolder($parent, $notesFolder);
}
}
/**
* Checks if there is enough space left on storage. Throws an Exception if storage is not sufficient.
* @param Folder $folder that needs storage
* @param int $requiredBytes amount of storage needed in $folder
* @throws InsufficientStorageException
*/
public function ensureSufficientStorage(Folder $folder, int $requiredBytes) : void {
$availableBytes = $folder->getFreeSpace();
if ($availableBytes >= 0 && $availableBytes < $requiredBytes) {
$this->util->logger->error(
'Insufficient storage in ' . $folder->getPath() . ': ' .
'available are ' . $availableBytes . '; ' .
'required are ' . $requiredBytes
);
throw new InsufficientStorageException($requiredBytes . ' are required in ' . $folder->getPath());
}
}
/**
* Checks if the file/folder is writable. Throws an Exception if not.
* @param Node $node to be checked
* @throws NoteNotWritableException
*/
public function ensureNoteIsWritable(Node $node) : void {
if (!$node->isUpdateable()) {
throw new NoteNotWritableException();
}
}
public function getShareTypes(File $file): array {
$userId = $file->getOwner()->getUID();
$requestedShareTypes = [
IShare::TYPE_USER,
IShare::TYPE_GROUP,
IShare::TYPE_LINK,
IShare::TYPE_REMOTE,
IShare::TYPE_EMAIL,
IShare::TYPE_ROOM,
IShare::TYPE_DECK,
// FIXME: Move to constant once Nextcloud 26 is the minimum supported version
15, // IShare::TYPE_SCIENCEMESH,
];
$shareTypes = [];
foreach ($requestedShareTypes as $shareType) {
$shares = $this->shareManager->getSharesBy($userId, $shareType, $file, false, 1, 0);
if (count($shares)) {
$shareTypes[] = $shareType;
}
}
return $shareTypes;
}
}