maintenance and new API v1

This commit is contained in:
korelstar 2020-04-12 19:26:57 +02:00
parent 02fdcee377
commit ffe6a02947
27 changed files with 1092 additions and 1048 deletions

View File

@ -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

View File

@ -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',
'name' => 'notes#updateProperty',
'url' => '/notes/{id}/{property}',
'verb' => 'PUT',
'requirements' => ['id' => '\d+'],
],
[
'name' => 'notes#title',
'url' => '/notes/{id}/title',
'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',
@ -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'],
]];

View File

@ -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);
}

View File

@ -9,7 +9,7 @@ class Capabilities implements ICapability {
public function getCapabilities() {
return [
'notes' => [
'api_version' => [ '0.2' ],
'api_version' => Application::$API_VERSIONS,
],
];
}

View File

@ -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);
}
}
}

43
lib/Controller/Helper.php Normal file
View File

@ -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;
}
}

View File

@ -1,127 +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);
}
}
$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);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @param int $id
* @param string $exclude
* @return DataResponse
*/
public function get($id, $exclude = '') {
try {
public function index(string $exclude = '', int $pruneBefore = 0) : DataResponse {
return $this->helper->handleErrorResponse(function () use ($exclude, $pruneBefore) {
$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);
}
$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);
}
return (new DataResponse($notesData))
->setLastModified($now)
->setETag($etag);
});
}
@ -129,86 +76,148 @@ 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 get(int $id, string $exclude = '') : DataResponse {
return $this->helper->handleErrorResponse(function () use ($id, $exclude) {
$exclude = explode(',', $exclude);
$note = $this->service->get($this->getUID(), $id);
return $note->getData($exclude);
});
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
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);
}
}
/**
* @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);
}
}
/**
* Updates a note, used by create and update
* @param int $id
* @param string|null $content
* @param int $modified
* @param boolean|null $favorite
* @return Note
*/
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);
}
return $note->getData();
});
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @param int $id
* @return DataResponse
* @deprecated this was used in API v0.2 only, use #create() instead
*/
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 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
*/
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();
});
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
* @deprecated this was used in API v0.2 only, use #update() instead
*/
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
*/
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);
});
}
}

View File

@ -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,108 +50,115 @@ class NotesController extends Controller {
/**
* @NoAdminRequired
*/
public function index() {
$settings = $this->settingsService->getAll($this->userId);
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, 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();
$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,
]);
return [
'notes' => $notesData,
'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)
);
public function get(int $id) : DataResponse {
return $this->helper->handleErrorResponse(function () use ($id) {
$note = $this->notesService->get($this->userId, $id);
$note = $this->notesService->get($id, $this->userId);
return new DataResponse($note);
}
/**
* @NoAdminRequired
*
* @param string $content
*/
public function create($content = '', $category = null) {
try {
$note = $this->notesService->create($this->userId);
$note = $this->notesService->update(
$note->getId(),
$content,
// 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();
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
*/
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();
}
} 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);
});
}
@ -172,67 +166,83 @@ class NotesController extends Controller {
* @NoAdminRequired
*/
public function update(int $id, string $content, bool $autotitle) : DataResponse {
try {
return $this->helper->handleErrorResponse(function () use ($id, $content, $autotitle) {
$note = $this->notesService->get($this->userId, $id);
$note->setContent($content);
if ($autotitle) {
$note = $this->notesService->update($id, $content, $this->userId);
} else {
$note = $this->notesService->setContent($this->userId, $id, $content);
$title = $this->notesService->getTitleFromContent($content);
$note->setTitle($title);
}
return new DataResponse($note);
} catch (InsufficientStorageException $e) {
return new DataResponse([], Http::STATUS_INSUFFICIENT_STORAGE);
}
}
/**
* @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
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 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
* @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
}
/**
* @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 [];
});
}
}

View File

@ -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,

View File

@ -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

View File

@ -2,6 +2,8 @@
namespace OCA\Notes\Db;
use OCA\Notes\Service\Note;
use OCP\AppFramework\Db\Entity;
/**

View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
namespace OCA\Notes\Service;

View File

@ -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());

141
lib/Service/Note.php Normal file
View File

@ -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);
}
}
}

View File

@ -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 {
}

View File

@ -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);
public function getL10N() : IL10N {
return $this->l10n;
}
// 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 getLogger() : ILogger {
return $this->logger;
}
// assemble new file path
$newFilePath = $basePath . '/' . $this->generateFileName($folder, $title, $fileSuffix, $id);
// if the current path is not the new path, the file has to be renamed
if ($currentFilePath !== $newFilePath) {
$file->move($newFilePath);
}
if ($currentBasePath !== $basePath) {
$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);
}
}

View File

@ -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 {
}

View File

@ -1,137 +1,61 @@
<?php
<?php declare(strict_types=1);
namespace OCA\Notes\Service;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\File;
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
*/
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
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,137 +65,46 @@ 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);
$file = $this->getFileById($notesFolder, $id);
$title = $this->noteUtil->getSafeTitleFromContent($content===null ? $file->getContent() : $content);
// rename/move file with respect to title/category
// this can fail if access rights are not sufficient or category name is illegal
try {
$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]);
}
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();
}
$this->noteUtil->moveNote($notesFolder, $file, $title, $category);
return $this->getNote($file, $notesFolder, $this->getTags($id));
}
/**
* 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
*/
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);
}
$note = $this->get($id, $userId, true);
}
return $note->getFavorite();
}
/**
* 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
*/
public function delete(int $id, string $userId) {
$notesFolder = $this->getFolderForUser($userId);
public function delete(string $userId, int $id) {
$notesFolder = $this->getNotesFolder($userId);
$file = $this->getFileById($notesFolder, $id);
$parent = $file->getParent();
$file->delete();
$this->noteUtil->deleteEmptyFolder($notesFolder, $parent);
$this->noteUtil->deleteEmptyFolder($parent, $notesFolder);
}
/**
* @param Folder $folder
* @param int $id
* @throws NoteDoesNotExistException
* @return \OCP\Files\File
*/
private function getFileById(Folder $folder, int $id) : File {
$file = $folder->getById($id);
if (count($file) <= 0 || !($file[0] instanceof File) || !$this->noteUtil->isNote($file[0])) {
throw new NoteDoesNotExistException();
}
return $file[0];
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);
}
/**
* @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');
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) {
@ -279,4 +112,43 @@ class NotesService {
}
return $folder;
}
/**
* gather note files in given directory and all subdirectories
*/
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;
}
}
return $files;
}
/**
* test if file is a note
*/
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);
}
/**
* @throws NoteDoesNotExistException
*/
private static function getFileById(Folder $folder, int $id) : File {
$file = $folder->getById($id);
if (count($file) <= 0 || !($file[0] instanceof File) || !self::isNote($file[0])) {
throw new NoteDoesNotExistException();
}
return $file[0];
}
}

View File

@ -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};

View File

@ -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);
}
}
}

View File

@ -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)
})

View File

@ -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)
}

10
tests/api/APIv1Test.php Normal file
View File

@ -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);
}
}

View File

@ -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;

View File

@ -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');
}
}

217
tests/api/CommonAPITest.php Normal file
View File

@ -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)
}