PluginProbe
Two Factor Authentication / 1.14.14
Two Factor Authentication v1.14.14
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.14, at simba-tfa/simba-tfa.php

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