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

NotesService::isNote()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
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
        if($tagger===null) {
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
        if($tagger===null) {
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, $category=null, $mtime=0) {
121
        $notesFolder = $this->getFolderForUser($userId);
122
        $file = $this->getFileById($notesFolder, $id);
123
        $folder = $file->getParent();
124
125
        // generate content from the first line of the title
126
        $splitContent = preg_split("/\R/", $content, 2);
127
        $title = $splitContent[0];
128
129
        if(!$title) {
130
            $title = $this->l10n->t('New note');
131
        }
132
133
        // prevent directory traversal
134
        $title = str_replace(array('/', '\\'), '',  $title);
135
        // remove hash and space characters from the beginning of the filename
136
        // in case of markdown
137
        $title = ltrim($title, ' #');
138
        // using a maximum of 100 chars should be enough
139
        $title = mb_substr($title, 0, 100, "UTF-8");
140
141
142
        // rename/move file with respect to title/category
143
        // this can fail if access rights are not sufficient or category name is illegal
144
        try {
145
            $currentFilePath = $file->getPath();
146
            $fileExtension = pathinfo($file->getName(), PATHINFO_EXTENSION);
147
148
            // detect (new) folder path based on category name
149
            if($category===null) {
150
                $basePath = pathinfo($file->getPath(), PATHINFO_DIRNAME);
151
            } else {
152
                $basePath = $notesFolder->getPath();
153
                if(!empty($category))
154
                    $basePath .= '/'.$category;
155
                $this->getOrCreateFolder($basePath);
156
            }
157
158
            // assemble new file path
159
            $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileExtension, $id);
160
161
            // if the current path is not the new path, the file has to be renamed
162
            if($currentFilePath !== $newFilePath) {
163
                $file->move($newFilePath);
164
            }
165
        } catch(\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
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
216
    /**
217
     * @param Folder $folder
218
     * @param int $id
219
     * @throws NoteDoesNotExistException
220
     * @return \OCP\Files\File
221
     */
222
    private function getFileById ($folder, $id) {
223
        $file = $folder->getById($id);
224
225
        if(count($file) <= 0 || !$this->isNote($file[0])) {
226
            throw new NoteDoesNotExistException();
227
        }
228
        return $file[0];
229
    }
230
231
232
    /**
233
     * @param string $userId the user id
234
     * @return Folder
235
     */
236
    private function getFolderForUser ($userId) {
237
        $path = '/' . $userId . '/files/Notes';
238
        return $this->getOrCreateFolder($path);
239
    }
240
241
242
    /**
243
     * Finds a folder and creates it if non-existent
244
     * @param string $path path to the folder
245
     * @return Folder
246
     */
247
    private function getOrCreateFolder($path) {
248
        if ($this->root->nodeExists($path)) {
249
            $folder = $this->root->get($path);
250
        } else {
251
            $folder = $this->root->newFolder($path);
252
        }
253
        return $folder;
254
    }
255
256
257
    /**
258
     * get path of file and the title.txt and check if they are the same
259
     * file. If not the title needs to be renamed
260
     *
261
     * @param Folder $folder a folder to the notes directory
262
     * @param string $title the filename which should be used
263
     * @param string $extension the extension which should be used
264
     * @param int $id the id of the note for which the title should be generated
265
     * used to see if the file itself has the title and not a different file for
266
     * checking for filename collisions
267
     * @return string the resolved filename to prevent overwriting different
268
     * files with the same title
269
     */
270
    private function generateFileName (Folder $folder, $title, $extension, $id) {
271
        $path = $title . '.' . $extension;
272
273
        // if file does not exist, that name has not been taken. Similar we don't
274
        // need to handle file collisions if it is the filename did not change
275
        if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
276
            return $path;
277
        } else {
278
            // increments name (2) to name (3)
279
            $match = preg_match('/\((?P<id>\d+)\)$/', $title, $matches);
280
            if($match) {
281
                $newId = ((int) $matches['id']) + 1;
282
                $newTitle = preg_replace('/(.*)\s\((\d+)\)$/',
283
                    '$1 (' . $newId . ')', $title);
284
            } else {
285
                $newTitle = $title . ' (2)';
286
            }
287
            return $this->generateFileName($folder, $newTitle, $extension, $id);
288
        }
289
    }
290
291
	/**
292
	 * gather note files in given directory and all subdirectories
293
	 * @param Folder $folder
294
	 * @return array
295
	 */
296
	private function gatherNoteFiles ($folder) {
297
		$notes = [];
298
		$nodes = $folder->getDirectoryListing();
299
		foreach($nodes as $node) {
300
			if($node->getType() === FileInfo::TYPE_FOLDER) {
301
				$notes = array_merge($notes, $this->gatherNoteFiles($node));
302
				continue;
303
			}
304
			if($this->isNote($node)) {
305
				$notes[] = $node;
306
			}
307
		}
308
		return $notes;
309
	}
310
311
312
    /**
313
     * test if file is a note
314
     *
315
     * @param \OCP\Files\File $file
316
     * @return bool
317
     */
318
    private function isNote($file) {
319
        $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
320
321
        if($file->getType() !== 'file') return false;
322
        if(!in_array(
323
            pathinfo($file->getName(), PATHINFO_EXTENSION),
324
            $allowedExtensions
325
        )) return false;
326
327
        return true;
328
    }
329
330
}
331