Completed
Pull Request — master (#87)
by korelstar
02:30
created

NotesService::getSafeTitleFromContent()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.8571
cc 2
eloc 13
nc 2
nop 1
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 = $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($file->getPath(), PATHINFO_DIRNAME);
143
            } else {
144
                $basePath = $notesFolder->getPath();
145
                if(!empty($category))
146
                    $basePath .= '/'.$category;
147
                $this->getOrCreateFolder($basePath);
148
            }
149
150
            // assemble new file path
151
            $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileExtension, $id);
152
153
            // if the current path is not the new path, the file has to be renamed
154
            if($currentFilePath !== $newFilePath) {
155
                $file->move($newFilePath);
156
            }
157
        } 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...
158
            $this->logger->error('Moving this note to the desired target is not allowed. Please check the note\'s target category.', array('app' => $this->appName));
159
        }
160
161
        $file->putContent($content);
162
163
        if($mtime) {
164
            $file->touch($mtime);
165
        }
166
167
        return Note::fromFile($file, $notesFolder, $this->getTags($id));
168
    }
169
170
171
    /**
172
     * Set or unset a note as favorite.
173
     * @param int $id the id of the note used to update
174
     * @param boolean $favorite whether the note should be a favorite or not
175
     * @throws NoteDoesNotExistException if note does not exist
176
     * @return boolean the new favorite state of the note
177
     */
178
    public function favorite ($id, $favorite, $userId){
179
        $folder = $this->getFolderForUser($userId);
180
        $file = $this->getFileById($folder, $id);
181
        if(!$this->isNote($file)) {
182
            throw new NoteDoesNotExistException();
183
        }
184
        $tagger = \OC::$server->getTagManager()->load('files');
185
        if($favorite)
186
            $tagger->addToFavorites($id);
187
        else
188
            $tagger->removeFromFavorites($id);
189
190
        $tags = $tagger->getTagsForObjects([$id]);
191
        return array_key_exists($id, $tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]);
192
    }
193
194
195
    /**
196
     * Deletes a note
197
     * @param int $id the id of the note which should be deleted
198
     * @param string $userId
199
     * @throws NoteDoesNotExistException if note does not
200
     * exist
201
     */
202
    public function delete ($id, $userId) {
203
        $folder = $this->getFolderForUser($userId);
204
        $file = $this->getFileById($folder, $id);
205
        $file->delete();
206
    }
207
208
    private function getSafeTitleFromContent($content) {
209
        // prepare content: remove markdown characters and empty spaces
210
        $content = preg_replace("/^\s*[*+-]\s+/m", "", $content); // list item
211
        $content = preg_replace("/^#+\s+(.*?)\s*#*$/m", "$1", $content); // headline
212
        $content = preg_replace("/^(=+|-+)$/m", "", $content); // separate line for headline
213
        $content = preg_replace("/(\*+|_+)(.*?)\\1/m", "$2", $content); // emphasis
214
        $content = trim($content);
215
216
        // generate content from the first line of the title
217
        $splitContent = preg_split("/\R/", $content, 2);
218
        $title = trim($splitContent[0]);
219
220
        // ensure that title is not empty
221
        if(empty($title)) {
222
            $title = $this->l10n->t('New note');
223
        }
224
225
        // prevent directory traversal
226
        $title = str_replace(array('/', '\\'), '',  $title);
227
228
        // using a maximum of 100 chars should be enough
229
        $title = mb_substr($title, 0, 100, "UTF-8");
230
231
        return $title;
232
    }
233
234
    /**
235
     * @param Folder $folder
236
     * @param int $id
237
     * @throws NoteDoesNotExistException
238
     * @return \OCP\Files\File
239
     */
240
    private function getFileById ($folder, $id) {
241
        $file = $folder->getById($id);
242
243
        if(count($file) <= 0 || !$this->isNote($file[0])) {
244
            throw new NoteDoesNotExistException();
245
        }
246
        return $file[0];
247
    }
248
249
250
    /**
251
     * @param string $userId the user id
252
     * @return Folder
253
     */
254
    private function getFolderForUser ($userId) {
255
        $path = '/' . $userId . '/files/Notes';
256
        return $this->getOrCreateFolder($path);
257
    }
258
259
260
    /**
261
     * Finds a folder and creates it if non-existent
262
     * @param string $path path to the folder
263
     * @return Folder
264
     */
265
    private function getOrCreateFolder($path) {
266
        if ($this->root->nodeExists($path)) {
267
            $folder = $this->root->get($path);
268
        } else {
269
            $folder = $this->root->newFolder($path);
270
        }
271
        return $folder;
272
    }
273
274
275
    /**
276
     * get path of file and the title.txt and check if they are the same
277
     * file. If not the title needs to be renamed
278
     *
279
     * @param Folder $folder a folder to the notes directory
280
     * @param string $title the filename which should be used
281
     * @param string $extension the extension which should be used
282
     * @param int $id the id of the note for which the title should be generated
283
     * used to see if the file itself has the title and not a different file for
284
     * checking for filename collisions
285
     * @return string the resolved filename to prevent overwriting different
286
     * files with the same title
287
     */
288
    private function generateFileName (Folder $folder, $title, $extension, $id) {
289
        $path = $title . '.' . $extension;
290
291
        // if file does not exist, that name has not been taken. Similar we don't
292
        // need to handle file collisions if it is the filename did not change
293
        if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
294
            return $path;
295
        } else {
296
            // increments name (2) to name (3)
297
            $match = preg_match('/\((?P<id>\d+)\)$/', $title, $matches);
298
            if($match) {
299
                $newId = ((int) $matches['id']) + 1;
300
                $newTitle = preg_replace('/(.*)\s\((\d+)\)$/',
301
                    '$1 (' . $newId . ')', $title);
302
            } else {
303
                $newTitle = $title . ' (2)';
304
            }
305
            return $this->generateFileName($folder, $newTitle, $extension, $id);
306
        }
307
    }
308
309
	/**
310
	 * gather note files in given directory and all subdirectories
311
	 * @param Folder $folder
312
	 * @return array
313
	 */
314
	private function gatherNoteFiles ($folder) {
315
		$notes = [];
316
		$nodes = $folder->getDirectoryListing();
317
		foreach($nodes as $node) {
318
			if($node->getType() === FileInfo::TYPE_FOLDER) {
319
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
320
				continue;
321
			}
322
			if($this->isNote($node)) {
323
				$notes[] = $node;
324
			}
325
		}
326
		return $notes;
327
	}
328
329
330
    /**
331
     * test if file is a note
332
     *
333
     * @param \OCP\Files\File $file
334
     * @return bool
335
     */
336
    private function isNote($file) {
337
        $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
338
339
        if($file->getType() !== 'file') return false;
340
        if(!in_array(
341
            pathinfo($file->getName(), PATHINFO_EXTENSION),
342
            $allowedExtensions
343
        )) return false;
344
345
        return true;
346
    }
347
348
}
349