Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/notes.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkorelstar <korelstar@users.noreply.github.com>2020-04-12 20:26:57 +0300
committerkorelstar <korelstar@users.noreply.github.com>2020-04-13 09:49:40 +0300
commitffe6a02947d40d9b5bdb7ed93b9b54df5d690cc1 (patch)
treede4c425076f188c650f492eeb5c3c4a42cd8c78c
parent02fdcee3774a5265e3dbc5d22f11eddc5b7b1747 (diff)
maintenance and new API v1
-rw-r--r--Makefile2
-rw-r--r--appinfo/routes.php111
-rw-r--r--lib/Application.php2
-rw-r--r--lib/Capabilities.php2
-rw-r--r--lib/Controller/Errors.php27
-rw-r--r--lib/Controller/Helper.php43
-rw-r--r--lib/Controller/NotesApiController.php271
-rw-r--r--lib/Controller/NotesController.php314
-rw-r--r--lib/Controller/PageController.php15
-rw-r--r--lib/Controller/SettingsController.php11
-rw-r--r--lib/Db/Meta.php2
-rw-r--r--lib/Db/Note.php123
-rw-r--r--lib/Service/InsufficientStorageException.php2
-rw-r--r--lib/Service/MetaService.php15
-rw-r--r--lib/Service/Note.php141
-rw-r--r--lib/Service/NoteDoesNotExistException.php7
-rw-r--r--lib/Service/NoteUtil.php128
-rw-r--r--lib/Service/NotesFolderException.php7
-rw-r--r--lib/Service/NotesService.php282
-rw-r--r--lib/Service/SettingsService.php8
-rw-r--r--lib/Service/TagService.php50
-rw-r--r--src/App.vue2
-rw-r--r--tests/api/APIv02Test.php194
-rw-r--r--tests/api/APIv1Test.php10
-rw-r--r--tests/api/AbstractAPITest.php26
-rw-r--r--tests/api/CapabilitiesTest.php58
-rw-r--r--tests/api/CommonAPITest.php217
27 files changed, 1057 insertions, 1013 deletions
diff --git a/Makefile b/Makefile
index d25b3c49..a4180ac4 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/routes.php b/appinfo/routes.php
index 59af979a..1c43732b 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -1,16 +1,6 @@
<?php
-/**
- * Nextcloud - Notes
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright Bernhard Posselt 2012, 2014
- */
-
return ['routes' => [
- // page
+ ////////// P A G E //////////
[
'name' => 'page#index',
'url' => '/',
@@ -30,7 +20,8 @@ return ['routes' => [
'requirements' => ['id' => '\d+'],
],
- // notes
+
+ ////////// N O T E S //////////
[
'name' => 'notes#index',
'url' => '/notes',
@@ -59,22 +50,13 @@ return ['routes' => [
'requirements' => ['id' => '\d+'],
],
[
- 'name' => 'notes#category',
- 'url' => '/notes/{id}/category',
- 'verb' => 'PUT',
- 'requirements' => ['id' => '\d+'],
- ],
- [
- 'name' => 'notes#title',
- 'url' => '/notes/{id}/title',
- 'verb' => 'PUT',
- 'requirements' => ['id' => '\d+'],
- ],
- [
- 'name' => 'notes#favorite',
- 'url' => '/notes/{id}/favorite',
+ 'name' => 'notes#updateProperty',
+ 'url' => '/notes/{id}/{property}',
'verb' => 'PUT',
- 'requirements' => ['id' => '\d+'],
+ 'requirements' => [
+ 'id' => '\d+',
+ 'property' => '(modified|title|category|favorite)',
+ ],
],
[
'name' => 'notes#destroy',
@@ -83,43 +65,88 @@ return ['routes' => [
'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
index 1f9010a6..a92bf5f7 100644
--- a/lib/Application.php
+++ b/lib/Application.php
@@ -6,6 +6,8 @@ use OCP\AppFramework\App;
class Application extends App {
+ public static $API_VERSIONS = [ '0.2', '1.0' ];
+
public function __construct(array $urlParams = []) {
parent::__construct('notes', $urlParams);
}
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 0f05aa78..8f4a0010 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -9,7 +9,7 @@ class Capabilities implements ICapability {
public function getCapabilities() {
return [
'notes' => [
- 'api_version' => [ '0.2' ],
+ 'api_version' => Application::$API_VERSIONS,
],
];
}
diff --git a/lib/Controller/Errors.php b/lib/Controller/Errors.php
deleted file mode 100644
index 138f05d8..00000000
--- a/lib/Controller/Errors.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-namespace OCA\Notes\Controller;
-
-use OCP\AppFramework\Http;
-use OCP\AppFramework\Http\DataResponse;
-
-use OCA\Notes\Service\NoteDoesNotExistException;
-
-/**
- * Class Errors
- *
- * @package OCA\Notes\Controller
- */
-trait Errors {
- /**
- * @param $callback
- * @return DataResponse
- */
- protected function respond($callback) {
- try {
- return new DataResponse($callback());
- } catch (NoteDoesNotExistException $ex) {
- return new DataResponse([], Http::STATUS_NOT_FOUND);
- }
- }
-}
diff --git a/lib/Controller/Helper.php b/lib/Controller/Helper.php
new file mode 100644
index 00000000..7eaf1dbe
--- /dev/null
+++ b/lib/Controller/Helper.php
@@ -0,0 +1,43 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Controller;
+
+use OCA\Notes\Application;
+use OCA\Notes\Service\InsufficientStorageException;
+use OCA\Notes\Service\NoteDoesNotExistException;
+
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\ILogger;
+
+class Helper {
+
+ private $logger;
+ private $appName;
+
+ public function __construct(
+ ILogger $logger,
+ string $appName
+ ) {
+ $this->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 52b553e2..f8e62ef3 100644
--- a/lib/Controller/NotesApiController.php
+++ b/lib/Controller/NotesApiController.php
@@ -1,106 +1,74 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Controller;
+use OCA\Notes\Service\NotesService;
+use OCA\Notes\Service\MetaService;
+
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use OCP\IUserSession;
-use OCA\Notes\Service\NotesService;
-use OCA\Notes\Service\MetaService;
-use OCA\Notes\Service\InsufficientStorageException;
-use OCA\Notes\Service\NoteDoesNotExistException;
-use OCA\Notes\Db\Note;
-
-/**
- * Class NotesApiController
- *
- * @package OCA\Notes\Controller
- */
class NotesApiController extends ApiController {
- use Errors;
-
/** @var NotesService */
private $service;
/** @var MetaService */
private $metaService;
+ /** @var Helper */
+ private $helper;
/** @var IUserSession */
private $userSession;
- /**
- * @param string $AppName
- * @param IRequest $request
- * @param NotesService $service
- * @param IUserSession $userSession
- */
public function __construct(
- $AppName,
+ string $AppName,
IRequest $request,
NotesService $service,
MetaService $metaService,
+ Helper $helper,
IUserSession $userSession
) {
parent::__construct($AppName, $request);
$this->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 @@ class NotesApiController extends ApiController {
* @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 @@ class NotesApiController extends ApiController {
* @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 13d22697..c2be6e73 100644
--- a/lib/Controller/NotesController.php
+++ b/lib/Controller/NotesController.php
@@ -1,7 +1,10 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Controller;
+use OCA\Notes\Service\NotesService;
+use OCA\Notes\Service\SettingsService;
+
use OCP\AppFramework\Controller;
use OCP\IRequest;
use OCP\IConfig;
@@ -9,23 +12,14 @@ use OCP\IL10N;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
-use OCA\Notes\Service\NotesService;
-use OCA\Notes\Service\SettingsService;
-use OCA\Notes\Service\InsufficientStorageException;
-
-/**
- * Class NotesController
- *
- * @package OCA\Notes\Controller
- */
class NotesController extends Controller {
- use Errors;
-
/** @var NotesService */
private $notesService;
/** @var SettingsService */
private $settingsService;
+ /** @var Helper */
+ private $helper;
/** @var IConfig */
private $settings;
/** @var string */
@@ -33,27 +27,20 @@ class NotesController extends Controller {
/** @var IL10N */
private $l10n;
- /**
- * @param string $AppName
- * @param IRequest $request
- * @param NotesService $notesService
- * @param SettingsService $settingsService
- * @param IConfig $settings
- * @param IL10N $l10n
- * @param string $UserId
- */
public function __construct(
- $AppName,
+ string $AppName,
IRequest $request,
NotesService $notesService,
SettingsService $settingsService,
+ Helper $helper,
IConfig $settings,
IL10N $l10n,
- $UserId
+ string $UserId
) {
parent::__construct($AppName, $request);
$this->notesService = $notesService;
$this->settingsService = $settingsService;
+ $this->helper = $helper;
$this->settings = $settings;
$this->userId = $UserId;
$this->l10n = $l10n;
@@ -63,176 +50,199 @@ class NotesController extends Controller {
/**
* @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,
- $this->userId,
- $category
- );
- return new DataResponse($note);
- } catch (InsufficientStorageException $e) {
- return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE);
- }
- }
-
+ public function get(int $id) : DataResponse {
+ return $this->helper->handleErrorResponse(function () use ($id) {
+ $note = $this->notesService->get($this->userId, $id);
- /**
- * @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,
+ // save the last viewed note
+ $this->settings->setUserValue(
$this->userId,
- $category,
- $modified
+ $this->appName,
+ 'notesLastViewedNote',
+ strval($id)
);
- $note->favorite = $this->notesService->favorite($note->getId(), $favorite, $this->userId);
- }
- return new DataResponse($note);
+
+ return $note->getData();
+ });
}
/**
* @NoAdminRequired
*/
- public function update(int $id, string $content, bool $autotitle) : DataResponse {
- try {
- if ($autotitle) {
- $note = $this->notesService->update($id, $content, $this->userId);
- } else {
- $note = $this->notesService->setContent($this->userId, $id, $content);
- }
- return new DataResponse($note);
- } catch (InsufficientStorageException $e) {
- return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE);
- }
+ 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 $category
- * @return DataResponse
*/
- public function category($id, $category) {
- $note = $this->notesService->setTitleCategory($this->userId, $id, null, $category);
- return new DataResponse($note->getCategory()); // @phan-suppress-current-line PhanTypeMismatchArgument
+ 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 $title
- * @return DataResponse
*/
- public function title($id, $title) {
- $note = $this->notesService->setTitleCategory($this->userId, $id, $title, null);
- return new DataResponse($note->getTitle()); // @phan-suppress-current-line PhanTypeMismatchArgument
+ 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 e595116e..03fe7896 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Controller;
@@ -7,18 +7,9 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\IRequest;
-/**
- * Class PageController
- *
- * @package OCA\Notes\Controller
- */
class PageController extends Controller {
- /**
- * @param string $AppName
- * @param IRequest $request
- */
- public function __construct($AppName, IRequest $request) {
+ public function __construct(string $AppName, IRequest $request) {
parent::__construct($AppName, $request);
}
@@ -29,7 +20,7 @@ class PageController extends Controller {
*
* @return TemplateResponse
*/
- public function index() {
+ public function index() : TemplateResponse {
$devMode = !is_file(dirname(__FILE__).'/../../js/notes.js');
$response = new TemplateResponse(
$this->appName,
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 19b00b79..681e7763 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -1,12 +1,13 @@
-<?php
+<?php declare(strict_types=1);
+
namespace OCA\Notes\Controller;
-use OCP\AppFramework\Controller;
+use OCA\Notes\Service\SettingsService;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
-use OCP\AppFramework\Http\JSONResponse;
-use OCA\Notes\Service\SettingsService;
class SettingsController extends Controller {
@@ -14,7 +15,7 @@ class SettingsController extends Controller {
private $userSession;
public function __construct(
- $appName,
+ string $appName,
IRequest $request,
SettingsService $service,
IUserSession $userSession
diff --git a/lib/Db/Meta.php b/lib/Db/Meta.php
index e970ed00..9144bea8 100644
--- a/lib/Db/Meta.php
+++ b/lib/Db/Meta.php
@@ -2,6 +2,8 @@
namespace OCA\Notes\Db;
+use OCA\Notes\Service\Note;
+
use OCP\AppFramework\Db\Entity;
/**
diff --git a/lib/Db/Note.php b/lib/Db/Note.php
deleted file mode 100644
index 69411de1..00000000
--- a/lib/Db/Note.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-namespace OCA\Notes\Db;
-
-use OCP\Files\File;
-use OCP\Files\Folder;
-use OCP\AppFramework\Db\Entity;
-
-/**
- * Class Note
- * @method integer getId()
- * @method void setId(integer $value)
- * @method string getEtag()
- * @method void setEtag(string $value)
- * @method integer getModified()
- * @method void setModified(integer $value)
- * @method string getTitle()
- * @method void setTitle(string $value)
- * @method string getCategory()
- * @method void setCategory(string $value)
- * @method string getContent()
- * @method void setContent(string $value)
- * @method boolean getFavorite()
- * @method void setFavorite(boolean $value)
- * @method boolean getError()
- * @method void setError(boolean $value)
- * @method string getErrorMessage()
- * @method void setErrorMessage(string $value)
- * @package OCA\Notes\Db
- */
-class Note extends Entity {
- public $etag;
- public $modified;
- public $title;
- public $category;
- public $content = null;
- public $favorite = false;
- public $error = false;
- public $errorMessage='';
-
- public function __construct() {
- $this->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 d62cd6a3..78821d3b 100644
--- a/lib/Service/InsufficientStorageException.php
+++ b/lib/Service/InsufficientStorageException.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
diff --git a/lib/Service/MetaService.php b/lib/Service/MetaService.php
index 337ea03d..e4da1333 100644
--- a/lib/Service/MetaService.php
+++ b/lib/Service/MetaService.php
@@ -1,15 +1,10 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
use OCA\Notes\Db\Meta;
use OCA\Notes\Db\MetaMapper;
-/**
- * Class MetaService
- *
- * @package OCA\Notes\Service
- */
class MetaService {
private $metaMapper;
@@ -18,7 +13,7 @@ class MetaService {
$this->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 @@ class MetaService {
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 @@ class MetaService {
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 00000000..1fa3192c
--- /dev/null
+++ b/lib/Service/Note.php
@@ -0,0 +1,141 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Service;
+
+use OCP\Files\File;
+use OCP\Files\Folder;
+
+class Note {
+
+ private $file;
+ private $notesFolder;
+ private $noteUtil;
+
+ public function __construct(File $file, Folder $notesFolder, NoteUtil $noteUtil) {
+ $this->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 fc904e5a..25362ac0 100644
--- a/lib/Service/NoteDoesNotExistException.php
+++ b/lib/Service/NoteDoesNotExistException.php
@@ -1,13 +1,8 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
use Exception;
-/**
- * Class NoteDoesNotExistException
- *
- * @package OCA\Notes\Service
- */
class NoteDoesNotExistException extends Exception {
}
diff --git a/lib/Service/NoteUtil.php b/lib/Service/NoteUtil.php
index 6f6bf99f..51d34a0a 100644
--- a/lib/Service/NoteUtil.php
+++ b/lib/Service/NoteUtil.php
@@ -1,109 +1,65 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
-use OCP\Files\IRootFolder;
-use OCP\Files\FileInfo;
-use OCP\Files\File;
-use OCP\Files\Folder;
class NoteUtil {
private $db;
private $l10n;
private $root;
+ private $tagService;
+ private $cachedTags;
private $logger;
private $appName;
- /**
- * @param IDBConnection $db
- * @param IRootFolder $root
- * @param IL10N $l10n
- * @param ILogger $logger
- * @param String $appName
- */
public function __construct(
- IDBConnection $db,
IRootFolder $root,
+ IDBConnection $db,
+ TagService $tagService,
IL10N $l10n,
ILogger $logger,
- $appName
+ string $appName
) {
- $this->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, string $title, ?string $category = null) : void {
- $id = $file->getId();
- $title = $this->getSafeTitle($title);
- $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);
}
/**
@@ -120,12 +76,13 @@ class NoteUtil {
* 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<id>\d+)\)$/u', $title, $matches);
@@ -143,15 +100,6 @@ class NoteUtil {
}
}
- 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
- return $this->getSafeTitle($content);
- }
-
public function getSafeTitle(string $content) : string {
// sanitize: prevent directory traversal, illegal characters and unintended file names
$content = $this->sanitisePath($content);
@@ -172,7 +120,7 @@ class NoteUtil {
}
/** 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(...)
@@ -212,10 +160,10 @@ class NoteUtil {
/*
* 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();
@@ -223,7 +171,7 @@ class NoteUtil {
$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);
}
}
diff --git a/lib/Service/NotesFolderException.php b/lib/Service/NotesFolderException.php
index 8b98f3de..f6e9b6f8 100644
--- a/lib/Service/NotesFolderException.php
+++ b/lib/Service/NotesFolderException.php
@@ -1,13 +1,8 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
use Exception;
-/**
- * Class NotesFolderException
- *
- * @package OCA\Notes\Service
- */
class NotesFolderException extends Exception {
}
diff --git a/lib/Service/NotesService.php b/lib/Service/NotesService.php
index 8d8050ea..3bfbf794 100644
--- a/lib/Service/NotesService.php
+++ b/lib/Service/NotesService.php
@@ -1,137 +1,61 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
-use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCA\Notes\Service\SettingsService;
+
use OCP\Files\File;
+use OCP\Files\FileInfo;
use OCP\Files\Folder;
-use OCP\Files\IRootFolder;
-use OCP\IConfig;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\ITagManager;
-use OCA\Notes\Db\Note;
-use OCA\Notes\Service\SettingsService;
-
-/**
- * Class NotesService
- *
- * @package OCA\Notes\Service
- */
class NotesService {
- private $l10n;
- private $root;
- private $logger;
- private $config;
- private $tags;
private $settings;
private $noteUtil;
- private $appName;
- /**
- * @param IRootFolder $root
- * @param IL10N $l10n
- * @param ILogger $logger
- * @param IConfig $config
- * @param ITagManager $tagManager
- * @param SettingsService $settings
- * @param NoteUtil $noteUtil
- * @param String $appName
- */
public function __construct(
- IRootFolder $root,
- IL10N $l10n,
- ILogger $logger,
- IConfig $config,
- ITagManager $tagManager,
SettingsService $settings,
- NoteUtil $noteUtil,
- $appName
+ NoteUtil $noteUtil
) {
- $this->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(string $userId, bool $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(int $id, string $userId, bool $onlyMeta = false) : Note {
- $folder = $this->getFolderForUser($userId);
- return $this->getNote($this->getFileById($folder, $id), $folder, $this->getTags($id), $onlyMeta);
- }
-
- private function getTags(int $id) {
- $tags = $this->tags->getTagsForObjects([$id]);
- return is_array($tags) && array_key_exists($id, $tags) ? $tags[$id] : [];
- }
-
- private function getNote(File $file, Folder $notesFolder, array $tags = [], bool $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(string $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,142 +65,90 @@ class NotesService {
$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(int $id, ?string $content, string $userId, ?string $category = null, int $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);
+ }
- // 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, $title, $category);
- } catch (\OCP\Files\NotPermittedException $e) {
- $err = 'Moving note '.$file->getId().' ('.$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]);
- }
+ 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);
+ }
- if ($content !== null) {
- $this->setContentForFile($file, $content);
- }
- if ($mtime) {
- $file->touch($mtime);
- }
- return $this->getNote($file, $notesFolder, $this->getTags($id));
- }
- private function setContentForFile(File $file, $content) : void {
- $this->noteUtil->ensureSufficientStorage($file->getParent(), strlen($content));
- $file->putContent($content);
- }
- public function setContent(string $userId, int $id, string $content) : Note {
- $notesFolder = $this->getFolderForUser($userId);
- $file = $this->getFileById($notesFolder, $id);
- $this->setContentForFile($file, $content);
- return $this->getNote($file, $notesFolder, $this->getTags($id));
- }
- public function setTitleCategory(string $userId, int $id, ?string $title, ?string $category = null) : Note {
- $notesFolder = $this->getFolderForUser($userId);
- $file = $this->getFileById($notesFolder, $id);
- if ($title === null) {
- $note = $this->getNote($file, $notesFolder, [], true);
- $title = $note->getTitle();
+ /**
+ * @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);
}
- $this->noteUtil->moveNote($notesFolder, $file, $title, $category);
-
- return $this->getNote($file, $notesFolder, $this->getTags($id));
+ 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(int $id, bool $favorite, string $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(int $id, string $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, int $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(string $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 1ce7add7..5c2308c3 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
namespace OCA\Notes\Service;
@@ -24,7 +24,7 @@ class SettingsService {
/**
* @throws \OCP\PreConditionNotMetException
*/
- public function set($uid, $settings) {
+ public function set(string $uid, array $settings) : void {
// remove illegal, empty and default settings
foreach ($settings as $name => $value) {
if (!array_key_exists($name, $this->defaults)
@@ -37,7 +37,7 @@ class SettingsService {
$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 @@ class SettingsService {
/**
* @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 00000000..5f3d5f2f
--- /dev/null
+++ b/lib/Service/TagService.php
@@ -0,0 +1,50 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Service;
+
+use OCP\ITagManager;
+
+class TagService {
+ private $tagger;
+ private $cachedTags;
+
+ public function __construct(ITagManager $tagManager) {
+ $this->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 0d9a5c61..fe1bbabf 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/tests/api/APIv02Test.php b/tests/api/APIv02Test.php
index 93250eb8..cbca70de 100644
--- a/tests/api/APIv02Test.php
+++ b/tests/api/APIv02Test.php
@@ -2,199 +2,9 @@
namespace OCA\Notes\Tests\API;
-class APIv02Test extends AbstractAPITest {
+class APIv02Test extends CommonAPITest {
public function __construct() {
- parent::__construct('v0.2');
+ parent::__construct('0.2', true);
}
-
- public function testCheckForReferenceNotes() : array {
- $response = $this->http->request('GET', 'notes');
- $this->checkResponse($response, 'Get existing notes', 200);
- $notes = json_decode($response->getBody()->getContents());
- $this->assertNotEmpty($notes, 'List of notes');
- return $notes;
- }
-
- /** @depends testCheckForReferenceNotes */
- public function testGetNotesWithExclude(array $refNotes) : void {
- $this->checkGetReferenceNotes(
- $refNotes,
- 'exclude content',
- '?exclude=content',
- false,
- ['content']
- );
- $this->checkGetReferenceNotes(
- $refNotes,
- 'exclude content and category',
- '?exclude=content,category',
- false,
- ['content','category']
- );
- }
-
- /** @depends testCheckForReferenceNotes */
- public function testGetNotesWithEtag(array $refNotes) : void {
- $response1 = $this->http->request('GET', 'notes');
- $this->checkResponse($response1, 'Initial response', 200);
- $this->assertTrue($response1->hasHeader('ETag'), 'Initial response has ETag header');
- $etag = $response1->getHeaderLine('ETag');
- $this->assertRegExp('/^"[[:alnum:]]{32}"$/', $etag, 'ETag format');
-
- // Test If-None-Match with ETag
- $response2 = $this->http->request('GET', 'notes', [ 'headers' => [ 'If-None-Match' => $etag ] ]);
- $this->checkResponse($response2, 'ETag response', 304);
- $this->assertEquals('', $response2->getBody(), 'ETag response body');
- }
-
- /** @depends testCheckForReferenceNotes */
- public function testGetNotesWithPruneBefore(array $refNotes) : void {
- sleep(1); // wait for 'Last-Modified' to be >= Last-change + 1
- $response1 = $this->http->request('GET', 'notes');
- $this->checkResponse($response1, 'Initial response', 200);
- $this->assertTrue($response1->hasHeader('Last-Modified'), 'Initial response has Last-Modified header');
- $lastModified = $response1->getHeaderLine('Last-Modified');
- $dt = \DateTime::createFromFormat(\DateTime::RFC2822, $lastModified);
- $this->assertInstanceOf(\DateTime::class, $dt);
-
- $this->checkGetReferenceNotes(
- $refNotes,
- 'pruneBefore with Last-Modified',
- '?pruneBefore='.$dt->getTimestamp(),
- true
- );
- $this->checkGetReferenceNotes(
- $refNotes,
- 'pruneBefore with 1',
- '?pruneBefore=1',
- false
- );
- $this->checkGetReferenceNotes(
- $refNotes,
- 'pruneBefore with PHP_INT_MAX (32bit)',
- '?pruneBefore=2147483647', // 2038-01-19 03:14:07
- true
- );
- $this->checkGetReferenceNotes(
- $refNotes,
- 'pruneBefore with PHP_INT_MAX (64bit)',
- '?pruneBefore=9223372036854775807',
- true
- );
- }
-
- /** @depends testCheckForReferenceNotes */
- public function testCreateNotes(array $refNotes) : array {
- $this->checkGetReferenceNotes($refNotes, 'Pre-condition');
- $testNotes = [];
- $testNotes[] = $this->createNote((object)[
- 'title' => 'This is not used',
- 'content' => '# *First* test/ note'.PHP_EOL.'This is some body content with some data.',
- 'favorite' => true,
- 'category' => 'Test/../New Category',
- 'modified' => mktime(8, 14, 30, 10, 2, 2020),
- ], (object)[
- 'title' => 'First test note',
- 'category' => 'Test/New Category',
- ]);
- $testNotes[] = $this->createNote((object)[
- 'content' => 'Note with Defaults'.PHP_EOL.'This is some body content with some data.',
- ], (object)[
- 'title' => 'Note with Defaults',
- 'favorite' => false,
- 'category' => '',
- 'modified' => time(),
- ]);
- $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'After creating notes');
- return $testNotes;
- }
-
- /**
- * @depends testCheckForReferenceNotes
- * @depends testCreateNotes
- */
- public function testGetSingleNote(array $refNotes, array $testNotes) : void {
- foreach ($testNotes as $testNote) {
- $response = $this->http->request('GET', 'notes/'.$testNote->id);
- $this->checkResponse($response, 'Get note '.$testNote->title, 200);
- $note = json_decode($response->getBody()->getContents());
- $this->checkReferenceNote($testNote, $note, 'Get single note');
- }
- // test non-existing note
- $response = $this->http->request('GET', 'notes/1');
- $this->assertEquals(404, $response->getStatusCode());
- }
-
-
- /**
- * @depends testCheckForReferenceNotes
- * @depends testCreateNotes
- */
- public function testUpdateNotes(array $refNotes, array $testNotes) : array {
- $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'Pre-condition');
- $note = $testNotes[0];
- // test update note with all attributes
- $this->updateNote($note, (object)[
- 'title' => 'This is not used',
- 'content' => '# *First* edited/ note'.PHP_EOL.'This is some body content with some data.',
- 'favorite' => false,
- 'category' => 'Test/Another Category',
- 'modified' => mktime(11, 46, 23, 4, 3, 2020),
- ], (object)[
- 'title' => 'First edited note',
- ]);
- // test update note with single attributes
- /* TODO this doesn't work (content is null)
- $this->updateNote($note, (object)[
- 'category' => 'Test/Third Category',
- ], (object)[]);
- */
- $this->updateNote($note, (object)[
- 'favorite' => true,
- ], (object)[]);
- $this->updateNote($note, (object)[
- 'content' => '# First multi edited note'.PHP_EOL.'This is some body content with some data.',
- ], (object)[
- 'title' => 'First multi edited note',
- 'modified' => time(),
- ]);
- $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'After updating notes');
- return $testNotes;
- }
-
- /**
- * @depends testCheckForReferenceNotes
- * @depends testUpdateNotes
- */
- public function testDeleteNotes(array $refNotes, array $testNotes) : void {
- $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'Pre-condition');
- foreach ($testNotes as $note) {
- $response = $this->http->request('DELETE', 'notes/'.$note->id);
- $this->checkResponse($response, 'Delete note '.$note->title, 200);
- }
- // test non-existing note
- $response = $this->http->request('DELETE', 'notes/1');
- $this->checkResponse($response, 'Delete non-existing note', 404);
- $this->checkGetReferenceNotes($refNotes, 'After deletion');
- }
-
- public function testInsuficientStorage() {
- $auth = ['quotatest', 'test'];
- // get notes must still work
- $response = $this->http->request('GET', 'notes', [ 'auth' => $auth ]);
- $this->checkResponse($response, 'Get existing notes', 200);
- $notes = json_decode($response->getBody()->getContents());
- $this->assertNotEmpty($notes, 'List of notes');
- $note = $notes[0]; // @phan-suppress-current-line PhanTypeArraySuspiciousNullable
- $request = (object)[ 'content' => 'New test content' ];
- // update will fail
- $response1 = $this->http->request('PUT', 'notes/'.$note->id, [ 'auth' => $auth, 'json' => $request]);
- $this->assertEquals(507, $response1->getStatusCode());
- // craete will fail
- $response2 = $this->http->request('POST', 'notes', [ 'auth' => $auth, 'json' => $request]);
- $this->assertEquals(507, $response2->getStatusCode());
- }
-
- // TODO Test settings (switch to another notes folder)
}
diff --git a/tests/api/APIv1Test.php b/tests/api/APIv1Test.php
new file mode 100644
index 00000000..acf8a03e
--- /dev/null
+++ b/tests/api/APIv1Test.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Tests\API;
+
+class APIv1Test extends CommonAPITest {
+
+ public function __construct() {
+ parent::__construct('1.0', false);
+ }
+}
diff --git a/tests/api/AbstractAPITest.php b/tests/api/AbstractAPITest.php
index 728b77f6..473c3af8 100644
--- a/tests/api/AbstractAPITest.php
+++ b/tests/api/AbstractAPITest.php
@@ -14,8 +14,13 @@ abstract class AbstractAPITest extends TestCase {
}
protected function setUp() : void {
+ if ($this->apiVersion === '0.2') {
+ $v = $this->apiVersion;
+ } else {
+ $v = intval($this->apiVersion);
+ }
$this->http = new \GuzzleHttp\Client([
- 'base_uri' => 'http://localhost:8080/index.php/apps/notes/api/'.$this->apiVersion.'/',
+ 'base_uri' => 'http://localhost:8080/index.php/apps/notes/api/v'.$v.'/',
'auth' => ['test', 'test'],
'http_errors' => false,
]);
@@ -28,12 +33,24 @@ abstract class AbstractAPITest extends TestCase {
string $contentTypeExp = 'application/json; charset=utf-8'
) {
$this->assertEquals($statusExp, $response->getStatusCode(), $message.': Response status code');
- $this->assertTrue($response->hasHeader('Content-Type'), $message.': Response has content-type header');
+ $this->assertTrue(
+ $response->hasHeader('Content-Type'),
+ $message.': Response has content-type header'
+ );
$this->assertEquals(
$contentTypeExp,
$response->getHeaderLine('Content-Type'),
$message.': Response content type'
);
+ $this->assertTrue(
+ $response->hasHeader('X-Notes-API-Versions'),
+ $message.': Response has Notes-API-Versions header'
+ );
+ $this->assertContains(
+ $this->apiVersion,
+ explode(', ', $response->getHeaderLine('X-Notes-API-Versions')),
+ $message.': Response Notes-API-Versions header'
+ );
}
protected function checkGetReferenceNotes(
@@ -47,7 +64,7 @@ abstract class AbstractAPITest extends TestCase {
$response = $this->http->request('GET', 'notes' . $param);
$this->checkResponse($response, $messagePrefix, 200);
$notes = json_decode($response->getBody()->getContents());
- $notesMap = self::getNotesIdMap($notes);
+ $notesMap = $this->getNotesIdMap($notes, $messagePrefix);
$this->assertEquals(count($refNotes), count($notes), $messagePrefix.': Number of notes');
foreach ($refNotes as $refNote) {
$this->assertArrayHasKey(
@@ -140,9 +157,10 @@ abstract class AbstractAPITest extends TestCase {
);
}
- protected static function getNotesIdMap(array $notes) : array {
+ protected function getNotesIdMap(array $notes, string $messagePrefix) : array {
$map = [];
foreach ($notes as $note) {
+ $this->assertObjectHasAttribute('id', $note, $messagePrefix.': Note has property id');
$map[$note->id] = $note;
}
return $map;
diff --git a/tests/api/CapabilitiesTest.php b/tests/api/CapabilitiesTest.php
new file mode 100644
index 00000000..2153a2e5
--- /dev/null
+++ b/tests/api/CapabilitiesTest.php
@@ -0,0 +1,58 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Tests\API;
+
+use PHPUnit\Framework\TestCase;
+
+class CapabilitiesTest extends TestCase {
+ protected $http;
+
+ protected function setUp() : void {
+ $this->http = new \GuzzleHttp\Client([
+ 'base_uri' => 'http://localhost:8080/',
+ 'auth' => ['test', 'test'],
+ 'http_errors' => false,
+ ]);
+ }
+
+ public function testCapabilities() {
+ $response = $this->http->request('GET', 'ocs/v2.php/cloud/capabilities', [
+ 'headers' => [
+ 'OCS-APIRequest' => 'true',
+ 'Accept' => 'application/json',
+ ]
+ ]);
+ $this->assertEquals(200, $response->getStatusCode(), 'Response status code');
+ $this->assertTrue(
+ $response->hasHeader('Content-Type'),
+ 'Response has content-type header'
+ );
+ $this->assertEquals(
+ 'application/json; charset=utf-8',
+ $response->getHeaderLine('Content-Type'),
+ 'Response content type'
+ );
+ $ocs = json_decode($response->getBody()->getContents());
+ $capabilities = $ocs->ocs->data->capabilities;
+ $this->assertObjectHasAttribute('notes', $capabilities, 'Nextcloud provides capabilities');
+ $notesCapability = $capabilities->notes;
+ $this->assertObjectHasAttribute('api_version', $notesCapability, 'Notes API-Version capability exists');
+ $apiVersions = $notesCapability->api_version;
+ $this->assertIsArray($apiVersions, 'Notes API-Version capability is array');
+ $this->assertNotEmpty($apiVersions, 'Notes API-Version capability array');
+ foreach ($apiVersions as $apiVersion) {
+ $this->assertStringMatchesFormat('%d.%d', $apiVersion, 'API Version format');
+ $v = $apiVersion === '0.2' ? '02' : intval($apiVersion);
+ $path = dirname(__FILE__).'/APIv'.$v.'Test.php';
+ $this->assertFileExists($path, 'Test for API v'.$apiVersion.' exists');
+ }
+ }
+
+ public function testInvalidVersion() {
+ $v = 7;
+ $response1 = $this->http->request('GET', 'index.php/apps/notes/api/v'.$v.'/notes');
+ $this->assertEquals(400, $response1->getStatusCode(), 'First response status code');
+ $response2 = $this->http->request('GET', 'index.php/apps/notes/api/v'.$v.'/notes/1');
+ $this->assertEquals(400, $response2->getStatusCode(), 'Second response status code');
+ }
+}
diff --git a/tests/api/CommonAPITest.php b/tests/api/CommonAPITest.php
new file mode 100644
index 00000000..5f69e02a
--- /dev/null
+++ b/tests/api/CommonAPITest.php
@@ -0,0 +1,217 @@
+<?php declare(strict_types=1);
+
+namespace OCA\Notes\Tests\API;
+
+abstract class CommonAPITest extends AbstractAPITest {
+
+ private $requiredAttributes = [
+ 'id' => 'integer',
+ 'content' => 'string',
+ 'title' => 'string',
+ 'category' => 'string',
+ 'modified' => 'integer',
+ 'favorite' => 'boolean',
+ ];
+
+ private $autotitle;
+
+ public function __construct(string $apiVersion, bool $autotitle) {
+ parent::__construct($apiVersion);
+ $this->autotitle = $autotitle;
+ }
+
+ public function testCheckForReferenceNotes() : array {
+ $response = $this->http->request('GET', 'notes');
+ $this->checkResponse($response, 'Get existing notes', 200);
+ $notes = json_decode($response->getBody()->getContents());
+ $this->assertNotEmpty($notes, 'List of notes');
+ foreach ($notes as $note) {
+ foreach ($this->requiredAttributes as $key => $type) {
+ $this->assertObjectHasAttribute($key, $note, 'Note has property '.$key);
+ $this->assertEquals($type, gettype($note->$key), 'Property type of '.$key);
+ }
+ }
+ return $notes;
+ }
+
+ /** @depends testCheckForReferenceNotes */
+ public function testGetNotesWithExclude(array $refNotes) : void {
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'exclude content',
+ '?exclude=content',
+ false,
+ ['content']
+ );
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'exclude content and category',
+ '?exclude=content,category',
+ false,
+ ['content','category']
+ );
+ }
+
+ /** @depends testCheckForReferenceNotes */
+ public function testGetNotesWithEtag(array $refNotes) : void {
+ $response1 = $this->http->request('GET', 'notes');
+ $this->checkResponse($response1, 'Initial response', 200);
+ $this->assertTrue($response1->hasHeader('ETag'), 'Initial response has ETag header');
+ $etag = $response1->getHeaderLine('ETag');
+ $this->assertRegExp('/^"[[:alnum:]]{32}"$/', $etag, 'ETag format');
+
+ // Test If-None-Match with ETag
+ $response2 = $this->http->request('GET', 'notes', [ 'headers' => [ 'If-None-Match' => $etag ] ]);
+ $this->checkResponse($response2, 'ETag response', 304);
+ $this->assertEquals('', $response2->getBody(), 'ETag response body');
+ }
+
+ /** @depends testCheckForReferenceNotes */
+ public function testGetNotesWithPruneBefore(array $refNotes) : void {
+ sleep(1); // wait for 'Last-Modified' to be >= Last-change + 1
+ $response1 = $this->http->request('GET', 'notes');
+ $this->checkResponse($response1, 'Initial response', 200);
+ $this->assertTrue($response1->hasHeader('Last-Modified'), 'Initial response has Last-Modified header');
+ $lastModified = $response1->getHeaderLine('Last-Modified');
+ $dt = \DateTime::createFromFormat(\DateTime::RFC2822, $lastModified);
+ $this->assertInstanceOf(\DateTime::class, $dt);
+
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'pruneBefore with Last-Modified',
+ '?pruneBefore='.$dt->getTimestamp(),
+ true
+ );
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'pruneBefore with 1',
+ '?pruneBefore=1',
+ false
+ );
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'pruneBefore with PHP_INT_MAX (32bit)',
+ '?pruneBefore=2147483647', // 2038-01-19 03:14:07
+ true
+ );
+ $this->checkGetReferenceNotes(
+ $refNotes,
+ 'pruneBefore with PHP_INT_MAX (64bit)',
+ '?pruneBefore=9223372036854775807',
+ true
+ );
+ }
+
+ /** @depends testCheckForReferenceNotes */
+ public function testCreateNotes(array $refNotes) : array {
+ $this->checkGetReferenceNotes($refNotes, 'Pre-condition');
+ $testNotes = [];
+ $testNotes[] = $this->createNote((object)[
+ 'title' => 'First *manual* title',
+ 'content' => '# *First* test/ note'.PHP_EOL.'This is some body content with some data.',
+ 'favorite' => true,
+ 'category' => 'Test/../New Category',
+ 'modified' => mktime(8, 14, 30, 10, 2, 2020),
+ ], (object)[
+ 'title' => $this->autotitle ? 'First test note' : 'First manual title',
+ 'category' => 'Test/New Category',
+ ]);
+ $testNotes[] = $this->createNote((object)[
+ 'content' => 'Note with Defaults'.PHP_EOL.'This is some body content with some data.',
+ ], (object)[
+ 'title' => $this->autotitle ? 'Note with Defaults' : 'New note', // waring: requires lang=C
+ 'favorite' => false,
+ 'category' => '',
+ 'modified' => time(),
+ ]);
+ $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'After creating notes');
+ return $testNotes;
+ }
+
+ /**
+ * @depends testCheckForReferenceNotes
+ * @depends testCreateNotes
+ */
+ public function testGetSingleNote(array $refNotes, array $testNotes) : void {
+ foreach ($testNotes as $testNote) {
+ $response = $this->http->request('GET', 'notes/'.$testNote->id);
+ $this->checkResponse($response, 'Get note '.$testNote->title, 200);
+ $note = json_decode($response->getBody()->getContents());
+ $this->checkReferenceNote($testNote, $note, 'Get single note');
+ }
+ // test non-existing note
+ $response = $this->http->request('GET', 'notes/1');
+ $this->assertEquals(404, $response->getStatusCode());
+ }
+
+
+ /**
+ * @depends testCheckForReferenceNotes
+ * @depends testCreateNotes
+ */
+ public function testUpdateNotes(array $refNotes, array $testNotes) : array {
+ $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'Pre-condition');
+ $note = $testNotes[0];
+ // test update note with all attributes
+ $this->updateNote($note, (object)[
+ 'title' => 'First *manual* edited title',
+ 'content' => '# *First* edited/ note'.PHP_EOL.'This is some body content with some data.',
+ 'favorite' => false,
+ 'category' => 'Test/Another Category',
+ 'modified' => mktime(11, 46, 23, 4, 3, 2020),
+ ], (object)[
+ 'title' => $this->autotitle ? 'First edited note' : 'First manual edited title',
+ ]);
+ // test update note with single attributes
+ $this->updateNote($note, (object)[
+ 'category' => 'Test/Third Category',
+ ], (object)[]);
+ // TODO test update category with read-only folder (target category)
+ $this->updateNote($note, (object)[
+ 'favorite' => true,
+ ], (object)[]);
+ $this->updateNote($note, (object)[
+ 'content' => '# First multi edited note'.PHP_EOL.'This is some body content with some data.',
+ ], (object)[
+ 'title' => $this->autotitle ? 'First multi edited note' : 'First manual edited title',
+ 'modified' => time(),
+ ]);
+ $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'After updating notes');
+ return $testNotes;
+ }
+
+ /**
+ * @depends testCheckForReferenceNotes
+ * @depends testUpdateNotes
+ */
+ public function testDeleteNotes(array $refNotes, array $testNotes) : void {
+ $this->checkGetReferenceNotes(array_merge($refNotes, $testNotes), 'Pre-condition');
+ foreach ($testNotes as $note) {
+ $response = $this->http->request('DELETE', 'notes/'.$note->id);
+ $this->checkResponse($response, 'Delete note '.$note->title, 200);
+ }
+ // test non-existing note
+ $response = $this->http->request('DELETE', 'notes/1');
+ $this->checkResponse($response, 'Delete non-existing note', 404);
+ $this->checkGetReferenceNotes($refNotes, 'After deletion');
+ }
+
+ public function testInsuficientStorage() {
+ $auth = ['quotatest', 'test'];
+ // get notes must still work
+ $response = $this->http->request('GET', 'notes', [ 'auth' => $auth ]);
+ $this->checkResponse($response, 'Get existing notes', 200);
+ $notes = json_decode($response->getBody()->getContents());
+ $this->assertNotEmpty($notes, 'List of notes');
+ $note = $notes[0]; // @phan-suppress-current-line PhanTypeArraySuspiciousNullable
+ $request = (object)[ 'content' => 'New test content' ];
+ // update will fail
+ $response1 = $this->http->request('PUT', 'notes/'.$note->id, [ 'auth' => $auth, 'json' => $request]);
+ $this->assertEquals(507, $response1->getStatusCode());
+ // craete will fail
+ $response2 = $this->http->request('POST', 'notes', [ 'auth' => $auth, 'json' => $request]);
+ $this->assertEquals(507, $response2->getStatusCode());
+ }
+
+ // TODO Test settings (switch to another notes folder)
+}