Completed
Pull Request — master (#88)
by korelstar
02:39
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
19
use OCA\Notes\Db\Note;
20
21
/**
22
 * Class NotesService
23
 *
24
 * @package OCA\Notes\Service
25
 */
26
class NotesService {
27
28
    private $l10n;
29
    private $root;
30
31
    /**
32
     * @param IRootFolder $root
33
     * @param IL10N $l10n
34
     */
35
    public function __construct (IRootFolder $root, IL10N $l10n) {
36
        $this->root = $root;
37
        $this->l10n = $l10n;
38
    }
39
40
41
    /**
42
     * @param string $userId
43
     * @return array with all notes in the current directory
44
     */
45
    public function getAll ($userId){
46
        $notesFolder = $this->getFolderForUser($userId);
47
        $notes = $this->gatherNoteFiles($notesFolder);
48
        $filesById = [];
49
        foreach($notes as $note) {
50
            $filesById[$note->getId()] = $note;
51
        }
52
        $tagger = \OC::$server->getTagManager()->load('files');
53 View Code Duplication
        if($tagger===null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
54
            $tags = [];
55
        } else {
56
            $tags = $tagger->getTagsForObjects(array_keys($filesById));
57
        }
58
59
        $notes = [];
60
        foreach($filesById as $id=>$file) {
61
            $notes[] = Note::fromFile($file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
62
        }
63
64
        return $notes;
65
    }
66
67
68
    /**
69
     * Used to get a single note by id
70
     * @param int $id the id of the note to get
71
     * @param string $userId
72
     * @throws NoteDoesNotExistException if note does not exist
73
     * @return Note
74
     */
75
    public function get ($id, $userId) {
76
        $folder = $this->getFolderForUser($userId);
77
        return Note::fromFile($this->getFileById($folder, $id), $folder, $this->getTags($id));
78
    }
79
80
    private function getTags ($id) {
81
        $tagger = \OC::$server->getTagManager()->load('files');
82 View Code Duplication
        if($tagger===null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
83
            $tags = [];
84
        } else {
85
            $tags = $tagger->getTagsForObjects([$id]);
86
        }
87
        return array_key_exists($id, $tags) ? $tags[$id] : [];
88
    }
89
90
    /**
91
     * Creates a note and returns the empty note
92
     * @param string $userId
93
     * @see update for setting note content
94
     * @return Note the newly created note
95
     */
96
    public function create ($userId) {
97
        $title = $this->l10n->t('New note');
98
        $folder = $this->getFolderForUser($userId);
99
100
        // check new note exists already and we need to number it
101
        // pass -1 because no file has id -1 and that will ensure
102
        // to only return filenames that dont yet exist
103
        $path = $this->generateFileName($folder, $title, "txt", -1);
104
        $file = $folder->newFile($path);
105
106
        return Note::fromFile($file, $folder);
107
    }
108
109
110
    /**
111
     * Updates a note. Be sure to check the returned note since the title is
112
     * dynamically generated and filename conflicts are resolved
113
     * @param int $id the id of the note used to update
114
     * @param string $content the content which will be written into the note
115
     * the title is generated from the first line of the content
116
     * @param int $mtime time of the note modification (optional)
117
     * @throws NoteDoesNotExistException if note does not exist
118
     * @return \OCA\Notes\Db\Note the updated note
119
     */
120
    public function update ($id, $content, $userId, $mtime=0) {
121
        $notesFolder = $this->getFolderForUser($userId);
122
        $file = $this->getFileById($notesFolder, $id);
123
        $folder = $file->getParent();
124
        $title = $this->getSafeTitleFromContent($content);
125
126
        // generate filename if there were collisions
127
        $currentFilePath = $file->getPath();
128
        $basePath = pathinfo($file->getPath(), PATHINFO_DIRNAME);
129
        $fileExtension = pathinfo($file->getName(), PATHINFO_EXTENSION);
130
        $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileExtension, $id);
131
132
        // if the current path is not the new path, the file has to be renamed
133
        if($currentFilePath !== $newFilePath) {
134
            $file->move($newFilePath);
135
        }
136
137
        $file->putContent($content);
138
139
        if($mtime) {
140
            $file->touch($mtime);
141
        }
142
143
        return Note::fromFile($file, $notesFolder, $this->getTags($id));
144
    }
145
146
147
    /**
148
     * Set or unset a note as favorite.
149
     * @param int $id the id of the note used to update
150
     * @param boolean $favorite whether the note should be a favorite or not
151
     * @throws NoteDoesNotExistException if note does not exist
152
     * @return boolean the new favorite state of the note
153
     */
154
    public function favorite ($id, $favorite, $userId){
155
        $folder = $this->getFolderForUser($userId);
156
        $file = $this->getFileById($folder, $id);
157
        if(!$this->isNote($file)) {
158
            throw new NoteDoesNotExistException();
159
        }
160
        $tagger = \OC::$server->getTagManager()->load('files');
161
        if($favorite)
162
            $tagger->addToFavorites($id);
163
        else
164
            $tagger->removeFromFavorites($id);
165
166
        $tags = $tagger->getTagsForObjects([$id]);
167
        return array_key_exists($id, $tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]);
168
    }
169
170
171
    /**
172
     * Deletes a note
173
     * @param int $id the id of the note which should be deleted
174
     * @param string $userId
175
     * @throws NoteDoesNotExistException if note does not
176
     * exist
177
     */
178
    public function delete ($id, $userId) {
179
        $folder = $this->getFolderForUser($userId);
180
        $file = $this->getFileById($folder, $id);
181
        $file->delete();
182
    }
183
184
    private function getSafeTitleFromContent($content) {
185
        // prepare content: remove markdown characters and empty spaces
186
        $content = preg_replace("/^\s*[*+-]\s+/m", "", $content); // list item
187
        $content = preg_replace("/^#+\s+(.*?)\s*#*$/m", "$1", $content); // headline
188
        $content = preg_replace("/^(=+|-+)$/m", "", $content); // separate line for headline
189
        $content = preg_replace("/(\*+|_+)(.*?)\\1/m", "$2", $content); // emphasis
190
        $content = trim($content);
191
192
        // generate content from the first line of the title
193
        $splitContent = preg_split("/\R/", $content, 2);
194
        $title = trim($splitContent[0]);
195
196
        // ensure that title is not empty
197
        if(empty($title)) {
198
            $title = $this->l10n->t('New note');
199
        }
200
201
        // prevent directory traversal
202
        $title = str_replace(array('/', '\\'), '',  $title);
203
204
        // using a maximum of 100 chars should be enough
205
        $title = mb_substr($title, 0, 100, "UTF-8");
206
207
        return $title;
208
    }
209
210
    /**
211
     * @param Folder $folder
212
     * @param int $id
213
     * @throws NoteDoesNotExistException
214
     * @return \OCP\Files\File
215
     */
216
    private function getFileById ($folder, $id) {
217
        $file = $folder->getById($id);
218
219
        if(count($file) <= 0 || !$this->isNote($file[0])) {
220
            throw new NoteDoesNotExistException();
221
        }
222
        return $file[0];
223
    }
224
225
226
    /**
227
     * @param string $userId the user id
228
     * @return Folder
229
     */
230
    private function getFolderForUser ($userId) {
231
        $path = '/' . $userId . '/files/Notes';
232
        if ($this->root->nodeExists($path)) {
233
            $folder = $this->root->get($path);
234
        } else {
235
            $folder = $this->root->newFolder($path);
236
        }
237
        return $folder;
238
    }
239
240
241
    /**
242
     * get path of file and the title.txt and check if they are the same
243
     * file. If not the title needs to be renamed
244
     *
245
     * @param Folder $folder a folder to the notes directory
246
     * @param string $title the filename which should be used
247
     * @param string $extension the extension which should be used
248
     * @param int $id the id of the note for which the title should be generated
249
     * used to see if the file itself has the title and not a different file for
250
     * checking for filename collisions
251
     * @return string the resolved filename to prevent overwriting different
252
     * files with the same title
253
     */
254
    private function generateFileName (Folder $folder, $title, $extension, $id) {
255
        $path = $title . '.' . $extension;
256
257
        // if file does not exist, that name has not been taken. Similar we don't
258
        // need to handle file collisions if it is the filename did not change
259
        if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
260
            return $path;
261
        } else {
262
            // increments name (2) to name (3)
263
            $match = preg_match('/\((?P<id>\d+)\)$/', $title, $matches);
264
            if($match) {
265
                $newId = ((int) $matches['id']) + 1;
266
                $newTitle = preg_replace('/(.*)\s\((\d+)\)$/',
267
                    '$1 (' . $newId . ')', $title);
268
            } else {
269
                $newTitle = $title . ' (2)';
270
            }
271
            return $this->generateFileName($folder, $newTitle, $extension, $id);
272
        }
273
    }
274
275
276
	/**
277
	 * gather note files in given directory and all subdirectories
278
	 * @param Folder $folder
279
	 * @return array
280
	 */
281
	private function gatherNoteFiles ($folder) {
282
		$notes = [];
283
		$nodes = $folder->getDirectoryListing();
284
		foreach($nodes as $node) {
285
			if($node->getType() === FileInfo::TYPE_FOLDER) {
286
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
287
				continue;
288
			}
289
			if($this->isNote($node)) {
290
				$notes[] = $node;
291
			}
292
		}
293
		return $notes;
294
	}
295
296
297
    /**
298
     * test if file is a note
299
     *
300
     * @param \OCP\Files\File $file
301
     * @return bool
302
     */
303
    private function isNote($file) {
304
        $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
305
306
        if($file->getType() !== 'file') return false;
307
        if(!in_array(
308
            pathinfo($file->getName(), PATHINFO_EXTENSION),
309
            $allowedExtensions
310
        )) return false;
311
312
        return true;
313
    }
314
315
}
316