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

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