diff --git a/admin/class-convertkit-admin-bulk-edit.php b/admin/class-convertkit-admin-bulk-edit.php index ffc0817d8..62128d245 100644 --- a/admin/class-convertkit-admin-bulk-edit.php +++ b/admin/class-convertkit-admin-bulk-edit.php @@ -111,6 +111,13 @@ public function bulk_edit_save() { // Iterate through each Post, updating its settings. foreach ( $post_ids as $post_id ) { + // Skip Posts the current user cannot edit. + // The post type's edit_posts capability checked above does not grant + // permission to edit every Post of that type. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + continue; + } + WP_ConvertKit()->get_class( 'admin_post' )->save_post_settings( $post_id, wp_unslash( $_REQUEST['wp-convertkit'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } diff --git a/admin/class-convertkit-admin-cache-plugins.php b/admin/class-convertkit-admin-cache-plugins.php index a5b44add1..cf4e14272 100644 --- a/admin/class-convertkit-admin-cache-plugins.php +++ b/admin/class-convertkit-admin-cache-plugins.php @@ -54,6 +54,11 @@ public function __construct() { */ public function maybe_configure_cache_plugins() { + // Only allow Administrators to update third party Plugin settings. + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + // If no Pages, Posts or CPTs are configured to use Restrict Content, don't // configure any caching plugins. if ( ! WP_ConvertKit()->get_class( 'admin_restrict_content' )->restrict_content_enabled() ) { diff --git a/admin/class-convertkit-admin-category.php b/admin/class-convertkit-admin-category.php index c77486338..febf00a4a 100644 --- a/admin/class-convertkit-admin-category.php +++ b/admin/class-convertkit-admin-category.php @@ -181,6 +181,11 @@ public function edit_category_form_fields( $term ) { */ public function save_category_fields( $term_id ) { + // Bail if the current user cannot edit this Category. + if ( ! current_user_can( 'edit_term', $term_id ) ) { + return; + } + // Bail if no nonce field exists. if ( ! isset( $_POST['wp-convertkit-save-meta-nonce'] ) ) { return; diff --git a/admin/class-convertkit-admin-landing-page.php b/admin/class-convertkit-admin-landing-page.php index 24888f1e0..54fd37048 100644 --- a/admin/class-convertkit-admin-landing-page.php +++ b/admin/class-convertkit-admin-landing-page.php @@ -54,6 +54,11 @@ public function register_add_new_button( $buttons, $post_type ) { return $buttons; } + // Don't show the button if the user cannot create and publish Pages. + if ( ! convertkit_user_can_create_published_post_type( $post_type ) ) { + return $buttons; + } + // Register button. $buttons['convertkit_landing_page_setup'] = array( 'url' => add_query_arg( diff --git a/admin/class-convertkit-admin-post.php b/admin/class-convertkit-admin-post.php index e544cff1b..3b84de5ac 100644 --- a/admin/class-convertkit-admin-post.php +++ b/admin/class-convertkit-admin-post.php @@ -267,6 +267,11 @@ public function save_post_meta( $post_id ) { return; } + // Bail if the current user cannot edit this Post. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + // Bail if no nonce field exists. if ( ! isset( $_POST['wp-convertkit-save-meta-nonce'] ) ) { return; diff --git a/admin/class-convertkit-admin-restrict-content.php b/admin/class-convertkit-admin-restrict-content.php index 722885253..8f89b009e 100644 --- a/admin/class-convertkit-admin-restrict-content.php +++ b/admin/class-convertkit-admin-restrict-content.php @@ -251,6 +251,11 @@ public function register_add_new_button( $buttons, $post_type ) { return $buttons; } + // Don't show the button if the user cannot create and publish this Post Type. + if ( ! convertkit_user_can_create_published_post_type( $post_type ) ) { + return $buttons; + } + // Register button. $buttons['convertkit_restrict_content_setup'] = array( 'url' => add_query_arg( diff --git a/admin/class-convertkit-admin-setup-wizard.php b/admin/class-convertkit-admin-setup-wizard.php index 98c86e97b..cc81f3d6b 100644 --- a/admin/class-convertkit-admin-setup-wizard.php +++ b/admin/class-convertkit-admin-setup-wizard.php @@ -37,6 +37,15 @@ class ConvertKit_Admin_Setup_Wizard { */ public $error = false; + /** + * Holds the Post Type to generate. + * + * @since 3.3.9 + * + * @var string + */ + public $post_type = 'page'; + /** * The required user capability to access the setup wizard. * @@ -507,4 +516,29 @@ public function user_has_access() { } + /** + * Sets the Post Type from the request, ensuring it is supported by the Plugin. + * + * @since 3.3.9 + */ + protected function set_post_type() { + + $this->post_type = ( filter_has_var( INPUT_GET, 'ck_post_type' ) ? filter_input( INPUT_GET, 'ck_post_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : 'page' ); + + if ( ! in_array( $this->post_type, convertkit_get_supported_post_types(), true ) ) { + wp_die( + sprintf( + /* translators: Post Type */ + esc_html__( 'The post type `%s` is not supported for Member Content.', 'convertkit' ), + esc_html( $this->post_type ) + ), + esc_html__( 'WordPress Error', 'convertkit' ), + array( + 'back_link' => true, + ) + ); + } + + } + } diff --git a/admin/setup-wizard/class-convertkit-admin-setup-wizard-landing-page.php b/admin/setup-wizard/class-convertkit-admin-setup-wizard-landing-page.php index bb053c7ea..1888a9fd8 100644 --- a/admin/setup-wizard/class-convertkit-admin-setup-wizard-landing-page.php +++ b/admin/setup-wizard/class-convertkit-admin-setup-wizard-landing-page.php @@ -14,15 +14,6 @@ */ class ConvertKit_Admin_Setup_Wizard_Landing_Page extends ConvertKit_Admin_Setup_Wizard { - /** - * Holds the Post Type to generate. - * - * @since 2.5.5 - * - * @var string - */ - public $post_type = 'page'; - /** * Holds the ConvertKit Products resource class. * @@ -128,6 +119,13 @@ public function define_steps( $steps ) { */ public function process_form( $step ) { + // Set and authorize the Post Type before processing data. + $this->set_post_type(); + if ( ! convertkit_user_can_create_published_post_type( $this->post_type ) ) { + $this->error = __( 'You are not allowed to create and publish this type of content.', 'convertkit' ); + return; + } + // Run security checks. if ( ! isset( $_REQUEST['_wpnonce'] ) ) { return; @@ -181,26 +179,10 @@ public function load_screen_data( $step ) { wp_die( esc_html__( 'Connect your Kit account in the Kit Plugin\'s settings to get started', 'convertkit' ) ); } - // Get Post Type. - if ( filter_has_var( INPUT_GET, 'ck_post_type' ) ) { - $this->post_type = filter_input( INPUT_GET, 'ck_post_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); - } else { - $this->post_type = 'page'; - } - - // Bail if the Post Type isn't supported. - if ( ! in_array( $this->post_type, convertkit_get_supported_post_types(), true ) ) { - wp_die( - sprintf( - /* translators: Post Type */ - esc_html__( 'The post type `%s` is not supported for Member Content.', 'convertkit' ), - esc_html( $this->post_type ) - ), - esc_html__( 'WordPress Error', 'convertkit' ), - array( - 'back_link' => true, - ) - ); + // Set and authorize the Post Type. + $this->set_post_type(); + if ( ! convertkit_user_can_create_published_post_type( $this->post_type ) ) { + wp_die( esc_html__( 'Sorry, you are not allowed to create and publish this type of content.', 'convertkit' ) ); } // Define Exit URL to take the user back to the WP_List_Table for the Post Type they were viewing. diff --git a/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php b/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php index 2b9b935d9..e00acf1a6 100644 --- a/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php +++ b/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php @@ -72,7 +72,7 @@ class ConvertKit_Admin_Setup_Wizard_Plugin extends ConvertKit_Admin_Setup_Wizard * * @var string */ - public $required_capability = 'edit_posts'; + public $required_capability = 'manage_options'; /** * The programmatic name for this wizard. diff --git a/admin/setup-wizard/class-convertkit-admin-setup-wizard-restrict-content.php b/admin/setup-wizard/class-convertkit-admin-setup-wizard-restrict-content.php index bf68d0a2c..58b0f324b 100644 --- a/admin/setup-wizard/class-convertkit-admin-setup-wizard-restrict-content.php +++ b/admin/setup-wizard/class-convertkit-admin-setup-wizard-restrict-content.php @@ -14,15 +14,6 @@ */ class ConvertKit_Admin_Setup_Wizard_Restrict_Content extends ConvertKit_Admin_Setup_Wizard { - /** - * Holds the Post Type to generate Members Content for. - * - * @since 2.1.0 - * - * @var string - */ - public $post_type = 'page'; - /** * Holds the type of Member's Content to generate (course|download). * @@ -185,6 +176,13 @@ public function define_steps( $steps ) { */ public function process_form( $step ) { + // Set and authorize the Post Type before processing data. + $this->set_post_type(); + if ( ! convertkit_user_can_create_published_post_type( $this->post_type ) ) { + $this->error = __( 'You are not allowed to create and publish this type of content.', 'convertkit' ); + return; + } + // Run security checks. if ( ! isset( $_REQUEST['_wpnonce'] ) ) { return; @@ -256,26 +254,10 @@ public function load_screen_data( $step ) { wp_die( esc_html__( 'Connect your ConvertKit account in the ConvertKit Plugin\'s settings to get started', 'convertkit' ) ); } - // Get the Post Type. - if ( filter_has_var( INPUT_GET, 'ck_post_type' ) ) { - $this->post_type = filter_input( INPUT_GET, 'ck_post_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); - } else { - $this->post_type = 'page'; - } - - // Bail if the Post Type isn't supported. - if ( ! in_array( $this->post_type, convertkit_get_supported_post_types(), true ) ) { - wp_die( - sprintf( - /* translators: Post Type */ - esc_html__( 'The post type `%s` is not supported for Member Content.', 'convertkit' ), - esc_html( $this->post_type ) - ), - esc_html__( 'WordPress Error', 'convertkit' ), - array( - 'back_link' => true, - ) - ); + // Set and authorize the Post Type. + $this->set_post_type(); + if ( ! convertkit_user_can_create_published_post_type( $this->post_type ) ) { + wp_die( esc_html__( 'Sorry, you are not allowed to create and publish this type of content.', 'convertkit' ) ); } // Define Exit URL to take the user back to the WP_List_Table for the Post Type they were viewing. diff --git a/includes/class-convertkit-broadcasts-exporter.php b/includes/class-convertkit-broadcasts-exporter.php index 48cb05fa5..15b871911 100644 --- a/includes/class-convertkit-broadcasts-exporter.php +++ b/includes/class-convertkit-broadcasts-exporter.php @@ -109,6 +109,11 @@ public function run_row_action() { return; } + // Bail if the current user cannot edit the Post being exported. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( esc_html__( 'Sorry, you are not allowed to export this Post.', 'convertkit' ) ); + } + // Export Post to a draft ConvertKit Broadcast. $result = $this->export_post_to_broadcast( $post_id ); diff --git a/includes/functions.php b/includes/functions.php index 42037ad75..89846dee1 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -147,6 +147,26 @@ function convertkit_get_supported_post_types() { } +/** + * Determines whether the current user can create and publish the given Post Type. + * + * @since 3.3.9 + * + * @param string $post_type Post Type. + * @return bool User can create and publish Post Type. + */ +function convertkit_user_can_create_published_post_type( $post_type ) { + + $post_type_object = get_post_type_object( $post_type ); + + if ( ! $post_type_object ) { + return false; + } + + return current_user_can( $post_type_object->cap->create_posts ) && current_user_can( $post_type_object->cap->publish_posts ); + +} + /** * Helper method to get supported Post Types for Restricted Content (Member's Content) * diff --git a/includes/pre-publish-actions/class-convertkit-pre-publish-action.php b/includes/pre-publish-actions/class-convertkit-pre-publish-action.php index 9b4c6c756..86e0e277d 100644 --- a/includes/pre-publish-actions/class-convertkit-pre-publish-action.php +++ b/includes/pre-publish-actions/class-convertkit-pre-publish-action.php @@ -201,6 +201,11 @@ public function save_post_meta( $post_id ) { return; } + // Bail if the current user cannot edit this Post. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + // Bail if no nonce field exists. if ( ! isset( $_POST['wp-convertkit-pre-publish-actions-nonce'] ) ) { return; diff --git a/tests/EndToEnd/forms/post-types/BulkQuickEditFormCest.php b/tests/EndToEnd/forms/post-types/BulkQuickEditFormCest.php index 17a9ce8e0..5d6592ddf 100644 --- a/tests/EndToEnd/forms/post-types/BulkQuickEditFormCest.php +++ b/tests/EndToEnd/forms/post-types/BulkQuickEditFormCest.php @@ -181,6 +181,71 @@ public function testBulkEditUsingDefaultForm(EndToEndTester $I) } } + /** + * Test that Bulk Edit does not save Kit settings for a Post the current user cannot edit. + * + * @since 3.3.9 + * + * @param EndToEndTester $I Tester. + */ + public function testBulkEditDoesNotSaveSettingsForPostUserCannotEdit(EndToEndTester $I) + { + // Create an Author and a Post that they own. + $authorID = $I->haveUserInDatabase('convertkit_bulk_edit_author', 'author'); + $postID = $I->havePostInDatabase( + [ + 'post_author' => $authorID, + 'post_type' => 'post', + 'post_title' => 'Kit: Bulk Edit: Author Post', + ] + ); + + // Create a Post owned by the Administrator. + $administratorID = $I->grabUserIdFromDatabase($_ENV['WORDPRESS_ADMIN_USER']); + $otherPostID = $I->havePostInDatabase( + [ + 'post_author' => $administratorID, + 'post_type' => 'post', + 'post_title' => 'Kit: Bulk Edit: Administrator Post', + ] + ); + + // Login as the Author. + $I->logOut(); + $I->loginAs('convertkit_bulk_edit_author', 'convertkit_bulk_edit_author'); + + // Open Bulk Edit for the Author's Post. + $I->openBulkEdit($I, 'post', [ $postID ]); + + // Add an Administrator-owned Post ID to the submitted request. + $I->executeJS( + ' + var postID = document.createElement("input"); + postID.setAttribute("type", "hidden"); + postID.setAttribute("name", "post[]"); + postID.setAttribute("value", "' . $otherPostID . '"); + document.querySelector("#bulk-edit").appendChild(postID); + ' + ); + + // Set a Kit Form and save the Bulk Edit request. + $I->selectOption('#convertkit-bulk-edit #wp-convertkit-bulk-edit-form', $_ENV['CONVERTKIT_API_FORM_NAME']); + $I->click('#bulk_edit'); + $I->waitForElementVisible('div.updated'); + + // Confirm no settings were saved against the Administrator-owned Post. + $I->dontSeePostMetaInDatabase( + [ + 'post_id' => $otherPostID, + 'meta_key' => '_wp_convertkit_post_meta', + ] + ); + + // Login as the Administrator so the test suite can deactivate the Plugin. + $I->logOut(); + $I->doLoginAsAdmin($I); + } + /** * Test that the defined form displays when chosen via * WordPress' Bulk Edit functionality. diff --git a/tests/EndToEnd/landing-pages/PageLandingPageSetupWizardCest.php b/tests/EndToEnd/landing-pages/PageLandingPageSetupWizardCest.php index af0a795c5..659079fd6 100644 --- a/tests/EndToEnd/landing-pages/PageLandingPageSetupWizardCest.php +++ b/tests/EndToEnd/landing-pages/PageLandingPageSetupWizardCest.php @@ -84,6 +84,53 @@ public function testAddNewLandingPageButtonNotDisplayedWhenDisabled(EndToEndTest $I->dontSeeElementInDOM('span.convertkit-action.page-title-action'); } + /** + * Test that the Add New Landing Page and Member Content buttons do not display when the user + * cannot publish Pages. + * + * @since 3.3.9 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewContentButtonsNotDisplayedWhenUserCannotPublishPages(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + + // Create an Editor who cannot publish Pages. + $userID = $I->haveUserInDatabase('convertkit_pages_contributor', 'editor'); + $I->dontHaveUserMetaInDatabase( + [ + 'user_id' => $userID, + 'meta_key' => $I->grabTablePrefix() . 'capabilities', + ] + ); + $I->haveUserMetaInDatabase( + $userID, + $I->grabTablePrefix() . 'capabilities', + serialize( + [ + 'editor' => true, + 'publish_pages' => false, + ] + ) + ); + + // Login as the user who cannot publish Pages. + $I->logOut(); + $I->loginAs('convertkit_pages_contributor', 'convertkit_pages_contributor'); + + // Navigate to Pages. + $I->amOnAdminPage('edit.php?post_type=page'); + + // Confirm no content creation buttons are displayed. + $I->dontSeeElementInDOM('span.convertkit-action.page-title-action'); + + // Login as the Administrator so the test suite can deactivate the Plugin. + $I->logOut(); + $I->doLoginAsAdmin($I); + } + /** * Test that the Dashboard submenu item for this wizard does not display when a * third party Admin Menu editor type Plugin is installed and active. diff --git a/tests/Integration/PermissionsTest.php b/tests/Integration/PermissionsTest.php new file mode 100644 index 000000000..9d938c855 --- /dev/null +++ b/tests/Integration/PermissionsTest.php @@ -0,0 +1,239 @@ +settings = new \ConvertKit_Settings(); + $this->settings->save( + array( + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + 'token_expires' => ( time() + 10000 ), + ) + ); + } + + /** + * Performs actions after each test. + * + * @since 3.3.9 + */ + public function tearDown(): void + { + // Delete credentials from Plugin settings. + $this->settings->delete_credentials(); + + // Reset request data used by admin request handlers. + $_POST = array(); + $_REQUEST = array(); + + parent::tearDown(); + } + + /** + * Test that users must be able to create and publish the requested Post Type. + * + * @since 3.3.9 + */ + public function testUserCanCreatePublishedPostType() + { + // Administrators and Editors can create and publish Pages. + $this->actAs( 'administrator' ); + $this->assertTrue( convertkit_user_can_create_published_post_type( 'page' ) ); + + $this->actAs( 'editor' ); + $this->assertTrue( convertkit_user_can_create_published_post_type( 'page' ) ); + + // Authors can create and publish Posts, but not Pages. + $this->actAs( 'author' ); + $this->assertTrue( convertkit_user_can_create_published_post_type( 'post' ) ); + $this->assertFalse( convertkit_user_can_create_published_post_type( 'page' ) ); + + // Contributors cannot publish Posts. + $this->actAs( 'contributor' ); + $this->assertFalse( convertkit_user_can_create_published_post_type( 'post' ) ); + + // Unknown Post Types cannot be created. + $this->assertFalse( convertkit_user_can_create_published_post_type( 'convertkit_unknown' ) ); + } + + /** + * Test that users unable to publish Pages are not offered content creation buttons. + * + * @since 3.3.9 + */ + public function testListTableButtonsNotRegisteredWhenUserCannotPublishPages() + { + // Create and become an Author, who cannot create or publish Pages. + $this->actAs( 'author' ); + + // Confirm no content creation buttons are registered. + $landing_page = new \ConvertKit_Admin_Landing_Page(); + $this->assertSame( array(), $landing_page->register_add_new_button( array(), 'page' ) ); + + $restrict_content = new \ConvertKit_Admin_Restrict_Content(); + $this->assertSame( array(), $restrict_content->register_add_new_button( array(), 'page' ) ); + } + + /** + * Test that only Administrators can access the Plugin setup wizard. + * + * @since 3.3.9 + */ + public function testPluginSetupWizardRequiresManageOptionsCapability() + { + $wizard = new \ConvertKit_Admin_Setup_Wizard_Plugin(); + + // Editors cannot access the Plugin setup wizard. + $this->actAs( 'editor' ); + $this->assertFalse( $wizard->user_has_access() ); + + // Administrators can access the Plugin setup wizard. + $this->actAs( 'administrator' ); + $this->assertTrue( $wizard->user_has_access() ); + } + + /** + * Test that Post settings cannot be saved for a Post the user cannot edit. + * + * @since 3.3.9 + */ + public function testPostSettingsNotSavedWhenUserCannotEditPost() + { + // Create a Post owned by an Administrator. + $administrator_id = static::factory()->user->create( array( 'role' => 'administrator' ) ); + $post_id = static::factory()->post->create( + array( + 'post_author' => $administrator_id, + ) + ); + + // Become an Author, who cannot edit another user's Post. + $this->actAs( 'author' ); + $_POST = array( + 'wp-convertkit-save-meta-nonce' => wp_create_nonce( 'wp-convertkit-save-meta' ), + 'wp-convertkit' => array( + 'form' => '123', + ), + ); + + // Attempt to save the Post's settings. + ( new \ConvertKit_Admin_Post() )->save_post_meta( $post_id ); + + // Confirm no settings were saved. + $this->assertSame( '', get_post_meta( $post_id, '_wp_convertkit_post_meta', true ) ); + } + + /** + * Test that Category settings cannot be saved when the user cannot edit the Category. + * + * @since 3.3.9 + */ + public function testCategorySettingsNotSavedWhenUserCannotEditCategory() + { + // Create a Category. + $term = static::factory()->term->create_and_get( + array( + 'taxonomy' => 'category', + ) + ); + + // Become an Author, who cannot manage Categories. + $this->actAs( 'author' ); + $_POST = array( + 'wp-convertkit-save-meta-nonce' => wp_create_nonce( 'wp-convertkit-save-meta' ), + 'wp-convertkit' => array( + 'form' => '123', + ), + ); + + // Attempt to save the Category's settings. + ( new \ConvertKit_Admin_Category() )->save_category_fields( $term->term_id ); + + // Confirm no settings were saved. + $this->assertSame( '', get_term_meta( $term->term_id, '_wp_convertkit_term_meta', true ) ); + } + + /** + * Test that exporting a Post to a Broadcast stops when the user cannot edit the Post. + * + * @since 3.3.9 + */ + public function testBroadcastExportStopsWhenUserCannotEditPost() + { + // Enable Broadcast exports. + $broadcasts_settings = new \ConvertKit_Settings_Broadcasts(); + $broadcasts_settings->save( array( 'enabled_export' => 'on' ) ); + + // Create a Post owned by an Administrator. + $administrator_id = static::factory()->user->create( array( 'role' => 'administrator' ) ); + $post_id = static::factory()->post->create( + array( + 'post_author' => $administrator_id, + ) + ); + + // Become an Author, who cannot edit the Administrator's Post. + $this->actAs( 'author' ); + $_REQUEST = array( + 'nonce' => wp_create_nonce( 'action-convertkit-broadcast-export' ), + 'convertkit-action' => 'broadcast-export', + 'id' => $post_id, + ); + + // Confirm the export stops before a Broadcast can be created. + $this->expectException( \WPDieException::class ); + $this->expectExceptionMessage( 'Sorry, you are not allowed to export this Post.' ); + ( new \ConvertKit_Broadcasts_Exporter() )->run_row_action(); + } + + /** + * Creates a User with the given role and makes them the current User. + * + * @since 3.3.9 + * + * @param string $role User role. + */ + private function actAs( $role ) + { + $user_id = static::factory()->user->create( array( 'role' => $role ) ); + wp_set_current_user( $user_id ); + } +}