Simple History - Version 2.17

Version Description

(June 2017) =

  • Fix search date range inputs not showing correctly.
  • Change the message for when a plugin is deactivated due to an error. Now the plugin slug is included, so you know exactly what plugin has been deactivated. Also the reason for the deactivation is included (one of "Invalid plugin path", "Plugin file does not exist", or "The plugin does not have a valid header.").
  • Added more filters to log message. Now filter simple_history_log_debug exists, together with filters for all other 7 log levels. So you can use simple_history_log_{loglevel} where {loglevel} is any of emergency, alert, critical, error, warning, notice, info or debug.
  • Add support for logging the changing of "locale" on a user profile, something that was added in WordPress 4.7.
  • Add sidebar box with link to the settings page.
  • Don't log when old posts are deleted from the trash during cron job wp_scheduled_delete.
  • HHVM is not used for any tests any longer because PHP 7 and Travis not supporting it or something. I dunno. Something like that.
  • When "development debug mode" is activated also log current filters.
  • Show an admin warning if a logger slug is longer than 30 chars.
  • Fix fatal error when calling log() method with null as context argument.
Download this release

Release Info

Developer eskapism
Plugin Icon 128x128 Simple History
Version 2.17
Comparing to
See all releases

Code changes from version 2.16 to 2.17

README.md CHANGED
@@ -1,6 +1,6 @@
1
  # Simple History 2 – a simple, lightweight, extendable logger for WordPress
2
 
3
- Simple History is a WordPress plugin that logs various things that occur in WordPress and then presents those events in a very nice GUI.
4
 
5
  Download from WordPress.org:
6
  https://wordpress.org/plugins/simple-history/
@@ -37,7 +37,26 @@ Developers can easily log their own things using a simple API:
37
 
38
  ```php
39
  <?php
40
- // Add events to the log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  SimpleLogger()->info("This is a message sent to the log");
42
 
43
  // Add events of different severity
1
  # Simple History 2 – a simple, lightweight, extendable logger for WordPress
2
 
3
+ Simple History is a WordPress plugin that logs various things that occur in WordPress and then presents those events in a very nice GUI. It's great way to view user activity and keep an eye on what the users are doing.
4
 
5
  Download from WordPress.org:
6
  https://wordpress.org/plugins/simple-history/
37
 
38
  ```php
39
  <?php
40
+
41
+ // This is the easiest and safest way to add messages to the log
42
+ // If the plugin is disabled this way will not generate in any error
43
+ apply_filters("simple_history_log", "This is a logged message");
44
+
45
+ // Or with some context and with log level debug:
46
+ apply_filters(
47
+ 'simple_history_log',
48
+ 'My message about something',
49
+ [
50
+ 'debugThing' => $myThingThatIWantIncludedInTheLoggedEvent,
51
+ 'anotherThing' => $anotherThing
52
+ ],
53
+ 'debug'
54
+ );
55
+
56
+ // Or just debug a message quickly
57
+ apply_filters('simple_history_log_debug', 'My debug message');
58
+
59
+ // You can olsy use functions/methods to add events to the log
60
  SimpleLogger()->info("This is a message sent to the log");
61
 
62
  // Add events of different severity
dropins/SimpleHistoryFilterDropin.css CHANGED
@@ -172,6 +172,11 @@
172
  transition: max-height .25s ease-in-out, opacity .25s ease-in-out, visibility 0s 0s;
173
  }
174
 
 
 
 
 
 
175
  .postbox .SimpleHistory__filters__filter--dayValuesWrap {
176
  margin-left: 0;
177
  }
172
  transition: max-height .25s ease-in-out, opacity .25s ease-in-out, visibility 0s 0s;
173
  }
174
 
175
+ .is-customDateFilterActive .SimpleHistory__filters__filterRow--date {
176
+ line-height: inherit;
177
+ height: inherit;
178
+ }
179
+
180
  .postbox .SimpleHistory__filters__filter--dayValuesWrap {
181
  margin-left: 0;
182
  }
dropins/SimpleHistoryFilterDropin.js CHANGED
@@ -229,8 +229,6 @@ var SimpleHistoryFilterDropin = (function($) {
229
  $elms.filter_container.removeClass("is-customDateFilterActive");
230
  }
231
 
232
-
233
-
234
  }
235
 
236
  function formatUsers(userdata) {
229
  $elms.filter_container.removeClass("is-customDateFilterActive");
230
  }
231
 
 
 
232
  }
233
 
234
  function formatUsers(userdata) {
dropins/SimpleHistoryFilterDropin.php CHANGED
@@ -163,7 +163,7 @@ class SimpleHistoryFilterDropin {
163
 
164
  ?>
165
 
166
- <p class="SimpleHistory__filters__filterRow" data-debug-daysAndPages='<?php echo json_encode( $arr_days_and_pages ) ?>'>
167
 
168
  <label class="SimpleHistory__filters__filterLabel"><?php _ex("Dates:", "Filter label", "simple-history") ?></label>
169
 
163
 
164
  ?>
165
 
166
+ <p class="SimpleHistory__filters__filterRow SimpleHistory__filters__filterRow--date" data-debug-daysAndPages='<?php echo json_encode( $arr_days_and_pages ) ?>'>
167
 
168
  <label class="SimpleHistory__filters__filterLabel"><?php _ex("Dates:", "Filter label", "simple-history") ?></label>
169
 
dropins/SimpleHistorySidebarSettings.php ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ defined( 'ABSPATH' ) or die();
4
+
5
+ /*
6
+ Dropin Name: Sidebar with link to settings
7
+ Dropin URI: http://simple-history.com/
8
+ Author: Pär Thernström
9
+ */
10
+
11
+ class SimpleHistorySidebarSettings {
12
+
13
+ /**
14
+ * Simple History isntance
15
+ *
16
+ * @var object $sh Simple History instance.
17
+ */
18
+ private $sh;
19
+
20
+ /**
21
+ * Constructor.
22
+ *
23
+ * @param object $sh Simple History instance.
24
+ */
25
+ function __construct( $sh ) {
26
+
27
+ $this->init( $sh );
28
+
29
+ }
30
+
31
+ /**
32
+ * Init
33
+ *
34
+ * @param object $sh Simple History instance.
35
+ */
36
+ function init( $sh ) {
37
+
38
+ $this->sh = $sh;
39
+
40
+ add_action( 'simple_history/dropin/sidebar/sidebar_html', array( $this, 'on_sidebar_html' ), 5 );
41
+
42
+ }
43
+
44
+ /**
45
+ * Output HTML
46
+ */
47
+ function on_sidebar_html() {
48
+
49
+ ?>
50
+
51
+ <div class="postbox">
52
+
53
+ <h3 class="hndle"><?php esc_html_e( 'Settings', 'simple-history' ) ?></h3>
54
+
55
+ <div class="inside">
56
+
57
+ <p>
58
+ <?php
59
+
60
+ /*
61
+ Visit the settings page to change the number of items to show and
62
+ where to show
63
+ rss feed
64
+ clear log
65
+
66
+ - Visit the settings page to change the number of events to show, to get
67
+ - Visit the settings page
68
+ */
69
+ printf(
70
+ wp_kses(
71
+ /* translators: 1: URL to settings page */
72
+ __( '<a href="%1$s">Visit the settings page</a> to change things like the number of events to show and to get access to the RSS feed with all events, and more.', 'simple-history' ),
73
+ array(
74
+ 'a' => array(
75
+ 'href' => array(),
76
+ ),
77
+ )
78
+ ),
79
+ esc_url( menu_page_url( SimpleHistory::SETTINGS_MENU_SLUG, false ) )
80
+ );
81
+ ?>
82
+ </p>
83
+
84
+ </div>
85
+ </div>
86
+
87
+ <?php
88
+
89
+ }
90
+
91
+ }
inc/SimpleHistory.php CHANGED
@@ -103,16 +103,16 @@ class SimpleHistory {
103
  add_action( 'after_setup_theme', array( $this, 'load_loggers' ) );
104
  add_action( 'after_setup_theme', array( $this, 'load_dropins' ) );
105
 
106
- // Run before loading of loggers and before menu items are added
107
  add_action( 'after_setup_theme', array( $this, 'check_for_upgrade' ), 5 );
108
 
109
  add_action( 'after_setup_theme', array( $this, 'setup_cron' ) );
110
 
111
- // Filters and actions not called during regular boot
112
- add_filter( "gettext", array( $this, 'filter_gettext' ), 20, 3 );
113
- add_filter( "gettext_with_context", array( $this, 'filter_gettext_with_context' ), 20, 4 );
114
 
115
- add_filter( 'gettext', array( $this, "filter_gettext_storeLatestTranslations" ), 10, 3 );
116
 
117
  add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_network_menu_item' ), 40 );
118
  add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_menu_item' ), 40 );
@@ -130,7 +130,23 @@ class SimpleHistory {
130
  *
131
  * @since 2.13
132
  */
133
- add_filter( 'simple_history_log', array($this, "on_filter_simple_history_log"), 10, 3 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  if ( is_admin() ) {
136
 
@@ -173,6 +189,10 @@ class SimpleHistory {
173
  $context["_debug_is_admin"] = json_encode( is_admin() );
174
  $context["_debug_is_doing_cron"] = json_encode( defined('DOING_CRON') && DOING_CRON );
175
 
 
 
 
 
176
  return $context;
177
 
178
  }, 10, 4 );
@@ -187,22 +207,92 @@ class SimpleHistory {
187
  * Function called when running filter "simple_history_log"
188
  *
189
  * @since 2.13
190
- * @param mixed $logMessage
191
- * @param array $context Optional context to add to the logged data
192
  * @param string $level The loglevel. Must be one of the existing ones. Defaults to "info".
193
  */
194
- public function on_filter_simple_history_log( $message = null, $context = null, $level = "info" ) {
 
 
195
 
196
- if (empty($message)) {
197
- return;
198
- }
 
 
 
 
 
 
199
 
200
- if (!is_array($context)) {
201
- $context = array();
202
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
- SimpleLogger()->log($level, $message, $context);
 
 
 
 
 
 
 
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
  /**
@@ -986,6 +1076,12 @@ class SimpleHistory {
986
 
987
  $loggerInfo = $loggerInstance->getInfo();
988
 
 
 
 
 
 
 
989
  /*
990
  $loggerInfo["messages"]
991
  [messages] => Array
@@ -1019,29 +1115,28 @@ class SimpleHistory {
1019
 
1020
  $arr_messages_by_message_key = array();
1021
 
1022
- if ( isset( $loggerInfo["messages"] ) ) {
1023
-
1024
- foreach ( (array) $loggerInfo["messages"] as $message_key => $message_translated ) {
1025
-
1026
- // Find message in array with both translated and non translated strings
1027
- foreach ( $loggerInstance->messages as $one_message_with_translation_info ) {
1028
-
1029
- /*
1030
- [0] => Array
1031
- (
1032
- [untranslated_text] => ...
1033
- [translated_text] => ...
1034
- [domain] => simple-history
1035
- [context] => ...
1036
- )
1037
- */
1038
- if ( $message_translated == $one_message_with_translation_info["translated_text"] ) {
1039
- $arr_messages_by_message_key[ $message_key ] = $one_message_with_translation_info;
1040
- continue;
1041
- }
1042
 
1043
  }
1044
-
1045
  }
1046
 
1047
  }
@@ -1081,6 +1176,7 @@ class SimpleHistory {
1081
  $dropinsDir . "SimpleHistorySettingsDebugDropin.php",
1082
  $dropinsDir . "SimpleHistorySidebarDropin.php",
1083
  $dropinsDir . "SimpleHistorySidebarStats.php",
 
1084
  $dropinsDir . "SimpleHistoryWPCLIDropin.php",
1085
  );
1086
 
@@ -3345,6 +3441,17 @@ Because Simple History was just recently installed, this feed does not contain m
3345
 
3346
  } // get_unique_events_for_days
3347
 
 
 
 
 
 
 
 
 
 
 
 
3348
  } // class
3349
 
3350
 
103
  add_action( 'after_setup_theme', array( $this, 'load_loggers' ) );
104
  add_action( 'after_setup_theme', array( $this, 'load_dropins' ) );
105
 
106
+ // Run before loading of loggers and before menu items are added.
107
  add_action( 'after_setup_theme', array( $this, 'check_for_upgrade' ), 5 );
108
 
109
  add_action( 'after_setup_theme', array( $this, 'setup_cron' ) );
110
 
111
+ // Filters and actions not called during regular boot.
112
+ add_filter( 'gettext', array( $this, 'filter_gettext' ), 20, 3 );
113
+ add_filter( 'gettext_with_context', array( $this, 'filter_gettext_with_context' ), 20, 4 );
114
 
115
+ add_filter( 'gettext', array( $this, 'filter_gettext_storeLatestTranslations' ), 10, 3 );
116
 
117
  add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_network_menu_item' ), 40 );
118
  add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_menu_item' ), 40 );
130
  *
131
  * @since 2.13
132
  */
133
+ add_filter( 'simple_history_log', array( $this, 'on_filter_simple_history_log' ), 10, 3 );
134
+
135
+ /**
136
+ * Filter to log with specific log level, for example:
137
+ * apply_filters('simple_history_log_debug', 'My debug message');
138
+ * apply_filters('simple_history_log_warning', 'My warning message');
139
+ *
140
+ * @since 2.17
141
+ */
142
+ add_filter( 'simple_history_log_emergency', array( $this, 'on_filter_simple_history_log_emergency' ), 10, 3 );
143
+ add_filter( 'simple_history_log_alert', array( $this, 'on_filter_simple_history_log_alert' ), 10, 2 );
144
+ add_filter( 'simple_history_log_critical', array( $this, 'on_filter_simple_history_log_critical' ), 10, 2 );
145
+ add_filter( 'simple_history_log_error', array( $this, 'on_filter_simple_history_log_error' ), 10, 2 );
146
+ add_filter( 'simple_history_log_warning', array( $this, 'on_filter_simple_history_log_warning' ), 10, 2 );
147
+ add_filter( 'simple_history_log_notice', array( $this, 'on_filter_simple_history_log_notice' ), 10, 2 );
148
+ add_filter( 'simple_history_log_info', array( $this, 'on_filter_simple_history_log_info' ), 10, 2 );
149
+ add_filter( 'simple_history_log_debug', array( $this, 'on_filter_simple_history_log_debug' ), 10, 2 );
150
 
151
  if ( is_admin() ) {
152
 
189
  $context["_debug_is_admin"] = json_encode( is_admin() );
190
  $context["_debug_is_doing_cron"] = json_encode( defined('DOING_CRON') && DOING_CRON );
191
 
192
+ global $wp_current_filter;
193
+ $context['_debug_current_filter_array'] = $wp_current_filter;
194
+ $context['_debug_current_filter'] = current_filter();
195
+
196
  return $context;
197
 
198
  }, 10, 4 );
207
  * Function called when running filter "simple_history_log"
208
  *
209
  * @since 2.13
210
+ * @param string $message The message to log.
211
+ * @param array $context Optional context to add to the logged data.
212
  * @param string $level The loglevel. Must be one of the existing ones. Defaults to "info".
213
  */
214
+ public function on_filter_simple_history_log( $message = null, $context = null, $level = 'info' ) {
215
+ SimpleLogger()->log( $level, $message, $context );
216
+ }
217
 
218
+ /**
219
+ * Log a message, triggered by filter 'on_filter_simple_history_log_emergency'.
220
+ *
221
+ * @param string $message The message to log.
222
+ * @param array $context The context (optional).
223
+ */
224
+ public function on_filter_simple_history_log_emergency( $message = null, $context = null ) {
225
+ SimpleLogger()->log( 'emergency', $message, $context );
226
+ }
227
 
228
+ /**
229
+ * Log a message, triggered by filter 'on_filter_simple_history_log_alert'.
230
+ *
231
+ * @param string $message The message to log.
232
+ * @param array $context The context (optional).
233
+ */
234
+ public function on_filter_simple_history_log_alert( $message = null, $context = null ) {
235
+ SimpleLogger()->log( 'alert', $message, $context );
236
+ }
237
+
238
+ /**
239
+ * Log a message, triggered by filter 'on_filter_simple_history_log_critical'.
240
+ *
241
+ * @param string $message The message to log.
242
+ * @param array $context The context (optional).
243
+ */
244
+ public function on_filter_simple_history_log_critical( $message = null, $context = null ) {
245
+ SimpleLogger()->log( 'critical', $message, $context );
246
+ }
247
+
248
+ /**
249
+ * Log a message, triggered by filter 'on_filter_simple_history_log_error'.
250
+ *
251
+ * @param string $message The message to log.
252
+ * @param array $context The context (optional).
253
+ */
254
+ public function on_filter_simple_history_log_error( $message = null, $context = null ) {
255
+ SimpleLogger()->log( 'error', $message, $context );
256
+ }
257
+
258
+ /**
259
+ * Log a message, triggered by filter 'on_filter_simple_history_log_warning'.
260
+ *
261
+ * @param string $message The message to log.
262
+ * @param array $context The context (optional).
263
+ */
264
+ public function on_filter_simple_history_log_warning( $message = null, $context = null ) {
265
+ SimpleLogger()->log( 'warning', $message, $context );
266
+ }
267
 
268
+ /**
269
+ * Log a message, triggered by filter 'on_filter_simple_history_log_notice'.
270
+ *
271
+ * @param string $message The message to log.
272
+ * @param array $context The context (optional).
273
+ */
274
+ public function on_filter_simple_history_log_notice( $message = null, $context = null ) {
275
+ SimpleLogger()->log( 'notice', $message, $context );
276
+ }
277
 
278
+ /**
279
+ * Log a message, triggered by filter 'on_filter_simple_history_log_info'.
280
+ *
281
+ * @param string $message The message to log.
282
+ * @param array $context The context (optional).
283
+ */
284
+ public function on_filter_simple_history_log_info( $message = null, $context = null ) {
285
+ SimpleLogger()->log( 'info', $message, $context );
286
+ }
287
+
288
+ /**
289
+ * Log a message, triggered by filter 'on_filter_simple_history_log_debug'.
290
+ *
291
+ * @param string $message The message to log.
292
+ * @param array $context The context (optional).
293
+ */
294
+ public function on_filter_simple_history_log_debug( $message = null, $context = null ) {
295
+ SimpleLogger()->log( 'debug', $message, $context );
296
  }
297
 
298
  /**
1076
 
1077
  $loggerInfo = $loggerInstance->getInfo();
1078
 
1079
+ // Check so no logger has a logger slug with more than 30 chars,
1080
+ // because db column is only 30 chars.
1081
+ if ( strlen( $loggerInstance->slug ) > 30 ) {
1082
+ add_action( 'admin_notices', array( $this, 'admin_notice_logger_slug_to_long' ) );
1083
+ }
1084
+
1085
  /*
1086
  $loggerInfo["messages"]
1087
  [messages] => Array
1115
 
1116
  $arr_messages_by_message_key = array();
1117
 
1118
+ if ( isset( $loggerInfo["messages"] ) ) {
1119
+
1120
+ foreach ( (array) $loggerInfo["messages"] as $message_key => $message_translated ) {
1121
+
1122
+ // Find message in array with both translated and non translated strings
1123
+ foreach ( $loggerInstance->messages as $one_message_with_translation_info ) {
1124
+
1125
+ /*
1126
+ [0] => Array
1127
+ (
1128
+ [untranslated_text] => ...
1129
+ [translated_text] => ...
1130
+ [domain] => simple-history
1131
+ [context] => ...
1132
+ )
1133
+ */
1134
+ if ( $message_translated == $one_message_with_translation_info["translated_text"] ) {
1135
+ $arr_messages_by_message_key[ $message_key ] = $one_message_with_translation_info;
1136
+ continue;
1137
+ }
1138
 
1139
  }
 
1140
  }
1141
 
1142
  }
1176
  $dropinsDir . "SimpleHistorySettingsDebugDropin.php",
1177
  $dropinsDir . "SimpleHistorySidebarDropin.php",
1178
  $dropinsDir . "SimpleHistorySidebarStats.php",
1179
+ $dropinsDir . "SimpleHistorySidebarSettings.php",
1180
  $dropinsDir . "SimpleHistoryWPCLIDropin.php",
1181
  );
1182
 
3441
 
3442
  } // get_unique_events_for_days
3443
 
3444
+ /**
3445
+ * Output an admin notice about logger slug being to long
3446
+ */
3447
+ public function admin_notice_logger_slug_to_long() {
3448
+ ?>
3449
+ <div class="error notice">
3450
+ <p><?php echo esc_html__( 'The slug for a logger in Simple History can be max 30 chars long.', 'simple-history' ); ?></p>
3451
+ </div>
3452
+ <?php
3453
+ }
3454
+
3455
  } // class
3456
 
3457
 
index.php CHANGED
@@ -5,7 +5,7 @@ Plugin URI: http://simple-history.com
5
  Text Domain: simple-history
6
  Domain Path: /languages
7
  Description: Plugin that logs various things that occur in WordPress and then presents those events in a very nice GUI.
8
- Version: 2.16
9
  Author: Pär Thernström
10
  Author URI: http://simple-history.com/
11
  License: GPL2
@@ -42,7 +42,7 @@ if ( version_compare( phpversion(), "5.3", ">=") ) {
42
  // register_activation_hook( trailingslashit(WP_PLUGIN_DIR) . trailingslashit( plugin_basename(__DIR__) ) . "index.php" , array("SimpleHistory", "on_plugin_activate" ) );
43
 
44
  if ( ! defined( 'SIMPLE_HISTORY_VERSION' ) ) {
45
- define( 'SIMPLE_HISTORY_VERSION', '2.16' );
46
  }
47
 
48
  if ( ! defined( 'SIMPLE_HISTORY_PATH' ) ) {
5
  Text Domain: simple-history
6
  Domain Path: /languages
7
  Description: Plugin that logs various things that occur in WordPress and then presents those events in a very nice GUI.
8
+ Version: 2.17
9
  Author: Pär Thernström
10
  Author URI: http://simple-history.com/
11
  License: GPL2
42
  // register_activation_hook( trailingslashit(WP_PLUGIN_DIR) . trailingslashit( plugin_basename(__DIR__) ) . "index.php" , array("SimpleHistory", "on_plugin_activate" ) );
43
 
44
  if ( ! defined( 'SIMPLE_HISTORY_VERSION' ) ) {
45
+ define( 'SIMPLE_HISTORY_VERSION', '2.17' );
46
  }
47
 
48
  if ( ! defined( 'SIMPLE_HISTORY_PATH' ) ) {
loggers/SimpleLogger.php CHANGED
@@ -939,22 +939,37 @@ class SimpleLogger {
939
  /**
940
  * Logs with an arbitrary level.
941
  *
942
- * @param mixed $level
943
- * @param string $message
944
- * @param array $context
945
- * @return null
946
  */
947
- public function log($level, $message, array $context = array()) {
948
 
949
  global $wpdb;
950
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
951
  /*
952
  * Filter that makes it possible to shortcut this log.
953
  * Return bool false to cancel.
954
  *
955
  * @since 2.3.1
956
  */
957
- $do_log = apply_filters( "simple_history/log/do_log", true, $level, $message, $context, $this );
958
 
959
  if ( $do_log === false ) {
960
  return $this;
939
  /**
940
  * Logs with an arbitrary level.
941
  *
942
+ * @param mixed $level The log level.
943
+ * @param string $message The log message.
944
+ * @param array $context The log context.
945
+ * @return class SimpleLogger instance
946
  */
947
+ public function log( $level = 'info', $message = '', $context = array() ) {
948
 
949
  global $wpdb;
950
 
951
+ // Check that passed args are of correct types.
952
+ if ( ! is_string( $level ) || ! is_string( $message ) ) {
953
+ return $this;
954
+ }
955
+
956
+ // Context must be array, but can be passed as null and so on.
957
+ if ( ! is_array( $context ) ) {
958
+ $context = array();
959
+ }
960
+
961
+ // Don't go on if message is empty.
962
+ if ( empty( $message ) ) {
963
+ return $this;
964
+ }
965
+
966
  /*
967
  * Filter that makes it possible to shortcut this log.
968
  * Return bool false to cancel.
969
  *
970
  * @since 2.3.1
971
  */
972
+ $do_log = apply_filters( 'simple_history/log/do_log', true, $level, $message, $context, $this );
973
 
974
  if ( $do_log === false ) {
975
  return $this;
loggers/SimplePluginLogger.php CHANGED
@@ -1,15 +1,28 @@
1
  <?php
2
 
3
- defined( 'ABSPATH' ) or die();
4
 
5
  /**
6
  * Logs plugin installs, updates, and deletions
7
  */
8
  class SimplePluginLogger extends SimpleLogger {
9
 
10
- // The logger slug. Defaulting to the class name is nice and logical I think
 
 
 
 
11
  public $slug = __CLASS__;
12
 
 
 
 
 
 
 
 
 
 
13
  /**
14
  * Get array with information about this logger
15
  *
@@ -18,10 +31,10 @@ class SimplePluginLogger extends SimpleLogger {
18
  function getInfo() {
19
 
20
  $arr_info = array(
21
- "name" => "Plugin Logger",
22
- "description" => "Logs plugin installs, uninstalls and updates",
23
- "capability" => "activate_plugins", // install_plugins, activate_plugins,
24
- "messages" => array(
25
 
26
  'plugin_activated' => _x(
27
  'Activated plugin "{plugin_name}"',
@@ -65,65 +78,68 @@ class SimplePluginLogger extends SimpleLogger {
65
  'simple-history'
66
  ),
67
 
68
- // bulk versions
69
  'plugin_bulk_updated' => _x(
70
  'Updated plugin "{plugin_name}" to {plugin_version} from {plugin_prev_version}',
71
  'Plugin was updated in bulk',
72
  'simple-history'
73
  ),
74
 
75
- // plugin disabled due to some error
76
  'plugin_disabled_because_error' => _x(
77
- 'Deactivated a plugin because of an error: {error_message}',
78
  'Plugin was disabled because of an error',
79
  'simple-history'
80
  ),
81
 
82
- ), // messages
83
- "labels" => array(
84
- "search" => array(
85
- "label" => _x("Plugins", "Plugin logger: search", "simple-history"),
86
- "label_all" => _x("All plugin activity", "Plugin logger: search", "simple-history"),
87
- "options" => array(
88
- _x("Activated plugins", "Plugin logger: search", "simple-history") => array(
89
  'plugin_activated'
90
  ),
91
- _x("Deactivated plugins", "Plugin logger: search", "simple-history") => array(
92
  'plugin_deactivated',
93
- 'plugin_disabled_because_error'
94
  ),
95
- _x("Installed plugins", "Plugin logger: search", "simple-history") => array(
96
  'plugin_installed'
97
  ),
98
- _x("Failed plugin installs", "Plugin logger: search", "simple-history") => array(
99
  'plugin_installed_failed'
100
  ),
101
- _x("Updated plugins", "Plugin logger: search", "simple-history") => array(
102
  'plugin_updated',
103
- 'plugin_bulk_updated'
104
  ),
105
- _x("Failed plugin updates", "Plugin logger: search", "simple-history") => array(
106
  'plugin_update_failed'
107
  ),
108
- _x("Deleted plugins", "Plugin logger: search", "simple-history") => array(
109
  'plugin_deleted'
110
  ),
111
- )
112
- ) // search array
113
- ) // labels
114
  );
115
 
116
  return $arr_info;
117
 
118
  }
119
 
 
 
 
120
  public function loaded() {
121
 
122
  /**
123
  * At least the plugin bulk upgrades fires this action before upgrade
124
  * We use it to fetch the current version of all plugins, before they are upgraded
125
  */
126
- add_filter( 'upgrader_pre_install', array( $this, "save_versions_before_update"), 10, 2);
127
 
128
  // Clear our transient after an update is done
129
  // Removed because something probably changed in core and this was fired earlier than it used to be
@@ -146,38 +162,91 @@ class SimplePluginLogger extends SimpleLogger {
146
  // Detect files removed
147
  add_action( 'setted_transient', array( $this, 'on_setted_transient_for_remove_files' ), 10, 2 );
148
 
149
- add_action("admin_action_delete-selected", array($this, "on_action_delete_selected"), 10, 1);
150
 
151
  // Ajax function to get info from GitHub repo. Used by "View plugin info"-link for plugin installs
152
  add_action("wp_ajax_SimplePluginLogger_GetGitHubPluginInfo", array($this, "ajax_GetGitHubPluginInfo"));
153
 
154
  // If the Github Update plugin is not installed we need to get extra fields used by it.
155
  // So need to hook filter "extra_plugin_headers" ourself.
156
- add_filter( "extra_plugin_headers", function($arr_headers) {
157
  $arr_headers[] = "GitHub Plugin URI";
158
  return $arr_headers;
159
  } );
160
 
161
- // There is no way to ue a filter and detect a plugin that is disabled because it can't be found or similar error.
162
  // So we hook into gettext and look for the usage of the error that is returned when this happens.
163
- add_filter( 'gettext', array( $this, "on_gettext" ), 10, 3 );
 
164
 
165
  }
166
 
167
  /**
168
- * There is no way to ue a filter and detect a plugin that is disabled because it can't be found or similar error.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  * we hook into gettext and look for the usage of the error that is returned when this happens.
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  */
171
  function on_gettext( $translation, $text, $domain ) {
172
 
173
- // The errors we can get is:
174
- // return new WP_Error('plugin_invalid', __('Invalid plugin path.'));
175
- // return new WP_Error('plugin_not_found', __('Plugin file does not exist.'));
176
- // return new WP_Error('no_plugin_header', __('The plugin does not have a valid header.'));
177
-
178
  global $pagenow;
179
 
180
- // We only act on page plugins.php
181
  if ( ! isset( $pagenow ) || $pagenow !== "plugins.php" ) {
182
  return $translation;
183
  }
@@ -185,30 +254,47 @@ class SimplePluginLogger extends SimpleLogger {
185
  // We only act if the untranslated text is among the following ones
186
  // (Literally these, no translation)
187
  $untranslated_texts = array(
188
- "Plugin file does not exist.",
189
- "Invalid plugin path.",
190
- "The plugin does not have a valid header."
191
  );
192
 
193
- if ( ! in_array( $text, $untranslated_texts )) {
194
  return $translation;
195
  }
196
 
197
- // We don't know what plugin that was that got this error and currently there does not seem to be a way to determine that
198
- // So that's why we use such generic log messages
199
- $this->warningMessage(
200
- "plugin_disabled_because_error",
201
- array(
202
- "_initiator" => SimpleLoggerLogInitiators::WORDPRESS,
203
- "error_message" => $text
204
- )
205
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  return $translation;
208
 
209
  } // on_gettext
210
 
211
-
212
  /**
213
  * Show readme from github in a modal win
214
  */
1
  <?php
2
 
3
+ defined( 'ABSPATH' ) || die();
4
 
5
  /**
6
  * Logs plugin installs, updates, and deletions
7
  */
8
  class SimplePluginLogger extends SimpleLogger {
9
 
10
+ /**
11
+ * The logger slug. Defaulting to the class name is nice and logical I think
12
+ *
13
+ * @var string $slug
14
+ */
15
  public $slug = __CLASS__;
16
 
17
+ /**
18
+ * This variable is set if a plugins has been disabled due to an error,
19
+ * like when the plugin file does not exist. We need to store this in this
20
+ * weird way because there is no other way for us to get the reason.
21
+ *
22
+ * @var string $latest_plugin_deactivation_because_of_error_reason
23
+ */
24
+ public $latest_plugin_deactivation_because_of_error_reason = array();
25
+
26
  /**
27
  * Get array with information about this logger
28
  *
31
  function getInfo() {
32
 
33
  $arr_info = array(
34
+ 'name' => 'Plugin Logger',
35
+ 'description' => 'Logs plugin installs, uninstalls and updates',
36
+ 'capability' => 'activate_plugins',
37
+ 'messages' => array(
38
 
39
  'plugin_activated' => _x(
40
  'Activated plugin "{plugin_name}"',
78
  'simple-history'
79
  ),
80
 
81
+ // Bulk versions.
82
  'plugin_bulk_updated' => _x(
83
  'Updated plugin "{plugin_name}" to {plugin_version} from {plugin_prev_version}',
84
  'Plugin was updated in bulk',
85
  'simple-history'
86
  ),
87
 
88
+ // Plugin disabled due to some error.
89
  'plugin_disabled_because_error' => _x(
90
+ 'Deactivated plugin "{plugin_slug}" because of an error ("{deactivation_reason}").',
91
  'Plugin was disabled because of an error',
92
  'simple-history'
93
  ),
94
 
95
+ ), // Messages.
96
+ 'labels' => array(
97
+ 'search' => array(
98
+ 'label' => _x( 'Plugins', 'Plugin logger: search', 'simple-history' ),
99
+ 'label_all' => _x( 'All plugin activity', 'Plugin logger: search', 'simple-history' ),
100
+ 'options' => array(
101
+ _x( 'Activated plugins', 'Plugin logger: search', 'simple-history' ) => array(
102
  'plugin_activated'
103
  ),
104
+ _x( 'Deactivated plugins', 'Plugin logger: search', 'simple-history' ) => array(
105
  'plugin_deactivated',
106
+ 'plugin_disabled_because_error',
107
  ),
108
+ _x( 'Installed plugins', 'Plugin logger: search', 'simple-history' ) => array(
109
  'plugin_installed'
110
  ),
111
+ _x( 'Failed plugin installs', 'Plugin logger: search', 'simple-history' ) => array(
112
  'plugin_installed_failed'
113
  ),
114
+ _x( 'Updated plugins', 'Plugin logger: search', 'simple-history' ) => array(
115
  'plugin_updated',
116
+ 'plugin_bulk_updated',
117
  ),
118
+ _x( 'Failed plugin updates', 'Plugin logger: search', 'simple-history' ) => array(
119
  'plugin_update_failed'
120
  ),
121
+ _x( 'Deleted plugins', 'Plugin logger: search', 'simple-history' ) => array(
122
  'plugin_deleted'
123
  ),
124
+ ),
125
+ ), // search array.
126
+ ), // labels.
127
  );
128
 
129
  return $arr_info;
130
 
131
  }
132
 
133
+ /**
134
+ * Plugin loaded
135
+ */
136
  public function loaded() {
137
 
138
  /**
139
  * At least the plugin bulk upgrades fires this action before upgrade
140
  * We use it to fetch the current version of all plugins, before they are upgraded
141
  */
142
+ add_filter( 'upgrader_pre_install', array( $this, "save_versions_before_update" ), 10, 2 );
143
 
144
  // Clear our transient after an update is done
145
  // Removed because something probably changed in core and this was fired earlier than it used to be
162
  // Detect files removed
163
  add_action( 'setted_transient', array( $this, 'on_setted_transient_for_remove_files' ), 10, 2 );
164
 
165
+ add_action("admin_action_delete-selected", array($this, "on_action_delete_selected" ), 10, 1);
166
 
167
  // Ajax function to get info from GitHub repo. Used by "View plugin info"-link for plugin installs
168
  add_action("wp_ajax_SimplePluginLogger_GetGitHubPluginInfo", array($this, "ajax_GetGitHubPluginInfo"));
169
 
170
  // If the Github Update plugin is not installed we need to get extra fields used by it.
171
  // So need to hook filter "extra_plugin_headers" ourself.
172
+ add_filter( "extra_plugin_headers", function( $arr_headers ) {
173
  $arr_headers[] = "GitHub Plugin URI";
174
  return $arr_headers;
175
  } );
176
 
177
+ // There is no way to use a filter and detect a plugin that is disabled because it can't be found or similar error.
178
  // So we hook into gettext and look for the usage of the error that is returned when this happens.
179
+ add_filter( 'gettext', array( $this, 'on_gettext_detect_plugin_error_deactivation_reason' ), 10, 3 );
180
+ add_filter( 'gettext', array( $this, 'on_gettext' ), 10, 3 );
181
 
182
  }
183
 
184
  /**
185
+ * Things
186
+ *
187
+ * @param string $translation Translation.
188
+ * @param string $text Text.
189
+ * @param string $domain Domin.
190
+ */
191
+ function on_gettext_detect_plugin_error_deactivation_reason( $translation, $text, $domain ) {
192
+
193
+ global $pagenow;
194
+
195
+ // We only act on page plugins.php.
196
+ if ( ! isset( $pagenow ) || 'plugins.php' !== $pagenow ) {
197
+ return $translation;
198
+ }
199
+
200
+ // We only act if the untranslated text is among the following ones
201
+ // (Literally these, no translation).
202
+ $untranslated_texts = array(
203
+ 'Plugin file does not exist.',
204
+ 'Invalid plugin path.',
205
+ 'The plugin does not have a valid header.',
206
+ );
207
+
208
+ if ( ! in_array( $text, $untranslated_texts, true ) ) {
209
+ return $translation;
210
+ }
211
+
212
+ // Text was among our wanted texts.
213
+ switch ( $text ) {
214
+ case 'Plugin file does not exist.':
215
+ $this->latest_plugin_deactivation_because_of_error_reason[] = 'file_does_not_exist';
216
+ break;
217
+ case 'Invalid plugin path.':
218
+ $this->latest_plugin_deactivation_because_of_error_reason[] = 'invalid_path';
219
+ break;
220
+ case 'The plugin does not have a valid header.':
221
+ $this->latest_plugin_deactivation_because_of_error_reason[] = 'no_valid_header';
222
+ break;
223
+ }
224
+
225
+ return $translation;
226
+ }
227
+
228
+ /**
229
+ * There is no way to use a filter and detect a plugin that is disabled because it can't be found or similar error.
230
  * we hook into gettext and look for the usage of the error that is returned when this happens.
231
+ *
232
+ * A plugin gets deactivated when plugins.php is visited function validate_active_plugins()
233
+ * return new WP_Error('plugin_not_found', __('Plugin file does not exist.'));
234
+ * and if invalid plugin is found then this is outputed
235
+ * printf(
236
+ * /* translators: 1: plugin file 2: error message
237
+ * __( 'The plugin %1$s has been <strong>deactivated</strong> due to an error: %2$s' ),
238
+ * '<code>' . esc_html( $plugin_file ) . '</code>',
239
+ * $error->get_error_message() );
240
+ *
241
+ * @param string $translation Translation.
242
+ * @param string $text Text.
243
+ * @param string $domain Domin.
244
  */
245
  function on_gettext( $translation, $text, $domain ) {
246
 
 
 
 
 
 
247
  global $pagenow;
248
 
249
+ // We only act on page plugins.php.
250
  if ( ! isset( $pagenow ) || $pagenow !== "plugins.php" ) {
251
  return $translation;
252
  }
254
  // We only act if the untranslated text is among the following ones
255
  // (Literally these, no translation)
256
  $untranslated_texts = array(
257
+ // This string is called later than the above
258
+ 'The plugin %1$s has been <strong>deactivated</strong> due to an error: %2$s'
 
259
  );
260
 
261
+ if ( ! in_array( $text, $untranslated_texts ) ) {
262
  return $translation;
263
  }
264
 
265
+ // Directly after the string is translated 'esc_html' is called with the plugin name.
266
+ // This is one of the few ways we can get the name of the plugin.
267
+ // The esc_html filter is used pretty much but we make sure we only do our.
268
+ // stuff the first time it's called (directly after the gettet for the plugin disabled-error..).
269
+ $logger_instance = $this;
270
+
271
+ add_filter( 'esc_html', function( $safe_text, $text ) use ( $logger_instance ) {
272
+ static $is_called = false;
273
+
274
+ if ( false === $is_called ) {
275
+ $is_called = true;
276
+
277
+ $deactivation_reason = array_shift( $logger_instance->latest_plugin_deactivation_because_of_error_reason );
278
+
279
+ // We don't know what plugin that was that got this error and currently there does not seem to be a way to determine that.
280
+ // So that's why we use such generic log messages.
281
+ $logger_instance->warningMessage(
282
+ 'plugin_disabled_because_error',
283
+ array(
284
+ '_initiator' => SimpleLoggerLogInitiators::WORDPRESS,
285
+ 'plugin_slug' => $text,
286
+ 'deactivation_reason' => $deactivation_reason,
287
+ )
288
+ );
289
+ }
290
+
291
+ return $safe_text;
292
+ }, 10, 2 );
293
 
294
  return $translation;
295
 
296
  } // on_gettext
297
 
 
298
  /**
299
  * Show readme from github in a modal win
300
  */
loggers/SimplePostLogger.php CHANGED
@@ -288,12 +288,31 @@ class SimplePostLogger extends SimpleLogger
288
  return;
289
  }
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  $this->infoMessage(
292
- "post_deleted",
293
  array(
294
- "post_id" => $post_id,
295
- "post_type" => get_post_type($post),
296
- "post_title" => get_the_title($post)
297
  )
298
  );
299
 
288
  return;
289
  }
290
 
291
+ /*
292
+ Posts that have been in the trash for 30 days (default)
293
+ are deleted using a cron job that is called with action hook "wp_scheduled_delete".
294
+ We skip logging these because users are confused and think that the real post has been
295
+ deleted.
296
+ We detect this by checking $wp_current_filter for 'wp_scheduled_delete'
297
+ [
298
+ "wp_scheduled_delete",
299
+ "delete_post",
300
+ "simple_history\/log_argument\/context"
301
+ ]
302
+ */
303
+ global $wp_current_filter;
304
+ if ( isset( $wp_current_filter ) && is_array( $wp_current_filter ) ) {
305
+ if ( in_array( 'wp_scheduled_delete', $wp_current_filter, true ) ) {
306
+ return;
307
+ }
308
+ }
309
+
310
  $this->infoMessage(
311
+ 'post_deleted',
312
  array(
313
+ 'post_id' => $post_id,
314
+ 'post_type' => get_post_type( $post ),
315
+ 'post_title' => get_the_title( $post ),
316
  )
317
  );
318
 
loggers/SimpleUserLogger.php CHANGED
@@ -196,7 +196,10 @@ class SimpleUserLogger extends SimpleLogger
196
  $arr_keys_to_check = _get_additional_user_keys($user);
197
 
198
  // Somehow some fields are not include above, so add them manually
199
- $arr_keys_to_check = array_merge($arr_keys_to_check, array("user_email", "user_url", "display_name"));
 
 
 
200
 
201
  // Skip some keys, because to much info or I don't know what they are
202
  $arr_keys_to_check = array_diff($arr_keys_to_check, array("use_ssl"));
@@ -248,6 +251,8 @@ class SimpleUserLogger extends SimpleLogger
248
  // Will contain the differences
249
  $user_data_diff = array();
250
 
 
 
251
  // Check all keys for diff values
252
  foreach ($arr_keys_to_check as $one_key_to_check) {
253
  $old_val = $user->$one_key_to_check;
@@ -297,6 +302,7 @@ class SimpleUserLogger extends SimpleLogger
297
  }
298
  }
299
 
 
300
 
301
  $this->infoMessage("user_updated_profile", $context);
302
 
@@ -851,9 +857,11 @@ class SimpleUserLogger extends SimpleLogger
851
  "title" => _x("Website", "User logger", "simple-history")
852
  ),
853
  "role" => array(
854
- //"title" => _x("Display name publicly as", "User logger", "simple-history")
855
  "title" => _x("Role", "User logger", "simple-history")
856
- )
 
 
 
857
  );
858
 
859
  foreach ($arr_user_keys_to_show_diff_for as $key => $val) {
196
  $arr_keys_to_check = _get_additional_user_keys($user);
197
 
198
  // Somehow some fields are not include above, so add them manually
199
+ $arr_keys_to_check = array_merge(
200
+ $arr_keys_to_check,
201
+ array( 'user_email', 'user_url', 'display_name')
202
+ );
203
 
204
  // Skip some keys, because to much info or I don't know what they are
205
  $arr_keys_to_check = array_diff($arr_keys_to_check, array("use_ssl"));
251
  // Will contain the differences
252
  $user_data_diff = array();
253
 
254
+ // locale: sv_SE, empty = english, site-default = site....default!
255
+
256
  // Check all keys for diff values
257
  foreach ($arr_keys_to_check as $one_key_to_check) {
258
  $old_val = $user->$one_key_to_check;
302
  }
303
  }
304
 
305
+ // print_r($context);exit;
306
 
307
  $this->infoMessage("user_updated_profile", $context);
308
 
857
  "title" => _x("Website", "User logger", "simple-history")
858
  ),
859
  "role" => array(
 
860
  "title" => _x("Role", "User logger", "simple-history")
861
+ ),
862
+ "locale" => array(
863
+ "title" => _x("Locale", "User logger", "simple-history")
864
+ ),
865
  );
866
 
867
  foreach ($arr_user_keys_to_show_diff_for as $key => $val) {
readme.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: http://eskapism.se/sida/donate/
4
  Tags: history, log, changes, changelog, audit, trail, pages, attachments, users, dashboard, admin, syslog, feed, activity, stream, audit trail, brute-force
5
  Requires at least: 4.5.1
6
  Tested up to: 4.7
7
- Stable tag: 2.16
8
 
9
  View changes made by users within WordPress. See who created a page, uploaded an attachment or approved an comment, and more.
10
 
@@ -136,7 +136,7 @@ https://github.com/bonny/WordPress-Simple-History
136
  #### Donation & more plugins
137
 
138
  * If you like this plugin don't forget to [donate to support further development](http://eskapism.se/sida/donate/).
139
- * More [WordPress CMS plugins](http://wordpress.org/extend/plugins/profile/eskapism) by the same author.
140
 
141
 
142
  == Screenshots ==
@@ -162,6 +162,19 @@ A simple way to see any uncommon activity, for example an increased number of lo
162
 
163
  ## Changelog
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  = 2.16 (May 2017) =
166
 
167
  - Added [WP-CLI](https://wp-cli.org) command for Simple History. Now you can write `wp simple-history list` to see the latest entries from the history log. For now `list` is the only available command. Let me know if you need more commands!
4
  Tags: history, log, changes, changelog, audit, trail, pages, attachments, users, dashboard, admin, syslog, feed, activity, stream, audit trail, brute-force
5
  Requires at least: 4.5.1
6
  Tested up to: 4.7
7
+ Stable tag: 2.17
8
 
9
  View changes made by users within WordPress. See who created a page, uploaded an attachment or approved an comment, and more.
10
 
136
  #### Donation & more plugins
137
 
138
  * If you like this plugin don't forget to [donate to support further development](http://eskapism.se/sida/donate/).
139
+ * More [WordPress CMS plugins](https://profiles.wordpress.org/eskapism#content-plugins) by the same author.
140
 
141
 
142
  == Screenshots ==
162
 
163
  ## Changelog
164
 
165
+ = 2.17 (June 2017) =
166
+
167
+ - Fix search date range inputs not showing correctly.
168
+ - Change the message for when a plugin is deactivated due to an error. Now the plugin slug is included, so you know exactly what plugin has been deactivated. Also the reason for the deactivation is included (one of "Invalid plugin path", "Plugin file does not exist", or "The plugin does not have a valid header.").
169
+ - Added more filters to log message. Now filter `simple_history_log_debug` exists, together with filters for all other 7 log levels. So you can use `simple_history_log_{loglevel}` where {loglevel} is any of emergency, alert, critical, error, warning, notice, info or debug.
170
+ - Add support for logging the changing of "locale" on a user profile, something that was added in WordPress 4.7.
171
+ - Add sidebar box with link to the settings page.
172
+ - Don't log when old posts are deleted from the trash during cron job wp_scheduled_delete.
173
+ - HHVM is not used for any tests any longer because PHP 7 and Travis not supporting it or something. I dunno. Something like that.
174
+ - When "development debug mode" is activated also log current filters.
175
+ - Show an admin warning if a logger slug is longer than 30 chars.
176
+ - Fix fatal error when calling log() method with null as context argument.
177
+
178
  = 2.16 (May 2017) =
179
 
180
  - Added [WP-CLI](https://wp-cli.org) command for Simple History. Now you can write `wp simple-history list` to see the latest entries from the history log. For now `list` is the only available command. Let me know if you need more commands!