PluginProbe
Simple Login Log / trunk
Simple Login Log vtrunk
2.0.2 trunk 0.2 0.3 0.5 0.7 0.8 0.9 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 1.0 1.1.1 1.1.2 1.1.3 2.0.0 2.0.1
simple-login-log / simple-login-log.php

simple-login-log.php in Simple Login Log trunk, at simple-login-log.php

1,551 lines 68.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) exit;
3 /*
4 Plugin Name: Simple Login Log
5 Plugin URI: https://apio.systems
6 Description: This plugin keeps a log of WordPress user logins. Offers user filtering and export features.
7 Version: 2.0.2
8 Author: Joris Le Blansch
9 Author URI: https://apio.systems
10 License: MIT
11 License URI: https://github.com/apio-sys/simple-login-log/blob/main/LICENSE
12 Text Domain: simple-login-log
13 Requires at least: 6.5
14 Requires PHP: 7.4
15 */
16
17 if( !class_exists( 'SimpleLoginLog' ) )
18 {
19
20 class SimpleLoginLog
21 {
22 const VERSION = '2.0.2';
23 private $db_ver = "1.3";
24 public $table = 'simple_login_log';
25 private $log_duration = null; //days
26 private $opt_name = 'simple_login_log';
27 private $opt = false;
28 private $login_success = 1;
29 public $data_labels = array();
30 private $installed_ver = null;
31 private $values;
32 private $log_table = null;
33
34 function __construct()
35 {
36 global $wpdb;
37
38 if ( is_multisite() )
39 {
40 $main_prefix = $wpdb->get_blog_prefix(1);
41 $this->table = $main_prefix . $this->table;
42 }
43 else
44 {
45 $this->table = $wpdb->prefix . $this->table;
46 }
47 $this->opt = get_option($this->opt_name);
48
49 if ( ! is_array( $this->opt ) ) {
50 $this->opt = array();
51 }
52
53 $this->installed_ver = get_option( "sll_db_ver" );
54
55 add_action( 'admin_menu', array($this, 'sll_admin_menu') );
56 add_action('admin_init', array($this, 'settings_api_init') );
57 add_action('admin_head', array($this, 'screen_options') );
58
59 add_action('plugins_loaded', array($this, 'update_db_check') );
60
61 add_action( 'init', array($this, 'load_textdomain') );
62 add_action( 'init', array($this, 'init_login_actions') );
63
64 add_action('admin_init', array($this, 'init_csv_export') );
65 add_action('admin_init', array($this, 'delete_all') );
66
67 add_action( 'admin_enqueue_scripts', array($this, 'admin_enqueue_styles') );
68
69 add_action( 'wp', array($this, 'init_scheduled_events') );
70 add_action('truncate_sll', array($this, 'cron') );
71
72 register_deactivation_hook(__FILE__, array($this, 'deactivation') );
73
74 }
75
76 function load_textdomain()
77 {
78 $this->data_labels = array(
79 'Successful' => __('Successful', 'simple-login-log'),
80 'Failed' => __('Failed', 'simple-login-log'),
81 'Login' => __('Login', 'simple-login-log'),
82 'User Agent' => __('User Agent', 'simple-login-log'),
83 'Login Redirect' => __('Login Redirect', 'simple-login-log'),
84 'id' => __('#', 'simple-login-log'),
85 'uid' => __('User ID', 'simple-login-log'),
86 'user_login' => __('Username', 'simple-login-log'),
87 'user_role' => __('User Role', 'simple-login-log'),
88 'name' => __('Name', 'simple-login-log'),
89 'time' => __('Time', 'simple-login-log'),
90 'ip' => __('IP Address', 'simple-login-log'),
91 'login_result' => __('Login Result', 'simple-login-log'),
92 'data' => __('Data', 'simple-login-log'),
93 );
94 }
95
96 function set($name, $value)
97 {
98 $this->values[$name] = $value;
99 }
100
101 function get($name)
102 {
103 return (isset($this->values[$name])) ? $this->values[$name] : false;
104 }
105
106 function cron()
107 {
108 SimpleLoginLog::truncate_log();
109 }
110
111 function screen_options()
112 {
113 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
114 $page = ( isset($_GET['page']) ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : false;
115 if( 'login_log' != $page )
116 return;
117
118 $current_screen = get_current_screen();
119
120 $per_page_field = 'per_page';
121 $per_page_option = $current_screen->id . '_' . $per_page_field;
122
123 // phpcs:disable WordPress.Security.NonceVerification.Recommended
124 if( isset($_REQUEST['wp_screen_options']) && isset($_REQUEST['wp_screen_options']['value']) )
125 {
126 update_option( $per_page_option, absint( wp_unslash( $_REQUEST['wp_screen_options']['value'] ) ) );
127 }
128 // phpcs:enable WordPress.Security.NonceVerification.Recommended
129
130 $per_page_val = get_option($per_page_option, 20);
131 $args = array('label' => __('Records', 'simple-login-log'), 'default' => $per_page_val );
132
133 add_screen_option($per_page_field, $args);
134 $_per_page = get_option('users_page_login_log_per_page');
135
136 //needs to be initialized early enough to pre-fill screen options section in the upper (hidden) area.
137 $this->log_table = new SLL_List_Table;
138 }
139
140 function init_login_actions()
141 {
142 //condition to check if "log failed attempts" option is selected
143
144 //Action on successful login
145 add_action( 'wp_login', array($this, 'login_success') );
146
147 //Action on failed login
148 if( isset($this->opt['failed_attempts']) ){
149 add_action( 'wp_login_failed', array($this, 'login_failed') );
150 }
151
152 }
153
154 function login_success( $user_login )
155 {
156 $this->login_success = 1;
157 $this->login_action( $user_login );
158 }
159
160 function login_failed( $user_login )
161 {
162 $this->login_success = 0;
163 $this->login_action( $user_login );
164 }
165
166 function init_scheduled_events()
167 {
168
169 $log_duration = get_option('simple_login_log');
170
171 if ( $log_duration && !wp_next_scheduled( 'truncate_sll' ) )
172 {
173 $start = time();
174 wp_schedule_event($start, 'daily', 'truncate_sll');
175 } elseif( !$log_duration || 0 == $log_duration)
176 {
177 $timestamp = wp_next_scheduled( 'truncate_sll' );
178 (!$timestamp) ? false : wp_unschedule_event($timestamp, 'truncate_sll');
179
180 }
181 }
182
183 function deactivation()
184 {
185 wp_clear_scheduled_hook('truncate_sll');
186
187 //clean up old cron jobs that no longer exist
188 wp_clear_scheduled_hook('truncate_log');
189 wp_clear_scheduled_hook('SimpleLoginLog::truncate_log');
190 }
191
192 function truncate_log()
193 {
194 global $wpdb;
195
196 $opt = get_option('simple_login_log');
197 $log_duration = (int)$opt['log_duration'];
198
199 if( 0 < $log_duration ){
200 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
201 $wpdb->query(
202 $wpdb->prepare(
203 'DELETE FROM %i WHERE time < DATE_SUB(CURDATE(), INTERVAL %d DAY)',
204 $this->table,
205 $log_duration
206 )
207 );
208 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
209 }
210
211 }
212
213 function delete_all()
214 {
215 global $wpdb;
216
217 $nonce = isset($_REQUEST['_wpnonce']) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : false;
218
219 if (!wp_verify_nonce($nonce, 'delete_sll'))
220 {
221 return;
222 }
223
224 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
225 $result = $wpdb->query(
226 $wpdb->prepare( 'DELETE FROM %i WHERE 1 = %d', $this->table, 1 )
227 );
228 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
229
230 if ($result)
231 {
232 $this->set('deleted', true);
233 }
234 }
235
236 /**
237 * Runs via plugin activation hook & creates a database
238 */
239 function install()
240 {
241 global $wpdb;
242
243 if( $this->installed_ver != $this->db_ver )
244 {
245 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
246 $table_exists = $wpdb->get_var(
247 $wpdb->prepare( "SHOW TABLES LIKE %s", $this->table )
248 );
249 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
250
251 if( !$table_exists ){
252 // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
253 $sql = "CREATE TABLE `{$this->table}`
254 (
255 id INT( 11 ) NOT NULL AUTO_INCREMENT ,
256 uid INT( 11 ) NOT NULL ,
257 user_login VARCHAR( 60 ) NOT NULL ,
258 user_role VARCHAR( 255 ) NOT NULL ,
259 time DATETIME DEFAULT '0000-00-00 00:00:00' NOT NULL ,
260 ip VARCHAR( 100 ) NOT NULL ,
261 login_result VARCHAR (1) ,
262 data LONGTEXT NOT NULL ,
263 PRIMARY KEY ( id ) ,
264 INDEX ( uid, ip, login_result )
265 );";
266 // phpcs:enable WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
267
268 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
269 dbDelta($sql);
270
271 update_option( "sll_db_ver", $this->db_ver );
272 }
273 }
274
275 }
276
277 /**
278 * Checks if the installed database version is the same as the db version of the current plugin
279 * calls the version specific function if upgrade is required
280 */
281 function update_db_check()
282 {
283 if ( get_site_option( 'sll_db_ver' ) != $this->db_ver )
284 {
285 switch( $this->db_ver )
286 {
287 case "1.1":
288 $this->db_update_1_1();
289 break;
290 case "1.2":
291 $this->db_update_1_2();
292 break;
293 case "1.3":
294 $this->db_update_1_3();
295 break;
296 }
297 }
298 }
299
300 /**
301 * DB version specific updates
302 */
303 function db_update_1_1()
304 {
305 /* this version adds a new field "login_result"
306 * check if this field exists
307 */
308 global $wpdb;
309
310 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
311 $fields = $wpdb->get_row(
312 $wpdb->prepare( 'SELECT * FROM %i LIMIT %d', $this->table, 1 ),
313 'ARRAY_A'
314 );
315 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
316
317 if( !$fields ){
318 $this->install();
319 return;
320 }
321
322 $field_names = array_keys( $fields );
323
324 if( !array_search('login_result', $field_names) )
325 {
326 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
327 $insert = $wpdb->query(
328 $wpdb->prepare(
329 'ALTER TABLE %i ADD COLUMN login_result varchar(1) NOT NULL AFTER ip, ADD INDEX (login_result)',
330 $this->table
331 )
332 );
333 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
334
335 if( false !== $insert )
336 update_option( "sll_db_ver", $this->db_ver );
337
338 }
339
340 }
341
342 function db_update_1_2()
343 {
344 /* this version adds a new field "user_role"
345 * check if this field exists
346 */
347 global $wpdb;
348
349 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
350 $fields = $wpdb->get_row(
351 $wpdb->prepare( 'SELECT * FROM %i LIMIT %d', $this->table, 1 ),
352 'ARRAY_A'
353 );
354 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
355
356 if( !$fields ){
357 $this->install();
358 return;
359 }
360
361 $field_names = array_keys( $fields );
362
363 if( !array_search('user_role', $field_names) )
364 {
365 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
366 $insert = $wpdb->query(
367 $wpdb->prepare(
368 'ALTER TABLE %i ADD COLUMN user_role varchar(30) NOT NULL AFTER user_login',
369 $this->table
370 )
371 );
372 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
373
374 if( false !== $insert )
375 update_option( "sll_db_ver", $this->db_ver );
376
377 }
378 }
379
380 function db_update_1_3()
381 {
382 /**
383 * modifies column data length for user_role
384 */
385 global $wpdb;
386
387 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
388 $fields = $wpdb->get_row(
389 $wpdb->prepare( 'SELECT * FROM %i LIMIT %d', $this->table, 1 ),
390 'ARRAY_A'
391 );
392 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
393
394 if( !$fields ){
395 $this->install();
396 return;
397 }
398
399 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
400 $insert = $wpdb->query(
401 $wpdb->prepare(
402 'ALTER TABLE %i MODIFY user_role varchar(255) NOT NULL',
403 $this->table
404 )
405 );
406 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
407
408 if( false !== $insert )
409 update_option( "sll_db_ver", $this->db_ver );
410
411 }
412
413 //Initializing Settings
414 function settings_api_init()
415 {
416 add_settings_section('simple_login_log', __('Simple Login Log', 'simple-login-log'), array($this, 'sll_settings'), 'general');
417 add_settings_field('field_log_duration', __('Truncate Log Entries', 'simple-login-log'), array($this, 'field_log_duration'), 'general', 'simple_login_log');
418 add_settings_field('field_log_failed_attempts', __('Log Failed Attempts', 'simple-login-log'), array($this, 'field_log_failed_attempts'), 'general', 'simple_login_log');
419 register_setting( 'general', 'simple_login_log', array($this, 'sanitize_settings') );
420
421 }
422
423 function sanitize_settings( $input )
424 {
425 // Get existing settings to preserve values not being updated
426 $existing = get_option( 'simple_login_log', array() );
427
428 // Start with existing settings
429 $sanitized = is_array( $existing ) ? $existing : array();
430
431 // Update with new sanitized values
432 if ( isset( $input['log_duration'] ) ) {
433 $sanitized['log_duration'] = absint( $input['log_duration'] );
434 }
435
436 // Checkboxes don't send anything when unchecked, so we always set this value
437 // If isset, it's checked (true), if not isset, it's unchecked (false)
438 $sanitized['failed_attempts'] = isset( $input['failed_attempts'] ) ? (bool) $input['failed_attempts'] : false;
439
440 return $sanitized;
441 }
442
443 function sll_admin_menu()
444 {
445 add_submenu_page( 'users.php', __('Simple Login Log', 'simple-login-log'), __('Login Log', 'simple-login-log'), 'list_users', 'login_log', array($this, 'log_manager') );
446 }
447
448 function sll_settings()
449 {
450 //content that goes before the fields output
451 }
452
453 function field_log_duration()
454 {
455 $duration = ( isset($this->opt['log_duration']) ) ? $this->opt['log_duration'] : '';
456 ?>
457 <input type="text" value="<?php echo esc_attr($duration); ?>" name="simple_login_log[log_duration]" size="10" class="code" /> <?php echo esc_html__('days and older.', 'simple-login-log'); ?>
458 <p><?php echo esc_html__("Leave empty or enter 0 if you don't want the log to be truncated.", 'simple-login-log'); ?></p>
459 <?php
460
461 //since we're on the General Settings page - update cron schedule if settings has been updated
462 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
463 if( isset($_REQUEST['settings-updated']) ){
464 wp_clear_scheduled_hook('truncate_sll');
465 //$this->init_scheduled_events();
466 }
467 }
468
469 function field_log_failed_attempts()
470 {
471 $failed_attempts = ( isset($this->opt['failed_attempts']) ) ? $this->opt['failed_attempts'] : false;
472 echo '<input type="checkbox" name="simple_login_log[failed_attempts]" value="1" ' . checked( $failed_attempts, 1, false ) . ' /> ' . esc_html__('Logs failed attempts where user name and password are entered. Will not log if at least one of the mentioned fields is empty.', 'simple-login-log');
473 }
474
475 /**
476 * Enqueue admin styles for the login log page.
477 *
478 * @param string $hook_suffix The current admin page hook suffix.
479 */
480 function admin_enqueue_styles( $hook_suffix )
481 {
482 // Only load on the login_log page (under Users menu)
483 if ( 'users_page_login_log' !== $hook_suffix ) {
484 return;
485 }
486
487 wp_register_style( 'simple-login-log-admin', false, array(), self::VERSION );
488 wp_enqueue_style( 'simple-login-log-admin' );
489 wp_add_inline_style( 'simple-login-log-admin', 'table.users { table-layout: auto; }' );
490 }
491
492 //Catch messages on successful login
493 function login_action($user_login)
494 {
495
496 $userdata = get_user_by('login', $user_login);
497
498 $uid = ($userdata && $userdata->ID) ? $userdata->ID : 0;
499
500 $data[$this->data_labels['Login']] = ( 1 == $this->login_success ) ? $this->data_labels['Successful'] : $this->data_labels['Failed'];
501 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
502 if ( isset( $_REQUEST['redirect_to'] ) ) { $data[$this->data_labels['Login Redirect']] = sanitize_text_field( wp_unslash( $_REQUEST['redirect_to'] ) ); }
503 $data[$this->data_labels['User Agent']] = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
504
505 $serialized_data = serialize($data);
506
507 $user_role = '';
508 if( $uid ){
509 $user = new WP_User( $uid );
510 if ( !empty( $user->roles ) && is_array( $user->roles ) ) {
511 $user_role = implode(', ', $user->roles);
512 }
513 }
514
515 $values = array(
516 'uid' => $uid,
517 'user_login' => $user_login,
518 'user_role' => $user_role,
519 'time' => current_time('mysql'),
520 'ip' => isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) : ( isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ),
521 'login_result' => $this->login_success,
522 'data' => $serialized_data,
523 );
524
525 $format = array('%d', '%s', '%s', '%s', '%s', '%s', '%s');
526
527 $this->save_data($values, $format);
528 }
529
530 function save_data($values, $format)
531 {
532 global $wpdb;
533
534 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
535 $wpdb->insert( $this->table, $values, $format );
536 }
537
538 /**
539 * Build filter data for WHERE clause.
540 * Returns sanitized filter values that can be used to build queries.
541 *
542 * @return array{filter: string|false, user_role: string|false, result: string|false, year: int|false, month: int|false}
543 */
544 function get_filter_values()
545 {
546 $filters = array(
547 'filter' => false,
548 'user_role' => false,
549 'result' => false,
550 'year' => false,
551 'month' => false,
552 );
553
554 // phpcs:disable WordPress.Security.NonceVerification.Recommended
555 if ( isset($_GET['filter']) && '' !== $_GET['filter'] ) {
556 $filters['filter'] = sanitize_text_field( wp_unslash( $_GET['filter'] ) );
557 }
558 if ( isset($_GET['user_role']) && '' !== $_GET['user_role'] ) {
559 $filters['user_role'] = sanitize_text_field( wp_unslash( $_GET['user_role'] ) );
560 }
561 if ( isset($_GET['result']) && '' !== $_GET['result'] ) {
562 $filters['result'] = sanitize_text_field( wp_unslash( $_GET['result'] ) );
563 }
564 if ( isset($_GET['datefilter']) && '' !== $_GET['datefilter'] ) {
565 $datefilter = sanitize_text_field( wp_unslash( $_GET['datefilter'] ) );
566 // Validate format: YYYYMM
567 if ( preg_match( '/^(\d{4})(\d{2})$/', $datefilter, $matches ) ) {
568 $filters['year'] = (int) $matches[1];
569 $filters['month'] = (int) $matches[2];
570 }
571 }
572 // phpcs:enable WordPress.Security.NonceVerification.Recommended
573
574 return $filters;
575 }
576
577 /**
578 * Get date filter values only (used for stats).
579 *
580 * @return array{year: int|false, month: int|false}
581 */
582 function get_datefilter_values()
583 {
584 $filters = array(
585 'year' => false,
586 'month' => false,
587 );
588
589 // phpcs:disable WordPress.Security.NonceVerification.Recommended
590 if ( isset($_GET['datefilter']) && '' !== $_GET['datefilter'] ) {
591 $datefilter = sanitize_text_field( wp_unslash( $_GET['datefilter'] ) );
592 // Validate format: YYYYMM
593 if ( preg_match( '/^(\d{4})(\d{2})$/', $datefilter, $matches ) ) {
594 $filters['year'] = (int) $matches[1];
595 $filters['month'] = (int) $matches[2];
596 }
597 }
598 // phpcs:enable WordPress.Security.NonceVerification.Recommended
599
600 return $filters;
601 }
602
603 /**
604 * Check if any filters are active.
605 *
606 * @return bool
607 */
608 function has_active_filters()
609 {
610 $filters = $this->get_filter_values();
611 return ( $filters['filter'] !== false || $filters['user_role'] !== false ||
612 $filters['result'] !== false || $filters['year'] !== false );
613 }
614
615 /**
616 * Get total count of records with current filters applied.
617 *
618 * @return int
619 */
620 function get_total_filtered_count()
621 {
622 global $wpdb;
623
624 $filters = $this->get_filter_values();
625 $table = $this->table;
626
627 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
628 if ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
629 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
630 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
631 $count = $wpdb->get_var(
632 $wpdb->prepare(
633 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
634 $table, $filter_like, $filter_like, $role_like, $filters['result'], $filters['year'], $filters['month'])
635 );
636 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false ) {
637 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
638 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
639 $count = $wpdb->get_var(
640 $wpdb->prepare(
641 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s",
642 $table, $filter_like, $filter_like, $role_like, $filters['result'])
643 );
644 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['year'] !== false ) {
645 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
646 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
647 $count = $wpdb->get_var(
648 $wpdb->prepare(
649 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d",
650 $table, $filter_like, $filter_like, $role_like, $filters['year'], $filters['month'])
651 );
652 } elseif ( $filters['filter'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
653 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
654 $count = $wpdb->get_var(
655 $wpdb->prepare(
656 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
657 $table, $filter_like, $filter_like, $filters['result'], $filters['year'], $filters['month'])
658 );
659 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
660 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
661 $count = $wpdb->get_var(
662 $wpdb->prepare(
663 "SELECT COUNT(*) FROM %i WHERE user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
664 $table, $role_like, $filters['result'], $filters['year'], $filters['month'])
665 );
666 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false ) {
667 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
668 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
669 $count = $wpdb->get_var(
670 $wpdb->prepare(
671 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s",
672 $table, $filter_like, $filter_like, $role_like)
673 );
674 } elseif ( $filters['filter'] !== false && $filters['result'] !== false ) {
675 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
676 $count = $wpdb->get_var(
677 $wpdb->prepare(
678 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s",
679 $table, $filter_like, $filter_like, $filters['result'])
680 );
681 } elseif ( $filters['filter'] !== false && $filters['year'] !== false ) {
682 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
683 $count = $wpdb->get_var(
684 $wpdb->prepare(
685 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND YEAR(time) = %d AND MONTH(time) = %d",
686 $table, $filter_like, $filter_like, $filters['year'], $filters['month'])
687 );
688 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false ) {
689 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
690 $count = $wpdb->get_var(
691 $wpdb->prepare(
692 "SELECT COUNT(*) FROM %i WHERE user_role LIKE %s AND login_result = %s",
693 $table, $role_like, $filters['result'])
694 );
695 } elseif ( $filters['user_role'] !== false && $filters['year'] !== false ) {
696 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
697 $count = $wpdb->get_var(
698 $wpdb->prepare(
699 "SELECT COUNT(*) FROM %i WHERE user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d",
700 $table, $role_like, $filters['year'], $filters['month'])
701 );
702 } elseif ( $filters['result'] !== false && $filters['year'] !== false ) {
703 $count = $wpdb->get_var(
704 $wpdb->prepare(
705 "SELECT COUNT(*) FROM %i WHERE login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
706 $table, $filters['result'], $filters['year'], $filters['month'])
707 );
708 } elseif ( $filters['filter'] !== false ) {
709 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
710 $count = $wpdb->get_var(
711 $wpdb->prepare(
712 "SELECT COUNT(*) FROM %i WHERE (user_login LIKE %s OR ip LIKE %s)",
713 $table, $filter_like, $filter_like)
714 );
715 } elseif ( $filters['user_role'] !== false ) {
716 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
717 $count = $wpdb->get_var(
718 $wpdb->prepare(
719 "SELECT COUNT(*) FROM %i WHERE user_role LIKE %s",
720 $table, $role_like)
721 );
722 } elseif ( $filters['result'] !== false ) {
723 $count = $wpdb->get_var(
724 $wpdb->prepare(
725 "SELECT COUNT(*) FROM %i WHERE login_result = %s",
726 $table, $filters['result'])
727 );
728 } elseif ( $filters['year'] !== false ) {
729 $count = $wpdb->get_var(
730 $wpdb->prepare(
731 "SELECT COUNT(*) FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d",
732 $table, $filters['year'], $filters['month'])
733 );
734 } else {
735 $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) );
736 }
737 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
738
739 return (int) $count;
740 }
741
742 function getLimit()
743 {
744 return ' LIMIT ' . get_option('users_page_login_log_per_page', 20);
745 }
746
747 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
748 function log_get_data($orderby = false, $order = false, $limit = 0, $offset = 0)
749 {
750 global $wpdb;
751
752 // $order and $orderby are validated against strict whitelists
753 $allowed_columns = array( 'uid', 'user_login', 'time', 'ip' );
754 $orderby = in_array( $orderby, $allowed_columns, true ) ? $orderby : 'time';
755 $order = ( 'asc' === strtolower( $order ) ) ? 'ASC' : 'DESC';
756
757 $filters = $this->get_filter_values();
758 $table = $this->table;
759
760 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
761 if ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
762 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
763 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
764 $data = $wpdb->get_results(
765 $wpdb->prepare(
766 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
767 $table, $filter_like, $filter_like, $role_like, $filters['result'], $filters['year'], $filters['month'], $orderby, $limit, $offset),
768 'ARRAY_A'
769 );
770 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false ) {
771 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
772 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
773 $data = $wpdb->get_results(
774 $wpdb->prepare(
775 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
776 $table, $filter_like, $filter_like, $role_like, $filters['result'], $orderby, $limit, $offset),
777 'ARRAY_A'
778 );
779 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['year'] !== false ) {
780 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
781 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
782 $data = $wpdb->get_results(
783 $wpdb->prepare(
784 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
785 $table, $filter_like, $filter_like, $role_like, $filters['year'], $filters['month'], $orderby, $limit, $offset),
786 'ARRAY_A'
787 );
788 } elseif ( $filters['filter'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
789 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
790 $data = $wpdb->get_results(
791 $wpdb->prepare(
792 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
793 $table, $filter_like, $filter_like, $filters['result'], $filters['year'], $filters['month'], $orderby, $limit, $offset),
794 'ARRAY_A'
795 );
796 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
797 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
798 $data = $wpdb->get_results(
799 $wpdb->prepare(
800 "SELECT * FROM %i WHERE user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
801 $table, $role_like, $filters['result'], $filters['year'], $filters['month'], $orderby, $limit, $offset),
802 'ARRAY_A'
803 );
804 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false ) {
805 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
806 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
807 $data = $wpdb->get_results(
808 $wpdb->prepare(
809 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
810 $table, $filter_like, $filter_like, $role_like, $orderby, $limit, $offset),
811 'ARRAY_A'
812 );
813 } elseif ( $filters['filter'] !== false && $filters['result'] !== false ) {
814 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
815 $data = $wpdb->get_results(
816 $wpdb->prepare(
817 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
818 $table, $filter_like, $filter_like, $filters['result'], $orderby, $limit, $offset),
819 'ARRAY_A'
820 );
821 } elseif ( $filters['filter'] !== false && $filters['year'] !== false ) {
822 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
823 $data = $wpdb->get_results(
824 $wpdb->prepare(
825 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
826 $table, $filter_like, $filter_like, $filters['year'], $filters['month'], $orderby, $limit, $offset),
827 'ARRAY_A'
828 );
829 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false ) {
830 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
831 $data = $wpdb->get_results(
832 $wpdb->prepare(
833 "SELECT * FROM %i WHERE user_role LIKE %s AND login_result = %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
834 $table, $role_like, $filters['result'], $orderby, $limit, $offset),
835 'ARRAY_A'
836 );
837 } elseif ( $filters['user_role'] !== false && $filters['year'] !== false ) {
838 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
839 $data = $wpdb->get_results(
840 $wpdb->prepare(
841 "SELECT * FROM %i WHERE user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
842 $table, $role_like, $filters['year'], $filters['month'], $orderby, $limit, $offset),
843 'ARRAY_A'
844 );
845 } elseif ( $filters['result'] !== false && $filters['year'] !== false ) {
846 $data = $wpdb->get_results(
847 $wpdb->prepare(
848 "SELECT * FROM %i WHERE login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
849 $table, $filters['result'], $filters['year'], $filters['month'], $orderby, $limit, $offset),
850 'ARRAY_A'
851 );
852 } elseif ( $filters['filter'] !== false ) {
853 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
854 $data = $wpdb->get_results(
855 $wpdb->prepare(
856 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) ORDER BY %i {$order} LIMIT %d OFFSET %d",
857 $table, $filter_like, $filter_like, $orderby, $limit, $offset),
858 'ARRAY_A'
859 );
860 } elseif ( $filters['user_role'] !== false ) {
861 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
862 $data = $wpdb->get_results(
863 $wpdb->prepare(
864 "SELECT * FROM %i WHERE user_role LIKE %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
865 $table, $role_like, $orderby, $limit, $offset),
866 'ARRAY_A'
867 );
868 } elseif ( $filters['result'] !== false ) {
869 $data = $wpdb->get_results(
870 $wpdb->prepare(
871 "SELECT * FROM %i WHERE login_result = %s ORDER BY %i {$order} LIMIT %d OFFSET %d",
872 $table, $filters['result'], $orderby, $limit, $offset),
873 'ARRAY_A'
874 );
875 } elseif ( $filters['year'] !== false ) {
876 $data = $wpdb->get_results(
877 $wpdb->prepare(
878 "SELECT * FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d ORDER BY %i {$order} LIMIT %d OFFSET %d",
879 $table, $filters['year'], $filters['month'], $orderby, $limit, $offset),
880 'ARRAY_A'
881 );
882 } else {
883 $data = $wpdb->get_results(
884 $wpdb->prepare(
885 "SELECT * FROM %i ORDER BY %i {$order} LIMIT %d OFFSET %d",
886 $table, $orderby, $limit, $offset),
887 'ARRAY_A'
888 );
889 }
890 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
891
892 return $data;
893 }
894
895 function log_manager()
896 {
897
898 $log_table = $this->log_table;
899
900 $log_table->prepare_items();
901
902 echo '<div class="wrap srp">';
903 echo '<h2>' . esc_html__('Login Log', 'simple-login-log') . '</h2>';
904
905 if ($this->get('deleted'))
906 {
907 echo '<div class="updated"><p>All records were deleted.</p></div>';
908 }
909
910 echo '<div class="tablenav top">';
911 echo '<div class="alignleft actions">';
912 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
913 echo $this->date_filter();
914 echo '</div>';
915
916 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
917 $username = ( isset($_GET['filter']) ) ? sanitize_text_field( wp_unslash( $_GET['filter'] ) ) : '';
918 echo '<form method="get" class="alignright">';
919 echo '<p class="search-box">';
920 echo '<input type="hidden" name="page" value="login_log" />';
921 echo '<label>' . esc_html__('Username:', 'simple-login-log') . ' </label><input type="text" name="filter" class="filter-username" value="' . esc_attr($username) . '" /> <input class="button" type="submit" value="' . esc_attr__('Filter User', 'simple-login-log') . '" />';
922 echo '<br />';
923 echo '</p>';
924 echo '</form>';
925 echo '</div>';
926 echo '<div class="tablenav top">';
927
928 if( isset($this->opt['failed_attempts']) ){
929 echo '<div class="alignleft actions">';
930 $log_table->views();
931 echo '</div>';
932 }
933
934 echo '<div class="alignright actions">';
935 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
936 $mode = ( isset($_GET['mode']) ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : '';
937 $log_table->view_switcher($mode);
938 echo '</div>';
939 echo '</div>';
940
941 $log_table->display();
942
943 echo '<form method="get" id="export-login-log">';
944 if ( function_exists('wp_nonce_field') )
945 wp_nonce_field('ssl_export_log');
946
947 echo '<input type="hidden" name="page" value="login_log" />';
948 echo '<input type="hidden" name="download-login-log" value="true" />';
949 echo '<p class="submit">';
950 echo '<input type="submit" name="submit" id="submit" class="button" value="Export Log to CSV">';
951 echo '&nbsp;&nbsp;<a id="delete-all" href="' . esc_url(wp_nonce_url('users.php?page=login_log&action=delete', 'delete_sll')) . '" onclick="return confirm(\'IMPORTANT: All User Log records will be deleted.\')">Delete All</a>';
952 echo '</p>';
953 echo '</form>';
954 //if filtered results - add export filtered results button
955 $where = false;
956 // phpcs:disable WordPress.Security.NonceVerification.Recommended
957 if( isset( $_GET['filter'] ) || isset( $_GET['user_role'] ) || isset( $_GET['datefilter'] ) || isset( $_GET['result'] ) )
958 {
959 $where = array();
960 foreach($_GET as $k => $v)
961 {
962 $where[ sanitize_key( $k ) ] = sanitize_text_field( $v );
963 }
964 // phpcs:enable WordPress.Security.NonceVerification.Recommended
965 echo '<form method="get" id="export-login-log">';
966 if ( function_exists('wp_nonce_field') )
967 wp_nonce_field('ssl_export_log');
968
969 echo '<input type="hidden" name="page" value="login_log" />';
970 echo '<input type="hidden" name="download-login-log" value="true" />';
971 echo '<input type="hidden" name="where" value="' . esc_attr( wp_json_encode( $where ) ) . '" />';
972 submit_button( __('Export Current Results to CSV', 'simple-login-log'), 'secondary' );
973 echo '</form>';
974
975 }
976
977 echo '</div>';
978 }
979
980 function date_filter()
981 {
982 global $wpdb;
983
984 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
985 $results = $wpdb->get_results(
986 $wpdb->prepare(
987 'SELECT DISTINCT YEAR(time) as year, MONTH(time) as month FROM %i ORDER BY YEAR(time), MONTH(time) DESC',
988 $this->table
989 )
990 );
991 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
992
993 if(!$results)
994 return '';
995
996 $option = '';
997 foreach($results as $row)
998 {
999 $timestamp = mktime(0, 0, 0, $row->month, 1, $row->year);
1000 $month = (strlen($row->month) == 1) ? '0' . $row->month : $row->month;
1001 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1002 $datefilter = ( isset($_GET['datefilter']) ) ? sanitize_text_field( wp_unslash( $_GET['datefilter'] ) ) : '';
1003 $option .= '<option value="' . esc_attr($row->year . $month) . '" ' . selected($row->year . $month, $datefilter, false) . '>' . esc_html(date_i18n('F', $timestamp)) . ' ' . esc_html($row->year) . '</option>';
1004 }
1005
1006 $output = '<form method="get">';
1007 $output .= '<input type="hidden" name="page" value="login_log" />';
1008 $output .= '<select name="datefilter"><option value="">' . esc_html__('View All', 'simple-login-log') . '</option>' . $option . '</select>';
1009 $output .= '<input class="button" type="submit" value="' . esc_attr__('Filter', 'simple-login-log') . '" />';
1010 $output .= '</form>';
1011
1012 // Allow form elements for the date filter
1013 $allowed_html = array(
1014 'form' => array('method' => array()),
1015 'input' => array(
1016 'type' => array(),
1017 'name' => array(),
1018 'value' => array(),
1019 'class' => array(),
1020 ),
1021 'select' => array('name' => array()),
1022 'option' => array(
1023 'value' => array(),
1024 'selected' => array(),
1025 ),
1026 );
1027
1028 return wp_kses($output, $allowed_html);
1029 }
1030
1031 function init_csv_export()
1032 {
1033
1034 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1035 $download = (isset($_GET['download-login-log'])) ? sanitize_text_field( wp_unslash( $_GET['download-login-log'] ) ) : false;
1036
1037 if($download)
1038 {
1039 check_admin_referer( 'ssl_export_log' );
1040
1041 $where_json = ( isset($_GET['where']) && '' !== $_GET['where'] ) ? sanitize_text_field( wp_unslash( $_GET['where'] ) ) : false;
1042
1043 if ( $where_json ) {
1044 $where = json_decode( $where_json, true );
1045 if ( is_array( $where ) ) {
1046 foreach( $where as $k => $v ) {
1047 $_GET[ sanitize_key( $k ) ] = sanitize_text_field( $v );
1048 }
1049 }
1050 }
1051
1052 $this->export_to_CSV( $this->get_filter_values() );
1053 }
1054 }
1055
1056 function export_to_CSV( $filters = false ){
1057 global $wpdb;
1058
1059 $table = $this->table;
1060
1061 if ( ! $filters ) {
1062 $filters = array(
1063 'filter' => false,
1064 'user_role' => false,
1065 'result' => false,
1066 'year' => false,
1067 'month' => false,
1068 );
1069 }
1070
1071 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1072 if ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
1073 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1074 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1075 $data = $wpdb->get_results(
1076 $wpdb->prepare(
1077 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
1078 $table, $filter_like, $filter_like, $role_like, $filters['result'], $filters['year'], $filters['month']),
1079 'ARRAY_A'
1080 );
1081 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['result'] !== false ) {
1082 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1083 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1084 $data = $wpdb->get_results(
1085 $wpdb->prepare(
1086 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND login_result = %s",
1087 $table, $filter_like, $filter_like, $role_like, $filters['result']),
1088 'ARRAY_A'
1089 );
1090 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false && $filters['year'] !== false ) {
1091 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1092 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1093 $data = $wpdb->get_results(
1094 $wpdb->prepare(
1095 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d",
1096 $table, $filter_like, $filter_like, $role_like, $filters['year'], $filters['month']),
1097 'ARRAY_A'
1098 );
1099 } elseif ( $filters['filter'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
1100 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1101 $data = $wpdb->get_results(
1102 $wpdb->prepare(
1103 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
1104 $table, $filter_like, $filter_like, $filters['result'], $filters['year'], $filters['month']),
1105 'ARRAY_A'
1106 );
1107 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false && $filters['year'] !== false ) {
1108 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1109 $data = $wpdb->get_results(
1110 $wpdb->prepare(
1111 "SELECT * FROM %i WHERE user_role LIKE %s AND login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
1112 $table, $role_like, $filters['result'], $filters['year'], $filters['month']),
1113 'ARRAY_A'
1114 );
1115 } elseif ( $filters['filter'] !== false && $filters['user_role'] !== false ) {
1116 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1117 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1118 $data = $wpdb->get_results(
1119 $wpdb->prepare(
1120 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND user_role LIKE %s",
1121 $table, $filter_like, $filter_like, $role_like),
1122 'ARRAY_A'
1123 );
1124 } elseif ( $filters['filter'] !== false && $filters['result'] !== false ) {
1125 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1126 $data = $wpdb->get_results(
1127 $wpdb->prepare(
1128 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND login_result = %s",
1129 $table, $filter_like, $filter_like, $filters['result']),
1130 'ARRAY_A'
1131 );
1132 } elseif ( $filters['filter'] !== false && $filters['year'] !== false ) {
1133 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1134 $data = $wpdb->get_results(
1135 $wpdb->prepare(
1136 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s) AND YEAR(time) = %d AND MONTH(time) = %d",
1137 $table, $filter_like, $filter_like, $filters['year'], $filters['month']),
1138 'ARRAY_A'
1139 );
1140 } elseif ( $filters['user_role'] !== false && $filters['result'] !== false ) {
1141 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1142 $data = $wpdb->get_results(
1143 $wpdb->prepare(
1144 "SELECT * FROM %i WHERE user_role LIKE %s AND login_result = %s",
1145 $table, $role_like, $filters['result']),
1146 'ARRAY_A'
1147 );
1148 } elseif ( $filters['user_role'] !== false && $filters['year'] !== false ) {
1149 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1150 $data = $wpdb->get_results(
1151 $wpdb->prepare(
1152 "SELECT * FROM %i WHERE user_role LIKE %s AND YEAR(time) = %d AND MONTH(time) = %d",
1153 $table, $role_like, $filters['year'], $filters['month']),
1154 'ARRAY_A'
1155 );
1156 } elseif ( $filters['result'] !== false && $filters['year'] !== false ) {
1157 $data = $wpdb->get_results(
1158 $wpdb->prepare(
1159 "SELECT * FROM %i WHERE login_result = %s AND YEAR(time) = %d AND MONTH(time) = %d",
1160 $table, $filters['result'], $filters['year'], $filters['month']),
1161 'ARRAY_A'
1162 );
1163 } elseif ( $filters['filter'] !== false ) {
1164 $filter_like = '%' . $wpdb->esc_like( $filters['filter'] ) . '%';
1165 $data = $wpdb->get_results(
1166 $wpdb->prepare(
1167 "SELECT * FROM %i WHERE (user_login LIKE %s OR ip LIKE %s)",
1168 $table, $filter_like, $filter_like),
1169 'ARRAY_A'
1170 );
1171 } elseif ( $filters['user_role'] !== false ) {
1172 $role_like = '%' . $wpdb->esc_like( $filters['user_role'] ) . '%';
1173 $data = $wpdb->get_results(
1174 $wpdb->prepare(
1175 "SELECT * FROM %i WHERE user_role LIKE %s",
1176 $table, $role_like),
1177 'ARRAY_A'
1178 );
1179 } elseif ( $filters['result'] !== false ) {
1180 $data = $wpdb->get_results(
1181 $wpdb->prepare(
1182 "SELECT * FROM %i WHERE login_result = %s",
1183 $table, $filters['result']),
1184 'ARRAY_A'
1185 );
1186 } elseif ( $filters['year'] !== false ) {
1187 $data = $wpdb->get_results(
1188 $wpdb->prepare(
1189 "SELECT * FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d",
1190 $table, $filters['year'], $filters['month']),
1191 'ARRAY_A'
1192 );
1193 } else {
1194 // No filters - get all records
1195 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1196 $data = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i', $table ), 'ARRAY_A' );
1197 }
1198 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1199
1200 if(!$data)
1201 return;
1202
1203 $suffix = wp_date('Ymd_Hi');
1204
1205 header( 'Content-Type: text/csv' );
1206 header( 'Content-Disposition: attachment;filename=simple-login-log_' . $suffix . '.csv');
1207 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
1208 $fp = fopen('php://output', 'w');
1209
1210 $i = 0;
1211 foreach($data as $row){
1212 $tmp = unserialize($row['data']);
1213 if(0 == $i)
1214 {
1215 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv
1216 fputcsv( $fp, array_keys($row), ',', '"', '\\' );
1217 }
1218 $row_data = (!empty($tmp)) ? array_map(function($key, $value) {
1219 return $key.": ".$value." | ";
1220 }, array_keys($tmp), array_values($tmp)) : array();
1221 $row['data'] = implode($row_data);
1222 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv
1223 fputcsv($fp, $row, ',', '"', '\\');
1224 $i++;
1225 }
1226
1227 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
1228 fclose($fp);
1229 die();
1230 }
1231
1232 }
1233
1234 }
1235
1236 if( class_exists( 'SimpleLoginLog' ) )
1237 {
1238 $sll = new SimpleLoginLog;
1239 //Register for activation
1240 register_activation_hook( __FILE__, array(&$sll, 'install') );
1241
1242 }
1243
1244 if(!class_exists('WP_List_Table'))
1245 {
1246 require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
1247 }
1248
1249 class SLL_List_Table extends WP_List_Table
1250 {
1251 private $sllData;
1252 protected $data_labels;
1253
1254 function __construct()
1255 {
1256 global $sll, $_wp_column_headers;
1257
1258 //Set parent defaults
1259 parent::__construct( array(
1260 'singular' => 'user', //singular name of the listed records
1261 'plural' => 'users', //plural name of the listed records
1262 'ajax' => false //does this table support ajax?
1263 ) );
1264
1265 $this->data_labels = $sll->data_labels;
1266 }
1267
1268 function set($name, $value)
1269 {
1270 $this->sllData[$name] = $value;
1271 }
1272
1273 function get($name)
1274 {
1275 return (isset($this->sllData[$name])) ? $this->sllData[$name] : false;
1276 }
1277
1278 function column_default($item, $column_name)
1279 {
1280 $item = apply_filters('sll-output-data', $item);
1281
1282 //unset existing filter and pagination
1283 $current_url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
1284 $args = wp_parse_args( wp_parse_url( $current_url, PHP_URL_QUERY ) );
1285 unset($args['filter']);
1286 unset($args['paged']);
1287
1288 switch($column_name){
1289 case 'id':
1290 case 'uid':
1291 case 'time':
1292 case 'ip':
1293 return esc_html($item[$column_name]);
1294 case 'user_login':
1295 $url = esc_url(add_query_arg( array('filter' => $item[$column_name]), menu_page_url('login_log', false) ));
1296 $allowed_html = array(
1297 'a' => array(
1298 'href' => array(),
1299 'title' => array(),
1300 ),
1301 );
1302 return wp_kses("<a href='" . $url . "' title='" . esc_attr__('Filter log by this name', 'simple-login-log') . "'>" . esc_html($item[$column_name]) . "</a>", $allowed_html);
1303 case 'name':
1304 $user_info = get_userdata($item['uid']);
1305 return ( is_object($user_info) ) ? esc_html($user_info->first_name) . " " . esc_html($user_info->last_name) : '';
1306 case 'login_result':
1307 if ( '' == $item[$column_name]) return '';
1308 $allowed_html = array('div' => array('class' => array()));
1309 return ( '1' == $item[$column_name] ) ? esc_html($this->data_labels['Successful']) : wp_kses('<div class="login-failed">' . esc_html($this->data_labels['Failed']) . '</div>', $allowed_html);
1310 case 'user_role':
1311 if( !$item['uid'] )
1312 return;
1313
1314 global $wp_roles;
1315
1316 $user = new WP_User( $item['uid'] );
1317 if ( !empty( $user->roles ) && is_array( $user->roles ) )
1318 {
1319 $roles = array();
1320 foreach($user->roles as $role)
1321 {
1322 $roleName = isset($wp_roles->roles[$role]['name']) ? $wp_roles->roles[$role]['name'] : $role;
1323 $url = esc_url(add_query_arg( array('user_role' => $role), menu_page_url('login_log', false) ));
1324 $roles[] = "<a href='" . $url . "' title='" . esc_attr__('Filter log by User Role', 'simple-login-log') . "'>" . esc_html($roleName) . "</a>";
1325 }
1326 // Allow anchor tags with href and title attributes
1327 $allowed_html = array(
1328 'a' => array(
1329 'href' => array(),
1330 'title' => array(),
1331 ),
1332 );
1333 return wp_kses(implode(', ', $roles), $allowed_html);
1334 }
1335 break;
1336 case 'data':
1337 $data = unserialize($item[$column_name]);
1338 if(is_array($data))
1339 {
1340 $output = '';
1341 foreach($data as $k => $v)
1342 {
1343 $output .= esc_html($k) .': '. esc_html($v) .'<br />';
1344 }
1345
1346 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1347 $mode_value = isset($_GET['mode']) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : '';
1348 $output = ( 'excerpt' === $mode_value ) ? $output : substr($output, 0, 50) . '...';
1349
1350 // Allow only br and div tags with specific class for the output
1351 $allowed_html = array(
1352 'br' => array(),
1353 'div' => array('class' => array()),
1354 );
1355
1356 if( isset($data[$this->data_labels['Login']]) && $data[$this->data_labels['Login']] == $this->data_labels['Failed'] ){
1357 return wp_kses('<div class="login-failed">' . $output . '</div>', $allowed_html);
1358 }
1359 return wp_kses($output, $allowed_html);
1360 }
1361 break;
1362 default:
1363 return esc_html($item[$column_name]);
1364 }
1365 }
1366
1367 function get_columns()
1368 {
1369 global $status;
1370 $columns = array(
1371 'id' => __('#', 'simple-login-log'),
1372 'uid' => __('User ID', 'simple-login-log'),
1373 'user_login' => __('Username', 'simple-login-log'),
1374 'user_role' => __('User Role', 'simple-login-log'),
1375 'name' => __('Name', 'simple-login-log'),
1376 'time' => __('Time', 'simple-login-log'),
1377 'ip' => __('IP Address', 'simple-login-log'),
1378 'login_result' => __('Login Result', 'simple-login-log'),
1379 'data' => __('Data', 'simple-login-log'),
1380 );
1381 return $columns;
1382 }
1383
1384 function get_sortable_columns()
1385 {
1386 $sortable_columns = array(
1387 //'id' => array('id',true), //doesn't sort correctly
1388 'uid' => array('uid',false),
1389 'user_login' => array('user_login', false),
1390 'time' => array('time',true),
1391 'ip' => array('ip', false),
1392 );
1393 return $sortable_columns;
1394 }
1395
1396 // Read-only function that checks URL parameters for display state
1397 // phpcs:disable WordPress.Security.NonceVerification.Recommended
1398 function get_views()
1399 {
1400 if( !isset($_GET['result']) ){
1401 $all = 'class="current"';
1402 $success = '';
1403 $failed = '';
1404 }else{
1405 $all = '';
1406 $result_value = isset($_GET['result']) ? sanitize_text_field( wp_unslash( $_GET['result'] ) ) : '';
1407 $success = ( '1' === $result_value ) ? 'class="current"' : '';
1408 $failed = ( '0' === $result_value ) ? 'class="current"' : '';
1409 }
1410
1411 $date_label = '';
1412 if( isset($_GET['datefilter']) && !empty($_GET['datefilter']) ){
1413 $datefilter_value = sanitize_text_field( wp_unslash( $_GET['datefilter'] ) );
1414 $year = substr($datefilter_value, 0, 4);
1415 $month = substr($datefilter_value, -2);
1416 $timestamp = mktime(0, 0, 0, $month, 1, $year);
1417 $date_label = esc_html(date_i18n('F', $timestamp)) . ' ' . esc_html($year) . ' ';
1418 }
1419
1420 $current_url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
1421 $args = wp_parse_args( wp_parse_url( $current_url, PHP_URL_QUERY ) );
1422 $param = array();
1423 if( isset($args['mode']) )
1424 $param['mode'] = $args['mode'];
1425
1426 if( isset($args['datefilter']) )
1427 $param['datefilter'] = $args['datefilter'];
1428
1429 $menu_page_url = menu_page_url('login_log', false);
1430 ( !empty($param) ) ? $url = add_query_arg( $param, $menu_page_url) : $url = $menu_page_url;
1431
1432 $views = array(
1433 'all' => $date_label . esc_html__('Login Results', 'simple-login-log') . ': <a ' . $all . ' href="' . esc_url($url) . '">' . esc_html__('All', 'simple-login-log') . '</a>' . '(' . absint($this->get('allTotal')) . ')',
1434 'success' => '<a ' . $success . ' href="' . esc_url($url) . '&result=1">' . esc_html__('Successful', 'simple-login-log') . '</a> (' . absint($this->get('successTotal')) . ')',
1435 'failed' => '<a ' . $failed . ' href="' . esc_url($url) . '&result=0">' . esc_html__('Failed', 'simple-login-log') . '</a>' . '(' . absint($this->get('failedTotal')) . ')',
1436 );
1437
1438 return $views;
1439 }
1440 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1441
1442 function prepare_items()
1443 {
1444 global $wpdb, $sll;
1445
1446 $table = $sll->table;
1447
1448 $date_filters = $sll->get_datefilter_values();
1449
1450 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1451 if ( $date_filters['year'] !== false ) {
1452 $allTotal = $wpdb->get_var(
1453 $wpdb->prepare(
1454 "SELECT COUNT(*) FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d",
1455 $table, $date_filters['year'],
1456 $date_filters['month'])
1457 );
1458 $successTotal = $wpdb->get_var(
1459 $wpdb->prepare(
1460 "SELECT COUNT(*) FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d AND login_result = %s",
1461 $table, $date_filters['year'],
1462 $date_filters['month'],
1463 '1')
1464 );
1465 $failedTotal = $wpdb->get_var(
1466 $wpdb->prepare(
1467 "SELECT COUNT(*) FROM %i WHERE YEAR(time) = %d AND MONTH(time) = %d AND login_result = %s",
1468 $table, $date_filters['year'],
1469 $date_filters['month'],
1470 '0')
1471 );
1472 } else {
1473 $allTotal = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) );
1474 $successTotal = $wpdb->get_var(
1475 $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE login_result = %s', $table, '1' )
1476 );
1477 $failedTotal = $wpdb->get_var(
1478 $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE login_result = %s', $table, '0' )
1479 );
1480 }
1481 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1482
1483 $this->set('allTotal', $allTotal);
1484 $this->set('successTotal', $successTotal);
1485 $this->set('failedTotal', $failedTotal);
1486
1487 $screen = get_current_screen();
1488
1489 /**
1490 * First, lets decide how many records per page to show
1491 */
1492 $per_page_option = $screen->id . '_per_page';
1493 $per_page = get_option($per_page_option, 20);
1494 $per_page = ($per_page != false) ? $per_page : 20;
1495
1496 $offset = $per_page * ($this->get_pagenum() - 1);
1497
1498 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1499 $orderby = (isset($_REQUEST['orderby']) && !empty($_REQUEST['orderby'])) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : false;
1500 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1501 $order = (isset($_REQUEST['order']) && !empty($_REQUEST['order'])) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : false;
1502
1503 $this->items = $sll->log_get_data($orderby, $order, $per_page, $offset);
1504
1505 /**
1506 * REQUIRED. Now we need to define our column headers. This includes a complete
1507 * array of columns to be displayed (slugs & titles), a list of columns
1508 * to keep hidden, and a list of columns that are sortable. Each of these
1509 * can be defined in another method (as we've done here) before being
1510 * used to build the value for our _column_headers property.
1511 */
1512 $columns = $this->get_columns();
1513 $hidden_cols = get_user_option( 'manage' . $screen->id . 'columnshidden' );
1514 $hidden = ( $hidden_cols ) ? $hidden_cols : array();
1515 $sortable = $this->get_sortable_columns();
1516
1517 /**
1518 * REQUIRED. Finally, we build an array to be used by the class for column
1519 * headers. The $this->_column_headers property takes an array which contains
1520 * 3 other arrays. One for all columns, one for hidden columns, and one
1521 * for sortable columns.
1522 */
1523 $this->_column_headers = array($columns, $hidden, $sortable);
1524 $columns = get_column_headers( $screen );
1525
1526 /**
1527 * REQUIRED for pagination. Let's figure out what page the user is currently
1528 * looking at. We'll need this later, so you should always include it in
1529 * your own package classes.
1530 */
1531 $current_page = $this->get_pagenum();
1532
1533 /**
1534 * REQUIRED for pagination. Let's check how many items are in our data array.
1535 * Get total items based on current filters
1536 */
1537 $total_items = $sll->get_total_filtered_count();
1538
1539 /**
1540 * REQUIRED. We also have to register our pagination options & calculations.
1541 */
1542 $this->set_pagination_args( array(
1543 'total_items' => $total_items, //WE have to calculate the total number of items
1544 'per_page' => $per_page, //WE have to determine how many items to show on a page
1545 'total_pages' => ceil($total_items/$per_page) //WE have to calculate the total number of pages
1546 ) );
1547
1548 }
1549
1550 }
1551