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

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