From 864cb47e661bebd8e2b71f29a8c6638eaa11af81 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 24 Jun 2026 13:41:47 +0800 Subject: [PATCH 1/3] Abilities API: Plugin Settings: Member Content --- ...s-convertkit-settings-restrict-content.php | 132 +++++++++ includes/mcp/class-convertkit-mcp.php | 1 + .../MCPSettingsRestrictContentTest.php | 257 ++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 tests/Integration/MCPSettingsRestrictContentTest.php diff --git a/includes/class-convertkit-settings-restrict-content.php b/includes/class-convertkit-settings-restrict-content.php index 6653c18df..e6f403cc3 100644 --- a/includes/class-convertkit-settings-restrict-content.php +++ b/includes/class-convertkit-settings-restrict-content.php @@ -115,6 +115,138 @@ public function get_by_key( $key ) { } + /** + * Returns this settings group's programmatic name. + * + * @since 3.4.0 + * + * @return string + */ + public function get_name() { + + return 'restrict-content'; + + } + + /** + * Returns the title of this settings group. + * + * @since 3.4.0 + * + * @return string + */ + public function get_title() { + + return __( 'Member Content Settings', 'convertkit' ); + + } + + /** + * Returns the keys in this settings group that hold credentials or other + * sensitive values. + * + * Member Content settings hold no secrets; returned for interface + * consistency. + * + * @since 3.4.0 + * + * @return string[] + */ + public function get_secret_keys() { + + return array(); + + } + + /** + * Returns the JSON Schema describing this settings group, in the shape + * stored by save() / returned by get(), excluding secret keys. + * + * @since 3.4.0 + * + * @return array + */ + public function get_schema() { + + return array( + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'permit_crawlers' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether search engine crawlers are permitted to index Member Content.', 'convertkit' ), + ), + 'no_access_text_form' => array( + 'type' => 'string', + 'description' => __( 'Message shown to a visitor without access when content is restricted by Form.', 'convertkit' ), + ), + 'subscribe_heading' => array( + 'type' => 'string', + 'description' => __( 'Heading shown above the subscribe call-to-action when content is restricted by Product.', 'convertkit' ), + ), + 'subscribe_text' => array( + 'type' => 'string', + 'description' => __( 'Body text shown alongside the subscribe call-to-action when content is restricted by Product.', 'convertkit' ), + ), + 'no_access_text' => array( + 'type' => 'string', + 'description' => __( 'Message shown to a visitor without access when content is restricted by Product.', 'convertkit' ), + ), + 'subscribe_heading_tag' => array( + 'type' => 'string', + 'description' => __( 'Heading shown above the subscribe call-to-action when content is restricted by Tag.', 'convertkit' ), + ), + 'subscribe_text_tag' => array( + 'type' => 'string', + 'description' => __( 'Body text shown alongside the subscribe call-to-action when content is restricted by Tag.', 'convertkit' ), + ), + 'require_tag_login' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether visitors must log in by email to access Member Content restricted by Tag.', 'convertkit' ), + ), + 'no_access_text_tag' => array( + 'type' => 'string', + 'description' => __( 'Message shown to a visitor without access when content is restricted by Tag.', 'convertkit' ), + ), + 'subscribe_button_label' => array( + 'type' => 'string', + 'description' => __( 'Label for the Subscribe button.', 'convertkit' ), + ), + 'email_text' => array( + 'type' => 'string', + 'description' => __( 'Body text shown above the email log-in form.', 'convertkit' ), + ), + 'email_button_label' => array( + 'type' => 'string', + 'description' => __( 'Label for the email log-in button.', 'convertkit' ), + ), + 'email_heading' => array( + 'type' => 'string', + 'description' => __( 'Heading shown above the email log-in form.', 'convertkit' ), + ), + 'email_description_text' => array( + 'type' => 'string', + 'description' => __( 'Description shown beneath the email log-in heading.', 'convertkit' ), + ), + 'email_check_heading' => array( + 'type' => 'string', + 'description' => __( 'Heading shown after the visitor requests a magic log-in code.', 'convertkit' ), + ), + 'email_check_text' => array( + 'type' => 'string', + 'description' => __( 'Body text shown after the visitor requests a magic log-in code.', 'convertkit' ), + ), + 'container_css_classes' => array( + 'type' => 'string', + 'description' => __( 'Additional CSS classes appended to the Restrict Content container element.', 'convertkit' ), + ), + ), + ); + + } + /** * The default settings, used when the ConvertKit Restrict Content Settings haven't been saved * e.g. on a new installation. diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index a20e0009a..f9f8f7b13 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -101,6 +101,7 @@ public function register_settings_abilities( $abilities ) { $groups = array( new ConvertKit_Settings(), new ConvertKit_Settings_Broadcasts(), + new ConvertKit_Settings_Restrict_Content(), ); // Iterate through settings groups, registering the get and update abilities. diff --git a/tests/Integration/MCPSettingsRestrictContentTest.php b/tests/Integration/MCPSettingsRestrictContentTest.php new file mode 100644 index 000000000..95a68010a --- /dev/null +++ b/tests/Integration/MCPSettingsRestrictContentTest.php @@ -0,0 +1,257 @@ + \ConvertKit_MCP_Ability_Settings_Get::class, + 'kit/settings-restrict-content-update' => \ConvertKit_MCP_Ability_Settings_Update::class, + ); + + // Assert that the abilities are registered and are instances of the expected classes. + foreach ( $expected as $name => $class ) { + $this->assertArrayHasKey($name, $abilities); + $this->assertInstanceOf($class, $abilities[ $name ]); + } + } + + /** + * Test that the permission_callback() rejects a user who cannot manage options. + * + * @since 3.4.0 + */ + public function testPermissionCallbackDeniesWithoutManageOptionsCapability() + { + // Become a Subscriber (no manage_options capability). + $subscriber_id = static::factory()->user->create([ 'role' => 'subscriber' ]); + wp_set_current_user($subscriber_id); + + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Assert that the abilities are permission denied. + foreach ( self::ABILITY_NAMES as $name ) { + // Execute the ability. + $result = $abilities[ $name ]->permission_callback([]); + + // Assert that the result is a WP_Error. + $this->assertInstanceOf(\WP_Error::class, $result); + } + } + + /** + * Test that kit/settings-restrict-content-get returns the current settings. + * + * @since 3.4.0 + */ + public function testGetSettings() + { + // Populate settings. + $this->populateSettings(); + + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-restrict-content-get']->execute_callback([]); + + // Confirm expected settings are returned. + $this->assertArrayHasKey('permit_crawlers', $result); + $this->assertEquals('on', $result['permit_crawlers']); + $this->assertArrayHasKey('no_access_text_form', $result); + $this->assertArrayHasKey('subscribe_heading', $result); + $this->assertArrayHasKey('subscribe_text', $result); + $this->assertArrayHasKey('no_access_text', $result); + $this->assertArrayHasKey('subscribe_heading_tag', $result); + $this->assertArrayHasKey('subscribe_text_tag', $result); + $this->assertArrayHasKey('require_tag_login', $result); + $this->assertEquals('on', $result['require_tag_login']); + $this->assertArrayHasKey('no_access_text_tag', $result); + $this->assertArrayHasKey('subscribe_button_label', $result); + $this->assertArrayHasKey('email_text', $result); + $this->assertArrayHasKey('email_button_label', $result); + $this->assertArrayHasKey('email_heading', $result); + $this->assertArrayHasKey('email_description_text', $result); + $this->assertArrayHasKey('email_check_heading', $result); + $this->assertArrayHasKey('email_check_text', $result); + $this->assertArrayHasKey('container_css_classes', $result); + } + + /** + * Test that kit/settings-restrict-content-update updates the settings. + * + * @since 3.4.0 + */ + public function testUpdateSettings() + { + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-restrict-content-update']->execute_callback( + [ + 'permit_crawlers' => 'on', + 'subscribe_heading' => 'Updated subscribe heading', + 'subscribe_text' => 'Updated subscribe text', + 'require_tag_login' => 'on', + 'subscribe_button_label' => 'Join now', + 'container_css_classes' => 'kit-restrict kit-restrict-custom', + ] + ); + + // Confirm expected settings are returned. + $this->assertArrayHasKey('permit_crawlers', $result); + $this->assertArrayHasKey('no_access_text_form', $result); + $this->assertArrayHasKey('subscribe_heading', $result); + $this->assertArrayHasKey('subscribe_text', $result); + $this->assertArrayHasKey('no_access_text', $result); + $this->assertArrayHasKey('subscribe_heading_tag', $result); + $this->assertArrayHasKey('subscribe_text_tag', $result); + $this->assertArrayHasKey('require_tag_login', $result); + $this->assertArrayHasKey('no_access_text_tag', $result); + $this->assertArrayHasKey('subscribe_button_label', $result); + $this->assertArrayHasKey('email_text', $result); + $this->assertArrayHasKey('email_button_label', $result); + $this->assertArrayHasKey('email_heading', $result); + $this->assertArrayHasKey('email_description_text', $result); + $this->assertArrayHasKey('email_check_heading', $result); + $this->assertArrayHasKey('email_check_text', $result); + $this->assertArrayHasKey('container_css_classes', $result); + + // Confirm settings are updated. + $this->assertEquals('on', $result['permit_crawlers']); + $this->assertEquals('Updated subscribe heading', $result['subscribe_heading']); + $this->assertEquals('Updated subscribe text', $result['subscribe_text']); + $this->assertEquals('on', $result['require_tag_login']); + $this->assertEquals('Join now', $result['subscribe_button_label']); + $this->assertEquals('kit-restrict kit-restrict-custom', $result['container_css_classes']); + } + + /** + * Test that kit/settings-restrict-content-update returns an error if an invalid key is provided. + * + * @since 3.4.0 + */ + public function testUpdateSettingsWithInvalidKeyReturnsError() + { + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-restrict-content-update']->execute_callback([ 'invalid_key' => 'invalid_value' ]); + } + + /** + * Populate the settings with some sensible values for testing. + * + * @since 3.4.0 + */ + private function populateSettings() + { + update_option( + self::SETTINGS_NAME, + [ + 'permit_crawlers' => 'on', + 'no_access_text_form' => 'No access (form).', + 'subscribe_heading' => 'Read with a premium subscription', + 'subscribe_text' => 'Only available to premium subscribers.', + 'no_access_text' => 'No access (product).', + 'subscribe_heading_tag' => 'Subscribe to keep reading', + 'subscribe_text_tag' => 'Free but only available to subscribers.', + 'require_tag_login' => 'on', + 'no_access_text_tag' => 'No access (tag).', + 'subscribe_button_label' => 'Subscribe', + 'email_text' => 'Already subscribed?', + 'email_button_label' => 'Log in', + 'email_heading' => 'Log in to read this post', + 'email_description_text' => 'We\'ll email you a magic code to log you in.', + 'email_check_heading' => 'We just emailed you a log in code', + 'email_check_text' => 'Enter the code below to finish logging in', + 'container_css_classes' => '', + ] + ); + } +} From b88e31070044e6785c1fd44ac7089cbf99195a61 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 24 Jun 2026 16:16:11 +0800 Subject: [PATCH 2/3] Member Content: Refresh Settings on Save --- ...s-convertkit-settings-restrict-content.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/includes/class-convertkit-settings-restrict-content.php b/includes/class-convertkit-settings-restrict-content.php index 6653c18df..022aabd75 100644 --- a/includes/class-convertkit-settings-restrict-content.php +++ b/includes/class-convertkit-settings-restrict-content.php @@ -179,6 +179,27 @@ public function save( $settings ) { update_option( self::SETTINGS_NAME, array_merge( $this->get(), $settings ) ); + // Reload settings in class, to reflect changes. + $this->refresh_settings(); + + } + + /** + * Reloads settings from the options table so this instance has the latest values. + * + * @since 3.3.5 + */ + private function refresh_settings() { + + $settings = get_option( self::SETTINGS_NAME ); + + if ( ! $settings ) { + $this->settings = $this->get_defaults(); + return; + } + + $this->settings = array_merge( $this->get_defaults(), $settings ); + } } From 75c84c70b4f009144760e64a7fc0141f8e2723d8 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 25 Jun 2026 13:46:38 +0800 Subject: [PATCH 3/3] Update Member Content settings class --- ...s-convertkit-settings-restrict-content.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/includes/class-convertkit-settings-restrict-content.php b/includes/class-convertkit-settings-restrict-content.php index e6f403cc3..6fc496328 100644 --- a/includes/class-convertkit-settings-restrict-content.php +++ b/includes/class-convertkit-settings-restrict-content.php @@ -311,6 +311,27 @@ public function save( $settings ) { update_option( self::SETTINGS_NAME, array_merge( $this->get(), $settings ) ); + // Reload settings in class, to reflect changes. + $this->refresh_settings(); + + } + + /** + * Reloads settings from the options table so this instance has the latest values. + * + * @since 3.3.5 + */ + private function refresh_settings() { + + $settings = get_option( self::SETTINGS_NAME ); + + if ( ! $settings ) { + $this->settings = $this->get_defaults(); + return; + } + + $this->settings = array_merge( $this->get_defaults(), $settings ); + } }