Simple Login Log - Version 0.2

Version Description

Download this release

Release Info

Developer maxchirkov
Plugin Icon wp plugin Simple Login Log
Version 0.2
Comparing to
See all releases

Version 0.2

Files changed (4) hide show
  1. readme.txt +30 -0
  2. screenshot-1.jpg +0 -0
  3. screenshot-2.jpg +0 -0
  4. simple-login-log.php +476 -0
readme.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Plugin Name ===
2
+ Contributors: Max Chirkov
3
+ Donate link: http://www.ibsteam.net/donate
4
+ Tags: login, log, users
5
+ Requires at least: 3.0
6
+ Tested up to: 3.2.1
7
+ Stable tag: 0.2
8
+
9
+ This plugin keeps a log of WordPress user logins. Offers user and date filtering, and export features.
10
+
11
+ == Description ==
12
+
13
+ Simple log of user logins. Tracks username, time of login, IP address and browser user agent.
14
+
15
+ **Features include:**
16
+
17
+ 1. ability to filter by username, month and year;
18
+ 2. export into CSV file;
19
+ 3. log auto-truncation.
20
+
21
+ == Installation ==
22
+
23
+ 1. Install and activate like any other basic plugin.
24
+ 2. If you wish to set log trancation, go to Settings => General. Scroll down to Simple Login Log.
25
+ 3. To view login log, go to Users => Login Log. You can export the log to CVS file form the same page.
26
+
27
+ == Screenshots ==
28
+
29
+ 1. Simple Login Log Settings.
30
+ 2. Login Log Management Screen.
screenshot-1.jpg ADDED
Binary file
screenshot-2.jpg ADDED
Binary file
simple-login-log.php ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ Plugin Name: Simple Login Log
4
+ Plugin URI: http://simplerealtytheme.com
5
+ Description: This plugin keeps a log of WordPress user logins. Offers user filtering and export features.
6
+ Author: Max Chirkov
7
+ Version: 0.2
8
+ Author URI: http://SimpleRealtyTheme.com
9
+ */
10
+
11
+
12
+ //TODO: add cleanup method on uninstall
13
+
14
+ if( !class_exists( 'SimpleLoginLog' ) )
15
+ {
16
+ class SimpleLoginLog {
17
+ private $table = 'simple_login_log';
18
+ private $log_duration = null; //days
19
+ private $opt_name = 'simple_login_log';
20
+ private $opt = false;
21
+
22
+ function __construct()
23
+ {
24
+ global $wpdb;
25
+ $this->table = $wpdb->prefix . $this->table;
26
+ $this->opt = get_option($this->opt_name);
27
+
28
+ if(isset($_GET['download-login-log']))
29
+ {
30
+ $this->export_to_CSV();
31
+ }
32
+
33
+
34
+ add_action( 'admin_menu', array(&$this, 'sll_admin_menu') );
35
+ add_action('admin_init', array(&$this, 'settings_api_init') );
36
+
37
+ //Action on successfull loging
38
+ add_action( 'wp_login', array(&$this, 'login_success') );
39
+
40
+ //Style the log table
41
+ add_action( 'admin_head', array(&$this, 'admin_header') );
42
+
43
+ //Initialize scheduled events
44
+ add_action( 'wp', array(&$this, 'init_scheduled_events') );
45
+ }
46
+
47
+ function init_scheduled_events()
48
+ {
49
+ if ( $this->opt['log_duration'] && !wp_next_scheduled( 'truncate_log' ) )
50
+ {
51
+ wp_schedule_event(time(), 'daily', 'truncate_log');
52
+ }elseif( !$this->opt['log_duration'] || 0 == $this->opt['log_duration'])
53
+ {
54
+ $timestamp = wp_next_scheduled( 'truncate_log' );
55
+ (!$timestamp) ? false : wp_unschedule_event($timestamp, 'truncate_log');
56
+
57
+ }
58
+ }
59
+
60
+ function truncate_log()
61
+ {
62
+ global $wpdb;
63
+
64
+ if( 0 < (int) $this->opt['log_duration'] ){
65
+ $sql = $wpdb->prepare( "DELETE FROM {$this->table} WHERE time < DATE_SUB(CURDATE(),INTERVAL %d DAY)", array($this->opt['log_duration']));
66
+ $wpdb->query($sql);
67
+ }
68
+ }
69
+
70
+ function install()
71
+ {
72
+ global $wpdb;
73
+
74
+ $sql = "CREATE TABLE " . $this->table . "
75
+ (
76
+ id INT( 11 ) NOT NULL AUTO_INCREMENT ,
77
+ uid INT( 11 ) NOT NULL ,
78
+ user_login VARCHAR( 60 ) NOT NULL ,
79
+ time DATETIME DEFAULT '0000-00-00 00:00:00' NOT NULL ,
80
+ ip VARCHAR( 100 ) NOT NULL ,
81
+ data LONGTEXT NOT NULL ,
82
+ PRIMARY KEY ( id ) ,
83
+ INDEX ( uid, ip )
84
+ );";
85
+
86
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
87
+ dbDelta($sql);
88
+ }
89
+
90
+ //Initializing Settings
91
+ function settings_api_init()
92
+ {
93
+ add_settings_section('simple_login_log', 'Simple Login Log', array(&$this, 'sll_settings'), 'general');
94
+ add_settings_field('field_log_duration', 'Truncate Log Entries', array(&$this, 'field_log_duration'), 'general', 'simple_login_log');
95
+ register_setting( 'general', 'simple_login_log' );
96
+ }
97
+
98
+ function sll_admin_menu()
99
+ {
100
+ add_submenu_page( 'users.php', __('Simple Login Log', 'sll'), __('Login Log', 'sll'), 'edit_users', 'login_log', array(&$this, 'log_manager') );
101
+ }
102
+
103
+ function sll_settings()
104
+ {
105
+ //content that goes before the fields output
106
+ }
107
+
108
+ function field_log_duration()
109
+ {
110
+ $duration = (null !== $this->opt['log_duration']) ? $this->opt['log_duration'] : $this->log_duration;
111
+ $output = '<input type="text" value="' . $duration . '" name="simple_login_log[log_duration]" size="10" class="code" /> days and older.';
112
+ echo $output;
113
+ echo "<p>Leave empty or enter 0 if you don't want the log to be truncated.</p>";
114
+ }
115
+
116
+ function admin_header()
117
+ {
118
+ if($_GET['page'] != 'login_log')
119
+ return;
120
+
121
+ echo '<style type="text/css">';
122
+ echo '.wp-list-table .column-id { width: 5%; }';
123
+ echo '.wp-list-table .column-uid { width: 10%; }';
124
+ echo '.wp-list-table .column-user_login { width: 10%; }';
125
+ echo '.wp-list-table .column-name { width: 15%; }';
126
+ echo '.wp-list-table .column-time { width: 15%; }';
127
+ echo '.wp-list-table .column-ip { width: 10%; }';
128
+ echo '</style>';
129
+ }
130
+
131
+ //Catch messages on successful login
132
+ function login_success($user_login){
133
+
134
+ $userdata = get_user_by('login', $user_login);
135
+
136
+ $uid = ($userdata->ID) ? $userdata->ID : 0;
137
+
138
+ //Stop if login form wasn't submitted
139
+ if( !$_REQUEST['wp-submit'] )
140
+ return;
141
+
142
+ if ( isset( $_REQUEST['redirect_to'] ) ) { $data['Login Redirect'] = $_REQUEST['redirect_to']; }
143
+ $data['User Agent'] = $_SERVER['HTTP_USER_AGENT'];
144
+
145
+ $serialized_data = serialize($data);
146
+
147
+ $values = array(
148
+ 'uid' => $uid,
149
+ 'user_login' => $user_login,
150
+ 'time' => current_time('mysql'),
151
+ 'ip' => $_SERVER['REMOTE_ADDR'],
152
+ 'data' => $serialized_data,
153
+ );
154
+
155
+ $format = array('%d', '%s', '%s', '%s', '%s');
156
+
157
+ $this->save_data($values, $format);
158
+ }
159
+
160
+ function save_data($values, $format){
161
+ global $wpdb;
162
+
163
+ $wpdb->insert( $this->table, $values, $format );
164
+ }
165
+
166
+ function log_manager()
167
+ {
168
+ global $wpdb, $ssl_list_table;
169
+
170
+ $log_table = new SLL_List_Table;
171
+
172
+ $limit = 20;
173
+ $offset = ( isset($_REQUEST['page']) ) ? $limit * $_REQUEST['page'] : 0;
174
+
175
+ if($_GET['filter'])
176
+ {
177
+ $where = "WHERE user_login = '{$_GET['filter']}'";
178
+ }
179
+ if($_GET['datefilter'])
180
+ {
181
+ $year = substr($_GET['datefilter'], 0, 4);
182
+ $month = substr($_GET['datefilter'], -2);
183
+ $where = "WHERE YEAR(time) = {$year} AND MONTH(time) = {$month}";
184
+ }
185
+
186
+ $sql = "SELECT * FROM $this->table $where ORDER BY time DESC LIMIT $limit OFFSET $offset";
187
+ $data = $wpdb->get_results($sql, 'ARRAY_A');
188
+
189
+ $log_table->items = $data;
190
+ $log_table->prepare_items();
191
+
192
+ echo '<div class="wrap srp">';
193
+ echo '<h2>Login Log</h2>';
194
+ echo '<div class="tablenav top">';
195
+ echo '<div class="alignleft actions">';
196
+ echo $this->date_filter();
197
+ echo '</div>';
198
+ echo '<form method="get">';
199
+ echo '<p class="search-box">';
200
+ echo '<input type="hidden" name="page" value="login_log" />';
201
+ echo '<label>Username: </label><input type="text" name="filter" class="filter-username" /> <input class="button" type="submit" value="Filter User" />';
202
+ echo '</p>';
203
+ echo '</form>';
204
+ echo '</div>';
205
+ $log_table->display();
206
+ echo '</div>';
207
+ echo '<form method="get" id="export-login-log">';
208
+ echo '<input type="hidden" name="page" value="login_log" />';
209
+ echo '<input type="hidden" name="download-login-log" value="true" />';
210
+ submit_button( __('Export Log to CSV'), 'secondary' );
211
+ echo '</form>';
212
+ }
213
+
214
+ private function date_filter()
215
+ {
216
+ global $wpdb;
217
+ $sql = "SELECT DISTINCT YEAR(time) as year, MONTH(time)as month FROM {$this->table}";
218
+ $results = $wpdb->get_results($sql);
219
+
220
+ if(!$results)
221
+ return;
222
+
223
+
224
+
225
+ foreach($results as $row)
226
+ {
227
+ //represent month in double digits
228
+ $timestamp = mktime(0, 0, 0, $row->month, 1, $row->year);
229
+ $month = (strlen($row->month) == 1) ? '0' . $row->month : $row->month;
230
+
231
+ $option .= '<option value="' . $row->year . $month . '" ' . selected($row->year . $month, $_GET['datefilter'], false) . '>' . date('F', $timestamp) . ' ' . $row->year . '</option>';
232
+ }
233
+
234
+ $output = '<form method="get">';
235
+ $output .= '<input type="hidden" name="page" value="login_log" />';
236
+ $output .= '<select name="datefilter"><option value="">View All</option>' . $option . '</select>';
237
+ $output .= '<input class="button" type="submit" value="Filter" />';
238
+ $output .= '</form>';
239
+ return $output;
240
+ }
241
+
242
+ function export_to_CSV(){
243
+ global $wpdb;
244
+
245
+ $sql = "SELECT * FROM $this->table";
246
+ $data = $wpdb->get_results($sql, 'ARRAY_A');
247
+
248
+ if(!$data)
249
+ return;
250
+
251
+ // send response headers to the browser
252
+ header( 'Content-Type: text/csv' );
253
+ header( 'Content-Disposition: attachment;filename=login_log.csv');
254
+ $fp = fopen('php://output', 'w');
255
+
256
+ $i = 0;
257
+ foreach($data as $row){
258
+ $tmp = unserialize($row['data']);
259
+ //output header row
260
+ if(0 == $i)
261
+ {
262
+ fputcsv( $fp, array_keys($row) );
263
+ }
264
+ $row_data = (!empty($tmp)) ? array_map(create_function('$key, $value', 'return $key.": ".$value." | ";'), array_keys($tmp), array_values($tmp)) : array();
265
+ $row['data'] = implode($row_data);
266
+ fputcsv($fp, $row);
267
+ $i++;
268
+ }
269
+
270
+ fclose($fp);
271
+ die();
272
+ }
273
+
274
+ }
275
+ }
276
+
277
+ if( class_exists( 'SimpleLoginLog' ) )
278
+ {
279
+ $sll = new SimpleLoginLog;
280
+ //Register for activation
281
+ register_activation_hook( __FILE__, array(&$sll, 'install') );
282
+
283
+ }
284
+
285
+ if(!class_exists('WP_List_Table')){
286
+ require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
287
+ }
288
+ class SLL_List_Table extends WP_List_Table
289
+ {
290
+ function __construct()
291
+ {
292
+ global $status, $page;
293
+
294
+ //Set parent defaults
295
+ parent::__construct( array(
296
+ 'singular' => 'user', //singular name of the listed records
297
+ 'plural' => 'users', //plural name of the listed records
298
+ 'ajax' => false //does this table support ajax?
299
+ ) );
300
+
301
+ }
302
+
303
+ function column_default($item, $column_name)
304
+ {
305
+ switch($column_name){
306
+ case 'id':
307
+ case 'uid':
308
+ case 'time':
309
+ case 'ip':
310
+ return $item[$column_name];
311
+ case 'user_login':
312
+ return "<a href='" . get_admin_url() . "users.php?page=login_log&filter={$item[$column_name]}' title='Filter log by this name'>{$item[$column_name]}</a>";
313
+ case 'name';
314
+ $user_info = get_userdata($item['uid']);
315
+ return $user_info->first_name . " " . $user_info->last_name;
316
+ case 'data':
317
+ $data = unserialize($item[$column_name]);
318
+ if(is_array($data))
319
+ {
320
+ foreach($data as $k => $v)
321
+ {
322
+ $output .= $k .': '. $v .'<br />';
323
+ }
324
+ return $output;
325
+ }
326
+ break;
327
+ default:
328
+ print_r($item);
329
+ }
330
+ }
331
+
332
+ function get_columns()
333
+ {
334
+ $columns = array(
335
+ 'id' => '#',
336
+ 'uid' => 'User ID',
337
+ 'user_login' => 'Username',
338
+ 'name' => 'Name',
339
+ 'time' => 'Time',
340
+ 'ip' => 'IP Address',
341
+ 'data' => 'Data',
342
+ );
343
+ return $columns;
344
+ }
345
+
346
+ function get_sortable_columns()
347
+ {
348
+ $sortable_columns = array(
349
+ //'id' => array('id',true), //doesn't sort correctly
350
+ 'uid' => array('uid',false),
351
+ 'time' => array('time',true),
352
+ 'ip' => array('ip', false),
353
+ );
354
+ return $sortable_columns;
355
+ }
356
+
357
+ function prepare_items()
358
+ {
359
+
360
+ /**
361
+ * First, lets decide how many records per page to show
362
+ */
363
+ $per_page = 20;
364
+
365
+
366
+ /**
367
+ * REQUIRED. Now we need to define our column headers. This includes a complete
368
+ * array of columns to be displayed (slugs & titles), a list of columns
369
+ * to keep hidden, and a list of columns that are sortable. Each of these
370
+ * can be defined in another method (as we've done here) before being
371
+ * used to build the value for our _column_headers property.
372
+ */
373
+ $columns = $this->get_columns();
374
+ $hidden = array();
375
+ $sortable = $this->get_sortable_columns();
376
+
377
+
378
+ /**
379
+ * REQUIRED. Finally, we build an array to be used by the class for column
380
+ * headers. The $this->_column_headers property takes an array which contains
381
+ * 3 other arrays. One for all columns, one for hidden columns, and one
382
+ * for sortable columns.
383
+ */
384
+ $this->_column_headers = array($columns, $hidden, $sortable);
385
+
386
+
387
+ /**
388
+ * Optional. You can handle your bulk actions however you see fit. In this
389
+ * case, we'll handle them within our package just to keep things clean.
390
+ */
391
+ //$this->process_bulk_action();
392
+
393
+
394
+ /**
395
+ * Instead of querying a database, we're going to fetch the example data
396
+ * property we created for use in this plugin. This makes this example
397
+ * package slightly different than one you might build on your own. In
398
+ * this example, we'll be using array manipulation to sort and paginate
399
+ * our data. In a real-world implementation, you will probably want to
400
+ * use sort and pagination data to build a custom query instead, as you'll
401
+ * be able to use your precisely-queried data immediately.
402
+ */
403
+ $data = $this->items;
404
+
405
+
406
+ /**
407
+ * This checks for sorting input and sorts the data in our array accordingly.
408
+ *
409
+ * In a real-world situation involving a database, you would probably want
410
+ * to handle sorting by passing the 'orderby' and 'order' values directly
411
+ * to a custom query. The returned data will be pre-sorted, and this array
412
+ * sorting technique would be unnecessary.
413
+ */
414
+ function usort_reorder($a,$b){
415
+ $orderby = (!empty($_REQUEST['orderby'])) ? $_REQUEST['orderby'] : 'time'; //If no sort, default to title
416
+ $order = (!empty($_REQUEST['order'])) ? $_REQUEST['order'] : 'desc'; //If no order, default to asc
417
+ $result = strcmp($a[$orderby], $b[$orderby]); //Determine sort order
418
+ return ($order==='asc') ? $result : -$result; //Send final sort direction to usort
419
+ }
420
+ usort($data, 'usort_reorder');
421
+
422
+
423
+ /***********************************************************************
424
+ * ---------------------------------------------------------------------
425
+ * vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
426
+ *
427
+ * In a real-world situation, this is where you would place your query.
428
+ *
429
+ * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
430
+ * ---------------------------------------------------------------------
431
+ **********************************************************************/
432
+
433
+
434
+ /**
435
+ * REQUIRED for pagination. Let's figure out what page the user is currently
436
+ * looking at. We'll need this later, so you should always include it in
437
+ * your own package classes.
438
+ */
439
+ $current_page = $this->get_pagenum();
440
+
441
+ /**
442
+ * REQUIRED for pagination. Let's check how many items are in our data array.
443
+ * In real-world use, this would be the total number of items in your database,
444
+ * without filtering. We'll need this later, so you should always include it
445
+ * in your own package classes.
446
+ */
447
+ $total_items = count($data);
448
+
449
+
450
+ /**
451
+ * The WP_List_Table class does not handle pagination for us, so we need
452
+ * to ensure that the data is trimmed to only the current page. We can use
453
+ * array_slice() to
454
+ */
455
+ $data = array_slice($data,(($current_page-1)*$per_page),$per_page);
456
+
457
+
458
+
459
+ /**
460
+ * REQUIRED. Now we can add our *sorted* data to the items property, where
461
+ * it can be used by the rest of the class.
462
+ */
463
+ $this->items = $data;
464
+
465
+
466
+ /**
467
+ * REQUIRED. We also have to register our pagination options & calculations.
468
+ */
469
+ $this->set_pagination_args( array(
470
+ 'total_items' => $total_items, //WE have to calculate the total number of items
471
+ 'per_page' => $per_page, //WE have to determine how many items to show on a page
472
+ 'total_pages' => ceil($total_items/$per_page) //WE have to calculate the total number of pages
473
+ ) );
474
+ }
475
+
476
+ }