PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.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 / component / ip / class-antibot-global-firewall.php

class-antibot-global-firewall.php in Defender Security – Malware Scanner, Login Security & Firewall 5.3.1, at src/component/ip/class-antibot-global-firewall.php

711 lines 19.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling AntiBot Global Firewall.
4 *
5 * @package WP_Defender\Component\IP
6 */
7
8 namespace WP_Defender\Component\IP;
9
10 use Exception;
11 use Generator;
12 use WP_Defender\Behavior\WPMUDEV;
13 use WP_Defender\Component;
14 use WP_Defender\Controller\Firewall;
15 use WP_Defender\Integrations\Antibot_Global_Firewall_Client;
16 use WP_Defender\Model\Antibot_Global_Firewall as Antibot_Global_Firewall_Model;
17 use WP_Defender\Model\Setting\Antibot_Global_Firewall_Setting;
18 use WP_Defender\Traits\Defender_Dashboard_Client;
19 use WP_Error;
20
21 /**
22 * Provides methods for downloading IPs from AntiBot Global Firewall, storing, deleting and checking IPs in DB table.
23 */
24 class Antibot_Global_Firewall extends Component {
25 use Defender_Dashboard_Client;
26
27 public const REASON_SLUG = 'antibot_global_firewall';
28
29 public const LOG_FILE_NAME = 'antibot-global-firewall.log';
30
31 public const DOWNLOAD_SYNC_SCHEDULE = 'twicedaily';
32
33 public const DOWNLOAD_SYNC_NEXT_RUN_OPTION = 'wpdef_antibot_global_firewall_download_sync_next_run';
34
35 public const LAST_SYNC_OPTION = 'wpdef_antibot_global_firewall_last_sync';
36
37 public const NOTICE_SLUG = 'wpdef_show_antibot_global_firewall_notice';
38
39 public const BLOCKLIST_STATS_KEY = 'wpdef_antibot_global_firewall_stats';
40
41 public const IS_SWITCHING_TO_PLUGIN_IN_PROGRESS = 'wpdef_antibot_global_firewall_switching_to_plugin_in_progress';
42
43 /**
44 * The AntiBot Global Firewall model for storing IPs.
45 *
46 * @var Antibot_Global_Firewall_Model
47 */
48 private $model;
49
50 /**
51 * The AntiBot Global Firewall setting model.
52 *
53 * @var Antibot_Global_Firewall_Setting
54 */
55 private $model_setting;
56
57 /**
58 * The WPMUDEV object.
59 *
60 * @var WPMUDEV
61 */
62 private $wpmudev;
63
64 /**
65 * The client object for AntiBot Global Firewall.
66 *
67 * @var Antibot_Global_Firewall_Client
68 */
69 private $antibot_client;
70
71 /**
72 * Lock file name for firewall updates.
73 *
74 * @var string
75 */
76 protected string $lock_filename = 'antibot_global_firewall.lock';
77
78 /**
79 * Initializes the class with the Antibot_Global_Firewall_Model, Antibot_Global_Firewall_Setting, WPMUDEV and Antibot_Global_Firewall_Client instances.
80 *
81 * @param Antibot_Global_Firewall_Model $model The AntiBot Global Firewall model.
82 * @param Antibot_Global_Firewall_Setting $model_setting The AntiBot Global FIrewall setting model.
83 * @param WPMUDEV $wpmudev The WPMUDEV object.
84 * @param Antibot_Global_Firewall_Client $antibot_client The client object for AntiBot Global Firewall API.
85 */
86 public function __construct( Antibot_Global_Firewall_Model $model, Antibot_Global_Firewall_Setting $model_setting, WPMUDEV $wpmudev, Antibot_Global_Firewall_Client $antibot_client ) {
87 $this->model = $model;
88 $this->model_setting = $model_setting;
89 $this->wpmudev = $wpmudev;
90 $this->antibot_client = $antibot_client;
91
92 add_action( 'wpdef_confirm_antibot_toggle_on_hosting', array( $this, 'confirm_toggle_on_hosting' ) );
93 }
94
95 /**
96 * Check if AntiBot Global Firewall feature is enabled.
97 *
98 * @return bool True for enabled or false for disabled.
99 */
100 public function is_enabled(): bool {
101 $is_enabled = $this->model_setting->enabled;
102
103 /**
104 * Filter to enable or disable the AntiBot Global Firewall.
105 *
106 * @param bool $is_enabled True for enabled or false for disabled.
107 */
108 return (bool) apply_filters( 'wpdef_antibot_enabled', $is_enabled );
109 }
110
111 /**
112 * Check if the AntiBot Global Firewall is enabled on WPMU DEV.
113 *
114 * @return bool True if the AntiBot Global Firewall is enabled, false otherwise.
115 */
116 public function hosting_is_enabled(): bool {
117 return true === defender_get_hosting_feature_state( 'antibot' );
118 }
119
120 /**
121 * Check if the AntiBot Global Firewall is enabled based on managed by.
122 *
123 * @return bool True if the AntiBot Global Firewall is enabled, false otherwise.
124 */
125 public function frontend_is_enabled(): bool {
126 return 'plugin' === $this->get_managed_by() ?
127 $this->is_enabled() :
128 $this->hosting_is_enabled();
129 }
130
131 /**
132 * Check if the AntiBot Global Firewall is enabled and the site is connected to the HUB.
133 *
134 * @return bool True if the AntiBot Global Firewall is active, false otherwise.
135 */
136 public function is_active(): bool {
137 return $this->frontend_is_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
138 }
139
140 /**
141 * Check if the AntiBot Global Firewall is active via plugin.
142 *
143 * @since 5.1.1
144 * @return bool True if the AntiBot Global Firewall is active via plugin, false otherwise.
145 */
146 public function is_active_via_plugin(): bool {
147 return 'plugin' === $this->get_managed_by() && $this->is_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
148 }
149
150 /**
151 * Check if the IP is blocked.
152 *
153 * @param string $ip The IP address.
154 *
155 * @return bool True if the IP is blocked, false otherwise.
156 */
157 public function is_ip_blocked( string $ip ): bool {
158 $result = $this->model->get_by_ip( $ip );
159
160 return isset( $result->unlocked ) && 1 !== $result->unlocked;
161 }
162
163 /**
164 * Delete complete blocklist.
165 */
166 public function delete_blocklist() {
167 $this->model->truncate();
168
169 delete_site_option( self::LAST_SYNC_OPTION );
170 }
171
172 /**
173 * Check if the Unlock Me Captcha should be displayed.
174 *
175 * @param array $ips The IP addresses.
176 *
177 * @return bool True if the IP is blocked by AntiBot Global Firewall, false otherwise.
178 */
179 public function is_displayed( array $ips ): bool {
180 if ( ! $this->is_enabled() ) {
181 return false;
182 }
183
184 foreach ( $ips as $ip ) {
185 if ( $this->is_ip_blocked( $ip ) ) {
186 return true;
187 }
188 }
189
190 return false;
191 }
192
193 /**
194 * Get the Unlock Me Captcha button text.
195 *
196 * @return string The button text.
197 */
198 public static function get_button_text(): string {
199 return esc_html__( 'AntiBot Unlock Me', 'defender-security' );
200 }
201
202 /**
203 * Handle the download and store blocklist.
204 *
205 * @since 4.8.0
206 * @return void
207 */
208 public function download_and_store_blocklist(): void {
209 if ( ! $this->is_site_connected_to_hub_via_hcm_or_dash() ) {
210 return;
211 }
212
213 if ( $this->has_lock() ) {
214 $this->log( 'Fallback as already a process is running', Firewall::FIREWALL_LOG );
215 return;
216 }
217
218 $this->create_lock();
219 $file_path = $this->download_blocklist();
220
221 if ( ! empty( $file_path ) ) {
222 $this->store_blocklist( $file_path );
223 }
224 $this->remove_lock();
225 }
226
227 /**
228 * Download blocklist.
229 *
230 * @since 4.8.0
231 * @return string|void
232 */
233 private function download_blocklist() {
234 $response = $this->antibot_client->get_blocklist_download( $this->model_setting->mode );
235
236 if ( is_wp_error( $response ) ) {
237 $this->log( sprintf( 'AntiBot Global Firewall Error: %s', $response->get_error_message() ), Firewall::FIREWALL_LOG );
238 return;
239 } elseif ( isset( $response['status'] ) && 'error' === $response['status'] ) {
240 $this->log( sprintf( 'AntiBot Global Firewall Error: %s', $response['message'] ), Firewall::FIREWALL_LOG );
241 return;
242 } elseif ( empty( $response['data']['download_url'] ) && empty( $response['data']['hashes']['sha256'] ) ) {
243 $this->log( 'AntiBot Global Firewall Error: Download link not found in the response.', Firewall::FIREWALL_LOG );
244 return;
245 }
246
247 $file_url = $response['data']['download_url'];
248
249 if ( ! function_exists( 'download_url' ) ) {
250 require_once ABSPATH . 'wp-admin/includes/file.php';
251 }
252
253 $tmp_file = download_url( $file_url );
254
255 if ( is_wp_error( $tmp_file ) ) {
256 $this->log( 'AntiBot Global Firewall Error: Unable to download file', Firewall::FIREWALL_LOG );
257 return;
258 }
259
260 $expected_file_hash = '';
261 $response = wp_remote_get( $response['data']['hashes']['sha256'] );
262 if ( wp_remote_retrieve_response_code( $response ) === 200 ) {
263 $expected_file_hash = trim( wp_remote_retrieve_body( $response ) );
264 }
265
266 $file_hash = hash_file( 'sha256', $tmp_file );
267 if ( $file_hash !== $expected_file_hash ) {
268 $this->log( 'AntiBot Global Firewall Error: File is not legit. Hash does not match.', Firewall::FIREWALL_LOG );
269 return;
270 }
271
272 $this->log( 'AntiBot Global Firewall: Downloaded ' . $this->model_setting->get_mode_label() . ' blocklist successfully.', Firewall::FIREWALL_LOG );
273
274 return $tmp_file;
275 }
276
277 /**
278 * Store blocklist.
279 *
280 * @param string $file_path File path.
281 *
282 * @since 4.8.0
283 * @return void
284 */
285 private function store_blocklist( string $file_path ): void {
286 try {
287 $generator = $this->read_file_generator( $file_path );
288
289 $this->delete_blocklist();
290 $this->model->bulk_insert( $generator );
291
292 self::set_last_sync();
293
294 $this->log( 'AntiBot Global Firewall: IPs stored in the DB table.', Firewall::FIREWALL_LOG );
295 } catch ( Exception $e ) {
296 $this->log( 'AntiBot Global Firewall Error: ' . $e->getMessage(), Firewall::FIREWALL_LOG );
297 }
298
299 // Remove temporary file.
300 wp_delete_file( $file_path );
301 }
302
303 /**
304 * Read file generator.
305 *
306 * @param string $file_path File path.
307 *
308 * @return Generator
309 * @throws Exception If file not found or could not open the file.
310 */
311 private function read_file_generator( $file_path ) {
312 if ( ! file_exists( $file_path ) ) {
313 throw new Exception( 'File not found.' );
314 }
315
316 global $wp_filesystem;
317 $lines = $wp_filesystem->get_contents_array( $file_path );
318
319 if ( empty( $lines ) || ! is_array( $lines ) ) {
320 throw new Exception( 'Could not retrieve the file contents!' );
321 }
322
323 try {
324 foreach ( $lines as $line ) {
325 yield trim( $line );
326 }
327 } catch ( Exception $exception ) {
328 throw $exception;
329 }
330 }
331
332 /**
333 * Get the blocklisted IP count.
334 *
335 * @return string The blocklisted IP count.
336 */
337 public function get_blocklisted_ip_count(): string {
338 // Check if the feature is enabled.
339 if ( ! $this->frontend_is_enabled() ) {
340 return '0';
341 }
342 // Since from v5.0.2 one method is used for counting.
343 return number_format( $this->get_cached_blocklisted_ips() );
344 }
345
346 /**
347 * Get the last sync timestamp.
348 *
349 * @return false|int The last sync timestamp.
350 */
351 public static function get_last_sync() {
352 return get_site_option( self::LAST_SYNC_OPTION, false );
353 }
354
355 /**
356 * Set the last sync timestamp.
357 */
358 public static function set_last_sync(): void {
359 update_site_option( self::LAST_SYNC_OPTION, time() );
360 }
361
362 /**
363 * Check if the IPs should be downloaded from Antibot service.
364 *
365 * @return bool True if the IPs should be downloaded, false otherwise.
366 */
367 public function maybe_download(): bool {
368 if ( $this->wpmudev->is_wpmu_hosting() && 'plugin' !== $this->get_managed_by() ) {
369 return false;
370 }
371
372 $last_sync = self::get_last_sync();
373
374 if ( false === $last_sync ) {
375 return true;
376 }
377
378 return time() - $last_sync >= 12 * HOUR_IN_SECONDS;
379 }
380
381 /**
382 * Get the default managed by.
383 *
384 * @return string The default managed by.
385 */
386 public function get_default_managed_by(): string {
387 return $this->wpmudev->is_wpmu_hosting() ? 'hosting' : 'plugin';
388 }
389
390 /**
391 * Get the AntiBot managed by.
392 *
393 * @return string The AntiBot managed by.
394 */
395 public function get_managed_by(): string {
396 $managed_by = $this->model_setting->managed_by;
397
398 return '' !== $managed_by ? $managed_by : $this->get_default_managed_by();
399 }
400
401 /**
402 * Get the AntiBot managed by label.
403 *
404 * @return string The AntiBot managed by label.
405 */
406 public function get_managed_by_label(): string {
407 return 'plugin' === $this->get_managed_by() ?
408 esc_html__( 'Defender Plugin', 'defender-security' ) :
409 esc_html__( 'WPMU DEV Hosting', 'defender-security' );
410 }
411
412 /**
413 * Set the AntiBot managed by.
414 *
415 * @param string $managed_by The managed by value.
416 *
417 * @return bool True if the managed by is set, false otherwise.
418 */
419 public function set_managed_by( string $managed_by ): bool {
420 if ( in_array( $managed_by, Antibot_Global_Firewall_Setting::MANAGED_BY_ALLOWED, true ) ) {
421 $this->model_setting->managed_by = $managed_by;
422 $this->model_setting->save();
423
424 return true;
425 }
426
427 return false;
428 }
429
430 /**
431 * Switch the AntiBot managed by.
432 *
433 * @return string|false The managed by value if it's switched, false otherwise.
434 */
435 public function switch_managed_by() {
436 $managed_by = 'plugin' === $this->get_managed_by() ? 'hosting' : 'plugin';
437
438 if ( $this->set_managed_by( $managed_by ) ) {
439 if ( 'plugin' === $managed_by ) {
440 return false === $this->managed_by_plugin_action() ? false : $managed_by;
441 } else {
442 return false === $this->managed_by_hosting_action() ? false : $managed_by;
443 }
444 }
445
446 return false;
447 }
448
449 /**
450 * Action when the AntiBot Global Firewall is enabled/disabled by plugin.
451 *
452 * @param bool $enabled True to enable, false to disable.
453 *
454 * @return bool True if the enabled/disabled action is successful, false otherwise.
455 */
456 public function managed_by_plugin_action( bool $enabled = true ): bool {
457 if ( $enabled ) {
458 set_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS, true, 30 );
459
460 // Disable AntiBot on hosting if it's currently enabled.
461 $this->toggle_on_hosting( false, 0.1 );
462
463 // Schedule a single event to confirm AntiBot is disabled on hosting after 15 seconds.
464 if ( ! wp_next_scheduled( 'wpdef_confirm_antibot_toggle_on_hosting' ) ) {
465 wp_schedule_single_event( time() + 15, 'wpdef_confirm_antibot_toggle_on_hosting', array( false ) );
466 }
467
468 // Enable AntiBot on plugin side.
469 $this->model_setting->enabled = true;
470 $this->model_setting->save();
471
472 // Download IPs.
473 $this->download_and_store_blocklist();
474 } else {
475 // Disable AntiBot on plugin side.
476 $this->model_setting->enabled = false;
477 $this->model_setting->save();
478
479 // Remove IPs.
480 $this->delete_blocklist();
481 }
482
483 return true;
484 }
485
486 /**
487 * Action when the AntiBot Global Firewall is enabled/disabled by hosting.
488 *
489 * @param bool $enabled True to enable, false to disable.
490 * @return bool True if the enabled/disabled action is successful, false otherwise.
491 */
492 public function managed_by_hosting_action( bool $enabled = true ): bool {
493 if ( $enabled ) {
494 wp_clear_scheduled_hook( 'wpdef_confirm_antibot_toggle_on_hosting' );
495 delete_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS );
496
497 // Disable AntiBot on plugin side.
498 $this->model_setting->enabled = false;
499 $this->model_setting->save();
500
501 // Remove IPs from DB table.
502 $this->delete_blocklist();
503
504 // Enable AntiBot on hosting if it's currently disabled.
505 $result = $this->toggle_on_hosting( true );
506 if ( is_wp_error( $result ) ) {
507 return false;
508 }
509 } else {
510 $result = $this->toggle_on_hosting( false );
511 if ( is_wp_error( $result ) ) {
512 return false;
513 }
514 }
515
516 return true;
517 }
518
519 /**
520 * Toggle the AntiBot Global Firewall on hosting.
521 *
522 * @param bool $enable True to enable, false to disable.
523 * @param float $timeout The timeout for the request.
524 *
525 * @return bool|WP_Error True if the AntiBot Global Firewall is toggled, false if not, WP_Error otherwise.
526 */
527 public function toggle_on_hosting( bool $enable, float $timeout = 30 ) {
528 if ( ! $this->wpmudev->is_wpmu_hosting() ) {
529 return false;
530 }
531
532 $hosting_enable = defender_get_hosting_feature_state( 'antibot' );
533
534 if (
535 ( $enable && ! $hosting_enable ) ||
536 ( ! $enable && $hosting_enable )
537 ) {
538 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
539
540 $body = array( 'is_active' => $enable );
541 if ( $enable ) {
542 $body['mode'] = $this->get_hosting_mode();
543 }
544
545 $data = $this->make_wpmu_request(
546 WPMUDEV::API_ANTIBOT_GLOBAL_FIREWALL,
547 $body,
548 array(
549 'method' => 'PUT',
550 'timeout' => $timeout,
551 )
552 );
553
554 if ( $timeout > 1 && is_wp_error( $data ) ) {
555 $this->log( 'AntiBot Global Firewall Error: ' . $data->get_error_message(), Firewall::FIREWALL_LOG );
556 return $data;
557 }
558 }
559
560 return true;
561 }
562
563 /**
564 * Fetches the number of blocklisted IPs from cache or from the Blocklist API.
565 *
566 * If the value is not cached, it will fetch the number of blocklisted IPs from the Blocklist API and cache it.
567 *
568 * @return int The number of blocklisted IPs.
569 */
570 public function get_cached_blocklisted_ips(): int {
571 $mode = $this->frontend_mode();
572 $stats_key = self::BLOCKLIST_STATS_KEY . '_' . $mode;
573 $cached_data = get_site_transient( $stats_key );
574 if ( false !== $cached_data ) {
575 return (int) $cached_data;
576 }
577
578 $blocklist_stats = $this->antibot_client->get_blocklist_stats();
579
580 if ( is_wp_error( $blocklist_stats ) ) {
581 $this->log( 'AntiBot Global Firewall Error: ' . $blocklist_stats->get_error_message(), Firewall::FIREWALL_LOG );
582 return 0;
583 }
584
585 $blocklisted_ips_key = Antibot_Global_Firewall_Setting::MODE_BASIC === $mode ? 'blocked_ips' : 'strict_blocked_ips';
586 if ( empty( $blocklist_stats[ $blocklisted_ips_key ] ) ) {
587 $this->log( 'AntiBot Global Firewall Error: Stats missing for mode: ' . $mode, Firewall::FIREWALL_LOG );
588 return 0;
589 }
590
591 $blocklisted_ips = $blocklist_stats[ $blocklisted_ips_key ];
592 set_site_transient( $stats_key, $blocklisted_ips, 12 * HOUR_IN_SECONDS );
593
594 return $blocklisted_ips;
595 }
596
597 /**
598 * Confirm the toggle on hosting.
599 *
600 * @param bool $enable True to enable, false to disable.
601 *
602 * @return void
603 */
604 public function confirm_toggle_on_hosting( bool $enable ): void {
605 $this->toggle_on_hosting( $enable );
606 delete_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS );
607 }
608
609 /**
610 * Logs IP-related messages if logging is enabled via filter.
611 *
612 * @param string $message The message to be logged.
613 */
614 public function log_ip_message( string $message ): void {
615 /**
616 * Filters whether IP logging is enabled.
617 *
618 * This filter allows developers to enable or disable IP logging globally.
619 * Returning false will prevent the log message from being written.
620 *
621 * @param bool $is_enabled Whether IP logging is enabled. Default true.
622 * @since 5.1.0
623 */
624 $is_logging_enabled = (bool) apply_filters( 'wpdef_antibot_global_firewall_ip_log', true );
625
626 if ( ! $is_logging_enabled ) {
627 return;
628 }
629
630 $this->log( $message, self::LOG_FILE_NAME );
631 }
632
633 /**
634 * Get the AntiBot mode status in DB.
635 *
636 * @return string The AntiBot mode.
637 */
638 public function get_mode(): string {
639 return $this->model_setting->mode;
640 }
641
642 /**
643 * Get the AntiBot mode on WPMU DEV.
644 *
645 * @return string The AntiBot mode label.
646 */
647 public function get_hosting_mode(): string {
648 $mode = defender_get_hosting_feature_state( 'antibot_mode' );
649
650 return '' !== $mode ? $mode : Antibot_Global_Firewall_Setting::MODE_BASIC;
651 }
652
653 /**
654 * Get AntiBot mode based on managed by.
655 *
656 * @return string The AntiBot mode.
657 */
658 public function frontend_mode(): string {
659 return 'plugin' === $this->get_managed_by() ?
660 $this->get_mode() :
661 $this->get_hosting_mode();
662 }
663
664 /**
665 * Switch the AntiBot mode.
666 *
667 * @return string|false|WP_Error The AntiBot mode value if it's switched, false otherwise.
668 */
669 public function switch_mode() {
670 if ( 'plugin' === $this->get_managed_by() ) {
671 $mode = $this->get_mode();
672
673 $this->model_setting->mode = Antibot_Global_Firewall_Setting::MODE_STRICT === $mode
674 ? Antibot_Global_Firewall_Setting::MODE_BASIC
675 : Antibot_Global_Firewall_Setting::MODE_STRICT;
676 $this->model_setting->save();
677
678 $this->download_and_store_blocklist();
679
680 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $mode );
681 } else {
682 if ( ! $this->wpmudev->is_wpmu_hosting() ) {
683 return false;
684 }
685
686 $mode = $this->get_hosting_mode();
687
688 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
689 $data = $this->make_wpmu_request(
690 WPMUDEV::API_ANTIBOT_GLOBAL_FIREWALL,
691 array(
692 'is_active' => $this->hosting_is_enabled(),
693 'mode' => Antibot_Global_Firewall_Setting::MODE_STRICT === $mode
694 ? Antibot_Global_Firewall_Setting::MODE_BASIC
695 : Antibot_Global_Firewall_Setting::MODE_STRICT,
696 ),
697 array( 'method' => 'PUT' )
698 );
699
700 if ( is_wp_error( $data ) ) {
701 $this->log( 'AntiBot Global Firewall Error: ' . $data->get_error_message(), Firewall::FIREWALL_LOG );
702 return $data;
703 }
704
705 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $mode );
706 }
707
708 return $this->frontend_mode();
709 }
710 }
711