true ) ); } // On multisite, other sites get their tables lazily through maybe_upgrade(). unset( $network_wide ); self::install(); Cron::ensure_schedules(); Logger::log( 'info', 'Plugin activated.' ); } /** * Deactivation hook. Keeps all data; only stops background work. */ public static function deactivate() { Cron::clear_all(); Lock::release_all(); // Running jobs are paused so they can be resumed after re-activation. global $wpdb; $table = Db::table( 'scans' ); if ( Db::table_exists( 'scans' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( $wpdb->prepare( "UPDATE %i SET status = %s, error_message = %s WHERE status IN ('queued','running')", $table, 'paused', 'Paused because the plugin was deactivated.' ) ); } Logger::log( 'info', 'Plugin deactivated. Data retained.' ); } /** * Run schema install / upgrade when needed (cheap option read on every load). */ public static function maybe_upgrade() { self::migrate_old_names(); $installed = (int) get_option( self::DB_OPTION, 0 ); if ( $installed >= SITEAUDITHUB_DB_VERSION ) { return; } self::install(); } /** * Move data saved by pre-release builds, which used the short "sah_" * prefix, to the "siteaudithub_" names. Runs once, before anything else, * so settings and scan history are kept. */ public static function migrate_old_names() { global $wpdb; $old_prefix = 'sah' . '_'; if ( false !== get_option( self::DB_OPTION, false ) || false === get_option( $old_prefix . 'db_version', false ) ) { return; } foreach ( array( 'scans', 'items', 'links', 'findings', 'baseline', 'logins' ) as $name ) { $old = $wpdb->prefix . $old_prefix . $name; $new = Db::table( $name ); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- One-time rename of the plugin's own tables. $old_exists = $old === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old ) ) ); $new_exists = $new === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $new ) ) ); if ( $old_exists && $new_exists && 0 === (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $new ) ) ) { $wpdb->query( $wpdb->prepare( 'DROP TABLE %i', $new ) ); // An empty table from an interrupted earlier attempt. $new_exists = false; } if ( $old_exists && ! $new_exists ) { $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i RENAME TO %i', $old, $new ) ); // Works in MySQL, MariaDB and SQLite. } // phpcs:enable } foreach ( array( 'settings', 'ignored', 'log', 'mail_log', 'schedule_state', 'plugin_directory', 'spam_blocked', 'db_version' ) as $name ) { $value = get_option( $old_prefix . $name, null ); if ( null !== $value ) { update_option( 'siteaudithub_' . $name, $value, false ); delete_option( $old_prefix . $name ); } } // Old background events and cached data under the old names. wp_clear_scheduled_hook( $old_prefix . 'process_queue' ); wp_clear_scheduled_hook( $old_prefix . 'hourly' ); $like = array( $wpdb->esc_like( '_transient_' . $old_prefix ) . '%', $wpdb->esc_like( '_transient_timeout_' . $old_prefix ) . '%', $wpdb->esc_like( $old_prefix . 'lock_' ) . '%', ); foreach ( $like as $pattern ) { $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s', $wpdb->options, $pattern ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- One-time clean-up of the plugin's own cached data. } wp_cache_delete( 'alloptions', 'options' ); Settings::flush(); // Fallback when a database cannot rename tables: create the new tables and copy the rows. $created = false; foreach ( array( 'scans', 'items', 'links', 'findings', 'baseline', 'logins' ) as $name ) { $old = $wpdb->prefix . $old_prefix . $name; $new = Db::table( $name ); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- One-time copy of the plugin's own tables. if ( $old !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old ) ) ) ) { continue; // Renamed (or never existed). } if ( ! $created ) { self::create_tables(); $created = true; } if ( 0 === (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $new ) ) ) { $wpdb->query( $wpdb->prepare( 'INSERT INTO %i SELECT * FROM %i', $new, $old ) ); } if ( (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $new ) ) >= (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $old ) ) ) { $wpdb->query( $wpdb->prepare( 'DROP TABLE %i', $old ) ); } // phpcs:enable } // Make sure every table exists so the plugin keeps working. foreach ( array( 'scans', 'items', 'links', 'findings', 'baseline', 'logins' ) as $name ) { if ( ! Db::table_exists( $name ) ) { self::create_tables(); break; } } } /** * Create or update tables and default options. */ public static function install() { self::migrate_old_names(); // Must run before any table or option is created under the new names. $from = (int) get_option( self::DB_OPTION, 0 ); self::create_tables(); if ( false === get_option( Settings::OPTION, false ) ) { add_option( Settings::OPTION, Settings::defaults(), '', false ); } // Versioned migrations run in order; each must be idempotent. $migrations = array( 1 => array( __CLASS__, 'migration_1' ), 2 => array( __CLASS__, 'migration_2' ), 3 => array( __CLASS__, 'migration_3' ), 4 => array( __CLASS__, 'migration_4' ), ); foreach ( $migrations as $version => $callback ) { if ( $from < $version ) { call_user_func( $callback ); // Record progress after each step so an interrupted upgrade resumes safely. update_option( self::DB_OPTION, $version, false ); } } update_option( self::DB_OPTION, SITEAUDITHUB_DB_VERSION, false ); } /** * Initial migration (placeholder for data changes; schema handled by dbDelta). */ public static function migration_1() { add_option( 'siteaudithub_ignored', array(), '', false ); } /** * 1.0.1: raise the integrity inspection size limit from the old 1 MB default * to 3 MB so bundled WordPress editor scripts are inspected. Custom values are kept. */ public static function migration_2() { $settings = get_option( Settings::OPTION, array() ); if ( is_array( $settings ) && isset( $settings['integrity_max_file_kb'] ) && 1024 === (int) $settings['integrity_max_file_kb'] ) { $settings['integrity_max_file_kb'] = 3072; update_option( Settings::OPTION, $settings, false ); Settings::flush(); } } /** * DB v4: login activity table (created by dbDelta in create_tables()). */ public static function migration_3() { Settings::flush(); } /** * Refresh cached health results after checks were removed or changed. */ public static function migration_4() { delete_transient( 'siteaudithub_health_results' ); } /** * Create tables via dbDelta (idempotent). */ public static function create_tables() { global $wpdb; $charset = $wpdb->get_charset_collate(); $scans = Db::table( 'scans' ); $items = Db::table( 'items' ); $links = Db::table( 'links' ); $findings = Db::table( 'findings' ); $baseline = Db::table( 'baseline' ); $logins = Db::table( 'logins' ); $sql = array(); $sql[] = "CREATE TABLE {$scans} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, scan_type varchar(20) NOT NULL DEFAULT 'site', status varchar(20) NOT NULL DEFAULT 'queued', stage varchar(30) NOT NULL DEFAULT '', trigger_source varchar(20) NOT NULL DEFAULT 'manual', created_by bigint(20) unsigned NOT NULL DEFAULT 0, created_at datetime NOT NULL, started_at datetime NULL, finished_at datetime NULL, heartbeat_at datetime NULL, total_items int(11) unsigned NOT NULL DEFAULT 0, processed_items int(11) unsigned NOT NULL DEFAULT 0, failed_items int(11) unsigned NOT NULL DEFAULT 0, skipped_items int(11) unsigned NOT NULL DEFAULT 0, findings_count int(11) unsigned NOT NULL DEFAULT 0, prev_scan_id bigint(20) unsigned NOT NULL DEFAULT 0, state longtext NULL, summary longtext NULL, config longtext NULL, error_message text NULL, PRIMARY KEY (id), KEY type_status (scan_type,status) ) {$charset};"; $sql[] = "CREATE TABLE {$items} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, scan_id bigint(20) unsigned NOT NULL, item_hash char(40) NOT NULL, item_key text NOT NULL, item_type varchar(20) NOT NULL DEFAULT 'url', source varchar(20) NOT NULL DEFAULT '', object_id bigint(20) unsigned NOT NULL DEFAULT 0, depth smallint(5) unsigned NOT NULL DEFAULT 0, in_sitemap tinyint(1) NOT NULL DEFAULT 0, status varchar(20) NOT NULL DEFAULT 'pending', http_status smallint(5) unsigned NOT NULL DEFAULT 0, attempts tinyint(3) unsigned NOT NULL DEFAULT 0, response_ms int(11) unsigned NOT NULL DEFAULT 0, final_url text NULL, title_hash char(32) NOT NULL DEFAULT '', desc_hash char(32) NOT NULL DEFAULT '', content_hash char(32) NOT NULL DEFAULT '', word_count int(11) unsigned NOT NULL DEFAULT 0, findings_count int(11) unsigned NOT NULL DEFAULT 0, data longtext NULL, claimed_at datetime NULL, processed_at datetime NULL, PRIMARY KEY (id), UNIQUE KEY scan_item (scan_id,item_hash), KEY scan_status (scan_id,status), KEY scan_title (scan_id,title_hash), KEY scan_desc (scan_id,desc_hash), KEY scan_content (scan_id,content_hash) ) {$charset};"; $sql[] = "CREATE TABLE {$links} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, scan_id bigint(20) unsigned NOT NULL, source_id bigint(20) unsigned NOT NULL, target_hash char(40) NOT NULL, target_url text NOT NULL, is_external tinyint(1) NOT NULL DEFAULT 0, anchor varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (id), KEY scan_target (scan_id,target_hash), KEY scan_source (scan_id,source_id) ) {$charset};"; $sql[] = "CREATE TABLE {$findings} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, scan_id bigint(20) unsigned NOT NULL, module varchar(20) NOT NULL, check_id varchar(60) NOT NULL, severity varchar(15) NOT NULL, classification varchar(20) NOT NULL, item_key text NULL, title varchar(255) NOT NULL DEFAULT '', description text NULL, evidence text NULL, recommendation text NULL, detection_method varchar(255) NOT NULL DEFAULT '', fingerprint char(40) NOT NULL, change_status varchar(15) NOT NULL DEFAULT 'new', created_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY scan_fp (scan_id,fingerprint), KEY scan_module (scan_id,module), KEY scan_severity (scan_id,severity), KEY fingerprint (fingerprint) ) {$charset};"; $sql[] = "CREATE TABLE {$baseline} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, path_hash char(40) NOT NULL, rel_path text NOT NULL, file_hash char(64) NOT NULL DEFAULT '', file_size bigint(20) unsigned NOT NULL DEFAULT 0, updated_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY path_hash (path_hash) ) {$charset};"; $sql[] = "CREATE TABLE {$logins} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL DEFAULT 0, user_login varchar(60) NOT NULL DEFAULT '', event varchar(20) NOT NULL, device varchar(80) NOT NULL DEFAULT '', created_at datetime NOT NULL, PRIMARY KEY (id), KEY created_at (created_at), KEY user_event (user_id,event) ) {$charset};"; if ( ! function_exists( 'dbDelta' ) ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } foreach ( $sql as $statement ) { dbDelta( $statement ); } } /** * Drop all plugin tables and options (used by uninstall when the admin opted in). */ public static function drop_everything() { global $wpdb; foreach ( array( 'scans', 'items', 'links', 'findings', 'baseline', 'logins' ) as $t ) { $table = Db::table( $t ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange } foreach ( array( Settings::OPTION, self::DB_OPTION, 'siteaudithub_ignored', 'siteaudithub_log', 'siteaudithub_mail_log', 'siteaudithub_schedule_state', 'siteaudithub_plugin_directory', 'siteaudithub_spam_blocked' ) as $option ) { delete_option( $option ); } Login_Limiter::clear_all(); delete_transient( 'siteaudithub_health_results' ); } }