parsely = $parsely; } /** * Registers the dashboard page. * * @since 3.19.0 */ public function run(): void { add_action( 'admin_menu', array( $this, 'add_dashboard_page_to_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_dashboard_page_scripts' ) ); add_filter( 'parent_file', array( $this, 'fix_submenu_highlighting' ) ); add_action( 'wp_ajax_parsely_post_preview', array( $this, 'handle_preview_template' ) ); add_filter( 'the_content', array( $this, 'add_parsely_preview_wrapper' ) ); } /** * Handles the preview template. * * @since 3.19.0 */ public function handle_preview_template(): void { // Verify user capabilities. if ( ! current_user_can( 'edit_posts' ) ) { wp_die( esc_html__( 'You do not have permission to access this preview.', 'wp-parsely' ) ); } // Verify nonce. $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( false === wp_verify_nonce( $nonce, 'parsely_preview' ) ) { wp_die( esc_html__( 'Invalid preview request.', 'wp-parsely' ) ); } $post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : 0; $post = get_post( $post_id ); if ( null === $post ) { wp_die( esc_html__( 'Post not found.', 'wp-parsely' ) ); } // Additional check: verify user can edit this specific post. if ( ! current_user_can( 'edit_post', $post_id ) ) { wp_die( esc_html__( 'You do not have permission to preview this post.', 'wp-parsely' ) ); } // Disable admin bar. show_admin_bar( false ); // phpcs:ignore WordPressVIPMinimum.UserExperience.AdminBarRemoval.RemovalDetected // Set up the minimal editor environment. wp_enqueue_style( 'wp-block-library' ); wp_enqueue_style( 'wp-block-library-theme' ); wp_enqueue_style( 'wp-edit-post' ); if ( has_blocks( $post ) ) { // Get the parsed blocks. $blocks = parse_blocks( $post->post_content ); $block_content = ''; foreach ( $blocks as $block ) { $block_content .= render_block( $block ); } } else { // If the post has no blocks, fallback to the_content filter. $block_content = apply_filters( 'the_content', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } // Get the post title. $post_title = $post->post_title; // Output the preview template. include_once __DIR__ . '/../content-helper/dashboard-page/pages/traffic-boost/preview/preview-post.php'; exit; } /** * Adds a wrapper div for Parse.ly preview functionality. * * @since 3.19.0 * * @param mixed $content The post content. * @return mixed The modified content with wrapper div if needed, or the * unmodified content value in case of a non-string. */ public function add_parsely_preview_wrapper( $content ) { if ( ! is_string( $content ) ) { // $content should always be a string, but is filterable by `the_content`. // If we get a non-string value, return it as is to avoid fatal errors. return $content; } if ( ! isset( $_GET['parsely_preview'] ) || 'true' !== $_GET['parsely_preview'] ) { return $content; } // Validate nonce. $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( false === wp_verify_nonce( $nonce, 'parsely_preview' ) ) { return $content; } // Add a marker class to the content container and add the wrapper as a sibling. return $content . '
'; } /** * Adds the dashboard page to the admin menu. * * @since 3.19.0 */ public function add_dashboard_page_to_menu(): void { $show_traffic_boost_submenu = $this->parsely->api_secret_is_set() && Permissions::current_user_can_use_pch_feature( 'traffic_boost', $this->parsely->get_options()['content_helper'] ); // phpcs:ignore WordPress.WP.Capabilities.Undetermined $show_settings_submenu = current_user_can( Parsely::CAPABILITY ); if ( ! $show_traffic_boost_submenu && ! $show_settings_submenu ) { return; } // Base64-encoded version of leaf-icon.tsx with size of 20 and fill of #1d2327. // . $icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA2MCA2NSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNMjMuNzIsNTEuNTNjMC0uMTgsMC0uMzQtLjA2LS41MmExMy4xMSwxMy4xMSwwLDAsMC0yLjEtNS41M0ExNC43NCwxNC43NCwwLDAsMCwxOS4xMiw0M2MtLjI3LS4yMS0uNS0uMTEtLjUxLjIybC0uMjQsMy40MmMwLC4zMy0uMzguMzUtLjQ5LDBsLTEuNS00LjhhMS40LDEuNCwwLDAsMC0uNzctLjc4LDIzLjkxLDIzLjkxLDAsMCwwLTMuMS0uODRjLTEuMzgtLjI0LTMuMzktLjM5LTMuMzktLjM5LS4zNCwwLS40NS4yMS0uMjUuNDlsMi4wNiwzLjc2Yy4yLjI3LDAsLjU0LS4yOS4zM2wtNC41MS0zLjZhMy42OCwzLjY4LDAsMCwwLTIuODYtLjQ4Yy0xLC4xNi0yLjQ0LjQ2LTIuNDQuNDZhLjY4LjY4LDAsMCwwLS4zOS4yNS43My43MywwLDAsMC0uMTQuNDVTLjQxLDQzLC41NCw0NGEzLjYzLDMuNjMsMCwwLDAsMS4yNSwyLjYyTDYuNDgsNTBjLjI4LjIuMDkuNDktLjIzLjM3bC00LjE4LS45NGMtLjMyLS4xMi0uNSwwLS40LjM3LDAsMCwuNjksMS44OSwxLjMxLDMuMTZhMjQsMjQsMCwwLDAsMS42NiwyLjc0LDEuMzQsMS4zNCwwLDAsMCwxLC41Mmw1LC4xM2MuMzMsMCwuNDEuMzguMS40OEw3LjUxLDU4Yy0uMzEuMS0uMzQuMzUtLjA3LjU1YTE0LjI5LDE0LjI5LDAsMCwwLDMuMDUsMS42NiwxMy4wOSwxMy4wOSwwLDAsMCw1LjkuNSwyNS4xMywyNS4xMywwLDAsMCw0LjM0LTEsOS41NSw5LjU1LDAsMCwxLS4wOC0xLjIsOS4zMiw5LjMyLDAsMCwxLDMuMDctNi45MSI+PC9wYXRoPjxwYXRoIGZpbGw9IiMxZDIzMjciIGQ9Ik01OS43LDQxLjUzYS43My43MywwLDAsMC0uMTQtLjQ1LjY4LjY4LDAsMCwwLS4zOS0uMjVzLTEuNDMtLjMtMi40NC0uNDZhMy42NCwzLjY0LDAsMCwwLTIuODYuNDhsLTQuNTEsMy42Yy0uMjYuMjEtLjQ5LS4wNi0uMjktLjMzbDIuMDYtMy43NmMuMi0uMjguMDktLjQ5LS4yNS0uNDksMCwwLTIsLjE1LTMuMzkuMzlhMjMuOTEsMjMuOTEsMCwwLDAtMy4xLjg0LDEuNCwxLjQsMCwwLDAtLjc3Ljc4bC0xLjUsNC44Yy0uMTEuMzItLjQ4LjMtLjQ5LDBsLS4yNC0zLjQyYzAtLjMzLS4yNC0uNDMtLjUxLS4yMmExNC43NCwxNC43NCwwLDAsMC0yLjQ0LDIuNDdBMTMuMTEsMTMuMTEsMCwwLDAsMzYuMzQsNTFjMCwuMTgsMCwuMzQtLjA2LjUyYTkuMjYsOS4yNiwwLDAsMSwzLDguMSwyNC4xLDI0LjEsMCwwLDAsNC4zNCwxLDEzLjA5LDEzLjA5LDAsMCwwLDUuOS0uNSwxNC4yOSwxNC4yOSwwLDAsMCwzLjA1LTEuNjZjLjI3LS4yLjI0LS40NS0uMDctLjU1bC0zLjIyLTEuMTdjLS4zMS0uMS0uMjMtLjQ3LjEtLjQ4bDUtLjEzYTEuMzgsMS4zOCwwLDAsMCwxLS41MkEyNC42LDI0LjYsMCwwLDAsNTcsNTIuOTJjLjYxLTEuMjcsMS4zMS0zLjE2LDEuMzEtMy4xNi4xLS4zMy0uMDgtLjQ5LS40LS4zN2wtNC4xOC45NGMtLjMyLjEyLS41MS0uMTctLjIzLS4zN2w0LjY5LTMuMzRBMy42MywzLjYzLDAsMCwwLDU5LjQ2LDQ0Yy4xMy0xLC4yNC0yLjQ3LjI0LTIuNDciPjwvcGF0aD48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNNDYuNSwyNS42MWMwLS41My0uMzUtLjcyLS44LS40M2wtNC44NiwyLjY2Yy0uNDUuMjgtLjU2LS4yNy0uMjMtLjY5bDQuNjYtNi4yM2EyLDIsMCwwLDAsLjI4LTEuNjgsMzYuNTEsMzYuNTEsMCwwLDAtMi4xOS00Ljg5LDM0LDM0LDAsMCwwLTIuODEtMy45NGMtLjMzLS40MS0uNzQtLjM1LS45MS4xNmwtMi4yOCw1LjY4Yy0uMTYuNS0uNi40OC0uNTktLjA1bC4yOC04LjkzYTIuNTQsMi41NCwwLDAsMC0uNjYtMS42NFMzNSw0LjI3LDMzLjg4LDMuMjcsMzAuNzguNjksMzAuNzguNjlhMS4yOSwxLjI5LDAsMCwwLTEuNTQsMHMtMS44OCwxLjQ5LTMuMTIsMi41OS0yLjQ4LDIuMzUtMi40OCwyLjM1QTIuNSwyLjUsMCwwLDAsMjMsNy4yN2wuMjcsOC45M2MwLC41My0uNDEuNTUtLjU4LjA1bC0yLjI5LTUuNjljLS4xNy0uNS0uNTctLjU2LS45MS0uMTRhMzUuNzcsMzUuNzcsMCwwLDAtMyw0LjIsMzUuNTUsMzUuNTUsMCwwLDAtMiw0LjYyLDIsMiwwLDAsMCwuMjcsMS42N2w0LjY3LDYuMjRjLjMzLjQyLjIzLDEtLjIyLjY5bC00Ljg3LTIuNjZjLS40NS0uMjktLjgyLS4xLS44Mi40M2ExOC42LDE4LjYsMCwwLDAsLjgzLDUuMDcsMjAuMTYsMjAuMTYsMCwwLDAsNS4zNyw3Ljc3YzMuMTksMyw1LjkzLDcuOCw3LjQ1LDExLjA4QTkuNiw5LjYsMCwwLDEsMzAsNDkuMDlhOS4zMSw5LjMxLDAsMCwxLDIuODYuNDVjMS41Mi0zLjI4LDQuMjYtOC4xMSw3LjQ0LTExLjA5YTIwLjQ2LDIwLjQ2LDAsMCwwLDUuMDktNywxOSwxOSwwLDAsMCwxLjExLTUuODIiPjwvcGF0aD48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNMzYuMTIsNTguNDRBNi4xMiw2LjEyLDAsMSwxLDMwLDUyLjMyYTYuMTEsNi4xMSwwLDAsMSw2LjEyLDYuMTIiPjwvcGF0aD48L3N2Zz4='; // The capability is irrelevant here (but required by the functions), as // whether to display the menu/submenu is dictated by the // $show_settings_submenu and $show_traffic_boost_submenu variables. $capability = 'edit_posts'; // Capability in Contributor role and above. add_menu_page( 'Parse.ly', 'Parse.ly', $capability, // phpcs:ignore WordPress.WP.Capabilities.Undetermined 'parsely-dashboard-page', array( $this, 'add_dashboard_page_placeholder' ), $icon, 30 ); add_submenu_page( 'parsely-dashboard-page', 'Parse.ly Traffic Boost', 'Traffic Boost (beta)', $capability, // phpcs:ignore WordPress.WP.Capabilities.Undetermined 'parsely-dashboard-page', '__return_null' ); // Always make the menu display a submenu, even if it's only one item. // This temporary submenu gets removed before displaying the final menu. add_submenu_page( 'parsely-dashboard-page', 'Temporary', 'Temporary', $capability, // phpcs:ignore WordPress.WP.Capabilities.Undetermined 'parsely-dashboard-page-temporary', '__return_null' ); /** * Settings submenu is registered in add_settings_sub_menu() at * src/UI/class-settings-page.php. * * @see Settings_Page::add_settings_sub_menu() */ // Remove the submenus that shouldn't be displayed. add_action( 'admin_head', function () use ( $show_traffic_boost_submenu, $show_settings_submenu ) { $dashboard_page_slug = 'parsely-dashboard-page'; // Always remove the temporary submenu. remove_submenu_page( $dashboard_page_slug, 'parsely-dashboard-page-temporary' ); if ( ! $show_traffic_boost_submenu ) { // Remove the Traffic Boost submenu. remove_submenu_page( $dashboard_page_slug, $dashboard_page_slug ); } if ( ! $show_settings_submenu ) { // Remove the Settings submenu. remove_submenu_page( $dashboard_page_slug, Parsely::MENU_SLUG ); } } ); } /** * Adds a placeholder for the dashboard page to render into. * * @since 3.19.0 */ public function add_dashboard_page_placeholder(): void { echo ''; } /** * Fixes the highlighting of the submenu items. * * This removes the highlighting from the submenu items when the dashboard page is active, so it can * later be added by the React app. * * @since 3.19.0 * * @param string $parent_file The parent file. * @return string The parent file. */ public function fix_submenu_highlighting( string $parent_file ): string { global $submenu_file, $current_screen; if ( 'toplevel_page_parsely-dashboard-page' === $current_screen->base ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $submenu_file = 'non-existent-slug'; } return $parent_file; } /** * Enqueues all needed scripts and styles for the dashboard page. * * @since 3.19.0 * * @param ?string $hook_suffix The current page being loaded. */ public function enqueue_dashboard_page_scripts( ?string $hook_suffix ): void { // Only load the scripts on the dashboard page. if ( 'toplevel_page_parsely-dashboard-page' !== $hook_suffix ) { return; } $asset_info = Utils::get_asset_info( 'build/content-helper/dashboard-page.asset.php' ); $built_assets_url = plugin_dir_url( PARSELY_FILE ) . '/build/content-helper/'; wp_enqueue_script( 'parsely-dashboard-page', $built_assets_url . 'dashboard-page.js', $asset_info['dependencies'], $asset_info['version'], false ); // Inline scripts must be injected after enqueueing the main script. $this->inject_content_helper_permissions(); $this->inject_traffic_boost_settings(); wp_enqueue_style( 'wp-components' ); wp_enqueue_style( 'parsely-dashboard-page', $built_assets_url . 'dashboard-page.css', array(), $asset_info['version'] ); $inline_script_lines = array( 'window.wpParselyDependencies = {};', sprintf( 'window.wpParselyDependencies.urlWpComponents = %s;', wp_json_encode( includes_url( 'css/dist/components/style.css' ) ) ), sprintf( 'window._parsely_traffic_boost_preview_nonce = %s;', wp_json_encode( wp_create_nonce( 'parsely_preview' ) ) ), ); wp_add_inline_script( 'parsely-dashboard-page', join( "\n", $inline_script_lines ), 'before' ); if ( $this->parsely->site_id_is_set() ) { wp_add_inline_script( 'parsely-dashboard-page', 'window.wpParselySiteId = ' . wp_json_encode( $this->parsely->get_site_id() ) . ';', 'before' ); } } /** * Injects Content Helper permissions into the dashboard page. * * @since 3.19.0 */ protected function inject_content_helper_permissions(): void { $permissions_json = Permissions::get_pch_permissions_json( $this->parsely->get_options()['content_helper'] ); wp_add_inline_script( 'parsely-dashboard-page', "window.wpParselyContentHelperPermissions = '$permissions_json';", 'before' ); } /** * Injects Traffic Boost settings into the dashboard page. * * @since 3.19.0 */ protected function inject_traffic_boost_settings(): void { $settings = ''; if ( ! defined( 'INTEGRATION_TESTS_RUNNING' ) ) { $settings = rest_do_request( new WP_REST_Request( 'GET', '/wp-parsely/v2/settings/traffic-boost' ) )->get_data(); } if ( ! is_array( $settings ) ) { $settings = array(); } $settings = wp_json_encode( $settings ); wp_add_inline_script( 'parsely-dashboard-page', "window.wpParselyContentHelperSettings = '$settings';", 'before' ); } }