PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.0.0
Defender Security – Malware Scanner, Login Security & Firewall v6.0.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
defender-security / src / traits / defender-bootstrap.php

defender-bootstrap.php in Defender Security – Malware Scanner, Login Security & Firewall 6.0.0, at src/traits/defender-bootstrap.php

840 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handle common bootstrap functionalities.
4 *
5 * @package WP_Defender\Traits
6 */
7
8 namespace WP_Defender\Traits;
9
10 use WP_CLI;
11 use Calotes\DB\Mapper;
12 use WP_Defender\Admin;
13 use WP_Defender\Component\Cli;
14 use Calotes\Helper\Array_Cache;
15 use WP_Defender\Controller\HUB;
16 use WP_Defender\Controller\WAF;
17 use WP_Defender\Controller\Scan;
18 use WP_Defender\Component\Crypt;
19 use WP_Defender\Behavior\WPMUDEV;
20 use WP_Defender\Controller\Onboard;
21 use WP_Defender\Controller\Webauthn;
22 use WP_Defender\Controller\Dashboard;
23 use WP_Defender\Controller\Captcha;
24 use WP_Defender\Controller\Recipients;
25 use WP_Defender\Controller\Quarantine;
26 use WP_Defender\Controller\Mask_Login;
27 use WP_Defender\Controller\Two_Factor;
28 use WP_Defender\Component\Hub_Connector;
29 use WP_Defender\Controller\Main_Setting;
30 use WP_Defender\Controller\Notification;
31 use WP_Defender\Controller\Audit_Logging;
32 use WP_Defender\Controller\Data_Tracking;
33 use WP_Defender\Controller\Login_Access;
34 use WP_Defender\Controller\Password_Reset;
35 use WP_Defender\Controller\Setup_Wizard;
36 use WP_Defender\Controller\Strong_Password;
37 use WP_Defender\Controller\Expert_Services;
38 use WP_Defender\Controller\Security_Tweaks;
39 use WP_Defender\Controller\Security_Headers;
40 use WP_Defender\Controller\Blocklist_Monitor;
41 use WP_Defender\Controller\Session_Protection;
42 use WP_Defender\Controller\Password_Protection;
43 use WP_Defender\Component\Network_Cron_Manager;
44 use WP_Defender\Component\Logger\Rotation_Logger;
45 use WP_Defender\Component\Firewall as Firewall_Component;
46 use WP_Defender\Controller\Firewall as Firewall_Controller;
47 use WP_Defender\Controller\Hub_Connector as Hub_Connector_Controller;
48 use WP_Defender\Model\Onboard as Onboard_Model;
49 use WP_Defender\Controller\Rate as Rate_Controller;
50 use WP_Defender\Component\Rate as Rate_Component;
51 use WP_Defender\Upgrader;
52
53 trait Defender_Bootstrap {
54
55 /**
56 * Table name for quarantine.
57 *
58 * @var string
59 */
60 private $quarantine_table = 'defender_quarantine';
61
62 /**
63 * Table name for scan item.
64 *
65 * @var string
66 */
67 private $scan_item_table = 'defender_scan_item';
68
69 /**
70 * Activation.
71 */
72 private function activation_hook_common(): void {
73 Upgrader::date_activated();
74 $this->create_database_tables_common();
75 $this->on_activation();
76 $this->set_activation_redirect_flag();
77 // Create a file with a random key if it doesn't exist.
78 ( new Crypt() )->create_key_file();
79 // If this is a plugin reactivating, then track it. No need the check by 'wd_nofresh_install' key because the option is disabled by default.
80 $settings = wd_di()->get( Main_Setting::class );
81 $settings->set_intention( 'Reactivation' );
82 $settings->track_opt( true );
83
84 $service = wd_di()->get( Firewall_Component::class );
85 $service->auto_switch_ip_detection_option();
86 $service->maybe_show_misconfigured_ip_detection_option_notice();
87 $service->maybe_dismiss_cf_notice();
88 wp_schedule_single_event( time() + 5, 'wpdef_smart_ip_detection_ping' );
89 }
90
91 /**
92 * Deactivation - clears shared cron hooks present in both free and pro versions.
93 */
94 protected function deactivation_hook_common(): void {
95 wp_clear_scheduled_hook( 'firewall_clean_up_logs' );
96 wp_clear_scheduled_hook( 'wdf_maybe_send_report' );
97 wp_clear_scheduled_hook( 'wp_defender_clear_logs' );
98 wp_clear_scheduled_hook( 'wpdef_sec_key_gen' );
99 wp_clear_scheduled_hook( 'wpdef_clear_scan_logs' );
100 wp_clear_scheduled_hook( 'wpdef_log_rotational_delete' );
101 wp_clear_scheduled_hook( 'wpdef_update_geoip' );
102 wp_clear_scheduled_hook( 'wpdef_fetch_global_ip_list' );
103 wp_clear_scheduled_hook( 'wpdef_firewall_clean_up_lockout' );
104 wp_clear_scheduled_hook( 'wpdef_firewall_send_compact_logs_to_api' );
105 wp_clear_scheduled_hook( 'wpdef_firewall_fetch_trusted_proxy_preset_ips' );
106 wp_clear_scheduled_hook( 'wpdef_firewall_clean_up_unlockout' );
107 wp_clear_scheduled_hook( 'wpdef_antibot_global_firewall_fetch_blocklist' );
108 wp_clear_scheduled_hook( 'wpdef_smart_ip_detection_ping' );
109 wp_clear_scheduled_hook( 'wpdef_confirm_antibot_toggle_on_hosting' );
110 wp_clear_scheduled_hook( 'wpdef_firewall_whitelist_server_public_ip' );
111 wp_clear_scheduled_hook( 'wpdef_rotate_malicious_bot_secret_hash' );
112
113 // Remove old legacy cron jobs if they exist.
114 wp_clear_scheduled_hook( 'lockoutReportCron' );
115 wp_clear_scheduled_hook( 'cleanUpOldLog' );
116 wp_clear_scheduled_hook( 'scanReportCron' );
117 wp_clear_scheduled_hook( 'tweaksSendNotification' );
118 }
119
120 /**
121 * Creates the 'defender_unlockout' table if it doesn't exist in the database.
122 *
123 * @return void
124 */
125 public function create_table_unlockout() {
126 global $wpdb;
127
128 $charset_collate = $wpdb->get_charset_collate();
129
130 $sql = <<<SQL
131 CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_unlockout (
132 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
133 `ip` varchar(45) DEFAULT NULL,
134 `type` varchar(16) NOT NULL,
135 `email` varchar(255) NOT NULL,
136 `status` varchar(16) NOT NULL,
137 `timestamp` int(11) NOT NULL,
138 PRIMARY KEY (`id`),
139 KEY `ip` (`ip`),
140 KEY `type` (`type`),
141 KEY `email` (`email`),
142 KEY `status` (`status`)
143 ) {$charset_collate};
144 SQL;
145 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
146 }
147
148 /**
149 * Create blocklist table.
150 *
151 * @since 2.8.0
152 * @return void
153 */
154 public function create_table_blocklist(): void {
155 global $wpdb;
156
157 $charset_collate = $wpdb->get_charset_collate();
158
159 $sql = <<<SQL
160 CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_antibot (
161 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
162 `ip` varchar(45) NOT NULL,
163 `unlocked` tinyint(1) DEFAULT NULL,
164 `unlocked_at` int(11) DEFAULT NULL,
165 PRIMARY KEY (`id`),
166 UNIQUE KEY ip (ip)
167 ) {$charset_collate};
168 SQL;
169 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
170 }
171
172 /**
173 * Creates Defender's tables.
174 *
175 * @since 2.7.1 No use dbDelta because PHP v8.1 triggers an error when calling query "DESCRIBE {$table};" if the
176 * table doesn't exist.
177 */
178 protected function create_database_tables_common(): void {
179 global $wpdb;
180
181 $charset_collate = $wpdb->get_charset_collate();
182 // Hide errors.
183 $wpdb->hide_errors();
184 // Email log table.
185 $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_email_log (
186 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
187 `timestamp` int NOT NULL,
188 `source` varchar(255) NOT NULL,
189 `to` varchar(255) NOT NULL,
190 PRIMARY KEY (`id`),
191 KEY `source` (`source`)
192 ) $charset_collate;";
193 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
194
195 // Scan item table.
196 $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_scan_item (
197 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
198 `parent_id` int NOT NULL,
199 `type` varchar(255) NOT NULL,
200 `status` varchar(255) NOT NULL,
201 `raw_data` text NOT NULL,
202 PRIMARY KEY (`id`),
203 KEY `type` (`type`),
204 KEY `status` (`status`)
205 ) $charset_collate;";
206 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
207
208 // Scan table.
209 $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_scan (
210 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
211 `percent` float NOT NULL,
212 `total_tasks` tinyint(4) NOT NULL,
213 `task_checkpoint` varchar(255) NOT NULL,
214 `status` varchar(255) NOT NULL,
215 `date_start` datetime NOT NULL,
216 `date_end` datetime NOT NULL,
217 `is_automation` bool NOT NULL,
218 PRIMARY KEY (`id`)
219 ) $charset_collate;";
220 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
221
222 // Lockout log table.
223 $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_lockout_log (
224 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
225 `log` text,
226 `ip` varchar(45) DEFAULT NULL,
227 `date` int(11) DEFAULT NULL,
228 `type` varchar(16) DEFAULT NULL,
229 `user_agent` varchar(255) DEFAULT NULL,
230 `blog_id` int(11) DEFAULT NULL,
231 `tried` varchar(255),
232 `country_iso_code` char(2) DEFAULT NULL,
233 PRIMARY KEY (`id`),
234 KEY `ip` (`ip`),
235 KEY `type` (`type`),
236 KEY `tried` (`tried`),
237 KEY `country_iso_code` (`country_iso_code`)
238 ) $charset_collate;";
239 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
240
241 // Lockout table.
242 $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->base_prefix}defender_lockout (
243 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
244 `ip` varchar(45) DEFAULT NULL,
245 `status` varchar(16) DEFAULT NULL,
246 `lockout_message` text,
247 `release_time` int(11) DEFAULT NULL,
248 `lock_time` int(11) DEFAULT NULL,
249 `lock_time_404` int(11) DEFAULT NULL,
250 `attempt` int(11) DEFAULT NULL,
251 `attempt_404` int(11) DEFAULT NULL,
252 `meta` text,
253 PRIMARY KEY (`id`),
254 KEY `ip` (`ip`),
255 KEY `status` (`status`),
256 KEY `attempt` (`attempt`),
257 KEY `attempt_404` (`attempt_404`)
258 ) $charset_collate;";
259 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
260
261 // Create unlockout table.
262 $this->create_table_unlockout();
263 // Create Blocklist table.
264 $this->create_table_blocklist();
265 // Create Quarantine table.
266 $this->create_table_quarantine();
267 }
268
269 /**
270 * Check if all quarantine-dependent tables use the InnoDB storage engine.
271 *
272 * @return bool True if all dependent tables are InnoDB, false otherwise.
273 */
274 private function is_quarantine_dependent_tables_innodb(): bool {
275 global $wpdb;
276
277 $tables = array( $wpdb->users, $wpdb->base_prefix . $this->scan_item_table );
278 $total_table = count( $tables );
279
280 return $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
281 $wpdb->prepare(
282 "SELECT COUNT(`ENGINE`) = %d FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND `ENGINE` = %s AND TABLE_NAME IN ( '{$wpdb->users}', '{$wpdb->base_prefix}defender_scan_item' );",
283 $total_table,
284 $wpdb->dbname,
285 'innodb',
286 )
287 ) === '1';
288 }
289
290 /**
291 * Creates the quarantine table if it doesn't exist.
292 * Uses InnoDB foreign key constraints when the dependent tables support it,
293 * otherwise falls back to plain KEY indexes.
294 *
295 * @return void
296 */
297 public function create_table_quarantine(): void {
298 global $wpdb;
299
300 $quarantine_table = $wpdb->base_prefix . $this->quarantine_table;
301 $scan_item_table = $wpdb->base_prefix . $this->scan_item_table;
302 $charset_collate = $wpdb->get_charset_collate();
303 $unique_id = uniqid( $wpdb->prefix );
304
305 $common_columns = <<<'SQL'
306 `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
307 `defender_scan_item_id` int UNSIGNED DEFAULT NULL,
308 `file_hash` char(53) NOT NULL,
309 `file_full_path` text NOT NULL,
310 `file_original_name` tinytext NOT NULL,
311 `file_extension` varchar(16) DEFAULT NULL,
312 `file_mime_type` varchar(64) DEFAULT NULL,
313 `file_rw_permission` smallint UNSIGNED DEFAULT NULL,
314 `file_owner` varchar(255) DEFAULT NULL,
315 `file_group` varchar(255) DEFAULT NULL,
316 `file_version` varchar(32) DEFAULT NULL,
317 `file_category` tinyint UNSIGNED DEFAULT 0,
318 `file_modified_time` datetime NOT NULL,
319 `source_slug` varchar(255) NOT NULL,
320 `created_time` datetime NOT NULL,
321 `created_by` bigint UNSIGNED DEFAULT NULL,
322 PRIMARY KEY (`id`)
323 SQL;
324
325 // Define key names.
326 $scan_item_key = "{$unique_id}_defender_scan_item_id";
327 $created_by_key = "{$unique_id}_created_by";
328
329 // Build the SQL statement based on the storage engine.
330 if ( $this->is_quarantine_dependent_tables_innodb() ) {
331 $sql = <<<SQL
332 CREATE TABLE IF NOT EXISTS `{$quarantine_table}` (
333 $common_columns,
334 CONSTRAINT `{$scan_item_key}`
335 FOREIGN KEY (`defender_scan_item_id`) REFERENCES {$scan_item_table}(`id`)
336 ON UPDATE CASCADE ON DELETE SET NULL,
337 CONSTRAINT `{$created_by_key}`
338 FOREIGN KEY (`created_by`) REFERENCES {$wpdb->users}(`ID`)
339 ON UPDATE CASCADE ON DELETE SET NULL
340 ) {$charset_collate};
341 SQL;
342 } else {
343 $sql = <<<SQL
344 CREATE TABLE IF NOT EXISTS `{$quarantine_table}` (
345 $common_columns,
346 KEY `{$scan_item_key}` (`defender_scan_item_id`),
347 KEY `{$created_by_key}` (`created_by`)
348 ) {$charset_collate};
349 SQL;
350 }
351
352 // Execute the SQL query.
353 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
354 }
355
356 /**
357 * Check if this is onboarding.
358 *
359 * @return bool
360 */
361 private function is_onboarding(): bool {
362 // Temporary switch for setup-wizard showcase phase:
363 // keep legacy onboarding disabled until new flow integration.
364 $enable_legacy_onboarding = (bool) apply_filters( 'wpdef_enable_legacy_onboarding', false );
365 if ( ! $enable_legacy_onboarding ) {
366 return false;
367 }
368
369 /**
370 * Display Onboarding if:
371 * it's a fresh install and there were no requests from the Hub before,
372 * after Reset Settings.
373 *
374 * @var HUB
375 */
376 $hub_class = wd_di()->get( HUB::class );
377 $hub_class->set_onboarding_status( Onboard_Model::maybe_show_onboarding() );
378
379 return $hub_class->get_onboarding_status() && ! defender_is_wp_cli();
380 }
381
382 /**
383 * Initialize the common modules of the application.
384 *
385 * @return void
386 */
387 private function init_modules_common(): void {
388 // Init main ORM.
389 Array_Cache::set( 'orm', new Mapper() );
390
391 // New setup wizard is rendered directly inside Dashboard React UI.
392 // Keep legacy onboarding initialization commented for rollback/reference.
393 // if ( $this->is_onboarding() ) {
394 // If it's cli we should start this normally.
395 // Array_Cache::set( 'onboard', wd_di()->get( Onboard::class) );
396 // } else {
397 // Initialize the main controllers of every module.
398 // wd_di()->get( Dashboard::class );
399 // }
400 wd_di()->get( Setup_Wizard::class );
401 wd_di()->get( \WP_Defender\Controller\Activity_Log::class );
402 wd_di()->get( Dashboard::class );
403 wd_di()->get( Scan::class );
404 wd_di()->get( Security_Tweaks::class );
405 wd_di()->get( Audit_Logging::class );
406 wd_di()->get( Firewall_Controller::class );
407 wd_di()->get( Login_Access::class );
408 // Initialize Mask Login globally so runtime protections hook into wp-login/wp-admin requests.
409 wd_di()->get( Mask_Login::class );
410 wd_di()->get( WAF::class );
411 wd_di()->get( Two_Factor::class );
412 wd_di()->get( Mask_Login::class );
413 // wd_di()->get( Security_Headers::class );
414 wd_di()->get( Captcha::class );
415 wd_di()->get( Notification::class );
416 wd_di()->get( Recipients::class );
417 wd_di()->get( Main_Setting::class );
418 wd_di()->get( Blocklist_Monitor::class );
419 wd_di()->get( Password_Protection::class );
420 wd_di()->get( Password_Reset::class );
421 wd_di()->get( Webauthn::class );
422 // wd_di()->get( Expert_Services::class );
423 wd_di()->get( Hub_Connector_Controller::class );
424 wd_di()->get( Strong_Password::class );
425 wd_di()->get( Session_Protection::class );
426 wd_di()->get( Quarantine::class );
427 wd_di()->get( Data_Tracking::class );
428 if ( defender_is_wp_org_version() ) {
429 wd_di()->get( Rate_Controller::class );
430 }
431
432 if ( is_multisite() ) {
433 wd_di()->get( Network_Cron_Manager::class );
434 }
435 }
436
437 /**
438 * Adds a specific class to the body tag if the current page is a Defender page.
439 *
440 * @param string $classes The existing body classes.
441 *
442 * @return string The modified body classes.
443 */
444 public function add_sui_to_body( $classes ) {
445 if ( ! is_defender_page() ) {
446 return $classes;
447 }
448 $classes .= sprintf( ' sui-%s ', DEFENDER_SUI );
449
450 return $classes;
451 }
452
453 /**
454 * Registers the necessary styles for the plugin.
455 *
456 * @return void
457 */
458 private function register_styles(): void {
459 wp_enqueue_style( 'defender-menu', WP_DEFENDER_BASE_URL . 'assets/css/defender-icon.css', array(), DEFENDER_VERSION );
460 wp_add_inline_style(
461 'defender-menu',
462 '#toplevel_page_wp-defender .wd-issue-indicator-sidebar {
463 position: relative;
464 background-color: transparent !important;
465 width: 8px;
466 min-width: unset;
467 padding: 0;
468 margin-left: 4px;
469 }
470 #toplevel_page_wp-defender .wd-issue-indicator-sidebar::after {
471 content: "";
472 position: absolute;
473 width: 8px;
474 height: 8px;
475 top: 50%;
476 left: 50%;
477 transform: translate(-50%, -50%);
478 border-radius: 50%;
479 background: #d63638;
480 }'
481 );
482
483 $css_files = array(
484 'defender' => WP_DEFENDER_BASE_URL . 'assets/css/styles.css',
485 'def-sui' => WP_DEFENDER_BASE_URL . 'assets/css/shared-ui.css',
486 'def-admin' => WP_DEFENDER_BASE_URL . 'assets/css/admin.css',
487 'def-showcase' => WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
488 );
489
490 foreach ( $css_files as $slug => $file ) {
491 wp_register_style( $slug, $file, array(), DEFENDER_VERSION );
492 }
493 }
494
495 /**
496 * Registers the necessary scripts for the plugin.
497 *
498 * @return void
499 */
500 private function register_scripts(): void {
501 $base_url = WP_DEFENDER_BASE_URL;
502 $dependencies = array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' );
503 $js_files = array(
504 'wpmudev-sui' => array(
505 $base_url . 'assets/js/shared-ui.js',
506 ),
507 'defender' => array(
508 $base_url . 'assets/js/scripts.js',
509 ),
510 'def-deactivation-survey' => array(
511 $base_url . 'assets/js/deactivation-survey.js',
512 array( 'clipboard', 'wpmudev-sui' ),
513 ),
514 'def-admin' => array(
515 $base_url . 'assets/js/admin.js',
516 array( 'def-deactivation-survey' ),
517 ),
518 'def-vue' => array(
519 $base_url . 'assets/js/vendor.js',
520 ),
521 'def-manifest' => array(
522 $base_url . 'assets/js/manifest.js',
523 ),
524 'def-core-ui' => array(
525 $base_url . 'assets/js/core-ui.js',
526 array( 'def-vue', 'def-manifest' ),
527 ),
528 // React files.
529 'def-showcase' => array(
530 $base_url . 'assets/js/showcase.js',
531 $dependencies,
532 ),
533 'def-setup-wizard' => array(
534 $base_url . 'assets/js/setup-wizard.js',
535 $dependencies,
536 ),
537 );
538
539 foreach ( $js_files as $slug => $file ) {
540 if ( isset( $file[1] ) ) {
541 // This ensures that when JavaScript file changes,
542 // browsers will load the new version instead of
543 // serving a cached old version.
544 $file_path = str_replace( $base_url, WP_DEFENDER_DIR, $file[0] );
545 $file_version = file_exists( $file_path ) ? filemtime( $file_path ) : DEFENDER_VERSION;
546 wp_register_script( $slug, $file[0], $file[1], $file_version, true );
547 wp_set_script_translations( $slug, 'defender-security' );
548 } else {
549 wp_register_script( $slug, $file[0], array( 'jquery' ), DEFENDER_VERSION, true );
550 }
551 }
552 }
553
554 /**
555 * Localizes the script by adding necessary data to the 'defender' object.
556 *
557 * @return void
558 */
559 private function localize_script(): void {
560 $wpmu_dev = new WPMUDEV();
561 global $wp_defender_central;
562
563 $misc = array();
564 $data_tracking = wd_di()->get( Data_Tracking::class );
565 $is_tracking = $data_tracking->show_tracking_modal();
566 if ( $is_tracking ) {
567 $misc = $data_tracking->get_tracking_modal();
568 }
569 $misc['high_contrast'] = defender_high_contrast();
570 $is_wp_org = defender_is_wp_org_version();
571 if ( $is_wp_org ) {
572 $misc['rating'] = array();
573 $rate_service = Rate_Component::is_achievement_displayed();
574 if ( $rate_service['is_displayed'] ) {
575 $misc['rating'] = wd_di()->get( Rate_Controller::class )->data_frontend();
576 $misc['rating']['text'] = Rate_Component::get_notice_by_slug( $rate_service['slug'] );
577 }
578
579 $misc['rating']['is_displayed'] = $rate_service['is_displayed'];
580 $misc['rating']['type'] = $rate_service['slug'];
581 } else {
582 $misc['rating']['is_displayed'] = false;
583 }
584
585 wp_localize_script(
586 'def-vue',
587 'defender',
588 array(
589 'whitelabel' => defender_white_label_status(),
590 'misc' => $misc,
591 'home_url' => network_home_url(),
592 'site_url' => network_site_url(),
593 'admin_url' => network_admin_url(),
594 'defender_url' => WP_DEFENDER_BASE_URL,
595 // There might be Pro version but without pro features, for example when the membership expires.
596 'is_free' => $wpmu_dev->is_pro() ? 0 : 1,
597 // Strictly Free version.
598 'is_wp_org' => $is_wp_org ? 1 : 0,
599 'is_membership' => true,
600 'is_whitelabel' => $wpmu_dev->is_whitelabel_enabled() ? 'enabled' : 'disabled',
601 'wpmu_dev_url_action' => $wpmu_dev->hide_wpmu_dev_urls() ? 'hide' : 'show',
602 'opcache_save_comments' => $wp_defender_central->is_opcache_save_comments_disabled() ? 'disabled' : 'enabled',
603 'opcache_message' => $wp_defender_central->display_opcache_message(),
604 'wpmudev_url' => WP_DEFENDER_DOCS_LINK,
605 'wpmudev_support_ticket_text' => defender_support_ticket_text(),
606 'wpmudev_api_base_url' => $wpmu_dev->get_api_base_url(),
607 'upgrade_title' => esc_html__( 'UPGRADE TO PRO', 'defender-security' ),
608 'tracking_modal' => $is_tracking ? 'show' : 'hide',
609 'hosted' => $wpmu_dev->is_wpmu_hosting(),
610 'file_upload_nonce' => wp_create_nonce( 'defender_file_upload' ),
611 'wpmudev_hub_link' => 'https://wpmudev.com/hub2/',
612 )
613 );
614
615 wp_localize_script( 'defender', 'defenderGetText', defender_gettext_translations() );
616
617 wp_localize_script(
618 'def-deactivation-survey',
619 'defender',
620 array(
621 'usage_tracking' => wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class )->usage_tracking,
622 'admin_url' => admin_url( 'admin-ajax.php' ),
623 'nonce' => wp_create_nonce( 'defender_deactivation_survey_modal' ),
624 )
625 );
626 }
627
628 /**
629 * Register all core assets.
630 */
631 public function register_assets(): void {
632 $this->register_styles();
633 $this->register_scripts();
634 $this->localize_script();
635
636 do_action( 'defender_enqueue_assets' );
637 }
638
639 /**
640 * Trigger mandatory actions on activation.
641 */
642 private function on_activation(): void {
643 update_site_option( Data_Tracking::TRACKING_SLUG, true );
644
645 add_action(
646 'admin_init',
647 function () {
648 $security_tweaks = wd_di()->get( Security_Tweaks::class );
649 $security_tweaks->get_security_key()->cron_schedule();
650 }
651 );
652 }
653
654 /**
655 * Returns the cron schedules.
656 *
657 * @param array $schedules The existing cron schedules.
658 *
659 * @return array The updated cron schedules.
660 */
661 public function cron_schedules( array $schedules ) {
662 return defender_cron_schedules( $schedules );
663 }
664
665 /**
666 * Serves script translations from the single MO file our in-site translation
667 * tools (e.g. WPMU DEV Hub's translation editor) produce, since those only
668 * generate one MO/PO file per plugin rather than the per-script JSON files
669 * `wp_set_script_translations()` normally expects.
670 *
671 * @param string|false $translations Translations as JSON, false if there are none.
672 * @param string|false $file Path to the translation file to load, false if there isn't one.
673 * @param string $handle Name of the script to register a translation domain to.
674 * @param string $domain The text domain.
675 *
676 * @return string|false
677 */
678 public function provide_script_translations( $translations, $file, $handle, $domain ) {
679 if ( 'defender-security' !== $domain ) {
680 return $translations;
681 }
682
683 static $served = false;
684 static $cached_json = null;
685
686 if ( $served ) {
687 return wp_json_encode(
688 array(
689 'locale_data' => array(
690 'defender-security' => array(
691 '' => array(
692 'domain' => 'defender-security',
693 ),
694 ),
695 ),
696 )
697 );
698 }
699
700 if ( null !== $cached_json ) {
701 return $cached_json;
702 }
703
704 $locale = is_admin() ? get_user_locale() : get_locale();
705 $mo_file = WP_LANG_DIR . "/plugins/{$domain}-{$locale}.mo";
706
707 if ( ! file_exists( $mo_file ) ) {
708 return $translations;
709 }
710
711 $mo = new \MO();
712 if ( ! $mo->import_from_file( $mo_file ) ) {
713 return $translations;
714 }
715
716 $locale_data = array(
717 '' => array(
718 'domain' => $domain,
719 'lang' => $locale,
720 ),
721 );
722
723 if ( ! empty( $mo->headers['Plural-Forms'] ) ) {
724 $locale_data['']['plural-forms'] = $mo->headers['Plural-Forms'];
725 }
726
727 foreach ( $mo->entries as $msgid => $entry ) {
728 $locale_data[ $msgid ] = $entry->translations;
729 }
730
731 $cached_json = wp_json_encode(
732 array(
733 'locale_data' => array(
734 $domain => $locale_data,
735 ),
736 )
737 );
738
739 $served = true;
740
741 return $cached_json;
742 }
743
744 /**
745 * Initialize the modules and register the plugin routes. Also include the admin class, adds WP-CLI commands.
746 *
747 * @return void
748 */
749 public function includes(): void {
750 // Initialize modules.
751 add_action(
752 'after_setup_theme',
753 function () {
754 add_filter( 'cron_schedules', array( $this, 'cron_schedules' ) );
755 if ( method_exists( $this, 'init_modules' ) ) {
756 $this->init_modules();
757 }
758 }
759 );
760 // Register routes.
761 add_action(
762 'init',
763 function () {
764 require_once WP_DEFENDER_DIR . 'src/routes.php';
765 },
766 9
767 );
768 // Serve script translations from the single MO file our in-site translation tools produce.
769 add_filter( 'pre_load_script_translations', array( $this, 'provide_script_translations' ), 10, 4 );
770 // Register the Hub Connector early to handle the auth callback during the admin init hook.
771 add_action( 'plugins_loaded', array( wd_di()->get( Hub_Connector::class ), 'init' ) );
772 // Register the Cross-Sell module.
773 // add_action( 'init', array( wd_di()->get( \WP_Defender\Component\Cross_Sell::class ), 'init' ), 9 );
774 // Include admin class. Don't use is_admin().
775 add_action( 'admin_init', array( ( new Admin() ), 'init' ) );
776 // Initialize deactivation survey.
777 add_action( 'admin_enqueue_scripts', array( ( new Admin() ), 'init_deactivation_survey' ) );
778 // Add WP-CLI commands.
779 if ( defender_is_wp_cli() ) {
780 WP_CLI::add_command( 'defender', Cli::class );
781 }
782 // Rotational logger initialization.
783 add_action( 'init', array( ( new Rotation_Logger() ), 'init' ), 99 );
784 // Handle plugin deactivation.
785 add_action( 'deactivated_plugin', array( ( new HUB() ), 'intercept_deactivate' ) );
786 }
787
788 /**
789 * Sets a one-time redirect flag after plugin activation.
790 *
791 * The actual redirect is executed later during `admin_init` to avoid exiting activation flow prematurely.
792 *
793 * @return void
794 */
795 public function set_activation_redirect_flag(): void {
796 if ( defender_is_wp_cli() ) {
797 return;
798 }
799
800 update_site_option( 'wp_defender_activation_redirect', 1 );
801 }
802
803 /**
804 * Redirects once to the Defender dashboard after activation.
805 *
806 * Safety checks:
807 * - Redirect only when the activation flag exists.
808 * - Skip AJAX, cron and WP-CLI contexts.
809 * - Redirect only for users with proper admin capability.
810 *
811 * @return void
812 */
813 public function maybe_redirect_after_activation(): void {
814 if ( ! (bool) get_site_option( 'wp_defender_activation_redirect' ) ) {
815 return;
816 }
817
818 delete_site_option( 'wp_defender_activation_redirect' );
819
820 if ( wp_doing_ajax() || wp_doing_cron() || defender_is_wp_cli() ) {
821 return;
822 }
823
824 $cap = is_multisite() ? 'manage_network_options' : 'manage_options';
825 if ( ! current_user_can( $cap ) ) {
826 return;
827 }
828
829 $url = add_query_arg(
830 array(
831 'page' => 'wp-defender',
832 ),
833 is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' )
834 );
835
836 wp_safe_redirect( $url );
837 exit;
838 }
839 }
840