Completed
Pull Request — master (#148)
by korelstar
31:47
created

NotesService::update()   C

Complexity

Conditions 7
Paths 76

Size

Total Lines 49
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
c 7
b 1
f 0
dl 0
loc 49
rs 6.7272
cc 7
eloc 29
nc 76
nop 5
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
20
use OCA\Notes\Db\Note;
21
22
/**
23
 * Class NotesService
24
 *
25
 * @package OCA\Notes\Service
26
 */
27
class NotesService {
28
29
    private $l10n;
30
    private $root;
31
    private $logger;
32
    private $appName;
33
34
    /**
35
     * @param IRootFolder $root
36
     * @param IL10N $l10n
37
     * @param ILogger $logger
38
     * @param String $appName
39
     */
40
    public function __construct (IRootFolder $root, IL10N $l10n, ILogger $logger, $appName) {
41
        $this->root = $root;
42
        $this->l10n = $l10n;
43
        $this->logger = $logger;
44
        $this->appName = $appName;
45
    }
46
47
48
    /**
49
     * @param string $userId
50
     * @return array with all notes in the current directory
51
     */
52
    public function getAll ($userId){
53
        $notesFolder = $this->getFolderForUser($userId);
54
        $notes = $this->gatherNoteFiles($notesFolder);
55
        $filesById = [];
56
        foreach($notes as $note) {
57
            $filesById[$note->getId()] = $note;
58
        }
59
        $tagger = \OC::$server->getTagManager()->load('files');
60
        if($tagger===null) {
61
            $tags = [];
62
        } else {
63
            $tags = $tagger->getTagsForObjects(array_keys($filesById));
64
        }
65
66
        $notes = [];
67
        foreach($filesById as $id=>$file) {
68
            $notes[] = Note::fromFile($file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
69
        }
70
71
        return $notes;
72
    }
73
74
75
    /**
76
     * Used to get a single note by id
77
     * @param int $id the id of the note to get
78
     * @param string $userId
79
     * @throws NoteDoesNotExistException if note does not exist
80
     * @return Note
81
     */
82
    public function get ($id, $userId) {
83
        $folder = $this->getFolderForUser($userId);
84
        return Note::fromFile($this->getFileById($folder, $id), $folder, $this->getTags($id));
85
    }
86
87
    private function getTags ($id) {
88
        $tagger = \OC::$server->getTagManager()->load('files');
89
        if($tagger===null) {
90
            $tags = [];
91
        } else {
92
            $tags = $tagger->getTagsForObjects([$id]);
93
        }
94
        return array_key_exists($id, $tags) ? $tags[$id] : [];
95
    }
96
97
    /**
98
     * Creates a note and returns the empty note
99
     * @param string $userId
100
     * @see update for setting note content
101
     * @return Note the newly created note
102
     */
103
    public function create ($userId) {
104
        $title = $this->l10n->t('New note');
105
        $folder = $this->getFolderForUser($userId);
106
107
        // check new note exists already and we need to number it
108
        // pass -1 because no file has id -1 and that will ensure
109
        // to only return filenames that dont yet exist
110
        $path = $this->generateFileName($folder, $title, "txt", -1);
111
        $file = $folder->newFile($path);
112
113
        return Note::fromFile($file, $folder);
114
    }
115
116
117
    /**
118
     * Updates a note. Be sure to check the returned note since the title is
119
     * dynamically generated and filename conflicts are resolved
120
     * @param int $id the id of the note used to update
121
     * @param string $content the content which will be written into the note
122
     * the title is generated from the first line of the content
123
     * @param int $mtime time of the note modification (optional)
124
     * @throws NoteDoesNotExistException if note does not exist
125
     * @return \OCA\Notes\Db\Note the updated note
126
     */
127
    public function update ($id, $content, $userId, $category=null, $mtime=0) {
128
        $notesFolder = $this->getFolderForUser($userId);
129
        $file = $this->getFileById($notesFolder, $id);
130
        $folder = $file->getParent();
131
        $title = $this->getSafeTitleFromContent($content);
132
133
134
        // rename/move file with respect to title/category
135
        // this can fail if access rights are not sufficient or category name is illegal
136
        try {
137
            $currentFilePath = $this->root->getFullPath($file->getPath());
138
            $fileExtension = pathinfo($file->getName(), PATHINFO_EXTENSION);
139
140
            // detect (new) folder path based on category name
141
            if($category===null) {
142
                $basePath = pathinfo($currentFilePath, PATHINFO_DIRNAME);
143
            } else {
144
                $basePath = $notesFolder->getPath();
145
                if(!empty($category)) {
146
                    // sanitise path
147
                    $cats = explode('/', $category);
148
                    $cats = array_map([$this, 'sanitisePath'], $cats);
149
                    $cats = array_filter($cats, function($str) { return !empty($str); });
150
                    $basePath .= '/'.implode('/', $cats);
151
                }
152
                $this->getOrCreateFolder($basePath);
153
            }
154
155
            // assemble new file path
156
            $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileExtension, $id);
157
158
            // if the current path is not the new path, the file has to be renamed
159
            if($currentFilePath !== $newFilePath) {
160
                $file->move($newFilePath);
161
            }
162
        } 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...
163
            $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]);
164
        } catch(\Exception $e) {
165
            $this->logger->error('Moving note '.$id.' ('.$title.') to the desired target has failed with a '.get_class($e).': '.$e->getMessage(), ['app' => $this->appName]);
166
        }
167
168
        $file->putContent($content);
169
170
        if($mtime) {
171
            $file->touch($mtime);
172
        }
173
174
        return Note::fromFile($file, $notesFolder, $this->getTags($id));
175
    }
176
177
178
    /**
179
     * Set or unset a note as favorite.
180
     * @param int $id the id of the note used to update
181
     * @param boolean $favorite whether the note should be a favorite or not
182
     * @throws NoteDoesNotExistException if note does not exist
183
     * @return boolean the new favorite state of the note
184
     */
185
    public function favorite ($id, $favorite, $userId){
186
        $folder = $this->getFolderForUser($userId);
187
        $file = $this->getFileById($folder, $id);
188
        if(!$this->isNote($file)) {
189
            throw new NoteDoesNotExistException();
190
        }
191
        $tagger = \OC::$server->getTagManager()->load('files');
192
        if($favorite)
193
            $tagger->addToFavorites($id);
194
        else
195
            $tagger->removeFromFavorites($id);
196
197
        $tags = $tagger->getTagsForObjects([$id]);
198
        return array_key_exists($id, $tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]);
199
    }
200
201
202
    /**
203
     * Deletes a note
204
     * @param int $id the id of the note which should be deleted
205
     * @param string $userId
206
     * @throws NoteDoesNotExistException if note does not
207
     * exist
208
     */
209
    public function delete ($id, $userId) {
210
        $folder = $this->getFolderForUser($userId);
211
        $file = $this->getFileById($folder, $id);
212
        $file->delete();
213
    }
214
215
    // removes characters that are illegal in a file or folder name on some operating systems
216
    private function sanitisePath($str) {
217
        $str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str); // problematic characters
218
        // if mysql doesn't support 4byte UTF-8, then remove those characters
219
        if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
220
            $str = preg_replace('%(?:
221
                \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
222
              | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
223
              | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
224
              )%xs', '', $str);
225
        }
226
        $str = preg_replace("/^[\. ]+/mu", "", $str); // hidden files
227
        return trim($str);
228
    }
229
230
    private function getSafeTitleFromContent($content) {
231
        // prepare content: remove markdown characters and empty spaces
232
        $content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item
233
        $content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline
234
        $content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline
235
        $content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis
236
237
        // prevent directory traversal, illegal characters and unintended file names
238
        $content = $this->sanitisePath($content);
239
240
        // generate title from the first line of the content
241
        $splitContent = preg_split("/\R/u", $content, 2);
242
        $title = trim($splitContent[0]);
243
244
        // ensure that title is not empty
245
        if(empty($title)) {
246
            $title = $this->l10n->t('New note');
247
        }
248
249
        // using a maximum of 100 chars should be enough
250
        $title = mb_substr($title, 0, 100, "UTF-8");
251
252
        return $title;
253
    }
254
255
    /**
256
     * @param Folder $folder
257
     * @param int $id
258
     * @throws NoteDoesNotExistException
259
     * @return \OCP\Files\File
260
     */
261
    private function getFileById ($folder, $id) {
262
        $file = $folder->getById($id);
263
264
        if(count($file) <= 0 || !$this->isNote($file[0])) {
265
            throw new NoteDoesNotExistException();
266
        }
267
        return $file[0];
268
    }
269
270
271
    /**
272
     * @param string $userId the user id
273
     * @return Folder
274
     */
275
    private function getFolderForUser ($userId) {
276
        $path = '/' . $userId . '/files/Notes';
277
        return $this->getOrCreateFolder($path);
278
    }
279
280
281
    /**
282
     * Finds a folder and creates it if non-existent
283
     * @param string $path path to the folder
284
     * @return Folder
285
     */
286
    private function getOrCreateFolder($path) {
287
        if ($this->root->nodeExists($path)) {
288
            $folder = $this->root->get($path);
289
        } else {
290
            $folder = $this->root->newFolder($path);
291
        }
292
        return $folder;
293
    }
294
295
296
    /**
297
     * get path of file and the title.txt and check if they are the same
298
     * file. If not the title needs to be renamed
299
     *
300
     * @param Folder $folder a folder to the notes directory
301
     * @param string $title the filename which should be used
302
     * @param string $extension the extension which should be used
303
     * @param int $id the id of the note for which the title should be generated
304
     * used to see if the file itself has the title and not a different file for
305
     * checking for filename collisions
306
     * @return string the resolved filename to prevent overwriting different
307
     * files with the same title
308
     */
309
    private function generateFileName (Folder $folder, $title, $extension, $id) {
310
        $path = $title . '.' . $extension;
311
312
        // if file does not exist, that name has not been taken. Similar we don't
313
        // need to handle file collisions if it is the filename did not change
314
        if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
315
            return $path;
316
        } else {
317
            // increments name (2) to name (3)
318
            $match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
319
            if($match) {
320
                $newId = ((int) $matches['id']) + 1;
321
                $newTitle = preg_replace('/(.*)\s\((\d+)\)$/u',
322
                    '$1 (' . $newId . ')', $title);
323
            } else {
324
                $newTitle = $title . ' (2)';
325
            }
326
            return $this->generateFileName($folder, $newTitle, $extension, $id);
327
        }
328
    }
329
330
	/**
331
	 * gather note files in given directory and all subdirectories
332
	 * @param Folder $folder
333
	 * @return array
334
	 */
335
	private function gatherNoteFiles ($folder) {
336
		$notes = [];
337
		$nodes = $folder->getDirectoryListing();
338
		foreach($nodes as $node) {
339
			if($node->getType() === FileInfo::TYPE_FOLDER) {
340
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
341
				continue;
342
			}
343
			if($this->isNote($node)) {
344
				$notes[] = $node;
345
			}
346
		}
347
		return $notes;
348
	}
349
350
351
    /**
352
     * test if file is a note
353
     *
354
     * @param \OCP\Files\File $file
355
     * @return bool
356
     */
357
    private function isNote($file) {
358
        $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
359
360
        if($file->getType() !== 'file') return false;
361
        if(!in_array(
362
            pathinfo($file->getName(), PATHINFO_EXTENSION),
363
            $allowedExtensions
364
        )) return false;
365
366
        return true;
367
    }
368
369
}
370