diff --git a/Makefile b/Makefile index d25b3c49e..a4180ac48 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ appstore: clean lint build-js-production ### from vueexample -all: dev-setup lint build-js-production test +all: dev-setup build-js-production # Dev env management dev-setup: clean clean-dev init diff --git a/appinfo/app.php b/appinfo/app.php index 4477c5e64..8344e61a0 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -1,29 +1,4 @@ - * @copyright Bernhard Posselt 2012, 2014 - */ -namespace OCA\Notes\AppInfo; - -use OCP\AppFramework\App; - -$app = new App('notes'); -$container = $app->getContainer(); - -$container->query('OCP\INavigationManager')->add(function () use ($container) { - $urlGenerator = $container->query('OCP\IURLGenerator'); - $l10n = $container->query('OCP\IL10N'); - return [ - 'id' => 'notes', - 'order' => 10, - 'href' => $urlGenerator->linkToRoute('notes.page.index'), - 'icon' => $urlGenerator->imagePath('notes', 'notes.svg'), - 'name' => $l10n->t('Notes') - ]; -}); +$app = new OCA\Notes\Application(); +$app->register(); diff --git a/appinfo/routes.php b/appinfo/routes.php index e38f5994c..1c43732bc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -1,16 +1,6 @@ - * @copyright Bernhard Posselt 2012, 2014 - */ - return ['routes' => [ - // page + ////////// P A G E ////////// [ 'name' => 'page#index', 'url' => '/', @@ -30,7 +20,8 @@ 'requirements' => ['id' => '\d+'], ], - // notes + + ////////// N O T E S ////////// [ 'name' => 'notes#index', 'url' => '/notes', @@ -59,16 +50,13 @@ 'requirements' => ['id' => '\d+'], ], [ - 'name' => 'notes#category', - 'url' => '/notes/{id}/category', + 'name' => 'notes#updateProperty', + 'url' => '/notes/{id}/{property}', 'verb' => 'PUT', - 'requirements' => ['id' => '\d+'], - ], - [ - 'name' => 'notes#favorite', - 'url' => '/notes/{id}/favorite', - 'verb' => 'PUT', - 'requirements' => ['id' => '\d+'], + 'requirements' => [ + 'id' => '\d+', + 'property' => '(modified|title|category|favorite)', + ], ], [ 'name' => 'notes#destroy', @@ -77,43 +65,88 @@ 'requirements' => ['id' => '\d+'], ], - // api + + ////////// S E T T I N G S ////////// + ['name' => 'settings#set', 'url' => '/settings', 'verb' => 'PUT'], + ['name' => 'settings#get', 'url' => '/settings', 'verb' => 'GET'], + + + ////////// A P I ////////// [ 'name' => 'notes_api#index', - 'url' => '/api/v0.2/notes', + 'url' => '/api/{apiVersion}/notes', 'verb' => 'GET', + 'requirements' => [ + 'apiVersion' => '(v0.2|v1)', + ], ], [ 'name' => 'notes_api#get', - 'url' => '/api/v0.2/notes/{id}', + 'url' => '/api/{apiVersion}/notes/{id}', 'verb' => 'GET', - 'requirements' => ['id' => '\d+'], + 'requirements' => [ + 'apiVersion' => '(v0.2|v1)', + 'id' => '\d+', + ], + ], + [ + 'name' => 'notes_api#createAutoTitle', + 'url' => '/api/{apiVersion}/notes', + 'verb' => 'POST', + 'requirements' => [ + 'apiVersion' => '(v0.2)', + ], ], [ 'name' => 'notes_api#create', - 'url' => '/api/v0.2/notes', + 'url' => '/api/{apiVersion}/notes', 'verb' => 'POST', + 'requirements' => [ + 'apiVersion' => '(v1)', + ], + ], + [ + 'name' => 'notes_api#updateAutoTitle', + 'url' => '/api/{apiVersion}/notes/{id}', + 'verb' => 'PUT', + 'requirements' => [ + 'apiVersion' => '(v0.2)', + 'id' => '\d+', + ], ], [ 'name' => 'notes_api#update', - 'url' => '/api/v0.2/notes/{id}', + 'url' => '/api/{apiVersion}/notes/{id}', 'verb' => 'PUT', - 'requirements' => ['id' => '\d+'], + 'requirements' => [ + 'apiVersion' => '(v1)', + 'id' => '\d+', + ], ], [ 'name' => 'notes_api#destroy', - 'url' => '/api/v0.2/notes/{id}', + 'url' => '/api/{apiVersion}/notes/{id}', 'verb' => 'DELETE', - 'requirements' => ['id' => '\d+'], + 'requirements' => [ + 'apiVersion' => '(v0.2|v1)', + 'id' => '\d+', + ], + ], + [ + 'name' => 'notes_api#fail', + 'url' => '/api/{catchAll}', + 'verb' => 'GET', + 'requirements' => [ + 'catchAll' => '.*', + ], ], [ 'name' => 'notes_api#preflighted_cors', - 'url' => '/api/v0.2/{path}', + 'url' => '/api/{apiVersion}/{path}', 'verb' => 'OPTIONS', - 'requirements' => ['path' => '.+'], + 'requirements' => [ + 'apiVersion' => '(v0.2|v1)', + 'path' => '.+', + ], ], - - // settings - ['name' => 'settings#set', 'url' => '/settings', 'verb' => 'PUT'], - ['name' => 'settings#get', 'url' => '/settings', 'verb' => 'GET'], ]]; diff --git a/lib/Application.php b/lib/Application.php new file mode 100644 index 000000000..a92bf5f7b --- /dev/null +++ b/lib/Application.php @@ -0,0 +1,31 @@ +getContainer(); + $container->registerCapability(Capabilities::class); + $server = $container->getServer(); + $server->getNavigationManager()->add(function () use ($server) { + $urlGenerator = $server->getURLGenerator(); + $l10n = $server->getL10N('notes'); + return [ + 'id' => 'notes', + 'order' => 10, + 'href' => $urlGenerator->linkToRoute('notes.page.index'), + 'icon' => $urlGenerator->imagePath('notes', 'notes.svg'), + 'name' => $l10n->t('Notes'), + ]; + }); + } +} diff --git a/lib/Capabilities.php b/lib/Capabilities.php new file mode 100644 index 000000000..8f4a0010f --- /dev/null +++ b/lib/Capabilities.php @@ -0,0 +1,16 @@ + [ + 'api_version' => Application::$API_VERSIONS, + ], + ]; + } +} diff --git a/lib/Controller/Errors.php b/lib/Controller/Errors.php deleted file mode 100644 index 138f05d82..000000000 --- a/lib/Controller/Errors.php +++ /dev/null @@ -1,27 +0,0 @@ -logger = $logger; + $this->appName = $appName; + } + + public function handleErrorResponse(callable $respond) : DataResponse { + try { + $data = $respond(); + $response = $data instanceof DataResponse ? $data : new DataResponse($data); + } catch (NoteDoesNotExistException $e) { + $this->logger->logException($e, [ 'app' => $this->appName ]); + $response = new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (InsufficientStorageException $e) { + $this->logger->logException($e, [ 'app' => $this->appName ]); + $response = new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE); + } catch (\Throwable $e) { + $this->logger->logException($e, [ 'app' => $this->appName ]); + $response = new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + $response->addHeader('X-Notes-API-Versions', implode(', ', Application::$API_VERSIONS)); + return $response; + } +} diff --git a/lib/Controller/NotesApiController.php b/lib/Controller/NotesApiController.php index 52b553e28..f8e62ef30 100644 --- a/lib/Controller/NotesApiController.php +++ b/lib/Controller/NotesApiController.php @@ -1,106 +1,74 @@ -service = $service; $this->metaService = $metaService; + $this->helper = $helper; $this->userSession = $userSession; } - private function getUID() { + private function getUID() : string { return $this->userSession->getUser()->getUID(); } - /** - * @param Note $note - * @param string[] $exclude the fields that should be removed from the - * notes - * @return Note - */ - private function excludeFields(Note &$note, array $exclude) { - if (count($exclude) > 0) { - foreach ($exclude as $field) { - if (property_exists($note, $field)) { - unset($note->$field); - } - } - } - return $note; - } - /** * @NoAdminRequired * @CORS * @NoCSRFRequired - * - * @param string $exclude - * @return DataResponse */ - public function index($exclude = '', $pruneBefore = 0) { - $exclude = explode(',', $exclude); - $now = new \DateTime(); // this must be before loading notes if there are concurrent changes possible - $notes = $this->service->getAll($this->getUID()); - $metas = $this->metaService->updateAll($this->getUID(), $notes); - foreach ($notes as $note) { - $lastUpdate = $metas[$note->getId()]->getLastUpdate(); - if ($pruneBefore && $lastUpdate<$pruneBefore) { - $vars = get_object_vars($note); - unset($vars['id']); - $this->excludeFields($note, array_keys($vars)); - } else { - $this->excludeFields($note, $exclude); + public function index(string $exclude = '', int $pruneBefore = 0) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($exclude, $pruneBefore) { + $exclude = explode(',', $exclude); + $now = new \DateTime(); // this must be before loading notes if there are concurrent changes possible + $notes = $this->service->getAll($this->getUID()); + $metas = $this->metaService->updateAll($this->getUID(), $notes); + $notesData = array_map(function ($note) use ($metas, $pruneBefore, $exclude) { + $lastUpdate = $metas[$note->getId()]->getLastUpdate(); + if ($pruneBefore && $lastUpdate<$pruneBefore) { + return [ 'id' => $note->getId() ]; + } else { + return $note->getData($exclude); + } + }, $notes); + $etag = md5(json_encode($notesData)); + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return new DataResponse([], Http::STATUS_NOT_MODIFIED); } - } - $etag = md5(json_encode($notes)); - if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { - return new DataResponse([], Http::STATUS_NOT_MODIFIED); - } - return (new DataResponse($notes)) - ->setLastModified($now) - ->setETag($etag); + return (new DataResponse($notesData)) + ->setLastModified($now) + ->setETag($etag); + }); } @@ -108,20 +76,13 @@ public function index($exclude = '', $pruneBefore = 0) { * @NoAdminRequired * @CORS * @NoCSRFRequired - * - * @param int $id - * @param string $exclude - * @return DataResponse */ - public function get($id, $exclude = '') { - try { + public function get(int $id, string $exclude = '') : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id, $exclude) { $exclude = explode(',', $exclude); - $note = $this->service->get($id, $this->getUID()); - $note = $this->excludeFields($note, $exclude); - return new DataResponse($note); - } catch (NoteDoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); - } + $note = $this->service->get($this->getUID(), $id); + return $note->getData($exclude); + }); } @@ -129,86 +90,134 @@ public function get($id, $exclude = '') { * @NoAdminRequired * @CORS * @NoCSRFRequired - * - * @param string $content - * @param string $category - * @param int $modified - * @param boolean $favorite - * @return DataResponse */ - public function create($content, $category = null, $modified = 0, $favorite = null) { - try { - $note = $this->service->create($this->getUID()); + public function create( + string $category = '', + string $title = '', + string $content = '', + int $modified = 0, + bool $favorite = false + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($category, $title, $content, $modified, $favorite) { + $note = $this->service->create($this->getUID(), $title, $category); try { - $note = $this->updateData($note->getId(), $content, $category, $modified, $favorite); + $note->setContent($content); + if ($modified) { + $note->setModified($modified); + } + if ($favorite) { + $note->setFavorite($favorite); + } } catch (\Throwable $e) { // roll-back note creation - $this->service->delete($note->getId(), $this->getUID()); + $this->service->delete($this->getUID(), $note->getId()); throw $e; } - return new DataResponse($note); - } catch (InsufficientStorageException $e) { - return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE); - } + return $note->getData(); + }); } + /** + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + * @deprecated this was used in API v0.2 only, use #create() instead + */ + public function createAutoTitle( + string $category = '', + string $content = '', + int $modified = 0, + bool $favorite = false + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($category, $content, $modified, $favorite) { + $title = $this->service->getTitleFromContent($content); + return $this->create($category, $title, $content, $modified, $favorite); + }); + } /** * @NoAdminRequired * @CORS * @NoCSRFRequired - * - * @param int $id - * @param string $content - * @param string $category - * @param int $modified - * @param boolean $favorite - * @return DataResponse */ - public function update($id, $content = null, $category = null, $modified = 0, $favorite = null) { - try { - $note = $this->updateData($id, $content, $category, $modified, $favorite); - return new DataResponse($note); - } catch (NoteDoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); - } catch (InsufficientStorageException $e) { - return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE); - } + public function update( + int $id, + ?string $content = null, + ?int $modified = null, + ?string $title = null, + ?string $category = null, + ?bool $favorite = null + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ( + $id, + $content, + $modified, + $title, + $category, + $favorite + ) { + $note = $this->service->get($this->getUID(), $id); + if ($content !== null) { + $note->setContent($content); + } + if ($modified !== null) { + $note->setModified($modified); + } + if ($title !== null) { + $note->setTitleCategory($title, $category); + } elseif ($category !== null) { + $note->setCategory($category); + } + if ($favorite !== null) { + $note->setFavorite($favorite); + } + return $note->getData(); + }); } /** - * Updates a note, used by create and update - * @param int $id - * @param string|null $content - * @param int $modified - * @param boolean|null $favorite - * @return Note + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + * @deprecated this was used in API v0.2 only, use #update() instead */ - private function updateData($id, $content, $category, $modified, $favorite) { - if ($favorite!==null) { - $this->service->favorite($id, $favorite, $this->getUID()); - } - if ($content===null) { - return $this->service->get($id, $this->getUID()); - } else { - return $this->service->update($id, $content, $this->getUID(), $category, $modified); - } + public function updateAutoTitle( + int $id, + ?string $content = null, + ?int $modified = null, + ?string $category = null, + ?bool $favorite = null + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id, $content, $modified, $category, $favorite) { + if ($content === null) { + $note = $this->service->get($this->getUID(), $id); + $title = $this->service->getTitleFromContent($note->getContent()); + } else { + $title = $this->service->getTitleFromContent($content); + } + return $this->update($id, $content, $modified, $title, $category, $favorite); + }); } /** * @NoAdminRequired * @CORS * @NoCSRFRequired - * - * @param int $id - * @return DataResponse */ - public function destroy($id) { - try { - $this->service->delete($id, $this->getUID()); - return new DataResponse([]); - } catch (NoteDoesNotExistException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); - } + public function destroy(int $id) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id) { + $this->service->delete($this->getUID(), $id); + return []; + }); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function fail() : DataResponse { + return $this->helper->handleErrorResponse(function () { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + }); } } diff --git a/lib/Controller/NotesController.php b/lib/Controller/NotesController.php index e7d9780fc..c2be6e733 100644 --- a/lib/Controller/NotesController.php +++ b/lib/Controller/NotesController.php @@ -1,7 +1,10 @@ -notesService = $notesService; $this->settingsService = $settingsService; + $this->helper = $helper; $this->settings = $settings; $this->userId = $UserId; $this->l10n = $l10n; @@ -63,163 +50,199 @@ public function __construct( /** * @NoAdminRequired */ - public function index() { - $settings = $this->settingsService->getAll($this->userId); - - $errorMessage = null; - $lastViewedNote = (int) $this->settings->getUserValue( - $this->userId, - $this->appName, - 'notesLastViewedNote' - ); - // check if notes folder is accessible - $notes = null; - try { - $notes = $this->notesService->getAll($this->userId, true); - if ($lastViewedNote) { - // check if note exists - try { - $this->notesService->get($lastViewedNote, $this->userId); - } catch (\Exception $ex) { - $this->settings->deleteUserValue($this->userId, $this->appName, 'notesLastViewedNote'); - $lastViewedNote = 0; - $errorMessage = $this->l10n->t('The last viewed note cannot be accessed. ').$ex->getMessage(); + public function index() : DataResponse { + return $this->helper->handleErrorResponse(function () { + $settings = $this->settingsService->getAll($this->userId); + + $errorMessage = null; + $lastViewedNote = (int) $this->settings->getUserValue( + $this->userId, + $this->appName, + 'notesLastViewedNote' + ); + // check if notes folder is accessible + $notes = null; + try { + $notes = $this->notesService->getAll($this->userId); + $notesData = array_map(function ($note) { + return $note->getData([ 'content' ]); + }, $notes); + if ($lastViewedNote) { + // check if note exists + try { + $this->notesService->get($this->userId, $lastViewedNote); + } catch (\Exception $ex) { + $this->settings->deleteUserValue($this->userId, $this->appName, 'notesLastViewedNote'); + $lastViewedNote = 0; + $errorMessage = $this->l10n->t('The last viewed note cannot be accessed. ').$ex->getMessage(); + } } + } catch (\Exception $e) { + $errorMessage = $this->l10n->t('The notes folder is not accessible: %s', $e->getMessage()); } - } catch (\Exception $e) { - $errorMessage = $this->l10n->t('The notes folder is not accessible: %s', $e->getMessage()); - } - - return new DataResponse([ - 'notes' => $notes, - 'settings' => $settings, - 'lastViewedNote' => $lastViewedNote, - 'errorMessage' => $errorMessage, - ]); - } - - /** - * @NoAdminRequired - * - * @param int $id - * @return DataResponse - */ - public function get($id) { - // save the last viewed note - $this->settings->setUserValue( - $this->userId, - $this->appName, - 'notesLastViewedNote', - strval($id) - ); - - $note = $this->notesService->get($id, $this->userId); - return new DataResponse($note); + return [ + 'notes' => $notesData, + 'settings' => $settings, + 'lastViewedNote' => $lastViewedNote, + 'errorMessage' => $errorMessage, + ]; + }); } /** * @NoAdminRequired - * - * @param string $content */ - public function create($content = '', $category = null) { - try { - $note = $this->notesService->create($this->userId); - $note = $this->notesService->update( - $note->getId(), - $content, + public function get(int $id) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id) { + $note = $this->notesService->get($this->userId, $id); + + // save the last viewed note + $this->settings->setUserValue( $this->userId, - $category + $this->appName, + 'notesLastViewedNote', + strval($id) ); - return new DataResponse($note); - } catch (InsufficientStorageException $e) { - return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE); - } + + return $note->getData(); + }); } /** * @NoAdminRequired - * - * @param string $content */ - public function undo($id, $content, $category, $modified, $favorite) { - try { - // check if note still exists - $note = $this->notesService->get($id, $this->userId); - if ($note->getError()) { - throw new \Exception(); - } - } catch (\Throwable $e) { - // re-create if note doesn't exit anymore - $note = $this->notesService->create($this->userId); - $note = $this->notesService->update( - $note->getId(), - $content, - $this->userId, - $category, - $modified - ); - $note->favorite = $this->notesService->favorite($note->getId(), $favorite, $this->userId); - } - return new DataResponse($note); + public function create(string $category) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($category) { + $note = $this->notesService->create($this->userId, '', $category); + $note->setContent(''); + return $note->getData(); + }); } /** * @NoAdminRequired - * - * @param int $id - * @param string $content - * @return DataResponse */ - public function update($id, $content) { - try { - $note = $this->notesService->update($id, $content, $this->userId); - return new DataResponse($note); - } catch (InsufficientStorageException $e) { - return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE); - } + public function undo( + int $id, + string $title, + string $content, + string $category, + int $modified, + bool $favorite + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ( + $id, + $title, + $content, + $category, + $modified, + $favorite + ) { + try { + // check if note still exists + $note = $this->notesService->get($this->userId, $id); + $noteData = $note->getData(); + if ($noteData['error']) { + throw new \Exception(); + } + return $noteData; + } catch (\Throwable $e) { + // re-create if note doesn't exit anymore + $note = $this->notesService->create($this->userId, $title, $category); + $note->setContent($content); + $note->setModified($modified); + $note->setFavorite($favorite); + return $note->getData(); + } + }); } - /** * @NoAdminRequired - * - * @param int $id - * @param string $category - * @return DataResponse */ - public function category($id, $category) { - $note = $this->notesService->update($id, null, $this->userId, $category); - return new DataResponse($note->category); + public function update(int $id, string $content, bool $autotitle) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id, $content, $autotitle) { + $note = $this->notesService->get($this->userId, $id); + $note->setContent($content); + if ($autotitle) { + $title = $this->notesService->getTitleFromContent($content); + $note->setTitle($title); + } + return $note->getData(); + }); } /** * @NoAdminRequired - * - * @param int $id - * @param boolean $favorite - * @return DataResponse */ - public function favorite($id, $favorite) { - $result = $this->notesService->favorite($id, $favorite, $this->userId); - return new DataResponse($result); // @phan-suppress-current-line PhanTypeMismatchArgument + public function updateProperty( + int $id, + string $property, + ?int $modified = null, + ?string $title = null, + ?string $category = null, + ?bool $favorite = null + ) : DataResponse { + return $this->helper->handleErrorResponse(function () use ( + $id, + $property, + $modified, + $title, + $category, + $favorite + ) { + $note = $this->notesService->get($this->userId, $id); + $result = null; + switch ($property) { + case 'modified': + if ($modified !== null) { + $note->setModified($modified); + } + $result = $note->getModified(); + break; + + case 'title': + if ($title !== null) { + $note->setTitle($title); + } + $result = $note->getTitle(); + break; + + case 'category': + if ($category !== null) { + $note->setCategory($category); + } + $result = $note->getCategory(); + break; + + case 'favorite': + if ($favorite !== null) { + $note->setFavorite($favorite); + } + $result = $note->getFavorite(); + break; + + default: + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + return $result; + }); } /** * @NoAdminRequired - * - * @param int $id - * @return DataResponse */ - public function destroy($id) { - $this->notesService->delete($id, $this->userId); - return new DataResponse([]); + public function destroy(int $id) : DataResponse { + return $this->helper->handleErrorResponse(function () use ($id) { + $this->notesService->delete($this->userId, $id); + return []; + }); } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index e595116e2..03fe7896f 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -1,4 +1,4 @@ -appName, diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 19b00b796..681e77630 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -1,12 +1,13 @@ -addType('modified', 'integer'); - $this->addType('favorite', 'boolean'); - } - - /** - * @param File $file - * @return static - */ - public static function fromFile(File $file, Folder $notesFolder, $tags = [], $onlyMeta = false) { - $note = new static(); - $note->initCommonBaseFields($file, $notesFolder, $tags); - if (!$onlyMeta) { - $fileContent=$file->getContent(); - $note->setContent(self::convertEncoding($fileContent)); - } - if (!$onlyMeta) { - $note->updateETag(); - } - $note->resetUpdatedFields(); - return $note; - } - - /** - * @param File $file - * @return static - */ - public static function fromException($message, File $file, Folder $notesFolder, $tags = []) { - $note = new static(); - $note->initCommonBaseFields($file, $notesFolder, $tags); - $note->setErrorMessage($message); - $note->setError(true); - $note->setContent($message); - $note->resetUpdatedFields(); - return $note; - } - - private static function convertEncoding($str) { - if (!mb_check_encoding($str, 'UTF-8')) { - $str = mb_convert_encoding($str, 'UTF-8'); - } - return $str; - } - - // TODO NC19: replace this by OCP\ITags::TAG_FAVORITE - // OCP\ITags::TAG_FAVORITE was introduced in NC19 - // https://github.com/nextcloud/server/pull/19412 - /** - * @suppress PhanUndeclaredClassConstant - * @suppress PhanUndeclaredConstant - * @suppress PhanUndeclaredConstantOfClass - */ - private static function getTagFavorite() { - if (defined('OCP\ITags::TAG_FAVORITE')) { - return \OCP\ITags::TAG_FAVORITE; - } else { - return \OC\Tags::TAG_FAVORITE; - } - } - - private function initCommonBaseFields(File $file, Folder $notesFolder, $tags) { - $this->setId($file->getId()); - $this->setTitle(pathinfo($file->getName(), PATHINFO_FILENAME)); // remove extension - $this->setModified($file->getMTime()); - $subdir = substr(dirname($file->getPath()), strlen($notesFolder->getPath())+1); - $this->setCategory($subdir ? $subdir : ''); - if (is_array($tags) && in_array(self::getTagFavorite(), $tags)) { - $this->setFavorite(true); - } - } - - private function updateETag() { - // collect all relevant attributes - $data = ''; - foreach (get_object_vars($this) as $key => $val) { - if ($key!=='etag') { - $data .= $val; - } - } - $etag = md5($data); - $this->setEtag($etag); - } -} diff --git a/lib/Service/InsufficientStorageException.php b/lib/Service/InsufficientStorageException.php index d62cd6a31..78821d3b5 100644 --- a/lib/Service/InsufficientStorageException.php +++ b/lib/Service/InsufficientStorageException.php @@ -1,4 +1,4 @@ -metaMapper = $metaMapper; } - public function updateAll($userId, Array $notes) { + public function updateAll(string $userId, array $notes) : array { $metas = $this->metaMapper->getAll($userId); $metas = $this->getIndexedArray($metas, 'fileId'); $notes = $this->getIndexedArray($notes, 'id'); @@ -42,7 +37,7 @@ public function updateAll($userId, Array $notes) { return $metas; } - private function getIndexedArray(array $data, $property) { + private function getIndexedArray(array $data, string $property) : array { $property = ucfirst($property); $getter = 'get'.$property; $result = array(); @@ -52,13 +47,13 @@ private function getIndexedArray(array $data, $property) { return $result; } - private function create($userId, $note) { + private function create(string $userId, Note $note) : Meta { $meta = Meta::fromNote($note, $userId); $this->metaMapper->insert($meta); return $meta; } - private function updateIfNeeded(&$meta, $note) { + private function updateIfNeeded(Meta &$meta, Note $note) : void { if ($note->getEtag()!==$meta->getEtag()) { $meta->setEtag($note->getEtag()); $meta->setLastUpdate(time()); diff --git a/lib/Service/Note.php b/lib/Service/Note.php new file mode 100644 index 000000000..1fa3192c1 --- /dev/null +++ b/lib/Service/Note.php @@ -0,0 +1,141 @@ +file = $file; + $this->notesFolder = $notesFolder; + $this->noteUtil = $noteUtil; + } + + + public function getId() : int { + return $this->file->getId(); + } + + public function getTitle() : string { + return pathinfo($this->file->getName(), PATHINFO_FILENAME); + } + + public function getCategory() : string { + $subdir = substr( + dirname($this->file->getPath()), + strlen($this->notesFolder->getPath()) + 1 + ); + return $subdir === false ? '' : $subdir; + } + + public function getContent() : string { + $content = $this->file->getContent(); + if (!mb_check_encoding($content, 'UTF-8')) { + $content = mb_convert_encoding($content, 'UTF-8'); + } + return $content; + } + + public function getModified() : int { + return $this->file->getMTime(); + } + + public function getFavorite() : bool { + return $this->noteUtil->getTagService()->isFavorite($this->getId()); + } + + + public function getData(array $exclude = []) : array { + $data = []; + if (!in_array('id', $exclude)) { + $data['id'] = $this->getId(); + } + if (!in_array('title', $exclude)) { + $data['title'] = $this->getTitle(); + } + if (!in_array('modified', $exclude)) { + $data['modified'] = $this->getModified(); + } + if (!in_array('category', $exclude)) { + $data['category'] = $this->getCategory(); + } + if (!in_array('favorite', $exclude)) { + $data['favorite'] = $this->getFavorite(); + } + $data['error'] = false; + $data['errorMessage'] = ''; + if (!in_array('content', $exclude)) { + try { + $data['content'] = $this->getContent(); + } catch (\Throwable $e) { + $message = $this->noteUtil->getL10N()->t('Error').': ('.$this->file->getName().') '.$e->getMessage(); + $data['content'] = $message; + $data['error'] = true; + $data['errorMessage'] = $message; + } + } + return $data; + } + + public function getEtag() : string { + $data = $this->getData(); + // collect all relevant attributes + $str = ''; + foreach ($data as $key => $val) { + $str .= $val; + } + return md5($str); + } + + + public function setTitle(string $title) : void { + $this->setTitleCategory($title); + } + + public function setCategory(string $category) : void { + $this->setTitleCategory($this->getTitle(), $category); + } + + /** + * @throws \OCP\Files\NotPermittedException + */ + public function setTitleCategory(string $title, ?string $category = null) : void { + if ($category===null) { + $category = $this->getCategory(); + } + $oldParent = $this->file->getParent(); + $currentFilePath = $this->noteUtil->getRoot()->getFullPath($this->file->getPath()); + $fileSuffix = '.' . pathinfo($this->file->getName(), PATHINFO_EXTENSION); + + $folder = $this->noteUtil->getCategoryFolder($this->notesFolder, $category); + $filename = $this->noteUtil->generateFileName($folder, $title, $fileSuffix, $this->getId()); + $newFilePath = $folder->getPath() . '/' . $filename; + + // if the current path is not the new path, the file has to be renamed + if ($currentFilePath !== $newFilePath) { + $this->file->move($newFilePath); + } + $this->noteUtil->deleteEmptyFolder($oldParent, $this->notesFolder); + } + + public function setContent(string $content) : void { + $this->noteUtil->ensureSufficientStorage($this->file->getParent(), strlen($content)); + $this->file->putContent($content); + } + + public function setModified(int $modified) : void { + $this->file->touch($modified); + } + + public function setFavorite(bool $favorite) : void { + if ($favorite !== $this->getFavorite()) { + $this->noteUtil->getTagService()->setFavorite($this->getId(), $favorite); + } + } +} diff --git a/lib/Service/NoteDoesNotExistException.php b/lib/Service/NoteDoesNotExistException.php index fc904e5ab..25362ac0b 100644 --- a/lib/Service/NoteDoesNotExistException.php +++ b/lib/Service/NoteDoesNotExistException.php @@ -1,13 +1,8 @@ -db = $db; $this->root = $root; + $this->db = $db; + $this->tagService = $tagService; $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 && $node instanceof Folder) { - $notes = array_merge($notes, $this->gatherNoteFiles($node)); - continue; - } - if ($this->isNote($node)) { - $notes[] = $node; - } - } - return $notes; + public function getRoot() : IRootFolder { + return $this->root; } - - /** - * test if file is a note - */ - public function isNote(FileInfo $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 getTagService() : TagService { + return $this->tagService; } - public function moveNote(Folder $notesFolder, File $file, $category, string $title) : void { - $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); + public function getL10N() : IL10N { + return $this->l10n; + } - // assemble new file path - $newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileSuffix, $id); + public function getLogger() : ILogger { + return $this->logger; + } - // if the current path is not the new path, the file has to be renamed - if ($currentFilePath !== $newFilePath) { - $file->move($newFilePath); - } - if ($currentBasePath !== $basePath) { - $fileBasePath = $this->root->get($currentBasePath); - if ($fileBasePath instanceof Folder) { - $this->deleteEmptyFolder($notesFolder, $fileBasePath); - } - } + public function getCategoryFolder(Folder $notesFolder, string $category) { + $path = $notesFolder->getPath(); + // sanitise path + $cats = explode('/', $category); + $cats = array_map([$this, 'sanitisePath'], $cats); + $cats = array_filter($cats, function ($str) { + return $str !== ''; + }); + $path .= '/'.implode('/', $cats); + return $this->getOrCreateFolder($path); } /** @@ -119,12 +76,13 @@ public function moveNote(Folder $notesFolder, File $file, $category, string $tit * files with the same title */ public function generateFileName(Folder $folder, string $title, string $suffix, int $id) : string { - $path = $title . $suffix; + $title = $this->getSafeTitle($title); + $filename = $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; + if (!$folder->nodeExists($filename) || $folder->get($filename)->getId() === $id) { + return $filename; } else { // increments name (2) to name (3) $match = preg_match('/\((?P\d+)\)$/u', $title, $matches); @@ -142,13 +100,7 @@ public function generateFileName(Folder $folder, string $title, string $suffix, } } - 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 - + public function getSafeTitle(string $content) : string { // sanitize: prevent directory traversal, illegal characters and unintended file names $content = $this->sanitisePath($content); @@ -168,7 +120,7 @@ public function getSafeTitleFromContent(string $content) : string { } /** removes characters that are illegal in a file or folder name on some operating systems */ - public function sanitisePath(string $str) : string { + private 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(...) @@ -208,10 +160,10 @@ public function getOrCreateFolder(string $path) : Folder { /* * Delete a folder and it's parent(s) if it's/they're empty - * @param Folder $notesFolder root folder for notes * @param Folder $folder folder to delete + * @param Folder $notesFolder root notes folder */ - public function deleteEmptyFolder(Folder $notesFolder, Folder $folder) : void { + public function deleteEmptyFolder(Folder $folder, Folder $notesFolder) : void { $content = $folder->getDirectoryListing(); $isEmpty = !count($content); $isNotesFolder = $folder->getPath()===$notesFolder->getPath(); @@ -219,7 +171,7 @@ public function deleteEmptyFolder(Folder $notesFolder, Folder $folder) : void { $this->logger->info('Deleting empty category folder '.$folder->getPath(), ['app' => $this->appName]); $parent = $folder->getParent(); $folder->delete(); - $this->deleteEmptyFolder($notesFolder, $parent); + $this->deleteEmptyFolder($parent, $notesFolder); } } @@ -229,7 +181,7 @@ public function deleteEmptyFolder(Folder $notesFolder, Folder $folder) : void { * @param int $requiredBytes amount of storage needed in $folder * @throws InsufficientStorageException */ - public function ensureSufficientStorage(Folder $folder, $requiredBytes) : void { + public function ensureSufficientStorage(Folder $folder, int $requiredBytes) : void { $availableBytes = $folder->getFreeSpace(); if ($availableBytes >= 0 && $availableBytes < $requiredBytes) { $this->logger->error( diff --git a/lib/Service/NotesFolderException.php b/lib/Service/NotesFolderException.php index 8b98f3de3..f6e9b6f84 100644 --- a/lib/Service/NotesFolderException.php +++ b/lib/Service/NotesFolderException.php @@ -1,13 +1,8 @@ -root = $root; - $this->l10n = $l10n; - $this->logger = $logger; - $this->config = $config; - $this->tags = $tagManager->load('files'); $this->settings = $settings; $this->noteUtil = $noteUtil; - $this->appName = $appName; } - - /** - * @param string $userId - * @return array with all notes in the current directory - */ - public function getAll($userId, $onlyMeta = false) { - $notesFolder = $this->getFolderForUser($userId); - $notes = $this->noteUtil->gatherNoteFiles($notesFolder); - $filesById = []; - foreach ($notes as $note) { - $filesById[$note->getId()] = $note; - } - $tags = $this->tags->getTagsForObjects(array_keys($filesById)); - - $notes = []; - foreach ($filesById as $id => $file) { - $noteTags = is_array($tags) && array_key_exists($id, $tags) ? $tags[$id] : []; - $notes[] = $this->getNote($file, $notesFolder, $noteTags, $onlyMeta); - } - + public function getAll(string $userId) { + $notesFolder = $this->getNotesFolder($userId); + $files = $this->gatherNoteFiles($notesFolder); + $fileIds = array_map(function (File $file) : int { + return $file->getId(); + }, $files); + // pre-load tags for all notes (performance improvement) + $this->noteUtil->getTagService()->loadTags($fileIds); + $notes = array_map(function (File $file) use ($notesFolder) : Note { + return new Note($file, $notesFolder, $this->noteUtil); + }, $files); return $notes; } - - /** - * Used to get a single note by id - * @param int $id the id of the note to get - * @param string $userId - * @throws NoteDoesNotExistException if note does not exist - * @return Note - */ - public function get($id, $userId, $onlyMeta = false) : Note { - $folder = $this->getFolderForUser($userId); - return $this->getNote($this->getFileById($folder, $id), $folder, $this->getTags($id), $onlyMeta); - } - - private function getTags($id) { - $tags = $this->tags->getTagsForObjects([$id]); - return is_array($tags) && array_key_exists($id, $tags) ? $tags[$id] : []; - } - - 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) { - $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) { - $message = $this->l10n->t('Error').': ('.$file->getName().') '.$e->getMessage(); - $note = Note::fromException($message, $file, $notesFolder, array_key_exists($id, $tags) ? $tags[$id] : []); - } - return $note; + public function get(string $userId, int $id) : Note { + $notesFolder = $this->getNotesFolder($userId); + return new Note($this->getFileById($notesFolder, $id), $notesFolder, $this->noteUtil); } /** - * Creates a note and returns the empty note - * @param string $userId - * @see update for setting note content - * @return Note the newly created note + * @throws \OCP\Files\NotPermittedException */ - public function create($userId) : Note { - $title = $this->l10n->t('New note'); - $folder = $this->getFolderForUser($userId); + public function create(string $userId, string $title, string $category) : Note { + // get folder based on category + $notesFolder = $this->getNotesFolder($userId); + $folder = $this->noteUtil->getCategoryFolder($notesFolder, $category); $this->noteUtil->ensureSufficientStorage($folder, 1); - // 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->noteUtil->generateFileName($folder, $title, $this->settings->get($userId, 'fileSuffix'), -1); - $file = $folder->newFile($path); + // get file name + $fileSuffix = $this->settings->get($userId, 'fileSuffix'); + $filename = $this->noteUtil->generateFileName($folder, $title, $fileSuffix, -1); + + // create file + $file = $folder->newFile($filename); // try to write some content try { @@ -141,119 +65,90 @@ public function create($userId) : Note { $file->putContent(' '); } catch (\Throwable $e) { // if writing the content fails, we have to roll back the note creation - $this->delete($file->getId(), $userId); + $this->delete($userId, $file->getId()); throw $e; } - return $this->getNote($file, $folder); + return new Note($file, $notesFolder, $this->noteUtil); } /** - * Updates a note. Be sure to check the returned note since the title is - * dynamically generated and filename conflicts are resolved - * @param int $id the id of the note used to update - * @param string|null $content the content which will be written into the note - * the title is generated from the first line of the content - * @param string|null $category the category in which the note should be saved - * @param int $mtime time of the note modification (optional) * @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) : Note { - $notesFolder = $this->getFolderForUser($userId); + public function delete(string $userId, int $id) { + $notesFolder = $this->getNotesFolder($userId); $file = $this->getFileById($notesFolder, $id); - $title = $this->noteUtil->getSafeTitleFromContent($content===null ? $file->getContent() : $content); + $parent = $file->getParent(); + $file->delete(); + $this->noteUtil->deleteEmptyFolder($parent, $notesFolder); + } + + public function getTitleFromContent(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 + return $this->noteUtil->getSafeTitle($content); + } + + - // rename/move file with respect to title/category - // this can fail if access rights are not sufficient or category name is illegal - try { - $this->noteUtil->moveNote($notesFolder, $file, $category, $title); - } catch (\OCP\Files\NotPermittedException $e) { - $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) { - $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) { - $this->noteUtil->ensureSufficientStorage($file->getParent(), strlen($content)); - $file->putContent($content); - } - if ($mtime) { - $file->touch($mtime); - } - return $this->getNote($file, $notesFolder, $this->getTags($id)); + /** + * @param string $userId the user id + * @return Folder + */ + private function getNotesFolder(string $userId) : Folder { + $userPath = $this->noteUtil->getRoot()->getUserFolder($userId)->getPath(); + $path = $userPath . '/' . $this->settings->get($userId, 'notesPath'); + try { + $folder = $this->noteUtil->getOrCreateFolder($path); + } catch (\Exception $e) { + throw new NotesFolderException($path); + } + return $folder; } /** - * Set or unset a note as favorite. - * @param int $id the id of the note used to update - * @param boolean $favorite whether the note should be a favorite or not - * @throws NoteDoesNotExistException if note does not exist - * @return boolean the new favorite state of the note + * gather note files in given directory and all subdirectories */ - public function favorite($id, $favorite, $userId) { - $note = $this->get($id, $userId, true); - if ($favorite !== $note->getFavorite()) { - if ($favorite) { - $this->tags->addToFavorites($id); - } else { - $this->tags->removeFromFavorites($id); + private static function gatherNoteFiles(Folder $folder) : array { + $files = []; + $nodes = $folder->getDirectoryListing(); + foreach ($nodes as $node) { + if ($node->getType() === FileInfo::TYPE_FOLDER && $node instanceof Folder) { + $files = array_merge($files, self::gatherNoteFiles($node)); + continue; + } + if (self::isNote($node)) { + $files[] = $node; } - $note = $this->get($id, $userId, true); } - return $note->getFavorite(); + return $files; } - /** - * Deletes a note - * @param int $id the id of the note which should be deleted - * @param string $userId - * @throws NoteDoesNotExistException if note does not - * exist + * test if file is a note */ - public function delete($id, $userId) { - $notesFolder = $this->getFolderForUser($userId); - $file = $this->getFileById($notesFolder, $id); - $parent = $file->getParent(); - $file->delete(); - $this->noteUtil->deleteEmptyFolder($notesFolder, $parent); + private static function isNote(FileInfo $file) : bool { + static $allowedExtensions = ['txt', 'org', 'markdown', 'md', 'note']; + $ext = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)); + return $file->getType() === 'file' && in_array($ext, $allowedExtensions); } /** - * @param Folder $folder - * @param int $id * @throws NoteDoesNotExistException - * @return \OCP\Files\File */ - private function getFileById(Folder $folder, $id) : File { + private static function getFileById(Folder $folder, int $id) : File { $file = $folder->getById($id); - if (count($file) <= 0 || !($file[0] instanceof File) || !$this->noteUtil->isNote($file[0])) { + if (count($file) <= 0 || !($file[0] instanceof File) || !self::isNote($file[0])) { throw new NoteDoesNotExistException(); } return $file[0]; } - - /** - * @param string $userId the user id - * @return Folder - */ - private function getFolderForUser($userId) : Folder { - // TODO use IRootFolder->getUserFolder() ? - $path = '/' . $userId . '/files/' . $this->settings->get($userId, 'notesPath'); - try { - $folder = $this->noteUtil->getOrCreateFolder($path); - } catch (\Exception $e) { - throw new NotesFolderException($path); - } - return $folder; - } } diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 1ce7add79..5c2308c30 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -1,4 +1,4 @@ - $value) { if (!array_key_exists($name, $this->defaults) @@ -37,7 +37,7 @@ public function set($uid, $settings) { $this->config->setUserValue($uid, 'notes', 'settings', json_encode($settings)); } - public function getAll($uid) { + public function getAll(string $uid) : \stdClass { $settings = json_decode($this->config->getUserValue($uid, 'notes', 'settings')); if (is_object($settings)) { // use default for empty settings @@ -55,7 +55,7 @@ public function getAll($uid) { /** * @throws \OCP\PreConditionNotMetException */ - public function get($uid, $name) { + public function get(string $uid, string $name) : string { $settings = $this->getAll($uid); if (property_exists($settings, $name)) { return $settings->{$name}; diff --git a/lib/Service/TagService.php b/lib/Service/TagService.php new file mode 100644 index 000000000..5f3d5f2f9 --- /dev/null +++ b/lib/Service/TagService.php @@ -0,0 +1,50 @@ +tagger = $tagManager->load('files'); + } + + public function loadTags(array $fileIds) : void { + $this->cachedTags = $this->tagger->getTagsForObjects($fileIds); + } + + // TODO NC19: replace this by OCP\ITags::TAG_FAVORITE + // OCP\ITags::TAG_FAVORITE was introduced in NC19 + // https://github.com/nextcloud/server/pull/19412 + /** + * @suppress PhanUndeclaredClassConstant + * @suppress PhanUndeclaredConstant + * @suppress PhanUndeclaredConstantOfClass + */ + private static function getTagFavorite() { + if (defined('OCP\ITags::TAG_FAVORITE')) { + return \OCP\ITags::TAG_FAVORITE; + } else { + return \OC\Tags::TAG_FAVORITE; + } + } + + public function isFavorite($fileId) : bool { + $alltags = $this->cachedTags; + if (!is_array($alltags)) { + $alltags = $this->tagger->getTagsForObjects([$fileId]); + } + return array_key_exists($fileId, $alltags) && in_array(self::getTagFavorite(), $alltags[$fileId]); + } + + public function setFavorite($fileId, $favorite) : void { + if ($favorite) { + $this->tagger->addToFavorites($fileId); + } else { + $this->tagger->removeFromFavorites($fileId); + } + } +} diff --git a/src/App.vue b/src/App.vue index 0d9a5c61d..fe1bbabf5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -198,7 +198,7 @@ export default { return } this.loading.create = true - createNote(this.filter.category) + createNote(this.filter.category || '') .then(note => { this.routeToNote(note.id) }) diff --git a/src/NotesService.js b/src/NotesService.js index a56f69897..cdb6dbefa 100644 --- a/src/NotesService.js +++ b/src/NotesService.js @@ -58,6 +58,7 @@ export const fetchNote = noteId => { .get(url('/notes/' + noteId)) .then(response => { const localNote = store.getters.getNote(parseInt(noteId)) + response.data.autotitle = response.data.content.length === 0 // only overwrite if there are no unsaved changes if (!localNote || !localNote.unsaved) { store.commit('add', response.data) @@ -77,6 +78,19 @@ export const fetchNote = noteId => { }) } +export const setTitle = (noteId, title) => { + return axios + .put(url('/notes/' + noteId + '/title'), { title: title }) + .then(response => { + store.commit('setNoteAttribute', { noteId: noteId, attribute: 'title', value: response.data }) + }) + .catch(err => { + console.error(err) + handleSyncError(t('notes', 'Renaming note {id} has failed.', { id: noteId })) + throw err + }) +} + export const createNote = category => { return axios .post(url('/notes'), { category: category }) @@ -97,7 +111,7 @@ export const createNote = category => { function _updateNote(note) { return axios - .put(url('/notes/' + note.id), { content: note.content }) + .put(url('/notes/' + note.id), { content: note.content, autotitle: note.autotitle }) .then(response => { const updated = response.data note.saveError = false diff --git a/src/components/NavigationNoteItem.vue b/src/components/NavigationNoteItem.vue index 17637b714..94edcd290 100644 --- a/src/components/NavigationNoteItem.vue +++ b/src/components/NavigationNoteItem.vue @@ -5,17 +5,23 @@ :menu-open.sync="actionsOpen" :to="{ name: 'note', params: { noteId: note.id.toString() } }" :class="{ actionsOpen }" + :loading="loading.note" + :editable="true" + :edit-label="t('notes', 'Rename')" + :edit-placeholder="t('notes', 'Note\'s title')" + @update:title="onRename" > @@ -23,17 +29,19 @@