PluginProbe
Two Factor Authentication / 1.14.16
Two Factor Authentication v1.14.16
1.12.2 1.13.0 1.14.10 1.14.11 1.14.14 1.14.15 1.14.16 1.14.17 1.14.23 1.14.24 1.14.26 1.14.27 1.14.3 1.14.4 1.14.5 1.14.7 1.14.8 1.15.5 1.16.0 1.2.10 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 All 98 releases
two-factor-authentication / simba-tfa / simba-tfa.php

simba-tfa.php in Two Factor Authentication 1.14.16, at simba-tfa/simba-tfa.php

1,527 lines 51.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('Access denied.');
4
5 class Simba_Two_Factor_Authentication_1 {
6
7 /**
8 * Simba 2FA frontend object
9 *
10 * @var Object
11 */
12 protected $frontend;
13
14 /**
15 * Simba 2FA TOTP object
16 *
17 * @var Object
18 */
19 protected $controllers = array();
20
21 /**
22 * Flag for prevent PHP notices in AJAX
23 *
24 * @var Boolean
25 */
26 private $output_buffering;
27
28 /**
29 * Logged error lines array
30 *
31 * @var Array
32 */
33 private $logged;
34
35 /**
36 * URL slug for the plugin's option page
37 *
38 * @var String
39 */
40 private $user_settings_page_slug;
41
42 /**
43 * Settings page heading for plugin's option page
44 *
45 * @var String
46 */
47 private $settings_page_heading;
48
49 /**
50 * Plugin translate url
51 *
52 * @var string
53 */
54 private $plugin_translate_url;
55
56 /**
57 * URL slug for the site-wide administration options
58 *
59 * @var String
60 */
61 private $site_wide_administration_url;
62
63 /**
64 * URL for the premium version
65 *
66 * @var String
67 */
68 private $premium_version_url;
69
70 /**
71 * URL for the FAQ
72 *
73 * @var String
74 */
75 private $faq_url;
76
77 /**
78 * Authentication slug. Verify that two-factor authentication should not be repeated for the same slug.
79 *
80 * @var String
81 */
82 private $authentication_slug = 'updraft';
83
84 private static $is_authenticated = array();
85
86 /**
87 * Class Constructor, Set basic settings.
88 *
89 * @return Void
90 */
91 public function __construct() {
92
93 $load_providers = apply_filters('simbatfa_load_providers', array('totp'));
94
95 foreach ($load_providers as $provider_id) {
96 $class_name = "Simba_TFA_Provider_$provider_id";
97 if (!class_exists($class_name)) {
98 require_once(__DIR__.'/providers/'.$provider_id.'/loader.php');
99 }
100 $this->controllers[$provider_id] = new $class_name($this);
101 }
102
103 // Process login form AJAX events
104 add_action('wp_ajax_nopriv_simbatfa-init-otp', array($this, 'tfaInitLogin'));
105 add_action('wp_ajax_simbatfa-init-otp', array($this, 'tfaInitLogin'));
106
107 add_action('wp_ajax_simbatfa_shared_ajax', array($this, 'shared_ajax'));
108
109 if (!class_exists('Simba_TFA_Login_Form_Integrations')) require_once($this->includes_dir().'/login-form-integrations.php');
110 new Simba_TFA_Login_Form_Integrations($this);
111
112 if (!class_exists('Simba_TFA_Encryption_Muplugin')) require_once($this->includes_dir().'/tfa-encryption-muplugin.php');
113 $this->tfa_muplugin = new Simba_TFA_Encryption_Muplugin($this);
114
115 // Add TFA column on admin users list
116 add_action('manage_users_columns', array($this, 'manage_users_columns_tfa'));
117 add_action('wpmu_users_columns', array($this, 'manage_users_columns_tfa'));
118 add_action('manage_users_custom_column', array($this, 'manage_users_custom_column_tfa'), 10, 3);
119
120 // CSS for admin users screen
121 add_action('admin_print_styles-users.php', array($this, 'load_users_css'), 10, 0);
122
123 add_action('admin_menu', array($this, 'admin_menu'), 9);
124
125 add_action('admin_init', array($this, 'register_two_factor_auth_settings'));
126 add_action('init', array($this, 'init'));
127
128 if (!defined('TWO_FACTOR_DISABLE') || !TWO_FACTOR_DISABLE) {
129 add_filter('authenticate', array($this, 'tfaVerifyCodeAndUser'), 99999999999, 3);
130 }
131
132 add_action('show_user_profile', array($this, 'show_user_profile'), 1);
133
134 add_filter('pre_update_option', array($this, 'setup_secret_encryption'), 10, 2);
135
136 if (defined('DOING_AJAX') && DOING_AJAX && defined('WP_ADMIN') && WP_ADMIN && !empty($_REQUEST['action']) && 'simbatfa-init-otp' == $_REQUEST['action']) {
137 // Try to prevent PHP notices breaking the AJAX conversation
138 $this->output_buffering = true;
139 $this->logged = array();
140 set_error_handler(array($this, 'get_php_errors'), E_ALL & ~E_STRICT);
141 ob_start();
142 }
143 }
144
145 /**
146 * Runs upon the WP action show_user_profile
147 *
148 * @param WP_User $user - the user that the profile is for
149 */
150 public function show_user_profile($user) {
151 if ($user->ID !== get_current_user_id() || !$this->is_activated_for_user($user->ID)) return;
152 echo '<h2>'.__('Two Factor Authentication', 'two-factor-authentication').'</h2>';
153 $settings_url = admin_url('admin.php').'?page='.$this->get_user_settings_page_slug();
154 printf('<a target="_blank" href="%s">%s</a>', $settings_url, __('Go here for your two factor authentication settings...', 'two-factor-authentication'));
155 }
156
157 /**
158 * This function is called via the filter `pre_update_option` if the option being saved is `tfa_encrypt_secrets` then we will proceed to setup the encryption
159 *
160 * @param mixed $value - the value of the option
161 * @param string $option_name - the option name
162 *
163 * @return mixed - returns 0 on error to prevent the feature from being turned on otherwise returns the value passed in
164 */
165 public function setup_secret_encryption($value, $option_name) {
166 if ('tfa_encrypt_secrets' !== $option_name) return $value;
167
168 if (!$this->tfa_muplugin->muplugin_exists() || !defined('SIMBA_TFA_DB_ENCRYPTION_KEY') || '' === SIMBA_TFA_DB_ENCRYPTION_KEY) {
169 $result = $this->tfa_muplugin->insert_contents();
170
171 if (is_wp_error($result)) {
172 add_settings_error('tfa_encrypt_secrets', $result->get_error_code(), $result->get_error_message());
173 return 0;
174 }
175
176 // We now need to include the file as it won't be loaded until WordPress refreshes but we want to use it now
177 include_once($this->tfa_muplugin->get_file_path());
178 }
179
180 $result = $this->get_controller('totp')->potentially_encrypt_private_keys();
181
182 if (is_wp_error($result)) {
183 add_settings_error('tfa_encrypt_secrets', $result->get_error_code(), $result->get_error_message());
184 return 0;
185 }
186
187 return $value;
188 }
189
190 /**
191 * Runs upon the WP filter admin_menu
192 */
193 public function admin_menu() {
194 $this->get_controller('totp')->potentially_port_private_keys();
195 }
196
197 /**
198 * Give the filesystem path to the plugin's includes directory
199 *
200 * @return String
201 */
202 public function includes_dir() {
203 return __DIR__.'/includes';
204 }
205
206 /**
207 * Give the URL for the plugin's includes directory
208 *
209 * @return String
210 */
211 public function includes_url() {
212 return plugins_url('', __FILE__).'/includes';
213 }
214
215 /**
216 * Set URL slug for the plugin's option page.
217 *
218 * @param String Setting page URL slug.
219 * @return Void
220 */
221 public function set_user_settings_page_slug($user_settings_page_slug) {
222 $this->user_settings_page_slug = $user_settings_page_slug;
223 }
224
225 /**
226 * Get URL slug for the plugin's option page.
227 *
228 * @return String Setting page URL slug.
229 */
230 public function get_user_settings_page_slug() {
231 return $this->user_settings_page_slug;
232 }
233
234 /**
235 * Set settings page heading for plugin's option page
236 *
237 * @param String $settings_page_heading String.
238 *
239 * @return String
240 */
241 public function set_settings_page_heading($settings_page_heading) {
242 $this->settings_page_heading = $settings_page_heading;
243 }
244
245 /**
246 * Get settings page heading for plugin's option page.
247 *
248 * @return String Setting page heading.
249 */
250 public function get_settings_page_heading() {
251 return $this->settings_page_heading;
252 }
253
254 /**
255 * Set plugin translate url
256 *
257 * @param String $plugin_translate_url Plugin translation URL.
258 * @return Void
259 */
260 public function set_plugin_translate_url($plugin_translate_url) {
261 $this->plugin_translate_url = $plugin_translate_url;
262 }
263
264 /**
265 * Get plugin translate url
266 *
267 * @return String Plugin translate URL
268 */
269 public function get_plugin_translate_url() {
270 return $this->plugin_translate_url;
271 }
272
273 /**
274 * Set plugin premium version url
275 *
276 * @param String $premium_version_url Plugin premium version url.
277 * @return Void
278 */
279 public function set_premium_version_url($premium_version_url) {
280 $this->premium_version_url = $premium_version_url;
281 }
282
283 /**
284 * Get plugin premium version URL.
285 *
286 * @return String Plugin premium version URL.
287 */
288 public function get_premium_version_url() {
289 return $this->premium_version_url;
290 }
291
292 /**
293 * Set plugin FAQ URL
294 *
295 * @param String $faq_url Plugin FAQ URL.
296 * @return Void
297 */
298 public function set_faq_url($faq_url) {
299 $this->faq_url = $faq_url;
300 }
301
302 /**
303 * Get plugin FAQ URL.
304 *
305 * @return String Plugin FAQ URL.
306 */
307 public function get_faq_url() {
308 return $this->faq_url;
309 }
310
311 /**
312 * Set plugin site wide administration URL
313 *
314 * @param String $site_wide_administration_url Plugin site wide administration URL.
315 * @return Void
316 */
317 public function set_site_wide_administration_url($site_wide_administration_url) {
318 $this->site_wide_administration_url = $site_wide_administration_url;
319 }
320
321 /**
322 * Get plugin site wide administration URL.
323 *
324 * @return String Plugin site wide administration URL
325 */
326 public function get_site_wide_administration_url() {
327 return $this->site_wide_administration_url;
328 }
329
330 /**
331 * Give the filesystem path to the plugin's templates directory
332 *
333 * @return String
334 */
335 public function templates_dir() {
336 return __DIR__.'/templates';
337 }
338
339 /**
340 * Include the user settings page code
341 */
342 public function show_dashboard_user_settings_page() {
343 $this->include_template('user-settings.php');
344 }
345
346 /**
347 * Enqueue CSS styling on the users page
348 */
349 public function load_users_css() {
350 $css_version = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/users.css');
351 wp_enqueue_style(
352 'tfa-users-css',
353 $this->includes_url().'/users.css',
354 array(),
355 $css_version,
356 'screen'
357 );
358 }
359
360 /**
361 * Add the 2FA label to the users list table header.
362 *
363 * @param Array $columns Table columns.
364 *
365 * @return Array
366 */
367 public function manage_users_columns_tfa($columns = array()) {
368 $columns['tfa-status'] = __('2FA', 'two-factor-authentication');
369 return $columns;
370 }
371
372 /**
373 * Add status into TFA column.
374 *
375 * @param String $value String.
376 * @param String $column_name Column name.
377 * @param Integer $user_id User ID.
378 *
379 * @return String
380 */
381 public function manage_users_custom_column_tfa($value = '', $column_name = '', $user_id = 0) {
382
383 // Only for this column name.
384 if ('tfa-status' === $column_name) {
385
386 if (!$this->is_activated_for_user($user_id)) {
387 $value = '&#8212;';
388 } elseif ($this->is_activated_by_user($user_id)) {
389 // Use value.
390 $value = '<span title="'.__('Enabled', 'two-factor-authentication').'" class="dashicons dashicons-yes"></span>';
391 } else {
392 // No group.
393 $value = '<span title="'.__('Disabled', 'two-factor-authentication').'" class="dashicons dashicons-no"></span>';
394 }
395 }
396
397 return $value;
398 }
399
400 /**
401 * Paint out an admin notice
402 *
403 * @param String $message - the caller should already have taken care of any escaping
404 * @param String $class
405 */
406 public function show_admin_warning($message, $class = 'updated') {
407 echo '<div class="tfamessage '.$class.'">'."<p>$message</p></div>";
408 }
409
410 /**
411 * Returns all two factor authentication setting name => group pairs.
412 *
413 * @return Array
414 */
415 private function get_config_keys() {
416 global $wp_roles;
417
418 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
419
420 $keys = array(
421 'tfa_requireafter' => 'tfa_user_roles_required_group',
422 'tfa_require_enforce_after' => 'tfa_user_roles_required_group',
423 'tfa_if_required_redirect_to' => 'tfa_user_roles_required_group',
424 'tfa_hide_turn_off' => 'tfa_user_roles_required_group',
425 'tfa_trusted_for' => 'tfa_user_roles_trusted_group',
426 'tfa_wc_add_section' => 'simba_tfa_woocommerce_group',
427 'tfa_bot_protection' => 'simba_tfa_woocommerce_group',
428 'tfa_default_hmac' => 'simba_tfa_default_hmac_group',
429 'tfa_encrypt_secrets' => 'simba_tfa_encrypt_secrets_group',
430 'tfa_xmlrpc_on' => 'tfa_xmlrpc_status_group',
431 );
432
433 foreach ($wp_roles->role_names as $id => $name) {
434 $keys['tfa_'.$id] = 'tfa_user_roles_group';
435 $keys['tfa_trusted_'.$id] = 'tfa_user_roles_trusted_group';
436 $keys['tfa_required_'.$id] = 'tfa_user_roles_required_group';
437 }
438
439 if (is_multisite()) {
440 $keys['tfa__super_admin'] = 'tfa_user_roles_group';
441 $keys['tfa_trusted__super_admin'] = 'tfa_user_roles_trusted_group';
442 $keys['tfa_required__super_admin'] = 'tfa_user_roles_required_group';
443 }
444
445 return $keys;
446 }
447
448 /**
449 * Registers all two factor authentication settings. Runs upon the WP action admin_init.
450 */
451 public function register_two_factor_auth_settings() {
452 $config_keys = $this->get_config_keys();
453
454 foreach ($config_keys as $name => $group) {
455 register_setting($group, $name);
456 }
457 }
458
459 /**
460 * Returns all two factor authentication options from the WP database.
461 *
462 * @return Array
463 */
464 public function get_configs() {
465 $config_keys = $this->get_config_keys();
466
467 $configs = array();
468
469 foreach (array_keys($config_keys) as $name) {
470 if (false !== $this->get_option($name)) {
471 $configs[$name] = $this->get_option($name);
472 }
473 }
474
475 return $configs;
476 }
477
478 /**
479 * Sets two factor authentication options from array.
480 *
481 * @param Array $configs
482 *
483 * @return Boolean
484 */
485 public function set_configs($configs) {
486 $result = false;
487
488 foreach ($configs as $key => $value) {
489 $result = $this->update_option($key, $value) ? true : $result;
490 }
491
492 return $result;
493 }
494
495 /**
496 * Deletes all two factor authentication options from the WP database.
497 *
498 * @return Void
499 */
500 public function delete_configs() {
501 $config_keys = $this->get_config_keys();
502
503 foreach (array_keys($config_keys) as $name) {
504 $this->delete_option($name);
505 }
506 }
507
508 /**
509 * See whether TFA is available or not for a particular user - i.e. whether the administrator has permitted it for their user level
510 *
511 * @param Integer $user_id - WordPress user ID
512 *
513 * @return Boolean
514 */
515 public function is_activated_for_user($user_id) {
516
517 if (empty($user_id)) return false;
518
519 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
520 if (is_multisite() && is_super_admin($user_id)) {
521 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
522 $role = '_super_admin';
523 $db_val = $this->get_option('tfa_'.$role);
524 // Defaults to true if no setting has been saved
525 return (false === $db_val || $db_val) ? true : false;
526 }
527
528 $roles = $this->get_user_roles($user_id);
529
530 // N.B. This populates with roles on the current site within a multisite
531 foreach ($roles as $role) {
532 $db_val = $this->get_option('tfa_'.$role);
533 if (false === $db_val || $db_val) return true;
534 }
535
536 return false;
537
538 }
539
540 /**
541 * Get all user roles for a given user (if on multisite, amalgamates all roles from all sites)
542 *
543 * @param Integer $user_id - WordPress user ID
544 *
545 * @return Array
546 */
547 protected function get_user_roles($user_id) {
548
549 // Get roles on the main site
550 $user = new WP_User($user_id);
551 $roles = (array) $user->roles;
552
553 // On multisite, also check roles on non-main sites
554 if (is_multisite()) {
555 global $wpdb, $table_prefix;
556 $roles_db = $wpdb->get_results($wpdb->prepare("SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id=%d AND meta_key LIKE '".esc_sql($table_prefix)."%_capabilities'", $user_id));
557 if (is_array($roles_db)) {
558 foreach ($roles_db as $role_info) {
559 if (empty($role_info->meta_key) || !preg_match('/^'.$table_prefix.'\d+_capabilities$/', $role_info->meta_key) || empty($role_info->meta_value) || !preg_match('/^a:/', $role_info->meta_value)) continue;
560 $site_roles = unserialize($role_info->meta_value);
561 if (!is_array($site_roles)) continue;
562 foreach ($site_roles as $role => $active) {
563 if ($active && !in_array($role, $roles)) $roles[] = $role;
564 }
565 }
566 }
567 }
568
569 return $roles;
570 }
571
572 /**
573 * Check if TFA is required for a specified user
574 *
575 * N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
576 *
577 * @param $user_id Integer - the WP user ID
578 *
579 * @return Boolean
580 */
581 public function is_required_for_user($user_id) {
582 return apply_filters('simba_tfa_required_for_user', $this->user_property_active($user_id, 'required_'), $user_id);
583 }
584
585 /**
586 * See if a particular user property is active
587 *
588 * @param Integer $user_id
589 * @param String $prefix - e.g. "required_", "trusted_"
590 *
591 * @return Boolean
592 */
593 public function user_property_active($user_id, $prefix = 'required_') {
594
595 if (empty($user_id)) return false;
596
597 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
598 if (is_multisite() && is_super_admin($user_id)) {
599 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
600 $role = '_super_admin';
601 $db_val = $this->get_option('tfa_'.$prefix.$role);
602 return $db_val ? true : false;
603 }
604
605 $roles = $this->get_user_roles($user_id);
606
607 foreach ($roles as $role) {
608 $db_val = $this->get_option('tfa_'.$prefix.$role);
609 if ($db_val) return true;
610 }
611
612 return false;
613
614 }
615
616 /**
617 * Whether TFA is activated by a specific user. Note that this doesn't check if TFA is enabled for the user's role; the caller should check that first.
618 *
619 * @param Integer $user_id
620 *
621 * @return Boolean
622 */
623 public function is_activated_by_user($user_id) {
624 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
625 return !empty($enabled);
626 }
627
628 /**
629 * Get a list of trusted devices for the user
630 *
631 * @param Integer|Boolean $user_id - WordPress user ID, or false for the current user
632 *
633 * @return Array
634 */
635 public function user_get_trusted_devices($user_id = false) {
636
637 if (false === $user_id) {
638 global $current_user;
639 $user_id = $current_user->ID;
640 }
641
642 $trusted_devices = get_user_meta($user_id, 'tfa_trusted_devices', true);
643
644 if (!is_array($trusted_devices)) $trusted_devices = array();
645
646 return $trusted_devices;
647 }
648
649 /**
650 * Trust the current device
651 *
652 * @param Integer $user_id - WordPress user ID
653 * @param Integer $trusted_for - time to trust for, in days
654 */
655 public function trust_device($user_id, $trusted_for) {
656
657 $trusted_devices = $this->user_get_trusted_devices($user_id);
658
659 $time_now = time();
660
661 foreach ($trusted_devices as $k => $device) {
662 if (empty($device['until']) || $device['until'] <= $time_now) unset($trusted_devices[$k]);
663 }
664
665 $until = $time_now + $trusted_for * 86400;
666
667 $token = bin2hex($this->random_bytes(40));
668
669 $trusted_devices[] = array(
670 'ip' => $_SERVER['REMOTE_ADDR'],
671 'until' => $until,
672 'user_agent' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : (string) $_SERVER['HTTP_USER_AGENT'],
673 'token' => $token
674 );
675
676 $this->user_set_trusted_devices($user_id, $trusted_devices);
677
678 $this->set_cookie('simbatfa_trust_token', $token, $until);
679 }
680
681 /**
682 * Returns true if running on a PHP version on which mcrypt has been deprecated
683 *
684 * @return Boolean
685 */
686 public function is_mcrypt_deprecated() {
687 return (7 == PHP_MAJOR_VERSION && PHP_MINOR_VERSION >= 1);
688 }
689
690 /**
691 * Return the specified number of bytes
692 *
693 * @param Integer $bytes
694 *
695 * @throws Exception
696 *
697 * @return String
698 */
699 public function random_bytes($bytes) {
700 if (function_exists('random_bytes')) {
701 return random_bytes($bytes);
702 } elseif (function_exists('mcrypt_create_iv')) {
703 return $this->is_mcrypt_deprecated() ? @mcrypt_create_iv($bytes, MCRYPT_RAND) : mcrypt_create_iv($bytes, MCRYPT_RAND);
704 } elseif (function_exists('openssl_random_pseudo_bytes')) {
705 return openssl_random_pseudo_bytes($bytes);
706 }
707 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
708 }
709
710 /**
711 * Set a cookie so that, however we logged in, it can be found
712 *
713 * @param String $name - the cookie name
714 * @param String $value - the cookie value
715 * @param Integer $expires - when the cookie expires, in epoch time. Defaults to 24 hours' time. Values in the past cause cookie deletion.
716 */
717 protected function set_cookie($name, $value, $expires = null) {
718 if (null === $expires) $expires = time() + 86400;
719 $secure = is_ssl();
720 $secure_logged_in_cookie = ($secure && 'https' === parse_url(get_option('home'), PHP_URL_SCHEME));
721 $secure = apply_filters('secure_auth_cookie', $secure, get_current_user_id());
722 $secure_logged_in_cookie = apply_filters('secure_logged_in_cookie', $secure_logged_in_cookie, get_current_user_id(), $secure);
723
724 setcookie($name, $value, $expires, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true);
725 setcookie($name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
726 if (COOKIEPATH != SITECOOKIEPATH) {
727 setcookie($name, $value, $expires, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
728 }
729 }
730
731 /**
732 * Get a list of trusted devices for the user
733 *
734 * @param Integer $user_id - WordPress user ID
735 * @param Array $trusted_devices - the list of devices
736 */
737 public function user_set_trusted_devices($user_id, $trusted_devices) {
738 update_user_meta($user_id, 'tfa_trusted_devices', $trusted_devices);
739 }
740
741 /**
742 * Get the user capability needed for managing TFA users.
743 * You'll want to think carefully about changing this to a non-admin, as it can give the ability to lock admins out (though, if you have FTP/files access, you can always disable TFA or any plugin)
744 *
745 * @return String
746 */
747 public function get_management_capability() {
748 return apply_filters('simba_tfa_management_capability', 'manage_options');
749 }
750
751 /**
752 * Used with set_error_handler()
753 *
754 * @param Integer $errno
755 * @param String $errstr
756 * @param String $errfile
757 * @param Integer $errline
758 *
759 * @return Boolean
760 */
761 public function get_php_errors($errno, $errstr, $errfile, $errline) {
762 if (0 == error_reporting()) return true;
763 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
764 $this->logged[] = $logline;
765 # Don't pass it up the chain (since it's going to be output to the user always)
766 return true;
767 }
768
769 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
770 switch ($errno) {
771 case 1: $e_type = 'E_ERROR'; break;
772 case 2: $e_type = 'E_WARNING'; break;
773 case 4: $e_type = 'E_PARSE'; break;
774 case 8: $e_type = 'E_NOTICE'; break;
775 case 16: $e_type = 'E_CORE_ERROR'; break;
776 case 32: $e_type = 'E_CORE_WARNING'; break;
777 case 64: $e_type = 'E_COMPILE_ERROR'; break;
778 case 128: $e_type = 'E_COMPILE_WARNING'; break;
779 case 256: $e_type = 'E_USER_ERROR'; break;
780 case 512: $e_type = 'E_USER_WARNING'; break;
781 case 1024: $e_type = 'E_USER_NOTICE'; break;
782 case 2048: $e_type = 'E_STRICT'; break;
783 case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break;
784 case 8192: $e_type = 'E_DEPRECATED'; break;
785 case 16384: $e_type = 'E_USER_DEPRECATED'; break;
786 case 30719: $e_type = 'E_ALL'; break;
787 default: $e_type = "E_UNKNOWN ($errno)"; break;
788 }
789
790 if (!is_string($errstr)) $errstr = serialize($errstr);
791
792 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
793
794 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
795
796 }
797
798 /**
799 * Runs upon the WordPress 'init' action
800 */
801 public function init() {
802 if ((!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && is_user_logged_in() && file_exists($this->includes_dir().'/tfa_frontend.php')) {
803 $this->load_frontend();
804 } else {
805 add_shortcode('twofactor_user_settings', array($this, 'shortcode_when_not_logged_in'));
806 }
807 }
808
809 /**
810 * Return the TOTP provider object.
811 *
812 * @param String $controller_id - which controller
813 *
814 * @return Simba_TFA_Provider_totp
815 */
816 public function get_controller($controller_id = 'totp') {
817 return $this->controllers[$controller_id];
818 }
819
820 /**
821 * Return all OTP controllers
822 *
823 * @return Array
824 */
825 public function get_controllers() {
826 return $this->controllers;
827 }
828
829 /**
830 * Deprecated synonym for get_controller('totp')
831 *
832 * @return Simba_TFA_Provider_totp
833 */
834 public function get_totp_controller() {
835 trigger_error("Deprecated: Call get_controller('totp'), not get_totp_controller()", E_USER_WARNING);
836 return $this->get_controller('totp');
837 }
838
839 /**
840 * "Shared" - i.e. could be called from either front-end or back-end
841 */
842 public function shared_ajax() {
843
844 if (empty($_POST['subaction']) || empty($_POST['nonce']) || !is_user_logged_in() || !wp_verify_nonce($_POST['nonce'], 'tfa_shared_nonce')) die('Security check (3).');
845
846 global $current_user;
847
848 $subaction = $_POST['subaction'];
849
850 if ('refreshotp' == $subaction) {
851
852 $code = $this->get_controller('totp')->get_current_code($current_user->ID);
853
854 if (false === $code) die(json_encode(array('code' => '')));
855
856 die(json_encode(array('code' => $code)));
857
858 } elseif ('untrust_device' == $subaction && isset($_POST['device_id'])) {
859 $this->untrust_device(stripslashes($_POST['device_id']));
860 ob_start();
861 $this->include_template('trusted-devices-inner-box.php', array('trusted_devices' => $this->user_get_trusted_devices()));
862 echo json_encode(array('trusted_list' => ob_get_clean()));
863 }
864
865 exit;
866
867 }
868
869 /**
870 * Mark a device as untrusted for the current user
871 *
872 * @param String $device_id
873 */
874 protected function untrust_device($device_id) {
875
876 $trusted_devices = $this->user_get_trusted_devices();
877
878 unset($trusted_devices[$device_id]);
879
880 global $current_user;
881 $current_user_id = $current_user->ID;
882
883 $this->user_set_trusted_devices($current_user_id, $trusted_devices);
884
885 }
886
887 /**
888 * Called upon the AJAX action simbatfa-init-otp . Will die.
889 *
890 * Uses these keys from $_POST: user
891 */
892 public function tfaInitLogin() {
893
894 if (empty($_POST['user'])) die('Security check (2).');
895
896 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
897 $res = array('result' => false, 'user_can_trust' => false);
898 } else {
899
900 if (!function_exists('sanitize_user')) require_once ABSPATH.WPINC.'/formatting.php';
901
902 // WP's password-checking sanitizes the supplied user, so we must do the same to check if TFA is enabled for them
903 $auth_info = array('log' => sanitize_user(stripslashes((string)$_POST['user'])));
904
905 if (!empty($_COOKIE['simbatfa_trust_token'])) $auth_info['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
906
907 $res = $this->pre_auth($auth_info, 'array');
908 }
909
910 $results = array(
911 'jsonstarter' => 'justhere',
912 'status' => $res['result'],
913 );
914
915 if (!empty($res['user_can_trust'])) {
916 $results['user_can_trust'] = 1;
917 if (!empty($res['user_already_trusted'])) $results['user_already_trusted'] = 1;
918 }
919
920
921 if (!empty($this->output_buffering)) {
922 if (!empty($this->logged)) {
923 $results['php_output'] = $this->logged;
924 }
925 restore_error_handler();
926 $buffered = ob_get_clean();
927 if ($buffered) $results['extra_output'] = $buffered;
928 }
929
930 $results = apply_filters('simbatfa_check_tfa_requirements_ajax_response', $results);
931
932 echo json_encode($results);
933
934 exit;
935 }
936
937 /**
938 * Enable or disable TFA for a user
939 *
940 * @param Integer $user_id - the WordPress user ID
941 * @param String $setting - either "true" (to turn on) or "false" (to turn off)
942 */
943 public function change_tfa_enabled_status($user_id, $setting) {
944 $previously_enabled = $this->is_activated_by_user($user_id) ? 1 : 0;
945 $setting = ('true' === $setting) ? 1 : 0;
946 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
947 do_action('simba_tfa_activation_status_saved', $user_id, $setting, $previously_enabled, $this);
948 }
949
950 /**
951 * Here's where the login action happens. Called on the WP 'authenticate' action (which also happens when wp-login.php loads, so parameters need checking).
952 *
953 * @param WP_Error|WP_User $user
954 * @param String $username - this is not necessarily the WP username; it is whatever was typed in the form, so can be an email address
955 * @param String $password
956 *
957 * @return WP_Error|WP_User
958 */
959 public function tfaVerifyCodeAndUser($user, $username, $password) {
960 // When both the AIOWPS and Two Factor Authentication plugins are active, this function is called more than once; that should be short-circuited.
961 if (isset(self::$is_authenticated[$this->authentication_slug]) && self::$is_authenticated[$this->authentication_slug]) {
962 return $user;
963 }
964
965 $original_user = $user;
966 $params = stripslashes_deep($_POST);
967
968 // If (only) the error was a wrong password, but it looks like the user appended a TFA code to their password, then have another go
969 if (is_wp_error($user) && array('incorrect_password') == $user->get_error_codes() && !isset($params['two_factor_code']) && false !== ($from_password = apply_filters('simba_tfa_tfa_from_password', false, $password))) {
970 // This forces a new password authentication below
971 $user = false;
972 }
973
974 if (is_wp_error($user)) {
975 $ret = $user;
976 } else {
977
978 if (is_object($user) && isset($user->ID) && isset($user->user_login)) {
979 $params['log'] = $user->user_login;
980 // Confirm that this is definitely a username regardless of its format
981 $may_be_email = false;
982 } else {
983 $params['log'] = $username;
984 $may_be_email = true;
985 }
986
987 $params['caller'] = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['REQUEST_URI'];
988 if (!empty($_COOKIE['simbatfa_trust_token'])) $params['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
989
990 if (isset($from_password) && false !== $from_password) {
991 // Support login forms that can't be hooked via appending to the password
992 $speculatively_try_appendage = true;
993 $params['two_factor_code'] = $from_password['tfa_code'];
994 }
995
996 $code_ok = $this->authorise_user_from_login($params, $may_be_email);
997
998 if (is_wp_error($code_ok)) {
999 $ret = $code_ok;
1000 } elseif (!$code_ok) {
1001 $encryption_enabled = $this->get_option('tfa_encrypt_secrets');
1002 $additional = ($encryption_enabled && (!defined('SIMBA_TFA_DB_ENCRYPTION_KEY') || '' === SIMBA_TFA_DB_ENCRYPTION_KEY)) ? ' ' . htmlspecialchars(__('The "encrypt secrets" feature is currently enabled, but no encryption key has been found (set via the SIMBA_TFA_DB_ENCRYPTION_KEY constant).', 'two-factor-authentication').' '.__('This indicates that either setup failed, or your WordPress installation has been corrupted.', 'two-factor-authentication')) . ' <a href="' . esc_url($this->get_faq_url()) . '">'. __('Go here for the FAQs, which explain how a website owner can de-activate the plugin without needing to login.', 'two-factor-authentication') .'</a>' : '';
1003 $ret = new WP_Error('authentication_failed', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.apply_filters('simba_tfa_message_code_incorrect', __('The one-time password (TFA code) you entered was incorrect.', 'two-factor-authentication') . $additional));
1004 } elseif ($user) {
1005 $ret = $user;
1006 } else {
1007
1008 if (!empty($speculatively_try_appendage) && true === $code_ok) {
1009 $password = $from_password['password'];
1010 }
1011
1012 $username_is_email = false;
1013
1014 if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) {
1015 global $wpdb;
1016 // This has to match self::authorise_user_from_login()
1017 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username));
1018 if (is_object($response)) $username_is_email = true;
1019 }
1020
1021 $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password);
1022 }
1023
1024 }
1025
1026 $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password);
1027
1028 // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true
1029 if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) {
1030 // Though $_SERVER['SERVER_NAME'] can't always be trusted (if the webserver is misconfigured), anyone using this already has password and TFA clearance.
1031 if (!empty($params['simba_tfa_mark_as_trusted']) && $this->user_can_trust($ret->ID) && (is_ssl() || (!empty($_SERVER['SERVER_NAME']) && ('localhost' == $_SERVER['SERVER_NAME'] ||'127.0.0.1' == $_SERVER['SERVER_NAME'] || preg_match('/\.localdomain$/', $_SERVER['SERVER_NAME']))))) {
1032
1033 $trusted_for = $this->get_option('tfa_trusted_for');
1034 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1035
1036 $this->trust_device($ret->ID, $trusted_for);
1037 }
1038 }
1039
1040 self::$is_authenticated[$this->authentication_slug] = true;
1041
1042 return $ret;
1043 }
1044
1045 // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
1046 public function user_can_trust($user_id) {
1047 // Default is false because this is a new feature and we don't want to surprise existing users by granting broader access than they expected upon an upgrade
1048 return apply_filters('simba_tfa_user_can_trust', false, $user_id);
1049 }
1050
1051 /**
1052 * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices?
1053 *
1054 * @param Array $params - the key used is 'log', indicating the username or email address
1055 * @param String $response_format - 'simple' (historic format) or 'array' (richer info)
1056 *
1057 * @return Boolean
1058 */
1059 public function pre_auth($params, $response_format = 'simple') {
1060 global $wpdb;
1061
1062 $query = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']);
1063 $user = $wpdb->get_row($query);
1064
1065 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1066 // Corner-case: login looks like an email, but is a username rather than email address
1067 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1068 }
1069
1070 $is_activated_for_user = true;
1071 $is_activated_by_user = false;
1072
1073 $result = false;
1074
1075 $totp_controller = $this->get_controller('totp');
1076
1077 if ($user) {
1078 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
1079 $is_activated_for_user = $this->is_activated_for_user($user->ID);
1080 $is_activated_by_user = $this->is_activated_by_user($user->ID);
1081
1082 if ($is_activated_for_user && $is_activated_by_user) {
1083
1084 // No private key yet, generate one. This shouldn't really be possible.
1085 if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID);
1086
1087 $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key);
1088
1089 $result = true;
1090 }
1091 }
1092
1093 if ('array' != $response_format) return $result;
1094
1095 $ret = array('result' => $result);
1096
1097 if ($result) {
1098 $ret['user_can_trust'] = $this->user_can_trust($user->ID);
1099 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) {
1100 $ret['user_already_trusted'] = 1;
1101 }
1102 }
1103
1104 return $ret;
1105 }
1106
1107 /**
1108 * Print the radio buttons for enabling/disabling TFA
1109 *
1110 * @param Integer $user_id - the WordPress user ID
1111 * @param Boolean $long_label - whether to use a long label rather than a short one
1112 * @param String $style - valid values are "show_current" and "require_current"
1113 */
1114 public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') {
1115
1116 if (!$user_id) return;
1117
1118 if ('require_current' != $style) $style = 'show_current';
1119
1120 $is_required = $this->is_required_for_user($user_id);
1121 $is_activated = $this->is_activated_by_user($user_id);
1122
1123 if ($is_required) {
1124 $require_after = absint($this->get_option('tfa_requireafter'));
1125 echo '<p class="tfa_required_warning" style="font-weight:bold; font-style:italic;">'.sprintf(__('N.B. This site is configured to forbid you to log in if you disable two-factor authentication after your account is %d days old', 'two-factor-authentication'), $require_after).'</p>';
1126 }
1127
1128 $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'two-factor-authentication') : __('Enabled', 'two-factor-authentication');
1129
1130 if ('show_current' == $style) {
1131 $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'two-factor-authentication'), $this->get_controller('totp')->current_otp_code($user_id));
1132 } elseif ('require_current' == $style) {
1133 $tfa_enabled_label .= ' '.sprintf(__('(you must enter the current code: %s)', 'two-factor-authentication'), '<input type="text" class="tfa_enable_current" name="tfa_enable_current" size="6" style="height">');
1134 }
1135
1136 $show_disable = ((is_multisite() && is_super_admin()) || (!is_multisite() && current_user_can($this->get_management_capability())) || false == $is_activated || !$is_required || !$this->get_option('tfa_hide_turn_off')) ? true : false;
1137
1138 $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'two-factor-authentication') : __('Disabled', 'two-factor-authentication');
1139
1140 if ('require_current' == $style) echo '<input type="hidden" name="require_current" value="1">'."\n";
1141
1142 echo '<input type="radio" class="tfa_enable_radio" id="tfa_enable_tfa_true" name="tfa_enable_tfa" value="true" '.(true == $is_activated ? 'checked="checked"' : '').'> <label class="tfa_enable_radio_label" for="tfa_enable_tfa_true">'.apply_filters('simbatfa_radiolabel_enabled', $tfa_enabled_label, $long_label).'</label> <br>';
1143
1144 // Show the 'disabled' option if the user is an admin, or if it is currently set, or if TFA is not compulsory, or if the site owner doesn't require it to be hidden
1145 // Note that this just hides the option in the UI. The user could POST to turn off TFA, but, since it's required, they won't be able to log in.
1146 if ($show_disable) {
1147 echo '<input type="radio" class="tfa_enable_radio" id="tfa_enable_tfa_false" name="tfa_enable_tfa" value="false" '.(false == $is_activated ? 'checked="checked"' :'').'> <label class="tfa_enable_radio_label" for="tfa_enable_tfa_false">'.apply_filters('simbatfa_radiolabel_disabled', $tfa_disabled_label, $long_label).'</label> <br>';
1148 }
1149 }
1150
1151 /**
1152 * Retrieve a saved option
1153 *
1154 * @param String $key - option key
1155 *
1156 * @return Mixed
1157 */
1158 public function get_option($key) {
1159 if (!is_multisite()) return get_option($key);
1160 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1161 $get_option_site_id = apply_filters('simba_tfa_get_option_site_id', $main_site_id);
1162 switch_to_blog($get_option_site_id);
1163 $value = get_option($key);
1164 restore_current_blog();
1165 return $value;
1166 }
1167
1168 /**
1169 * Updates an option.
1170 *
1171 * @param String $key - option key
1172 * @param Mixed $value - option value
1173 *
1174 * @return Boolean
1175 */
1176 public function update_option($key, $value) {
1177 if (!is_multisite()) return update_option($key, $value);
1178
1179 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1180 $update_option_site_id = apply_filters('simba_tfa_update_option_site_id', $main_site_id);
1181
1182 switch_to_blog($update_option_site_id);
1183 $result = update_option($key, $value);
1184 restore_current_blog();
1185
1186 return $result;
1187 }
1188
1189 /**
1190 * Deletes an option.
1191 *
1192 * @param String $key - option key
1193 *
1194 * @return Boolean
1195 */
1196 public function delete_option($key) {
1197 if (!is_multisite()) return delete_option($key);
1198
1199 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1200 $delete_option_site_id = apply_filters('simba_tfa_delete_option_site_id', $main_site_id);
1201
1202 switch_to_blog($delete_option_site_id);
1203 $result = delete_option($key);
1204 restore_current_blog();
1205
1206 return $result;
1207 }
1208
1209 /**
1210 * Paint a list of checkboxes, one for each role
1211 *
1212 * @param String $prefix
1213 * @param Integer $default - default value (0 or 1)
1214 */
1215 public function list_user_roles_checkboxes($prefix = '', $default = 1) {
1216 if (is_multisite()) {
1217 // Not a real WP role; needs separate handling
1218 $id = '_super_admin';
1219 $name = __('Multisite Super Admin', 'two-factor-authentication');
1220 $setting = $this->get_option('tfa_'.$prefix.$id);
1221 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1222
1223 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" class="tfa_'.$prefix.'user_roles" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars($name)."</label><br>\n";
1224 }
1225
1226 global $wp_roles;
1227 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
1228
1229 foreach ($wp_roles->role_names as $id => $name) {
1230 $setting = $this->get_option('tfa_'.$prefix.$id);
1231 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1232
1233 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" class="tfa_'.$prefix.'user_roles" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars(translate_user_role($name))."</label><br>\n";
1234 }
1235
1236 }
1237
1238 public function tfa_list_xmlrpc_status_radios() {
1239
1240 $setting = $this->get_option('tfa_xmlrpc_on');
1241 $setting = $setting ? 1 : 0;
1242
1243 $types = array(
1244 '0' => __('Do not require 2FA over XMLRPC (best option if you must use XMLRPC and your client does not support 2FA)', 'two-factor-authentication'),
1245 '1' => __('Do require 2FA over XMLRPC (best option if you do not use XMLRPC or are unsure)', 'two-factor-authentication')
1246 );
1247
1248 foreach($types as $id => $name) {
1249 print '<input type="radio" name="tfa_xmlrpc_on" id="tfa_xmlrpc_on_'.$id.'" value="'.$id.'" '.($setting == $id ? 'checked="checked"' : '').'> <label for="tfa_xmlrpc_on_'.$id.'">'.htmlspecialchars($name)."</label><br>\n";
1250 }
1251 }
1252
1253 protected function is_caller_active() {
1254
1255 if (!defined('XMLRPC_REQUEST') || !XMLRPC_REQUEST) return true;
1256
1257 $saved_data = $this->get_option('tfa_xmlrpc_on');
1258
1259 return $saved_data ? true : false;
1260
1261 }
1262
1263 /**
1264 * @param Array $params
1265 * @param Boolean $may_be_email
1266 *
1267 * @return WP_Error|Boolean|Integer - WP_Error or false means failure; true or 1 means success, but true means the TFA code was validated
1268 */
1269 public function authorise_user_from_login($params, $may_be_email = false) {
1270
1271 $params = apply_filters('simbatfa_auth_user_from_login_params', $params);
1272
1273 global $wpdb;
1274
1275 if (!$this->is_caller_active()) return 1;
1276
1277 $query = ($may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) ? $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']);
1278 $response = $wpdb->get_row($query);
1279
1280 if (!$response && $may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1281 // Corner-case: login looks like an email, but is a username rather than email address
1282 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1283 }
1284
1285 $user_id = is_object($response) ? $response->ID : false;
1286 $user_registered = is_object($response) ? $response->user_registered : false;
1287
1288 $user_code = isset($params['two_factor_code']) ? str_replace(' ', '', trim($params['two_factor_code'])) : '';
1289
1290 // This condition in theory should not be possible
1291 if (!$user_id) return new WP_Error('tfa_user_not_found', apply_filters('simbatfa_tfa_user_not_found', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The indicated user could not be found.', 'two-factor-authentication')));
1292
1293 if (!$this->is_activated_for_user($user_id)) return 1;
1294
1295 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user_id, $params['trust_token'])) {
1296 return 1;
1297 }
1298
1299 if (!$this->is_activated_by_user($user_id)) {
1300
1301 if (!$this->is_required_for_user($user_id)) return 1;
1302
1303 $enforce_require_after_check = true;
1304
1305 $require_enforce_after = $this->get_option('tfa_require_enforce_after');
1306
1307 // Don't enforce if the setting has never been saved
1308 if (is_string($require_enforce_after) && preg_match('#^(\d+)-(\d+)-(\d+)$#', $require_enforce_after, $enforce_matches)) {
1309
1310 // wp_date() is WP 5.3+, but performs translation into the site locale
1311 $current_date = function_exists('wp_date') ? wp_date('Y-m-d') : get_date_from_gmt(gmdate('Y-m-d H:i:s'), 'Y-m-d');
1312
1313 if (preg_match('#^(\d+)-(\d+)-(\d+)$#', $current_date, $current_date_matches)) {
1314 if ($current_date_matches[0] < $enforce_matches[0] || ($current_date_matches[0] == $enforce_matches[0] && ($current_date_matches[1] < $enforce_matches[1] || ($current_date_matches[1] == $enforce_matches[1] && $current_date_matches[2] < $enforce_matches[2])))) {
1315 // Enforcement not yet begun; skip
1316 $enforce_require_after_check = false;
1317 }
1318 }
1319
1320 }
1321
1322 $require_after = absint($this->get_option('tfa_requireafter')) * 86400;
1323
1324 $account_age = time() - strtotime($user_registered);
1325
1326 if ($account_age > $require_after && apply_filters('simbatfa_enforce_require_after_check', $enforce_require_after_check, $user_id, $require_after, $account_age)) {
1327
1328 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The site owner has forbidden you to login without two-factor authentication. Please contact the site owner to re-gain access.', 'two-factor-authentication')));
1329 }
1330
1331 return 1;
1332 }
1333
1334 $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_id;
1335
1336 if ($tfa_creds_user_id != $user_id) {
1337
1338 // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/)
1339 // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA
1340
1341 if (!$this->is_activated_for_user($tfa_creds_user_id) || !$this->is_activated_by_user($tfa_creds_user_id)) {
1342 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login_altuser', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('You are attempting to log in to an account that has two-factor authentication enabled; this requires you to also have two-factor authentication enabled on the account whose credentials you are using.', 'two-factor-authentication')));
1343 }
1344
1345 }
1346
1347 return $this->get_controller('totp')->check_code_for_user($tfa_creds_user_id, $user_code);
1348
1349 }
1350
1351 /**
1352 * Evaluate whether a trust token is valid for a user
1353 *
1354 * @param Integer $user_id - WP user ID
1355 * @param String $trust_token - trust token
1356 *
1357 * @return Boolean
1358 */
1359 protected function user_trust_token_valid($user_id, $trust_token) {
1360
1361 if (!is_string($trust_token) || strlen($trust_token) < 30) return false;
1362
1363 $trusted_devices = $this->user_get_trusted_devices($user_id);
1364
1365 $time_now = time();
1366
1367 foreach ($trusted_devices as $device) {
1368 if (empty($device['until']) || $device['until'] <= $time_now) continue;
1369 if (!empty($device['token']) && $device['token'] === $trust_token) {
1370 return true;
1371 }
1372 }
1373
1374 return false;
1375 }
1376
1377 /**
1378 * This deals with the issue that wp-login.php does not redirect to a canonical URL. As a result, if a website is available under more than one host, then admin_url('admin-ajax.php') might return a different one than the visitor is using, resulting in AJAX failing due to CORS errors.
1379 *
1380 * @return String
1381 */
1382 protected function get_ajax_url() {
1383 $ajax_url = admin_url('admin-ajax.php');
1384 $parsed_url = parse_url($ajax_url);
1385 if (strtolower($parsed_url['host']) !== strtolower($_SERVER['HTTP_HOST']) && !empty($parsed_url['path'])) {
1386 // Mismatch - return the relative URL only
1387 $ajax_url = $parsed_url['path'];
1388 }
1389 return $ajax_url;
1390 }
1391
1392 /**
1393 * Called not only upon the WP action login_enqueue_scripts, but potentially upon the action 'init' and various others from other plugins too. It can handle being called multiple times.
1394 */
1395 public function login_enqueue_scripts() {
1396 if (!$this->should_enqueue_login_scripts()) {
1397 return;
1398 }
1399
1400 if (isset($_GET['action']) && 'logout ' != $_GET['action'] && 'login' != $_GET['action']) return;
1401
1402 static $already_done = false;
1403 if ($already_done) return;
1404 $already_done = true;
1405
1406 // Prevent caching when in debug mode
1407 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/tfa.js');
1408
1409 wp_enqueue_script('tfa-ajax-request', $this->includes_url().'/tfa.js', array('jquery'), $script_ver);
1410
1411 $trusted_for = $this->get_option('tfa_trusted_for');
1412 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1413
1414 $localize = array(
1415 'ajaxurl' => $this->get_ajax_url(),
1416 'click_to_enter_otp' => __("Click to enter One Time Password", 'two-factor-authentication'),
1417 'enter_username_first' => __('You have to enter a username first.', 'two-factor-authentication'),
1418 'otp' => __('One Time Password (i.e. 2FA)', 'two-factor-authentication'),
1419 'otp_login_help' => __('(check your OTP app to get this password)', 'two-factor-authentication'),
1420 'mark_as_trusted' => sprintf(_n('Trust this device (allow login without 2FA for %d day)', 'Trust this device (allow login without TFA for %d days)', $trusted_for, 'two-factor-authentication'), $trusted_for),
1421 'is_trusted' => __('(Trusted device - no OTP code required)', 'two-factor-authentication'),
1422 'nonce' => wp_create_nonce('simba_tfa_loginform_nonce'),
1423 'login_form_selectors' => '',
1424 'login_form_off_selectors' => '',
1425 'error' => __('An error has occurred. Site owners can check the JavaScript console for more details.', 'two-factor-authentication'),
1426 );
1427
1428 // Spinner exists since WC 3.8. Use the proper functions to avoid SSL warnings.
1429 if (file_exists(ABSPATH.'wp-admin/images/spinner-2x.gif')) {
1430 $localize['spinnerimg'] = admin_url('images/spinner-2x.gif');
1431 } elseif (file_exists(ABSPATH.WPINC.'/images/spinner-2x.gif')) {
1432 $localize['spinnerimg'] = includes_url('images/spinner-2x.gif');
1433 }
1434
1435 $localize = apply_filters('simba_tfa_login_enqueue_localize', $localize);
1436
1437 wp_localize_script('tfa-ajax-request', 'simba_tfasettings', $localize);
1438
1439 }
1440
1441 /**
1442 * Check whether TFA login scripts should be enqueued or not.
1443 *
1444 * @return boolean True if the TFA login script should be enqueued, otherwise false.
1445 */
1446 private function should_enqueue_login_scripts() {
1447 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
1448 return apply_filters('simbatfa_enqueue_login_scripts', false);
1449 }
1450
1451 global $wpdb;
1452 $sql = $wpdb->prepare('SELECT COUNT(user_id) FROM ' . $wpdb->usermeta . ' WHERE meta_key = %s AND meta_value = %d LIMIT 1', 'tfa_enable_tfa', 1);
1453 $count_user_id = $wpdb->get_var($sql);
1454
1455 if (is_null($count_user_id)) { // Error in query.
1456 return apply_filters('simbatfa_enqueue_login_scripts', true);
1457 } elseif ($count_user_id > 0) { // A user exists with TFA enabled.
1458 return apply_filters('simbatfa_enqueue_login_scripts', true);
1459 }
1460
1461 // No user exists with TFA enabled.
1462 return apply_filters('simbatfa_enqueue_login_scripts', false);
1463 }
1464
1465
1466 /**
1467 * Return or output view content
1468 *
1469 * @param String $path - path to template, usually relative to templates/ within the plugin directory
1470 * @param Array $extract_these - key/value pairs for substitution into the scope of the template
1471 * @param Boolean $return_instead_of_echo - what to do with the results
1472 *
1473 * @return String|Void
1474 */
1475 public function include_template($path, $extract_these = array(), $return_instead_of_echo = false) {
1476
1477 if ($return_instead_of_echo) ob_start();
1478
1479 $template_file = apply_filters('simatfa_template_file', $this->templates_dir().'/'.$path, $path, $extract_these, $return_instead_of_echo);
1480
1481 do_action('simbatfa_before_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1482
1483 if (!file_exists($template_file)) {
1484 error_log("TFA: template not found: $template_file (from $path)");
1485 echo __('Error:', 'two-factor-authentication').' '.__('Template path not found:', 'two-factor-authentication')." (".htmlspecialchars($path).")";
1486 } else {
1487 extract($extract_these);
1488 // The following are useful variables which can be used in the template.
1489 // They appear as unused, but may be used in the $template_file.
1490 $wpdb = $GLOBALS['wpdb'];// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wpdb might be used in the included template
1491 $simba_tfa = $this;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1492 $totp_controller = $this->get_controller('totp');// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1493 include $template_file;
1494 }
1495
1496 do_action('simbatfa_after_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1497
1498 if ($return_instead_of_echo) return ob_get_clean();
1499 }
1500
1501 /**
1502 * Make sure that self::$frontend is the instance of Simba_TFA_Frontend, and return it
1503 *
1504 * @return Simba_TFA_Frontend
1505 */
1506 public function load_frontend() {
1507 if (!class_exists('Simba_TFA_Frontend')) require_once($this->includes_dir().'/tfa_frontend.php');
1508 if (empty($this->frontend)) $this->frontend = new Simba_TFA_Frontend($this);
1509 return $this->frontend;
1510 }
1511
1512 // __return_empty_string() does not exist until WP 3.7
1513 public function shortcode_when_not_logged_in() {
1514 return '';
1515 }
1516
1517 /**
1518 * Set authentication slug.
1519 *
1520 * @param String $authentication_slug - Authentication slug. Verify that two-factor authentication should not be repeated for the same slug.
1521 */
1522 public function set_authentication_slug($authentication_slug) {
1523 $this->authentication_slug = $authentication_slug;
1524 }
1525
1526 }
1527