JezK
Edit File: class-options-settings.php
<?php /** * Options. * * @package ThemeIsle\GutenbergBlocks\Plugins */ namespace ThemeIsle\GutenbergBlocks\Plugins; use ThemeIsle\GutenbergBlocks\Server\Template_Cloud_Server; use ThemeIsle\GutenbergBlocks\Plugins\Template_Cloud; /** * Class Options_Settings */ class Options_Settings { /** * Canonical AI toolbar actions option. */ const AI_TOOLBAR_ACTIONS_OPTION = 'themeisle_blocks_settings_ai_toolbar_actions'; /** * Migration flag for AI toolbar actions. */ const AI_TOOLBAR_ACTIONS_MIGRATED_OPTION = 'themeisle_blocks_ai_toolbar_actions_migrated'; /** * WordPress AI Client provider/model preferences for Otter AI features. */ const AI_WP_CLIENT_OPTION = 'themeisle_blocks_settings_ai_wp_client'; /** * Legacy AI toolbar actions option. */ const LEGACY_TOOLBAR_ACTIONS_OPTION = 'themeisle_blocks_settings_prompt_actions'; /** * Maximum number of custom toolbar actions. */ const MAX_CUSTOM_TOOLBAR_ACTIONS = 5; /** * The main instance var. * * @var Options_Settings|null */ protected static $instance = null; /** * Initialize the class */ public function init() { $this->maybe_default_atomic_wind_blocks(); add_action( 'init', array( $this, 'register_settings' ), 99 ); add_action( 'init', array( $this, 'migrate_ai_toolbar_actions' ), 100 ); add_action( 'init', array( $this, 'default_block' ), 99 ); add_action( 'init', array( $this, 'register_meta' ), 19 ); } /** * Enable Atomic Wind for fresh Otter installs that never set the option. * * @return void */ public function maybe_default_atomic_wind_blocks() { if ( get_option( 'themeisle_blocks_settings_atomic_wind_defaulted', false ) ) { return; } if ( 'NOT_SET' === get_option( 'themeisle_blocks_settings_atomic_wind_blocks', 'NOT_SET' ) ) { $installed = (int) get_option( 'otter_blocks_install', 0 ); if ( 0 === $installed || $installed > ( time() - DAY_IN_SECONDS ) ) { add_option( 'themeisle_blocks_settings_atomic_wind_blocks', true ); } } update_option( 'themeisle_blocks_settings_atomic_wind_defaulted', true ); } /** * Register Settings * * @since 1.2.0 * @access public */ public function register_settings() { register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_redirect', array( 'type' => 'boolean', 'description' => __( 'Redirect on new install.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_onboarding', array( 'type' => 'boolean', 'description' => __( 'Show Editor Onboarding.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_css_module', array( 'type' => 'boolean', 'description' => __( 'Custom CSS module allows to add custom CSS to each block in Block Editor.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_blocks_animation', array( 'type' => 'boolean', 'description' => __( 'Blocks Animation module allows to add CSS animations to each block in Block Editor.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_block_conditions', array( 'type' => 'boolean', 'description' => __( 'Blocks Conditions module allows to hide/display blocks to your users based on selected conditions.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_patterns_library', array( 'type' => 'boolean', 'description' => __( 'Patterns Library module allows to add predefined patterns to your blocks.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_dynamic_content', array( 'type' => 'boolean', 'description' => __( 'Dynamic Content allows you to display dynamic content, link and images in your content.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_highlight_dynamic', array( 'type' => 'boolean', 'description' => __( 'Easily differentiate between dynamic and normal text in the editor.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_optimize_animations_css', array( 'type' => 'boolean', 'description' => __( 'Optimize Animations CSS.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_disable_review_schema', array( 'type' => 'boolean', 'description' => __( 'Enable Rich Schema in Product Review Block.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_review_scale', array( 'type' => 'boolean', 'description' => __( 'Use 0.5 Scale for Review Block.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => false, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_block_ai_toolbar_module', array( 'type' => 'boolean', 'description' => __( 'Enable the AI Block Toolbar in Editor', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_atomic_wind_blocks', array( 'type' => 'boolean', 'description' => __( 'Enable Atomic Wind Blocks', 'otter-blocks' ), 'show_in_rest' => true, 'default' => false, ) ); register_setting( 'themeisle_blocks_settings', 'otter_blocks_logger_flag', array( 'type' => 'string', 'description' => __( 'Become a contributor by opting in to our anonymous data tracking. We guarantee no sensitive data is collected.', 'otter-blocks' ), 'show_in_rest' => true, ) ); register_setting( 'themeisle_blocks_settings', 'otter_blocks_logger_data', array( 'type' => 'object', 'description' => __( 'Anonymous data tracking object.', 'otter-blocks' ), 'show_in_rest' => array( 'schema' => array( 'type' => 'object', 'properties' => array( 'blocks' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'name' => array( 'type' => 'string', ), 'instances' => array( 'type' => 'number', ), ), ), ), 'templates' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'url' => array( 'type' => 'string', ), 'instances' => array( 'type' => 'number', ), ), ), ), ), ), ), 'default' => array( 'blocks' => array(), 'templates' => array(), ), ) ); register_setting( 'themeisle_blocks_settings', 'otter_activation_first_save', array( 'type' => 'boolean', 'description' => __( 'Whether the activation first-save milestone has already been tracked for this site.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => false, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_google_map_block_api_key', array( 'type' => 'string', 'description' => __( 'Google Map API key for the Google Maps Gutenberg Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_stripe_api_key', array( 'type' => 'string', 'description' => __( 'Stripe API key for the Stripe Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_google_captcha_api_site_key', array( 'type' => 'string', 'description' => __( 'Google reCaptcha Site API key for the Form Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_google_captcha_api_secret_key', array( 'type' => 'string', 'description' => __( 'Google reCaptcha Secret API key for the Form Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_cloudflare_turnstile_site_key', array( 'type' => 'string', 'description' => __( 'Cloudflare Turnstile Site key for the Form Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_cloudflare_turnstile_secret_key', array( 'type' => 'string', 'description' => __( 'Cloudflare Turnstile Secret key for the Form Block.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_default_block', array( 'type' => 'boolean', 'description' => __( 'Make Section block your default block for Pages?', 'otter-blocks' ), 'sanitize_callback' => 'rest_sanitize_boolean', 'show_in_rest' => true, 'default' => false, ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_global_defaults', array( 'type' => 'string', 'description' => __( 'Global defaults for Gutenberg Blocks.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_form_emails', array( 'type' => 'array', 'description' => __( 'Email used in the Form block.', 'otter-blocks' ), 'sanitize_callback' => function ( $email_settings ) { return array_map( function ( $item ) { if ( isset( $item['form'] ) ) { $item['form'] = sanitize_text_field( $item['form'] ); } if ( isset( $item['email'] ) ) { $item['email'] = sanitize_text_field( $item['email'] ); } if ( isset( $item['redirectLink'] ) ) { $item['redirectLink'] = sanitize_text_field( $item['redirectLink'] ); } if ( isset( $item['captchaProvider'] ) ) { $item['captchaProvider'] = sanitize_text_field( $item['captchaProvider'] ); } if ( isset( $item['titleSubject'] ) ) { $item['titleSubject'] = sanitize_text_field( $item['titleSubject'] ); } if ( isset( $item['fromName'] ) ) { $item['fromName'] = sanitize_text_field( $item['fromName'] ); } if ( isset( $item['fromEmail'] ) ) { $item['fromEmail'] = sanitize_email( $item['fromEmail'] ); } if ( isset( $item['cc'] ) ) { $item['cc'] = sanitize_text_field( $item['cc'] ); } if ( isset( $item['bcc'] ) ) { $item['bcc'] = sanitize_text_field( $item['bcc'] ); } if ( isset( $item['replyTo'] ) ) { $item['replyTo'] = sanitize_email( $item['replyTo'] ); } if ( isset( $item['autoresponder']['body'] ) ) { $item['autoresponder']['body'] = wp_kses( $item['autoresponder']['body'], $this::get_allowed_mail_html() ); } if ( isset( $item['autoresponder']['subject'] ) ) { $item['autoresponder']['subject'] = sanitize_text_field( $item['autoresponder']['subject'] ); } if ( isset( $item['aiAutoresponder']['enabled'] ) ) { $item['aiAutoresponder']['enabled'] = ! empty( $item['aiAutoresponder']['enabled'] ); } if ( isset( $item['aiAutoresponder']['prompt'] ) ) { $item['aiAutoresponder']['prompt'] = sanitize_textarea_field( $item['aiAutoresponder']['prompt'] ); } if ( isset( $item['submitMessage'] ) ) { $item['submitMessage'] = sanitize_text_field( $item['submitMessage'] ); } if ( isset( $item['errorMessage'] ) ) { $item['errorMessage'] = sanitize_text_field( $item['errorMessage'] ); } if ( isset( $item['integration']['provider'] ) ) { $item['integration']['provider'] = sanitize_text_field( $item['integration']['provider'] ); } if ( isset( $item['integration']['apiKey'] ) ) { $item['integration']['apiKey'] = sanitize_text_field( $item['integration']['apiKey'] ); } if ( isset( $item['integration']['listId'] ) ) { $item['integration']['listId'] = sanitize_text_field( $item['integration']['listId'] ); } if ( isset( $item['integration']['action'] ) ) { $item['integration']['action'] = sanitize_text_field( $item['integration']['action'] ); } if ( isset( $item['submissionsSaveLocation'] ) ) { $item['submissionsSaveLocation'] = sanitize_text_field( $item['submissionsSaveLocation'] ); } if ( isset( $item['emailNotification'] ) ) { $item['emailNotification'] = rest_sanitize_boolean( $item['emailNotification'] ); } if ( isset( $item['requiredFields'] ) ) { if ( is_array( $item['requiredFields'] ) ) { $item['requiredFields'] = array_map( 'sanitize_text_field', $item['requiredFields'] ); } else { $item['requiredFields'] = array(); } } return $item; }, $email_settings ); }, 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'form' => array( 'type' => 'string', ), 'hasCaptcha' => array( 'type' => array( 'boolean', 'number', 'string' ), ), 'captchaProvider' => array( 'type' => 'string', ), 'email' => array( 'type' => 'string', ), 'fromName' => array( 'type' => 'string', ), 'fromEmail' => array( 'type' => 'string', ), 'redirectLink' => array( 'type' => 'string', ), 'emailSubject' => array( 'type' => 'string', ), 'submitMessage' => array( 'type' => 'string', ), 'errorMessage' => array( 'type' => 'string', ), 'cc' => array( 'type' => 'string', ), 'bcc' => array( 'type' => 'string', ), 'replyTo' => array( 'type' => 'string', ), 'autoresponder' => array( 'type' => 'object', 'properties' => array( 'subject' => array( 'type' => 'string', ), 'body' => array( 'type' => 'string', ), ), ), 'aiAutoresponder' => array( 'type' => 'object', 'properties' => array( 'enabled' => array( 'type' => 'boolean', ), 'prompt' => array( 'type' => 'string', ), ), ), 'integration' => array( 'type' => 'object', 'properties' => array( 'provider' => array( 'type' => 'string', ), 'apiKey' => array( 'type' => 'string', ), 'listId' => array( 'type' => 'string', ), 'action' => array( 'type' => 'string', ), ), ), 'submissionsSaveLocation' => array( 'type' => 'string', ), 'emailNotification' => array( 'type' => array( 'boolean', 'number', 'string' ), ), 'webhookId' => array( 'type' => 'string', ), 'requiredFields' => array( 'type' => 'array', 'items' => array( 'type' => 'string', ), ), ), ), ), ), 'default' => array(), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_form_fields_option', array( 'type' => 'array', 'description' => __( 'Form Fields used in the Form block.', 'otter-blocks' ), 'sanitize_callback' => function ( $fields ) { return array_map( function ( $item ) { if ( isset( $item['fieldOptionName'] ) ) { $item['fieldOptionName'] = sanitize_text_field( $item['fieldOptionName'] ); } if ( isset( $item['fieldOptionType'] ) ) { $item['fieldOptionType'] = sanitize_text_field( $item['fieldOptionType'] ); } if ( isset( $item['options']['maxFileSize'] ) ) { $item['options']['maxFileSize'] = sanitize_text_field( $item['options']['maxFileSize'] ); } if ( isset( $item['options']['allowedFileTypes'] ) && is_array( $item['options']['allowedFileTypes'] ) ) { foreach ( $item['options']['allowedFileTypes'] as $key => $value ) { $item['options']['allowedFileTypes'][ $key ] = sanitize_text_field( $value ); } } if ( isset( $item['options']['saveFiles'] ) ) { $item['options']['saveFiles'] = sanitize_text_field( $item['options']['saveFiles'] ); } if ( isset( $item['options']['maxFilesNumber'] ) && ! is_int( $item['options']['maxFilesNumber'] ) ) { $item['options']['maxFilesNumber'] = sanitize_text_field( $item['options']['maxFilesNumber'] ); } if ( isset( $item['stripe']['product'] ) ) { $item['stripe']['product'] = sanitize_text_field( $item['stripe']['product'] ); } if ( isset( $item['stripe']['price'] ) ) { $item['stripe']['price'] = sanitize_text_field( $item['stripe']['price'] ); } if ( isset( $item['stripe']['quantity'] ) && ! is_int( $item['stripe']['quantity'] ) ) { $item['stripe']['quantity'] = sanitize_text_field( $item['stripe']['quantity'] ); } return $item; }, $fields ); }, 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'fieldOptionName' => array( 'type' => 'string', ), 'fieldOptionType' => array( 'type' => 'string', ), 'options' => array( 'type' => 'object', 'properties' => array( 'maxFileSize' => array( 'type' => array( 'string', 'number' ), ), 'allowedFileTypes' => array( 'type' => 'array', 'items' => array( 'type' => 'string', ), ), 'saveFiles' => array( 'type' => 'string', ), 'maxFilesNumber' => array( 'type' => array( 'string', 'number' ), ), ), 'default' => array(), ), 'stripe' => array( 'type' => 'object', 'properties' => array( 'product' => array( 'type' => 'string', ), 'price' => array( 'type' => 'string', ), 'quantity' => array( 'type' => 'number', 'default' => 1, ), ), ), ), ), ), ), 'default' => array(), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_notifications', array( 'type' => 'object', 'description' => __( 'Notifications Logs.', 'otter-blocks' ), 'sanitize_callback' => function ( $notifications ) { return array_map( function ( $item ) { if ( isset( $item['editor_upsell'] ) ) { $item['editor_upsell'] = boolval( $item['editor_upsell'] ); } return $item; }, $notifications ); }, 'show_in_rest' => array( 'schema' => array( 'type' => 'object', 'properties' => array( 'editor_upsell' => array( 'type' => array( 'boolean', 'number', 'string' ), ), ), ), ), 'default' => array( 'editor_upsell' => false, ), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_webhooks_options', array( 'type' => 'array', 'description' => __( 'Otter Registered Webhooks.', 'otter-blocks' ), 'sanitize_callback' => function ( $webhooks ) { return array_map( function ( $item ) { if ( isset( $item['id'] ) ) { $item['id'] = sanitize_text_field( $item['id'] ); } if ( isset( $item['url'] ) ) { $item['url'] = esc_url_raw( $item['url'] ); } if ( isset( $item['name'] ) ) { $item['name'] = sanitize_text_field( $item['name'] ); } if ( isset( $item['method'] ) ) { $item['method'] = sanitize_text_field( $item['method'] ); } if ( isset( $item['headers'] ) && is_array( $item['headers'] ) ) { foreach ( $item['headers'] as &$header ) { $header['key'] = sanitize_text_field( $header['key'] ); $header['value'] = sanitize_text_field( $header['value'] ); } } else { $item['headers'] = array(); } return $item; }, $webhooks ); }, 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'string', ), 'url' => array( 'type' => 'string', ), 'headers' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'type' => 'string', ), 'value' => array( 'type' => 'string', ), ), ), ), 'name' => array( 'type' => 'string', ), 'method' => array( 'type' => 'string', ), ), ), ), ), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_open_ai_api_key', array( 'type' => 'string', 'description' => __( 'The OpenAI API Key required for usage of Otter AI features.', 'otter-blocks' ), 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '', ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_otter_ai_usage', array( 'type' => 'object', 'description' => __( 'Usage of Otter AI features.', 'otter-blocks' ), 'sanitize_callback' => function ( $value ) { $sanitized = array( 'usage_count' => array(), 'prompts' => array(), ); if ( ! is_array( $value ) ) { return $sanitized; } if ( isset( $value['usage_count'] ) && is_array( $value['usage_count'] ) ) { foreach ( $value['usage_count'] as $item ) { if ( ! is_array( $item ) || ! isset( $item['key'], $item['value'] ) || ! is_string( $item['key'] ) || ! is_numeric( $item['value'] ) ) { continue; } $sanitized['usage_count'][] = array( 'key' => substr( sanitize_text_field( $item['key'] ), 0, 100 ), 'value' => max( 0, (int) $item['value'] ), ); if ( count( $sanitized['usage_count'] ) >= 50 ) { break; } } } if ( isset( $value['prompts'] ) && is_array( $value['prompts'] ) ) { foreach ( $value['prompts'] as $item ) { if ( ! is_array( $item ) || ! isset( $item['key'], $item['values'] ) || ! is_string( $item['key'] ) || ! is_array( $item['values'] ) ) { continue; } $values = array(); foreach ( $item['values'] as $prompt ) { if ( is_string( $prompt ) ) { $values[] = substr( sanitize_textarea_field( $prompt ), 0, 1000 ); } } $sanitized['prompts'][] = array( 'key' => substr( sanitize_text_field( $item['key'] ), 0, 100 ), 'values' => array_slice( $values, -10 ), ); if ( count( $sanitized['prompts'] ) >= 50 ) { break; } } } return $sanitized; }, 'show_in_rest' => array( 'schema' => array( 'type' => 'object', 'properties' => array( 'usage_count' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'type' => 'string', ), 'value' => array( 'type' => 'integer', ), ), ), 'default' => array(), ), 'prompts' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'type' => 'string', ), 'values' => array( 'type' => 'array', 'items' => array( 'type' => 'string', ), ), ), ), 'default' => array(), ), ), ), ), 'default' => array(), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_disabled_blocks', array( 'type' => 'array', 'description' => __( 'The disabled blocks that will no longer be shown in Inserter.', 'otter-blocks' ), 'sanitize_callback' => function ( $disabled_blocks ) { return array_map( 'sanitize_text_field', $disabled_blocks ); }, 'default' => array(), 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'string', ), ), ), ) ); register_setting( 'themeisle_blocks_settings', 'themeisle_blocks_settings_onboarding_wizard', array( 'type' => 'boolean', 'description' => __( 'Enable FSE Onboarding Wizard.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => true, ) ); $toolbar_actions_schema = array( 'type' => 'array', 'description' => __( 'The prompt actions list of toolbar.', 'otter-blocks' ), 'sanitize_callback' => array( self::class, 'sanitize_prompt_actions' ), 'default' => self::get_default_prompt_actions(), 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'string', ), 'title' => array( 'type' => 'string', ), 'prompt' => array( 'type' => 'string', ), 'enabled' => array( 'type' => 'boolean', ), 'custom' => array( 'type' => 'boolean', ), ), ), ), ), ); register_setting( 'themeisle_blocks_settings', self::LEGACY_TOOLBAR_ACTIONS_OPTION, $toolbar_actions_schema ); register_setting( 'themeisle_blocks_settings', self::AI_TOOLBAR_ACTIONS_OPTION, $toolbar_actions_schema ); register_setting( 'themeisle_blocks_settings', self::AI_TOOLBAR_ACTIONS_MIGRATED_OPTION, array( 'type' => 'boolean', 'description' => __( 'Whether AI toolbar actions were migrated to the canonical option.', 'otter-blocks' ), 'show_in_rest' => true, 'default' => false, ) ); register_setting( 'themeisle_blocks_settings', self::AI_WP_CLIENT_OPTION, array( 'type' => 'object', 'description' => __( 'WordPress AI Client provider and model preferences for Otter AI features.', 'otter-blocks' ), 'sanitize_callback' => array( $this, 'sanitize_ai_wp_client_settings' ), 'show_in_rest' => array( 'schema' => array( 'type' => 'object', 'properties' => array( 'provider' => array( 'type' => 'string', 'default' => '', ), 'model' => array( 'type' => 'string', 'default' => '', ), ), ), ), 'default' => array( 'provider' => '', 'model' => '', ), ) ); register_setting( 'themeisle_blocks_settings', Template_Cloud::SOURCES_SETTING_KEY, array( 'type' => 'array', 'description' => __( 'The template cloud sources from which patterns will be loaded.', 'otter-blocks' ), 'sanitize_callback' => [ Template_Cloud_Server::class, 'sanitize_template_cloud_sources' ], 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'key' => array( 'type' => 'string', ), 'url' => array( 'type' => 'string', ), 'name' => array( 'type' => 'string', ), ), ), ), ), 'default' => [], ) ); } /** * Register post meta. * * @since 1.7.0 * @access public * @link https://developer.wordpress.org/reference/functions/register_meta/ */ public function register_meta() { register_post_meta( '', '_themeisle_gutenberg_block_has_review', array( 'show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); } /** * Display Default Block * * @since 1.2.0 * @access public */ public function default_block() { if ( ! get_option( 'themeisle_blocks_settings_default_block', false ) ) { return; } $attributes = array(); $defaults = get_option( 'themeisle_blocks_settings_global_defaults' ); if ( ! empty( $defaults ) ) { $defaults = json_decode( $defaults, true ); if ( isset( $defaults['themeisle-blocks/advanced-columns'] ) ) { $attributes = $defaults['themeisle-blocks/advanced-columns']; } } $post_type_object = get_post_type_object( 'page' ); $post_type_object->template = array( array( 'themeisle-blocks/advanced-columns', $attributes ), ); } /** * Built-in toolbar action ids. * * @static * @access public * * @return array<int, string> */ public static function get_known_prompt_action_ids() { return array( 'rewrite', 'summarize', 'expand', 'shorten', 'translate', 'tone', 'grammar', 'simplify' ); } /** * Default toolbar prompt actions. * * @static * @access public * * @return array<int, array<string, mixed>> */ public static function get_default_prompt_actions() { return array( array( 'id' => 'rewrite', 'title' => __( 'Rewrite', 'otter-blocks' ), 'prompt' => __( 'Rewrite this for clarity and flow.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'summarize', 'title' => __( 'Summarize', 'otter-blocks' ), 'prompt' => __( 'Summarize this concisely.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'expand', 'title' => __( 'Expand', 'otter-blocks' ), 'prompt' => __( 'Expand this with useful supporting detail.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'shorten', 'title' => __( 'Shorten', 'otter-blocks' ), 'prompt' => __( 'Make this shorter while preserving the key meaning.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'translate', 'title' => __( 'Translate', 'otter-blocks' ), 'prompt' => __( 'Translate this into English.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'tone', 'title' => __( 'Change Tone', 'otter-blocks' ), 'prompt' => __( 'Rewrite this in a more professional tone.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'grammar', 'title' => __( 'Fix Grammar', 'otter-blocks' ), 'prompt' => __( 'Fix any spelling and grammar mistakes, keeping the original tone.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), array( 'id' => 'simplify', 'title' => __( 'Simplify', 'otter-blocks' ), 'prompt' => __( 'Simplify this so it is easier to read.', 'otter-blocks' ), 'enabled' => true, 'custom' => false, ), ); } /** * Sanitize toolbar prompt actions. * * @static * @access public * * @param mixed $prompts Prompt actions payload. * * @return array<int, array<string, mixed>> */ public static function sanitize_prompt_actions( $prompts ) { if ( ! is_array( $prompts ) ) { return array(); } $known_ids = self::get_known_prompt_action_ids(); $custom_count = 0; $sanitized = array(); foreach ( $prompts as $item ) { if ( ! is_array( $item ) ) { continue; } $action = array(); if ( isset( $item['id'] ) ) { $action['id'] = sanitize_key( $item['id'] ); } if ( isset( $item['title'] ) ) { $action['title'] = sanitize_text_field( $item['title'] ); } if ( isset( $item['prompt'] ) ) { $action['prompt'] = sanitize_textarea_field( $item['prompt'] ); } if ( empty( $action['title'] ) && empty( $action['prompt'] ) ) { continue; } $action['enabled'] = isset( $item['enabled'] ) ? self::sanitize_boolean_value( $item['enabled'] ) : true; if ( isset( $item['custom'] ) ) { $action['custom'] = self::sanitize_boolean_value( $item['custom'] ); } else { $action['custom'] = ! isset( $action['id'] ) || ! in_array( $action['id'], $known_ids, true ); } if ( $action['custom'] ) { ++$custom_count; if ( $custom_count > self::MAX_CUSTOM_TOOLBAR_ACTIONS ) { $action['enabled'] = false; } if ( empty( $action['id'] ) ) { $action['id'] = self::generate_custom_toolbar_action_id(); } } $sanitized[] = $action; } return $sanitized; } /** * Coerce a mixed REST payload value into a boolean. * * Toolbar action flags arrive as `mixed`; rest_sanitize_boolean only accepts * bool|string|int, so non-scalar values fall back to false. * * @static * @access private * * @param mixed $value Raw value from the request payload. * * @return bool Sanitized boolean value. */ private static function sanitize_boolean_value( $value ) { if ( is_bool( $value ) || is_string( $value ) || is_int( $value ) ) { return rest_sanitize_boolean( $value ); } return false; } /** * Append the builtin default actions whose ids are missing from the given list. * * Existing actions keep their position; missing builtins are appended in * their default order. * * @static * @access public * * @param mixed $actions Toolbar actions list. * * @return array<int, array<string, mixed>> */ public static function merge_missing_builtin_actions( $actions ) { if ( ! is_array( $actions ) ) { $actions = array(); } $existing_ids = array(); foreach ( $actions as $action ) { if ( is_array( $action ) && ! empty( $action['id'] ) ) { $existing_ids[] = $action['id']; } } foreach ( self::get_default_prompt_actions() as $builtin ) { if ( ! in_array( $builtin['id'], $existing_ids, true ) ) { $actions[] = $builtin; } } return $actions; } /** * Generate a stable custom toolbar action id. * * @static * @access public * @return string */ public static function generate_custom_toolbar_action_id() { if ( function_exists( 'wp_generate_uuid4' ) ) { return 'custom-' . wp_generate_uuid4(); } return 'custom-' . wp_generate_password( 8, false, false ); } /** * Migrate legacy toolbar actions into the canonical option. * * @access public * @return void */ public function migrate_ai_toolbar_actions() { if ( get_option( self::AI_TOOLBAR_ACTIONS_MIGRATED_OPTION ) ) { return; } $canonical = get_option( self::AI_TOOLBAR_ACTIONS_OPTION, null ); if ( is_array( $canonical ) && ! empty( $canonical ) ) { update_option( self::AI_TOOLBAR_ACTIONS_MIGRATED_OPTION, true ); return; } $legacy = get_option( self::LEGACY_TOOLBAR_ACTIONS_OPTION, null ); if ( is_array( $legacy ) && ! empty( $legacy ) ) { $migrated = self::merge_missing_builtin_actions( self::sanitize_prompt_actions( $legacy ) ); } else { $migrated = self::get_default_prompt_actions(); } update_option( self::AI_TOOLBAR_ACTIONS_OPTION, $migrated ); update_option( self::AI_TOOLBAR_ACTIONS_MIGRATED_OPTION, true ); } /** * Read toolbar actions from the canonical option with legacy fallback. * * @static * @access public * * @return array<int, array<string, mixed>> */ public static function get_ai_toolbar_actions() { $actions = get_option( self::AI_TOOLBAR_ACTIONS_OPTION, null ); if ( is_array( $actions ) && ! empty( $actions ) ) { return $actions; } $legacy = get_option( self::LEGACY_TOOLBAR_ACTIONS_OPTION, null ); if ( is_array( $legacy ) && ! empty( $legacy ) ) { return $legacy; } return self::get_default_prompt_actions(); } /** * Sanitize WordPress AI Client provider/model settings. * * @param mixed $value Raw option value. * @return array{provider: string, model: string} */ public function sanitize_ai_wp_client_settings( $value ) { $sanitized = array( 'provider' => '', 'model' => '', ); if ( ! is_array( $value ) ) { return $sanitized; } if ( isset( $value['provider'] ) && is_string( $value['provider'] ) ) { $sanitized['provider'] = sanitize_key( $value['provider'] ); } if ( isset( $value['model'] ) && is_string( $value['model'] ) ) { $sanitized['model'] = sanitize_text_field( $value['model'] ); } return $sanitized; } /** * Get saved WordPress AI Client provider/model preferences for Otter AI. * * Empty provider means auto-select among configured connectors. * Empty model means auto-select within the chosen provider. * * @return array{provider: string, model: string} */ public static function get_ai_wp_client_config() { $stored = get_option( self::AI_WP_CLIENT_OPTION, array() ); if ( ! is_array( $stored ) ) { return array( 'provider' => '', 'model' => '', ); } return array( 'provider' => isset( $stored['provider'] ) && is_string( $stored['provider'] ) ? sanitize_key( $stored['provider'] ) : '', 'model' => isset( $stored['model'] ) && is_string( $stored['model'] ) ? sanitize_text_field( $stored['model'] ) : '', ); } /** * Get allowed HTML for mail body. * * @static * @access public * @return array */ public static function get_allowed_mail_html() { $allowed_html = wp_kses_allowed_html( 'post' ); $not_allowed_tags = array( 'input', 'label', 'form', 'select', 'textarea', 'button', 'fieldset', 'legend', 'datalist', 'output', 'option', 'optgroup', 'video', 'audio' ); foreach ( $not_allowed_tags as $tag ) { unset( $allowed_html[ $tag ] ); } return $allowed_html; } /** * The instance method for the static class. * Defines and returns the instance of the static class. * * @static * @since 1.2.0 * @access public * @return Options_Settings */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); self::$instance->init(); } return self::$instance; } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @access public * @since 1.2.0 * @return void */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); } /** * Disable unserializing of the class * * @access public * @since 1.2.0 * @return void */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); } }