Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ app_name=notes
project_dir=$(CURDIR)/../$(app_name)
build_dir=$(CURDIR)/build/artifacts
cert_dir=$(HOME)/.nextcloud/certificates
php_dirs=appinfo/ lib/ tests/api/


all: dev-setup build
Expand Down Expand Up @@ -99,7 +100,7 @@ lint-phpfast: lint-php-lint lint-php-ncversion lint-php-cs-fixer lint-php-phpcs

lint-php-lint:
# Check PHP syntax errors
@! find lib/ -name "*.php" | xargs -I{} php -l '{}' | grep -v "No syntax errors detected"
@! find $(php_dirs) -name "*.php" | xargs -I{} php -l '{}' | grep -v "No syntax errors detected"

lint-php-ncversion:
# Check min-version consistency
Expand All @@ -111,7 +112,7 @@ lint-php-phan:

lint-php-phpcs:
# PHP CodeSniffer
vendor/bin/phpcs --standard=tests/phpcs.xml appinfo/ lib/ tests/api/ --report=checkstyle | vendor/bin/cs2pr --graceful-warnings --colorize
vendor/bin/phpcs --standard=tests/phpcs.xml $(php_dirs) --report=checkstyle | vendor/bin/cs2pr --graceful-warnings --colorize

lint-php-cs-fixer:
# PHP Coding Standards Fixer (with Nextcloud coding standards)
Expand All @@ -136,7 +137,7 @@ lint-xml:
lint-fix: lint-php-fix lint-js-fix lint-css-fix

lint-php-fix:
vendor/bin/phpcbf --standard=tests/phpcs.xml appinfo/ lib/ tests/api/
vendor/bin/phpcbf --standard=tests/phpcs.xml $(php_dirs)
vendor/bin/php-cs-fixer fix

lint-js-fix:
Expand Down
16 changes: 16 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@
'id' => '\d+',
],
],
[
'name' => 'notes_api#setSettings',
'url' => '/api/{apiVersion}/settings',
'verb' => 'PUT',
'requirements' => [
'apiVersion' => '(v1)',
],
],
[
'name' => 'notes_api#getSettings',
'url' => '/api/{apiVersion}/settings',
'verb' => 'GET',
'requirements' => [
'apiVersion' => '(v1)',
],
],
[
'name' => 'notes_api#fail',
'url' => '/api/{catchAll}',
Expand Down
66 changes: 66 additions & 0 deletions docs/api/v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ The app and the API is mainly about notes. So, let's have a look about the attri
| `modified` | integer | Unix timestamp for the last modified date/time of the note. If not provided on note creation or content update, the current time is used. | 1.0 |


## Settings

Since API version 1.2, it is possible to change app settings using the API. The following settings attributes exist:

| Attribute | Type | Description | since API version |
|:----------|:-----|:------------|:------------------|
| `notesPath` | string | Path to the folder, where note's files are stored in Nextcloud. The path must be relative to the user folder. Default is the localized string `Notes`. | 1.2 |
| `fileSuffix` | string | Newly created note's files will have this file suffix. Only the values `.txt` or `.md` are allowed. Default is `.txt`. | 1.2 |


## Endpoints and Operations

The base URL for all calls is:
Expand Down Expand Up @@ -180,3 +190,59 @@ No valid authentication credentials supplied.
##### 404 Not Found
Note not found.
</details>


### Get settings (`GET /settings`)
<details><summary>Details</summary>

*(since API v1.2)*

#### Request parameters
None.

#### Response
##### 200 OK
- **Body**: user's app settings (see section [Settings](#settings)), example:
```js
{
"notesPath": "Notes",
"fileSuffix": ".txt"
}
```

##### 400 Bad Request
Endpoint not supported by installed notes app version (requires API version 1.2).

##### 401 Unauthorized
No valid authentication credentials supplied.
</details>


### Change settings (`PUT /settings`)
<details><summary>Details</summary>

*(since API v1.2)*

#### Request parameters
- **Body**: some or all settings attributes (see section [Settings](#settings)).
Omitted settings attributes are not changed.
Empty values are replaced by the settings attribute's default value.
All values are sanitized (e.g. prevent path traversal attacks, check allowed suffixes), so the result can differ from the request (the request will still succeed).
The client may show an information to the user if the response differs from the request.
Example:
```js
{
"fileSuffix": ".md"
}
```

#### Response
##### 200 OK
- **Body**: user's app settings after validation (see section [Settings](#settings)), example see section [Get settings](#get-settings-get-settings).

##### 400 Bad Request
Endpoint not supported by installed notes app version (requires API version 1.2).

##### 401 Unauthorized
No valid authentication credentials supplied.
</details>
27 changes: 27 additions & 0 deletions lib/Controller/NotesApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use OCA\Notes\Service\NotesService;
use OCA\Notes\Service\MetaService;
use OCA\Notes\Service\SettingsService;

use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
Expand All @@ -18,6 +19,8 @@ class NotesApiController extends ApiController {
private $service;
/** @var MetaService */
private $metaService;
/** @var SettingsService */
private $settingsService;
/** @var Helper */
private $helper;

Expand All @@ -26,11 +29,13 @@ public function __construct(
IRequest $request,
NotesService $service,
MetaService $metaService,
SettingsService $settingsService,
Helper $helper
) {
parent::__construct($AppName, $request);
$this->service = $service;
$this->metaService = $metaService;
$this->settingsService = $settingsService;
$this->helper = $helper;
}

Expand Down Expand Up @@ -207,6 +212,28 @@ public function destroy(int $id) : JSONResponse {
});
}

/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function setSettings() {
return $this->helper->handleErrorResponse(function () {
$this->settingsService->set($this->helper->getUID(), $this->request->getParams());
return $this->getSettings();
});
}

/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function getSettings() {
return $this->helper->handleErrorResponse(function () {
return $this->settingsService->getAll($this->helper->getUID());
});
}
/**
* @NoAdminRequired
* @NoCSRFRequired
Expand Down
62 changes: 51 additions & 11 deletions lib/Service/SettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class SettingsService {
private $l10n;
private $root;

/* Default values */
private $defaults;
/* Allowed attributes */
private $attrs;

public function __construct(
IConfig $config,
Expand All @@ -26,11 +26,35 @@ public function __construct(
$this->config = $config;
$this->l10n = $l10n;
$this->root = $root;
$this->defaults = [
'fileSuffix' => '.txt',
'notesPath' => function (string $uid) {
return $this->getDefaultNotesPath($uid);
},
$this->attrs = [
'fileSuffix' => [
'default' => '.txt',
'validate' => function ($value) {
if (in_array($value, [ '.txt', '.md' ])) {
return $value;
} else {
return '.txt';
}
},
],
'notesPath' => [
'default' => function (string $uid) {
return $this->getDefaultNotesPath($uid);
},
'validate' => function ($value) {
$value = str_replace([ '/', '\\' ], DIRECTORY_SEPARATOR, $value);
$parts = explode(DIRECTORY_SEPARATOR, $value);
$path = [];
foreach ($parts as $part) {
if ($part === '..') {
array_pop($path);
} elseif (strlen($part) && $part !== '.') {
array_push($path, $part);
}
}
return implode(DIRECTORY_SEPARATOR, $path);
},
],
];
}

Expand All @@ -48,27 +72,43 @@ private function getDefaultNotesPath(string $uid) : string {
* @throws \OCP\PreConditionNotMetException
*/
public function set(string $uid, array $settings) : void {
// load existing values for missing attributes
$oldSettings = $this->getSettingsFromDB($uid);
foreach ($oldSettings as $name => $value) {
if (!array_key_exists($name, $settings)) {
$settings[$name] = $value;
}
}
// remove illegal, empty and default settings
foreach ($settings as $name => $value) {
if (!array_key_exists($name, $this->defaults)
if ($value !== null && array_key_exists($name, $this->attrs)) {
$settings[$name] = $value = $this->attrs[$name]['validate']($value);
}
if (!array_key_exists($name, $this->attrs)
|| empty($value)
|| $value === $this->defaults[$name]
|| $value === $this->attrs[$name]['default']
) {
unset($settings[$name]);
}
}
$this->config->setUserValue($uid, Application::APP_ID, 'settings', json_encode($settings));
}

public function getAll(string $uid) : \stdClass {
private function getSettingsFromDB(string $uid) : \stdClass {
$settings = json_decode($this->config->getUserValue($uid, Application::APP_ID, 'settings'));
if (!is_object($settings)) {
$settings = new \stdClass();
}
return $settings;
}

public function getAll(string $uid) : \stdClass {
$settings = $this->getSettingsFromDB($uid);
// use default for empty settings
$toBeSaved = false;
foreach ($this->defaults as $name => $defaultValue) {
foreach ($this->attrs as $name => $attr) {
if (!property_exists($settings, $name) || empty($settings->{$name})) {
$defaultValue = $attr['default'];
if (is_callable($defaultValue)) {
$settings->{$name} = $defaultValue($uid);
$toBeSaved = true;
Expand Down
1 change: 1 addition & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export default {
this.$router.push('/')
}
store.commit('removeAllNotes')
store.commit('clearSyncCache')
this.loading.notes = true
this.loadNotes()
},
Expand Down
5 changes: 5 additions & 0 deletions src/store/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const mutations = {
}
},

clearSyncCache(state) {
state.etag = null
state.lastModified = 0
},

setSyncActive(state, active) {
state.active = active
},
Expand Down
37 changes: 37 additions & 0 deletions tests/api/AbstractAPITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,41 @@ protected function updateNote(\stdClass &$note, \stdClass $request, \stdClass $e
}
$this->checkReferenceNote($note, $responseNote, 'Updated note');
}

protected function checkObject(
\stdClass $ref,
\stdClass $obj,
string $messagePrefix
) : void {
foreach (get_object_vars($ref) as $key => $val) {
$this->assertObjectHasAttribute(
$key,
$obj,
$messagePrefix.': Object has property '.$key
);
$this->assertEquals(
$ref->$key,
$obj->$key,
$messagePrefix.': Property '.$key
);
}
}

protected function updateSettings(
\stdClass &$settings,
\stdClass $request,
\stdClass $expected,
string $messagePrefix
) {
$response = $this->http->request('PUT', 'settings', [ 'json' => $request ]);
$this->checkResponse($response, $messagePrefix, 200);
$responseSettings = json_decode($response->getBody()->getContents());
foreach (get_object_vars($request) as $key => $val) {
$settings->$key = $val;
}
foreach (get_object_vars($expected) as $key => $val) {
$settings->$key = $val;
}
$this->checkObject($settings, $responseSettings, $messagePrefix);
}
}
Loading