diff --git a/composer.json b/composer.json index 25d1a31..3fd33c6 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "wp-coding-standards/wpcs": "^3.1", "squizlabs/php_codesniffer": "^3.10", "phpcompatibility/phpcompatibility-wp": "^2.1", - "overtrue/phplint": "^3.4" + "overtrue/phplint": "^3.4", + "roots/wordpress-no-content": "^6.6" }, "scripts": { "build": [ diff --git a/composer.lock b/composer.lock index 97a5424..d8dcc23 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8528064048016873b62d9c97f6209903", + "content-hash": "c2b3105098b6e486ef25a00d3cb5cf75", "packages": [ { "name": "datakit/sdk", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/UseDataKit/SDK.git", - "reference": "27fea2c3cbce26370bf842a62a4ae7b26b15df49" + "reference": "dc89bc13b1e6a54377709f8397f3b9b1926123cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/UseDataKit/SDK/zipball/27fea2c3cbce26370bf842a62a4ae7b26b15df49", - "reference": "27fea2c3cbce26370bf842a62a4ae7b26b15df49", + "url": "https://api.github.com/repos/UseDataKit/SDK/zipball/dc89bc13b1e6a54377709f8397f3b9b1926123cb", + "reference": "dc89bc13b1e6a54377709f8397f3b9b1926123cb", "shasum": "" }, "require": { @@ -90,7 +90,7 @@ "source": "https://github.com/UseDataKit/SDK/tree/main", "issues": "https://github.com/UseDataKit/SDK/issues" }, - "time": "2024-09-11T11:33:34+00:00" + "time": "2024-09-16T09:56:21+00:00" } ], "packages-dev": [ @@ -1499,6 +1499,76 @@ }, "time": "2021-11-05T16:50:12+00:00" }, + { + "name": "roots/wordpress-no-content", + "version": "6.6.2", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress.git", + "reference": "6.6.2" + }, + "dist": { + "type": "zip", + "url": "https://downloads.wordpress.org/release/wordpress-6.6.2-no-content.zip", + "shasum": "b496b8a9bb3c6d20a781344cbe3e189f7fd83871" + }, + "require": { + "php": ">= 7.2.24" + }, + "provide": { + "wordpress/core-implementation": "6.6.2" + }, + "suggest": { + "ext-curl": "Performs remote request operations.", + "ext-dom": "Used to validate Text Widget content and to automatically configuring IIS7+.", + "ext-exif": "Works with metadata stored in images.", + "ext-fileinfo": "Used to detect mimetype of file uploads.", + "ext-hash": "Used for hashing, including passwords and update packages.", + "ext-imagick": "Provides better image quality for media uploads.", + "ext-json": "Used for communications with other servers.", + "ext-libsodium": "Validates Signatures and provides securely random bytes.", + "ext-mbstring": "Used to properly handle UTF8 text.", + "ext-mysqli": "Connects to MySQL for database interactions.", + "ext-openssl": "Permits SSL-based connections to other hosts.", + "ext-pcre": "Increases performance of pattern matching in code searches.", + "ext-xml": "Used for XML parsing, such as from a third-party site.", + "ext-zip": "Used for decompressing Plugins, Themes, and WordPress update packages." + }, + "type": "wordpress-core", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress Community", + "homepage": "https://wordpress.org/about/" + } + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org/", + "keywords": [ + "blog", + "cms", + "wordpress" + ], + "support": { + "docs": "https://developer.wordpress.org/", + "forum": "https://wordpress.org/support/", + "irc": "irc://irc.freenode.net/wordpress", + "issues": "https://core.trac.wordpress.org/", + "rss": "https://wordpress.org/news/feed/", + "source": "https://core.trac.wordpress.org/browser", + "wiki": "https://codex.wordpress.org/" + }, + "funding": [ + { + "url": "https://wordpressfoundation.org/donate/", + "type": "other" + } + ], + "time": "2024-09-10T15:25:31+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.3", diff --git a/src/AccessControl/WordPressAccessController.php b/src/AccessControl/WordPressAccessController.php new file mode 100644 index 0000000..4ff137f --- /dev/null +++ b/src/AccessControl/WordPressAccessController.php @@ -0,0 +1,85 @@ +user = $user; + $this->previous = new ReadOnlyAccessController(); + } + + /** + * {@inheritDoc} + * + * @since $ver$ + */ + public function can( Capability $capability ): bool { + $can = $this->previous->can( $capability ); + + if ( $this->user && $this->user->exists() ) { + $can = $this->user->has_cap( 'administrator' ); + } + + return $this->filter_result( $can, $capability ); + } + + /** + * Applies filter on the result. + * + * @since $ver$ + * + * @param bool $can The result to return. + * @param Capability $capability The capability. + * + * @return bool The filtered result. + */ + private function filter_result( bool $can, Capability $capability ): bool { + /** + * Modifies the capability check. + * + * @filter `datakit/access-control/can` + * + * @since $ver$ + * + * @param bool $can Whether the user can. + * @param Capability $capability Whether the user can. + * @param ?WP_User $user The WP_User. + */ + return (bool) apply_filters( 'datakit/access-control/can', $can, $capability, $this->user ); + } +} diff --git a/src/Component/DataViewShortcode.php b/src/Component/DataViewShortcode.php index aeff2b0..336253c 100644 --- a/src/Component/DataViewShortcode.php +++ b/src/Component/DataViewShortcode.php @@ -2,6 +2,8 @@ namespace DataKit\Plugin\Component; +use DataKit\DataViews\AccessControl\AccessController; +use DataKit\DataViews\AccessControl\Capability; use DataKit\DataViews\DataView\DataViewRepository; use DataKit\DataViews\DataViewException; use DataKit\Plugin\Rest\Router; @@ -48,13 +50,23 @@ final class DataViewShortcode { */ private array $rendered = []; + /** + * The Access Controller. + * + * @since $ver$ + * + * @var AccessController + */ + private AccessController $access_controller; + /** * Creates the shortcode instance. * * @since $ver$ */ - private function __construct( DataViewRepository $data_view_repository ) { + private function __construct( DataViewRepository $data_view_repository, AccessController $access_controller ) { $this->data_view_repository = $data_view_repository; + $this->access_controller = $access_controller; add_shortcode( self::SHORTCODE, [ $this, 'render_shortcode' ] ); } @@ -81,13 +93,17 @@ public function render_shortcode( array $attributes ): string { // Only add data set once per ID. if ( ! in_array( $id, $this->rendered, true ) ) { - wp_enqueue_script( 'datakit/dataview' ); - wp_enqueue_style( 'datakit/dataview' ); - try { $dataview = $this->data_view_repository->get( $id ); - $js = sprintf( 'datakit_dataviews["%s"] = %s;', esc_attr( $id ), $dataview->to_js() ); - $js = str_replace( '{REST_ENDPOINT}', Router::get_url(), $js ); + if ( ! $this->access_controller->can( new Capability\ViewDataView( $dataview ) ) ) { + return ''; + } + + wp_enqueue_script( 'datakit/dataview' ); + wp_enqueue_style( 'datakit/dataview' ); + + $js = sprintf( 'datakit_dataviews["%s"] = %s;', esc_attr( $id ), $dataview->to_js() ); + $js = str_replace( '{REST_ENDPOINT}', Router::get_url(), $js ); } catch ( DataViewException $e ) { return ''; } @@ -119,9 +135,12 @@ function () use ( $js ) { * * @return self The singleton. */ - public static function get_instance( DataViewRepository $data_view_repository ): self { + public static function get_instance( + DataViewRepository $data_view_repository, + AccessController $access_controller + ): self { if ( ! isset( self::$instance ) ) { - self::$instance = new self( $data_view_repository ); + self::$instance = new self( $data_view_repository, $access_controller ); } return self::$instance; diff --git a/src/DataKitPlugin.php b/src/DataKitPlugin.php index 8cce863..32a0b31 100644 --- a/src/DataKitPlugin.php +++ b/src/DataKitPlugin.php @@ -2,9 +2,11 @@ namespace DataKit\Plugin; +use DataKit\DataViews\AccessControl\AccessControlManager; use DataKit\DataViews\DataView\DataView; use DataKit\DataViews\DataView\DataViewRepository; use DataKit\DataViews\DataView\Pagination; +use DataKit\Plugin\AccessControl\WordPressAccessController; use DataKit\Plugin\Rest\Router; use DataKit\Plugin\Component\DataViewShortcode; @@ -41,8 +43,12 @@ final class DataKitPlugin { */ private function __construct( DataViewRepository $data_view_repository ) { $this->data_view_repository = $data_view_repository; + + do_action( 'datakit/loading' ); + + AccessControlManager::set( new WordPressAccessController( wp_get_current_user() ) ); Router::get_instance( $this->data_view_repository ); - DataViewShortcode::get_instance( $this->data_view_repository ); + DataViewShortcode::get_instance( $this->data_view_repository, AccessControlManager::current() ); /** * Modifies the default amount of results per page. @@ -59,6 +65,8 @@ private function __construct( DataViewRepository $data_view_repository ) { add_action( 'datakit/dataview/register', [ $this, 'register_data_view' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] ); + + do_action( 'datakit/loaded' ); } /** @@ -133,8 +141,6 @@ public function register_scripts(): void { public static function get_instance( DataViewRepository $repository ): self { if ( ! isset( self::$instance ) ) { self::$instance = new self( $repository ); - - do_action( 'datakit/loaded' ); } return self::$instance; diff --git a/src/Rest/Router.php b/src/Rest/Router.php index a54d831..42ebe10 100644 --- a/src/Rest/Router.php +++ b/src/Rest/Router.php @@ -2,6 +2,7 @@ namespace DataKit\Plugin\Rest; +use DataKit\DataViews\AccessControl\AccessControlManager; use DataKit\DataViews\Data\MutableDataSource; use DataKit\DataViews\DataView\DataViewRepository; use DataKit\DataViews\Translation\Translatable; @@ -69,10 +70,13 @@ final class Router { private function __construct( DataViewRepository $data_view_repository ) { $this->data_view_repository = $data_view_repository; $this->translator = new WordPressTranslator(); - $this->view_controller = new ViewController( $data_view_repository, $this->translator ); + $this->view_controller = new ViewController( + $data_view_repository, + AccessControlManager::current(), + $this->translator, + ); - // @phpstan-ignore return.missing - add_filter( 'rest_api_init', [ $this, 'register_routes' ] ); + add_action( 'rest_api_init', [ $this, 'register_routes' ] ); } /** diff --git a/src/Rest/ViewController.php b/src/Rest/ViewController.php index 03d9f68..16c4bfd 100644 --- a/src/Rest/ViewController.php +++ b/src/Rest/ViewController.php @@ -2,9 +2,12 @@ namespace DataKit\Plugin\Rest; +use DataKit\DataViews\AccessControl\AccessController; +use DataKit\DataViews\AccessControl\Capability; use DataKit\DataViews\Data\Exception\DataNotFoundException; use DataKit\DataViews\DataView\DataItem; use DataKit\DataViews\DataView\DataView; +use DataKit\DataViews\DataView\DataViewNotFoundException; use DataKit\DataViews\DataView\DataViewRepository; use DataKit\DataViews\DataView\Filters; use DataKit\DataViews\DataView\Pagination; @@ -30,6 +33,15 @@ final class ViewController { */ private DataViewRepository $dataview_repository; + /** + * The Access Controller. + * + * @since $ver$ + * + * @var AccessController + */ + private AccessController $access_controller; + /** * The translator. * @@ -46,8 +58,13 @@ final class ViewController { * * @param DataViewRepository $dataview_repository The DataView repository. */ - public function __construct( DataViewRepository $dataview_repository, WordPressTranslator $translator ) { + public function __construct( + DataViewRepository $dataview_repository, + AccessController $access_controller, + WordPressTranslator $translator + ) { $this->dataview_repository = $dataview_repository; + $this->access_controller = $access_controller; $this->translator = $translator; } @@ -59,12 +76,20 @@ public function __construct( DataViewRepository $dataview_repository, WordPressT * @param WP_REST_Request $request The request object. * * @return bool Whether the current user can view the result. - * @todo Add security from DataView. */ public function can_view( WP_REST_Request $request ): bool { $view_id = (string) ( $request->get_param( 'view_id' ) ?? '' ); + if ( ! $this->dataview_repository->has( $view_id ) ) { + return false; + } - return true; + try { + $dataview = $this->dataview_repository->get( $view_id ); + + return $this->access_controller->can( new Capability\ViewDataView( $dataview ) ); + } catch ( DataViewNotFoundException $e ) { + return false; + } } /** diff --git a/tests/AccessControl/WordPressAccessControllerTest.php b/tests/AccessControl/WordPressAccessControllerTest.php new file mode 100644 index 0000000..0960aa6 --- /dev/null +++ b/tests/AccessControl/WordPressAccessControllerTest.php @@ -0,0 +1,40 @@ + 1 ], 'admin' ); + $user->add_cap( 'administrator' ); + + $guest = new WordPressAccessController( null ); + $admin = new WordPressAccessController( $user ); + + self::assertTrue( $guest->can( new ViewDataView( $dataview ) ) ); + self::assertFalse( $guest->can( new EditDataView( $dataview ) ) ); + self::assertFalse( $guest->can( new DeleteDataView( $dataview ) ) ); + + self::assertTrue( $admin->can( new EditDataView( $dataview ) ) ); + self::assertTrue( $admin->can( new DeleteDataView( $dataview ) ) ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 48d6f69..6fe3b5b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,22 +1,10 @@