PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/class-upgrader.php +378 -6 5.9.0 → 6.3.0 View file →
@@ -338,8 +338,9 @@
338 338
339 339 $db_version = get_site_option( 'wd_db_version' );
340 340 if ( ! is_string( $db_version ) || '' === $db_version ) {
341 341 self::cache_event_time( 'plugin_installed' );
342 + $this->create_database_tables_common();
342 343 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
343 344 update_site_option( Feature_Modal::FEATURE_SLUG, true );
344 345
345 346 return;
@@ -345,14 +346,21 @@
345 346 return;
346 347 }
347 348
348 349 if ( DEFENDER_DB_VERSION === $db_version ) {
350 + // Check if we need to create pro tables (Free to Pro upgrade scenario).
351 + if ( class_exists( Bootstrap::class ) && method_exists( Bootstrap::class, 'create_database_tables' ) && false === (bool) get_site_option( 'wd_pro_tables_created', false ) ) {
352 + wd_di()->get( Bootstrap::class )->create_database_tables();
353 + }
349 354 return;
350 355 }
351 356 // Cache last updated time.
352 357 self::cache_event_time( 'plugin_upgraded' );
353 358
354 - $this->create_database_tables();
359 + $this->create_database_tables_common();
360 + if ( class_exists( Bootstrap::class ) && method_exists( Bootstrap::class, 'create_database_tables' ) ) {
361 + wd_di()->get( Bootstrap::class )->create_database_tables();
362 + }
355 363 $this->maybe_show_new_features( $db_version );
356 364 $this->migrate_configs( $db_version );
357 365
358 366 if ( version_compare( $db_version, '2.2.9', '<' ) ) {
@@ -469,8 +477,17 @@
469 477 }
470 478 if ( version_compare( $db_version, '5.7.0', '<' ) ) {
471 479 $this->upgrade_5_7_0();
472 480 }
481 + if ( version_compare( $db_version, '6.0.0', '<' ) ) {
482 + $this->upgrade_6_0_0();
483 + }
484 + if ( version_compare( $db_version, '6.1.0', '<' ) ) {
485 + $this->upgrade_6_1_0();
486 + }
487 + if ( version_compare( $db_version, '6.2.0', '<' ) ) {
488 + $this->upgrade_6_2_0();
489 + }
473 490 // This is not a new installation. Make a mark.
474 491 defender_no_fresh_install();
475 492 // Don't run any function below this line.
476 493 update_site_option( 'wd_db_version', DEFENDER_DB_VERSION );
@@ -1371,9 +1388,8 @@
1371 1388 */
1372 1389 private function update_malware_notification_setting(): void {
1373 1390 $models = array(
1374 1391 wd_di()->get( Malware_Notification::class ),
1375 - wd_di()->get( Malware_Report::class ),
1376 1392 );
1377 1393
1378 1394 foreach ( $models as $model ) {
1379 1395 $is_updated = false;
@@ -1555,10 +1571,8 @@
1555 1571 // Changes for Lockout table.
1556 1572 $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_lockout MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1557 1573 // Changes for Lockout log table.
1558 1574 $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_lockout_log MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1559 - // Changes for Audit log table.
1560 - $wpdb->query( "ALTER TABLE{$wpdb->base_prefix}defender_audit_log MODIFY COLUMN ip VARCHAR(45)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1561 1575 $wpdb->show_errors( $prev_val );
1562 1576 }
1563 1577
1564 1578 /**
@@ -1982,8 +1996,366 @@
1982 1996 // Add the "What's new" modal.
1983 1997 update_site_option( Feature_Modal::FEATURE_SLUG, true );
1984 1998 // Add Cloudflare challenge-platform to 404 whitelist.
1985 1999 $this->force_nf_lockout_exclusions();
1986 - // Remove the prev Breadcrumbs.
1987 - wd_di()->get( \WP_Defender\Component\Breadcrumbs::class )->delete_previous_meta();
2000 + }
2001 +
2002 + /**
2003 + * Migrates the uninstall settings from v5.11 to v6.0.
2004 + *
2005 + * During the upgrade:
2006 + * - If at least one of the three options is set to Preserve/Keep,
2007 + * all three options are set to Preserve/Keep.
2008 + */
2009 + private function combine_all_preserve_settings_to_one() {
2010 + $main_settings = wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class );
2011 +
2012 + if (
2013 + 'preserve' === $main_settings->uninstall_settings ||
2014 + 'keep' === $main_settings->uninstall_data ||
2015 + 'keep' === $main_settings->uninstall_quarantine
2016 + ) {
2017 + $main_settings->uninstall_settings = 'preserve';
2018 + $main_settings->uninstall_data = 'keep';
2019 + $main_settings->uninstall_quarantine = 'keep';
2020 + $main_settings->save();
2021 + }
2022 + }
2023 +
2024 + /**
2025 + * Since with version 6.0.0, two nested options:
2026 + * - Scan core files
2027 + * - Scan plugin files,
2028 + * are hidden, and automatically take the value of the parent "File change detection" option.
2029 + */
2030 + private function combine_all_file_change_detection_settings_to_one() {
2031 + $scan_settings = wd_di()->get( Scan_Settings::class );
2032 + if ( $scan_settings->integrity_check ) {
2033 + $scan_settings->check_core = true;
2034 + $scan_settings->check_plugins = true;
2035 + } else {
2036 + $scan_settings->check_core = false;
2037 + $scan_settings->check_plugins = false;
2038 + }
2039 + $scan_settings->save();
2040 + }
2041 +
2042 + /**
2043 + * Upgrade to 6.0.0: Ensure quarantine table exists for Free HC users and other changes.
2044 + * The quarantine feature is now available in the free version.
2045 + *
2046 + * @return void
2047 + */
2048 + private function upgrade_6_0_0(): void {
2049 + $bootstrap = wd_di()->get( Bootstrap::class );
2050 + $bootstrap->create_table_quarantine();
2051 + $this->combine_all_preserve_settings_to_one();
2052 + $this->combine_all_file_change_detection_settings_to_one();
2053 + $this->update_issues_report_email_template();
2054 + $this->fix_scheduled_scanning_left_disabled();
2055 + $this->migrate_tweak_reminder_schedule();
2056 + $this->migrate_two_fa_force_auth_roles();
2057 + // Add the "What's new" modal only when landing on the exact release it belongs to,
2058 + // so it does not persist into later patch/minor releases (e.g. 6.0.1, 6.1).
2059 + Feature_Modal::maybe_enable_welcome_modal();
2060 + }
2061 +
2062 + /**
2063 + * Upgrade to 6.1.0: Fix Tweak Reminder est_timestamp left as 0 by the 6.0.0 migration.
2064 + * Also sync all report schedules to match Malware Report.
2065 + *
2066 + * @return void
2067 + */
2068 + private function upgrade_6_1_0(): void {
2069 + $model = wd_di()->get( Tweak_Reminder::class );
2070 + if ( 'report' === $model->type && (int) $model->est_timestamp <= 0 ) {
2071 + // Calling save() recovers the correct state: last_sent is
2072 + // reset to now if it is 0, then est_timestamp is recomputed via get_next_run().
2073 + $model->save();
2074 + }
2075 +
2076 + $this->sync_all_report_schedules();
2077 + }
2078 +
2079 + /**
2080 + * Upgrade to 6.2.0.
2081 + *
2082 + * @return void
2083 + */
2084 + private function upgrade_6_2_0(): void {
2085 + $this->fix_notification_recipients_format();
2086 + $this->cancel_recipients_of_inactive_reports();
2087 + }
2088 +
2089 + /**
2090 + * Unsubscribe every recipient of each inactive report so their stored preferences match
2091 + * the reports' last saved on/off state.
2092 + *
2093 + * @return void
2094 + */
2095 + private function cancel_recipients_of_inactive_reports(): void {
2096 + $report_classes = array(
2097 + Tweak_Reminder::class,
2098 + Malware_Report::class,
2099 + Firewall_Report::class,
2100 + Audit_Report::class,
2101 + );
2102 +
2103 + foreach ( $report_classes as $report_class ) {
2104 + $model = wd_di()->get( $report_class );
2105 + if ( $model->check_active_status() ) {
2106 + continue;
2107 + }
2108 +
2109 + $changed = false;
2110 + foreach ( array( 'in_house_recipients', 'out_house_recipients' ) as $group ) {
2111 + $recipients = 'in_house_recipients' === $group
2112 + ? $model->in_house_recipients
2113 + : $model->out_house_recipients;
2114 + if ( ! is_array( $recipients ) ) {
2115 + continue;
2116 + }
2117 +
2118 + foreach ( $recipients as $key => $recipient ) {
2119 + if ( Notification::USER_SUBSCRIBE_CANCELED !== ( $recipient['status'] ?? '' ) ) {
2120 + $recipients[ $key ]['status'] = Notification::USER_SUBSCRIBE_CANCELED;
2121 + $changed = true;
2122 + }
2123 + }
2124 +
2125 + if ( 'in_house_recipients' === $group ) {
2126 + $model->in_house_recipients = $recipients;
2127 + } else {
2128 + $model->out_house_recipients = $recipients;
2129 + }
2130 + }
2131 +
2132 + if ( $changed ) {
2133 + $model->save();
2134 + }
2135 + }
2136 + }
2137 +
2138 + /**
2139 + * Heal recipients that older 6.x versions stored as email-keyed objects instead of arrays,
2140 + * which breaks Defender 5 on downgrade.
2141 + *
2142 + * @return void
2143 + */
2144 + private function fix_notification_recipients_format(): void {
2145 + $tables = array(
2146 + wd_di()->get( Audit_Report::class )->get_table(),
2147 + wd_di()->get( Firewall_Notification::class )->get_table(),
2148 + wd_di()->get( Firewall_Report::class )->get_table(),
2149 + wd_di()->get( Malware_Notification::class )->get_table(),
2150 + wd_di()->get( Malware_Report::class )->get_table(),
2151 + wd_di()->get( Tweak_Reminder::class )->get_table(),
2152 + );
2153 +
2154 + foreach ( $tables as $table ) {
2155 + $raw = get_site_option( $table );
2156 + if ( false === $raw ) {
2157 + continue;
2158 + }
2159 +
2160 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
2161 + if ( ! is_array( $data ) ) {
2162 + continue;
2163 + }
2164 +
2165 + $changed = false;
2166 + foreach ( array( 'in_house_recipients', 'out_house_recipients', 'all_subscribers' ) as $key ) {
2167 + if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) ) {
2168 + // array_values() leaves a proper list untouched but reindexes an email-keyed one.
2169 + $reindexed = array_values( $data[ $key ] );
2170 + if ( $reindexed !== $data[ $key ] ) {
2171 + $data[ $key ] = $reindexed;
2172 + $changed = true;
2173 + }
2174 + }
2175 + }
2176 +
2177 + if ( $changed ) {
2178 + update_site_option( $table, wp_json_encode( $data ) );
2179 + }
2180 + }
2181 + }
2182 +
2183 + /**
2184 + * Sync all report schedules to match Malware Report.
2185 + *
2186 + * Since 6.0.0 the UI enforces a single shared schedule for all reports via sync_report_schedule.
2187 + * Sites upgrading from older versions may have each report on a different schedule.
2188 + * This migration copies Malware_Report's schedule to Scan_Settings (the UI master) and to
2189 + * all four report models, then recomputes est_timestamp on each so all report emails
2190 + * arrive at the same time.
2191 + *
2192 + * @return void
2193 + */
2194 + private function sync_all_report_schedules(): void {
2195 + // Get the schedule from Malware_Report.
2196 + $malware_report = wd_di()->get( Malware_Report::class );
2197 + $frequency = $malware_report->frequency;
2198 + $day = $malware_report->day;
2199 + $day_n = (int) $malware_report->day_n;
2200 + $time = $malware_report->time;
2201 +
2202 + // Update Scan_Settings so the schedule pill shows the correct label.
2203 + $scan_settings = wd_di()->get( Scan_Settings::class );
2204 + $scan_settings->frequency = $frequency;
2205 + $scan_settings->day = $day;
2206 + $scan_settings->day_n = $day_n;
2207 + $scan_settings->time = $time;
2208 + $scan_settings->save();
2209 +
2210 + // Apply the same schedule to all report models and recompute est_timestamp
2211 + // so every report fires at the same next occurrence.
2212 + $report_models = array(
2213 + $malware_report,
2214 + wd_di()->get( Firewall_Report::class ),
2215 + wd_di()->get( Audit_Report::class ),
2216 + wd_di()->get( Tweak_Reminder::class ),
2217 + );
2218 +
2219 + foreach ( $report_models as $model ) {
2220 + $model->frequency = $frequency;
2221 + $model->day = $day;
2222 + $model->day_n = $day_n;
2223 + $model->time = $time;
2224 +
2225 + $next_run = $model->get_next_run();
2226 + if ( $next_run instanceof \DateTime ) {
2227 + $model->est_timestamp = $next_run->getTimestamp();
2228 + }
2229 +
2230 + $model->save();
2231 + }
2232 +
2233 + // Show a one-time dashboard bubble so users know about the shared schedule.
2234 + update_site_option( \WP_Defender\Controller\Dashboard::REPORT_SCHEDULE_NOTICE_OPTION, true );
2235 + }
2236 +
2237 + /**
2238 + * Heal 'Scheduled Scanning' left disabled while its related report stayed active.
2239 + *
2240 + * The bundled "Basic Config" preset stored 'scheduled_scanning' out of sync with its
2241 + * 'report' flag, so applying it left scans disabled. Corrects the live setting and any
2242 + * already-stored config presets with the same mismatch.
2243 + *
2244 + * @return void
2245 + */
2246 + private function fix_scheduled_scanning_left_disabled(): void {
2247 + $scan_settings = wd_di()->get( Scan_Settings::class );
2248 + $malware_report = wd_di()->get( Malware_Report::class );
2249 + if ( ! $scan_settings->scheduled_scanning && Notification::STATUS_ACTIVE === $malware_report->status ) {
2250 + $scan_settings->scheduled_scanning = true;
2251 + $scan_settings->save();
2252 + }
2253 +
2254 + $service = wd_di()->get( Backup_Settings::class );
2255 + $configs = Config_Hub_Helper::get_configs( $service );
2256 +
2257 + foreach ( $configs as $key => $config ) {
2258 + $scan = $config['configs']['scan'] ?? null;
2259 + if ( ! is_array( $scan ) || ! isset( $scan['report'], $scan['scheduled_scanning'] ) ) {
2260 + continue;
2261 + }
2262 +
2263 + if ( 'enabled' === $scan['report'] && false === $scan['scheduled_scanning'] ) {
2264 + $configs[ $key ]['configs']['scan']['scheduled_scanning'] = true;
2265 + update_site_option( $key, $configs[ $key ] );
2266 + Config_Hub_Helper::update_on_hub( $configs[ $key ] );
2267 + }
2268 + }
2269 +
2270 + delete_site_transient( Config_Hub_Helper::CONFIGS_TRANSIENT_KEY );
2271 + }
2272 +
2273 + /**
2274 + * Clear force_auth_roles when user_roles is empty.
2275 + *
2276 + * @return void
2277 + */
2278 + private function migrate_two_fa_force_auth_roles(): void {
2279 + $model = wd_di()->get( Two_Fa_Settings::class );
2280 + if ( count( $model->user_roles ) === 0 ) {
2281 + $model->force_auth = false;
2282 + $model->force_auth_roles = array();
2283 + $model->save();
2284 + }
2285 + }
2286 +
2287 + /**
2288 + * Migrate the Hardening report (Tweak Reminder) schedule from 5.11 to 6.0.0.
2289 + *
2290 + * In 5.11, this report was saved with type 'notification' and its frequency was kept in
2291 + * `configs['reminder']`. In 6.0.0, it's saved with type 'report' and uses new, dedicated
2292 + * fields instead: `frequency`, `day`, `day_n`, `time`, `est_timestamp`.
2293 + *
2294 + * Without this migration, a site updated from 5.11 keeps the old type 'notification', so the
2295 + * new scheduling code can't calculate a valid next-send date and the report email gets sent
2296 + * on every cron run instead of on the configured schedule.
2297 + *
2298 + * @return void
2299 + */
2300 + private function migrate_tweak_reminder_schedule(): void {
2301 + $model = wd_di()->get( Tweak_Reminder::class );
2302 + if ( 'notification' !== $model->type ) {
2303 + return;
2304 + }
2305 +
2306 + $model->type = 'report';
2307 +
2308 + $valid_frequencies = array( 'daily', 'weekly', 'monthly' );
2309 + $reminder = $model->configs['reminder'] ?? '';
2310 + $model->frequency = in_array( $reminder, $valid_frequencies, true ) ? $reminder : 'weekly';
2311 +
2312 + if ( null === $model->day || '' === $model->day ) {
2313 + $model->day = 'sunday';
2314 + }
2315 + if ( null === $model->time || '' === $model->time ) {
2316 + $model->time = '4:00';
2317 + }
2318 + if ( ! is_int( $model->day_n ) || $model->day_n < 1 ) {
2319 + $model->day_n = 1;
2320 + }
2321 + // Compute est_timestamp.
2322 + $next_run = $model->get_next_run();
2323 + if ( $next_run instanceof \DateTime ) {
2324 + $model->est_timestamp = $next_run->getTimestamp();
2325 + }
2326 + // Clear the old notification style configs key.
2327 + unset( $model->configs['reminder'] );
2328 + $model->save();
2329 + }
2330 +
2331 + /**
2332 + * Update Issues Report email subject and body to match the new copy.
2333 + *
2334 + * @return void
2335 + */
2336 + private function update_issues_report_email_template(): void {
2337 + $models = array(
2338 + wd_di()->get( Malware_Notification::class ),
2339 + wd_di()->get( Malware_Report::class ),
2340 + );
2341 +
2342 + $default_subject = esc_html__( 'Issues Report for {SITE_URL}: {ISSUES_COUNT} issue(s) found.', 'defender-security' );
2343 + $default_body = esc_html__(
2344 + 'Hi {USER_NAME},
2345 +
2346 +A scan of {SITE_URL} identified {ISSUES_COUNT} issue(s). The issue(s) found is/are listed below.
2347 +
2348 +{ISSUES_LIST}',
2349 + 'defender-security'
2350 + );
2351 +
2352 + foreach ( $models as $model ) {
2353 + if ( ! isset( $model->configs['template']['found'] ) ) {
2354 + continue;
2355 + }
2356 + $model->configs['template']['found']['subject'] = $default_subject;
2357 + $model->configs['template']['found']['body'] = $default_body;
2358 + $model->save();
2359 + }
1988 2360 }
1989 2361 }