diff --git a/admin/class-convertkit-admin-post.php b/admin/class-convertkit-admin-post.php index b36a5cbe9..e544cff1b 100644 --- a/admin/class-convertkit-admin-post.php +++ b/admin/class-convertkit-admin-post.php @@ -8,8 +8,8 @@ /** * Registers a metabox on Posts, Pages and public facing Custom Post Types - * and saves its settings when the Post is saved in the WordPress Administration - * interface. + * that do not use the block editor, saving settings when the Post is saved + * in the WordPress Administration interface. * * @package ConvertKit * @author ConvertKit @@ -27,7 +27,7 @@ public function __construct() { add_filter( 'views_edit-page', array( $this, 'output_wp_list_table_buttons' ) ); add_action( 'post_submitbox_misc_actions', array( $this, 'output_pre_publish_actions' ) ); - add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) ); + add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 10, 2 ); add_action( 'save_post', array( $this, 'save_post_meta' ) ); } @@ -181,9 +181,10 @@ public function output_pre_publish_actions( $post ) { * * @since 1.9.6 * - * @param string $post_type Post Type. + * @param string $post_type Post Type. + * @param WP_Post $post Post. */ - public function add_meta_boxes( $post_type ) { + public function add_meta_boxes( $post_type, $post ) { // Don't register the meta box if this Post Type isn't supported. $supported_post_types = convertkit_get_supported_post_types(); @@ -191,6 +192,11 @@ public function add_meta_boxes( $post_type ) { return; } + // Don't register the meta box if the block editor is being used, as register_post_meta() handles saving post meta. + if ( function_exists( 'use_block_editor_for_post' ) && use_block_editor_for_post( $post ) ) { + return; + } + // Register Meta Box. add_meta_box( 'wp-convertkit-meta-box', __( 'Kit', 'convertkit' ), array( $this, 'display_meta_box' ), $post_type, 'normal' ); diff --git a/includes/class-convertkit-gutenberg.php b/includes/class-convertkit-gutenberg.php index 1c51589cf..2ee100eeb 100644 --- a/includes/class-convertkit-gutenberg.php +++ b/includes/class-convertkit-gutenberg.php @@ -32,6 +32,9 @@ public function __construct() { // Register Gutenberg Blocks. add_action( 'init', array( $this, 'add_blocks' ) ); + // Register Gutenberg Plugin Sidebars. + add_action( 'init', array( $this, 'add_plugin_sidebars' ) ); + // Register REST API routes. add_action( 'rest_api_init', array( $this, 'register_routes' ) ); @@ -187,6 +190,71 @@ public function add_blocks() { } + /** + * Registers post meta for any registered plugin sidebars using register_post_meta(), + * so data is saved when using the Gutenberg editor. + * + * @since 3.3.0 + */ + public function add_plugin_sidebars() { + + // Get plugin sidebars. + $plugin_sidebars = convertkit_get_plugin_sidebars(); + + // Bail if no plugin sidebars are available. + if ( ! count( $plugin_sidebars ) ) { + return; + } + + foreach ( $plugin_sidebars as $plugin_sidebar ) { + register_post_meta( + '', + $plugin_sidebar['meta_key'], + array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => $plugin_sidebar['attributes'], + ), + ), + 'single' => true, + 'type' => 'object', + 'default' => $plugin_sidebar['default_values'], + 'sanitize_callback' => function ( $meta ) use ( $plugin_sidebar ) { + + // If the value is not an array, return the default values. + if ( ! is_array( $meta ) ) { + return $plugin_sidebar['default_values']; + } + + // Iterate through the attributes and sanitize the meta. + foreach ( $plugin_sidebar['attributes'] as $key => $attribute ) { + $meta[ $key ] = sanitize_text_field( $meta[ $key ] ?? $attribute['default'] ); + } + + // If a Form or Landing Page was specified, request a review. + // This can safely be called multiple times, as the review request + // class will ensure once a review request is dismissed by the user, + // it is never displayed again. + if ( $meta['form'] || $meta['landing_page'] ) { + WP_ConvertKit()->get_class( 'review_request' )->request_review(); + } + + // Return the sanitized meta. + return $meta; + + }, + 'auth_callback' => function () use ( $plugin_sidebar ) { + + return current_user_can( $plugin_sidebar['minimum_capability'] ); + + }, + ) + ); + } + + } + /** * Determines the block API version to use for registering blocks. * @@ -214,6 +282,7 @@ public function get_block_api_version() { return absint( $block_api_version ); } + /** * Enqueues scripts for Gutenberg blocks in the editor view. * @@ -233,13 +302,23 @@ public function enqueue_scripts() { $blocks = convertkit_get_blocks(); $block_formatters = convertkit_get_block_formatters(); $pre_publish_actions = convertkit_get_pre_publish_actions(); + $plugin_sidebars = convertkit_get_plugin_sidebars(); // Enqueue Gutenberg Javascript, and set the blocks data. wp_enqueue_script( 'convertkit-gutenberg', CONVERTKIT_PLUGIN_URL . 'resources/backend/js/gutenberg.js', array( 'jquery' ), CONVERTKIT_PLUGIN_VERSION, true ); wp_localize_script( 'convertkit-gutenberg', 'convertkit_blocks', $blocks ); + + // If pre-publish actions are available, set the data. if ( count( $pre_publish_actions ) ) { wp_localize_script( 'convertkit-gutenberg', 'convertkit_pre_publish_actions', $pre_publish_actions ); } + + // If plugin sidebars are available, set the data. + if ( count( $plugin_sidebars ) ) { + wp_localize_script( 'convertkit-gutenberg', 'convertkit_plugin_sidebars', $plugin_sidebars ); + } + + // Set the Gutenberg data. wp_localize_script( 'convertkit-gutenberg', 'convertkit_gutenberg', diff --git a/includes/class-wp-convertkit.php b/includes/class-wp-convertkit.php index 3dbb8f8b6..9e49cc858 100644 --- a/includes/class-wp-convertkit.php +++ b/includes/class-wp-convertkit.php @@ -196,6 +196,7 @@ private function initialize_global() { $this->classes['block_formatter_form_link'] = new ConvertKit_Block_Formatter_Form_Link(); $this->classes['block_formatter_product_link'] = new ConvertKit_Block_Formatter_Product_Link(); $this->classes['pre_publish_action_broadcast_export'] = new ConvertKit_Pre_Publish_Action_Broadcast_Export(); + $this->classes['plugin_sidebar_post_settings'] = new ConvertKit_Plugin_Sidebar_Post_Settings(); $this->classes['broadcasts_exporter'] = new ConvertKit_Broadcasts_Exporter(); $this->classes['broadcasts_importer'] = new ConvertKit_Broadcasts_Importer(); $this->classes['elementor'] = new ConvertKit_Elementor(); diff --git a/includes/functions.php b/includes/functions.php index f4f214ec1..8550c73d5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -234,6 +234,30 @@ function convertkit_get_block_formatters() { } +/** + * Helper method to get registered plugin sidebars. + * + * @since 3.3.0 + * + * @return array Plugin sidebars + */ +function convertkit_get_plugin_sidebars() { + + $plugin_sidebars = array(); + + /** + * Registers plugin sidebars for the WordPress block editor. + * + * @since 3.3.0 + * + * @param array $plugin_sidebars Plugin sidebars. + */ + $plugin_sidebars = apply_filters( 'convertkit_plugin_sidebars', $plugin_sidebars ); + + return $plugin_sidebars; + +} + /** * Helper method to get registered pre-publish actions. * diff --git a/includes/plugin-sidebars/class-convertkit-plugin-sidebar-post-settings.php b/includes/plugin-sidebars/class-convertkit-plugin-sidebar-post-settings.php new file mode 100644 index 000000000..9e4d934aa --- /dev/null +++ b/includes/plugin-sidebars/class-convertkit-plugin-sidebar-post-settings.php @@ -0,0 +1,234 @@ + array( + 'type' => 'string', + 'default' => $this->get_default_value( 'form' ), + ), + 'landing_page' => array( + 'type' => 'string', + 'default' => $this->get_default_value( 'landing_page' ), + ), + 'tag' => array( + 'type' => 'string', + 'default' => $this->get_default_value( 'tag' ), + ), + 'restrict_content' => array( + 'type' => 'string', + 'default' => $this->get_default_value( 'restrict_content' ), + ), + ); + + } + + /** + * Returns this plugin sidebar's Fields + * + * @since 3.3.0 + * + * @return bool|array + */ + public function get_fields() { + + // Load resource classes. + $convertkit_forms = new ConvertKit_Resource_Forms( 'post_settings' ); + $convertkit_landing_pages = new ConvertKit_Resource_Landing_Pages( 'post_settings' ); + $convertkit_tags = new ConvertKit_Resource_Tags( 'post_settings' ); + $convertkit_products = new ConvertKit_Resource_Products( 'post_settings' ); + + // Get Forms. + $forms = array( + '-1' => esc_html__( 'Default', 'convertkit' ), + '0' => esc_html__( 'None', 'convertkit' ), + ); + if ( $convertkit_forms->exist() ) { + foreach ( $convertkit_forms->get() as $form ) { + // Legacy forms don't include a `format` key, so define them as inline. + $forms[ absint( $form['id'] ) ] = sprintf( + '%s [%s]', + sanitize_text_field( $form['name'] ), + ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' ) + ); + } + } + + // Get Landing Pages. + $landing_pages = array( + '0' => esc_html__( 'None', 'convertkit' ), + ); + if ( $convertkit_landing_pages->exist() ) { + foreach ( $convertkit_landing_pages->get() as $landing_page ) { + $landing_pages[ absint( $landing_page['id'] ) ] = sanitize_text_field( $landing_page['name'] ); + } + } + + // Get Tags. + $tags = array( + '0' => esc_html__( 'None', 'convertkit' ), + ); + if ( $convertkit_tags->exist() ) { + foreach ( $convertkit_tags->get() as $tag ) { + $tags[ absint( $tag['id'] ) ] = sanitize_text_field( $tag['name'] ); + } + } + + // Get Products. + $restrict_content = array( + '0' => esc_html__( 'Don\'t restrict content to member-only', 'convertkit' ), + ); + if ( $convertkit_forms->exist() ) { + foreach ( $convertkit_forms->get() as $form ) { + // Legacy forms don't include a `format` key, so define them as inline. + $restrict_content[ 'form_' . absint( $form['id'] ) ] = sprintf( + '%s [%s]', + sanitize_text_field( $form['name'] ), + ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' ) + ); + } + } + if ( $convertkit_tags->exist() ) { + foreach ( $convertkit_tags->get() as $tag ) { + $restrict_content[ 'tag_' . absint( $tag['id'] ) ] = sanitize_text_field( $tag['name'] ); + } + } + if ( $convertkit_products->exist() ) { + foreach ( $convertkit_products->get() as $product ) { + $restrict_content[ 'product_' . $product['id'] ] = sanitize_text_field( $product['name'] ); + } + } + + return array( + 'form' => array( + 'label' => __( 'Form', 'convertkit' ), + 'type' => 'select', + 'description' => array( + __( 'Default', 'convertkit' ) . ': ' . __( 'Uses the form specified on the settings page.', 'convertkit' ), + __( 'None', 'convertkit' ) . ': ' . __( 'do not display a form.', 'convertkit' ), + __( 'Any other option will display that form after the main content.', 'convertkit' ), + ), + 'values' => $forms, + ), + 'landing_page' => array( + 'label' => __( 'Landing Page', 'convertkit' ), + 'type' => 'select', + 'description' => __( 'Select a landing page to make it appear in place of this page.', 'convertkit' ), + 'values' => $landing_pages, + ), + 'tag' => array( + 'label' => __( 'Tag', 'convertkit' ), + 'type' => 'select', + 'description' => __( 'Select a tag to apply to visitors of this page who are subscribed. A visitor is deemed to be subscribed if they have clicked a link in an email to this site which includes their subscriber ID, or have entered their email address in a Kit Form on this site.', 'convertkit' ), + 'values' => $tags, + ), + 'restrict_content' => array( + 'label' => __( 'Restrict Content', 'convertkit' ), + 'type' => 'select', + 'description' => __( 'Select the Kit form, tag or product that the visitor must be subscribed to, permitting them access to view this member-only content.', 'convertkit' ), + 'values' => $restrict_content, + ), + ); + + } + + /** + * Returns this block's Default Values + * + * @since 3.3.0 + * + * @return array + */ + public function get_default_values() { + + return array( + 'form' => '-1', + 'landing_page' => '0', + 'tag' => '0', + 'restrict_content' => '0', + ); + + } + +} diff --git a/includes/plugin-sidebars/class-convertkit-plugin-sidebar.php b/includes/plugin-sidebars/class-convertkit-plugin-sidebar.php new file mode 100644 index 000000000..9d58f75e6 --- /dev/null +++ b/includes/plugin-sidebars/class-convertkit-plugin-sidebar.php @@ -0,0 +1,203 @@ +is_admin_frontend_editor_or_admin_rest_request() ) { + $plugin_sidebars[ $this->get_name() ] = array( + 'name' => $this->get_name(), + 'minimum_capability' => $this->get_minimum_capability(), + 'meta_key' => $this->get_meta_key(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'title' => $this->get_title(), + 'attributes' => $this->get_attributes(), + 'default_values' => $this->get_default_values(), + ); + + return $plugin_sidebars; + } + + $plugin_sidebars[ $this->get_name() ] = array( + 'name' => $this->get_name(), + 'minimum_capability' => $this->get_minimum_capability(), + 'meta_key' => $this->get_meta_key(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'title' => $this->get_title(), + 'icon' => $this->get_icon(), + 'gutenberg_icon' => convertkit_get_file_contents( CONVERTKIT_PLUGIN_PATH . '/' . $this->get_icon() ), + 'fields' => $this->get_fields(), + 'attributes' => $this->get_attributes(), + 'default_values' => $this->get_default_values(), + ); + + return $plugin_sidebars; + + } + + /** + * Returns this plugin sidebar's meta key. + * + * @since 3.3.0 + */ + public function get_name() { + + return ''; + + } + + /** + * Returns this plugin sidebar's minimum capability required + * for displaying and permitting edits to the settings. + * + * @since 3.3.0 + * + * @return string + */ + public function get_minimum_capability() { + + return 'edit_posts'; + + } + + /** + * Returns this plugin sidebar's meta key. + * + * @since 3.3.0 + * + * @return string + */ + public function get_meta_key() { + + return ''; + + } + + /** + * Returns this plugin sidebar's title. + * + * @since 3.3.0 + */ + public function get_title() { + + return ''; + + } + + /** + * Returns this plugin sidebar's icon. + * + * @since 3.3.0 + */ + public function get_icon() { + + return ''; + + } + + /** + * Returns this plugin sidebar's attributes. + * + * @since 3.3.0 + * + * @return array + */ + public function get_attributes() { + + return array(); + + } + + /** + * Returns this plugin sidebar's Fields + * + * @since 3.3.0 + * + * @return array + */ + public function get_fields() { + + return array(); + + } + + /** + * Returns this plugin sidebar's Default Values + * + * @since 3.3.0 + * + * @return array + */ + public function get_default_values() { + + return array(); + + } + + /** + * Returns the given plugin sidebar's field's Default Value + * + * @since 3.3.0 + * + * @param string $field Field Name. + * @return string + */ + public function get_default_value( $field ) { + + $defaults = $this->get_default_values(); + if ( isset( $defaults[ $field ] ) ) { + return $defaults[ $field ]; + } + + return ''; + + } + + /** + * Determines if the request is a WordPress REST API request + * made by a logged in WordPress user who has the capability to edit posts. + * + * @since 3.3.0 + * + * @return bool + */ + public function is_admin_rest_request() { + + return defined( 'REST_REQUEST' ) && REST_REQUEST && current_user_can( 'edit_posts' ); + + } + + /** + * Determines if the request is for the WordPress Administration, frontend editor or REST API request. + * + * @since 3.3.0 + * + * @return bool + */ + public function is_admin_frontend_editor_or_admin_rest_request() { + + return WP_ConvertKit()->is_admin_or_frontend_editor() || $this->is_admin_rest_request(); + + } + +} diff --git a/resources/backend/images/kit-logo.svg b/resources/backend/images/kit-logo.svg index 946f6e5cc..dc2c1b69e 100644 --- a/resources/backend/images/kit-logo.svg +++ b/resources/backend/images/kit-logo.svg @@ -1,6 +1,6 @@ - + - + diff --git a/resources/backend/js/gutenberg.js b/resources/backend/js/gutenberg.js index ea849cf7f..a45df3db2 100644 --- a/resources/backend/js/gutenberg.js +++ b/resources/backend/js/gutenberg.js @@ -19,8 +19,17 @@ if (convertKitGutenbergEnabled()) { convertKitGutenbergRegisterBlock(convertkit_blocks[block]); } - // Register ConvertKit Pre-publish actions in Gutenberg if we're editing a Post. if (convertKitEditingPostInGutenberg()) { + // Register Plugin Sidebars in Gutenberg if we're editing a Post. + if (typeof convertkit_plugin_sidebars !== 'undefined') { + for (const pluginSidebar in convertkit_plugin_sidebars) { + convertKitGutenbergRegisterPluginSidebar( + convertkit_plugin_sidebars[pluginSidebar] + ); + } + } + + // Register ConvertKit Pre-publish actions in Gutenberg if we're editing a Post. if (typeof convertkit_pre_publish_actions !== 'undefined') { convertKitGutenbergRegisterPrePublishActions( convertkit_pre_publish_actions @@ -913,6 +922,173 @@ function convertKitGutenbergRegisterBlock(block) { ); } +/** + * Registers a Plugin Sidebar in Gutenberg. + * + * @since 3.3.0 + * + * @param {Object} sidebar Plugin Sidebars + */ +function convertKitGutenbergRegisterPluginSidebar(sidebar) { + (function (plugins, editor, element, components, data) { + // Define some constants for the various items we'll use. + const el = element.createElement; + const { registerPlugin } = plugins; + const { PluginSidebar } = editor; + const { TextControl, SelectControl, PanelBody, PanelRow } = components; + const { useSelect, useDispatch } = data; + + /** + * Returns a PluginDocumentSettingPanel for this plugin, containing + * post-level settings. + * + * @since 3.3.0 + */ + const RenderPanel = function () { + const meta = useSelect(function (wpSelect) { + return ( + wpSelect('core/editor').getEditedPostAttribute('meta') || {} + ); + }, []); + const { editPost: wpEditPost } = useDispatch('core/editor'); + const settings = meta[sidebar.meta_key] || sidebar.default_values; + + /** + * Updates the Post meta meta_key object. + * + * @since 3.3.0 + * + * @param {string} key Sub key within the meta_key object. + * @param {string} value Value to assign to the sub key. + */ + const updateSetting = function (key, value) { + wpEditPost({ + meta: { + [sidebar.meta_key]: Object.assign({}, settings, { + [key]: value, + }), + }, + }); + }; + + /** + * Return a field element for the settings panel. + * + * @since 3.3.0 + * + * @param {Object} field Field properties. + * @param {string} key Field name. + * @return {Object} Field element. + */ + const getField = function (field, key) { + // Define some field properties shared across all field types. + const fieldProperties = { + key: 'convertkit_plugin_sidebar_' + key, + label: field.label, + help: Array.isArray(field.description) + ? field.description.join('\n\n') + : field.description, + value: settings[key] || field.default_value || '', + + // Add __next40pxDefaultSize and __nextHasNoMarginBottom properties, + // preventing deprecation notices in the block editor and opt in to the new styles + // from 7.0. + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + + // Save Post Meta on value change. + onChange(value) { + updateSetting(key, value); + }, + }; + + const fieldOptions = []; + + // Define additional Field Properties and the Field Element, + // depending on the Field Type (select, textarea, text etc). + switch (field.type) { + case 'select': + // Build options for '); + + // Confirm no extra , or tags are output i.e. injecting the form doesn't result in DOMDocument adding tags. + $I->seeNoExtraHtmlHeadBodyTagsOutput($I); + } + } + + /** + * Test that specifying a non-inline Form specified in the Plugin Settings does not + * result in a fatal error when creating and viewing a new WordPress Page, and its position is set + * to after the 3rd paragraph of Post Type content. + * + * @since 2.6.8 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefaultNonInlineFormAfterParagraphElement(EndToEndTester $I) + { + // Setup Kit plugin with Default Form for Pages, Posts and Articles set to be output after the 3rd paragraph of content. + $I->setupKitPlugin( + $I, + [ + 'page_form' => $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'], + 'page_form_position' => 'after_element', + 'page_form_position_element' => 'p', + 'page_form_position_element_index' => 3, + 'post_form' => $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'], + 'post_form_position' => 'after_element', + 'post_form_position_element' => 'p', + 'post_form_position_element_index' => 3, + 'article_form' => $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'], + 'article_form_position' => 'after_element', + 'article_form_position_element' => 'p', + 'article_form_position_element_index' => 3, + ] + ); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Setup Post Type with placeholder content. + $pageID = $I->addGutenbergPageToDatabase( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Non-Inline Form: Default: After 3rd Paragraph Element' + ); + + // View the Post Type on the frontend site. + $I->amOnPage('?p=' . $pageID); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form. + $I->seeNumberOfElementsInDOM('form[data-sv-form="' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'] . '"]', 1); + + // Confirm character encoding is not broken due to using DOMDocument. + $I->seeInSource('Adhaésionés altéram improbis mi pariendarum sit stulti triarium'); + + // Confirm no meta tag exists within the content. + $I->dontSeeInSource(''); + + // Confirm no extra , or tags are output i.e. injecting the form doesn't result in DOMDocument adding tags. + $I->seeNoExtraHtmlHeadBodyTagsOutput($I); + } + } + + /** + * Test that the Default Form specified in the Plugin Settings works when + * creating and viewing a new WordPress Page, Post or Article, and its position is set + * to after the 2nd

element of Post Type content. + * + * @since 2.6.6 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefaultFormAfterHeadingElement(EndToEndTester $I) + { + // Setup Kit plugin with Default Form for Pages, Posts and Articles set to be output after the 2nd

of content. + $I->setupKitPlugin( + $I, + [ + 'page_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'page_form_position' => 'after_element', + 'page_form_position_element' => 'h2', + 'page_form_position_element_index' => 2, + 'post_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'post_form_position' => 'after_element', + 'post_form_position_element' => 'h2', + 'post_form_position_element_index' => 2, + 'article_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'article_form_position' => 'after_element', + 'article_form_position_element' => 'h2', + 'article_form_position_element_index' => 2, + ] + ); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Setup Post Type with placeholder content. + $pageID = $I->addGutenbergPageToDatabase( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: Default: After 2nd H2 Element' + ); + + // View the Post Type on the frontend site. + $I->amOnPage('?p=' . $pageID); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm that one Kit Form is output in the DOM after the second

element. + $I->seeFormOutput( + $I, + formID: $_ENV['CONVERTKIT_API_FORM_ID'], + position: 'after_element', + element: 'h2', + elementIndex: 2 + ); + + // Confirm character encoding is not broken due to using DOMDocument. + $I->seeInSource('Adhaésionés altéram improbis mi pariendarum sit stulti triarium'); + + // Confirm no meta tag exists within the content. + $I->dontSeeInSource(''); + + // Confirm no extra , or tags are output i.e. injecting the form doesn't result in DOMDocument adding tags. + $I->seeNoExtraHtmlHeadBodyTagsOutput($I); + } + } + + /** + * Test that the Default Form specified in the Plugin Settings works when + * creating and viewing a new WordPress Page, Post or Article, and its position is set + * to after the 2nd element of Post Type content. + * + * @since 2.6.2 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefaultFormAfterImageElement(EndToEndTester $I) + { + // Setup Kit plugin with Default Form for Pages, Posts and Articles set to be output after the 2nd of content. + $I->setupKitPlugin( + $I, + [ + 'page_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'page_form_position' => 'after_element', + 'page_form_position_element' => 'img', + 'page_form_position_element_index' => 2, + 'post_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'post_form_position' => 'after_element', + 'post_form_position_element' => 'img', + 'post_form_position_element_index' => 2, + 'article_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'article_form_position' => 'after_element', + 'article_form_position_element' => 'img', + 'article_form_position_element_index' => 2, + ] + ); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Setup Post Type with placeholder content. + $pageID = $I->addGutenbergPageToDatabase( + $I, + title: 'Kit: Page: Form: Default: After 2nd Image Element' + ); + + // View the Post Type on the frontend site. + $I->amOnPage('?p=' . $pageID); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm that one Kit Form is output in the DOM after the second element. + $I->seeFormOutput( + $I, + formID: $_ENV['CONVERTKIT_API_FORM_ID'], + position: 'after_element', + element: 'img', + elementIndex: 2 + ); + + // Confirm character encoding is not broken due to using DOMDocument. + $I->seeInSource('Adhaésionés altéram improbis mi pariendarum sit stulti triarium'); + + // Confirm no meta tag exists within the content. + $I->dontSeeInSource(''); + + // Confirm no extra , or tags are output i.e. injecting the form doesn't result in DOMDocument adding tags. + $I->seeNoExtraHtmlHeadBodyTagsOutput($I); + } + } + + /** + * Test that the Default Form specified in the Plugin Settings works when + * creating and viewing a new WordPress Page, Post or Article, and its position is set + * to a number greater than the number of elements in the content. + * + * @since 2.6.2 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefaultFormAfterOutOfBoundsElement(EndToEndTester $I) + { + // Setup Kit plugin with Default Form for Pages, Posts and Articles set to be output after the 9th paragraph of content. + $I->setupKitPlugin( + $I, + [ + 'page_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'page_form_position' => 'after_element', + 'page_form_position_element' => 'p', + 'page_form_position_element_index' => 9, + 'post_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'post_form_position' => 'after_element', + 'post_form_position_element' => 'p', + 'post_form_position_element_index' => 9, + 'article_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'article_form_position' => 'after_element', + 'article_form_position_element' => 'p', + 'article_form_position_element_index' => 9, + ] + ); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Setup Post Type with placeholder content. + $pageID = $I->addGutenbergPageToDatabase( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: Default: After 9th Paragraph Element' + ); + + // View the Post Type on the frontend site. + $I->amOnPage('?p=' . $pageID); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm that one Kit Form is output in the DOM after the content, as + // the number of paragraphs is less than the position. + $I->seeFormOutput( + $I, + formID: $_ENV['CONVERTKIT_API_FORM_ID'], + position: 'after_content' + ); + + // Confirm character encoding is not broken due to using DOMDocument. + $I->seeInSource('Adhaésionés altéram improbis mi pariendarum sit stulti triarium'); + + // Confirm no meta tag exists within the content. + $I->dontSeeInSource(''); + + // Confirm no extra , or tags are output i.e. injecting the form doesn't result in DOMDocument adding tags. + $I->seeNoExtraHtmlHeadBodyTagsOutput($I); + } + } + + /** + * Test that the Default Legacy Form specified in the Plugin Settings works when + * creating and viewing a new WordPress Page, Post or Article. + * + * @since 1.9.6.3 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefaultLegacyForm(EndToEndTester $I) + { + // Setup Plugin with API Key and Secret, which is required for Legacy Forms to work. + $I->setupKitPlugin( + $I, + [ + 'api_key' => $_ENV['CONVERTKIT_API_KEY'], + 'api_secret' => $_ENV['CONVERTKIT_API_SECRET'], + 'page_form' => $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + 'post_form' => $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + 'article_form' => $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], + ] + ); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: Legacy: Default' + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that the Kit Default Legacy Form displays. + $I->seeInSource('
'); + + // Confirm that the Legacy Form title's character encoding is correct. + $I->seeInSource('Vantar þinn ungling sjálfstraust í stærðfræði?'); + } + } + + /** + * Test that 'None' Form specified in the Page Settings works when + * creating and viewing a new WordPress Page, Post or Article. + * + * @since 1.9.6 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingNoForm(EndToEndTester $I) + { + // Setup Kit plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: None' + ); + + // Configure metabox's Form setting = None. + $I->configurePluginSidebarSettings( + $I, + form: 'None', + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that no Kit Form is displayed. + $I->dontSeeElementInDOM('form[data-sv-form]'); + } + } + + /** + * Test that the Form specified in the Page Settings works when + * creating and viewing a new WordPress Page, Post or Article. + * + * @since 1.9.6 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingDefinedForm(EndToEndTester $I) + { + // Setup Kit plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: ' . $_ENV['CONVERTKIT_API_FORM_NAME'] + ); + + // Configure metabox's Form setting = Inline Form. + $I->configurePluginSidebarSettings( + $I, + form: $_ENV['CONVERTKIT_API_FORM_NAME'] + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form. + $I->seeFormOutput($I, $_ENV['CONVERTKIT_API_FORM_ID']); + } + } + + /** + * Test that the Modal Form is output once when the Autoptimize Plugin is active and + * its "Defer JavaScript" setting is enabled for a WordPress Page, Post or Article. + * + * @since 2.4.9 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingModalFormWithAutoptimizePlugin(EndToEndTester $I) + { + // Setup Plugin and Resources. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Activate Autoptimize Plugin. + $I->activateThirdPartyPlugin($I, 'autoptimize'); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: ' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] . ': Autoptimize' + ); + + // Configure metabox's Form setting = Modal Form. + $I->configurePluginSidebarSettings( + $I, + form: $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form, + // and that Autoptimize hasn't moved the script embed to the footer of the site. + $I->seeNumberOfElementsInDOM('form[data-sv-form="' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'] . '"]', 1); + } + + // Deactivate Autoptimize Plugin. + $I->deactivateThirdPartyPlugin($I, 'autoptimize'); + } + + /** + * Test that the Modal Form is output once when the Debloat Plugin is active and + * its "Defer JavaScript" and "Delay All Scripts" settings are enabled for a WordPress Page, Post or Article. + * + * @since 2.8.6 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingModalFormWithDebloatPlugin(EndToEndTester $I) + { + // Setup Plugin and Resources. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Activate Debloat Plugin. + $I->activateThirdPartyPlugin($I, 'disable-_load_textdomain_just_in_time-doing_it_wrong-notice'); + $I->activateThirdPartyPlugin($I, 'debloat'); + + // Enable Debloat's "Defer JavaScript" and "Delay All Scripts" settings. + $I->enableJSDeferDelayAllScriptsDebloatPlugin($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: ' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] . ': Debloat' + ); + + // Configure metabox's Form setting = Modal Form. + $I->configurePluginSidebarSettings( + $I, + form: $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form, + // and that Debloat hasn't moved the script embed to the footer of the site. + $I->seeNumberOfElementsInDOM('form[data-sv-form="' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'] . '"]', 1); + } + + // Deactivate Debloat Plugin. + $I->deactivateThirdPartyPlugin($I, 'debloat'); + $I->deactivateThirdPartyPlugin($I, 'disable-_load_textdomain_just_in_time-doing_it_wrong-notice'); + } + + /** + * Test that the Modal Form is output once when the Jetpack Boost Plugin is active and + * its "Defer Non-Essential JavaScript" setting is enabled for a WordPress Page, Post or Article. + * + * @since 2.4.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingModalFormWithJetpackBoostPlugin(EndToEndTester $I) + { + // Setup Plugin and Resources. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Activate Jetpack Boost Plugin. + $I->activateThirdPartyPlugin($I, 'disable-_load_textdomain_just_in_time-doing_it_wrong-notice'); + $I->activateThirdPartyPlugin($I, 'jetpack-boost'); + + // Enable Jetpack Boost's "Defer Non-Essential JavaScript" setting. + $I->amOnAdminPage('admin.php?page=jetpack-boost'); + $I->click('#inspector-toggle-control-1'); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: ' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] . ': Jetpack Boost' + ); + + // Configure metabox's Form setting = Modal Form. + $I->configurePluginSidebarSettings( + $I, + form: $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form, + // and that Jetpack Boost hasn't moved the script embed to the footer of the site. + $I->seeNumberOfElementsInDOM('form[data-sv-form="' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'] . '"]', 1); + } + + // Deactivate Jetpack Boost Plugin. + $I->deactivateThirdPartyPlugin($I, 'jetpack-boost'); + $I->deactivateThirdPartyPlugin($I, 'disable-_load_textdomain_just_in_time-doing_it_wrong-notice'); + } + + /** + * Test that the Modal Form is output once when the LiteSpeed Cache Plugin is active and + * its "Load JS Deferred" setting is enabled for a WordPress Page, Post or Article. + * + * @since 2.4.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAddNewPostTypeUsingModalFormWithLiteSpeedCachePlugin(EndToEndTester $I) + { + // Setup Kit plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Activate and enable LiteSpeed Cache Plugin. + $I->activateThirdPartyPlugin($I, 'litespeed-cache'); + $I->enableCachingLiteSpeedCachePlugin($I); + + // Enable LiteSpeed Cache's "Load JS Deferred" setting. + $I->enableLiteSpeedCacheLoadJSDeferred($I); + + // Test each Post Type. + foreach ( $this->postTypes as $postType ) { + // Add a Post Type using the Gutenberg editor. + $I->addGutenbergPage( + $I, + postType: $postType, + title: 'Kit: ' . $postType . ': Form: ' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] . ': LiteSpeed Cache' + ); + + // Configure metabox's Form setting = Modal Form. + $I->configurePluginSidebarSettings( + $I, + form: $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_NAME'] + ); + + // Publish and view the Post Type on the frontend site. + $I->publishAndViewGutenbergPage($I); + + // Confirm that one Kit Form is output in the DOM. + // This confirms that there is only one script on the page for this form, which renders the form, + // and that LiteSpeed Cache hasn't moved the script embed to the footer of the site. + $I->seeNumberOfElementsInDOM('form[data-sv-form="' . $_ENV['CONVERTKIT_API_FORM_FORMAT_MODAL_ID'] . '"]', 1); + } + + // Deactivate LiteSpeed Cache Plugin. + $I->deactivateThirdPartyPlugin($I, 'litespeed-cache'); + } + + /** + * Test that the Modal Form