prefix . 'pys_options'; } private static function table_exists(): bool { if (self::$table_exists !== null) { return self::$table_exists; } // Try object cache first $cached = wp_cache_get('pys_options_table_exists', 'pys'); if ($cached !== false) { return self::$table_exists = (bool) $cached; } global $wpdb; $table = self::storage_table(); $exists = (bool) $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $table) ); if (!$exists) { // Create table $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, option_name VARCHAR(191) NOT NULL, option_value LONGTEXT NOT NULL, migrated TINYINT(1) NOT NULL DEFAULT 1, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY option_name (option_name) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); // After dbDelta table exists $exists = true; } // Cache for 12h; adjust as you like wp_cache_set('pys_options_table_exists', $exists, 'pys', 12 * HOUR_IN_SECONDS); return self::$table_exists = $exists; } /** * Constructor * * @param string $slug */ public function __construct( $slug ) { $this->slug = $slug; $this->option_key = 'pys_' . $slug; } public function getSlug() { return $this->slug; } /** * Load options fields and options defaults from specified files * * @param string $fields Path to options fields file * @param string $defaults Path to options defaults file */ public function locateOptions( $fields, $defaults ) { $this->loadJSON( $fields, false ); $this->loadJSON( $defaults, true ); $this->defaults_json_path = $defaults; self::table_exists(); } public function resetToDefaults() { if ( ! file_exists( $this->defaults_json_path ) ) { return; } // List of fields to preserve $preserve_fields = [ 'enabled', 'tracking_id', 'enable_server_container', 'server_container_url', 'transport_url', 'pixel_id', 'use_server_api', 'advanced_matching_enabled', 'server_access_api_token', 'use_server_api', 'verify_meta_tag', 'gtm_id', 'gtm_just_data_layer', ]; // Load current values $this->maybeLoad(); $current_values = $this->values; // Preserve the values of specified fields $preserved_values = []; foreach ( $preserve_fields as $field ) { if ( isset( $current_values[ $field ] ) ) { $preserved_values[ $field ] = $current_values[ $field ]; } } // Load default values $content = file_get_contents( $this->defaults_json_path ); $default_values = json_decode( $content, true ); // Merge preserved values with default values $merged_values = array_merge( $default_values, $preserved_values ); // Update options with the merged values $this->updateOptions( $merged_values ); } /** * Load options fields or options defaults from specified file * * @param string $file * @param bool $is_defaults */ private function loadJSON( $file, $is_defaults ) { if ( ! file_exists( $file ) ) { return; } $content = file_get_contents( $file ); $values = json_decode( $content, true ); if ( null === $values ) { return; } if ( $is_defaults ) { $this->defaults = $values; } else { $this->options = $values; } } /** * Reading from a new table. * * @return array|null array of values or null if there is no entry */ private function pys_get_from_storage( $option_key ) { global $wpdb; if (!self::$table_exists) { return null; } $table = self::storage_table(); // Safe to query the table $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $table WHERE option_name = %s LIMIT 1", $option_key ), ARRAY_A ); if ( ! $row ) { return null; } $val = maybe_unserialize( $row['option_value'] ); return is_array( $val ) ? $val : []; } /** * Write to a new table. * * @return bool */ private function pys_set_in_storage( $option_key, $value, $migrated = 1 ) { global $wpdb; if (!self::$table_exists) { return null; } $table = self::storage_table(); $data = [ 'option_value' => maybe_serialize( $value ), 'migrated' => (int) $migrated, ]; $format = [ '%s', '%d' ]; // Checking if there is a record $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE option_name = %s", $option_key ) ); if ( $exists ) { // We are updating $result = $wpdb->update( $table, $data, [ 'option_name' => $option_key ], $format, [ '%s' ] ); } else { // Insert a new one $result = $wpdb->insert( $table, array_merge( [ 'option_name' => $option_key ], $data ), array_merge( [ '%s' ], $format ) ); } return $result !== false; } /** * Add new option field * * @param string $key * @param string $field_type * @param mixed $default */ public function addOption( $key, $field_type, $default ) { $this->options[ $key ] = $field_type; $this->defaults[ $key ] = $default; } /** * Gets an option value or its default value * * @param string $key Option key * @param mixed $fallback Option fallback value if no default is set * * @return mixed The value specified for the option or a default value for the option. */ public function getOption( $key, $fallback = null ) { $this->maybeLoad(); // get option default if unset if ( ! isset( $this->values[ $key ] ) ) { $this->values[ $key ] = isset( $this->defaults[ $key ] ) ? $this->defaults[ $key ] : null; } // use fall back value if default is not set if ( null === $this->values[ $key ] && ! is_null( $fallback ) ) { $this->values[ $key ] = $fallback; } return $this->values[ $key ]; } public function setOption($key, $value){ $this->maybeLoad(); if (isset($value) ) { $this->values[ $key ] = $value; } } /** * Load values from database * * @param bool $force Force options load */ private function maybeLoad( $force = false ) { if ( ! $force && ! empty( $this->values ) ) { return; // already loaded } // 1) Let's try a new table $stored = $this->pys_get_from_storage( $this->option_key ); if ( is_array( $stored ) ) { $this->values = wp_parse_args( $stored, $this->defaults ); return; } // 2) Let's try the old way (wp_options) $legacy = get_option( $this->option_key, null ); if ( is_array( $legacy ) ) { // Migration to a new table $this->pys_set_in_storage( $this->option_key, $legacy, 1 ); $this->values = wp_parse_args( $legacy, $this->defaults ); return; } // 3) If nothing, we take defaults $this->values = $this->defaults; } public function reloadOptions() { $this->maybeLoad( true ); } /** * Sanitize and save options * * @param null|array $values Optional. If set, options values will be received from param instead of $_POST. */ public function updateOptions( $values = null ) { $this->maybeLoad(); if ( is_array( $values ) ) { $form_data = $values; } else { if ( isset( $_POST['pys'][ $this->slug ] ) && is_array( $_POST['pys'][ $this->slug ] ) ) { $form_data = $_POST['pys'][ $this->slug ]; } else { $form_data = []; } } // We only apply valid fields and sanitize them foreach ( $form_data as $key => $value ) { if ( isset( $this->options[ $key ] ) ) { $this->values[ $key ] = $this->sanitize_form_field( $key, $value ); } } // We write to a new table (primarily) $written = $this->pys_set_in_storage( $this->option_key, $this->values, 1 ); if ( ! $written ) { // Fallback: if we couldn't write to our table, we save it in wp_options update_option( $this->option_key, $this->values, false ); } } /** * Sanitize form field * * @param string $key Field key * @param array $value Field value * * @return mixed Sanitized field value */ private function sanitize_form_field( $key, $value ) { $type = $this->options[ $key ]; // look for very specific sanitization filter $filter_name = "{$this->option_key}_settings_sanitize_{$key}_field"; if ( has_filter( $filter_name ) ) { return apply_filters( $filter_name, $value ); } // look for a sanitize_FIELDTYPE_field method if ( is_callable( array( $this, 'sanitize_' . $type . '_field' ) ) ) { return $this->{'sanitize_' . $type . '_field'}( $value ); } // fallback to text return $this->sanitize_text_field( $value ); } /** * Output text input * * @param $key * @param string $placeholder * @param bool $disabled * @param bool $hidden * @param bool $empty */ public function render_text_input( $key, $placeholder = '', $disabled = false, $hidden = false, $empty = false, $type = 'standard' ) { $attr_name = "pys[$this->slug][$key]"; $attr_id = 'pys_' . $this->slug . '_' . $key; $attr_value = $empty == false ? $this->getOption( $key ) : ""; $classes = array( "input-$type" ); if ( $hidden ) { $classes[] = 'form-control-hidden'; } $classes = implode( ' ', $classes ); ?> type="text" name="" id="" value="" placeholder="" class=""> slug][$key]"; $attr_id = 'pys_' . $this->slug . '_' . $key; $attr_value = $empty == false ? $this->getOption( $key ) : ""; $classes = array( "input-$type", "passwordInput" ); if ( $hidden ) { $classes[] = 'form-control-hidden'; } $classes = implode( ' ', $classes ); ?>