From e6c1981637a196bf596a1427e3d8434cad9e4439 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 25 Apr 2023 14:40:27 +0200 Subject: [PATCH] feat(developer): Replace annotations with attributes Signed-off-by: Joas Schilling --- developer_manual/app_development/tutorial.rst | 178 +++++------------- developer_manual/basics/controllers.rst | 40 ++-- developer_manual/basics/setting.rst | 36 +++- .../digging_deeper/javascript-apis.rst | 2 +- .../digging_deeper/publicpage.rst | 8 +- developer_manual/digging_deeper/rest_apis.rst | 19 +- developer_manual/prologue/security.rst | 2 +- 7 files changed, 112 insertions(+), 173 deletions(-) diff --git a/developer_manual/app_development/tutorial.rst b/developer_manual/app_development/tutorial.rst index 69d6206d496..65ac8f9de37 100644 --- a/developer_manual/app_development/tutorial.rst +++ b/developer_manual/app_development/tutorial.rst @@ -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 - 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 ` 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); }); diff --git a/developer_manual/basics/controllers.rst b/developer_manual/basics/controllers.rst index c33bb8b9578..e474c914a7f 100644 --- a/developer_manual/basics/controllers.rst +++ b/developer_manual/basics/controllers.rst @@ -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>`__) + +.. note:: -* **@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>`__) + 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 `_ ``@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`` ^^^^^^^^^^^^^^^^^^^^ diff --git a/developer_manual/digging_deeper/publicpage.rst b/developer_manual/digging_deeper/publicpage.rst index 80c09eccb62..9974301e3c5 100644 --- a/developer_manual/digging_deeper/publicpage.rst +++ b/developer_manual/digging_deeper/publicpage.rst @@ -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 } diff --git a/developer_manual/digging_deeper/rest_apis.rst b/developer_manual/digging_deeper/rest_apis.rst index c85f331bde6..8a634f24027 100644 --- a/developer_manual/digging_deeper/rest_apis.rst +++ b/developer_manual/digging_deeper/rest_apis.rst @@ -13,8 +13,9 @@ Offering a RESTful API is not different from creating a :doc:`route <../basics/r '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); diff --git a/developer_manual/prologue/security.rst b/developer_manual/prologue/security.rst index 49c9cc7a6fb..23e838115bf 100644 --- a/developer_manual/prologue/security.rst +++ b/developer_manual/prologue/security.rst @@ -227,7 +227,7 @@ To prevent CSRF in an app, be sure to call the following method at the top of al