'#hex', 'accent' => '#hex', 'text' => '#hex'] */ function ovkit_get_theme_colors(): array { $defaults = array( 'background' => '#f6f6f7', 'accent' => '#28396f', 'text' => '#1a1a1a', ); // 1. Elementor global colors (most common for Ovanap clients). $elementor_active = defined( 'ELEMENTOR_VERSION' ); if ( ! $elementor_active && function_exists( 'is_plugin_active' ) ) { $elementor_active = is_plugin_active( 'elementor/elementor.php' ); } if ( $elementor_active ) { $kit_id = get_option( 'elementor_active_kit' ); if ( $kit_id ) { $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); if ( ! empty( $kit_meta['system_colors'] ) ) { foreach ( $kit_meta['system_colors'] as $color ) { $id = $color['_id'] ?? ''; $hex = $color['color'] ?? ''; if ( ! $hex ) { continue; } if ( 'primary' === $id ) { $defaults['accent'] = $hex; } if ( 'secondary' === $id ) { $defaults['text'] = $hex; } if ( 'text' === $id ) { $defaults['text'] = $hex; } } } // Also check custom colors. if ( ! empty( $kit_meta['custom_colors'] ) ) { foreach ( $kit_meta['custom_colors'] as $color ) { $title = strtolower( $color['title'] ?? '' ); $hex = $color['color'] ?? ''; if ( ! $hex ) { continue; } if ( strpos( $title, 'brand' ) !== false || strpos( $title, 'primary' ) !== false || strpos( $title, 'accent' ) !== false ) { $defaults['accent'] = $hex; } } } } } // 2. Theme.json (FSE themes). if ( function_exists( 'wp_get_global_settings' ) ) { $settings = wp_get_global_settings( array( 'color', 'palette', 'theme' ) ); if ( ! empty( $settings ) && is_array( $settings ) ) { foreach ( $settings as $color ) { if ( ! isset( $color['slug'], $color['color'] ) ) { continue; } $slug = strtolower( $color['slug'] ); if ( in_array( $slug, array( 'primary', 'brand', 'accent', 'main' ), true ) ) { $defaults['accent'] = $color['color']; } if ( in_array( $slug, array( 'base', 'background', 'base-2' ), true ) ) { $defaults['background'] = $color['color']; } if ( in_array( $slug, array( 'contrast', 'text', 'heading' ), true ) ) { $defaults['text'] = $color['color']; } } } } // 3. Customizer. $custom_bg = get_theme_mod( 'background_color', '' ); if ( $custom_bg ) { $defaults['background'] = '#' . ltrim( $custom_bg, '#' ); } $header_text = get_theme_mod( 'header_textcolor', '' ); if ( $header_text && 'blank' !== $header_text ) { $defaults['accent'] = '#' . ltrim( $header_text, '#' ); } // 4. GeneratePress. $gp_settings = get_option( 'generate_settings', array() ); if ( ! empty( $gp_settings['global_colors'] ) ) { foreach ( $gp_settings['global_colors'] as $gc ) { $slug = strtolower( $gc['slug'] ?? '' ); if ( in_array( $slug, array( 'accent', 'contrast' ), true ) ) { $defaults['accent'] = $gc['color']; } if ( 'base' === $slug ) { $defaults['background'] = $gc['color']; } } } return $defaults; } /** * Sanitize a single value by its declared field type. * * Used for values that do not come straight from a request (stored settings, * extras defaults). Request input goes through ovkit_read_posted_settings(). * * @param string $type Field type: text, textarea, url, color, int, bool, select. * @param mixed $value Raw value. * @param array $meta Type context: 'options' for select, 'pattern' for text. * @return mixed */ function ovkit_sanitize_by_type( string $type, $value, array $meta = array() ) { if ( ! is_scalar( $value ) ) { return 'bool' === $type || 'int' === $type ? 0 : ''; } switch ( $type ) { case 'textarea': return wp_kses_post( (string) $value ); case 'url': return esc_url_raw( trim( (string) $value ) ); case 'color': return (string) sanitize_hex_color( (string) $value ); case 'int': return absint( $value ); case 'bool': return $value ? 1 : 0; case 'select': return ovkit_pin_to_options( sanitize_key( (string) $value ), $meta ); case 'text': default: return ovkit_match_pattern( sanitize_text_field( (string) $value ), $meta ); } } /** * Pin a select value to its declared options; falls back to the first option. * * @param string $value Sanitized value. * @param array $meta Field meta with an 'options' map. * @return string */ function ovkit_pin_to_options( string $value, array $meta ): string { $options = isset( $meta['options'] ) && is_array( $meta['options'] ) ? $meta['options'] : array(); if ( ! $options || array_key_exists( $value, $options ) ) { return $value; } return (string) array_key_first( $options ); } /** * Validate a text value against an optional regular expression. * * Tracking IDs declare a 'pattern' (for example /^GTM-[A-Z0-9]+$/) so only a * well-formed ID can be stored; anything else is stored as an empty string. * * @param string $value Sanitized value. * @param array $meta Field meta with an optional 'pattern'. * @return string */ function ovkit_match_pattern( string $value, array $meta ): string { if ( empty( $meta['pattern'] ) || '' === $value ) { return $value; } $value = strtoupper( trim( $value ) ); return preg_match( $meta['pattern'], $value ) ? $value : ''; } /** * Build the field schema (key => type and meta) from the feature registry. * * @param array $features Registered feature definitions. * @return array Map of setting key => array( 'type' => string, 'meta' => array, 'default' => mixed ). */ function ovkit_settings_schema( array $features ): array { $schema = array(); foreach ( $features as $key => $f ) { $schema[ $key ] = array( 'type' => 'bool', 'meta' => array(), ); if ( empty( $f['extras'] ) ) { continue; } foreach ( $f['extras'] as $extra ) { $meta = array(); if ( isset( $extra['options'] ) ) { $meta['options'] = $extra['options']; } if ( isset( $extra['pattern'] ) ) { $meta['pattern'] = $extra['pattern']; } $schema[ $extra['key'] ] = array( 'type' => $extra['type'], 'meta' => $meta, ); if ( array_key_exists( 'default', $extra ) ) { $schema[ $extra['key'] ]['default'] = $extra['default']; } } } return $schema; } /** * Read the posted settings form, one known field at a time. * * Only keys declared in the feature registry are read. Each value is unslashed * and passed through the most restrictive sanitizer for its declared type the * moment it is read. Keys that were not posted fall back to off (toggles) or to * the declared default (extras). * * The caller must have verified the nonce and the manage_options capability; * the nonce is checked again here so this function is safe on its own. * * @param array $features Registered feature definitions. * @param string $nonce_action Nonce action the request was signed with. * @param array $skip Setting keys to leave out of the result. * @return array Clean settings array. */ function ovkit_read_posted_settings( array $features, string $nonce_action, array $skip = array() ): array { check_ajax_referer( $nonce_action, 'nonce' ); $clean = array(); foreach ( ovkit_settings_schema( $features ) as $key => $field ) { if ( isset( $skip[ $key ] ) ) { continue; } $posted = isset( $_POST['settings'][ $key ] ) && is_scalar( $_POST['settings'][ $key ] ); if ( ! $posted ) { if ( 'bool' === $field['type'] ) { $clean[ $key ] = 0; } elseif ( array_key_exists( 'default', $field ) ) { $clean[ $key ] = $field['default']; } continue; } switch ( $field['type'] ) { case 'bool': $clean[ $key ] = absint( wp_unslash( $_POST['settings'][ $key ] ) ) ? 1 : 0; break; case 'int': $clean[ $key ] = absint( wp_unslash( $_POST['settings'][ $key ] ) ); break; case 'url': $clean[ $key ] = trim( esc_url_raw( wp_unslash( $_POST['settings'][ $key ] ) ) ); break; case 'color': $clean[ $key ] = (string) sanitize_hex_color( wp_unslash( $_POST['settings'][ $key ] ) ); break; case 'select': $clean[ $key ] = ovkit_pin_to_options( sanitize_key( wp_unslash( $_POST['settings'][ $key ] ) ), $field['meta'] ); break; case 'textarea': $clean[ $key ] = wp_kses_post( wp_unslash( $_POST['settings'][ $key ] ) ); break; case 'text': default: $clean[ $key ] = ovkit_match_pattern( sanitize_text_field( wp_unslash( $_POST['settings'][ $key ] ) ), $field['meta'] ); break; } } return $clean; } /** * Re-validate a stored settings array (for example an activity-log snapshot) * against the current schema. Unknown keys are dropped; every value passes the * sanitizer for its declared type. * * @param array $data Settings array. * @param array $features Registered feature definitions. * @return array Clean settings array. */ function ovkit_validate_settings( array $data, array $features ): array { $clean = array(); foreach ( ovkit_settings_schema( $features ) as $key => $field ) { if ( array_key_exists( $key, $data ) ) { $clean[ $key ] = ovkit_sanitize_by_type( $field['type'], $data[ $key ], $field['meta'] ); } elseif ( 'bool' === $field['type'] ) { $clean[ $key ] = 0; } elseif ( array_key_exists( 'default', $field ) ) { $clean[ $key ] = $field['default']; } } return $clean; } /** * Attach CSS to an inline-only style handle and enqueue it. * * Call from wp_enqueue_scripts, admin_enqueue_scripts or login_enqueue_scripts. * * @param string $handle Style handle (registered on first use, without a file). * @param string $css CSS rules. Callers escape any dynamic values. */ function ovkit_add_inline_css( string $handle, string $css ): void { if ( ! wp_style_is( $handle, 'registered' ) ) { wp_register_style( $handle, false, array(), OVKIT_VERSION ); } wp_enqueue_style( $handle ); wp_add_inline_style( $handle, $css ); } /** * Store a new settings array and run the side effects of the change. * * Every write path (save, import, presets, safe defaults, rollback) goes * through here, so a feature switched off by any of them still gets its * disable_hook (for example maintenance mode restoring search engine visibility). * * @param array $old_settings Settings before the change. * @param array $new_settings Settings after the change. * @return string[] Page caches purged because the settings changed. */ function ovkit_commit_settings( array $old_settings, array $new_settings ): array { OVKIT_Activity_Log::log( $old_settings, $new_settings ); update_option( OVKIT_OPTION, $new_settings ); delete_transient( 'ovkit_scan_results' ); foreach ( OVKIT_Feature_Registry::instance()->get_all() as $key => $f ) { $is_on = ! empty( $new_settings[ $key ] ); $was_on = ! empty( $old_settings[ $key ] ); $hook = null; if ( $is_on && ! empty( $f['save_hook'] ) ) { $hook = $f['save_hook']; } elseif ( $was_on && ! $is_on && ! empty( $f['disable_hook'] ) ) { $hook = $f['disable_hook']; } if ( ! $hook || ! is_callable( $hook ) ) { continue; } try { call_user_func( $hook, $new_settings ); } catch ( \Throwable $e ) { ovkit_log( 'Settings hook error for ' . $key . ': ' . $e->getMessage(), null, 'error' ); } } // Cached pages would keep showing the old output until they expire. if ( $old_settings === $new_settings ) { return array(); } require_once OVKIT_PATH . 'includes/class-ovkit-cache-manager.php'; try { return OVKIT_Cache_Manager::instance()->purge_pages_after_settings_change(); } catch ( \Throwable $e ) { ovkit_log( 'Page cache purge after save failed: ' . $e->getMessage(), null, 'error' ); return array(); } }