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

799 lines 25.7 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 * Initialize the common modules of the application.
355 *
356 * @return void
357 */
358 private function init_modules_common(): void {
359 // Init main ORM.
360 Array_Cache::set( 'orm', new Mapper() );
361
362 // New setup wizard is rendered directly inside Dashboard React UI.
363 wd_di()->get( Setup_Wizard::class );
364 wd_di()->get( \WP_Defender\Controller\Activity_Log::class );
365 wd_di()->get( Dashboard::class );
366 wd_di()->get( Scan::class );
367 wd_di()->get( Security_Tweaks::class );
368 wd_di()->get( Audit_Logging::class );
369 wd_di()->get( Firewall_Controller::class );
370 wd_di()->get( Login_Access::class );
371 // Initialize Mask Login globally so runtime protections hook into wp-login/wp-admin requests.
372 wd_di()->get( Mask_Login::class );
373 wd_di()->get( WAF::class );
374 wd_di()->get( Two_Factor::class );
375 wd_di()->get( Mask_Login::class );
376 wd_di()->get( Captcha::class );
377 wd_di()->get( Notification::class );
378 wd_di()->get( Recipients::class );
379 wd_di()->get( Main_Setting::class );
380 wd_di()->get( Blocklist_Monitor::class );
381 wd_di()->get( Password_Protection::class );
382 wd_di()->get( Password_Reset::class );
383 wd_di()->get( Webauthn::class );
384 wd_di()->get( Hub_Connector_Controller::class );
385 wd_di()->get( Strong_Password::class );
386 wd_di()->get( Session_Protection::class );
387 wd_di()->get( Quarantine::class );
388 wd_di()->get( Data_Tracking::class );
389 if ( defender_is_wp_org_version() ) {
390 wd_di()->get( Rate_Controller::class );
391 }
392
393 if ( is_multisite() ) {
394 wd_di()->get( Network_Cron_Manager::class );
395 }
396 }
397
398 /**
399 * Adds a specific class to the body tag if the current page is a Defender page.
400 *
401 * @param string $classes The existing body classes.
402 *
403 * @return string The modified body classes.
404 */
405 public function add_sui_to_body( $classes ) {
406 if ( ! is_defender_page() ) {
407 return $classes;
408 }
409 $classes .= sprintf( ' sui-%s ', DEFENDER_SUI );
410
411 return $classes;
412 }
413
414 /**
415 * Registers the necessary styles for the plugin.
416 *
417 * @return void
418 */
419 private function register_styles(): void {
420 wp_enqueue_style( 'defender-menu', WP_DEFENDER_BASE_URL . 'assets/css/defender-icon.css', array(), DEFENDER_VERSION );
421 wp_add_inline_style(
422 'defender-menu',
423 '#toplevel_page_wp-defender .wd-issue-indicator-sidebar {
424 position: relative;
425 background-color: transparent !important;
426 width: 8px;
427 min-width: unset;
428 padding: 0;
429 margin-left: 4px;
430 }
431 #toplevel_page_wp-defender .wd-issue-indicator-sidebar::after {
432 content: "";
433 position: absolute;
434 width: 8px;
435 height: 8px;
436 top: 50%;
437 left: 50%;
438 transform: translate(-50%, -50%);
439 border-radius: 50%;
440 background: #d63638;
441 }'
442 );
443
444 $css_files = array(
445 'defender' => WP_DEFENDER_BASE_URL . 'assets/css/styles.css',
446 'def-sui' => WP_DEFENDER_BASE_URL . 'assets/css/shared-ui.css',
447 'def-admin' => WP_DEFENDER_BASE_URL . 'assets/css/admin.css',
448 'def-showcase' => WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
449 );
450
451 foreach ( $css_files as $slug => $file ) {
452 wp_register_style( $slug, $file, array(), DEFENDER_VERSION );
453 }
454 }
455
456 /**
457 * Registers the necessary scripts for the plugin.
458 *
459 * @return void
460 */
461 private function register_scripts(): void {
462 $base_url = WP_DEFENDER_BASE_URL;
463 $dependencies = array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' );
464 $js_files = array(
465 'wpmudev-sui' => array(
466 $base_url . 'assets/js/shared-ui.js',
467 ),
468 'defender' => array(
469 $base_url . 'assets/js/scripts.js',
470 ),
471 'def-deactivation-survey' => array(
472 $base_url . 'assets/js/deactivation-survey.js',
473 array( 'clipboard', 'wpmudev-sui' ),
474 ),
475 'def-admin' => array(
476 $base_url . 'assets/js/admin.js',
477 array( 'def-deactivation-survey' ),
478 ),
479 'def-vue' => array(
480 $base_url . 'assets/js/vendor.js',
481 ),
482 'def-manifest' => array(
483 $base_url . 'assets/js/manifest.js',
484 ),
485 'def-core-ui' => array(
486 $base_url . 'assets/js/core-ui.js',
487 array( 'def-vue', 'def-manifest' ),
488 ),
489 // React files.
490 'def-showcase' => array(
491 $base_url . 'assets/js/showcase.js',
492 $dependencies,
493 ),
494 'def-setup-wizard' => array(
495 $base_url . 'assets/js/setup-wizard.js',
496 $dependencies,
497 ),
498 );
499
500 foreach ( $js_files as $slug => $file ) {
501 if ( isset( $file[1] ) ) {
502 // This ensures that when JavaScript file changes,
503 // browsers will load the new version instead of
504 // serving a cached old version.
505 $file_path = str_replace( $base_url, WP_DEFENDER_DIR, $file[0] );
506 $file_version = file_exists( $file_path ) ? filemtime( $file_path ) : DEFENDER_VERSION;
507 wp_register_script( $slug, $file[0], $file[1], $file_version, true );
508 wp_set_script_translations( $slug, 'defender-security' );
509 } else {
510 wp_register_script( $slug, $file[0], array( 'jquery' ), DEFENDER_VERSION, true );
511 }
512 }
513 }
514
515 /**
516 * Localizes the script by adding necessary data to the 'defender' object.
517 *
518 * @return void
519 */
520 private function localize_script(): void {
521 $wpmu_dev = new WPMUDEV();
522 global $wp_defender_central;
523
524 $misc = array();
525 $data_tracking = wd_di()->get( Data_Tracking::class );
526 $is_tracking = $data_tracking->show_tracking_modal();
527 if ( $is_tracking ) {
528 $misc = $data_tracking->get_tracking_modal();
529 }
530 $misc['high_contrast'] = defender_high_contrast();
531 $is_wp_org = defender_is_wp_org_version();
532 if ( $is_wp_org ) {
533 $misc['rating'] = array();
534 $rate_service = Rate_Component::is_achievement_displayed();
535 if ( $rate_service['is_displayed'] ) {
536 $misc['rating'] = wd_di()->get( Rate_Controller::class )->data_frontend();
537 $misc['rating']['text'] = Rate_Component::get_notice_by_slug( $rate_service['slug'] );
538 }
539
540 $misc['rating']['is_displayed'] = $rate_service['is_displayed'];
541 $misc['rating']['type'] = $rate_service['slug'];
542 } else {
543 $misc['rating']['is_displayed'] = false;
544 }
545
546 wp_localize_script(
547 'def-vue',
548 'defender',
549 array(
550 'whitelabel' => defender_white_label_status(),
551 'misc' => $misc,
552 'home_url' => network_home_url(),
553 'site_url' => network_site_url(),
554 'admin_url' => network_admin_url(),
555 'defender_url' => WP_DEFENDER_BASE_URL,
556 // There might be Pro version but without pro features, for example when the membership expires.
557 'is_free' => $wpmu_dev->is_pro() ? 0 : 1,
558 // Strictly Free version.
559 'is_wp_org' => $is_wp_org ? 1 : 0,
560 'is_membership' => true,
561 'is_whitelabel' => $wpmu_dev->is_whitelabel_enabled() ? 'enabled' : 'disabled',
562 'wpmu_dev_url_action' => $wpmu_dev->hide_wpmu_dev_urls() ? 'hide' : 'show',
563 'opcache_save_comments' => $wp_defender_central->is_opcache_save_comments_disabled() ? 'disabled' : 'enabled',
564 'opcache_message' => $wp_defender_central->display_opcache_message(),
565 'wpmudev_url' => WP_DEFENDER_DOCS_LINK,
566 'wpmudev_support_ticket_text' => defender_support_ticket_text(),
567 'wpmudev_api_base_url' => $wpmu_dev->get_api_base_url(),
568 'upgrade_title' => esc_html__( 'UPGRADE TO PRO', 'defender-security' ),
569 'tracking_modal' => $is_tracking ? 'show' : 'hide',
570 'hosted' => $wpmu_dev->is_wpmu_hosting(),
571 'file_upload_nonce' => wp_create_nonce( 'defender_file_upload' ),
572 'wpmudev_hub_link' => 'https://wpmudev.com/hub2/',
573 )
574 );
575
576 wp_localize_script( 'defender', 'defenderGetText', defender_gettext_translations() );
577
578 wp_localize_script(
579 'def-deactivation-survey',
580 'defender',
581 array(
582 'usage_tracking' => wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class )->usage_tracking,
583 'admin_url' => admin_url( 'admin-ajax.php' ),
584 'nonce' => wp_create_nonce( 'defender_deactivation_survey_modal' ),
585 )
586 );
587 }
588
589 /**
590 * Register all core assets.
591 */
592 public function register_assets(): void {
593 $this->register_styles();
594 $this->register_scripts();
595 $this->localize_script();
596
597 do_action( 'defender_enqueue_assets' );
598 }
599
600 /**
601 * Trigger mandatory actions on activation.
602 */
603 private function on_activation(): void {
604 update_site_option( Data_Tracking::TRACKING_SLUG, true );
605
606 add_action(
607 'admin_init',
608 function () {
609 $security_tweaks = wd_di()->get( Security_Tweaks::class );
610 $security_tweaks->get_security_key()->cron_schedule();
611 }
612 );
613 }
614
615 /**
616 * Returns the cron schedules.
617 *
618 * @param array $schedules The existing cron schedules.
619 *
620 * @return array The updated cron schedules.
621 */
622 public function cron_schedules( array $schedules ) {
623 return defender_cron_schedules( $schedules );
624 }
625
626 /**
627 * Serves script translations from the single MO file our in-site translation
628 * tools (e.g. WPMU DEV Hub's translation editor) produce, since those only
629 * generate one MO/PO file per plugin rather than the per-script JSON files
630 * `wp_set_script_translations()` normally expects.
631 *
632 * @param string|false $translations Translations as JSON, false if there are none.
633 * @param string|false $file Path to the translation file to load, false if there isn't one.
634 * @param string $handle Name of the script to register a translation domain to.
635 * @param string $domain The text domain.
636 *
637 * @return string|false
638 */
639 public function provide_script_translations( $translations, $file, $handle, $domain ) {
640 if ( 'defender-security' !== $domain ) {
641 return $translations;
642 }
643
644 static $served = false;
645 static $cached_json = null;
646
647 if ( $served ) {
648 return wp_json_encode(
649 array(
650 'locale_data' => array(
651 'defender-security' => array(
652 '' => array(
653 'domain' => 'defender-security',
654 ),
655 ),
656 ),
657 )
658 );
659 }
660
661 if ( null !== $cached_json ) {
662 return $cached_json;
663 }
664
665 $locale = is_admin() ? get_user_locale() : get_locale();
666 $mo_file = WP_LANG_DIR . "/plugins/{$domain}-{$locale}.mo";
667
668 if ( ! file_exists( $mo_file ) ) {
669 return $translations;
670 }
671
672 $mo = new \MO();
673 if ( ! $mo->import_from_file( $mo_file ) ) {
674 return $translations;
675 }
676
677 $locale_data = array(
678 '' => array(
679 'domain' => $domain,
680 'lang' => $locale,
681 ),
682 );
683
684 if ( isset( $mo->headers['Plural-Forms'] ) && '' !== $mo->headers['Plural-Forms'] ) {
685 $locale_data['']['plural-forms'] = $mo->headers['Plural-Forms'];
686 }
687
688 foreach ( $mo->entries as $msgid => $entry ) {
689 $locale_data[ $msgid ] = $entry->translations;
690 }
691
692 $cached_json = wp_json_encode(
693 array(
694 'locale_data' => array(
695 $domain => $locale_data,
696 ),
697 )
698 );
699
700 $served = true;
701
702 return $cached_json;
703 }
704
705 /**
706 * Initialize the modules and register the plugin routes. Also include the admin class, adds WP-CLI commands.
707 *
708 * @return void
709 */
710 public function includes(): void {
711 // Initialize modules.
712 add_action(
713 'after_setup_theme',
714 function () {
715 add_filter( 'cron_schedules', array( $this, 'cron_schedules' ) );
716 if ( method_exists( $this, 'init_modules' ) ) {
717 $this->init_modules();
718 }
719 }
720 );
721 // Register routes.
722 add_action(
723 'init',
724 function () {
725 require_once WP_DEFENDER_DIR . 'src/routes.php';
726 },
727 9
728 );
729 // Serve script translations from the single MO file our in-site translation tools produce.
730 add_filter( 'pre_load_script_translations', array( $this, 'provide_script_translations' ), 10, 4 );
731 // Register the Hub Connector early to handle the auth callback during the admin init hook.
732 add_action( 'plugins_loaded', array( wd_di()->get( Hub_Connector::class ), 'init' ) );
733 // Include admin class. Don't use is_admin().
734 add_action( 'admin_init', array( ( new Admin() ), 'init' ) );
735 // Initialize deactivation survey.
736 add_action( 'admin_enqueue_scripts', array( ( new Admin() ), 'init_deactivation_survey' ) );
737 // Add WP-CLI commands.
738 if ( defender_is_wp_cli() ) {
739 WP_CLI::add_command( 'defender', Cli::class );
740 }
741 // Rotational logger initialization.
742 add_action( 'init', array( ( new Rotation_Logger() ), 'init' ), 99 );
743 // Handle plugin deactivation.
744 add_action( 'deactivated_plugin', array( ( new HUB() ), 'intercept_deactivate' ) );
745 }
746
747 /**
748 * Sets a one-time redirect flag after plugin activation.
749 *
750 * The actual redirect is executed later during `admin_init` to avoid exiting activation flow prematurely.
751 *
752 * @return void
753 */
754 public function set_activation_redirect_flag(): void {
755 if ( defender_is_wp_cli() ) {
756 return;
757 }
758
759 update_site_option( 'wp_defender_activation_redirect', 1 );
760 }
761
762 /**
763 * Redirects once to the Defender dashboard after activation.
764 *
765 * Safety checks:
766 * - Redirect only when the activation flag exists.
767 * - Skip AJAX, cron and WP-CLI contexts.
768 * - Redirect only for users with proper admin capability.
769 *
770 * @return void
771 */
772 public function maybe_redirect_after_activation(): void {
773 if ( ! (bool) get_site_option( 'wp_defender_activation_redirect' ) ) {
774 return;
775 }
776
777 delete_site_option( 'wp_defender_activation_redirect' );
778
779 if ( wp_doing_ajax() || wp_doing_cron() || defender_is_wp_cli() ) {
780 return;
781 }
782
783 $cap = is_multisite() ? 'manage_network_options' : 'manage_options';
784 if ( ! current_user_can( $cap ) ) {
785 return;
786 }
787
788 $url = add_query_arg(
789 array(
790 'page' => 'wp-defender',
791 ),
792 is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' )
793 );
794
795 wp_safe_redirect( $url );
796 exit;
797 }
798 }
799