Merge pull request #10206 from nextcloud/feat/noid/replace-annotations-with-attributes

feat(developer): Replace annotations with attributes
This commit is contained in:
Joas Schilling
2023-04-25 17:15:25 +02:00
committed by GitHub
7 changed files with 123 additions and 184 deletions

View File

@@ -97,7 +97,7 @@ On the server side we need to register a callback that is executed once the requ
This route calls the controller **OCA\\notestutorial\\PageController->index()** method which is defined in **notestutorial/lib/Controller/PageController.php**. The controller returns a :doc:`template <../basics/front-end/templates>`, in this case **notestutorial/templates/main.php**:
.. note:: **@NoAdminRequired** and **@NoCSRFRequired** in the comments above the method turn off security checks, see `Authentication on Controllers <../basics/controllers.html#authentication>`__
.. note:: The ``#[NoAdminRequired]`` and ``#[NoCSRFRequired]`` attributes on the methods turn off security checks, see `Authentication on Controllers <../basics/controllers.html#authentication>`__
.. code-block:: php
@@ -105,6 +105,8 @@ This route calls the controller **OCA\\notestutorial\\PageController->index()**
namespace OCA\NotesTutorial\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller;
@@ -114,10 +116,8 @@ This route calls the controller **OCA\\notestutorial\\PageController->index()**
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function index() {
return new TemplateResponse('notestutorial', 'main');
}
@@ -133,6 +133,7 @@ Since the route which returns the initial HTML has been taken care of, the contr
use OCP\IRequest;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
class NoteController extends Controller {
@@ -140,56 +141,33 @@ Since the route which returns the initial HTML has been taken care of, the contr
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*/
#[NoAdminRequired]
public function index() {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function show(int $id) {
// empty for now
}
/**
* @NoAdminRequired
*
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function create(string $title, string $content) {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function update(int $id, string $title, string $content) {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function destroy(int $id) {
// empty for now
}
}
.. note:: The parameters are extracted from the request body and the URL using the controller method's variable names. Since PHP does not support type hints for primitive types such as ints and booleans, we need to add them as annotations in the comments. In order to type cast a parameter to an int, add **@param int $parameterName**
Now the controller methods need to be connected to the corresponding URLs in the **notestutorial/appinfo/routes.php** file:
.. code-block:: php
@@ -225,8 +203,8 @@ Database
Now that the routes are set up and connected the notes should be saved in the
database. To do that first create a :doc:`database migration <../basics/storage/migrations>`
by creating a file **notestutorial/lib/Migration/VersionXXYYZZDateYYYYMMDDHHSSAA.php**,
so for example **notestutorial/lib/Migration/Version000000Date20181013124731.php**""
by creating a file ``notestutorial/lib/Migration/VersionXYYYDateYYYYMMDDHHSSAA.php``,
so for example version 1.4.3 goes with ``notestutorial/lib/Migration/Version1004Date20181013124731.php``
.. code-block:: php
@@ -239,7 +217,7 @@ so for example **notestutorial/lib/Migration/Version000000Date20181013124731.php
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version1400Date20181013124731 extends SimpleMigrationStep {
class Version1004Date20181013124731 extends SimpleMigrationStep {
/**
* @param IOutput $output
@@ -281,7 +259,7 @@ To create the tables in the database, run the :ref:`migration <migration_consol
php ./occ migrations:execute <appId> <versionNumber>
Example: sudo -u www-data php ./occ migrations:execute photos 000000Date20201002183800
Example: sudo -u www-data php ./occ migrations:execute notestutorial 1004Date20201002183800
.. note:: To trigger the table creation/alteration when user updating the app, update the :doc:`version tag <info>` in **notestutorial/appinfo/info.xml** . migration will be executed when user reload page after app upgrade
@@ -400,6 +378,7 @@ You can pass in the mapper by adding it as a type hinted parameter. Nextcloud wi
use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
@@ -417,18 +396,12 @@ You can pass in the mapper by adding it as a type hinted parameter. Nextcloud wi
$this->userId = $userId;
}
/**
* @NoAdminRequired
*/
#[NoAdminRequired]
public function index(): DataResponse {
return new DataResponse($this->mapper->findAll($this->userId));
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function show(int $id): DataResponse {
try {
return new DataResponse($this->mapper->find($id, $this->userId));
@@ -437,12 +410,7 @@ You can pass in the mapper by adding it as a type hinted parameter. Nextcloud wi
}
}
/**
* @NoAdminRequired
*
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function create(string $title, string $content): DataResponse {
$note = new Note();
$note->setTitle($title);
@@ -451,13 +419,7 @@ You can pass in the mapper by adding it as a type hinted parameter. Nextcloud wi
return new DataResponse($this->mapper->insert($note));
}
/**
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function update(int $id, string $title, string $content): DataResponse {
try {
$note = $this->mapper->find($id, $this->userId);
@@ -469,11 +431,7 @@ You can pass in the mapper by adding it as a type hinted parameter. Nextcloud wi
return new DataResponse($this->mapper->update($note));
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function destroy(int $id): DataResponse {
try {
$note = $this->mapper->find($id, $this->userId);
@@ -645,6 +603,7 @@ Now we can wire up the trait and the service inside the **NoteController**:
namespace OCA\NotesTutorial\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
@@ -664,52 +623,31 @@ Now we can wire up the trait and the service inside the **NoteController**:
$this->userId = $userId;
}
/**
* @NoAdminRequired
*/
#[NoAdminRequired]
public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @NoAdminRequired
*
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function create(string $title, string $content) {
return $this->service->create($title, $content, $this->userId);
}
/**
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
#[NoAdminRequired]
public function update(int $id, string $title, string $content): DataResponse {
return $this->handleNotFound(function () use ($id, $title, $content): Note {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @NoAdminRequired
*
* @param int $id
*/
#[NoAdminRequired]
public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id): Note {
return $this->service->delete($id, $this->userId);
@@ -958,6 +896,9 @@ With that in mind create a new controller in **notestutorial/lib/Controller/Note
namespace OCA\NotesTutorial\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\ApiController;
@@ -977,63 +918,42 @@ With that in mind create a new controller in **notestutorial/lib/Controller/Note
$this->userId = $userId;
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
public function index() {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param int $id
*/
public function show($id) {
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
public function show(int $id) {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param string $title
* @param string $content
*/
public function create($title, $content) {
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
public function create(string $title, string $content) {
return $this->service->create($title, $content, $this->userId);
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
public function update($id, $title, $content) {
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
public function update(int $id, string $title, string $content) {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param int $id
*/
public function destroy($id) {
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
public function destroy(int $id) {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});

View File

@@ -732,16 +732,12 @@ In order to ease migration from OCS API routes to the App Framework, an addition
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\OCSController;
class ShareController extends OCSController {
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @CORS
*/
#[NoAdminRequired]
public function getShares(): DataResponse {
return new DataResponse([
//Your data here
@@ -811,30 +807,38 @@ By default every controller method enforces the maximum security, which is:
Most of the time though it makes sense to also allow normal users to access the page and the PageController->index() method should not check the CSRF token because it has not yet been sent to the client and because of that can't work.
To turn off checks the following *Annotations* can be added before the controller:
To turn off checks the following *Attributes* can be added before the controller:
* **@NoAdminRequired**: Also users that are not admins can access the page
* **@PublicPage**: Everyone can access the page without having to log in
* **@NoTwoFactorRequired**: A user can access the page before the two-factor challenge has been passed (use this wisely and only in two-factor auth apps, e.g. to allow setup during login)
* **@NoCSRFRequired**: Don't check the CSRF token (use this wisely since you might create a security hole; to understand what it does see `CSRF in the security section <../prologue/security.html#cross-site-request-forgery>`__)
* ``#[NoAdminRequired]``: Also users that are not admins can access the page
* ``#[PublicPage]``: Everyone can access the page without having to log in
* ``#[NoTwoFactorRequired]``: A user can access the page before the two-factor challenge has been passed (use this wisely and only in two-factor auth apps, e.g. to allow setup during login)
* ``#[NoCSRFRequired]``: Don't check the CSRF token (use this wisely since you might create a security hole; to understand what it does see `CSRF in the security section <../prologue/security.html#cross-site-request-forgery>`__)
.. note::
The attributes are only available in Nextcloud 27 or later. In older versions annotations with the same names exist:
* ``@NoAdminRequired`` instead of ``#[NoAdminRequired]``
* ``@PublicPage``` instead of ``#[PublicPage]``
* ``@NoTwoFactorRequired``` instead of ``#[NoTwoFactorRequired]``
* ``@NoCSRFRequired``` instead of ``#[NoCSRFRequired]``
A controller method that turns off all checks would look like this:
.. code-block:: php
:emphasize-lines: 6-7,10-11
<?php
namespace OCA\MyApp\Controller;
use OCP\IRequest;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
class PageController extends Controller {
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*/
#[NoCSRFRequired]
#[PublicPage]
public function freeForAll() {
}
@@ -896,7 +900,7 @@ To enable brute force protection the following *Attribute* can be added to the c
The attribute is only available in Nextcloud 27 or later. In older versions the ``@BruteForceProtection(action=string)`` annotation can be used, but that does not allow multiple assignments to a single controller method.
Then the **throttle()** method has to be called on the response in case of a violation. Doing so will increase the throttle counter and make following requests slower, until a slowness of roughly 30 seconds is reached and the controller returns a ``429 Too Many Requests`` status without further validating or executing the request.
Then the **throttle()** method has to be called on the response in case of a violation. Doing so will increase the throttle counter and make following requests slower, until a slowness of roughly 30 seconds is reached and the controller returns a ``429 Too Many Requests`` status without further processing the request.
A controller method that would implement brute-force protection with an action of "foobar" would look as following:

View File

@@ -147,35 +147,51 @@ and implement two additional methods.
Additionally, if your setting class needs to fetch data or send data to some admin-only
controllers, you will need to mark the methods in the controller as accessible by the
setting with annotations.
setting with attribute.
.. note::
The attribute is only available in Nextcloud 27 or later. In older versions the ``@AuthorizedAdminSetting(settings=OCA\NotesTutorial\Settings\NotesAdmin)`` annotation can be used.
.. code-block:: php
:emphasize-lines: 8
<?php
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
class NotesSettingsController extends Controller {
/**
* Save settings
* @PasswordConfirmationRequired
* @AuthorizedAdminSetting(settings=OCA\NotesTutorial\Settings\NotesAdmin)
*/
public function saveSettings($mySetting) {
....
}
...
}
If you have several classes that implement `IDelegatedSettings` for a function. You must add them in the key "settings" and they must seperate with semi-colons.
.. code-block:: php
<?php
class NotesSettingsController extends Controller {
/**
* Save settings
* @PasswordConfirmationRequired
* @AuthorizedAdminSetting(settings=OCA\NotesTutorial\Settings\NotesAdmin;OCA\NotesTutorial\Settings\NotesSubAdmin)
*/
#[PasswordConfirmationRequired]
#[AuthorizedAdminSetting(settings: 'OCA\NotesTutorial\Settings\NotesAdmin')]
public function saveSettings($mySetting) {
....
}
...
}
If you have several ``IDelegatedSettings`` classes that are needed for a function, simply add the annotation multiple times.
them in the key "settings" and they must seperate with semi-colons.
.. note::
If you use the deprecated annotation specify the classes separated by semicolons:
``@AuthorizedAdminSetting(settings=OCA\NotesTutorial\Settings\NotesAdmin;OCA\NotesTutorial\Settings\NotesSubAdmin)``
.. code-block:: php
:emphasize-lines: 8-9
<?php
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
class NotesSettingsController extends Controller {
/**
* Save settings
*/
#[PasswordConfirmationRequired]
#[AuthorizedAdminSetting(settings: 'OCA\NotesTutorial\Settings\NotesAdmin')]
#[AuthorizedAdminSetting(settings: 'OCA\NotesTutorial\Settings\NotesSubAdmin')]
public function saveSettings($mySetting) {
....
}

View File

@@ -90,7 +90,7 @@ This package provides a modified version of `moment.js <https://momentjs.com/>`_
``@nextcloud/password-confirmation``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This package makes it possible to ask a user for confirmation on actions that have a `@PasswordConfirmationRequired` set on the controller method. Use it for critical actions. Documentation: https://nextcloud.github.io/nextcloud-password-confirmation/
This package makes it possible to ask a user for confirmation on actions that have the ``#[PasswordConfirmationRequired]`` attribute or ``@PasswordConfirmationRequired`` annotation set on the controller method. Use it for critical actions. Documentation: https://nextcloud.github.io/nextcloud-password-confirmation/
``@nextcloud/paths``
^^^^^^^^^^^^^^^^^^^^

View File

@@ -46,6 +46,7 @@ As said the PublicShareController is a very basic controller. You need to implem
namespace OCA\Share_Test\Controller;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\PublicShareController;
class PublicAPIController extends PublicShareController {
@@ -75,9 +76,8 @@ As said the PublicShareController is a very basic controller. You need to implem
/**
* Your normal controller function. The following annotation will allow guests
* to open the page as well
*
* @PublicPage
*/
#[PublicPage]
public function get() {
// Work your magic
}
@@ -105,6 +105,7 @@ you also implement the ``verifyPassword`` and ``showShare`` functions.
namespace OCA\Share_Test\Controller;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Http\Attribute\PublicPage;
class PublicDisplayController extends AuthPublicShareController {
/**
@@ -144,9 +145,8 @@ you also implement the ``verifyPassword`` and ``showShare`` functions.
/**
* Your normal controller function. The following annotation will allow guests
* to open the page as well
*
* @PublicPage
*/
#[PublicPage]
public function get() {
// Work your magic
}

View File

@@ -13,8 +13,9 @@ Offering a RESTful API is not different from creating a :doc:`route <../basics/r
<?php
namespace OCA\MyApp\Controller;
use \OCP\AppFramework\ApiController;
use \OCP\IRequest;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\IRequest;
class AuthorApiController extends ApiController {
@@ -22,9 +23,7 @@ Offering a RESTful API is not different from creating a :doc:`route <../basics/r
parent::__construct($appName, $request);
}
/**
* @CORS
*/
#[CORS]
public function index() {
}
@@ -38,9 +37,9 @@ CORS also needs a separate URL for the preflighted **OPTIONS** request that can
<?php
// appinfo/routes.php
array(
'name' => 'author_api#preflighted_cors',
'url' => '/api/1.0/{path}',
'verb' => 'OPTIONS',
'name' => 'author_api#preflighted_cors',
'url' => '/api/1.0/{path}',
'verb' => 'OPTIONS',
'requirements' => array('path' => '.+')
)
@@ -72,8 +71,8 @@ To add an additional method or header or allow less headers, simply pass additio
public function __construct($appName, IRequest $request) {
parent::__construct(
$appName,
$request,
$appName,
$request,
'PUT, POST, GET, DELETE, PATCH',
'Authorization, Content-Type, Accept',
1728000);

View File

@@ -227,7 +227,7 @@ To prevent CSRF in an app, be sure to call the following method at the top of al
<?php
OCP\JSON::callCheck();
If you are using the App Framework, every controller method is automatically checked for CSRF unless you explicitly exclude it by setting the @NoCSRFRequired annotation before the controller method, see :doc:`../basics/controllers`
If you are using the App Framework, every controller method is automatically checked for CSRF unless you explicitly exclude it by setting the ``#[NoCSRFRequired]`` attribute or ``@NoCSRFRequired`` annotation before the controller method, see :doc:`../basics/controllers`
Unvalidated redirects
---------------------