refactoring / code style
This commit is contained in:
parent
9a080fe185
commit
efd98043a7
|
@ -11,26 +11,102 @@
|
|||
|
||||
return ['routes' => [
|
||||
// page
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#index', 'url' => '/welcome', 'verb' => 'GET', 'postfix' => 'welcome'],
|
||||
['name' => 'page#index', 'url' => '/note/{id}', 'verb' => 'GET', 'postfix' => 'note', 'requirements' => ['id' => '\d+']],
|
||||
[
|
||||
'name' => 'page#index',
|
||||
'url' => '/',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'page#index',
|
||||
'url' => '/welcome',
|
||||
'verb' => 'GET',
|
||||
'postfix' => 'welcome',
|
||||
],
|
||||
[
|
||||
'name' => 'page#index',
|
||||
'url' => '/note/{id}',
|
||||
'verb' => 'GET',
|
||||
'postfix' => 'note',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
|
||||
// notes
|
||||
['name' => 'notes#index', 'url' => '/notes', 'verb' => 'GET'],
|
||||
['name' => 'notes#get', 'url' => '/notes/{id}', 'verb' => 'GET', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes#create', 'url' => '/notes', 'verb' => 'POST'],
|
||||
['name' => 'notes#update', 'url' => '/notes/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes#category', 'url' => '/notes/{id}/category', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes#favorite', 'url' => '/notes/{id}/favorite', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes#destroy', 'url' => '/notes/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '\d+']],
|
||||
[
|
||||
'name' => 'notes#index',
|
||||
'url' => '/notes',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'notes#get',
|
||||
'url' => '/notes/{id}',
|
||||
'verb' => 'GET',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes#create',
|
||||
'url' => '/notes',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
[
|
||||
'name' => 'notes#update',
|
||||
'url' => '/notes/{id}',
|
||||
'verb' => 'PUT',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes#category',
|
||||
'url' => '/notes/{id}/category',
|
||||
'verb' => 'PUT',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes#favorite',
|
||||
'url' => '/notes/{id}/favorite',
|
||||
'verb' => 'PUT',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes#destroy',
|
||||
'url' => '/notes/{id}',
|
||||
'verb' => 'DELETE',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
|
||||
// api
|
||||
['name' => 'notes_api#index', 'url' => '/api/v0.2/notes', 'verb' => 'GET'],
|
||||
['name' => 'notes_api#get', 'url' => '/api/v0.2/notes/{id}', 'verb' => 'GET', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes_api#create', 'url' => '/api/v0.2/notes', 'verb' => 'POST'],
|
||||
['name' => 'notes_api#update', 'url' => '/api/v0.2/notes/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes_api#destroy', 'url' => '/api/v0.2/notes/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '\d+']],
|
||||
['name' => 'notes_api#preflighted_cors', 'url' => '/api/v0.2/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
|
||||
[
|
||||
'name' => 'notes_api#index',
|
||||
'url' => '/api/v0.2/notes',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'notes_api#get',
|
||||
'url' => '/api/v0.2/notes/{id}',
|
||||
'verb' => 'GET',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes_api#create',
|
||||
'url' => '/api/v0.2/notes',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
[
|
||||
'name' => 'notes_api#update',
|
||||
'url' => '/api/v0.2/notes/{id}',
|
||||
'verb' => 'PUT',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes_api#destroy',
|
||||
'url' => '/api/v0.2/notes/{id}',
|
||||
'verb' => 'DELETE',
|
||||
'requirements' => ['id' => '\d+'],
|
||||
],
|
||||
[
|
||||
'name' => 'notes_api#preflighted_cors',
|
||||
'url' => '/api/v0.2/{path}',
|
||||
'verb' => 'OPTIONS',
|
||||
'requirements' => ['path' => '.+'],
|
||||
],
|
||||
|
||||
// settings
|
||||
['name' => 'settings#set', 'url' => '/settings', 'verb' => 'PUT'],
|
||||
|
|
|
@ -34,7 +34,13 @@ class NotesApiController extends ApiController {
|
|||
* @param NotesService $service
|
||||
* @param IUserSession $userSession
|
||||
*/
|
||||
public function __construct($AppName, IRequest $request, NotesService $service, MetaService $metaService, IUserSession $userSession) {
|
||||
public function __construct(
|
||||
$AppName,
|
||||
IRequest $request,
|
||||
NotesService $service,
|
||||
MetaService $metaService,
|
||||
IUserSession $userSession
|
||||
) {
|
||||
parent::__construct($AppName, $request);
|
||||
$this->service = $service;
|
||||
$this->metaService = $metaService;
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Notes\Service;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\ILogger;
|
||||
use OCP\Encryption\Exceptions\GenericEncryptionException;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
|
||||
use OCA\Notes\Db\Note;
|
||||
|
||||
class NoteUtil {
|
||||
|
||||
private $l10n;
|
||||
private $root;
|
||||
private $logger;
|
||||
private $appName;
|
||||
|
||||
/**
|
||||
* @param IRootFolder $root
|
||||
* @param IL10N $l10n
|
||||
* @param ILogger $logger
|
||||
* @param String $appName
|
||||
*/
|
||||
public function __construct(
|
||||
IRootFolder $root,
|
||||
IL10N $l10n,
|
||||
ILogger $logger,
|
||||
$appName
|
||||
) {
|
||||
$this->root = $root;
|
||||
$this->l10n = $l10n;
|
||||
$this->logger = $logger;
|
||||
$this->appName = $appName;
|
||||
}
|
||||
|
||||
/**
|
||||
* gather note files in given directory and all subdirectories
|
||||
*/
|
||||
public function gatherNoteFiles(Folder $folder) : array {
|
||||
$notes = [];
|
||||
$nodes = $folder->getDirectoryListing();
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->getType() === FileInfo::TYPE_FOLDER) {
|
||||
$notes = array_merge($notes, $this->gatherNoteFiles($node));
|
||||
continue;
|
||||
}
|
||||
if ($this->isNote($node)) {
|
||||
$notes[] = $node;
|
||||
}
|
||||
}
|
||||
return $notes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* test if file is a note
|
||||
*/
|
||||
public function isNote(File $file) : bool {
|
||||
$allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
|
||||
$ext = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION));
|
||||
return $file->getType() === 'file' && in_array($ext, $allowedExtensions);
|
||||
}
|
||||
|
||||
public function moveNote(Folder $notesFolder, File $file, $category, string $title) {
|
||||
$id = $file->getId();
|
||||
$currentFilePath = $this->root->getFullPath($file->getPath());
|
||||
$currentBasePath = pathinfo($currentFilePath, PATHINFO_DIRNAME);
|
||||
$fileSuffix = '.' . pathinfo($file->getName(), PATHINFO_EXTENSION);
|
||||
|
||||
// detect (new) folder path based on category name
|
||||
if ($category===null) {
|
||||
$basePath = $currentBasePath;
|
||||
} else {
|
||||
$basePath = $notesFolder->getPath();
|
||||
if (!empty($category)) {
|
||||
// sanitise path
|
||||
$cats = explode('/', $category);
|
||||
$cats = array_map([$this, 'sanitisePath'], $cats);
|
||||
$cats = array_filter($cats, function ($str) {
|
||||
return !empty($str);
|
||||
});
|
||||
$basePath .= '/'.implode('/', $cats);
|
||||
}
|
||||
}
|
||||
$folder = $this->getOrCreateFolder($basePath);
|
||||
|
||||
// assemble new file path
|
||||
$newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileSuffix, $id);
|
||||
|
||||
// if the current path is not the new path, the file has to be renamed
|
||||
if ($currentFilePath !== $newFilePath) {
|
||||
$file->move($newFilePath);
|
||||
}
|
||||
if ($currentBasePath !== $basePath) {
|
||||
$this->deleteEmptyFolder($notesFolder, $this->root->get($currentBasePath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get path of file and the title.txt and check if they are the same
|
||||
* file. If not the title needs to be renamed
|
||||
*
|
||||
* @param Folder $folder a folder to the notes directory
|
||||
* @param string $title the filename which should be used
|
||||
* @param string $suffix the suffix (incl. dot) which should be used
|
||||
* @param int $id the id of the note for which the title should be generated
|
||||
* used to see if the file itself has the title and not a different file for
|
||||
* checking for filename collisions
|
||||
* @return string the resolved filename to prevent overwriting different
|
||||
* files with the same title
|
||||
*/
|
||||
public function generateFileName(Folder $folder, string $title, string $suffix, int $id) : string {
|
||||
$path = $title . $suffix;
|
||||
|
||||
// if file does not exist, that name has not been taken. Similar we don't
|
||||
// need to handle file collisions if it is the filename did not change
|
||||
if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
|
||||
return $path;
|
||||
} else {
|
||||
// increments name (2) to name (3)
|
||||
$match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
|
||||
if ($match) {
|
||||
$newId = ((int) $matches['id']) + 1;
|
||||
$newTitle = preg_replace(
|
||||
'/(.*)\s\((\d+)\)$/u',
|
||||
'$1 (' . $newId . ')',
|
||||
$title
|
||||
);
|
||||
} else {
|
||||
$newTitle = $title . ' (2)';
|
||||
}
|
||||
return $this->generateFileName($folder, $newTitle, $suffix, $id);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSafeTitleFromContent(string $content) : string {
|
||||
// prepare content: remove markdown characters and empty spaces
|
||||
$content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item
|
||||
$content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline
|
||||
$content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline
|
||||
$content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis
|
||||
|
||||
// sanitize: prevent directory traversal, illegal characters and unintended file names
|
||||
$content = $this->sanitisePath($content);
|
||||
|
||||
// generate title from the first line of the content
|
||||
$splitContent = preg_split("/\R/u", $content, 2);
|
||||
$title = trim($splitContent[0]);
|
||||
|
||||
// using a maximum of 100 chars should be enough
|
||||
$title = mb_substr($title, 0, 100, "UTF-8");
|
||||
|
||||
// ensure that title is not empty
|
||||
if (empty($title)) {
|
||||
$title = $this->l10n->t('New note');
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
/** removes characters that are illegal in a file or folder name on some operating systems */
|
||||
public function sanitisePath(string $str) : string {
|
||||
// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
|
||||
// prevents also directory traversal by eliminiating slashes
|
||||
// see also \OC\Files\Storage\Common::verifyPosixPath(...)
|
||||
$str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
|
||||
|
||||
// if mysql doesn't support 4byte UTF-8, then remove those characters
|
||||
// see \OC\Files\Storage\Common::verifyPath(...)
|
||||
if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
|
||||
$str = preg_replace('%(?:
|
||||
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
|
||||
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
|
||||
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
|
||||
)%xs', '', $str);
|
||||
}
|
||||
|
||||
// prevent file to be hidden
|
||||
$str = preg_replace("/^[\. ]+/mu", "", $str);
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a folder and creates it if non-existent
|
||||
* @param string $path path to the folder
|
||||
* @return Folder
|
||||
*/
|
||||
public function getOrCreateFolder(string $path) : Folder {
|
||||
if ($this->root->nodeExists($path)) {
|
||||
$folder = $this->root->get($path);
|
||||
} else {
|
||||
$folder = $this->root->newFolder($path);
|
||||
}
|
||||
if (!($folder instanceof Folder)) {
|
||||
throw new NotesFolderException($path.' is not a folder');
|
||||
}
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete a folder and it's parent(s) if it's/they're empty
|
||||
* @param Folder root folder for notes
|
||||
* @param Folder folder to delete
|
||||
*/
|
||||
public function deleteEmptyFolder(Folder $notesFolder, Folder $folder) {
|
||||
$content = $folder->getDirectoryListing();
|
||||
$isEmpty = !count($content);
|
||||
$isNotesFolder = $folder->getPath()===$notesFolder->getPath();
|
||||
if ($isEmpty && !$isNotesFolder) {
|
||||
$this->logger->info('Deleting empty category folder '.$folder->getPath(), ['app' => $this->appName]);
|
||||
$parent = $folder->getParent();
|
||||
$folder->delete();
|
||||
$this->deleteEmptyFolder($notesFolder, $parent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
namespace OCA\Notes\Service;
|
||||
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\IL10N;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\ILogger;
|
||||
use OCP\Encryption\Exceptions\GenericEncryptionException;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\Folder;
|
||||
use OCA\Notes\Db\Note;
|
||||
use OCA\Notes\Service\SettingsService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserSession;
|
||||
|
||||
/**
|
||||
* Class NotesService
|
||||
|
@ -25,6 +25,7 @@ class NotesService {
|
|||
private $logger;
|
||||
private $config;
|
||||
private $settings;
|
||||
private $noteUtil;
|
||||
private $appName;
|
||||
|
||||
/**
|
||||
|
@ -32,15 +33,25 @@ class NotesService {
|
|||
* @param IL10N $l10n
|
||||
* @param ILogger $logger
|
||||
* @param IConfig $config
|
||||
* @param \OCA\Notes\Service\SettingsService $settings
|
||||
* @param SettingsService $settings
|
||||
* @param NoteUtil $noteUtil
|
||||
* @param String $appName
|
||||
*/
|
||||
public function __construct(IRootFolder $root, IL10N $l10n, ILogger $logger, IConfig $config, SettingsService $settings, $appName) {
|
||||
public function __construct(
|
||||
IRootFolder $root,
|
||||
IL10N $l10n,
|
||||
ILogger $logger,
|
||||
IConfig $config,
|
||||
SettingsService $settings,
|
||||
NoteUtil $noteUtil,
|
||||
$appName
|
||||
) {
|
||||
$this->root = $root;
|
||||
$this->l10n = $l10n;
|
||||
$this->logger = $logger;
|
||||
$this->config = $config;
|
||||
$this->settings = $settings;
|
||||
$this->noteUtil = $noteUtil;
|
||||
$this->appName = $appName;
|
||||
}
|
||||
|
||||
|
@ -51,7 +62,7 @@ class NotesService {
|
|||
*/
|
||||
public function getAll($userId, $onlyMeta = false) {
|
||||
$notesFolder = $this->getFolderForUser($userId);
|
||||
$notes = $this->gatherNoteFiles($notesFolder);
|
||||
$notes = $this->noteUtil->gatherNoteFiles($notesFolder);
|
||||
$filesById = [];
|
||||
foreach ($notes as $note) {
|
||||
$filesById[$note->getId()] = $note;
|
||||
|
@ -79,7 +90,7 @@ class NotesService {
|
|||
* @throws NoteDoesNotExistException if note does not exist
|
||||
* @return Note
|
||||
*/
|
||||
public function get($id, $userId) {
|
||||
public function get($id, $userId) : Note {
|
||||
$folder = $this->getFolderForUser($userId);
|
||||
return $this->getNote($this->getFileById($folder, $id), $folder, $this->getTags($id));
|
||||
}
|
||||
|
@ -94,14 +105,16 @@ class NotesService {
|
|||
return array_key_exists($id, $tags) ? $tags[$id] : [];
|
||||
}
|
||||
|
||||
private function getNote($file, $notesFolder, $tags = [], $onlyMeta = false) {
|
||||
private function getNote(File $file, Folder $notesFolder, $tags = [], $onlyMeta = false) : Note {
|
||||
$id = $file->getId();
|
||||
try {
|
||||
$note = Note::fromFile($file, $notesFolder, $tags, $onlyMeta);
|
||||
} catch (GenericEncryptionException $e) {
|
||||
$note = Note::fromException($this->l10n->t('Encryption Error').': ('.$file->getName().') '.$e->getMessage(), $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
|
||||
$message = $this->l10n->t('Encryption Error').': ('.$file->getName().') '.$e->getMessage();
|
||||
$note = Note::fromException($message, $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
|
||||
} catch (\Exception $e) {
|
||||
$note = Note::fromException($this->l10n->t('Error').': ('.$file->getName().') '.$e->getMessage(), $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
|
||||
$message = $this->l10n->t('Error').': ('.$file->getName().') '.$e->getMessage();
|
||||
$note = Note::fromException($message, $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []);
|
||||
}
|
||||
return $note;
|
||||
}
|
||||
|
@ -113,14 +126,14 @@ class NotesService {
|
|||
* @see update for setting note content
|
||||
* @return Note the newly created note
|
||||
*/
|
||||
public function create($userId) {
|
||||
public function create($userId) : Note {
|
||||
$title = $this->l10n->t('New note');
|
||||
$folder = $this->getFolderForUser($userId);
|
||||
|
||||
// check new note exists already and we need to number it
|
||||
// pass -1 because no file has id -1 and that will ensure
|
||||
// to only return filenames that dont yet exist
|
||||
$path = $this->generateFileName($folder, $title, $this->settings->get($userId, 'fileSuffix'), -1);
|
||||
$path = $this->noteUtil->generateFileName($folder, $title, $this->settings->get($userId, 'fileSuffix'), -1);
|
||||
$file = $folder->newFile($path);
|
||||
|
||||
// If server-side encryption is activated, the server creates an empty file without signature
|
||||
|
@ -143,50 +156,23 @@ class NotesService {
|
|||
* @throws NoteDoesNotExistException if note does not exist
|
||||
* @return \OCA\Notes\Db\Note the updated note
|
||||
*/
|
||||
public function update($id, $content, $userId, $category = null, $mtime = 0) {
|
||||
public function update($id, $content, $userId, $category = null, $mtime = 0) : Note {
|
||||
$notesFolder = $this->getFolderForUser($userId);
|
||||
$file = $this->getFileById($notesFolder, $id);
|
||||
$title = $this->getSafeTitleFromContent($content===null ? $file->getContent() : $content);
|
||||
|
||||
$title = $this->noteUtil->getSafeTitleFromContent($content===null ? $file->getContent() : $content);
|
||||
|
||||
// rename/move file with respect to title/category
|
||||
// this can fail if access rights are not sufficient or category name is illegal
|
||||
try {
|
||||
$currentFilePath = $this->root->getFullPath($file->getPath());
|
||||
$currentBasePath = pathinfo($currentFilePath, PATHINFO_DIRNAME);
|
||||
$fileSuffix = '.' . pathinfo($file->getName(), PATHINFO_EXTENSION);
|
||||
|
||||
// detect (new) folder path based on category name
|
||||
if ($category===null) {
|
||||
$basePath = $currentBasePath;
|
||||
} else {
|
||||
$basePath = $notesFolder->getPath();
|
||||
if (!empty($category)) {
|
||||
// sanitise path
|
||||
$cats = explode('/', $category);
|
||||
$cats = array_map([$this, 'sanitisePath'], $cats);
|
||||
$cats = array_filter($cats, function ($str) {
|
||||
return !empty($str);
|
||||
});
|
||||
$basePath .= '/'.implode('/', $cats);
|
||||
}
|
||||
}
|
||||
$folder = $this->getOrCreateFolder($basePath);
|
||||
|
||||
// assemble new file path
|
||||
$newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileSuffix, $id);
|
||||
|
||||
// if the current path is not the new path, the file has to be renamed
|
||||
if ($currentFilePath !== $newFilePath) {
|
||||
$file->move($newFilePath);
|
||||
}
|
||||
if ($currentBasePath !== $basePath) {
|
||||
$this->deleteEmptyFolder($notesFolder, $this->root->get($currentBasePath));
|
||||
}
|
||||
$this->noteUtil->moveNote($notesFolder, $file, $category, $title);
|
||||
} catch (\OCP\Files\NotPermittedException $e) {
|
||||
$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]);
|
||||
$err = 'Moving note '.$id.' ('.$title.') to the desired target is not allowed.'
|
||||
.' Please check the note\'s target category ('.$category.').';
|
||||
$this->logger->error($err, ['app' => $this->appName]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Moving note '.$id.' ('.$title.') to the desired target has failed with a '.get_class($e).': '.$e->getMessage(), ['app' => $this->appName]);
|
||||
$err = 'Moving note '.$id.' ('.$title.') to the desired target has failed '
|
||||
.'with a '.get_class($e).': '.$e->getMessage();
|
||||
$this->logger->error($err, ['app' => $this->appName]);
|
||||
}
|
||||
|
||||
if ($content !== null) {
|
||||
|
@ -209,10 +195,8 @@ class NotesService {
|
|||
*/
|
||||
public function favorite($id, $favorite, $userId) {
|
||||
$folder = $this->getFolderForUser($userId);
|
||||
$file = $this->getFileById($folder, $id);
|
||||
if (!$this->isNote($file)) {
|
||||
throw new NoteDoesNotExistException();
|
||||
}
|
||||
// check if file is note
|
||||
$this->getFileById($folder, $id);
|
||||
$tagger = \OC::$server->getTagManager()->load('files');
|
||||
if ($favorite) {
|
||||
$tagger->addToFavorites($id);
|
||||
|
@ -237,54 +221,7 @@ class NotesService {
|
|||
$file = $this->getFileById($notesFolder, $id);
|
||||
$parent = $file->getParent();
|
||||
$file->delete();
|
||||
$this->deleteEmptyFolder($notesFolder, $parent);
|
||||
}
|
||||
|
||||
// removes characters that are illegal in a file or folder name on some operating systems
|
||||
private function sanitisePath($str) {
|
||||
// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
|
||||
// prevents also directory traversal by eliminiating slashes
|
||||
// see also \OC\Files\Storage\Common::verifyPosixPath(...)
|
||||
$str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
|
||||
|
||||
// if mysql doesn't support 4byte UTF-8, then remove those characters
|
||||
// see \OC\Files\Storage\Common::verifyPath(...)
|
||||
if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
|
||||
$str = preg_replace('%(?:
|
||||
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
|
||||
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
|
||||
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
|
||||
)%xs', '', $str);
|
||||
}
|
||||
|
||||
// prevent file to be hidden
|
||||
$str = preg_replace("/^[\. ]+/mu", "", $str);
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
private function getSafeTitleFromContent($content) {
|
||||
// prepare content: remove markdown characters and empty spaces
|
||||
$content = preg_replace("/^\s*[*+-]\s+/mu", "", $content); // list item
|
||||
$content = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $content); // headline
|
||||
$content = preg_replace("/^(=+|-+)$/mu", "", $content); // separate line for headline
|
||||
$content = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $content); // emphasis
|
||||
|
||||
// sanitize: prevent directory traversal, illegal characters and unintended file names
|
||||
$content = $this->sanitisePath($content);
|
||||
|
||||
// generate title from the first line of the content
|
||||
$splitContent = preg_split("/\R/u", $content, 2);
|
||||
$title = trim($splitContent[0]);
|
||||
|
||||
// ensure that title is not empty
|
||||
if (empty($title)) {
|
||||
$title = $this->l10n->t('New note');
|
||||
}
|
||||
|
||||
// using a maximum of 100 chars should be enough
|
||||
$title = mb_substr($title, 0, 100, "UTF-8");
|
||||
|
||||
return $title;
|
||||
$this->noteUtil->deleteEmptyFolder($notesFolder, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,10 +230,10 @@ class NotesService {
|
|||
* @throws NoteDoesNotExistException
|
||||
* @return \OCP\Files\File
|
||||
*/
|
||||
private function getFileById($folder, $id) {
|
||||
private function getFileById(Folder $folder, $id) : File {
|
||||
$file = $folder->getById($id);
|
||||
|
||||
if (count($file) <= 0 || !$this->isNote($file[0])) {
|
||||
if (count($file) <= 0 || !($file[0] instanceof File) || !$this->noteUtil->isNote($file[0])) {
|
||||
throw new NoteDoesNotExistException();
|
||||
}
|
||||
return $file[0];
|
||||
|
@ -315,124 +252,14 @@ class NotesService {
|
|||
* @param string $userId the user id
|
||||
* @return Folder
|
||||
*/
|
||||
private function getFolderForUser($userId) {
|
||||
private function getFolderForUser($userId) : Folder {
|
||||
// TODO use IRootFolder->getUserFolder() ?
|
||||
$path = '/' . $userId . '/files/' . $this->settings->get($userId, 'notesPath');
|
||||
try {
|
||||
$folder = $this->getOrCreateFolder($path);
|
||||
$folder = $this->noteUtil->getOrCreateFolder($path);
|
||||
} catch (\Exception $e) {
|
||||
throw new NotesFolderException($path);
|
||||
}
|
||||
return $folder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds a folder and creates it if non-existent
|
||||
* @param string $path path to the folder
|
||||
* @return Folder
|
||||
*/
|
||||
private function getOrCreateFolder($path) {
|
||||
if ($this->root->nodeExists($path)) {
|
||||
$folder = $this->root->get($path);
|
||||
} else {
|
||||
$folder = $this->root->newFolder($path);
|
||||
}
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete a folder and it's parent(s) if it's/they're empty
|
||||
* @param Folder root folder for notes
|
||||
* @param Folder folder to delete
|
||||
*/
|
||||
private function deleteEmptyFolder(Folder $notesFolder, Folder $folder) {
|
||||
$content = $folder->getDirectoryListing();
|
||||
$isEmpty = !count($content);
|
||||
$isNotesFolder = $folder->getPath()===$notesFolder->getPath();
|
||||
if ($isEmpty && !$isNotesFolder) {
|
||||
$this->logger->info('Deleting empty category folder '.$folder->getPath(), ['app' => $this->appName]);
|
||||
$parent = $folder->getParent();
|
||||
$folder->delete();
|
||||
$this->deleteEmptyFolder($notesFolder, $parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get path of file and the title.txt and check if they are the same
|
||||
* file. If not the title needs to be renamed
|
||||
*
|
||||
* @param Folder $folder a folder to the notes directory
|
||||
* @param string $title the filename which should be used
|
||||
* @param string $suffix the suffix (incl. dot) which should be used
|
||||
* @param int $id the id of the note for which the title should be generated
|
||||
* used to see if the file itself has the title and not a different file for
|
||||
* checking for filename collisions
|
||||
* @return string the resolved filename to prevent overwriting different
|
||||
* files with the same title
|
||||
*/
|
||||
private function generateFileName(Folder $folder, $title, $suffix, $id) {
|
||||
$path = $title . $suffix;
|
||||
|
||||
// if file does not exist, that name has not been taken. Similar we don't
|
||||
// need to handle file collisions if it is the filename did not change
|
||||
if (!$folder->nodeExists($path) || $folder->get($path)->getId() === $id) {
|
||||
return $path;
|
||||
} else {
|
||||
// increments name (2) to name (3)
|
||||
$match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
|
||||
if ($match) {
|
||||
$newId = ((int) $matches['id']) + 1;
|
||||
$newTitle = preg_replace(
|
||||
'/(.*)\s\((\d+)\)$/u',
|
||||
'$1 (' . $newId . ')',
|
||||
$title
|
||||
);
|
||||
} else {
|
||||
$newTitle = $title . ' (2)';
|
||||
}
|
||||
return $this->generateFileName($folder, $newTitle, $suffix, $id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gather note files in given directory and all subdirectories
|
||||
* @param Folder $folder
|
||||
* @return array
|
||||
*/
|
||||
private function gatherNoteFiles($folder) {
|
||||
$notes = [];
|
||||
$nodes = $folder->getDirectoryListing();
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->getType() === FileInfo::TYPE_FOLDER) {
|
||||
$notes = array_merge($notes, $this->gatherNoteFiles($node));
|
||||
continue;
|
||||
}
|
||||
if ($this->isNote($node)) {
|
||||
$notes[] = $node;
|
||||
}
|
||||
}
|
||||
return $notes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* test if file is a note
|
||||
*
|
||||
* @param \OCP\Files\File $file
|
||||
* @return bool
|
||||
*/
|
||||
private function isNote($file) {
|
||||
$allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note'];
|
||||
|
||||
if ($file->getType() !== 'file') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ext = pathinfo($file->getName(), PATHINFO_EXTENSION);
|
||||
$iext = strtolower($ext);
|
||||
if (!in_array($iext, $allowedExtensions)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue