tg_* (interim), 2 = adpresso_* and tg_* -> guard_*. Sites still on 1 run it again once. */ public const TG_STORAGE_MIGRATION_VERSION = 2; /** * Legacy Traffic Guard table suffixes => current ones (both get $wpdb->prefix). * Interim tg_* names come first: they hold the newer data if a site has both generations. */ public const TG_LEGACY_TABLES = [ 'tg_logs' => 'guard_logs', 'tg_bans' => 'guard_bans', 'tg_clicks' => 'guard_clicks', 'tg_impressions' => 'guard_impressions', 'tg_daily' => 'guard_daily', 'adpresso_traffic_guard_logs' => 'guard_logs', 'adpresso_traffic_guard_bans' => 'guard_bans', 'adpresso_traffic_guard_clicks' => 'guard_clicks', 'adpresso_traffic_guard_impressions' => 'guard_impressions', 'adpresso_traffic_guard_daily' => 'guard_daily', ]; /** * Legacy Traffic Guard options => current ones. */ public const TG_LEGACY_OPTIONS = [ 'tg_settings' => 'guard_settings', 'tg_secret' => 'guard_secret', 'tg_identity_secret' => 'guard_identity_secret', 'tg_risk_history' => 'guard_risk_history', 'tg_snoozes' => 'guard_snoozes', 'tg_alert_state' => 'guard_alert_state', 'adpresso_pro_traffic_guard_settings' => 'guard_settings', 'adpresso_tg_secret' => 'guard_secret', 'adpresso_tg_identity_secret' => 'guard_identity_secret', 'adpresso_pro_traffic_guard_risk_history' => 'guard_risk_history', 'adpresso_pro_traffic_guard_snoozes' => 'guard_snoozes', 'adpresso_pro_traffic_guard_alert_state' => 'guard_alert_state', ]; /** * Legacy Traffic Guard cron hooks => current ones. */ public const TG_LEGACY_CRON_HOOKS = [ 'tg_cleanup' => 'guard_cleanup', 'tg_daily' => 'guard_daily', 'adpresso_traffic_guard_cleanup' => 'guard_cleanup', 'adpresso_traffic_guard_daily' => 'guard_daily', ]; /** * Columns every Traffic Guard table generation has: tells TG's tables apart from same-named foreign ones. */ public const TG_TABLE_SIGNATURES = [ 'guard_logs' => [ 'render_id', 'placement_id', 'ad_id', 'event_type', 'category', 'score' ], 'guard_bans' => [ 'identity_key', 'banned_until', 'score' ], 'guard_clicks' => [ 'identity_key', 'ad_id', 'click_count', 'expires_at' ], 'guard_impressions' => [ 'impression_id', 'identity_key', 'ad_id', 'clicked', 'expires_at' ], 'guard_daily' => [ 'stat_date', 'source', 'verified_impressions', 'verified_clicks' ], ]; /** * Option: ids of the one-time data migrations that already ran. */ public const DATA_MIGRATIONS_OPTION = 'adpresso_data_migrations'; /** * Posts per batch of the legacy meta migrations. */ private const LEGACY_META_BATCH = 200; /** * Whether this migration run found Traffic Guard storage under the generic tg_* names. * * @var bool */ private bool $found_interim_tg = false; /** * @return void */ public function hooks(): void { register_activation_hook( ADPRESSO_FILE, [ $this, 'install' ] ); add_action( 'plugins_loaded', [ $this, 'check_for_updates' ] ); } /** * @return void */ public function install(): void { // Activation of an already installed copy (e.g. updated while inactive) must not undo removed caps. $stored_version = get_option( 'adpresso_db_version', '' ); $was_installed = is_string( $stored_version ) && '' !== $stored_version && '0.0.0' !== $stored_version; $this->create_tables(); // A first activation applies the full capability set, honouring the editor setting. // pro-gate: pre-boot if ( Util::pro_class_available( CapabilityManager::class ) ) { if ( $was_installed ) { CapabilityManager::add_new_capabilities(); } else { CapabilityManager::add_capabilities(); } } $this->run_data_migrations(); // check_for_updates() sees no version gap after this, so the update's rebuild is scheduled here. if ( $was_installed && version_compare( $stored_version, ADPRESSO_VERSION, '<' ) ) { $this->handle_asset_masking_rebuild(); } update_option( 'adpresso_db_version', ADPRESSO_VERSION ); } /** * Update routine for a version bump without activation. * * @return void */ public function update(): void { $this->create_tables(); // Updates only add caps new in this version; caps an admin removed stay removed. // pro-gate: pre-boot if ( Util::pro_class_available( CapabilityManager::class ) ) { CapabilityManager::add_new_capabilities(); } $this->run_data_migrations(); $this->handle_asset_masking_rebuild(); update_option( 'adpresso_db_version', ADPRESSO_VERSION ); } /** * Creates or updates all tables (dbDelta is idempotent). * * @return void */ private function create_tables(): void { $this->create_notification_table(); $this->create_import_log_table(); DeliveryCounters::create_table(); // Traffic Guard is Pro-only; don't create its tables on Free-only installs. // pro-gate: pre-boot if ( Util::pro_class_available( TrafficGuardService::class ) ) { $this->create_traffic_guard_tables(); } TrackingAdmin::activate(); } /** * Creates all Traffic Guard tables. Called from install() when Pro is already active, and from the after_license_activation hook * * @return void */ public function create_traffic_guard_tables(): void { // Rename legacy tables first, otherwise dbDelta would create empty new ones next to them. $this->migrate_traffic_guard_storage(); $creators = [ 'guard_logs' => 'create_traffic_guard_log_table', 'guard_bans' => 'create_traffic_guard_ban_table', 'guard_clicks' => 'create_traffic_guard_click_table', 'guard_impressions' => 'create_traffic_guard_impression_table', 'guard_daily' => 'create_traffic_guard_daily_table', ]; foreach ( $creators as $suffix => $method ) { // Never let dbDelta alter a same-named table of another plugin. if ( $this->is_foreign_table( $suffix ) ) { $this->log_tg_migration( "{$suffix} exists without Traffic Guard's columns, left untouched." ); continue; } $this->$method(); } } /** * Checks if the version in the files is newer than the one in the database. * Starts the update routine, if necessary. */ public function check_for_updates(): void { $installed_version = get_option( 'adpresso_db_version', '0.0.0' ); if ( version_compare( $installed_version, ADPRESSO_VERSION, '<' ) ) { if ( '0.0.0' === $installed_version ) { // Never installed (activation hook missed): treat as a first install. $this->install(); $this->handle_asset_masking_rebuild(); } else { $this->update(); } } AssetMaskingAdmin::maybe_hook_scheduled_rebuild(); } /** * Callback for plugins_loaded: runs the Traffic Guard storage migration once per site, independent of Pro/license state. * * @return void */ public function maybe_migrate_traffic_guard_storage(): void { if ( $this->migrate_traffic_guard_storage() ) { // Legacy tables existed, so TG was installed here: bring the renamed tables to the current schema. $this->create_traffic_guard_tables(); } } /** * Moves Traffic Guard tables, options and cron events from the legacy adpresso_* and tg_* names to guard_*. Idempotent. * * @return bool True if this run found at least one legacy table. */ public function migrate_traffic_guard_storage(): bool { if ( (int) get_option( self::TG_STORAGE_MIGRATION_OPTION, 0 ) >= self::TG_STORAGE_MIGRATION_VERSION ) { return false; } $this->found_interim_tg = false; $found_legacy_tables = $this->migrate_traffic_guard_tables(); $this->migrate_traffic_guard_options(); $this->migrate_traffic_guard_cron(); update_option( self::TG_STORAGE_MIGRATION_OPTION, self::TG_STORAGE_MIGRATION_VERSION, true ); return $found_legacy_tables; } /** * Renames each legacy table; if the new one already exists, merges only into an empty one and keeps both otherwise. * * @return bool */ private function migrate_traffic_guard_tables(): bool { global $wpdb; $found = false; foreach ( self::TG_LEGACY_TABLES as $old_suffix => $new_suffix ) { $old = $wpdb->prefix . $old_suffix; $new = $wpdb->prefix . $new_suffix; if ( ! $this->table_exists( $old ) ) { continue; } // tg_* is generic: only a table with TG's columns is ours. if ( self::is_interim_name( $old_suffix ) ) { if ( ! self::has_traffic_guard_signature( $old, $new_suffix ) ) { continue; } $this->found_interim_tg = true; } $found = true; if ( ! $this->table_exists( $new ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange if ( false === $wpdb->query( $wpdb->prepare( 'RENAME TABLE %i TO %i', $old, $new ) ) ) { $this->log_tg_migration( "could not rename $old to $new, legacy table kept." ); } continue; } if ( ! self::has_traffic_guard_signature( $new, $new_suffix ) ) { $this->log_tg_migration( "$new belongs to another plugin, legacy table $old kept." ); continue; } $this->merge_legacy_traffic_guard_table( $old, $new ); } return $found; } /** * Whether a legacy name is one of the generic interim tg_* names (not our adpresso_* ones). * * @param string $name Legacy table suffix, option name or cron hook. * * @return bool */ private static function is_interim_name( string $name ): bool { return str_starts_with( $name, 'tg_' ); } /** * Whether a table has the columns every Traffic Guard generation of that table has. * * @param string $table Full table name. * @param string $suffix Current TG suffix (key of TG_TABLE_SIGNATURES). * * @return bool */ public static function has_traffic_guard_signature( string $table, string $suffix ): bool { global $wpdb; if ( ! isset( self::TG_TABLE_SIGNATURES[ $suffix ] ) ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $columns = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $table ) ); return [] === array_diff( self::TG_TABLE_SIGNATURES[ $suffix ], $columns ); } /** * Whether a value has the shape Traffic Guard stores under that option (tg_* names are too generic to trust). * * @param string $current_name Current TG option name (guard_*). * @param mixed $value Stored value. * * @return bool */ public static function is_traffic_guard_option_value( string $current_name, mixed $value ): bool { $entries_have = static fn( $entries, string $key ): bool => is_array( $entries ) && [] !== $entries && [] === array_filter( $entries, static fn( $entry ) => ! is_array( $entry ) || ! array_key_exists( $key, $entry ) ); switch ( $current_name ) { case 'guard_settings': return is_array( $value ) && [] !== array_intersect( [ 'protection_level', 'enforcement_mode', 'click_limit', 'click_window', 'ban_duration' ], array_keys( $value ) ); case 'guard_secret': case 'guard_identity_secret': // wp_generate_password( 64 ). return is_string( $value ) && 64 === strlen( $value ); case 'guard_risk_history': return $entries_have( $value, 'd' ) || $entries_have( $value, 'date' ); case 'guard_snoozes': return $entries_have( $value, 'until' ); case 'guard_alert_state': return is_array( $value ) && array_key_exists( 'last_fired_at', $value ); } return false; } /** * Whether {prefix}$suffix exists but is not a Traffic Guard table. * * @param string $suffix Current TG table suffix. * * @return bool */ private function is_foreign_table( string $suffix ): bool { global $wpdb; $table = $wpdb->prefix . $suffix; return $this->table_exists( $table ) && ! self::has_traffic_guard_signature( $table, $suffix ); } /** * Both tables exist: copies the legacy rows into the new table only if it is still empty, never drops unmerged data. * * @param string $old Legacy table name. * @param string $current Current table name. * * @return void */ private function merge_legacy_traffic_guard_table( string $old, string $current ): void { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange if ( (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $current ) ) > 0 ) { $this->log_tg_migration( "$old and $current both hold data, legacy table kept for a manual merge." ); return; } $old_columns = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $old ) ); $current_columns = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $current ) ); if ( empty( $old_columns ) || array_diff( $old_columns, $current_columns ) ) { $this->log_tg_migration( "$old has columns $current lacks, legacy table kept." ); return; } $old_count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $old ) ); $placeholders = implode( ', ', array_fill( 0, count( $old_columns ), '%i' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders holds only %i placeholders. $copied = $wpdb->query( $wpdb->prepare( "INSERT INTO %i ($placeholders) SELECT $placeholders FROM %i", $current, ...array_merge( $old_columns, $old_columns, [ $old ] ) ) ); if ( false === $copied || (int) $copied !== $old_count ) { $this->log_tg_migration( "copying $old into $current failed, legacy table kept." ); return; } $wpdb->query( $wpdb->prepare( 'DROP TABLE %i', $old ) ); // phpcs:enable $this->log_tg_migration( "merged $old_count rows from $old into the empty $current and dropped $old." ); } /** * Renames legacy options in place (value and autoload stay byte-identical, so signed secrets keep working). * * @return void */ private function migrate_traffic_guard_options(): void { global $wpdb; $names = array_merge( array_keys( self::TG_LEGACY_OPTIONS ), array_values( self::TG_LEGACY_OPTIONS ) ); $placeholders = implode( ', ', array_fill( 0, count( $names ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders holds only %s placeholders. $existing = (array) $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name IN ($placeholders)", ...$names ) ); foreach ( self::TG_LEGACY_OPTIONS as $old => $new ) { if ( ! in_array( $old, $existing, true ) ) { continue; } if ( self::is_interim_name( $old ) ) { if ( ! self::is_traffic_guard_option_value( $new, get_option( $old ) ) ) { continue; } $this->found_interim_tg = true; } if ( in_array( $new, $existing, true ) ) { $this->log_tg_migration( "options $old and $new both exist, kept $new and left $old untouched." ); continue; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_name = %s WHERE option_name = %s", $new, $old ) ); wp_cache_delete( $old, 'options' ); wp_cache_delete( $new, 'options' ); // Older generation of the same option must now see the target as taken. $existing[] = $new; } wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); // Legacy cache transients (dashboard stats, rDNS) are just dropped, they get rebuilt on demand. $transient_prefixes = $this->found_interim_tg ? [ 'adpresso_tg_stats_', 'tg_stats_', 'tg_rdns_' ] : [ 'adpresso_tg_stats_' ]; foreach ( $transient_prefixes as $transient_prefix ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_' . $transient_prefix ) . '%', $wpdb->esc_like( '_transient_timeout_' . $transient_prefix ) . '%' ) ); } } /** * Moves scheduled legacy cron events to the new hooks, keeping their next run and recurrence. * A legacy event only exists if TG scheduled it; TG also (re)schedules the new hooks itself whenever it boots. * * @return void */ private function migrate_traffic_guard_cron(): void { foreach ( self::TG_LEGACY_CRON_HOOKS as $old => $new ) { // A tg_* event is only TG's if its tables/options were found under tg_* too. if ( self::is_interim_name( $old ) && ! $this->found_interim_tg ) { continue; } $event = wp_get_scheduled_event( $old ); if ( ! $event ) { continue; } wp_clear_scheduled_hook( $old ); if ( ! empty( $event->schedule ) && ! wp_next_scheduled( $new ) ) { wp_schedule_event( (int) $event->timestamp, (string) $event->schedule, $new ); } } } /** * Whether the given table exists. * * @param string $table Full table name. * * @return bool */ private function table_exists( string $table ): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) === $table; } /** * Writes a migration note to the PHP error log. * * @param string $message Message without prefix. * * @return void */ private function log_tg_migration( string $message ): void { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only reached for the one-time storage migration's non-trivial cases. error_log( 'AdPresso: Traffic Guard storage migration: ' . $message ); } /** * Runs each one-time data migration that has not completed on this site yet. * * @return void */ public function run_data_migrations(): void { $done = get_option( self::DATA_MIGRATIONS_OPTION, [] ); $done = is_array( $done ) ? $done : []; $steps = [ 'lazy_load_default_threshold' => 'migrate_lazy_load_default_threshold', 'a2_conditions_tree' => 'migrate_a2_conditions_meta', 'a2_expiry_end_date' => 'migrate_a2_expiry_date_meta', 'stacked_notifications' => 'migrate_stacked_notifications', 'global_notifications' => 'migrate_global_notifications', ]; $changed = false; foreach ( $steps as $id => $method ) { if ( in_array( $id, $done, true ) ) { continue; } $this->$method(); $done[] = $id; $changed = true; } if ( $changed ) { update_option( self::DATA_MIGRATIONS_OPTION, $done, false ); } } /** * Up to 1.7 every import and block conversion added its own notice; the fixed-slug one now holds the last result. * * @return void */ private function migrate_stacked_notifications(): void { Notification_Center::get()->delete_stacked_notices(); } /** * Up to 1.7 a notice was stored per user who triggered it; now one global row per slug, dismissals per user. * * @return void */ private function migrate_global_notifications(): void { Notification_Center::get()->globalize_notices(); } /** * 1.5.0 saved the lazy-load default threshold in the Pro settings; it now lives in the module's own option. * * @return void */ private function migrate_lazy_load_default_threshold(): void { $pro_settings = get_option( 'adpresso_settings_pro', [] ); $old_value = is_array( $pro_settings ) ? ( $pro_settings['lazy_load_default_threshold'] ?? '' ) : ''; if ( '' === $old_value || null === $old_value || ! is_numeric( $old_value ) ) { return; } $module_settings = get_option( 'adpresso_pro_lazy_load_module_settings', [] ); $module_settings = is_array( $module_settings ) ? $module_settings : []; $current = $module_settings['default_threshold'] ?? ''; // 1.5.0 stored '200' here on every Pro tab save, so that value is not a customization. if ( '' !== $current && null !== $current && '200' !== (string) $current ) { return; } $module_settings['default_threshold'] = absint( $old_value ); update_option( 'adpresso_pro_lazy_load_module_settings', $module_settings ); } /** * The A2 importer up to 1.6.0 wrote conditions to adpresso_conditions, read nowhere: copy them to adpresso_conditions_tree. * * @return void */ private function migrate_a2_conditions_meta(): void { $this->copy_legacy_meta( Constants::META_PREFIX . 'conditions', Constants::META_PREFIX . 'conditions_tree', [ Constants::POST_TYPE_AD, Constants::POST_TYPE_PLACEMENT ], static function ( string $value ): ?string { $tree = json_decode( $value, true ); return is_array( $tree ) && isset( $tree['groups'] ) ? $value : null; } ); } /** * The A2 importer up to 1.6.0 wrote the UTC expiry timestamp to adpresso_expiry_date, read nowhere: convert it to adpresso_end_date. * * @return void */ private function migrate_a2_expiry_date_meta(): void { $this->copy_legacy_meta( Constants::META_PREFIX . 'expiry_date', Constants::META_PREFIX . 'end_date', [ Constants::POST_TYPE_AD ], static function ( string $value ): ?string { if ( ! ctype_digit( $value ) || (int) $value <= 0 ) { return null; } $date = wp_date( 'Y-m-d H:i:s', (int) $value ); return is_string( $date ) ? $date : null; } ); } /** * Copies a legacy meta value to its target key, in batches, for posts that have no target value yet. * * @param string $legacy_key Meta key written by older versions. * @param string $target_key Meta key read today. * @param string[] $post_types Post types to migrate. * @param callable(string):?string $convert Returns the target value, or null to skip the post. * * @return void */ private function copy_legacy_meta( string $legacy_key, string $target_key, array $post_types, callable $convert ): void { global $wpdb; $placeholders = implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ); $sql = "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id LEFT JOIN {$wpdb->postmeta} t ON t.post_id = pm.post_id AND t.meta_key = %s WHERE pm.meta_key = %s AND p.post_type IN ({$placeholders}) AND t.meta_id IS NULL AND pm.post_id > %d ORDER BY pm.post_id ASC LIMIT %d"; $last_id = 0; $seen = []; do { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders holds only %s placeholders. $rows = (array) $wpdb->get_results( $wpdb->prepare( $sql, $target_key, $legacy_key, ...array_merge( $post_types, [ $last_id, self::LEGACY_META_BATCH ] ) ) ); $row_count = count( $rows ); foreach ( $rows as $row ) { $post_id = (int) $row->post_id; $last_id = max( $last_id, $post_id ); if ( isset( $seen[ $post_id ] ) ) { continue; } $seen[ $post_id ] = true; $value = $convert( (string) $row->meta_value ); if ( null === $value ) { continue; } // Raw DB value: slash it so update_post_meta()'s unslash keeps backslashes intact. update_post_meta( $post_id, $target_key, wp_slash( $value ) ); } } while ( self::LEGACY_META_BATCH === $row_count ); } /** * Schedules the masked-assets rebuild for wp_loaded, when every asset is registered. * * @return void */ private function handle_asset_masking_rebuild(): void { AssetMaskingAdmin::schedule_rebuild_if_active(); } /** * Creates the table for the Traffic Guard logs. * * @return void */ public function create_traffic_guard_log_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'guard_logs'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, render_id VARCHAR(100) NOT NULL, placement_id BIGINT(20) UNSIGNED NOT NULL, ad_id BIGINT(20) UNSIGNED NOT NULL, event_type VARCHAR(50) NOT NULL, category VARCHAR(50) NOT NULL, score INT(11) NOT NULL DEFAULT 0, click_id_type VARCHAR(20) NOT NULL DEFAULT '', click_id_value VARCHAR(191) NOT NULL DEFAULT '', session_context TEXT NULL, placement_ref VARCHAR(191) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY render_id (render_id), KEY placement_ad (placement_id, ad_id), KEY event_type (event_type), KEY created_at (created_at), KEY click_id_type (click_id_type), KEY placement_ref (placement_ref) ) $charset_collate; " ); } /** * Creates the table backing BanManager. * * @return void */ public function create_traffic_guard_ban_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'guard_bans'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( identity_key VARCHAR(64) NOT NULL, banned_until DATETIME NOT NULL, score INT(11) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (identity_key), KEY banned_until (banned_until) ) $charset_collate; " ); } /** * Creates the table backing RateLimiter. * * @return void */ public function create_traffic_guard_click_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'guard_clicks'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( identity_key VARCHAR(64) NOT NULL, ad_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, target_key VARCHAR(191) NOT NULL DEFAULT '', click_count INT(11) UNSIGNED NOT NULL DEFAULT 0, expires_at DATETIME NOT NULL, PRIMARY KEY (identity_key, ad_id, target_key), KEY expires_at (expires_at) ) $charset_collate; " ); } /** * Creates the table backing ImpressionStore looked up again once a click comes in. * * @return void */ public function create_traffic_guard_impression_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'guard_impressions'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( impression_id VARCHAR(32) NOT NULL, identity_key VARCHAR(64) NOT NULL, ad_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, placement_id VARCHAR(191) NOT NULL DEFAULT '', clicked TINYINT(1) UNSIGNED NOT NULL DEFAULT 0, created_at INT(10) UNSIGNED NOT NULL, expires_at DATETIME NOT NULL, PRIMARY KEY (impression_id), KEY expires_at (expires_at) ) $charset_collate; " ); } /** * Creates the renderer-agnostic daily volume table (VolumeRecorder) - the verified-impression / verified-click denominator for the dashboard. * * @return void */ public function create_traffic_guard_daily_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'guard_daily'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( stat_date DATE NOT NULL, source VARCHAR(32) NOT NULL DEFAULT 'unknown', verified_impressions BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, verified_clicks BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (stat_date, source) ) $charset_collate; " ); } /** * Creates the table for the import protocol. * * @return void */ public function create_import_log_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'adpresso_import_log'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( " CREATE TABLE $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, session_key VARCHAR(100) NOT NULL, importer_id VARCHAR(100) NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, summary_message TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY session_key (session_key), KEY user_id (user_id) ) $charset_collate; " ); } /** * Creates the notification SQL table. * * @return void */ protected function create_notification_table(): void { global $wpdb; $table_name = $wpdb->prefix . 'adpresso_notifications'; $charset_collate = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $sql = "CREATE TABLE $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, type VARCHAR(20) NOT NULL DEFAULT 'info', slug VARCHAR(191) NOT NULL, message TEXT NOT NULL, link TEXT NULL, location VARCHAR(20) NOT NULL DEFAULT 'dashboard', dismissible TINYINT(1) DEFAULT 1, dismissed_until DATETIME DEFAULT NULL, user_id BIGINT(20) UNSIGNED NULL DEFAULT NULL, context VARCHAR(50) DEFAULT 'global', status VARCHAR(20) DEFAULT 'active', created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY slug_user_id (slug, user_id), KEY location_status (location, status), KEY user_id (user_id) ) $charset_collate;"; dbDelta( $sql ); } }