plugin_name = $plugin_name; $this->version = $version; // Redirect to setup wizard on first activation. add_action( 'admin_init', array( $this, 'maybe_redirect_to_setup' ) ); // Suppress third-party admin notices on our plugin pages. add_action( 'in_admin_header', array( $this, 'suppress_third_party_notices' ), 1 ); // AJAX handler for verifying API key (server-side to avoid CORS). add_action( 'wp_ajax_schema_ai_verify_key', array( $this, 'ajax_verify_key' ) ); // AJAX handler for dismissing the local-business info banner. add_action( 'wp_ajax_schema_ai_dismiss_business_banner', array( $this, 'ajax_dismiss_business_banner' ) ); // AJAX handler for manually triggering a dashboard sync. add_action( 'wp_ajax_schema_ai_force_sync', array( $this, 'ajax_force_sync' ) ); // Show admin notice if DB tables are still missing after auto-create. add_action( 'admin_notices', array( $this, 'maybe_show_db_notice' ) ); } /** * Show an admin notice to admins if the primary DB table is missing. * * Fires AFTER schema_ai_run() has already attempted maybe_create_tables(), * so if the table is still absent it means create_tables() itself failed * (e.g. insufficient MySQL privileges). The notice tells the admin what * happened without requiring access to debug.log. */ public function maybe_show_db_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } global $wpdb; $table = $wpdb->prefix . 'schema_ai_schemas'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- SHOW TABLES is safe; admin-only; not on user data. $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table; if ( $exists ) { return; } $last_error = $wpdb->last_error ? esc_html( $wpdb->last_error ) : __( 'Unknown error — check wp-content/debug.log', 'ai-schema-gen' ); echo '

'; echo '' . esc_html__( 'AI Schema Gen — Database Error', 'ai-schema-gen' ) . '
'; /* translators: %s: MySQL error message */ printf( esc_html__( 'The plugin\'s database tables could not be created. MySQL said: %s', 'ai-schema-gen' ), '' . $last_error . '' ); echo '
' . esc_html__( 'Please deactivate and reactivate the plugin, or contact your host to confirm your database user has CREATE TABLE privileges.', 'ai-schema-gen' ); echo '

'; } /** * AJAX handler: verify an API key by calling the SaaS backend server-side. */ public function ajax_verify_key() { check_ajax_referer( 'schema_ai_verify_key' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ai-schema-gen' ) ) ); } $api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : ''; if ( empty( $api_key ) ) { wp_send_json_error( array( 'message' => __( 'Please enter an API key.', 'ai-schema-gen' ) ) ); } $response = wp_remote_get( SCHEMA_AI_API_BASE . '/auth/me', array( 'headers' => array( 'X-API-Key' => $api_key ), 'timeout' => 15, ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'message' => __( 'Could not connect to AI Schema Gen server.', 'ai-schema-gen' ) ) ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 === $code && ! empty( $body['success'] ) && ! empty( $body['data']['user'] ) ) { $plan = isset( $body['data']['user']['plan'] ) ? $body['data']['user']['plan'] : 'free'; update_user_meta( get_current_user_id(), 'schema_ai_plan', $plan ); wp_send_json_success( array( 'plan' => $plan ) ); } else { $msg = isset( $body['error'] ) ? $body['error'] : __( 'Invalid API key.', 'ai-schema-gen' ); wp_send_json_error( array( 'message' => $msg ) ); } } /** * AJAX handler: dismiss the local-business info banner permanently. */ public function ajax_dismiss_business_banner() { check_ajax_referer( 'schema_ai_dismiss_business_banner' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } $settings = get_option( 'schema_ai_settings', array() ); $settings['not_local_business'] = true; update_option( 'schema_ai_settings', $settings ); wp_send_json_success(); } /** * AJAX handler: manually trigger a full site sync to the SaaS dashboard. */ public function ajax_force_sync() { check_ajax_referer( 'schema_ai_force_sync' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ai-schema-gen' ) ) ); } if ( ! class_exists( 'Schema_AI_Sync' ) ) { wp_send_json_error( array( 'message' => __( 'Sync class not available.', 'ai-schema-gen' ) ) ); } $sync = new Schema_AI_Sync(); $result = $sync->run_sync(); if ( $result ) { wp_send_json_success( array( 'message' => __( 'Sync complete. Your dashboard will update shortly.', 'ai-schema-gen' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Sync failed. Please check your API key and try again.', 'ai-schema-gen' ) ) ); } } /** * Redirect to setup wizard on first activation. */ public function maybe_redirect_to_setup() { if ( ! get_transient( 'schema_ai_activation_redirect' ) ) { return; } delete_transient( 'schema_ai_activation_redirect' ); // Don't redirect on multisite activation or AJAX. if ( is_network_admin() || isset( $_GET['activate-multi'] ) || wp_doing_ajax() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // Only redirect if setup not completed. $settings = get_option( 'schema_ai_settings', array() ); if ( ! empty( $settings['api_key'] ) ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=ai-schema-gen-setup' ) ); exit; } /** * Register the admin menu and submenu pages. * * Creates a top-level "Schema AI" menu with Dashboard, All Schemas, * and Settings submenus. */ public function add_admin_menu() { // Top-level menu. add_menu_page( __( 'AI Schema Gen', 'ai-schema-gen' ), __( 'AI Schema Gen', 'ai-schema-gen' ), 'edit_posts', 'ai-schema-gen', array( $this, 'display_dashboard_page' ), 'dashicons-editor-code', 30 ); // Dashboard submenu (replaces the default duplicate). add_submenu_page( 'ai-schema-gen', __( 'Dashboard', 'ai-schema-gen' ), __( 'Dashboard', 'ai-schema-gen' ), 'edit_posts', 'ai-schema-gen', array( $this, 'display_dashboard_page' ) ); // Schema Tester submenu. add_submenu_page( 'ai-schema-gen', __( 'Schema Tester', 'ai-schema-gen' ), __( 'Schema Tester', 'ai-schema-gen' ), 'edit_posts', 'ai-schema-gen-tester', array( $this, 'display_tester_page' ) ); // Settings submenu (requires manage_options). add_submenu_page( 'ai-schema-gen', __( 'Settings', 'ai-schema-gen' ), __( 'Settings', 'ai-schema-gen' ), 'manage_options', 'ai-schema-gen-settings', array( $this, 'display_settings_page' ) ); // Upgrade submenu. add_submenu_page( 'ai-schema-gen', __( 'Upgrade', 'ai-schema-gen' ), __( 'Upgrade', 'ai-schema-gen' ), 'edit_posts', 'ai-schema-gen-upgrade', array( $this, 'display_upgrade_page' ) ); // Schema Audit submenu. add_submenu_page( 'ai-schema-gen', __( 'Schema Audit', 'ai-schema-gen' ), __( 'Schema Audit', 'ai-schema-gen' ), 'manage_options', 'ai-schema-gen-audit', array( $this, 'display_audit_page' ) ); // Setup wizard (hidden from menu). add_submenu_page( null, __( 'AI Schema Gen Setup', 'ai-schema-gen' ), __( 'Setup', 'ai-schema-gen' ), 'manage_options', 'ai-schema-gen-setup', array( $this, 'display_setup_page' ) ); } /** * Register the stylesheets for the admin area. * * Loads admin CSS only on Schema AI pages and post edit screens * where the meta box is displayed. * * @param string $hook The current admin page hook suffix. */ public function enqueue_styles( $hook ) { // Load on Schema AI admin pages. $is_schema_ai_page = ( false !== strpos( $hook, 'ai-schema-gen' ) ); // Load on post edit screens for the meta box. $is_edit_screen = in_array( $hook, array( 'post.php', 'post-new.php' ), true ); if ( ! $is_schema_ai_page && ! $is_edit_screen ) { return; } wp_enqueue_style( 'ai-schema-gen-admin', plugin_dir_url( __FILE__ ) . 'assets/css/admin.css', array(), $this->version ); // Entity Authority repeater styles (settings page only). if ( false !== strpos( $hook, 'ai-schema-gen-settings' ) ) { wp_add_inline_style( 'ai-schema-gen-admin', '.ai-schema-gen-repeater-row{display:flex;gap:8px;margin-bottom:8px;align-items:center;}' . '.ai-schema-gen-repeater-row .regular-text{flex:1;margin:0;}' . '.ai-schema-gen-remove-row{flex-shrink:0;line-height:1;padding:0 8px;font-size:16px;}' ); } // Setup wizard styles. if ( false !== strpos( $hook, 'ai-schema-gen-setup' ) ) { wp_enqueue_style( 'ai-schema-gen-setup-wizard', plugin_dir_url( __FILE__ ) . 'assets/css/setup-wizard.css', array(), $this->version ); } } /** * Register the JavaScript for the admin area. * * Loads admin JS only on Schema AI pages and post edit screens. * Localizes script with REST API data, user plan, and settings. * * @param string $hook The current admin page hook suffix. */ public function enqueue_scripts( $hook ) { // Load on Schema AI admin pages. $is_schema_ai_page = ( false !== strpos( $hook, 'ai-schema-gen' ) ); // Load on post edit screens for the meta box. $is_edit_screen = in_array( $hook, array( 'post.php', 'post-new.php' ), true ); if ( ! $is_schema_ai_page && ! $is_edit_screen ) { return; } // Load the WordPress media library on the settings page (for logo upload). if ( false !== strpos( $hook, 'ai-schema-gen-settings' ) ) { wp_enqueue_media(); } wp_enqueue_script( 'ai-schema-gen-admin', plugin_dir_url( __FILE__ ) . 'assets/js/build/index.js', array( 'wp-element', 'wp-components', 'wp-i18n', 'wp-api-fetch' ), $this->version, true ); $current_user_id = get_current_user_id(); $plan = get_user_meta( $current_user_id, 'schema_ai_plan', true ); // Get site logo for defaults. $site_logo = ''; $custom_logo_id = get_theme_mod( 'custom_logo' ); if ( $custom_logo_id ) { $logo_src = wp_get_attachment_image_src( $custom_logo_id, 'full' ); if ( $logo_src ) { $site_logo = $logo_src[0]; } } // Settings page script. if ( false !== strpos( $hook, 'ai-schema-gen-settings' ) ) { wp_enqueue_script( 'ai-schema-gen-settings-page', plugin_dir_url( __FILE__ ) . 'assets/js/settings-page.js', array( 'jquery' ), $this->version, true ); wp_localize_script( 'ai-schema-gen-settings-page', 'schemaAiSettings', array( 'ajaxUrl' => esc_url( admin_url( 'admin-ajax.php' ) ), 'nonce' => wp_create_nonce( 'schema_ai_verify_key' ), 'syncNonce' => wp_create_nonce( 'schema_ai_force_sync' ), 'i18n' => array( 'enterApiKey' => __( 'Please enter an API key first.', 'ai-schema-gen' ), 'verifying' => __( 'Verifying...', 'ai-schema-gen' ), 'connected' => __( 'Connected successfully!', 'ai-schema-gen' ), 'connectionFailed' => __( 'Connection failed.', 'ai-schema-gen' ), 'connectionFailedRetry' => __( 'Connection failed. Please try again.', 'ai-schema-gen' ), 'selectLogo' => __( 'Select Organization Logo', 'ai-schema-gen' ), 'useLogo' => __( 'Use as Logo', 'ai-schema-gen' ), 'syncing' => __( 'Syncing...', 'ai-schema-gen' ), 'syncSuccess' => __( 'Sync complete. Dashboard will update shortly.', 'ai-schema-gen' ), 'syncFailed' => __( 'Sync failed. Check your API key and try again.', 'ai-schema-gen' ), ), ) ); // Site type dropdown + entity authority repeater scripts. $settings_raw = get_option( 'schema_ai_settings', array() ); wp_enqueue_script( 'ai-schema-gen-settings-site-type', plugin_dir_url( __FILE__ ) . 'assets/js/settings-site-type.js', array(), $this->version, true ); wp_localize_script( 'ai-schema-gen-settings-site-type', 'schemaAISiteTypeData', array( 'localSlugs' => class_exists( 'Schema_AI_Site_Type' ) ? Schema_AI_Site_Type::get_local_business_slugs() : array(), 'currentSiteType' => isset( $settings_raw['site_type'] ) ? $settings_raw['site_type'] : '', ) ); wp_enqueue_script( 'ai-schema-gen-settings-repeater', plugin_dir_url( __FILE__ ) . 'assets/js/settings-repeater.js', array(), $this->version, true ); } // Setup wizard script. if ( false !== strpos( $hook, 'ai-schema-gen-setup' ) ) { wp_enqueue_script( 'ai-schema-gen-setup-wizard', plugin_dir_url( __FILE__ ) . 'assets/js/setup-wizard.js', array( 'jquery' ), $this->version, true ); wp_localize_script( 'ai-schema-gen-setup-wizard', 'schemaAiWizard', array( 'nonce' => wp_create_nonce( 'schema_ai_verify_key' ), 'i18n' => array( 'enterApiKey' => __( 'Please enter an API key.', 'ai-schema-gen' ), 'verifying' => __( 'Verifying...', 'ai-schema-gen' ), 'connected' => __( 'Connected!', 'ai-schema-gen' ), 'plan' => __( 'Plan:', 'ai-schema-gen' ), 'invalidKey' => __( 'Invalid API key. Please check and try again.', 'ai-schema-gen' ), 'couldNotVerify' => __( 'Could not verify. Check your API key or try again later.', 'ai-schema-gen' ), 'verifyKey' => __( 'Verify Key', 'ai-schema-gen' ), ), ) ); } // Setup wizard: business subtype dropdown. if ( false !== strpos( $hook, 'ai-schema-gen-setup' ) ) { wp_enqueue_script( 'ai-schema-gen-setup-wizard-subtype', plugin_dir_url( __FILE__ ) . 'assets/js/setup-wizard-subtype.js', array(), $this->version, true ); wp_localize_script( 'ai-schema-gen-setup-wizard-subtype', 'schemaAIWizardData', array( 'localSlugs' => class_exists( 'Schema_AI_Site_Type' ) ? Schema_AI_Site_Type::get_local_business_slugs() : array(), ) ); } // Schema audit page: per-row regenerate button. if ( false !== strpos( $hook, 'ai-schema-gen-audit' ) ) { wp_enqueue_script( 'ai-schema-gen-schema-audit', plugin_dir_url( __FILE__ ) . 'assets/js/schema-audit.js', array(), $this->version, true ); wp_localize_script( 'ai-schema-gen-schema-audit', 'schemaAIAudit', array( 'regenerating' => __( 'Regenerating…', 'ai-schema-gen' ), 'done' => __( 'Done — reload to verify', 'ai-schema-gen' ), 'failed' => __( 'Failed — retry', 'ai-schema-gen' ), ) ); } $entity_score = class_exists( 'Schema_AI_Entity_Settings' ) ? Schema_AI_Entity_Settings::calculate_score() : array( 'score' => 0, 'breakdown' => array(), 'missing' => array() ); wp_localize_script( 'ai-schema-gen-admin', 'schemaAiData', array( 'restUrl' => esc_url( rest_url( 'ai-schema-gen/v1/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'postId' => get_the_ID(), 'postUrl' => get_the_ID() ? esc_url( get_permalink( get_the_ID() ) ) : '', 'adminUrl' => esc_url( admin_url() ), 'upgradeUrl' => esc_url( admin_url( 'admin.php?page=ai-schema-gen-upgrade' ) ), 'pluginUrl' => esc_url( SCHEMA_AI_PLUGIN_URL ), 'version' => $this->version, 'plan' => ! empty( $plan ) ? $plan : 'free', 'settings' => get_option( 'schema_ai_settings', array() ), 'activationTime' => (int) get_option( 'schema_ai_activation_time', 0 ), 'dismissBannerNonce' => wp_create_nonce( 'schema_ai_dismiss_business_banner' ), 'entityScore' => $entity_score, 'wpDefaults' => array( 'siteName' => get_bloginfo( 'name' ), 'siteUrl' => home_url( '/' ), 'siteLogo' => $site_logo, ), ) ); } /** * Register the schema meta box for post and page editors. * * Also adds the meta box for custom post types if configured in settings. */ public function add_schema_meta_box() { $post_types = array( 'post', 'page' ); // Include additional post types from settings. $settings = get_option( 'schema_ai_settings', array() ); if ( ! empty( $settings['auto_generate_post_types'] ) && is_array( $settings['auto_generate_post_types'] ) ) { $post_types = array_unique( array_merge( $post_types, $settings['auto_generate_post_types'] ) ); } foreach ( $post_types as $post_type ) { add_meta_box( 'ai-schema-gen-meta-box', __( 'AI Schema Gen', 'ai-schema-gen' ), array( $this, 'render_meta_box' ), $post_type, 'normal', 'high' ); } } /** * Render the content of the schema meta box. * * Outputs a nonce field and includes the meta-box partial template * which provides the UI for schema generation and preview. * * @param WP_Post $post The current post object. */ public function render_meta_box( $post ) { wp_nonce_field( 'schema_ai_meta_box', 'schema_ai_meta_box_nonce' ); include plugin_dir_path( __FILE__ ) . 'partials/meta-box.php'; } /** * Save the schema meta box data when a post is saved. * * Verifies the nonce, checks for autosave and permissions, then * sanitizes and stores any schema data submitted from the meta box. * * @param int $post_id The post ID being saved. * @param WP_Post $post The post object being saved. */ public function save_schema_meta_box( $post_id, $post ) { // Verify nonce. if ( ! isset( $_POST['schema_ai_meta_box_nonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['schema_ai_meta_box_nonce'] ) ), 'schema_ai_meta_box' ) ) { return; } // Check for autosave. if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Check user permissions. $post_type_object = get_post_type_object( $post->post_type ); if ( ! $post_type_object ) { return; } if ( ! current_user_can( $post_type_object->cap->edit_post, $post_id ) ) { return; } // Check if schema data was submitted. if ( ! isset( $_POST['schema_ai_data'] ) ) { return; } $raw_data = sanitize_text_field( wp_unslash( $_POST['schema_ai_data'] ) ); // Validate that the data is valid JSON. $schema_data = json_decode( $raw_data, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $schema_data ) ) { return; } // Sanitize the schema data recursively. $schema_data = $this->sanitize_schema_data( $schema_data ); // Save to the custom table. global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_schemas' ); // Determine schema type. $schema_type = isset( $schema_data['@type'] ) ? sanitize_text_field( $schema_data['@type'] ) : 'Unknown'; // Check if a schema already exists for this post. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $existing = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT id FROM {$table} WHERE post_id = %d AND schema_type = %s LIMIT 1", $post_id, $schema_type ) ); $json_data = wp_json_encode( $schema_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( $existing ) { // Update existing schema. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (UPDATE), caching not applicable. $wpdb->update( $table, array( 'schema_data' => $json_data, 'validation_status' => 'pending', 'updated_at' => current_time( 'mysql' ), ), array( 'id' => $existing ), array( '%s', '%s', '%s' ), array( '%d' ) ); } else { // Insert new schema. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; write operation (INSERT), caching not applicable. $wpdb->insert( $table, array( 'post_id' => $post_id, 'schema_type' => $schema_type, 'schema_data' => $json_data, 'is_auto_generated' => 0, 'validation_status' => 'pending', 'created_at' => current_time( 'mysql' ), 'updated_at' => current_time( 'mysql' ), ), array( '%d', '%s', '%s', '%d', '%s', '%s', '%s' ) ); } // Invalidate the schema cache for this post. if ( class_exists( 'Schema_AI_Schema_Cache' ) ) { $cache = new Schema_AI_Schema_Cache(); $cache->invalidate( $post_id ); } } /** * Display the Dashboard admin page. * * Outputs a wrapper div with a mount point for the React application. */ public function display_dashboard_page() { echo '
'; } /** * Display the Schema Tester admin page. */ public function display_tester_page() { echo '
'; } /** * Display the Upgrade admin page. */ public function display_upgrade_page() { echo '
'; } /** * Display the Schema Audit admin page. */ public function display_audit_page() { include plugin_dir_path( __FILE__ ) . 'partials/schema-audit.php'; } /** * Display the Settings admin page. * * Includes the settings partial template which handles form display * and submission processing. */ public function display_settings_page() { include plugin_dir_path( __FILE__ ) . 'partials/settings-page.php'; } /** * Display the Setup Wizard page. */ public function display_setup_page() { include plugin_dir_path( __FILE__ ) . 'partials/setup-wizard.php'; } /** * Add plugin action links on the Plugins page. * * Adds "Settings" and "Upgrade" links to the plugin row. * * @param array $links Existing plugin action links. * @return array Modified plugin action links. */ public function add_action_links( $links ) { $settings_link = sprintf( '%s', esc_url( admin_url( 'admin.php?page=ai-schema-gen-settings' ) ), esc_html__( 'Settings', 'ai-schema-gen' ) ); $upgrade_link = sprintf( '%s', esc_url( admin_url( 'admin.php?page=ai-schema-gen-upgrade' ) ), esc_html__( 'Upgrade', 'ai-schema-gen' ) ); array_unshift( $links, $settings_link, $upgrade_link ); return $links; } /** * Add a "Generate Schema" button to the WordPress admin bar on singular pages. * * @param WP_Admin_Bar $wp_admin_bar The admin bar instance. */ public function add_admin_bar_generate( $wp_admin_bar ) { if ( is_admin() || ! is_singular() || ! is_admin_bar_showing() ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { return; } $wp_admin_bar->add_node( array( 'id' => 'ai-schema-gen-generate', 'title' => '' . esc_html__( 'Generate Schema', 'ai-schema-gen' ) . '', 'href' => '#', ) ); // Dropdown child: Test with Google Rich Results $wp_admin_bar->add_node( array( 'id' => 'ai-schema-gen-test-google', 'parent' => 'ai-schema-gen-generate', 'title' => '' . esc_html__( 'Test with Google', 'ai-schema-gen' ), 'href' => 'https://search.google.com/test/rich-results?url=' . rawurlencode( get_permalink( $post_id ) ), 'meta' => array( 'target' => '_blank', 'rel' => 'noopener noreferrer', ), ) ); // Dropdown child: Explore Plugin (goes to the plugin dashboard on this site) $wp_admin_bar->add_node( array( 'id' => 'ai-schema-gen-explore', 'parent' => 'ai-schema-gen-generate', 'title' => '' . esc_html__( 'Explore Plugin', 'ai-schema-gen' ), 'href' => admin_url( 'admin.php?page=ai-schema-gen' ), ) ); } /** * Enqueue inline script and styles for the admin bar Generate Schema button. */ public function enqueue_admin_bar_script() { if ( is_admin() || ! is_singular() || ! is_admin_bar_showing() || ! is_user_logged_in() ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { return; } $js = sprintf( '(function(){' . 'var btn=document.getElementById("wp-admin-bar-ai-schema-gen-generate");' . 'if(!btn)return;' . 'var link=btn.querySelector("a");' . 'if(!link)return;' . 'var orig=link.innerHTML;' . 'link.addEventListener("click",function(e){' . 'e.preventDefault();' . 'if(btn.classList.contains("ai-schema-gen-ab-loading"))return;' . 'btn.classList.add("ai-schema-gen-ab-loading");' . 'btn.classList.remove("ai-schema-gen-ab-success","ai-schema-gen-ab-error");' . 'link.innerHTML="Generating\u2026";' . 'fetch("%s",{' . 'method:"POST",' . 'headers:{"Content-Type":"application/json","X-WP-Nonce":"%s"},' . 'credentials:"same-origin",' . 'body:JSON.stringify({post_id:%d})' . '}).then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d}})})' . '.then(function(res){' . 'btn.classList.remove("ai-schema-gen-ab-loading");' . 'if(res.ok&&res.data.success){' . 'btn.classList.add("ai-schema-gen-ab-success");' . 'link.innerHTML="Schema Generated";' . '}else{' . 'btn.classList.add("ai-schema-gen-ab-error");' . 'link.innerHTML="Failed";' . '}' . 'setTimeout(function(){' . 'btn.classList.remove("ai-schema-gen-ab-success","ai-schema-gen-ab-error");' . 'link.innerHTML=orig;' . '},3000);' . '}).catch(function(){' . 'btn.classList.remove("ai-schema-gen-ab-loading");' . 'btn.classList.add("ai-schema-gen-ab-error");' . 'link.innerHTML="Failed";' . 'setTimeout(function(){' . 'btn.classList.remove("ai-schema-gen-ab-error");' . 'link.innerHTML=orig;' . '},3000);' . '});' . '});' . '}());', esc_url( rest_url( 'ai-schema-gen/v1/generate' ) ), wp_create_nonce( 'wp_rest' ), $post_id ); wp_add_inline_script( 'admin-bar', $js ); $css = '#wp-admin-bar-ai-schema-gen-generate .ab-icon.dashicons{' . 'font-family:dashicons!important;font-size:20px;line-height:1;' . '}' . '#wp-admin-bar-ai-schema-gen-generate .ab-icon:before{' . 'content:"\f475";top:3px;' . '}' . '.ai-schema-gen-ab-loading .ab-icon.dashicons:before{' . 'content:"\f463"!important;animation:ai-schema-gen-spin 1s linear infinite;' . '}' . '.ai-schema-gen-ab-success #wp-admin-bar-ai-schema-gen-generate>a,' . '#wp-admin-bar-ai-schema-gen-generate.ai-schema-gen-ab-success>a{color:#46b450!important;}' . '.ai-schema-gen-ab-success .ab-icon.dashicons:before{content:"\f12a"!important;}' . '.ai-schema-gen-ab-error #wp-admin-bar-ai-schema-gen-generate>a,' . '#wp-admin-bar-ai-schema-gen-generate.ai-schema-gen-ab-error>a{color:#dc3232!important;}' . '.ai-schema-gen-ab-error .ab-icon.dashicons:before{content:"\f534"!important;}' . '@keyframes ai-schema-gen-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}'; wp_add_inline_style( 'admin-bar', $css ); } /** * Recursively sanitize schema data. * * Walks through the schema array and sanitizes all string values * while preserving the data structure. * * @param array $data The schema data to sanitize. * @return array The sanitized schema data. */ private function sanitize_schema_data( $data ) { $sanitized = array(); foreach ( $data as $key => $value ) { $key = sanitize_text_field( $key ); if ( is_array( $value ) ) { $sanitized[ $key ] = $this->sanitize_schema_data( $value ); } elseif ( is_string( $value ) ) { // Allow URLs to pass through with minimal sanitization. if ( filter_var( $value, FILTER_VALIDATE_URL ) ) { $sanitized[ $key ] = esc_url_raw( $value ); } else { $sanitized[ $key ] = sanitize_text_field( $value ); } } elseif ( is_numeric( $value ) ) { $sanitized[ $key ] = $value; } elseif ( is_bool( $value ) ) { $sanitized[ $key ] = $value; } else { $sanitized[ $key ] = sanitize_text_field( (string) $value ); } } return $sanitized; } /** * Remove all third-party admin notices on our plugin's pages. */ public function suppress_third_party_notices() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check to identify the current admin page; no form data processed. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $our_pages = array( 'ai-schema-gen', 'ai-schema-gen-tester', 'ai-schema-gen-settings', 'ai-schema-gen-upgrade', 'ai-schema-gen-setup', 'ai-schema-gen-audit', ); if ( in_array( $page, $our_pages, true ) ) { remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); remove_all_actions( 'user_admin_notices' ); } } }