maintenance and new API v1
This commit is contained in:
parent
02fdcee377
commit
ffe6a02947
2
Makefile
2
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
|
||||
|
|
|
@ -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'],
|
||||
]];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ class Capabilities implements ICapability {
|
|||
public function getCapabilities() {
|
||||
return [
|
||||
'notes' => [
|
||||
'api_version' => [ '0.2' ],
|
||||
'api_version' => Application::$API_VERSIONS,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace OCA\Notes\Db;
|
||||
|
||||
use OCA\Notes\Service\Note;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
|
|
123
lib/Db/Note.php
123
lib/Db/Note.php
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace OCA\Notes\Service;
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue