Completed
Pull Request — master (#210)
by korelstar
35:08
created

NotesService::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * Nextcloud - Notes
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Bernhard Posselt <[email protected]>
9
 * @copyright Bernhard Posselt 2012, 2014
10
 */
11
12
namespace OCA\Notes\Service;
13
14
use OCP\Files\FileInfo;
15
use OCP\IL10N;
16
use OCP\Files\IRootFolder;
17
use OCP\Files\Folder;
18
use OCP\ILogger;
19
use OC\Encryption\Exceptions\DecryptionFailedException;
20
use League\Flysystem\FileNotFoundException;
21
use OCA\Notes\Db\Note;
22
23
/**
24
 * Class NotesService
25
 *
26
 * @package OCA\Notes\Service
27
 */
28
class NotesService {
29
30
    private $l10n;
31
    private $root;
32
    private $logger;
33
    private $appName;
34
35
    /**
36
     * @param IRootFolder $root
37
     * @param IL10N $l10n
38
     * @param ILogger $logger
39
     * @param String $appName
40
     */
41
    public function __construct (IRootFolder $root, IL10N $l10n, ILogger $logger, $appName) {
42
        $this->root = $root;
43
        $this->l10n = $l10n;
44
        $this->logger = $logger;
45
        $this->appName = $appName;
46
    }
47
48
    public function getCategories($userId) {
49
        $notesFolder = $this->getFolderForUser($userId);
50
        $categories = $this->gatherSubfolders($notesFolder);
51
        return $categories;
52
    }
53
54
    /**
55
     * @param string $userId
56
     * @return array with all notes in the current directory
57
     */
58
    public function getAll ($userId){
59
        $notesFolder = $this->getFolderForUser($userId);
60
        $notes = $this->gatherNoteFiles($notesFolder);
61
        $filesById = [];
62
        foreach($notes as $note) {
63
            $filesById[$note->getId()] = $note;
64
        }
65
        $tagger = \OC::$server->getTagManager()->load('files');
66
        if($tagger===null) {
67
            $tags = [];
68
        } else {
69
            $tags = $tagger->getTagsForObjects(array_keys($filesById));
70
        }
71
72
        $notes = [];
73
        foreach($filesById as $id=>$file) {
74
            $notes[] = $this->getNote($file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
75
        }
76
77
        return $notes;
78
    }
79
80
81
    /**
82
     * Used to get a single note by id
83
     * @param int $id the id of the note to get
84
     * @param string $userId
85
     * @throws NoteDoesNotExistException if note does not exist
86
     * @return Note
87
     */
88
    public function get ($id, $userId) {
89
        $folder = $this->getFolderForUser($userId);
90
        return $this->getNote($this->getFileById($folder, $id), $folder, $this->getTags($id));
91
    }
92
93
    private function getTags ($id) {
94
        $tagger = \OC::$server->getTagManager()->load('files');
95
        if($tagger===null) {
96
            $tags = [];
97
        } else {
98
            $tags = $tagger->getTagsForObjects([$id]);
99
        }
100
        return array_key_exists($id, $tags) ? $tags[$id] : [];
101
    }
102
    private function getNote($file,$notesFolder,$tags=[]){
103
104
        $id=$file->getId();
105
106
        try{
107
            $note=Note::fromFile($file, $notesFolder, $tags);
108
        }catch(FileNotFoundException $e){
0 ignored issues
show
Bug introduced by
The class League\Flysystem\FileNotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
109
            $note = Note::fromException($this->l10n->t('File error').': ('.$file->getName().') '.$e->getMessage(), $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
110
        }catch(DecryptionFailedException $e){
0 ignored issues
show
Bug introduced by
The class OC\Encryption\Exceptions\DecryptionFailedException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
111
            $note = Note::fromException($this->l10n->t('Encryption Error').': ('.$file->getName().') '.$e->getMessage(), $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
112
        }catch(\Exception $e){
113
            $note = Note::fromException($this->l10n->t('Error').': ('.$file->getName().') '.$e->getMessage(), $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
114
        }
115
        return $note;
116
    }
117
    /**
118
     * Creates a note and returns the empty note
119
     * @param string $userId
120
     * @see update for setting note content
121
     * @return Note the newly created note
122
     */
123
    public function create ($userId) {
124
        $title = $this->l10n->t('New note');
125
        $folder = $this->getFolderForUser($userId);
126
127
        // check new note exists already and we need to number it
128
        // pass -1 because no file has id -1 and that will ensure
129
        // to only return filenames that dont yet exist
130
        $path = $this->generateFileName($folder, $title, "txt", -1);
131
        $file = $folder->newFile($path);
132
133
        return $this->getNote($file, $folder);
134
    }
135
136
137
    /**
138
     * Updates a note. Be sure to check the returned note since the title is
139
     * dynamically generated and filename conflicts are resolved
140
     * @param int $id the id of the note used to update
141
     * @param string $content the content which will be written into the note
142
     * the title is generated from the first line of the content
143
     * @param int $mtime time of the note modification (optional)
144
     * @throws NoteDoesNotExistException if note does not exist
145
     * @return \OCA\Notes\Db\Note the updated note
146
     */
147
    public function update ($id, $content, $userId, $category=null, $mtime=0) {
148
        $notesFolder = $this->getFolderForUser($userId);
149
        $file = $this->getFileById($notesFolder, $id);
150
        $folder = $file->getParent();
151
        $title = $this->getSafeTitleFromContent($content);
152
153
154
        // rename/move file with respect to title/category
155
        // this can fail if access rights are not sufficient or category name is illegal
156
        try {
157
            $currentFilePath = $this->root->getFullPath($file->getPath());
158
            $fileExtension = pathinfo($file->getName(), PATHINFO_EXTENSION);
159
160
            // detect (new) folder path based on category name
161
            if($category===null) {
162
                $basePath = pathinfo($currentFilePath, PATHINFO_DIRNAME);
163
            } else {
164
                $basePath = $notesFolder->getPath();
165
                if(!empty($category)) {
166
                    // sanitise path
167
                    $cats = explode('/', $category);
168
                    $cats = array_map([$this, 'sanitisePath'], $cats);
169
                    $cats = array_filter($cats, function($str) { return !empty($str); });
170
                    $basePath .= '/'.implode('/', $cats);
171
                }
172
                $this->getOrCreateFolder($basePath);
173
            }
174
175
            // assemble new file path
176
            $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileExtension, $id);
177
178
            // if the current path is not the new path, the file has to be renamed
179
            if($currentFilePath !== $newFilePath) {
180
                $file->move($newFilePath);
181
            }
182
        } catch(\OCP\Files\NotPermittedException $e) {
0 ignored issues
show
Bug introduced by
The class OCP\Files\NotPermittedException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
183
            $this->logger->error('Moving note '.$id.' ('.$title.') to the desired target is not allowed. Please check the note\'s target category ('.$category.').', ['app' => $this->appName]);
184
        } catch(\Exception $e) {
185
            $this->logger->error('Moving note '.$id.' ('.$title.') to the desired target has failed with a '.get_class($e).': '.$e->getMessage(), ['app' => $this->appName]);
186
        }
187
188
        $file->putContent($content);
189
190
        if($mtime) {
191
            $file->touch($mtime);
192
        }
193
194
        return $this->getNote($file, $notesFolder, $this->getTags($id));
195
    }
196
197
198
    /**
199
     * Set or unset a note as favorite.
200
     * @param int $id the id of the note used to update
201
     * @param boolean $favorite whether the note should be a favorite or not
202
     * @throws NoteDoesNotExistException if note does not exist
203
     * @return boolean the new favorite state of the note
204
     */
205
    public function favorite ($id, $favorite, $userId){
206
        $folder = $this->getFolderForUser($userId);
207
        $file = $this->getFileById($folder, $id);
208
        if(!$this->isNote($file)) {
209
            throw new NoteDoesNotExistException();
210
        }
211
        $tagger = \OC::$server->getTagManager()->load('files');
212
        if($favorite)
213
            $tagger->addToFavorites($id);
214
        else
215
            $tagger->removeFromFavorites($id);
216
217
        $tags = $tagger->getTagsForObjects([$id]);
218
        return array_key_exists($id, $tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]);
219
    }
220
221
222
    /**
223
     * Deletes a note
224
     * @param int $id the id of the note which should be deleted
225
     * @param string $userId
226
     * @throws NoteDoesNotExistException if note does not
227
     * exist
228
     */
229
    public function delete ($id, $userId) {
230
        $folder = $this->getFolderForUser($userId);
231
        $file = $this->getFileById($folder, $id);
232
        $file->delete();
233
    }
234
235
    // removes characters that are illegal in a file or folder name on some operating systems
236
    private function sanitisePath($str) {
237
        // remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
238
        // prevents also directory traversal by eliminiating slashes
239
        // see also \OC\Files\Storage\Common::verifyPosixPath(...)
240
        $str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
241
242
        // if mysql doesn't support 4byte UTF-8, then remove those characters
243
        // see \OC\Files\Storage\Common::verifyPath(...)
244
        if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
245
            $str = preg_replace('%(?:
246
                \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
247
              | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
248
              | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
249
              )%xs', '', $str);
250
        }
251
252
        // prevent file to be hidden
253
        $str = preg_replace("/^[\. ]+/mu", "", $str);
254
        return trim($str);
255
    }
256
257
    private function getSafeTitleFromContent($content) {
258
        // prepare content: remove markdown characters and empty spaces
259
        $content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item
260
        $content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline
261
        $content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline
262
        $content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis
263
264
        // sanitize: prevent directory traversal, illegal characters and unintended file names
265
        $content = $this->sanitisePath($content);
266
267
        // generate title from the first line of the content
268
        $splitContent = preg_split("/\R/u", $content, 2);
269
        $title = trim($splitContent[0]);
270
271
        // ensure that title is not empty
272
        if(empty($title)) {
273
            $title = $this->l10n->t('New note');
274
        }
275
276
        // using a maximum of 100 chars should be enough
277
        $title = mb_substr($title, 0, 100, "UTF-8");
278
279
        return $title;
280
    }
281
282
    /**
283
     * @param Folder $folder
284
     * @param int $id
285
     * @throws NoteDoesNotExistException
286
     * @return \OCP\Files\File
287
     */
288
    private function getFileById ($folder, $id) {
289
        $file = $folder->getById($id);
290
291
        if(count($file) <= 0 || !$this->isNote($file[0])) {
292
            throw new NoteDoesNotExistException();
293
        }
294
        return $file[0];
295
    }
296
297
298
    /**
299
     * @param string $userId the user id
300
     * @return Folder
301
     */
302
    private function getFolderForUser ($userId) {
303
        $path = '/' . $userId . '/files/Notes';
304
        return $this->getOrCreateFolder($path);
305
    }
306
307
308
    /**
309
     * Finds a folder and creates it if non-existent
310
     * @param string $path path to the folder
311
     * @return Folder
312
     */
313
    private function getOrCreateFolder($path) {
314
        if ($this->root->nodeExists($path)) {
315
            $folder = $this->root->get($path);
316
        } else {
317
            $folder = $this->root->newFolder($path);
318
        }
319
        return $folder;
320
    }
321
322
323
    /**
324
     * get path of file and the title.txt and check if they are the same
325
     * file. If not the title needs to be renamed
326
     *
327
     * @param Folder $folder a folder to the notes directory
328
     * @param string $title the filename which should be used
329
     * @param string $extension the extension which should be used
330
     * @param int $id the id of the note for which the title should be generated
331
     * used to see if the file itself has the title and not a different file for
332
     * checking for filename collisions
333
     * @return string the resolved filename to prevent overwriting different
334
     * files with the same title
335
     */
336
    private function generateFileName (Folder $folder, $title, $extension, $id) {
337
        $path = $title . '.' . $extension;
338
339
        // if file does not exist, that name has not been taken. Similar we don't
340
        // need to handle file collisions if it is the filename did not change
341
        if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
342
            return $path;
343
        } else {
344
            // increments name (2) to name (3)
345
            $match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
346
            if($match) {
347
                $newId = ((int) $matches['id']) + 1;
348
                $newTitle = preg_replace('/(.*)\s\((\d+)\)$/u',
349
                    '$1 (' . $newId . ')', $title);
350
            } else {
351
                $newTitle = $title . ' (2)';
352
            }
353
            return $this->generateFileName($folder, $newTitle, $extension, $id);
354
        }
355
    }
356
357
	/**
358
	 * gather sub-directories in given directory and all subdirectories
359
	 * @param Folder $folder
360
	 * @return array
361
	 */
362
	private function gatherSubfolders($folder) {
363
		$folders = [];
364
		$nodes = $folder->getDirectoryListing();
365
		foreach($nodes as $node) {
366
			if($node->getType() === FileInfo::TYPE_FOLDER) {
367
				$notes = $this->gatherNoteFiles($node);
368
				$folders[] = ['name'=>$node->getName(), 'notes'=>count($notes)];
369
			}
370
		}
371
/*
372
		foreach($nodes as $node) {
373
			$numNotes = 0;
374
			if($node->getType() === FileInfo::TYPE_FOLDER) {
375
				$folders = array_merge($folders, $this->gatherSubfolders($node));
376
			}
377
        		if($this->isNote($node)) {
378
				$numNotes++;
379
			}
380
		}
381
		$folders[] = ['name'=>..., 'notes'=>$numNotes];
382
 */
383
		return $folders;
384
	}
385
386
	/**
387
	 * gather note files in given directory and all subdirectories
388
	 * @param Folder $folder
389
	 * @return array
390
	 */
391
	private function gatherNoteFiles ($folder) {
392
		$notes = [];
393
		$nodes = $folder->getDirectoryListing();
394
		foreach($nodes as $node) {
395
			if($node->getType() === FileInfo::TYPE_FOLDER) {
396
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
397
				continue;
398
			}
399
			if($this->isNote($node)) {
400
				$notes[] = $node;
401
			}
402
		}
403
		return $notes;
404
	}
405
406
407
    /**
408
     * test if file is a note
409
     *
410
     * @param \OCP\Files\File $file
411
     * @return bool
412
     */
413
    private function isNote($file) {
414
        $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
415
416
        if($file->getType() !== 'file') return false;
417
        if(!in_array(
418
            pathinfo($file->getName(), PATHINFO_EXTENSION),
419
            $allowedExtensions
420
        )) return false;
421
422
        return true;
423
    }
424
425
}
426