'), 58 // Position: after Appearance (60) but before typical plugins ); // Convert submenu (replaces the auto-generated duplicate) add_submenu_page( 'htel-convert', __('Convert', 'aitoel-html-importer'), __('Convert', 'aitoel-html-importer'), 'edit_posts', 'htel-convert', array(__CLASS__, 'render_convert_page') ); // Settings submenu add_submenu_page( 'htel-convert', __('Settings', 'aitoel-html-importer'), __('Settings', 'aitoel-html-importer'), 'manage_options', 'htel-settings', array(__CLASS__, 'render_settings_page') ); } /** * Register settings. */ public static function register_settings() { register_setting('htel_settings', 'htel_api_url', array( 'type' => 'string', 'sanitize_callback' => array(__CLASS__, 'sanitize_api_url'), 'default' => HTEL_API_URL, )); register_setting('htel_settings', 'htel_telemetry_opt_in', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, )); // Conversion Quality Sampling — opt-OUT (see ToS §9.4). // Default false means sampling is ON; user checks to disable. register_setting('htel_settings', 'htel_sampling_opt_out', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, )); } /** * Sanitize callback for the htel_api_url setting. * * Refuses to persist an empty or malformed value — falls back to the * existing stored value (if it was valid) or to HTEL_API_URL constant. * * Why this is necessary on top of get_api_url() self-healing: * - get_api_url() is the READ path. Self-healing there means an * accidentally-saved-empty option silently disappears on next read. * That's defense-in-depth, but the customer's UI experience is * "I saved something, then it vanished" — confusing. * - This sanitize callback is the WRITE path. We refuse the bad * value at submit time and surface an admin notice explaining why. * The customer sees: "the API URL field can't be empty; reverted * to https://api.aitoelementor.com" — much clearer. * * Tests in wp-plugin/tests/test-settings-sanitize.php enforce this. */ public static function sanitize_api_url($value) { // Trim whitespace; many "empty" submissions are actually whitespace $value = is_string($value) ? trim($value) : ''; // Valid URL with http(s) scheme — accept (esc_url_raw for safety) if ($value !== '' && preg_match('#^https?://[^\s/]+#i', $value) && filter_var($value, FILTER_VALIDATE_URL)) { $clean = esc_url_raw($value); return rtrim($clean, '/'); } // Surface an admin notice so the user knows their submission was // refused (rather than silently saved-and-then-defaulted). add_settings_error( 'htel_api_url', 'htel_api_url_invalid', sprintf( /* translators: %s is the canonical API URL */ esc_html__('The API URL must be a complete URL with http:// or https://. Reverted to %s.', 'aitoel-html-importer'), esc_html(HTEL_API_URL) ), 'error' ); // Fall back to existing valid stored value, otherwise the constant $existing = get_option('htel_api_url', ''); if (is_string($existing) && $existing !== '' && preg_match('#^https?://[^\s/]+#i', $existing) && filter_var($existing, FILTER_VALIDATE_URL)) { return rtrim($existing, '/'); } return HTEL_API_URL; } /** * Enqueue admin CSS and JS on our pages only. */ public static function enqueue_assets($hook) { // Match our pages regardless of how WP sanitizes the menu title if (strpos($hook, 'htel-convert') === false && strpos($hook, 'htel-settings') === false) { return; } wp_enqueue_style( 'htel-admin', HTEL_PLUGIN_URL . 'assets/css/admin.css', array(), HTEL_VERSION ); wp_enqueue_script( 'htel-admin', HTEL_PLUGIN_URL . 'assets/js/admin.js', array('jquery'), HTEL_VERSION, true ); $license_status = HTEL_License::get_status(); $has_license = ($license_status === 'active' && !empty(HTEL_License::get_key())); wp_localize_script('htel-admin', 'htel_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'convert_nonce' => wp_create_nonce('htel_convert_nonce'), 'license_nonce' => wp_create_nonce('htel_license_nonce'), 'has_license' => $has_license ? '1' : '0', )); } /** * Render the conversion page. */ public static function render_convert_page() { include HTEL_PLUGIN_DIR . 'templates/admin-page.php'; } /** * Render the settings page. */ public static function render_settings_page() { include HTEL_PLUGIN_DIR . 'templates/settings-page.php'; } }