iThemes Security (formerly Better WP Security) - Version 6.9.0

Version Description

  • Enhancement: Updated logging system to keep track of more information and have more options to filter and sort log entries.
  • Enhancement: Improved efficiency of File Change Detection scanning.
  • Bug Fix: Fixed issue that could register loading the logging page as a failed login attempt on some sites.
Download this release

Release Info

Developer chrisjean
Plugin Icon 128x128 iThemes Security (formerly Better WP Security)
Version 6.9.0
Comparing to
See all releases

Code changes from version 6.8.1 to 6.9.0

Files changed (61) hide show
  1. better-wp-security.php +1 -1
  2. core/admin-pages/css/style.css +93 -0
  3. core/admin-pages/init.php +55 -1
  4. core/admin-pages/js/logs.js +279 -0
  5. core/admin-pages/js/settings.js +975 -0
  6. core/admin-pages/js/util.js +171 -0
  7. core/admin-pages/logs-list-table.php +427 -0
  8. core/admin-pages/page-logs.php +323 -153
  9. core/admin-pages/page-settings.php +2 -16
  10. core/core.php +4 -5
  11. core/files.php +1 -1
  12. core/history.txt +10 -2
  13. core/lib.php +21 -74
  14. core/lib/class-itsec-scheduler-cron.php +17 -0
  15. core/lib/class-itsec-scheduler-page-load.php +6 -3
  16. core/lib/class-itsec-scheduler.php +3 -2
  17. core/lib/class-itsec-wp-list-table.php +692 -423
  18. core/lib/debug.php +307 -0
  19. core/lib/form.php +23 -6
  20. core/lib/log-util.php +368 -0
  21. core/lib/log.php +238 -0
  22. core/lib/schema.php +89 -0
  23. core/lockout.php +208 -267
  24. core/logger-all-logs.php +0 -260
  25. core/logger.php +0 -554
  26. core/modules/404-detection/class-itsec-four-oh-four-log.php +0 -233
  27. core/modules/404-detection/class-itsec-four-oh-four.php +5 -77
  28. core/modules/404-detection/logs.php +28 -0
  29. core/modules/away-mode/class-itsec-away-mode.php +3 -38
  30. core/modules/away-mode/logs.php +18 -0
  31. core/modules/backup/class-itsec-backup.php +14 -35
  32. core/modules/backup/js/settings-page.js +17 -17
  33. core/modules/backup/logs.php +38 -0
  34. core/modules/brute-force/class-itsec-brute-force-log.php +0 -186
  35. core/modules/brute-force/class-itsec-brute-force.php +30 -151
  36. core/modules/brute-force/logs.php +47 -0
  37. core/modules/file-change/class-itsec-file-change-log.php +0 -255
  38. core/modules/file-change/class-itsec-file-change.php +0 -98
  39. core/modules/file-change/js/settings-page.js +7 -7
  40. core/modules/file-change/logs.php +56 -0
  41. core/modules/file-change/scanner.php +260 -284
  42. core/modules/global/js/settings-page.js +25 -6
  43. core/modules/global/settings.php +1 -1
  44. core/modules/ipcheck/class-itsec-ipcheck.php +63 -106
  45. core/modules/ipcheck/js/settings-page.js +1 -1
  46. core/modules/ipcheck/logs.php +57 -0
  47. core/modules/malware/class-itsec-malware-log.php +0 -340
  48. core/modules/malware/class-itsec-malware-scan-results-template.php +13 -0
  49. core/modules/malware/class-itsec-malware-scanner.php +161 -56
  50. core/modules/malware/class-itsec-malware.php +0 -74
  51. core/modules/malware/css/malware.css +3 -0
  52. core/modules/malware/css/settings.css +7 -0
  53. core/modules/malware/js/malware.js +1 -1
  54. core/modules/malware/js/settings-page.js +14 -14
  55. core/modules/malware/logs.php +64 -0
  56. core/modules/malware/sync-verbs/itsec-get-malware-scan-log.php +2 -17
  57. core/modules/notification-center/js/settings-page.js +2 -2
  58. core/modules/security-check/js/settings-page.js +3 -3
  59. core/setup.php +8 -12
  60. history.txt +4 -0
  61. readme.txt +9 -4
better-wp-security.php CHANGED
@@ -6,7 +6,7 @@
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
- * Version: 6.8.1
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
+ * Version: 6.9.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
core/admin-pages/css/style.css CHANGED
@@ -755,3 +755,96 @@ ul.itsec-settings-contacts + ul.itsec-settings-contacts {
755
  ul.itsec-settings-contacts li input[type="checkbox"] {
756
  margin-top: 0;
757
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
  ul.itsec-settings-contacts li input[type="checkbox"] {
756
  margin-top: 0;
757
  }
758
+
759
+ /**
760
+ * Styling for the logs page
761
+ */
762
+ @media screen and (max-width: 1200px) {
763
+ body.security_page_itsec-logs div#postbox-container-1 {
764
+ display: none;
765
+ }
766
+ body.security_page_itsec-logs #postbox-container-2 {
767
+ border-right: none !important;
768
+ padding-right: 0 !important;
769
+ }
770
+ body.security_page_itsec-logs #post-body {
771
+ margin-right: 0 !important;
772
+ }
773
+ }
774
+ .itsec-log-entries .dashicons {
775
+ color: #555;
776
+ font-size: 16px;
777
+ padding: 2px 0 0 2px;
778
+ visibility: hidden;
779
+ }
780
+ .itsec-log-entries td:hover .dashicons {
781
+ visibility: visible;
782
+ }
783
+ #itsec-log-details-container {
784
+ display: none;
785
+ }
786
+ #itsec-log-details-container.grid .itsec-module-settings-content {
787
+ max-width: none;
788
+ }
789
+ #itsec-log-details-container.grid .itsec-module-settings-content pre {
790
+ overflow: auto;
791
+ }
792
+ .itsec-log-entries .hidden {
793
+ display: none !important;
794
+ }
795
+ .itsec-logs-color > tbody > .itsec-log-type-critical-issue td,
796
+ .itsec-logs-color > tbody > .itsec-log-type-critical-issue th,
797
+ .itsec-logs-color > tbody > .itsec-log-type-fatal td,
798
+ .itsec-logs-color > tbody > .itsec-log-type-fatal th {
799
+ color: #000;
800
+ }
801
+ .itsec-logs-color > tbody > .itsec-log-type-critical-issue a,
802
+ .itsec-logs-color > tbody > .itsec-log-type-fatal a {
803
+ color: #124B66;
804
+ }
805
+ .itsec-logs-color > tbody > .itsec-log-type-critical-issue,
806
+ .itsec-logs-color > tbody > .itsec-log-type-fatal {
807
+ background-color: #FF7575;
808
+ }
809
+ .itsec-logs-color > tbody > :nth-child(2n+1).itsec-log-type-critical-issue,
810
+ .itsec-logs-color > tbody > :nth-child(2n+1).itsec-log-type-fatal {
811
+ background-color: #FF4848;
812
+ }
813
+ .itsec-logs-color > tbody > .itsec-log-type-error {
814
+ background-color: #FFDFDF;
815
+ }
816
+ .itsec-logs-color > tbody > :nth-child(2n+1).itsec-log-type-error {
817
+ background-color: #FFCECE;
818
+ }
819
+ .itsec-logs-color > tbody > .itsec-log-type-warning {
820
+ background-color: #FFFBDF;
821
+ }
822
+ .itsec-logs-color > tbody > :nth-child(2n+1).itsec-log-type-warning {
823
+ background-color: #FFF9CE;
824
+ }
825
+ .itsec-log-entries .column-description {
826
+ max-width: 30%;
827
+ }
828
+ .tablenav .actions {
829
+ overflow: visible;
830
+ }
831
+ body.security_page_itsec-logs .itsec-module-settings-content-main .form-table td pre {
832
+ margin: 0;
833
+ }
834
+ body.security_page_itsec-logs .itsec-module-settings-content-main .form-table th {
835
+ padding: 10px 10px 10px 0;
836
+ width: 150px;
837
+ }
838
+ body.security_page_itsec-logs .itsec-module-settings-content-main .form-table td {
839
+ padding: 5px 10px;
840
+ }
841
+ body.security_page_itsec-logs .itsec-module-settings-content-main .form-table td .itsec-log-raw-details {
842
+ display: none;
843
+ margin-top: 1em;
844
+ }
845
+ body.security_page_itsec-logs #old-logs-migration-status img {
846
+ vertical-align: middle;
847
+ }
848
+ body.security_page_itsec-logs #old-logs-migration-status p {
849
+ line-height: 1.4em;
850
+ }
core/admin-pages/init.php CHANGED
@@ -2,8 +2,11 @@
2
 
3
 
4
  final class ITSEC_Admin_Page_Loader {
 
 
5
  private $page_refs = array();
6
  private $page_id;
 
7
 
8
 
9
  public function __construct() {
@@ -22,6 +25,48 @@ final class ITSEC_Admin_Page_Loader {
22
  add_filter( 'itsec-user-setting-valid-itsec-settings-view', array( $this, 'validate_view' ), null, 2 );
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  public function add_admin_pages() {
26
  $capability = ITSEC_Core::get_required_cap();
27
  $page_refs = array();
@@ -65,6 +110,9 @@ final class ITSEC_Admin_Page_Loader {
65
  }
66
 
67
  public function load() {
 
 
 
68
  $this->load_file( 'page-%s.php' );
69
  }
70
 
@@ -90,9 +138,15 @@ final class ITSEC_Admin_Page_Loader {
90
  $id = $this->get_page_id();
91
 
92
  if ( empty( $id ) ) {
93
- return;
 
 
 
 
94
  }
95
 
 
 
96
  $file = dirname( __FILE__ ) . '/' . sprintf( $file, $id );
97
 
98
  if ( is_file( $file ) ) {
2
 
3
 
4
  final class ITSEC_Admin_Page_Loader {
5
+ private $version = 2.0;
6
+
7
  private $page_refs = array();
8
  private $page_id;
9
+ private $translations = array();
10
 
11
 
12
  public function __construct() {
25
  add_filter( 'itsec-user-setting-valid-itsec-settings-view', array( $this, 'validate_view' ), null, 2 );
26
  }
27
 
28
+ public function add_scripts() {
29
+ $this->set_translation_strings();
30
+
31
+ $vars = array(
32
+ 'ajax_action' => 'itsec_settings_page',
33
+ 'ajax_nonce' => wp_create_nonce( 'itsec-settings-nonce' ),
34
+ 'translations' => $this->translations,
35
+ );
36
+
37
+ wp_enqueue_script( 'itsec-util-script', plugins_url( 'js/util.js', __FILE__ ), array(), $this->version, true );
38
+ wp_localize_script( 'itsec-util-script', 'itsec_util', $vars );
39
+ }
40
+
41
+ public function add_styles() {
42
+ wp_enqueue_style( 'itsec-settings-page-style', plugins_url( 'css/style.css', __FILE__ ), array(), $this->version );
43
+ }
44
+
45
+ private function set_translation_strings() {
46
+ $this->translations = array(
47
+ 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
48
+
49
+ 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
50
+
51
+ 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
52
+
53
+ 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
54
+
55
+ 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
56
+
57
+ 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
58
+
59
+ 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
60
+ );
61
+
62
+ foreach ( $this->translations as $key => $message ) {
63
+ if ( is_wp_error( $message ) ) {
64
+ $messages = ITSEC_Response::get_error_strings( $message );
65
+ $this->translations[$key] = $messages[0];
66
+ }
67
+ }
68
+ }
69
+
70
  public function add_admin_pages() {
71
  $capability = ITSEC_Core::get_required_cap();
72
  $page_refs = array();
110
  }
111
 
112
  public function load() {
113
+ add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
114
+ add_action( 'admin_print_styles', array( $this, 'add_styles' ) );
115
+
116
  $this->load_file( 'page-%s.php' );
117
  }
118
 
138
  $id = $this->get_page_id();
139
 
140
  if ( empty( $id ) ) {
141
+ if ( isset( $GLOBALS['pagenow'] ) && 'admin.php' === $GLOBALS['pagenow'] && isset( $_GET['page'] ) && 'itsec-' === substr( $_GET['page'], 0, 6 ) ) {
142
+ $id = substr( $_GET['page'], 6 );
143
+ } else {
144
+ return;
145
+ }
146
  }
147
 
148
+ $id = str_replace( '_', '-', $id );
149
+
150
  $file = dirname( __FILE__ ) . '/' . sprintf( $file, $id );
151
 
152
  if ( is_file( $file ) ) {
core/admin-pages/js/logs.js ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var itsecLogsPage = {
4
+ init: function() {
5
+ this.bindEvents();
6
+
7
+ var id = itsecUtil.getUrlParameter( 'id' );
8
+
9
+ if ( false !== id ) {
10
+ itsecLogsPage.showLog( id );
11
+ }
12
+
13
+ itsecLogsPage.originalHREF = jQuery( '.itsec-module-cards-container .subsubsub a.current' ).attr( 'href' );
14
+
15
+ this.migrateOldLogs();
16
+ },
17
+
18
+ bindEvents: function() {
19
+ var $container = jQuery( '#wpcontent' );
20
+
21
+ $container.on( 'click', '.itsec-logs-view-details', this.showModal );
22
+ $container.on( 'click', '.itsec-close-modal, .itsec-modal-background', this.closeModal );
23
+ $container.on( 'click', '.itsec-log-raw-details-toggle', this.toggleRawDetails );
24
+ $container.on( 'keyup', this.closeModal );
25
+ },
26
+
27
+ toggleRawDetails: function( e ) {
28
+ e.preventDefault();
29
+
30
+ if ( jQuery( '.itsec-log-raw-details' ).is( ':visible' ) ) {
31
+ jQuery( this ).html( itsec_page.translations.show_raw_details );
32
+ jQuery( '.itsec-log-raw-details' ).hide();
33
+ } else {
34
+ jQuery( this ).html( itsec_page.translations.hide_raw_details );
35
+ jQuery( '.itsec-log-raw-details' ).show();
36
+ }
37
+ },
38
+
39
+ showModal: function( e ) {
40
+ e.preventDefault();
41
+
42
+ var id = jQuery( this ).parent().parent().find( 'td.column-id' ).html();
43
+
44
+ try {
45
+ if ( '' != itsecLogsPage.originalHREF ) {
46
+ window.history.replaceState( {}, '', itsecLogsPage.originalHREF + '&id=' + id );
47
+ }
48
+ } catch( err ) {}
49
+
50
+ itsecLogsPage.showLog( id );
51
+ },
52
+
53
+ showLog: function( id ) {
54
+ var $modalBackground = jQuery( '.itsec-modal-background' ),
55
+ $detailsContainer = jQuery( '#itsec-log-details-container' );
56
+
57
+ jQuery( '#itsec-log-details-container .itsec-module-messages-container' ).html( '' );
58
+ jQuery( '#itsec-log-details-container .itsec-module-settings-content-main' ).html( itsec_page.translations.loading );
59
+
60
+ $modalBackground.fadeIn();
61
+ $detailsContainer.fadeIn( 200 );
62
+
63
+ jQuery( 'body' ).addClass( 'itsec-modal-open' );
64
+
65
+ var $cached_data = jQuery( '#itsec-logs-cache-id-' + id );
66
+
67
+ if ( $cached_data.length ) {
68
+ jQuery( '#itsec-log-details-container' ).html( $cached_data.html() );
69
+ jQuery( '.itsec-log-raw-details' ).hide();
70
+ return;
71
+ }
72
+
73
+ var postData = {
74
+ 'action': itsec_page.ajax_action,
75
+ 'nonce': itsec_page.ajax_nonce,
76
+ 'id': id,
77
+ };
78
+
79
+ jQuery.post( ajaxurl, postData )
80
+ .always(function( a, status, b ) {
81
+ itsecLogsPage.updateDetails( a, status, b, id );
82
+ });
83
+ },
84
+
85
+ updateDetails: function( a, status, b, id ) {
86
+ var results = {
87
+ 'id': id,
88
+ 'status': status,
89
+ 'jqxhr': null,
90
+ 'success': false,
91
+ 'response': null,
92
+ 'errors': [],
93
+ 'warnings': [],
94
+ 'messages': [],
95
+ 'functionCalls': [],
96
+ 'redirect': false,
97
+ 'closeModal': true
98
+ };
99
+
100
+
101
+ if ( 'ITSEC_Response' === a.source && 'undefined' !== a.response ) {
102
+ // Successful response with a valid format.
103
+ results.jqxhr = b;
104
+ results.success = a.success;
105
+ results.response = a.response;
106
+ results.errors = a.errors;
107
+ results.warnings = a.warnings;
108
+ results.messages = a.messages;
109
+ results.functionCalls = a.functionCalls;
110
+ results.redirect = a.redirect;
111
+ results.closeModal = a.closeModal;
112
+ } else if ( a.responseText ) {
113
+ // Failed response.
114
+ results.jqxhr = a;
115
+ var errorThrown = b;
116
+
117
+ if ( 'undefined' === typeof results.jqxhr.status ) {
118
+ results.jqxhr.status = -1;
119
+ }
120
+
121
+ if ( 'timeout' === status ) {
122
+ var error = itsec_page.translations.ajax_timeout;
123
+ } else if ( 'parsererror' === status ) {
124
+ var error = itsec_page.translations.ajax_parsererror;
125
+ } else if ( 403 == results.jqxhr.status ) {
126
+ var error = itsec_page.translations.ajax_forbidden;
127
+ } else if ( 404 == results.jqxhr.status ) {
128
+ var error = itsec_page.translations.ajax_not_found;
129
+ } else if ( 500 == results.jqxhr.status ) {
130
+ var error = itsec_page.translations.ajax_server_error;
131
+ } else {
132
+ var error = itsec_page.translations.ajax_unknown;
133
+ }
134
+
135
+ error = error.replace( '%1$s', status );
136
+ error = error.replace( '%2$s', errorThrown );
137
+
138
+ results.errors = [ error ];
139
+ } else {
140
+ // Successful response with an invalid format.
141
+ results.jqxhr = b;
142
+
143
+ results.response = a;
144
+ results.errors = [ itsec_page.translations.ajax_invalid ];
145
+ }
146
+
147
+
148
+ if ( results.redirect ) {
149
+ window.location = results.redirect;
150
+ }
151
+
152
+
153
+ var $messages = jQuery( '#itsec-log-details-container .itsec-module-messages-container' ),
154
+ $content = jQuery( '#itsec-log-details-container .itsec-module-settings-content-main' );
155
+
156
+ $messages.html( '' );
157
+
158
+ for ( var i = 0; i < results.errors.length; i++ ) {
159
+ $messages.append( '<div class="error inline"><p><strong>' + results.errors[i] + '</strong></p></div>' );
160
+ }
161
+ for ( var i = 0; i < results.warnings.length; i++ ) {
162
+ $messages.append( '<div class="warning inline"><p><strong>' + results.warnings[i] + '</strong></p></div>' );
163
+ }
164
+ for ( var i = 0; i < results.messages.length; i++ ) {
165
+ $messages.append( '<div class="info inline"><p><strong>' + results.messages[i] + '</strong></p></div>' );
166
+ }
167
+
168
+ $content.html( results.response );
169
+
170
+
171
+ var $div = jQuery( '<div>', {id: 'itsec-logs-cache-id-' + id} );
172
+ $div.html( jQuery( '#itsec-log-details-container' ).html() );
173
+
174
+ jQuery( '#itsec-logs-cache' ).append( $div );
175
+ },
176
+
177
+ closeModal: function( e ) {
178
+ if ( 'undefined' !== typeof e ) {
179
+ e.preventDefault();
180
+
181
+ // For keyup events, only process esc
182
+ if ( 'keyup' === e.type && 27 !== e.which ) {
183
+ return;
184
+ }
185
+ }
186
+
187
+
188
+ try {
189
+ if ( '' != itsecLogsPage.originalHREF ) {
190
+ window.history.replaceState( {}, '', itsecLogsPage.originalHREF );
191
+ }
192
+ } catch( err ) {}
193
+
194
+
195
+ jQuery( '.itsec-modal-background' ).fadeOut();
196
+ jQuery( '#itsec-log-details-container' ).fadeOut( 200 );
197
+ jQuery( 'body' ).removeClass( 'itsec-modal-open' );
198
+ },
199
+
200
+ migrateOldLogs: function() {
201
+ var $status = jQuery( '#old-logs-migration-status' );
202
+
203
+ if ( $status.length < 1 ) {
204
+ return;
205
+ }
206
+
207
+ var message = itsec_page.translations.log_migration_started.replace( '%1$s', '<img src="' + itsec_page.translations.log_migration_loading_url + '" />' );
208
+
209
+ $status.append( '<div class="notice notice-info notice-alt"><p><strong>' + message + '</strong></p></div>' );
210
+
211
+ itsecLogsPage.sendMigrationRequest();
212
+ },
213
+
214
+ handleMigrationCallback: function( results ) {
215
+ var clearStatus = false;
216
+
217
+ if ( results.response && results.response.length ) {
218
+ if ( 'incomplete' === results.response ) {
219
+ if ( 'undefined' === typeof itsecLogsPage.callCount ) {
220
+ itsecLogsPage.callCount = 1;
221
+ }
222
+
223
+ itsecLogsPage.callCount++;
224
+
225
+ if ( 0 === itsecLogsPage.callCount % 10 ) {
226
+ // Every 10 requests, delay a bit to prevent from slamming the server.
227
+ setTimeout( itsecLogsPage.sendMigrationRequest, 5000 );
228
+ } else {
229
+ itsecLogsPage.sendMigrationRequest();
230
+ }
231
+
232
+ return;
233
+ }
234
+
235
+ jQuery('#old-logs-migration-status').html( results.response );
236
+ } else {
237
+ clearStatus = true;
238
+ }
239
+
240
+ if ( results.errors.length > 0 ) {
241
+ if ( 'undefined' === typeof itsecLogsPage.errorCount ) {
242
+ itsecLogsPage.errorCount = 0;
243
+ }
244
+
245
+ itsecLogsPage.errorCount++;
246
+
247
+ if ( itsecLogsPage.errorCount < 10 ) {
248
+ // Keep retrying until we reach 10 errors, but delay a bit before retrying.
249
+ setTimeout( itsecLogsPage.sendMigrationRequest, 5000 );
250
+
251
+ return;
252
+ }
253
+ }
254
+
255
+ if ( clearStatus ) {
256
+ jQuery('#old-logs-migration-status').html( '' );
257
+ }
258
+
259
+ if ( results.errors.length > 0 ) {
260
+ var message = '<div class="notice notice-error notice-alt"><p><strong>' + itsec_page.translations.log_migration_failed + '</strong></p></div>';
261
+ jQuery('#old-logs-migration-status').append( message );
262
+ }
263
+
264
+ if ( results.warnings.length > 0 ) {
265
+ jQuery.each( results.warnings, function( index, warning ) {
266
+ message = '<div class="notice notice-warning notice-alt"><p>' + warning + '</p></div>';
267
+ jQuery('#old-logs-migration-status').append( message );
268
+ } );
269
+ }
270
+ },
271
+
272
+ sendMigrationRequest: function() {
273
+ itsecUtil.sendAJAXRequest( 'logs', 'handle_logs_migration', {}, itsecLogsPage.handleMigrationCallback, itsec_page.ajax_action, itsec_page.ajax_nonce );
274
+ }
275
+ };
276
+
277
+ jQuery(document).ready(function( $ ) {
278
+ itsecLogsPage.init();
279
+ });
core/admin-pages/js/settings.js ADDED
@@ -0,0 +1,975 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var itsecSettingsPage = {
4
+
5
+ events: jQuery( {} ),
6
+
7
+ init: function() {
8
+ jQuery( '.itsec-module-settings-container' ).hide();
9
+
10
+ this.bindEvents();
11
+
12
+ jQuery( '.itsec-settings-view-toggle .itsec-selected' ).removeClass( 'itsec-selected' ).trigger( 'click' );
13
+ jQuery( '.itsec-settings-toggle' ).trigger( 'change' );
14
+
15
+ this.initFilters();
16
+ this.initCurrentModule();
17
+ this.makeNoticesDismissible();
18
+ },
19
+
20
+ initFilters: function() {
21
+ var module_type = itsecUtil.getUrlParameter( 'module_type' );
22
+ if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
23
+ module_type = 'recommended';
24
+ }
25
+ jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) + ' a' ).trigger( 'click' );
26
+ },
27
+
28
+ initCurrentModule: function() {
29
+
30
+ var module = itsecUtil.getUrlParameter( 'module' );
31
+ if ( 'string' === typeof module ) {
32
+ jQuery( '#itsec-module-card-' + module.replace( /[^\w-]/g, '' ) + ' button.itsec-toggle-settings' ).trigger( 'click' );
33
+ }
34
+ },
35
+
36
+ bindEvents: function() {
37
+
38
+ if ( itsecSettingsPage.bindEvents.bound ) {
39
+ return;
40
+ }
41
+
42
+ jQuery(window).on("popstate", function(e, data) {
43
+ if ( null !== e.originalEvent.state && 'string' == typeof e.originalEvent.state.module && '' !== e.originalEvent.state.module.replace( /[^\w-]/g, '' ) ) {
44
+ jQuery( '#itsec-module-card-' + e.originalEvent.state.module.replace( /[^\w-]/g, '' ) + ' button.itsec-toggle-settings' ).trigger( 'itsec-popstate' );
45
+ } else {
46
+ itsecSettingsPage.closeGridSettingsModal( e );
47
+ }
48
+
49
+ if ( null !== e.originalEvent.state && 'string' == typeof e.originalEvent.state.module_type && '' !== e.originalEvent.state.module_type.replace( /[^\w-]/g, '' ) ) {
50
+ jQuery( '#itsec-module-filter-' + e.originalEvent.state.module_type.replace( /[^\w-]/g, '' ) + ' a' ).trigger( 'itsec-popstate' );
51
+ }
52
+ });
53
+
54
+ var $container = jQuery( '#wpcontent' );
55
+
56
+ $container.on( 'click', '.itsec-module-filter a', this.filterView );
57
+ $container.on( 'itsec-popstate', '.itsec-module-filter a', this.filterView );
58
+ $container.on( 'click', '.itsec-settings-view-toggle a', this.toggleView );
59
+ // $container.on( 'click', '.itsec-toggle-settings, .itsec-module-card-content h2', this.toggleSettings );
60
+ $container.on( 'click', 'a[data-module-link]', this.openModuleFromLink );
61
+ $container.on( 'click', '.list .itsec-module-card:not(.itsec-module-pro-upsell) .itsec-module-card-content, .itsec-toggle-settings, .itsec-module-settings-cancel', this.toggleSettings );
62
+ $container.on( 'itsec-popstate', '.list .itsec-module-card-content, .itsec-toggle-settings', this.toggleSettings );
63
+ $container.on( 'click', '.itsec-close-modal, .itsec-modal-background', this.closeGridSettingsModal );
64
+ $container.on( 'keyup', this.closeGridSettingsModal );
65
+ $container.on( 'click', '.itsec-toggle-activation', this.toggleModuleActivation );
66
+ $container.on( 'click', '.itsec-module-settings-save', this.saveSettings );
67
+ $container.on( 'click', '.itsec-reload-module', this.reloadModule );
68
+ $container.on( 'click', '.itsec-details-toggle-container a[href="#"]', this.toggleDetails );
69
+
70
+ $container.on( 'change', '#itsec-filter', this.logPageChangeFilter );
71
+
72
+ // For use by module content to show/hide settings sections based upon an input.
73
+ $container.on( 'change', '.itsec-settings-toggle', this.toggleModuleContent );
74
+ $container.on( 'click', '.itsec-copy-trigger', this.handleCopy );
75
+
76
+ itsecSettingsPage.bindEvents.bound = true;
77
+ },
78
+
79
+ toggleDetails: function( e ) {
80
+ e.preventDefault();
81
+
82
+ var $details = jQuery(this).parent().find( '.itsec-details-toggle-details' ).toggleClass( 'hide-if-js' );
83
+
84
+ if ( $details.hasClass( 'hide-if-js' ) ) {
85
+ jQuery(this).html( itsec_page.translations.show_information );
86
+ } else {
87
+ jQuery(this).html( itsec_page.translations.hide_description );
88
+ }
89
+ },
90
+
91
+ logPageChangeFilter: function( e ) {
92
+ var filter = jQuery( this ).val();
93
+ var url = itsec_page.logs_page_url + '&filter=' + filter;
94
+ window.location.href = url;
95
+ },
96
+
97
+ toggleModuleContent: function( e ) {
98
+ if ( 'checkbox' === jQuery(this).attr( 'type' ) ) {
99
+ var show = jQuery(this).prop( 'checked' );
100
+ } else {
101
+ var show = ( jQuery(this).val() ) ? true : false;
102
+ }
103
+
104
+ var $content = jQuery( '.' + jQuery(this).attr( 'id' ) + '-content' );
105
+
106
+ if ( show ) {
107
+ $content.show();
108
+
109
+
110
+ var $container = jQuery( '.itsec-module-cards-container' );
111
+
112
+ if ( $container.hasClass( 'grid' ) ) {
113
+ var $modal = jQuery(this).parents( '.itsec-module-settings-content-container' );
114
+ var scrollOffset = $modal.scrollTop() + jQuery(this).parent().position().top;
115
+
116
+ $modal.animate( {'scrollTop': scrollOffset}, 'slow' );
117
+ }
118
+ } else {
119
+ $content.hide();
120
+ }
121
+ },
122
+
123
+ handleCopy: function( e ) {
124
+
125
+ e.preventDefault();
126
+
127
+ var $trigger = jQuery( e.currentTarget );
128
+ var fromId = $trigger.data( 'copy-from' );
129
+
130
+ if ( ! fromId.length ) {
131
+ return;
132
+ }
133
+
134
+ var el = document.getElementById( fromId );
135
+
136
+ var removeSelect = itsecSettingsPage.selectText( el );
137
+
138
+ try {
139
+
140
+ document.execCommand( 'copy' );
141
+ removeSelect();
142
+ $trigger.text( itsec_page.translations.copied );
143
+
144
+ } catch ( e ) {
145
+ var $p = jQuery( '<p></p>' ).text( itsec_page.translations.copy_instruction ),
146
+ $notice = jQuery( '<div class="notice notice-alt notice-info"></div>' ).append( $p ),
147
+ $el = jQuery( el );
148
+
149
+ $trigger.after( $notice );
150
+
151
+ var removeNotice = function () {
152
+ $notice.fadeOut( function () {
153
+ $notice.remove();
154
+ } );
155
+ };
156
+ var copy = function () {
157
+
158
+ setTimeout( function () {
159
+ removeNotice();
160
+ removeSelect();
161
+ }, 100 );
162
+
163
+ $el.off( 'copy', copy );
164
+
165
+ return true;
166
+ };
167
+
168
+ $el.on( 'copy', copy );
169
+
170
+ setTimeout( removeNotice, 5000 );
171
+ }
172
+ },
173
+
174
+ // https://stackoverflow.com/a/987376
175
+ selectText: function( element ) {
176
+ var doc = document, text = element, range, selection;
177
+
178
+ if ( doc.body.createTextRange ) { // ie
179
+ range = document.body.createTextRange();
180
+ range.moveToElementText( text );
181
+ range.select();
182
+ } else if ( window.getSelection ) {
183
+ selection = window.getSelection();
184
+ range = document.createRange();
185
+ range.selectNodeContents( text );
186
+ selection.removeAllRanges();
187
+ selection.addRange( range );
188
+ }
189
+
190
+ return function() {
191
+ if ( selection ) {
192
+ selection.removeAllRanges();
193
+ } else {
194
+ range.collapse();
195
+ }
196
+ };
197
+ },
198
+
199
+ saveSettings: function( e ) {
200
+ e.preventDefault();
201
+
202
+ var $button = jQuery(this);
203
+
204
+ if ( $button.hasClass( 'itsec-module-settings-save' ) ) {
205
+ var module = $button.parents( '.itsec-module-card' ).attr( 'id' ).replace( 'itsec-module-card-', '' );
206
+ } else {
207
+ var module = '';
208
+ }
209
+
210
+ $button.prop( 'disabled', true );
211
+
212
+ var data = {
213
+ '--itsec-form-serialized-data': jQuery( '#itsec-module-settings-form' ).serialize()
214
+ };
215
+
216
+ itsecUtil.sendAJAXRequest( module, 'save', data, itsecSettingsPage.saveSettingsCallback );
217
+ },
218
+
219
+ saveSettingsCallback: function( results ) {
220
+ if ( '' === results.module ) {
221
+ jQuery( '#itsec-save' ).prop( 'disabled', false );
222
+ } else {
223
+ jQuery( '#itsec-module-card-' + results.module + ' button.itsec-module-settings-save' ).prop( 'disabled', false );
224
+ }
225
+
226
+ var $container = jQuery( '.itsec-module-cards-container' );
227
+
228
+ if ( $container.hasClass( 'grid' ) ) {
229
+ var view = 'grid';
230
+ } else {
231
+ var view = 'list';
232
+ }
233
+
234
+ itsecSettingsPage.clearMessages();
235
+
236
+ if ( results.errors.length > 0 || results.warnings.length > 0 || ! results.closeModal ) {
237
+ itsecSettingsPage.showErrors( results.errors, results.module, 'open' );
238
+ itsecSettingsPage.showErrors( results.warnings, results.module, 'open', 'warning' );
239
+ itsecSettingsPage.showMessages( results.messages, results.module, 'open' );
240
+ itsecSettingsPage.showMessages( results.infos, results.module, 'open', 'info' );
241
+
242
+ if ( 'grid' === view ) {
243
+ $container.find( '.itsec-module-settings-content-container:visible' ).animate( {'scrollTop': 0}, 'fast' );
244
+ }
245
+
246
+ if ( 'list' === view ) {
247
+ jQuery(document).scrollTop( 0 );
248
+ }
249
+ } else {
250
+ itsecSettingsPage.showMessages( results.messages, results.module, 'closed' );
251
+ itsecSettingsPage.showMessages( results.infos, results.module, 'closed', 'info' );
252
+
253
+ if ( 'grid' === view ) {
254
+ $container.find( '.itsec-module-settings-content-container:visible' ).scrollTop( 0 );
255
+ itsecSettingsPage.closeGridSettingsModal();
256
+ }
257
+ }
258
+ },
259
+
260
+ clearMessages: function() {
261
+ jQuery( '#itsec-settings-messages-container, .itsec-module-messages-container' ).empty();
262
+ },
263
+
264
+ showErrors: function( errors, module, containerStatus, type ) {
265
+ jQuery.each( errors, function( index, error ) {
266
+ itsecSettingsPage.showError( error, module, containerStatus, type );
267
+ } );
268
+ },
269
+
270
+ showError: function( error, module, containerStatus, type ) {
271
+
272
+ type = type || 'error';
273
+
274
+ if ( jQuery( '.itsec-module-cards-container' ).hasClass( 'grid' ) ) {
275
+ var view = 'grid';
276
+ } else {
277
+ var view = 'list';
278
+ }
279
+
280
+ if ( 'closed' !== containerStatus && 'open' !== containerStatus ) {
281
+ containerStatus = 'closed';
282
+ }
283
+
284
+ if ( 'string' !== typeof module ) {
285
+ module = '';
286
+ }
287
+
288
+
289
+ if ( 'closed' === containerStatus || '' === module ) {
290
+ var container = jQuery( '#itsec-settings-messages-container' );
291
+
292
+ if ( '' === module ) {
293
+ container.addClass( 'no-module' );
294
+ }
295
+ } else {
296
+ var container = jQuery( '#itsec-module-card-' + module + ' .itsec-module-messages-container' );
297
+ }
298
+
299
+ var $notice = jQuery( '<div class="notice"><p><strong>' + error + '</strong></p></div>' );
300
+ $notice.addClass( 'notice-' + type );
301
+
302
+ if ( containerStatus === 'open' || module.length ) {
303
+ $notice.addClass( 'notice-alt' );
304
+ }
305
+
306
+ container.append( $notice ).addClass( 'visible' );
307
+ },
308
+
309
+ showMessages: function( messages, module, containerStatus, type ) {
310
+ jQuery.each( messages, function( index, message ) {
311
+ itsecSettingsPage.showMessage( message, module, containerStatus, type );
312
+ } );
313
+ },
314
+
315
+ showMessage: function( message, module, containerStatus, type ) {
316
+
317
+ type = type || 'success';
318
+
319
+ if ( jQuery( '.itsec-module-cards-container' ).hasClass( 'grid' ) ) {
320
+ var view = 'grid';
321
+ } else {
322
+ var view = 'list';
323
+ }
324
+
325
+ if ( 'closed' !== containerStatus && 'open' !== containerStatus ) {
326
+ containerStatus = 'closed';
327
+ }
328
+
329
+ if ( 'string' !== typeof module ) {
330
+ module = '';
331
+ }
332
+
333
+
334
+ if ( 'closed' === containerStatus || '' === module ) {
335
+ var container = jQuery( '#itsec-settings-messages-container' );
336
+
337
+ var dismiss = function () {
338
+
339
+ if ( container.is( ':hover' ) ) {
340
+ return setTimeout( dismiss, 2000 );
341
+ }
342
+
343
+ container.removeClass( 'visible' );
344
+ setTimeout( function () {
345
+ container.find( 'div' ).remove();
346
+ }, 500 );
347
+ };
348
+
349
+ setTimeout( dismiss, 4000 );
350
+ } else {
351
+ var container = jQuery( '#itsec-module-card-' + module + ' .itsec-module-messages-container' );
352
+ }
353
+
354
+ var $notice = jQuery( '<div class="notice fade"><p><strong>' + message + '</strong></p></div>' );
355
+ $notice.addClass( 'notice-' + type );
356
+
357
+ if ( containerStatus === 'open' || module.length ) {
358
+ $notice.addClass( 'notice-alt' );
359
+ }
360
+
361
+ container.append( $notice ).addClass( 'visible' );
362
+ },
363
+
364
+ filterView: function( e ) {
365
+ e.preventDefault();
366
+
367
+ var $activeLink = jQuery(this),
368
+ $oldLink = $activeLink.parents( '.itsec-feature-tabs' ).find( '.current' ),
369
+ type = $activeLink.parent().attr( 'id' ).substr( 20 );
370
+
371
+ $oldLink.removeClass( 'current' );
372
+ $activeLink.addClass( 'current' );
373
+
374
+ if ( 'all' === type ) {
375
+ jQuery( '.itsec-module-card' ).show();
376
+ } else {
377
+ jQuery( '.itsec-module-type-' + type ).show();
378
+ jQuery( '.itsec-module-card' ).not( '.itsec-module-type-' + type ).hide();
379
+ }
380
+
381
+ // We use this to avoid pushing a new state when we're trying to handle a popstate
382
+ if ( 'itsec-popstate' !== e.type ) {
383
+ var url = '?page=itsec&module_type=' + type;
384
+ var module = itsecUtil.getUrlParameter( 'module' );
385
+ if ( 'string' === typeof module ) {
386
+ url += '&module=' + module;
387
+ }
388
+
389
+ window.history.pushState( {'module_type':type}, type, url );
390
+ }
391
+ },
392
+
393
+ toggleView: function( e ) {
394
+ e.preventDefault();
395
+
396
+ var $self = jQuery(this);
397
+
398
+ if ( $self.hasClass( 'itsec-selected' ) ) {
399
+ // Do nothing if already selected.
400
+ return;
401
+ }
402
+
403
+ var $view = $self.attr( 'class' ).replace( 'itsec-', '' );
404
+
405
+ $self.addClass( 'itsec-selected' ).siblings().removeClass( 'itsec-selected' );
406
+ jQuery( '.itsec-module-settings-container' ).hide();
407
+
408
+ jQuery( '.itsec-toggle-settings' ).each(function( index ) {
409
+ var $button = jQuery( this );
410
+
411
+ if ( $button.parents( '.itsec-module-card' ).hasClass( 'itsec-module-type-enabled' ) && ! $button.hasClass( 'information-only' ) ) {
412
+ $button.html( itsec_page.translations.show_settings );
413
+ } else if ( $button.hasClass( 'information-only' ) ) {
414
+ $button.html( itsec_page.translations.information_only );
415
+ } else {
416
+ $button.html( itsec_page.translations.show_description );
417
+ }
418
+ });
419
+
420
+ var $cardContainer = jQuery( '.itsec-module-cards-container' );
421
+ jQuery.post( ajaxurl, {
422
+ 'action': 'itsec-set-user-setting',
423
+ 'itsec-user-setting-nonce': $self.parent().data( 'nonce' ),
424
+ 'setting': 'itsec-settings-view',
425
+ 'value': $view
426
+ } );
427
+
428
+ $cardContainer.fadeOut( 100, function() {
429
+ $cardContainer.removeClass( 'grid list' ).addClass( $view );
430
+ } );
431
+ $cardContainer.fadeIn( 100 );
432
+ },
433
+
434
+ openModuleFromLink: function( e ) {
435
+
436
+ var $link = jQuery( this ), module = $link.data( 'module-link' ),
437
+ $module = jQuery( '.itsec-module-card[data-module-id="' + module + '"]' ),
438
+ highlight = $link.data( 'highlight-setting-id' );
439
+
440
+ if ( ! $module.length ) {
441
+ return; // safety check
442
+ }
443
+
444
+ e.preventDefault();
445
+
446
+ jQuery( '.itsec-module-settings-container:visible' ).hide();
447
+
448
+ var $listClassElement = $module.parents( '.itsec-module-cards-container' ),
449
+ $toggleButton = $module.find( '.itsec-toggle-settings' );
450
+
451
+ if ( highlight && highlight.length ) {
452
+ jQuery( 'label[for="' + highlight + '"]', $module ).parents( 'tr' ).addClass( 'itsec-highlighted-setting' );
453
+ }
454
+
455
+ if ( $listClassElement.hasClass( 'list' ) ) {
456
+ itsecSettingsPage.toggleListSettingsCard.call( $toggleButton, e );
457
+ } else if ( $listClassElement.hasClass( 'grid' ) ) {
458
+ itsecSettingsPage.showGridSettingsModal.call( $toggleButton, e );
459
+ }
460
+
461
+ var type = $module.hasClass( 'itsec-module-type-advanced' ) ? 'advanced' : 'recommended';
462
+
463
+ window.history.pushState( {module: module}, module, '?page=itsec&module=' + module + '&module_type=' + type );
464
+
465
+ var href = $link.attr( 'href' );
466
+
467
+ if ( href && href.length > 1 && href.charAt( 0 ) === '#' ) {
468
+ setTimeout( function () {
469
+ jQuery( '.itsec-module-settings-content-container', '#itsec-module-card-notification-center' ).scrollTo( jQuery( href ), 'swing', { offset: -30 } );
470
+ }, 350 );
471
+ }
472
+ },
473
+
474
+ toggleSettings: function( e ) {
475
+ e.stopPropagation();
476
+
477
+ var $listClassElement = jQuery(e.currentTarget).parents( '.itsec-module-cards-container' );
478
+
479
+ if ( $listClassElement.hasClass( 'list') ) {
480
+ itsecSettingsPage.toggleListSettingsCard.call( this, e );
481
+ } else if ( $listClassElement.hasClass( 'grid' ) ) {
482
+ itsecSettingsPage.showGridSettingsModal.call( this, e );
483
+ }
484
+
485
+ // We use this to avoid pushing a new state when we're trying to handle a popstate
486
+ if ( 'itsec-popstate' !== e.type ) {
487
+ var module_id = jQuery(this).closest('.itsec-module-card').data( 'module-id' );
488
+
489
+ var module_type = itsecUtil.getUrlParameter( 'module_type' );
490
+ if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
491
+ module_type = 'recommended';
492
+ }
493
+ window.history.pushState( {'module':module_id}, module_id, '?page=itsec&module=' + module_id + '&module_type=' + module_type );
494
+ }
495
+ },
496
+
497
+ toggleListSettingsCard: function( e ) {
498
+ e.preventDefault();
499
+
500
+ var $container = jQuery(this);
501
+
502
+ if ( ! $container.hasClass( 'itsec-module-card-content' ) ) {
503
+ $container = $container.parents( '.itsec-module-card' ).find( '.itsec-module-card-content' );
504
+ }
505
+
506
+ var $settings = $container.siblings( '.itsec-module-settings-container' ),
507
+ isVisible = $settings.is( ':visible' );
508
+ $settings.stop().slideToggle( 300 );
509
+
510
+ if ( ! isVisible ) {
511
+ var $highlighted = jQuery( '.itsec-highlighted-setting', $settings );
512
+
513
+ if ( $highlighted.length ) {
514
+ setTimeout( function () {
515
+ jQuery.scrollTo( $highlighted.first(), 'swing', {
516
+ offset: { top: -30 },
517
+ onAfter: function() {
518
+ var $el = jQuery( 'input[type!="button"], textarea, select', $highlighted ).not( ':hidden' ).first();
519
+ itsecUtil.focus( $el, $highlighted );
520
+ }
521
+ } );
522
+ }, 50 );
523
+ } else {
524
+ var $el = jQuery( 'input[type!="button"], textarea, select', $settings ).not( ':hidden' ).first();
525
+ itsecUtil.focus( $el, $settings );
526
+ }
527
+ }
528
+
529
+ var $button = $container.find( '.itsec-toggle-settings' );
530
+
531
+ if ( $container.parent().hasClass( 'itsec-module-type-enabled' ) ) {
532
+ if ( $button.html() == itsec_page.translations.show_settings ) {
533
+ $button.html( itsec_page.translations.hide_settings );
534
+ } else {
535
+ $button.html( itsec_page.translations.show_settings );
536
+ }
537
+ } else {
538
+ if ( $button.hasClass( 'information-only' ) ) {
539
+ if ( $button.html() == itsec_page.translations.show_information ) {
540
+ $button.html( itsec_page.translations.hide_description );
541
+ } else {
542
+ $button.html( itsec_page.translations.show_information );
543
+ }
544
+ } else {
545
+ if ( $button.html() == itsec_page.translations.show_description ) {
546
+ $button.html( itsec_page.translations.hide_description );
547
+ } else {
548
+ $button.html( itsec_page.translations.show_description );
549
+ }
550
+ }
551
+ }
552
+ },
553
+
554
+ showGridSettingsModal: function( e ) {
555
+ e.preventDefault();
556
+
557
+ var $module = jQuery(this).parents( '.itsec-module-card' ),
558
+ $settingsContainer = $module.find( '.itsec-module-settings-container' ),
559
+ $modalBackground = jQuery( '.itsec-modal-background' );
560
+
561
+ $module.show();
562
+
563
+ $modalBackground.fadeIn();
564
+ $settingsContainer.fadeIn( 200 );
565
+
566
+ jQuery( 'body' ).addClass( 'itsec-modal-open' );
567
+
568
+ var $highlighted = jQuery( '.itsec-highlighted-setting', $module ).first();
569
+
570
+ if ( $highlighted.length ) {
571
+ jQuery( '.itsec-module-settings-content-container', $module ).scrollTo( $highlighted, 'swing', {
572
+ offset: { top: -20 },
573
+ onAfter: function() {
574
+ var $el = jQuery( 'input[type!="button"], textarea, select', $highlighted ).not( ':hidden' ).first();
575
+ itsecUtil.focus( $el, $highlighted );
576
+ }
577
+ } );
578
+ } else {
579
+ var $el = jQuery( 'input[type!="button"], textarea, select', $settingsContainer ).not( ':hidden' ).first();
580
+ itsecUtil.focus( $el, $settingsContainer );
581
+ }
582
+ },
583
+
584
+ closeGridSettingsModal: function( e ) {
585
+ if ( 'undefined' !== typeof e ) {
586
+ e.preventDefault();
587
+
588
+ // For keyup events, only process esc
589
+ if ( 'keyup' === e.type && 27 !== e.which ) {
590
+ return;
591
+ }
592
+ }
593
+
594
+ jQuery( '.itsec-modal-background' ).fadeOut();
595
+ jQuery( '.itsec-module-settings-container' ).fadeOut( 200 );
596
+ jQuery( 'body' ).removeClass( 'itsec-modal-open' );
597
+
598
+ if ( 'undefined' === typeof e || 'popstate' !== e.type ) {
599
+ var module_type = itsecUtil.getUrlParameter( 'module_type' );
600
+ if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
601
+ module_type = 'recommended';
602
+ }
603
+ window.history.pushState( {'module':'', 'module_type':module_type}, module_type, '?page=itsec&module_type=' + module_type );
604
+ }
605
+
606
+ if ( jQuery( '#search' ).val().length ) {
607
+ jQuery( '#search' ).focus();
608
+ }
609
+ },
610
+
611
+ toggleModuleActivation: function( e ) {
612
+ e.preventDefault();
613
+ e.stopPropagation();
614
+
615
+ var $button = jQuery(this),
616
+ $card = $button.parents( '.itsec-module-card' ),
617
+ $buttons = $card.find( '.itsec-toggle-activation' ),
618
+ module = $card.attr( 'id' ).replace( 'itsec-module-card-', '' );
619
+
620
+ $buttons.prop( 'disabled', true );
621
+
622
+ if ( $button.html() == itsec_page.translations.activate ) {
623
+ var method = 'activate';
624
+ } else {
625
+ var method = 'deactivate';
626
+ }
627
+
628
+ itsecUtil.sendAJAXRequest( module, method, {}, itsecSettingsPage.toggleModuleActivationCallback );
629
+ },
630
+
631
+ setModuleToActive: function( module ) {
632
+ var args = {
633
+ 'module': module,
634
+ 'method': 'activate',
635
+ 'errors': []
636
+ };
637
+
638
+ itsecSettingsPage.toggleModuleActivationCallback( args );
639
+ },
640
+
641
+ setModuleToInactive: function( module ) {
642
+ var args = {
643
+ 'module': module,
644
+ 'method': 'deactivate',
645
+ 'errors': []
646
+ };
647
+
648
+ itsecSettingsPage.toggleModuleActivationCallback( args );
649
+ },
650
+
651
+ toggleModuleActivationCallback: function( results ) {
652
+ var module = results.module;
653
+ var method = results.method;
654
+
655
+ var $card = jQuery( '#itsec-module-card-' + module ),
656
+ $buttons = $card.find( '.itsec-toggle-activation' )
657
+
658
+ if ( results.errors.length > 0 ) {
659
+ $buttons
660
+ .html( itsec_page.translations.error )
661
+ .addClass( 'button-secondary' )
662
+ .removeClass( 'button-primary' );
663
+
664
+ setTimeout( function() {
665
+ itsecSettingsPage.isModuleActive( module );
666
+ }, 1000 );
667
+
668
+ return;
669
+ }
670
+
671
+ if ( 'activate' === method ) {
672
+ $buttons
673
+ .html( itsec_page.translations.deactivate )
674
+ .addClass( 'button-secondary' )
675
+ .removeClass( 'button-primary' )
676
+ .prop( 'disabled', false );
677
+
678
+ $card
679
+ .addClass( 'itsec-module-type-enabled' )
680
+ .removeClass( 'itsec-module-type-disabled' );
681
+
682
+ var newToggleSettingsLabel = itsec_page.translations.show_settings;
683
+ } else {
684
+ $buttons
685
+ .html( itsec_page.translations.activate )
686
+ .addClass( 'button-primary' )
687
+ .removeClass( 'button-secondary' )
688
+ .prop( 'disabled', false );
689
+
690
+ $card
691
+ .addClass( 'itsec-module-type-disabled' )
692
+ .removeClass( 'itsec-module-type-enabled' );
693
+
694
+ var newToggleSettingsLabel = itsec_page.translations.show_description;
695
+ }
696
+
697
+ $card.find( '.itsec-toggle-settings' ).html( newToggleSettingsLabel );
698
+
699
+ var enabledCount = jQuery( '.itsec-module-type-enabled' ).length,
700
+ disabledCount = jQuery( '.itsec-module-type-disabled' ).length;
701
+
702
+ jQuery( '#itsec-module-filter-enabled .count' ).html( '(' + enabledCount + ')' );
703
+ jQuery( '#itsec-module-filter-disabled .count' ).html( '(' + disabledCount + ')' );
704
+
705
+
706
+ itsecSettingsPage.showErrors( results.warnings, results.module, 'closed', 'warning' );
707
+ itsecSettingsPage.showMessages( results.messages, results.module, 'closed' );
708
+ itsecSettingsPage.showMessages( results.infos, results.module, 'closed', 'info' );
709
+ },
710
+
711
+ isModuleActive: function( module ) {
712
+ var data = {
713
+ 'module': module,
714
+ 'method': 'is_active'
715
+ };
716
+
717
+ itsecUtil.sendAJAXRequest( module, 'is_active', {}, itsecSettingsPage.isModuleActiveCallback );
718
+ },
719
+
720
+ isModuleActiveCallback: function( results ) {
721
+ if ( true === results.response ) {
722
+ results.method = 'activate';
723
+ } else if ( false === results.response ) {
724
+ results.method = 'deactivate';
725
+ } else {
726
+ return;
727
+ }
728
+
729
+ itsecSettingsPage.toggleModuleActivationCallback( results );
730
+ },
731
+
732
+ reloadModule: function( module ) {
733
+ if ( module.preventDefault ) {
734
+ module.preventDefault();
735
+
736
+ module = jQuery(this).parents( '.itsec-module-card' ).attr( 'id' ).replace( 'itsec-module-card-', '' );
737
+ }
738
+
739
+ var method = 'get_refreshed_module_settings';
740
+ var data = {};
741
+
742
+ itsecUtil.sendAJAXRequest( module, method, data, function( results ) {
743
+ if ( results.success && results.response ) {
744
+ var $card = jQuery( '#itsec-module-card-' + module );
745
+ var isHidden = $card.is( ':hidden' );
746
+
747
+ jQuery( '.itsec-module-settings-content-main', $card ).html( results.response );
748
+
749
+ if ( isHidden ) {
750
+ $card.hide();
751
+ } else {
752
+ jQuery( '.itsec-settings-toggle' ).trigger( 'change' );
753
+ }
754
+ } else if ( results.errors && results.errors.length > 0 ) {
755
+ itsecSettingsPage.showErrors( results.errors, results.module, 'open' );
756
+ } else if ( results.warnings && results.warnings.length > 0 ) {
757
+ itsecSettingsPage.showErrors( results.warnings, results.module, 'open', 'warning' );
758
+ }
759
+
760
+ itsecSettingsPage.events.trigger( 'moduleReloaded', module );
761
+
762
+ itsecSettingsPage.makeNoticesDismissible();
763
+ } );
764
+ },
765
+
766
+ reloadAllModules: function( _, initialResponse) {
767
+ itsecUtil.sendAJAXRequest( '#', 'get_refreshed_module_form', null, function ( response ) {
768
+
769
+ if ( ! response.success || response.errors.length ) {
770
+ return;
771
+ }
772
+
773
+ var $open;
774
+
775
+ if ( jQuery( 'body' ).hasClass( 'itsec-modal-open' ) ) {
776
+ var $newModules = jQuery( response.response ), $cardsList = jQuery( '.itsec-module-cards' );
777
+ $open = jQuery( '.itsec-module-settings-container:visible' ).parents( '.itsec-module-card' );
778
+
779
+ jQuery( '.itsec-module-card', $newModules ).each( function () {
780
+ var $new = jQuery( this ), $current = jQuery( '#' + $new.attr( 'id' ), $cardsList );
781
+
782
+ if ( $new.attr( 'id' ).length && $new.attr( 'id' ) === $open.attr( 'id' ) ) {
783
+ jQuery( '.itsec-module-settings-content-main', $current ).html( jQuery( '.itsec-module-settings-content-main', $new ).html() );
784
+ } else {
785
+ jQuery( '.itsec-module-settings-container', $new ).hide();
786
+ $current.replaceWith( $new );
787
+ }
788
+ } );
789
+
790
+ } else {
791
+ jQuery( '.itsec-module-cards-container' ).html( response.response );
792
+ }
793
+
794
+ itsecSettingsPage.initFilters();
795
+
796
+ if ( ! $open ) {
797
+ jQuery( '.itsec-module-settings-container' ).hide();
798
+ }
799
+
800
+ if ( initialResponse ) {
801
+ itsecSettingsPage.showMessages( initialResponse.messages, initialResponse.module, $open ? 'open' : 'closed' );
802
+ itsecSettingsPage.showMessages( initialResponse.infos, initialResponse.module, $open ? 'open' : 'closed', 'info' );
803
+ itsecSettingsPage.showErrors( initialResponse.errors, initialResponse.module, $open ? 'open' : 'closed' );
804
+ itsecSettingsPage.showErrors( initialResponse.warnings, initialResponse.module, $open ? 'open' : 'closed', 'warning' );
805
+ }
806
+
807
+ itsecSettingsPage.makeNoticesDismissible();
808
+ itsecSettingsPage.events.trigger( 'modulesReloaded', initialResponse );
809
+ } );
810
+ },
811
+
812
+ reloadWidget: function( widget ) {
813
+ var method = 'get_refreshed_widget_settings';
814
+ var data = {};
815
+
816
+ itsecUtil.sendAJAXRequest( module, method, data, function( results ) {
817
+ if ( results.success && results.response ) {
818
+ jQuery( '#itsec-sidebar-widget-' + module + ' .inside' ).html( results.response );
819
+ } else {
820
+ itsecSettingsPage.showErrors( results.errors, results.module, 'closed' );
821
+ itsecSettingsPage.showErrors( results.warnings, results.module, 'closed', 'warning' );
822
+ }
823
+ } );
824
+ },
825
+
826
+ // Make notices dismissible
827
+ makeNoticesDismissible: function(){
828
+ jQuery( '.notice.itsec-is-dismissible' ).each( function() {
829
+ var $el = jQuery( this ),
830
+ $button = jQuery( '<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>' ),
831
+ btnText = itsec_page.translations.dismiss || '';
832
+
833
+ // Don't rebind twice
834
+ if ( jQuery( '.notice-dismiss', $el ).length ) {
835
+ return;
836
+ }
837
+
838
+ // Ensure plain text
839
+ $button.find( '.screen-reader-text' ).text( btnText );
840
+ $button.on( 'click.wp-dismiss-notice', function( event ) {
841
+ event.preventDefault();
842
+
843
+ $el.trigger( 'itsec-dismiss-notice' );
844
+
845
+ $el.fadeTo( 100, 0, function() {
846
+ $el.slideUp( 100, function() {
847
+ $el.remove();
848
+ });
849
+ });
850
+ });
851
+
852
+ $el.append( $button );
853
+ });
854
+ }
855
+ };
856
+
857
+ jQuery(document).ready(function( $ ) {
858
+ itsecSettingsPage.init();
859
+
860
+ if ( itsec_page.show_security_check ) {
861
+ jQuery( '.itsec-settings-view-toggle a.itsec-grid' ).trigger( 'click' );
862
+ jQuery( '#itsec-module-card-security-check .itsec-toggle-settings' ).trigger( 'click' );
863
+ }
864
+
865
+
866
+ jQuery( '.dialog' ).click( function ( event ) {
867
+ event.preventDefault();
868
+
869
+ var target = jQuery( this ).attr( 'href' );
870
+ var title = jQuery( this ).parents( '.inside' ).siblings( 'h3.hndle' ).children( 'span' ).text();
871
+
872
+ jQuery( '#' + target ).dialog( {
873
+ dialogClass : 'wp-dialog itsec-dialog itsec-dialog-logs',
874
+ modal : true,
875
+ closeOnEscape: true,
876
+ title : title,
877
+ height : ( jQuery( window ).height() * 0.8 ),
878
+ width : ( jQuery( window ).width() * 0.8 ),
879
+ open : function ( event, ui ) {
880
+ jQuery( '.ui-widget-overlay' ).bind( 'click', function () {
881
+ jQuery( this ).siblings( '.ui-dialog' ).find( '.ui-dialog-content' ).dialog( 'close' );
882
+ } );
883
+ }
884
+ } );
885
+
886
+ jQuery( '.ui-dialog :button' ).blur();
887
+ } );
888
+
889
+ var regex = /[^\w]/ig;
890
+
891
+ var $search = $( '#search' ), $cardsContainer = $( '.itsec-module-cards' ),
892
+ $cards = $( '.itsec-module-card', $cardsContainer ),
893
+ $searchFilter = $( '#itsec-module-filter-search' ),
894
+ $currentFilter = $( '.itsec-feature-tabs .current' ).parent();
895
+
896
+ itsecSettingsPage.events.on( 'modulesReloaded', function() {
897
+ $cardsContainer = $( '.itsec-module-cards' );
898
+ $cards = $( '.itsec-module-card', $cardsContainer );
899
+ } );
900
+
901
+ $search.on( 'input', _.debounce( function () {
902
+ var query = $search.val().trim().replace( regex, ' ' );
903
+
904
+ var $maybeCurrent = $( '.itsec-feature-tabs .current' ).parent();
905
+
906
+ if ( $maybeCurrent && $maybeCurrent.prop( 'id' ) !== 'itsec-module-filter-search' ) {
907
+ $currentFilter = $maybeCurrent;
908
+ }
909
+
910
+ $( '.itsec-highlighted-setting', $cards ).removeClass( 'itsec-highlighted-setting' );
911
+
912
+ if ( !query.length ) {
913
+ $searchFilter.addClass( 'hide-if-js' );
914
+ $( 'a', $searchFilter ).removeClass( 'current' );
915
+ $( 'a', $currentFilter ).addClass( 'current' );
916
+
917
+ var type = $currentFilter.prop( 'id' ).substr( 20 );
918
+
919
+ if ( 'all' === type ) {
920
+ $cards.show();
921
+ } else {
922
+ $( '.itsec-module-type-' + type ).show();
923
+ $( '.itsec-module-card' ).not( '.itsec-module-type-' + type ).hide();
924
+ }
925
+
926
+ return;
927
+ }
928
+
929
+ var $titleMatches = $( ".itsec-module-card-content > h2:itsecContains('" + query + "')", $cards ),
930
+ $titleMatchesCards = $titleMatches.parents( '.itsec-module-card' );
931
+
932
+ var $descriptionMatches = $( ".itsec-module-card-content > p:itsecContains('" + query + "')", $cards ),
933
+ $descriptionMatchesCards = $descriptionMatches.parents( '.itsec-module-card' );
934
+
935
+ var $settingMatches = $( ".itsec-module-settings-container .form-table tr > th > label:itsecContains('" + query + "')", $cards ),
936
+ $settingMatchesCards = $settingMatches.parents( '.itsec-module-card' );
937
+
938
+
939
+ var $matches = $titleMatchesCards.add( $descriptionMatchesCards ).add( $settingMatchesCards );
940
+
941
+ $searchFilter.removeClass( 'hide-if-js' );
942
+ $( 'a', $currentFilter ).removeClass( 'current' );
943
+ $( 'a', $searchFilter ).addClass( 'current' );
944
+ $( '.count', $searchFilter ).text( '(' + $matches.length + ')' );
945
+
946
+ $cards.hide();
947
+ $matches.show();
948
+
949
+ $settingMatches.parents( 'tr' ).addClass( 'itsec-highlighted-setting' );
950
+
951
+ if ( $matches.length === 1 ) {
952
+ $( '.itsec-toggle-settings', $matches.first() ).click();
953
+ }
954
+ }, 250 ) );
955
+
956
+ $.expr[":"].itsecContains = $.expr.createPseudo( function ( arg ) {
957
+ return function ( elem ) {
958
+ var candidate = $( elem ).text().toUpperCase().replace( regex, ' ' ), term = arg.toUpperCase();
959
+ var index = candidate.indexOf( term );
960
+
961
+ if ( index === -1 ) {
962
+ return false;
963
+ }
964
+
965
+ if ( index === 0 ) {
966
+ return true;
967
+ }
968
+
969
+ var prior = candidate.charAt( index - 1 ), next = candidate.charAt( term.length + index );
970
+
971
+ // full word
972
+ return prior === ' ' && ( next === ' ' || next === '' );
973
+ };
974
+ } );
975
+ });
core/admin-pages/js/util.js ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var itsecUtil = {
4
+
5
+ focus: function( $el, $fallback ) {
6
+ if ( itsecUtil.isElementVisible( $el ) && jQuery( window ).height() > 800 ) {
7
+ $el.focus();
8
+ } else {
9
+ $fallback.prop( 'tabindex', -1 ).focus();
10
+ }
11
+ },
12
+
13
+ isElementVisible: function( $el ) {
14
+
15
+ var $window = jQuery( window ), height = $window.height(), width = $window.width(), offset = $el.offset();
16
+
17
+ if ( ! $el || ! offset ) {
18
+ return false;
19
+ }
20
+
21
+ return offset.top < height && offset.left < width;
22
+ },
23
+
24
+ sendModuleAJAXRequest: function( module, data, callback ) {
25
+ itsecUtil.sendAJAXRequest( module, 'handle_module_request', data, callback );
26
+ },
27
+
28
+ sendWidgetAJAXRequest: function( widget, data, callback ) {
29
+ itsecUtil.sendAJAXRequest( widget, 'handle_widget_request', data, callback );
30
+ },
31
+
32
+ sendAJAXRequest: function( module, method, data, callback, action, nonce ) {
33
+ var postData = {
34
+ 'action': itsec_util.ajax_action,
35
+ 'nonce': itsec_util.ajax_nonce,
36
+ 'module': module,
37
+ 'method': method,
38
+ 'data': data,
39
+ };
40
+
41
+ if ( 'undefined' !== typeof action ) {
42
+ postData.action = action;
43
+ }
44
+
45
+ if ( 'undefined' !== typeof nonce ) {
46
+ postData.nonce = nonce;
47
+ }
48
+
49
+ jQuery.post( ajaxurl, postData )
50
+ .always(function( a, status, b ) {
51
+ itsecUtil.processAjaxResponse( a, status, b, module, method, data, callback );
52
+ });
53
+ },
54
+
55
+ processAjaxResponse: function( a, status, b, module, method, data, callback ) {
56
+ var results = {
57
+ 'module': module,
58
+ 'method': method,
59
+ 'data': data,
60
+ 'status': status,
61
+ 'jqxhr': null,
62
+ 'success': false,
63
+ 'response': null,
64
+ 'errors': [],
65
+ 'warnings': [],
66
+ 'messages': [],
67
+ 'infos': [],
68
+ 'functionCalls': [],
69
+ 'redirect': false,
70
+ 'closeModal': true
71
+ };
72
+
73
+
74
+ if ( 'ITSEC_Response' === a.source && 'undefined' !== a.response ) {
75
+ // Successful response with a valid format.
76
+ results.jqxhr = b;
77
+ results.success = a.success;
78
+ results.response = a.response;
79
+ results.errors = a.errors;
80
+ results.warnings = a.warnings;
81
+ results.messages = a.messages;
82
+ results.infos = a.infos;
83
+ results.functionCalls = a.functionCalls;
84
+ results.redirect = a.redirect;
85
+ results.closeModal = a.closeModal;
86
+ } else if ( a.responseText ) {
87
+ // Failed response.
88
+ results.jqxhr = a;
89
+ var errorThrown = b;
90
+
91
+ if ( 'undefined' === typeof results.jqxhr.status ) {
92
+ results.jqxhr.status = -1;
93
+ }
94
+
95
+ var error = '';
96
+
97
+ if ( 'timeout' === status ) {
98
+ error = itsec_util.translations.ajax_timeout;
99
+ } else if ( 'parsererror' === status ) {
100
+ error = itsec_util.translations.ajax_parsererror;
101
+ } else if ( 403 == results.jqxhr.status ) {
102
+ error = itsec_util.translations.ajax_forbidden;
103
+ } else if ( 404 == results.jqxhr.status ) {
104
+ error = itsec_util.translations.ajax_not_found;
105
+ } else if ( 500 == results.jqxhr.status ) {
106
+ error = itsec_util.translations.ajax_server_error;
107
+ } else {
108
+ error = itsec_util.translations.ajax_unknown;
109
+ }
110
+
111
+ error = error.replace( '%1$s', status );
112
+ error = error.replace( '%2$s', errorThrown );
113
+
114
+ results.errors = [ error ];
115
+ } else {
116
+ // Successful response with an invalid format.
117
+ results.jqxhr = b;
118
+
119
+ results.response = a;
120
+ results.errors = [ itsec_util.translations.ajax_invalid ];
121
+ }
122
+
123
+
124
+ if ( results.redirect ) {
125
+ window.location = results.redirect;
126
+ }
127
+
128
+
129
+ if ( 'function' === typeof callback ) {
130
+ callback( results );
131
+ } else if ( 'function' === typeof console.log ) {
132
+ console.log( 'ERROR: Unable to handle settings AJAX request due to an invalid callback:', callback, {'data': postData, 'results': results} );
133
+ }
134
+
135
+
136
+ if ( results.functionCalls ) {
137
+ for ( var i = 0; i < results.functionCalls.length; i++ ) {
138
+ if ( 'object' === typeof itsecSettingsPage && 'object' === typeof results.functionCalls[i] && 'string' === typeof results.functionCalls[i][0] && 'function' === typeof itsecSettingsPage[results.functionCalls[i][0]] ) {
139
+ itsecSettingsPage[results.functionCalls[i][0]]( results.functionCalls[i][1], results );
140
+ } else if ( 'string' === typeof results.functionCalls[i] && 'function' === typeof window[results.functionCalls[i]] ) {
141
+ window[results.functionCalls[i]]();
142
+ } else if ( 'object' === typeof results.functionCalls[i] && 'string' === typeof results.functionCalls[i][0] && 'function' === typeof window[results.functionCalls[i][0]] ) {
143
+ window[results.functionCalls[i][0]]( results.functionCalls[i][1] );
144
+ } else if ( 'function' === typeof console.log ) {
145
+ console.log( 'ERROR: Unable to call missing function:', results.functionCalls[i] );
146
+ }
147
+ }
148
+ }
149
+ },
150
+
151
+ getUrlParameter: function( name ) {
152
+ var pageURL = decodeURIComponent( window.location.search.substring( 1 ) ),
153
+ URLParameters = pageURL.split( '&' ),
154
+ parameterName,
155
+ i;
156
+
157
+ // Loop through all parameters
158
+ for ( i = 0; i < URLParameters.length; i++ ) {
159
+ parameterName = URLParameters[i].split( '=' );
160
+
161
+ // If this is the parameter we're looking for
162
+ if ( parameterName[0] === name ) {
163
+ // Return the value or true if there is no value
164
+ return parameterName[1] === undefined ? true : parameterName[1];
165
+ }
166
+ }
167
+ // If the requested parameter doesn't exist, return false
168
+ return false;
169
+ }
170
+
171
+ };
core/admin-pages/logs-list-table.php ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * List table for logs
5
+ *
6
+ * @package iThemes-Security
7
+ * @since 3.9
8
+ */
9
+ final class ITSEC_Logs_List_Table extends ITSEC_WP_List_Table {
10
+ private $user_cache = array();
11
+ private $raw_filters = array();
12
+ private $current_filters = array();
13
+ private $types = array();
14
+
15
+
16
+ public function __construct() {
17
+ parent::__construct(
18
+ array(
19
+ 'singular' => 'itsec-log-entry',
20
+ 'plural' => 'itsec-log-entries',
21
+ 'ajax' => true
22
+ )
23
+ );
24
+
25
+ $this->types = ITSEC_Log::get_types_for_display();
26
+ }
27
+
28
+ public function column_default( $entry, $field ) {
29
+ return esc_html( $entry[$field] );
30
+ }
31
+
32
+ public function column_description( $entry ) {
33
+ return $entry['description'];
34
+ }
35
+
36
+ public function column_type( $entry ) {
37
+ return $this->types[$entry['type']];
38
+ }
39
+
40
+ public function column_timestamp( $entry ) {
41
+ $timestamp = strtotime( $entry['timestamp'] );
42
+ $datetime = date( 'Y-m-d H:i:s', $timestamp + ITSEC_Core::get_time_offset() );
43
+ /* translators: 1: date and time, 2: time difference */
44
+ return sprintf( __( '%1$s - %2$s ago', 'better-wp-security' ), $datetime, human_time_diff( $timestamp, time() ) );
45
+ }
46
+
47
+ public function column_remote_ip( $entry ) {
48
+ if ( empty( $entry['remote_ip'] ) ) {
49
+ return '';
50
+ }
51
+
52
+ return '<code><a href="' . esc_url( ITSEC_Lib::get_trace_ip_link( $entry['remote_ip'] ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $entry['remote_ip'] ) . '</a></code>';
53
+ }
54
+
55
+ /**
56
+ * Define username column
57
+ *
58
+ * @param array $item array of row data
59
+ *
60
+ * @return string formatted output
61
+ *
62
+ **/
63
+ public function column_user_id( $item ) {
64
+ if ( $item['user_id'] > 0 ) {
65
+ if ( ! isset( $this->user_cache[$item['user_id']] ) ) {
66
+ $this->user_cache[$item['user_id']] = get_userdata( $item['user_id'] );
67
+ }
68
+
69
+ if ( false === $this->user_cache[$item['user_id']] ) {
70
+ return '';
71
+ }
72
+
73
+ return '<a href="' . esc_url( admin_url( 'user-edit.php?user_id=' . $item['user_id'] ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $this->user_cache[$item['user_id']]->user_login ) . '</a>';
74
+ }
75
+
76
+ return '';
77
+ }
78
+
79
+ public function column_details( $entry ) {
80
+ echo '<a class="itsec-logs-view-details" href="' . self::get_self_link( array( 'id' => $entry['id'] ), array() ) . '">' . esc_html__( 'View Details', 'better-wp-security' ) . '</a>';
81
+ }
82
+
83
+ /**
84
+ * Generates and display row actions links for the list table.
85
+ *
86
+ * @since 4.3.0
87
+ *
88
+ * @param object $item The item being acted upon.
89
+ * @param string $column_name Current column name.
90
+ * @param string $primary Primary column name.
91
+ * @return string The row actions HTML, or an empty string if the current column is the primary column.
92
+ */
93
+ protected function handle_row_actions( $item, $column_name, $primary ) {
94
+ $columns = $this->get_columns();
95
+ $column_header = $columns[$column_name];
96
+
97
+ if ( 'module_display' === $column_name ) {
98
+ $column_name = 'module';
99
+ } else if ( 'description' === $column_name ) {
100
+ $column_name = 'code';
101
+ }
102
+
103
+ if ( 'details' === $column_name || 'id' === $column_name ) {
104
+ return;
105
+ } else if ( 'timestamp' === $column_name ) {
106
+ list( $date, $time ) = explode( ' ', $item[$column_name] );
107
+ $url = self::get_self_link( array( 'filters' => "$column_name|$date%" ) );
108
+ return '&nbsp;<a class="dashicons dashicons-filter" href="' . esc_url( $url ) . '" title="' . esc_attr__( 'Show only entries for this day', 'better-wp-security' ) . '">&nbsp;</a>';
109
+ } else if ( empty( $item[$column_name] ) ) {
110
+ return;
111
+ }
112
+
113
+ if ( 'four_oh_four' === $item['module'] && 'code' === $column_name ) {
114
+ $url = self::get_self_link( array( 'filters[10]' => "url|{$item['url']}", 'filters[11]' => 'module|four_oh_four' ) );
115
+ } else {
116
+ $url = self::get_self_link( array( 'filters' => "$column_name|{$item[$column_name]}" ) );
117
+ }
118
+
119
+ $out = '&nbsp;<a class="dashicons dashicons-filter" href="' . esc_url( $url ) . '" title="' . sprintf( esc_attr__( 'Show only entries for this %s', 'better-wp-security' ), strtolower( $column_header ) ) . '">&nbsp;</a>';
120
+
121
+ if ( 'module' === $column_name ) {
122
+ $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
123
+ }
124
+
125
+ return $out;
126
+ }
127
+
128
+ private function get_self_link( $vars = array(), $current_vars = false ) {
129
+ if ( ! is_array( $current_vars ) ) {
130
+ $current_vars = $_GET;
131
+ unset( $current_vars['id'] );
132
+ unset( $current_vars['paged'] );
133
+ }
134
+
135
+ $vars = array_merge_recursive( $current_vars, $vars );
136
+
137
+ if ( ! isset( $vars['page'] ) ) {
138
+ $vars = array_merge( array( 'page' => $_GET['page'] ), $vars );
139
+ }
140
+
141
+ return network_admin_url( 'admin.php?' . http_build_query( $vars, null, '&' ) );
142
+ }
143
+
144
+ /**
145
+ * Define Columns
146
+ *
147
+ * @return array array of column titles
148
+ */
149
+ public function get_columns() {
150
+ return array(
151
+ 'id' => esc_html__( 'ID', 'better-wp-security' ),
152
+ 'module_display' => esc_html__( 'Module', 'better-wp-security' ),
153
+ 'type' => esc_html__( 'Type', 'better-wp-security' ),
154
+ 'description' => esc_html__( 'Description', 'better-wp-security' ),
155
+ 'timestamp' => esc_html__( 'Time', 'better-wp-security' ),
156
+ 'remote_ip' => esc_html__( 'Host', 'better-wp-security' ),
157
+ 'user_id' => esc_html__( 'User', 'better-wp-security' ),
158
+ 'details' => esc_html__( 'Details', 'better-wp-security' ),
159
+ );
160
+ }
161
+
162
+ /**
163
+ *
164
+ * @return array
165
+ */
166
+ protected function get_sortable_columns() {
167
+ return array(
168
+ 'timestamp' => array( 'timestamp', false ),
169
+ 'remote_ip' => array( 'remote_ip', false ),
170
+ );
171
+ }
172
+
173
+ private function get_raw_filters() {
174
+ if ( ! empty( $this->raw_filters ) ) {
175
+ return $this->raw_filters;
176
+ }
177
+
178
+ if ( empty( $_GET['filters'] ) ) {
179
+ $raw_filters = array();
180
+ } else {
181
+ $raw_filters = $_GET['filters'];
182
+ }
183
+
184
+ $filters = array();
185
+
186
+ foreach ( (array) $raw_filters as $var ) {
187
+ list( $field, $value ) = explode( '|', $var, 2 );
188
+
189
+ $filters[$field] = $value;
190
+ }
191
+
192
+ if ( ! isset( $filters['type'] ) ) {
193
+ if ( ! isset( $filters['type_not'] ) ) {
194
+ $options = ITSEC_Log_Util::get_logs_page_screen_options();
195
+
196
+ $filters['type'] = $options['default_view'];
197
+ } else {
198
+ $filters['type'] = 'all';
199
+ }
200
+ }
201
+
202
+ $this->raw_filters = $filters;
203
+
204
+ return $this->raw_filters;
205
+ }
206
+
207
+ private function get_current_view() {
208
+ $raw_filters = $this->get_raw_filters();
209
+
210
+ return $raw_filters['type'];
211
+ }
212
+
213
+ private function get_current_filters( $options ) {
214
+ if ( ! empty( $this->current_filters ) ) {
215
+ return $this->current_filters;
216
+ }
217
+
218
+ $filters = $this->get_raw_filters();
219
+
220
+ if ( 'process' === $filters['type'] ) {
221
+ $filters['type'] = 'process-start';
222
+ }
223
+
224
+ if ( 'all' === $filters['type'] ) {
225
+ $type_not = array();
226
+
227
+ if ( ! $options['show_debug'] ) {
228
+ $type_not[] = 'debug';
229
+ }
230
+ if ( ! $options['show_process'] ) {
231
+ $type_not[] = 'process-start';
232
+ }
233
+
234
+ unset( $filters['type'] );
235
+ } else if ( 'important' === $filters['type'] ) {
236
+ $type_not = array( 'action', 'notice', 'debug', 'process-start' );
237
+
238
+ unset( $filters['type'] );
239
+ }
240
+
241
+ if ( ! empty( $type_not ) ) {
242
+ if ( isset( $filters['type_not'] ) && is_array( $filters['type_not'] ) ) {
243
+ $filters['type_not'] = array_merge( $filters['type_not'], $type_not );
244
+ $filters['type_not'] = array_unique( $filters['type_not'] );
245
+ } else {
246
+ $filters['type_not'] = $type_not;
247
+ }
248
+ }
249
+
250
+ $this->current_filters = $filters;
251
+
252
+ return $this->current_filters;
253
+ }
254
+
255
+ /**
256
+ * Prepare data for table
257
+ *
258
+ * @return void
259
+ */
260
+ public function prepare_items() {
261
+ global $wpdb;
262
+
263
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
264
+ $options = ITSEC_Log_Util::get_logs_page_screen_options();
265
+
266
+ $filters = $this->get_current_filters( $options );
267
+ $columns = $this->get_columns();
268
+ $hidden_fields = array( 'id' );
269
+ $sortable_columns = $this->get_sortable_columns();
270
+
271
+ if ( isset( $_GET['orderby'], $_GET['order'] ) ) {
272
+ $sort_by_column = $_GET['orderby'];
273
+ $sort_direction = $_GET['order'];
274
+ } else {
275
+ $sort_by_column = 'timestamp';
276
+ $sort_direction = 'DESC';
277
+ }
278
+
279
+ if ( $options['last_seen'] > 0 ) {
280
+ $filters['__min_timestamp'] = $options['last_seen'];
281
+ }
282
+
283
+ $total_items = ITSEC_Log::get_number_of_entries( $filters );
284
+ $items = ITSEC_Log::get_entries( $filters, $options['per_page'], $this->get_pagenum(), $sort_by_column, $sort_direction );
285
+
286
+ ITSEC_Modules::load_module_file( 'logs.php' );
287
+
288
+ foreach ( $items as $item ) {
289
+ if ( false === strpos( $item['code'], '::' ) ) {
290
+ $code = $item['code'];
291
+ $data = array();
292
+ } else {
293
+ list( $code, $data ) = explode( '::', $item['code'], 2 );
294
+ $data = explode( ',', $data );
295
+ }
296
+
297
+ $item['description'] = $item['code'];
298
+ $item['module_display'] = $item['module'];
299
+ $item = apply_filters( "itsec_logs_prepare_{$item['module']}_entry_for_list_display", $item, $code, $data );
300
+
301
+ $this->items[$item['id']] = $item;
302
+ }
303
+
304
+ $this->_column_headers = array( $columns, $hidden_fields, $sortable_columns, 'module_display' );
305
+
306
+ $this->set_pagination_args(
307
+ array(
308
+ 'total_items' => $total_items,
309
+ 'per_page' => $options['per_page'],
310
+ )
311
+ );
312
+ }
313
+
314
+ protected function get_views() {
315
+ $options = ITSEC_Log_Util::get_logs_page_screen_options();
316
+ $counts = ITSEC_Log::get_type_counts( $options['last_seen'] );
317
+
318
+ $views = array(
319
+ 'important' => esc_html__( 'Important Events (%s)', 'better-wp-security' ),
320
+ 'all' => esc_html__( 'All Events (%s)', 'better-wp-security' ),
321
+ 'critical-issue' => esc_html__( 'Critical Issues (%s)', 'better-wp-security' ),
322
+ 'fatal-error' => esc_html__( 'Fatal Errors (%s)', 'better-wp-security' ),
323
+ 'error' => esc_html__( 'Errors (%s)', 'better-wp-security' ),
324
+ 'warning' => esc_html__( 'Warnings (%s)', 'better-wp-security' ),
325
+ 'action' => esc_html__( 'Actions (%s)', 'better-wp-security' ),
326
+ 'notice' => esc_html__( 'Notices (%s)', 'better-wp-security' ),
327
+ 'debug' => esc_html__( 'Debug (%s)', 'better-wp-security' ),
328
+ 'process' => esc_html__( 'Process (%s)', 'better-wp-security' ),
329
+ );
330
+
331
+ if ( ! $options['show_debug'] ) {
332
+ unset( $views['debug'] );
333
+ }
334
+ if ( ! $options['show_process'] ) {
335
+ unset( $views['process'] );
336
+ }
337
+
338
+ $important_count = 0;
339
+ $all_count = 0;
340
+
341
+ foreach ( $views as $type => $description ) {
342
+ if ( in_array( $type, array( 'important', 'all' ) ) ) {
343
+ continue;
344
+ }
345
+
346
+ if ( empty( $counts[$type] ) ) {
347
+ unset( $views[$type] );
348
+ continue;
349
+ }
350
+
351
+ $views[$type] = sprintf( $description, $counts[$type] );
352
+
353
+ if ( in_array( $type, array( 'critical-issue', 'fatal-error', 'error', 'warning' ) ) ) {
354
+ $important_count += $counts[$type];
355
+ }
356
+
357
+ $all_count += $counts[$type];
358
+ }
359
+
360
+ $views['important'] = sprintf( $views['important'], $important_count );
361
+ $views['all'] = sprintf( $views['all'], $all_count );
362
+ $formatted_views = array();
363
+ $current = $this->get_current_view();
364
+
365
+ foreach ( $views as $type => $description ) {
366
+ $url = self::get_self_link( array( 'filters' => "type|$type" ), array() );
367
+
368
+ if ( $current === $type ) {
369
+ $description = '<a href="' . esc_url( $url ) . '" class="current" aria-current="page">' . $description . '</a>';
370
+ } else {
371
+ $description = '<a href="' . esc_url( $url ) . '">' . $description . '</a>';
372
+ }
373
+
374
+ $formatted_views["itsec-$type"] = $description;
375
+ }
376
+
377
+ return $formatted_views;
378
+ }
379
+
380
+ public function single_row( $item ) {
381
+ echo "<tr class='itsec-log-type-{$item['type']}'>";
382
+ $this->single_row_columns( $item );
383
+ echo '</tr>';
384
+ }
385
+
386
+ protected function get_table_classes() {
387
+ $options = ITSEC_Log_Util::get_logs_page_screen_options();
388
+
389
+ $classes = array(
390
+ 'widefat',
391
+ 'striped',
392
+ $this->_args['plural']
393
+ );
394
+
395
+ if ( $options['color'] ) {
396
+ $classes[] = 'itsec-logs-color';
397
+ }
398
+
399
+ return $classes;
400
+ }
401
+
402
+ protected function extra_tablenav( $which ) {
403
+ echo '<div class="alignleft actions">';
404
+
405
+ if ( 'top' === $which ) {
406
+ /*
407
+ ob_start();
408
+
409
+ $output = ob_get_clean();
410
+
411
+ if ( ! empty( $output ) ) {
412
+ echo $output;
413
+ submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
414
+ }*/
415
+ }
416
+
417
+ /* if ( $this->is_trash && current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_others_posts ) && $this->has_items() ) {
418
+ submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
419
+ }*/
420
+
421
+ echo '</div>';
422
+ }
423
+
424
+ public function no_items() {
425
+ esc_html_e( 'No events.', 'better-wp-security' );
426
+ }
427
+ }
core/admin-pages/page-logs.php CHANGED
@@ -2,14 +2,12 @@
2
 
3
 
4
  final class ITSEC_Logs_Page {
5
- private $version = 1.7;
6
 
7
  private $self_url = '';
8
  private $modules = array();
9
  private $widgets = array();
10
  private $translations = array();
11
- private $logger_modules = array();
12
- private $logger_displays = array();
13
 
14
 
15
  public function __construct() {
@@ -20,11 +18,9 @@ final class ITSEC_Logs_Page {
20
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
21
  add_action( 'admin_print_styles', array( $this, 'add_styles' ) );
22
 
23
- $this->set_translation_strings();
24
-
25
 
26
- $this->logger_modules = apply_filters( 'itsec_logger_modules', array() );
27
- $this->logger_displays = apply_filters( 'itsec_logger_displays', array() );
28
 
29
 
30
  require( dirname( __FILE__ ) . '/module-settings.php' );
@@ -52,14 +48,14 @@ final class ITSEC_Logs_Page {
52
  }
53
 
54
  $vars = array(
55
- 'ajax_action' => 'itsec_settings_page',
56
- 'ajax_nonce' => wp_create_nonce( 'itsec-settings-nonce' ),
57
  'logs_page_url' => ITSEC_Core::get_logs_page_url(),
58
  'translations' => $this->translations,
59
  );
60
 
61
- wp_enqueue_script( 'itsec-settings-page-script', plugins_url( 'js/script.js', __FILE__ ), array( 'jquery-ui-dialog' ), $this->version, true );
62
- wp_localize_script( 'itsec-settings-page-script', 'itsec_page', $vars );
63
  }
64
 
65
  public function add_styles() {
@@ -69,21 +65,27 @@ final class ITSEC_Logs_Page {
69
 
70
  private function set_translation_strings() {
71
  $this->translations = array(
72
- 'successful_save' => __( 'Settings saved successfully for %1$s.', 'better-wp-security' ),
 
 
 
 
 
 
73
 
74
- 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
75
 
76
- 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
77
 
78
- 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
79
 
80
- 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
81
 
82
- 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
83
 
84
- 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
85
 
86
- 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
87
  );
88
 
89
  foreach ( $this->translations as $key => $message ) {
@@ -94,28 +96,37 @@ final class ITSEC_Logs_Page {
94
  }
95
  }
96
 
97
- public function register_widget( $widget ) {
98
- if ( ! is_object( $widget ) || ! is_a( $widget, 'ITSEC_Settings_Page_Sidebar_Widget' ) ) {
99
- trigger_error( 'An invalid widget was registered.', E_USER_ERROR );
100
  return;
101
  }
102
 
103
- if ( isset( $this->modules[$widget->id] ) ) {
104
- trigger_error( "A widget with the id of {$widget->id} is registered. Widget id's must be unique from any other module or widget." );
105
- return;
106
- }
107
 
108
- if ( isset( $this->widgets[$widget->id] ) ) {
109
- trigger_error( "A widget with the id of {$widget->id} is already registered. Widget id's must be unique from any other module or widget." );
110
- return;
111
- }
112
 
 
 
113
 
114
- $this->widgets[$widget->id] = $widget;
115
- }
 
116
 
117
- private function handle_post() {
118
- if ( ! empty( $_POST['itsec_clear_logs'] ) && 'clear_logs' === $_POST['itsec_clear_logs'] ) {
 
 
 
 
 
 
 
 
 
 
 
119
  if ( ! wp_verify_nonce( $_POST['wp_nonce'], 'itsec_clear_logs' ) ) {
120
  die( __( 'Security error!', 'better-wp-security' ) );
121
  }
@@ -183,69 +194,233 @@ final class ITSEC_Logs_Page {
183
  }
184
  }
185
 
186
- public function handle_page_load( $self_url ) {
187
- $this->self_url = $self_url;
 
 
188
 
189
- $this->show_settings_page();
190
- }
191
 
192
- private function get_widget_settings( $id, $form = false, $echo = false ) {
193
- if ( ! isset( $this->widgets[$id] ) ) {
194
- $error = new WP_Error( 'itsec-settings-page-get-widget-settings-invalid-id', sprintf( __( 'The requested widget (%s) does not exist. Logs for it cannot be rendered.', 'better-wp-security' ), $id ) );
195
 
196
- if ( $echo ) {
197
- ITSEC_Lib::show_error_message( $error );
 
 
 
198
  } else {
199
- return $error;
200
  }
201
- }
 
202
 
203
- if ( false === $form ) {
204
- $form = new ITSEC_Form();
205
- }
206
 
207
- $widget = $this->widgets[$id];
 
 
208
 
209
- $form->add_input_group( $id );
210
- $form->set_defaults( $widget->get_defaults() );
211
 
212
- if ( ! $echo ) {
213
- ob_start();
214
- }
 
 
 
 
 
215
 
216
- $widget->render( $form );
217
 
218
- $form->remove_all_input_groups();
 
 
 
 
 
 
219
 
220
- if ( ! $echo ) {
221
- return ob_get_clean();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
 
 
 
223
  }
224
 
225
- private function show_settings_page() {
226
- require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-wp-list-table.php' );
 
227
 
 
228
 
229
- if ( isset( $_GET['filter'] ) ) {
230
- $filter = $_GET['filter'];
231
- } else {
232
- $filter = 'all';
233
- }
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
- $form = new ITSEC_Form();
 
237
 
 
 
238
 
239
- $filters = array(
240
- 'all' => __( 'All Log Data', 'better-wp-security' ),
241
- );
242
 
243
- foreach ( $this->logger_displays as $log_provider ) {
244
- $filters[$log_provider['module']] = $log_provider['title'];
245
- }
246
 
247
 
248
- $form->set_option( 'filter', $filter );
249
 
250
  ?>
251
  <div class="wrap">
@@ -270,26 +445,39 @@ final class ITSEC_Logs_Page {
270
  <div id="poststuff">
271
  <div id="post-body" class="metabox-holder columns-2 hide-if-no-js">
272
  <div id="postbox-container-2" class="postbox-container">
 
 
273
  <?php if ( 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' ) ) : ?>
274
  <p><?php _e( 'To view logs within the plugin you must enable database logging in the Global Settings. File logging is not available for access within the plugin itself.', 'better-wp-security' ); ?></p>
 
275
  <?php else : ?>
276
  <div class="itsec-module-cards-container list">
277
- <p><?php _e( 'Below are various logs of information collected by iThemes Security Pro. This information can help you get a picture of what is happening with your site and the level of success you have achieved in your security efforts.', 'better-wp-security' ); ?></p>
278
- <p><?php _e( 'Logging settings can be managed in the Global Settings.', 'better-wp-security' ); ?></p>
279
-
280
-
281
- <?php $form->start_form( 'itsec-module-settings-form' ); ?>
282
- <?php $form->add_nonce( 'itsec-settings-page' ); ?>
283
- <p><?php $form->add_select( 'filter', $filters ); ?></p>
284
- <?php $form->end_form(); ?>
285
-
286
- <?php $form->start_form( array( 'method' => 'GET' ) ); ?>
287
- <?php $this->show_filtered_logs( $filter ); ?>
288
- <?php $form->end_form(); ?>
289
  </div>
290
  <?php endif; ?>
291
  </div>
292
  <div class="itsec-modal-background"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
  <div id="postbox-container-1" class="postbox-container">
295
  <?php foreach ( $this->widgets as $id => $widget ) : ?>
@@ -310,92 +498,74 @@ final class ITSEC_Logs_Page {
310
  <div class="hide-if-js">
311
  <p class="itsec-warning-message"><?php _e( 'iThemes Security requires Javascript in order for the settings to be modified. Please enable Javascript to configure the settings.', 'better-wp-security' ); ?></p>
312
  </div>
 
 
 
313
  </div>
314
  </div>
315
  <?php
316
 
317
  }
318
 
319
- private function show_filtered_logs( $filter ) {
320
- $callback = null;
 
 
 
321
 
322
- foreach ( $this->logger_displays as $display ) {
323
- if ( $display['module'] === $filter ) {
324
- $callback = $display['callback'];
325
- break;
326
- }
327
  }
328
 
329
- echo '<form method="get">';
330
- echo '<input type="hidden" name="page" value="' . ( isset( $_GET['page'] ) ? esc_attr( $_GET['page'] ) : '' ) . '">';
 
 
331
 
332
- if ( $callback ) {
333
- call_user_func( $callback );
334
- } else {
335
- $this->all_logs_content( false );
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
- echo '</form>';
 
 
339
 
340
- if ( ! $callback ) {
341
- $this->clear_logs_form();
 
 
 
 
 
342
  }
343
- }
344
 
345
- /**
346
- * Displays all logs content
347
- *
348
- * @since 4.3
349
- *
350
- * @param bool $include_clear_logs_form Whether to include the form to clear all logs.
351
- *
352
- * @return void
353
- */
354
- public function all_logs_content( $include_clear_logs_form = true ) {
355
-
356
- require_once( ITSEC_Core::get_core_dir() . '/logger-all-logs.php' );
357
-
358
- $log_display = new ITSEC_Logger_All_Logs();
359
- $log_display->prepare_items();
360
- $log_display->display();
361
-
362
- if ( $include_clear_logs_form ) {
363
- $this->clear_logs_form();
364
  }
365
  }
366
 
367
- /**
368
- * Display the clear logs form.
369
- */
370
- public function clear_logs_form() {
371
-
372
- global $wpdb;
373
- $log_count = $wpdb->get_var( "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_log`;" );
374
-
375
- ?>
376
- <form method="post" action="">
377
- <?php wp_nonce_field( 'itsec_clear_logs', 'wp_nonce' ); ?>
378
- <input type="hidden" name="itsec_clear_logs" value="clear_logs"/>
379
- <table class="form-table">
380
- <tr valign="top">
381
- <th scope="row" class="settinglabel">
382
- <?php _e( 'Log Summary', 'better-wp-security' ); ?>
383
- </th>
384
- <td class="settingfield">
385
-
386
- <p><?php _e( 'Your database contains', 'better-wp-security' ); ?>
387
- <strong><?php echo $log_count; ?></strong> <?php _e( 'log entries.', 'better-wp-security' ); ?>
388
- </p>
389
-
390
- <p><?php _e( 'Use the button below to purge the log table in your database. Please note this will purge all log entries in the database including 404s.', 'better-wp-security' ); ?></p>
391
-
392
- <p class="submit"><input type="submit" class="button-primary"
393
- value="<?php _e( 'Clear Logs', 'better-wp-security' ); ?>"/></p>
394
- </td>
395
- </tr>
396
- </table>
397
- </form>
398
- <?php
399
  }
400
  }
401
 
2
 
3
 
4
  final class ITSEC_Logs_Page {
5
+ private $version = 1.8;
6
 
7
  private $self_url = '';
8
  private $modules = array();
9
  private $widgets = array();
10
  private $translations = array();
 
 
11
 
12
 
13
  public function __construct() {
18
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
19
  add_action( 'admin_print_styles', array( $this, 'add_styles' ) );
20
 
21
+ add_filter( 'screen_settings', array( $this, 'filter_screen_settings' ) );
 
22
 
23
+ $this->set_translation_strings();
 
24
 
25
 
26
  require( dirname( __FILE__ ) . '/module-settings.php' );
48
  }
49
 
50
  $vars = array(
51
+ 'ajax_action' => 'itsec_logs_page',
52
+ 'ajax_nonce' => wp_create_nonce( 'itsec-logs-nonce' ),
53
  'logs_page_url' => ITSEC_Core::get_logs_page_url(),
54
  'translations' => $this->translations,
55
  );
56
 
57
+ wp_enqueue_script( 'itsec-logs-page-script', plugins_url( 'js/logs.js', __FILE__ ), array( 'jquery-ui-dialog' ), $this->version, true );
58
+ wp_localize_script( 'itsec-logs-page-script', 'itsec_page', $vars );
59
  }
60
 
61
  public function add_styles() {
65
 
66
  private function set_translation_strings() {
67
  $this->translations = array(
68
+ 'show_raw_details' => esc_html__( 'Show Raw Details', 'better-wp-security' ),
69
+ 'hide_raw_details' => esc_html__( 'Hide Raw Details', 'better-wp-security' ),
70
+ 'loading' => esc_html__( 'Loading...', 'better-wp-security' ),
71
+ /* translators: 1: loading gif image */
72
+ 'log_migration_started' => esc_html__( '%1$s Migrating log entries from an older format. This message will update when the migration is complete.', 'better-wp-security' ),
73
+ 'log_migration_failed' => esc_html__( 'The log entry migration failed. Reload the page to try again.', 'better-wp-security' ),
74
+ 'log_migration_loading_url' => admin_url( 'images/loading.gif' ),
75
 
76
+ 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
77
 
78
+ 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
79
 
80
+ 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
81
 
82
+ 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
83
 
84
+ 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
85
 
86
+ 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
87
 
88
+ 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
89
  );
90
 
91
  foreach ( $this->translations as $key => $message ) {
96
  }
97
  }
98
 
99
+ private function handle_post() {
100
+ if ( ITSEC_Core::is_ajax_request() ) {
 
101
  return;
102
  }
103
 
104
+ if ( ! empty( $_POST['screenoptionnonce'] ) ) {
105
+ check_admin_referer( 'screen-options-nonce', 'screenoptionnonce' );
 
 
106
 
107
+ if ( isset( $_POST['apply'] ) ) {
108
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
 
 
109
 
110
+ $options = ITSEC_Form::get_post_data( array_keys( ITSEC_Log_Util::get_logs_page_screen_options() ) );
111
+ ITSEC_Log_Util::set_logs_page_screen_options( $options );
112
 
113
+ ITSEC_Response::add_message( __( 'Your screen options saved successfully.', 'better-wp-security' ) );
114
+ } else if ( isset( $_POST['mark_all_seen'] ) ) {
115
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
116
 
117
+ $options['last_seen'] = $_POST['current_time_gmt'];
118
+ ITSEC_Log_Util::set_logs_page_screen_options( $options );
119
+
120
+ ITSEC_Response::add_message( __( 'Log entries hidden.', 'better-wp-security' ) );
121
+ } else if ( isset( $_POST['mark_all_unseen'] ) ) {
122
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
123
+
124
+ $options['last_seen'] = 0;
125
+ ITSEC_Log_Util::set_logs_page_screen_options( $options );
126
+
127
+ ITSEC_Response::add_message( __( 'Log entries shown.', 'better-wp-security' ) );
128
+ }
129
+ } else if ( ! empty( $_POST['itsec_clear_logs'] ) && 'clear_logs' === $_POST['itsec_clear_logs'] ) {
130
  if ( ! wp_verify_nonce( $_POST['wp_nonce'], 'itsec_clear_logs' ) ) {
131
  die( __( 'Security error!', 'better-wp-security' ) );
132
  }
194
  }
195
  }
196
 
197
+ public function handle_ajax_request() {
198
+ if ( WP_DEBUG ) {
199
+ ini_set( 'display_errors', 1 );
200
+ }
201
 
202
+ if ( ! empty( $_POST['method'] ) && 'handle_logs_migration' === $_POST['method'] ) {
203
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
204
 
205
+ $complete = ITSEC_Log_Util::migrate_old_log_entries();
 
 
206
 
207
+ if ( $complete ) {
208
+ $message = '<p>' . esc_html__( 'Migration complete. Please refresh the page to see all log entries.', 'better-wp-security' ) . '</p>';
209
+ $message .= '<a href="' . esc_url( $this->self_url ) . '" class="button-secondary">' . esc_html__( 'Refresh Page', 'better-wp-security' ) . '</a>';
210
+
211
+ ITSEC_Response::set_response( $message );
212
  } else {
213
+ ITSEC_Response::set_response( 'incomplete' );
214
  }
215
+ } else {
216
+ ITSEC_Core::set_interactive( true );
217
 
218
+ $id = ( isset( $_POST['id'] ) && is_string( $_POST['id'] ) ) ? $_POST['id'] : '';
 
 
219
 
220
+ if ( empty( $GLOBALS['hook_suffix'] ) ) {
221
+ $GLOBALS['hook_suffix'] = 'toplevel_page_itsec';
222
+ }
223
 
 
 
224
 
225
+ if ( false === check_ajax_referer( 'itsec-logs-nonce', 'nonce', false ) ) {
226
+ ITSEC_Response::add_error( new WP_Error( 'itsec-logs-page-failed-nonce', __( 'A nonce security check failed, preventing the request from completing as expected. Please try reloading the page and trying again.', 'better-wp-security' ) ) );
227
+ } else if ( ! ITSEC_Core::current_user_can_manage() ) {
228
+ ITSEC_Response::add_error( new WP_Error( 'itsec-logs-page-insufficient-privileges', __( 'A permissions security check failed, preventing the request from completing as expected. The currently logged in user does not have sufficient permissions to make this request. Please try reloading the page and trying again.', 'better-wp-security' ) ) );
229
+ } else if ( empty( $id ) ) {
230
+ ITSEC_Response::add_error( new WP_Error( 'itsec-logs-page-missing-method', __( 'The server did not receive a valid request. The required "id" argument is missing. Please try again.', 'better-wp-security' ) ) );
231
+ } else {
232
+ ITSEC_Modules::load_module_file( 'logs.php' );
233
 
234
+ $entry = ITSEC_Log::get_entry( $id );
235
 
236
+ if ( false === strpos( $entry['code'], '::' ) ) {
237
+ $code = $entry['code'];
238
+ $code_data = array();
239
+ } else {
240
+ list( $code, $code_data ) = explode( '::', $entry['code'], 2 );
241
+ $code_data = explode( ',', $code_data );
242
+ }
243
 
244
+
245
+ $timestamp = strtotime( $entry['timestamp'] );
246
+ $datetime = date( 'Y-m-d H:i:s', $timestamp + ITSEC_Core::get_time_offset() );
247
+ $types = ITSEC_Log::get_types_for_display();
248
+
249
+ if ( isset( $types[$entry['type']] ) ) {
250
+ $type = $types[$entry['type']];
251
+ } else {
252
+ $type = esc_html( $entry['type'] );
253
+ }
254
+
255
+ $user = get_user_by( 'id', $entry['user_id'] );
256
+
257
+ if ( $user ) {
258
+ $username = $user->user_login;
259
+ } else {
260
+ $username = '';
261
+ }
262
+
263
+ $details = array(
264
+ 'module' => array(
265
+ 'header' => esc_html__( 'Module', 'better-wp-security' ),
266
+ 'content' => esc_html( $entry['module'] ),
267
+ ),
268
+ 'type' => array(
269
+ 'header' => esc_html__( 'Type', 'better-wp-security' ),
270
+ 'content' => $type,
271
+ ),
272
+ 'description' => array(
273
+ 'header' => esc_html__( 'Description', 'better-wp-security' ),
274
+ 'content' => esc_html( $code ),
275
+ ),
276
+ 'timestamp' => array(
277
+ 'header' => esc_html__( 'Timestamp', 'better-wp-security' ),
278
+ 'content' => esc_html( $datetime ),
279
+ ),
280
+ 'host' => array(
281
+ 'header' => esc_html__( 'Host', 'better-wp-security' ),
282
+ 'content' => '<code>' . esc_html( $entry['remote_ip'] ) . '</code>',
283
+ ),
284
+ 'user' => array(
285
+ 'header' => esc_html__( 'User', 'better-wp-security' ),
286
+ 'content' => esc_html( $username ),
287
+ ),
288
+ 'url' => array(
289
+ 'header' => esc_html__( 'URL', 'better-wp-security' ),
290
+ 'content' => '<code>' . esc_html( $entry['url'] ) . '</code>',
291
+ ),
292
+ 'raw-details' => array(
293
+ 'header' => esc_html__( 'Raw Details', 'better-wp-security' ),
294
+ 'content' => true,
295
+ ),
296
+ );
297
+
298
+
299
+ $details = apply_filters( "itsec_logs_prepare_{$entry['module']}_entry_for_details_display", $details, $entry, $code, $code_data );
300
+
301
+ if ( isset( $details['raw-details'] ) ) {
302
+ if ( true === $details['raw-details']['content'] ) {
303
+ // Ensure that Raw Details is listed last.
304
+ $raw_details = $details['raw-details'];
305
+ unset( $details['raw-details'] );
306
+
307
+ if ( empty( $entry['parent_id'] ) ) {
308
+ unset( $entry['parent_id'] );
309
+ }
310
+
311
+ if ( strlen( serialize( $entry['data'] ) ) > 1048576 ) {
312
+ // Don't run the risk of crashing the process when trying to display a large data set.
313
+ $entry['data'] = '[' . esc_html__( 'Too large to display', 'better-wp-security' ) . ']';
314
+ }
315
+
316
+ $raw_details['content'] = '<pre>' . preg_replace( '/^ /m', '', substr( ITSEC_Lib::get_print_r( $entry ), 23 ) ) . '</pre>';
317
+ $details['raw-details'] = $raw_details;
318
+ }
319
+
320
+ $details['raw-details']['content'] = '<p><a class="itsec-log-raw-details-toggle" href="#">' . $this->translations['show_raw_details'] . '</a></p><div class="itsec-log-raw-details">' . $details['raw-details']['content'] . '</div>';
321
+ }
322
+ }
323
+
324
+ ob_start();
325
+
326
+ ?>
327
+ <table class="form-table">
328
+ <?php foreach ( $details as $row ) : ?>
329
+ <tr>
330
+ <th scope="row"><?php echo $row['header']; ?></th>
331
+ <td><?php echo $row['content'] ?></td>
332
+ </tr>
333
+ <?php endforeach; ?>
334
+ </table>
335
+ <?php
336
+
337
+ ITSEC_Response::set_response( ob_get_clean() );
338
  }
339
+
340
+
341
+ ITSEC_Response::send_json();
342
  }
343
 
344
+ public function filter_screen_settings( $settings ) {
345
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
346
+ $options = ITSEC_Log_Util::get_logs_page_screen_options();
347
 
348
+ $form = new ITSEC_Form( $options );
349
 
350
+ ob_start();
 
 
 
 
351
 
352
+ ?>
353
+ <fieldset class="screen-options">
354
+ <legend><?php esc_html_e( 'Pagination' ); ?></legend>
355
+ <label for="itsec_logs_page_entries_per_page"><?php esc_html_e( 'Number of items per page:' ); ?></label>
356
+ <?php $form->add_number( 'per_page', array( 'step' => 1, 'min' => 1, 'max' => 999, 'maxlength' => 3 ) ); ?>
357
+ </fieldset>
358
+
359
+ <fieldset>
360
+ <legend><?php esc_html_e( 'View Mode' ); ?></legend>
361
+
362
+ <label for="itsec-default_view-important">
363
+ <?php $form->add_radio( 'default_view', 'important' ); ?>
364
+ <?php esc_html_e( 'Important Events', 'better-wp-security' ); ?>
365
+ </label>
366
+ <label for="itsec-default_view-all">
367
+ <?php $form->add_radio( 'default_view', 'all' ); ?>
368
+ <?php esc_html_e( 'All Events', 'better-wp-security' ); ?>
369
+ </label>
370
+ <label for="itsec-default_view-critical-issue">
371
+ <?php $form->add_radio( 'default_view', 'critical-issue' ); ?>
372
+ <?php esc_html_e( 'Critical Issues', 'better-wp-security' ); ?>
373
+ </label>
374
+ </fieldset>
375
+
376
+ <fieldset>
377
+ <legend><?php esc_html_e( 'Colors', 'better-wp-security' ); ?></legend>
378
+ <label for="itsec-color">
379
+ <?php $form->add_checkbox( 'color' ); ?>
380
+ <?php esc_html_e( 'Use colors to indicate the severity of each entry.', 'better-wp-security' ); ?>
381
+ </label>
382
+ </fieldset>
383
+
384
+ <fieldset>
385
+ <legend><?php esc_html_e( 'Advanced Entries for Support and Developers', 'better-wp-security' ); ?></legend>
386
+ <label for="itsec-show_debug">
387
+ <?php $form->add_checkbox( 'show_debug' ); ?>
388
+ <?php esc_html_e( 'Show Debug entries.', 'better-wp-security' ); ?>
389
+ </label>
390
+ <br />
391
+ <label for="itsec-show_process">
392
+ <?php $form->add_checkbox( 'show_process' ); ?>
393
+ <?php esc_html_e( 'Show Process entries.', 'better-wp-security' ); ?>
394
+ </label>
395
+ </fieldset>
396
+
397
+ <p class="submit">
398
+ <?php $form->add_submit( 'apply', __( 'Apply' ) ); ?>
399
+ </p>
400
+
401
+ <p class="submit">
402
+ <?php $form->add_submit( 'mark_all_seen', array( 'class' => 'button-secondary', 'value' => esc_html__( 'Hide Current Entries', 'better-wp-security' ), 'title' => esc_html__( 'Hide existing entries from view without deleting them.', 'better-wp-security' ) ) ); ?>
403
+ &nbsp;
404
+ <?php $form->add_submit( 'mark_all_unseen', array( 'class' => 'button-secondary', 'value' => esc_html__( 'Show All Entries', 'better-wp-security' ), 'title' => esc_html__( 'Show all entries, including ones that were previously hidden.', 'better-wp-security' ) ) ); ?>
405
+ <?php $form->add_hidden( 'current_time_gmt', ITSEC_Core::get_current_time_gmt() ); ?>
406
+ </p>
407
+ <?php
408
 
409
+ return ob_get_clean();
410
+ }
411
 
412
+ public function handle_page_load( $self_url ) {
413
+ $this->self_url = $self_url;
414
 
415
+ $this->show_settings_page();
416
+ }
 
417
 
418
+ private function show_settings_page() {
419
+ require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-wp-list-table.php' );
420
+ require_once( ITSEC_Core::get_core_dir() . '/admin-pages/logs-list-table.php' );
421
 
422
 
423
+ $form = new ITSEC_Form();
424
 
425
  ?>
426
  <div class="wrap">
445
  <div id="poststuff">
446
  <div id="post-body" class="metabox-holder columns-2 hide-if-no-js">
447
  <div id="postbox-container-2" class="postbox-container">
448
+ <?php $this->show_old_logs_migration(); ?>
449
+
450
  <?php if ( 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' ) ) : ?>
451
  <p><?php _e( 'To view logs within the plugin you must enable database logging in the Global Settings. File logging is not available for access within the plugin itself.', 'better-wp-security' ); ?></p>
452
+ <p><?php printf( wp_kses( __( 'The log file can be found at: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), ITSEC_Log::get_log_file_path() ); ?></p>
453
  <?php else : ?>
454
  <div class="itsec-module-cards-container list">
455
+ <?php
456
+ $list = new ITSEC_Logs_List_Table();
457
+
458
+ $list->prepare_items();
459
+ $list->views();
460
+ $form->start_form( array( 'method' => 'GET' ) );
461
+ $list->display();
462
+ $form->end_form();
463
+ ?>
 
 
 
464
  </div>
465
  <?php endif; ?>
466
  </div>
467
  <div class="itsec-modal-background"></div>
468
+ <div id="itsec-log-details-container" class="grid">
469
+ <div class="itsec-module-settings-container">
470
+ <div class="itsec-modal-navigation">
471
+ <button class="dashicons itsec-close-modal"></button>
472
+ </div>
473
+ <div class="itsec-module-settings-content-container">
474
+ <div class="itsec-module-settings-content">
475
+ <div class="itsec-module-messages-container"></div>
476
+ <div class="itsec-module-settings-content-main"></div>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
 
482
  <div id="postbox-container-1" class="postbox-container">
483
  <?php foreach ( $this->widgets as $id => $widget ) : ?>
498
  <div class="hide-if-js">
499
  <p class="itsec-warning-message"><?php _e( 'iThemes Security requires Javascript in order for the settings to be modified. Please enable Javascript to configure the settings.', 'better-wp-security' ); ?></p>
500
  </div>
501
+
502
+ <div class="hidden" id="itsec-logs-cache">
503
+ </div>
504
  </div>
505
  </div>
506
  <?php
507
 
508
  }
509
 
510
+ public function register_widget( $widget ) {
511
+ if ( ! is_object( $widget ) || ! is_a( $widget, 'ITSEC_Settings_Page_Sidebar_Widget' ) ) {
512
+ trigger_error( 'An invalid widget was registered.', E_USER_ERROR );
513
+ return;
514
+ }
515
 
516
+ if ( isset( $this->modules[$widget->id] ) ) {
517
+ trigger_error( "A widget with the id of {$widget->id} is registered. Widget id's must be unique from any other module or widget." );
518
+ return;
 
 
519
  }
520
 
521
+ if ( isset( $this->widgets[$widget->id] ) ) {
522
+ trigger_error( "A widget with the id of {$widget->id} is already registered. Widget id's must be unique from any other module or widget." );
523
+ return;
524
+ }
525
 
526
+
527
+ $this->widgets[$widget->id] = $widget;
528
+ }
529
+
530
+ private function get_widget_settings( $id, $form = false, $echo = false ) {
531
+ if ( ! isset( $this->widgets[$id] ) ) {
532
+ $error = new WP_Error( 'itsec-settings-page-get-widget-settings-invalid-id', sprintf( __( 'The requested widget (%s) does not exist. Logs for it cannot be rendered.', 'better-wp-security' ), $id ) );
533
+
534
+ if ( $echo ) {
535
+ ITSEC_Lib::show_error_message( $error );
536
+ } else {
537
+ return $error;
538
+ }
539
  }
540
 
541
+ if ( false === $form ) {
542
+ $form = new ITSEC_Form();
543
+ }
544
 
545
+ $widget = $this->widgets[$id];
546
+
547
+ $form->add_input_group( $id );
548
+ $form->set_defaults( $widget->get_defaults() );
549
+
550
+ if ( ! $echo ) {
551
+ ob_start();
552
  }
 
553
 
554
+ $widget->render( $form );
555
+
556
+ $form->remove_all_input_groups();
557
+
558
+ if ( ! $echo ) {
559
+ return ob_get_clean();
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }
561
  }
562
 
563
+ private function show_old_logs_migration() {
564
+ require_once( ITSEC_Core::get_core_dir() . '/lib/log-util.php' );
565
+
566
+ if ( ITSEC_Log_Util::has_old_log_entries() ) {
567
+ echo '<div id="old-logs-migration-status"></div>';
568
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  }
570
  }
571
 
core/admin-pages/page-settings.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
 
4
  final class ITSEC_Settings_Page {
5
- private $version = 1.9;
6
 
7
  private static $instance;
8
 
@@ -94,7 +94,7 @@ final class ITSEC_Settings_Page {
94
  }
95
 
96
  wp_enqueue_script( 'itsec-scrollTo', plugins_url( 'js/scrollTo.js', dirname( __FILE__ ) ), array( 'jquery' ) );
97
- wp_enqueue_script( 'itsec-settings-page-script', plugins_url( 'js/script.js', __FILE__ ), array( 'underscore' ), $this->version, true );
98
  wp_localize_script( 'itsec-settings-page-script', 'itsec_page', $vars );
99
  }
100
 
@@ -120,20 +120,6 @@ final class ITSEC_Settings_Page {
120
 
121
  /* translators: 1: module name */
122
  'successful_save' => __( 'Settings saved successfully for %1$s.', 'better-wp-security' ),
123
-
124
- 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
125
-
126
- 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
127
-
128
- 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
129
-
130
- 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
131
-
132
- 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
133
-
134
- 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
135
-
136
- 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
137
  );
138
 
139
  foreach ( $this->translations as $key => $message ) {
2
 
3
 
4
  final class ITSEC_Settings_Page {
5
+ private $version = 2.0;
6
 
7
  private static $instance;
8
 
94
  }
95
 
96
  wp_enqueue_script( 'itsec-scrollTo', plugins_url( 'js/scrollTo.js', dirname( __FILE__ ) ), array( 'jquery' ) );
97
+ wp_enqueue_script( 'itsec-settings-page-script', plugins_url( 'js/settings.js', __FILE__ ), array( 'underscore' ), $this->version, true );
98
  wp_localize_script( 'itsec-settings-page-script', 'itsec_page', $vars );
99
  }
100
 
120
 
121
  /* translators: 1: module name */
122
  'successful_save' => __( 'Settings saved successfully for %1$s.', 'better-wp-security' ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  );
124
 
125
  foreach ( $this->translations as $key => $message ) {
core/core.php CHANGED
@@ -10,7 +10,6 @@
10
  * @since 4.0
11
  *
12
  * @global array $itsec_globals Global variables for use throughout iThemes Security.
13
- * @global object $itsec_logger iThemes Security logging class.
14
  * @global object $itsec_lockout Class for handling lockouts.
15
  *
16
  */
@@ -25,7 +24,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
25
  *
26
  * @access private
27
  */
28
- private $plugin_build = 4080;
29
 
30
  /**
31
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
@@ -88,7 +87,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
88
  *
89
  */
90
  public function init( $plugin_file, $plugin_name ) {
91
- global $itsec_globals, $itsec_logger, $itsec_lockout;
92
 
93
  $this->plugin_file = $plugin_file;
94
  $this->plugin_dir = dirname( $plugin_file ) . '/';
@@ -111,7 +110,6 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
111
  ITSEC_Modules::init_modules();
112
 
113
  require( $this->plugin_dir . 'core/lib.php' );
114
- require( $this->plugin_dir . 'core/logger.php' );
115
  require( $this->plugin_dir . 'core/lockout.php' );
116
  require( $this->plugin_dir . 'core/files.php' );
117
  require( $this->plugin_dir . 'core/notify.php' );
@@ -119,12 +117,13 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
119
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-user-activity.php' );
120
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-password-requirements.php' );
121
 
 
 
122
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
123
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
124
 
125
  $this->itsec_files = ITSEC_Files::get_instance();
126
  $this->itsec_notify = new ITSEC_Notify();
127
- $itsec_logger = new ITSEC_Logger();
128
  $itsec_lockout = new ITSEC_Lockout();
129
  $itsec_lockout->run();
130
 
10
  * @since 4.0
11
  *
12
  * @global array $itsec_globals Global variables for use throughout iThemes Security.
 
13
  * @global object $itsec_lockout Class for handling lockouts.
14
  *
15
  */
24
  *
25
  * @access private
26
  */
27
+ private $plugin_build = 4084;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
87
  *
88
  */
89
  public function init( $plugin_file, $plugin_name ) {
90
+ global $itsec_globals, $itsec_lockout;
91
 
92
  $this->plugin_file = $plugin_file;
93
  $this->plugin_dir = dirname( $plugin_file ) . '/';
110
  ITSEC_Modules::init_modules();
111
 
112
  require( $this->plugin_dir . 'core/lib.php' );
 
113
  require( $this->plugin_dir . 'core/lockout.php' );
114
  require( $this->plugin_dir . 'core/files.php' );
115
  require( $this->plugin_dir . 'core/notify.php' );
117
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-user-activity.php' );
118
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-password-requirements.php' );
119
 
120
+ require( $this->plugin_dir . 'core/lib/log.php' );
121
+
122
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
123
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
124
 
125
  $this->itsec_files = ITSEC_Files::get_instance();
126
  $this->itsec_notify = new ITSEC_Notify();
 
127
  $itsec_lockout = new ITSEC_Lockout();
128
  $itsec_lockout->run();
129
 
core/files.php CHANGED
@@ -143,9 +143,9 @@ final class ITSEC_Files {
143
  $host_rule .= "</IfModule>\n";
144
  $host_rule .= "<IfModule !mod_authz_core.c>\n";
145
  $host_rule .= "\tOrder allow,deny\n";
 
146
  $host_rule .= "\tDeny from env=DenyAccess\n";
147
  $host_rule .= "\tDeny from $host\n";
148
- $host_rule .= "\tAllow from all\n";
149
  $host_rule .= "</IfModule>\n";
150
  }
151
 
143
  $host_rule .= "</IfModule>\n";
144
  $host_rule .= "<IfModule !mod_authz_core.c>\n";
145
  $host_rule .= "\tOrder allow,deny\n";
146
+ $host_rule .= "\tAllow from all\n";
147
  $host_rule .= "\tDeny from env=DenyAccess\n";
148
  $host_rule .= "\tDeny from $host\n";
 
149
  $host_rule .= "</IfModule>\n";
150
  }
151
 
core/history.txt CHANGED
@@ -618,11 +618,11 @@
618
  Bug Fix: Prevent multiple cron tests from being scheduled at once.
619
  Bug Fix: Cron test being stuck in a loop preventing a site from switching back to the cron scheduler.
620
  Bug Fix: Prevent warnings when a single and recurring event were scheduled at the same time.
621
- 4.0.3 - 2017-01-04 - Chris Jean & Timothy Jacobs
622
  Bug Fix: Fix scheduling retries for Malware Scans on sites that don't fully support WordPress's cron system.
623
  Bug Fix: Reactivating Away Mode now replaces the active file if you had previously removed it.
624
  Bug Fix: Ensure lockouts take effect immediately, even on systems where changes to server configuration files do not take effect immediately.
625
- 4.0.4 - 2017-01-29 - Chris Jean & Timothy Jacobs
626
  Enhancement: Display user lockouts in Lockout Sidebar.
627
  Bug Fix: Load translations on the plugins_loaded hook.
628
  Bug Fix: Fixed method that could be used to discover hidden login slug on some sites.
@@ -630,3 +630,11 @@
630
  Bug Fix: Update to the REST API "Restricted Access" feature to protect against methods to work around the restricted access.
631
  Bug Fix: Prevent login page being hidden when following the "Confirm Email Address" notification URL.
632
  Bug Fix: Hide Backend notifications not being properly sent when first enabled.
 
 
 
 
 
 
 
 
618
  Bug Fix: Prevent multiple cron tests from being scheduled at once.
619
  Bug Fix: Cron test being stuck in a loop preventing a site from switching back to the cron scheduler.
620
  Bug Fix: Prevent warnings when a single and recurring event were scheduled at the same time.
621
+ 4.0.3 - 2018-01-04 - Chris Jean & Timothy Jacobs
622
  Bug Fix: Fix scheduling retries for Malware Scans on sites that don't fully support WordPress's cron system.
623
  Bug Fix: Reactivating Away Mode now replaces the active file if you had previously removed it.
624
  Bug Fix: Ensure lockouts take effect immediately, even on systems where changes to server configuration files do not take effect immediately.
625
+ 4.0.4 - 2018-01-29 - Chris Jean & Timothy Jacobs
626
  Enhancement: Display user lockouts in Lockout Sidebar.
627
  Bug Fix: Load translations on the plugins_loaded hook.
628
  Bug Fix: Fixed method that could be used to discover hidden login slug on some sites.
630
  Bug Fix: Update to the REST API "Restricted Access" feature to protect against methods to work around the restricted access.
631
  Bug Fix: Prevent login page being hidden when following the "Confirm Email Address" notification URL.
632
  Bug Fix: Hide Backend notifications not being properly sent when first enabled.
633
+ 4.1.0 - 2018-02-08 - Chris Jean & Timothy Jacobs
634
+ Enhancement: Updated logging system to keep track of more information and have more options to filter and sort log entries.
635
+ Enhancement: Improved efficiency of File Change Detection scanning.
636
+ Bug Fix: Fixed issue that could register loading the logging page as a failed login attempt on some sites.
637
+ 4.1.1 - 2018-02-08 - Chris Jean & Timothy Jacobs
638
+ Bug Fix: Fixed schema issue with new logs table.
639
+ 4.1.2 - 2018-02-12 - Chris Jean & Timothy Jacobs
640
+ Bug Fix: Fixed "undefined offset" error when displaying specific migrated old log entries.
core/lib.php CHANGED
@@ -52,84 +52,14 @@ final class ITSEC_Lib {
52
  /**
53
  * Creates appropriate database tables.
54
  *
55
- * Uses dbdelta to create database tables either on activation or in the event that one is missing.
56
- *
57
  * @since 4.0.0
58
  *
59
  * @return void
60
  */
61
  public static function create_database_tables() {
 
62
 
63
- global $wpdb;
64
-
65
- $charset_collate = '';
66
-
67
- if ( ! empty( $wpdb->charset ) ) {
68
- $charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
69
- }
70
-
71
- if ( ! empty( $wpdb->collate ) ) {
72
- $charset_collate .= " COLLATE $wpdb->collate";
73
- }
74
-
75
- //Set up log table
76
- $tables = "CREATE TABLE " . $wpdb->base_prefix . "itsec_log (
77
- log_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
78
- log_type varchar(20) NOT NULL DEFAULT '',
79
- log_function varchar(255) NOT NULL DEFAULT '',
80
- log_priority int(2) NOT NULL DEFAULT 1,
81
- log_date datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
82
- log_date_gmt datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
83
- log_host varchar(40),
84
- log_username varchar(60),
85
- log_user bigint(20) UNSIGNED,
86
- log_url varchar(255),
87
- log_referrer varchar(255),
88
- log_data longtext NOT NULL,
89
- PRIMARY KEY (log_id),
90
- KEY log_type (log_type),
91
- KEY log_date_gmt (log_date_gmt)
92
- ) " . $charset_collate . ";";
93
-
94
- //set up lockout table
95
- $tables .= "CREATE TABLE " . $wpdb->base_prefix . "itsec_lockouts (
96
- lockout_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
97
- lockout_type varchar(20) NOT NULL,
98
- lockout_start datetime NOT NULL,
99
- lockout_start_gmt datetime NOT NULL,
100
- lockout_expire datetime NOT NULL,
101
- lockout_expire_gmt datetime NOT NULL,
102
- lockout_host varchar(40),
103
- lockout_user bigint(20) UNSIGNED,
104
- lockout_username varchar(60),
105
- lockout_active int(1) NOT NULL DEFAULT 1,
106
- PRIMARY KEY (lockout_id),
107
- KEY lockout_expire_gmt (lockout_expire_gmt),
108
- KEY lockout_host (lockout_host),
109
- KEY lockout_user (lockout_user),
110
- KEY lockout_username (lockout_username),
111
- KEY lockout_active (lockout_active)
112
- ) " . $charset_collate . ";";
113
-
114
- //set up temp table
115
- $tables .= "CREATE TABLE " . $wpdb->base_prefix . "itsec_temp (
116
- temp_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
117
- temp_type varchar(20) NOT NULL,
118
- temp_date datetime NOT NULL,
119
- temp_date_gmt datetime NOT NULL,
120
- temp_host varchar(40),
121
- temp_user bigint(20) UNSIGNED,
122
- temp_username varchar(60),
123
- PRIMARY KEY (temp_id),
124
- KEY temp_date_gmt (temp_date_gmt),
125
- KEY temp_host (temp_host),
126
- KEY temp_user (temp_user),
127
- KEY temp_username (temp_username)
128
- ) " . $charset_collate . ";";
129
-
130
- require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
131
- @dbDelta( $tables );
132
-
133
  }
134
 
135
  /**
@@ -703,6 +633,12 @@ final class ITSEC_Lib {
703
  * @param string $username
704
  */
705
  public static function handle_wp_login_failed( $username ) {
 
 
 
 
 
 
706
  $authentication_types = array();
707
 
708
  if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
@@ -739,9 +675,8 @@ final class ITSEC_Lib {
739
  }
740
 
741
  $details = compact( 'source', 'authentication_types' );
742
- $details = apply_filters( 'itsec-filter-failed-login-details', $details );
743
 
744
- do_action( 'itsec-handle-failed-login', $username, $details );
745
  }
746
 
747
  /**
@@ -1084,6 +1019,18 @@ final class ITSEC_Lib {
1084
  return $array;
1085
  }
1086
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  /**
1088
  * Check if WP Cron appears to be running properly.
1089
  *
52
  /**
53
  * Creates appropriate database tables.
54
  *
 
 
55
  * @since 4.0.0
56
  *
57
  * @return void
58
  */
59
  public static function create_database_tables() {
60
+ require_once( ITSEC_Core::get_core_dir() . '/lib/schema.php' );
61
 
62
+ ITSEC_Schema::create_database_tables();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
 
65
  /**
633
  * @param string $username
634
  */
635
  public static function handle_wp_login_failed( $username ) {
636
+ $details = self::get_login_details();
637
+
638
+ do_action( 'itsec-handle-failed-login', $username, $details );
639
+ }
640
+
641
+ public static function get_login_details() {
642
  $authentication_types = array();
643
 
644
  if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
675
  }
676
 
677
  $details = compact( 'source', 'authentication_types' );
 
678
 
679
+ return apply_filters( 'itsec-filter-failed-login-details', $details );
680
  }
681
 
682
  /**
1019
  return $array;
1020
  }
1021
 
1022
+ public static function print_r( $data, $args = array() ) {
1023
+ require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' );
1024
+
1025
+ ITSEC_Debug::print_r( $data, $args );
1026
+ }
1027
+
1028
+ public static function get_print_r( $data, $args = array() ) {
1029
+ require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' );
1030
+
1031
+ return ITSEC_Debug::get_print_r( $data, $args );
1032
+ }
1033
+
1034
  /**
1035
  * Check if WP Cron appears to be running properly.
1036
  *
core/lib/class-itsec-scheduler-cron.php CHANGED
@@ -86,6 +86,23 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
86
  }
87
 
88
  public function is_single_scheduled( $id, $data = array() ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return (bool) wp_next_scheduled( self::HOOK, array( $id, $this->hash_data( $data ) ) );
90
  }
91
 
86
  }
87
 
88
  public function is_single_scheduled( $id, $data = array() ) {
89
+
90
+ if ( null === $data ) {
91
+ $options = $this->get_options();
92
+
93
+ if ( ! isset( $options['single'][ $id ] ) ) {
94
+ return false;
95
+ }
96
+
97
+ foreach ( $options['single'][ $id ] as $hash => $event ) {
98
+ if ( wp_next_scheduled( self::HOOK, array( $id, $hash ) ) ) {
99
+ return true;
100
+ }
101
+ }
102
+
103
+ return false;
104
+ }
105
+
106
  return (bool) wp_next_scheduled( self::HOOK, array( $id, $this->hash_data( $data ) ) );
107
  }
108
 
core/lib/class-itsec-scheduler-page-load.php CHANGED
@@ -79,15 +79,18 @@ class ITSEC_Scheduler_Page_Load extends ITSEC_Scheduler {
79
 
80
  public function is_single_scheduled( $id, $data = array() ) {
81
 
82
- $hash = $this->hash_data( $data );
83
  $options = $this->get_options();
84
 
85
  if ( empty( $options['single'][ $id ] ) ) {
86
  return false;
87
  }
88
 
89
- if ( empty( $options['single'][ $id ][ $hash ] ) ) {
90
- return false;
 
 
 
 
91
  }
92
 
93
  return true;
79
 
80
  public function is_single_scheduled( $id, $data = array() ) {
81
 
 
82
  $options = $this->get_options();
83
 
84
  if ( empty( $options['single'][ $id ] ) ) {
85
  return false;
86
  }
87
 
88
+ if ( null !== $data ) {
89
+ $hash = $this->hash_data( $data );
90
+
91
+ if ( empty( $options['single'][ $id ][ $hash ] ) ) {
92
+ return false;
93
+ }
94
  }
95
 
96
  return true;
core/lib/class-itsec-scheduler.php CHANGED
@@ -68,8 +68,9 @@ abstract class ITSEC_Scheduler {
68
  /**
69
  * Is a single event scheduled with the given data.
70
  *
71
- * @param string $id
72
- * @param array $data
 
73
  *
74
  * @return bool
75
  */
68
  /**
69
  * Is a single event scheduled with the given data.
70
  *
71
+ * @param string $id The event ID to check.
72
+ * @param array|null $data The event data. Pass null to check if any event is scheduled with that ID,
73
+ * regardless of the data.
74
  *
75
  * @return bool
76
  */
core/lib/class-itsec-wp-list-table.php CHANGED
@@ -1,100 +1,142 @@
1
  <?php
2
  /**
3
- * Copy of class WP_List_Table in case of change
4
  *
5
- * @package iThemes Security
6
- * @since 4.0
 
7
  */
8
 
9
  /**
10
  * Base class for displaying a list of items in an ajaxified HTML table.
11
  *
12
- * @package WordPress
13
- * @subpackage List_Table
14
- * @since 3.1.0
15
- * @access private
16
  */
17
  class ITSEC_WP_List_Table {
18
 
19
  /**
20
- * The current list of items
21
  *
22
- * @since 3.1.0
23
  * @var array
24
- * @access protected
25
  */
26
- var $items;
27
 
28
  /**
29
- * Various information about the current table
30
  *
31
- * @since 3.1.0
32
  * @var array
33
- * @access private
34
  */
35
- var $_args;
36
 
37
  /**
38
- * Various information needed for displaying the pagination
39
  *
40
- * @since 3.1.0
41
  * @var array
42
- * @access private
43
  */
44
- var $_pagination_args = array();
45
 
46
  /**
47
- * The current screen
48
  *
49
- * @since 3.1.0
50
  * @var object
51
- * @access protected
52
  */
53
- var $screen;
54
 
55
  /**
56
- * Cached bulk actions
57
  *
58
- * @since 3.1.0
59
  * @var array
60
- * @access private
61
  */
62
- var $_actions;
63
 
64
  /**
65
- * Cached pagination output
66
  *
67
- * @since 3.1.0
68
  * @var string
69
- * @access private
70
  */
71
- var $_pagination;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  /**
74
- * Constructor. The child class should call this constructor from its own constructor
75
  *
76
- * @param array $args An associative array with information about the current table
 
 
 
 
 
77
  *
78
- * @access protected
79
  */
80
- function __construct( $args = array() ) {
 
 
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  $args = wp_parse_args( $args, array(
83
- 'plural' => '',
84
  'singular' => '',
85
- 'ajax' => false,
86
- 'screen' => null,
87
  ) );
88
 
89
  $this->screen = convert_to_screen( $args['screen'] );
90
 
91
  add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
92
 
93
- if ( ! $args['plural'] ) {
94
  $args['plural'] = $this->screen->base;
95
- }
96
 
97
- $args['plural'] = sanitize_key( $args['plural'] );
98
  $args['singular'] = sanitize_key( $args['singular'] );
99
 
100
  $this->_args = $args;
@@ -103,57 +145,127 @@ class ITSEC_WP_List_Table {
103
  // wp_enqueue_script( 'list-table' );
104
  add_action( 'admin_footer', array( $this, '_js_vars' ) );
105
  }
 
 
 
 
 
 
 
106
  }
107
 
108
  /**
109
- * Checks the current user's permissions
110
  *
111
- * @uses wp_die()
112
  *
113
- * @since 3.1.0
114
- * @access public
115
- * @abstract
116
  */
117
- function ajax_user_can() {
 
 
 
 
118
 
119
- die( 'function WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
 
122
  /**
123
- * Prepares the list of items for displaying.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  *
125
- * @uses WP_List_Table::set_pagination_args()
126
  *
127
- * @since 3.1.0
128
- * @access public
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  * @abstract
130
  */
131
- function prepare_items() {
 
 
132
 
133
- die( 'function WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
 
 
 
 
 
 
 
 
134
  }
135
 
136
  /**
137
  * An internal method that sets all the necessary pagination arguments
138
  *
139
- * @param array $args An associative array with information about the pagination
140
  *
141
- * @access protected
142
  */
143
- function set_pagination_args( $args ) {
144
-
145
  $args = wp_parse_args( $args, array(
146
  'total_items' => 0,
147
  'total_pages' => 0,
148
- 'per_page' => 0,
149
  ) );
150
 
151
- if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
152
  $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
153
- }
154
 
155
- // redirect if page number is invalid and headers are not already sent
156
- if ( ! headers_sent() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
157
  wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
158
  exit;
159
  }
@@ -162,18 +274,16 @@ class ITSEC_WP_List_Table {
162
  }
163
 
164
  /**
165
- * Access the pagination args
166
  *
167
- * @since 3.1.0
168
- * @access public
169
  *
170
- * @param string $key
171
- *
172
- * @return array
173
  */
174
- function get_pagination_arg( $key ) {
175
-
176
- if ( 'page' == $key ) {
177
  return $this->get_pagenum();
178
  }
179
 
@@ -185,92 +295,77 @@ class ITSEC_WP_List_Table {
185
  /**
186
  * Whether the table has items to display or not
187
  *
188
- * @since 3.1.0
189
- * @access public
190
  *
191
  * @return bool
192
  */
193
- function has_items() {
194
-
195
- return ! empty( $this->items );
196
  }
197
 
198
  /**
199
  * Message to be displayed when there are no items
200
  *
201
- * @since 3.1.0
202
- * @access public
203
  */
204
- function no_items() {
205
-
206
  _e( 'No items found.' );
207
  }
208
 
209
  /**
210
- * Display the search box.
211
  *
212
- * @since 3.1.0
213
- * @access public
214
  *
215
- * @param string $text The search button text
216
- * @param string $input_id The search input id
217
  */
218
- function search_box( $text, $input_id ) {
219
-
220
- if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
221
  return;
222
- }
223
 
224
  $input_id = $input_id . '-search-input';
225
 
226
- if ( ! empty( $_REQUEST['orderby'] ) ) {
227
  echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
228
- }
229
- if ( ! empty( $_REQUEST['order'] ) ) {
230
  echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
231
- }
232
- if ( ! empty( $_REQUEST['post_mime_type'] ) ) {
233
  echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
234
- }
235
- if ( ! empty( $_REQUEST['detached'] ) ) {
236
  echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
237
- }
238
- ?>
239
- <p class="search-box">
240
- <label class="screen-reader-text" for="<?php echo $input_id ?>"><?php echo $text; ?>:</label> <input
241
- type="search" id="<?php echo $input_id ?>" name="s" value="<?php _admin_search_query(); ?>"/>
242
- <?php submit_button( $text, 'button', false, false, array( 'id' => 'search-submit' ) ); ?>
243
- </p>
244
- <?php
245
  }
246
 
247
  /**
248
  * Get an associative array ( id => link ) with the list
249
  * of views available on this table.
250
  *
251
- * @since 3.1.0
252
- * @access protected
253
  *
254
  * @return array
255
  */
256
- function get_views() {
257
-
258
  return array();
259
  }
260
 
261
  /**
262
  * Display the list of views available on this table.
263
  *
264
- * @since 3.1.0
265
- * @access public
266
  */
267
- function views() {
268
-
269
  $views = $this->get_views();
270
  /**
271
- * Filter the list of available list table views.
272
  *
273
- * The dynamic portion of the hook name, $this->screen->id, refers
274
  * to the ID of the current screen, usually a string.
275
  *
276
  * @since 3.5.0
@@ -279,13 +374,14 @@ class ITSEC_WP_List_Table {
279
  */
280
  $views = apply_filters( "views_{$this->screen->id}", $views );
281
 
282
- if ( empty( $views ) ) {
283
  return;
284
- }
 
285
 
286
  echo "<ul class='subsubsub'>\n";
287
  foreach ( $views as $class => $view ) {
288
- $views[$class] = "\t<li class='$class'>$view";
289
  }
290
  echo implode( " |</li>\n", $views ) . "</li>\n";
291
  echo "</ul>";
@@ -295,30 +391,29 @@ class ITSEC_WP_List_Table {
295
  * Get an associative array ( option_name => option_title ) with the list
296
  * of bulk actions available on this table.
297
  *
298
- * @since 3.1.0
299
- * @access protected
300
  *
301
  * @return array
302
  */
303
- function get_bulk_actions() {
304
-
305
  return array();
306
  }
307
 
308
  /**
309
  * Display the bulk actions dropdown.
310
  *
311
- * @since 3.1.0
312
- * @access public
 
 
313
  */
314
- function bulk_actions() {
315
-
316
  if ( is_null( $this->_actions ) ) {
317
- $no_new_actions = $this->_actions = $this->get_bulk_actions();
318
  /**
319
- * Filter the list table Bulk Actions drop-down.
320
  *
321
- * The dynamic portion of the hook name, $this->screen->id, refers
322
  * to the ID of the current screen, usually a string.
323
  *
324
  * This filter can currently only be used to remove bulk actions.
@@ -328,48 +423,46 @@ class ITSEC_WP_List_Table {
328
  * @param array $actions An array of the available bulk actions.
329
  */
330
  $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
331
- $this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions );
332
- $two = '';
333
  } else {
334
  $two = '2';
335
  }
336
 
337
- if ( empty( $this->_actions ) ) {
338
  return;
339
- }
340
 
341
- echo "<select name='action$two'>\n";
342
- echo "<option value='-1' selected='selected'>" . __( 'Bulk Actions' ) . "</option>\n";
 
343
 
344
  foreach ( $this->_actions as $name => $title ) {
345
- $class = 'edit' == $name ? ' class="hide-if-no-js"' : '';
346
 
347
- echo "\t<option value='$name'$class>$title</option>\n";
348
  }
349
 
350
  echo "</select>\n";
351
 
352
- submit_button( __( 'Apply' ), 'action', false, false, array( 'id' => "doaction$two" ) );
353
  echo "\n";
354
  }
355
 
356
  /**
357
  * Get the current action selected from the bulk actions dropdown.
358
  *
359
- * @since 3.1.0
360
- * @access public
361
  *
362
- * @return string|bool The action name or False if no action was selected
363
  */
364
- function current_action() {
 
 
365
 
366
- if ( isset( $_REQUEST['action'] ) && - 1 != $_REQUEST['action'] ) {
367
  return $_REQUEST['action'];
368
- }
369
 
370
- if ( isset( $_REQUEST['action2'] ) && - 1 != $_REQUEST['action2'] ) {
371
  return $_REQUEST['action2'];
372
- }
373
 
374
  return false;
375
  }
@@ -377,22 +470,18 @@ class ITSEC_WP_List_Table {
377
  /**
378
  * Generate row actions div
379
  *
380
- * @since 3.1.0
381
- * @access protected
382
- *
383
- * @param array $actions The list of actions
384
- * @param bool $always_visible Whether the actions should be always visible
385
  *
 
 
386
  * @return string
387
  */
388
- function row_actions( $actions, $always_visible = false ) {
389
-
390
  $action_count = count( $actions );
391
- $i = 0;
392
 
393
- if ( ! $action_count ) {
394
  return '';
395
- }
396
 
397
  $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
398
  foreach ( $actions as $action => $link ) {
@@ -402,28 +491,53 @@ class ITSEC_WP_List_Table {
402
  }
403
  $out .= '</div>';
404
 
 
 
405
  return $out;
406
  }
407
 
408
  /**
409
  * Display a monthly dropdown for filtering items
410
  *
411
- * @since 3.1.0
412
- * @access protected
 
 
 
 
413
  */
414
- function months_dropdown( $post_type ) {
415
-
416
  global $wpdb, $wp_locale;
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  $months = $wpdb->get_results( $wpdb->prepare( "
419
  SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
420
  FROM $wpdb->posts
421
  WHERE post_type = %s
 
422
  ORDER BY post_date DESC
423
  ", $post_type ) );
424
 
425
  /**
426
- * Filter the 'Months' drop-down results.
427
  *
428
  * @since 3.7.0
429
  *
@@ -434,100 +548,125 @@ class ITSEC_WP_List_Table {
434
 
435
  $month_count = count( $months );
436
 
437
- if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) {
438
  return;
439
- }
440
 
441
  $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
442
- ?>
443
- <select name='m'>
444
- <option<?php selected( $m, 0 ); ?> value='0'><?php _e( 'Show all dates' ); ?></option>
445
- <?php
446
- foreach ( $months as $arc_row ) {
447
- if ( 0 == $arc_row->year ) {
448
- continue;
449
- }
450
 
451
- $month = zeroise( $arc_row->month, 2 );
452
- $year = $arc_row->year;
453
 
454
- printf( "<option %s value='%s'>%s</option>\n",
455
- selected( $m, $year . $month, false ),
456
- esc_attr( $arc_row->year . $month ),
457
- /* translators: 1: month name, 2: 4-digit year */
458
- sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
459
- );
460
- }
461
- ?>
462
  </select>
463
- <?php
464
  }
465
 
466
  /**
467
  * Display a view switcher
468
  *
469
- * @since 3.1.0
470
- * @access protected
 
471
  */
472
- function view_switcher( $current_mode ) {
473
-
474
- $modes = array(
475
- 'list' => __( 'List View' ),
476
- 'excerpt' => __( 'Excerpt View' )
477
- );
478
-
479
- ?>
480
- <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>"/>
481
  <div class="view-switch">
482
- <?php
483
- foreach ( $modes as $mode => $title ) {
484
- $class = ( $current_mode == $mode ) ? 'class="current"' : '';
485
- echo "<a href='" . esc_url( add_query_arg( 'mode', $mode, $_SERVER['REQUEST_URI'] ) ) . "' $class><img id='view-switch-$mode' src='" . esc_url( includes_url( 'images/blank.gif' ) ) . "' width='20' height='20' title='$title' alt='$title' /></a>\n";
 
 
 
 
 
 
 
486
  }
487
- ?>
488
  </div>
489
- <?php
490
  }
491
 
492
  /**
493
  * Display a comment count bubble
494
  *
495
- * @since 3.1.0
496
- * @access protected
497
  *
498
- * @param int $post_id
499
- * @param int $pending_comments
500
  */
501
- function comments_bubble( $post_id, $pending_comments ) {
 
502
 
503
- $pending_phrase = sprintf( __( '%s pending' ), number_format( $pending_comments ) );
 
504
 
505
- if ( $pending_comments ) {
506
- echo '<strong>';
507
- }
508
 
509
- echo "<a href='" . esc_url( add_query_arg( 'p', $post_id, admin_url( 'edit-comments.php' ) ) ) . "' title='" . esc_attr( $pending_phrase ) . "' class='post-com-count'><span class='comment-count'>" . number_format_i18n( get_comments_number() ) . "</span></a>";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
  if ( $pending_comments ) {
512
- echo '</strong>';
 
 
 
 
 
 
 
 
 
513
  }
514
  }
515
 
516
  /**
517
  * Get the current page number
518
  *
519
- * @since 3.1.0
520
- * @access protected
521
  *
522
  * @return int
523
  */
524
- function get_pagenum() {
525
-
526
  $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
527
 
528
- if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
529
  $pagenum = $this->_pagination_args['total_pages'];
530
- }
531
 
532
  return max( 1, $pagenum );
533
  }
@@ -535,110 +674,144 @@ class ITSEC_WP_List_Table {
535
  /**
536
  * Get number of items to display on a single page
537
  *
538
- * @since 3.1.0
539
- * @access protected
540
  *
 
 
541
  * @return int
542
  */
543
- function get_items_per_page( $option, $default = 20 ) {
544
-
545
  $per_page = (int) get_user_option( $option );
546
- if ( empty( $per_page ) || $per_page < 1 ) {
547
  $per_page = $default;
548
- }
549
 
550
  /**
551
- * Filter the number of items to be displayed on each page of the list table.
552
  *
553
- * The dynamic hook name, $option, refers to the per page option depending
554
- * on the type of list table in use. Possible values may include:
555
- * 'edit_comments_per_page', 'sites_network_per_page', 'site_themes_network_per_page',
556
- * 'themes_netework_per_page', 'users_network_per_page', 'edit_{$post_type}', etc.
 
557
  *
558
  * @since 2.9.0
559
  *
560
  * @param int $per_page Number of items to be displayed. Default 20.
561
  */
562
-
563
- return (int) apply_filters( $option, $per_page );
564
  }
565
 
566
  /**
567
  * Display the pagination.
568
  *
569
- * @since 3.1.0
570
- * @access protected
 
571
  */
572
- function pagination( $which ) {
573
-
574
  if ( empty( $this->_pagination_args ) ) {
575
  return;
576
  }
577
 
578
- extract( $this->_pagination_args, EXTR_SKIP );
 
 
 
 
 
579
 
580
- $output = '<span class="displaying-num">' . sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) ) . '</span>';
 
 
 
 
581
 
582
  $current = $this->get_pagenum();
 
 
583
 
584
  $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
585
 
586
- $current_url = remove_query_arg( array( 'hotkeys_highlight_last', 'hotkeys_highlight_first' ), $current_url );
587
 
588
  $page_links = array();
589
 
590
- $disable_first = $disable_last = '';
591
- if ( $current == 1 ) {
592
- $disable_first = ' disabled';
 
 
 
 
 
 
 
 
593
  }
594
- if ( $current == $total_pages ) {
595
- $disable_last = ' disabled';
 
 
 
 
596
  }
597
 
598
- $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
599
- 'first-page' . $disable_first,
600
- esc_attr__( 'Go to the first page' ),
601
- esc_url( remove_query_arg( 'paged', $current_url ) ),
602
- '&laquo;'
603
- );
604
-
605
- $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
606
- 'prev-page' . $disable_first,
607
- esc_attr__( 'Go to the previous page' ),
608
- esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
609
- '&lsaquo;'
610
- );
611
 
612
- if ( 'bottom' == $which ) {
613
- $html_current_page = $current;
614
  } else {
615
- $html_current_page = sprintf( "<input class='current-page' title='%s' type='text' name='paged' value='%s' size='%d' />",
616
- esc_attr__( 'Current page' ),
617
- $current,
618
- strlen( $total_pages )
619
  );
620
  }
621
 
 
 
 
 
 
 
 
 
 
 
622
  $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
623
- $page_links[] = '<span class="paging-input">' . sprintf( _x( '%1$s of %2$s', 'paging' ), $html_current_page, $html_total_pages ) . '</span>';
624
 
625
- $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
626
- 'next-page' . $disable_last,
627
- esc_attr__( 'Go to the next page' ),
628
- esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
629
- '&rsaquo;'
630
- );
 
 
 
631
 
632
- $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
633
- 'last-page' . $disable_last,
634
- esc_attr__( 'Go to the last page' ),
635
- esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
636
- '&raquo;'
637
- );
 
 
 
638
 
639
  $pagination_links_class = 'pagination-links';
640
  if ( ! empty( $infinite_scroll ) ) {
641
- $pagination_links_class = ' hide-if-js';
642
  }
643
  $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
644
 
@@ -647,7 +820,6 @@ class ITSEC_WP_List_Table {
647
  } else {
648
  $page_class = ' no-pages';
649
  }
650
-
651
  $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
652
 
653
  echo $this->_pagination;
@@ -657,15 +829,13 @@ class ITSEC_WP_List_Table {
657
  * Get a list of columns. The format is:
658
  * 'internal-name' => 'Title'
659
  *
660
- * @since 3.1.0
661
- * @access protected
662
  * @abstract
663
  *
664
  * @return array
665
  */
666
- function get_columns() {
667
-
668
- die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
669
  }
670
 
671
  /**
@@ -676,38 +846,116 @@ class ITSEC_WP_List_Table {
676
  *
677
  * The second format will make the initial sorting order be descending
678
  *
679
- * @since 3.1.0
680
- * @access protected
681
  *
682
  * @return array
683
  */
684
- function get_sortable_columns() {
685
-
686
  return array();
687
  }
688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  /**
690
  * Get a list of all, hidden and sortable columns, with filter applied
691
  *
692
- * @since 3.1.0
693
- * @access protected
694
  *
695
  * @return array
696
  */
697
- function get_column_info() {
 
 
 
 
 
 
 
 
698
 
699
- if ( isset( $this->_column_headers ) ) {
700
- return $this->_column_headers;
701
  }
702
 
703
  $columns = get_column_headers( $this->screen );
704
- $hidden = get_hidden_columns( $this->screen );
705
 
706
  $sortable_columns = $this->get_sortable_columns();
707
  /**
708
- * Filter the list table sortable columns for a specific screen.
709
  *
710
- * The dynamic portion of the hook name, $this->screen->id, refers
711
  * to the ID of the current screen, usually a string.
712
  *
713
  * @since 3.5.0
@@ -718,19 +966,18 @@ class ITSEC_WP_List_Table {
718
 
719
  $sortable = array();
720
  foreach ( $_sortable as $id => $data ) {
721
- if ( empty( $data ) ) {
722
  continue;
723
- }
724
 
725
  $data = (array) $data;
726
- if ( ! isset( $data[1] ) ) {
727
  $data[1] = false;
728
- }
729
 
730
  $sortable[$id] = $data;
731
  }
732
 
733
- $this->_column_headers = array( $columns, $hidden, $sortable );
 
734
 
735
  return $this->_column_headers;
736
  }
@@ -738,30 +985,27 @@ class ITSEC_WP_List_Table {
738
  /**
739
  * Return number of visible columns
740
  *
741
- * @since 3.1.0
742
- * @access public
743
  *
744
  * @return int
745
  */
746
- function get_column_count() {
747
-
748
  list ( $columns, $hidden ) = $this->get_column_info();
749
  $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
750
-
751
  return count( $columns ) - count( $hidden );
752
  }
753
 
754
  /**
755
  * Print column headers, accounting for hidden and sortable columns.
756
  *
757
- * @since 3.1.0
758
- * @access protected
 
759
  *
760
  * @param bool $with_id Whether to set the id attribute or not
761
  */
762
- function print_column_headers( $with_id = true ) {
763
-
764
- list( $columns, $hidden, $sortable ) = $this->get_column_info();
765
 
766
  $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
767
  $current_url = remove_query_arg( 'paged', $current_url );
@@ -772,7 +1016,7 @@ class ITSEC_WP_List_Table {
772
  $current_orderby = '';
773
  }
774
 
775
- if ( isset( $_GET['order'] ) && 'desc' == $_GET['order'] ) {
776
  $current_order = 'desc';
777
  } else {
778
  $current_order = 'asc';
@@ -781,35 +1025,35 @@ class ITSEC_WP_List_Table {
781
  if ( ! empty( $columns['cb'] ) ) {
782
  static $cb_counter = 1;
783
  $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
784
- . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
785
- $cb_counter ++;
786
  }
787
 
788
  foreach ( $columns as $column_key => $column_display_name ) {
789
  $class = array( 'manage-column', "column-$column_key" );
790
 
791
- $style = '';
792
  if ( in_array( $column_key, $hidden ) ) {
793
- $style = 'display:none;';
794
  }
795
 
796
- $style = ' style="' . $style . '"';
797
-
798
- if ( 'cb' == $column_key ) {
799
  $class[] = 'check-column';
800
- } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) ) {
801
  $class[] = 'num';
 
 
 
802
  }
803
 
804
  if ( isset( $sortable[$column_key] ) ) {
805
  list( $orderby, $desc_first ) = $sortable[$column_key];
806
 
807
- if ( $current_orderby == $orderby ) {
808
- $order = 'asc' == $current_order ? 'desc' : 'asc';
809
  $class[] = 'sorted';
810
  $class[] = $current_order;
811
  } else {
812
- $order = $desc_first ? 'desc' : 'asc';
813
  $class[] = 'sortable';
814
  $class[] = $desc_first ? 'asc' : 'desc';
815
  }
@@ -817,113 +1061,110 @@ class ITSEC_WP_List_Table {
817
  $column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
818
  }
819
 
 
 
820
  $id = $with_id ? "id='$column_key'" : '';
821
 
822
- if ( ! empty( $class ) ) {
823
  $class = "class='" . join( ' ', $class ) . "'";
824
- }
825
 
826
- echo "<th scope='col' $id $class $style>$column_display_name</th>";
827
  }
828
  }
829
 
830
  /**
831
  * Display the table
832
  *
833
- * @since 3.1.0
834
- * @access public
835
  */
836
- function display() {
837
-
838
- extract( $this->_args );
839
 
840
  $this->display_tablenav( 'top' );
841
 
842
- ?>
843
- <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>" cellspacing="0">
844
- <thead>
845
- <tr>
846
- <?php $this->print_column_headers(); ?>
847
- </tr>
848
- </thead>
849
-
850
- <tfoot>
851
- <tr>
852
- <?php $this->print_column_headers( false ); ?>
853
- </tr>
854
- </tfoot>
855
-
856
- <tbody id="the-list"<?php if ( $singular ) {
857
- echo " data-wp-lists='list:$singular'";
858
- } ?>>
859
- <?php $this->display_rows_or_placeholder(); ?>
860
- </tbody>
861
- </table>
862
- <?php
 
 
 
863
  $this->display_tablenav( 'bottom' );
864
  }
865
 
866
  /**
867
- * Get a list of CSS classes for the <table> tag
868
  *
869
- * @since 3.1.0
870
- * @access protected
871
  *
872
- * @return array
873
  */
874
- function get_table_classes() {
875
-
876
- return array( 'widefat', 'fixed', $this->_args['plural'] );
877
  }
878
 
879
  /**
880
  * Generate the table navigation above or below the table
881
  *
882
- * @since 3.1.0
883
- * @access protected
884
  */
885
- function display_tablenav( $which ) {
886
-
887
- if ( 'top' == $which ) {
888
  wp_nonce_field( 'bulk-' . $this->_args['plural'] );
889
  }
890
  ?>
891
- <div class="tablenav <?php echo esc_attr( $which ); ?>">
892
-
893
- <div class="alignleft actions bulkactions">
894
- <?php $this->bulk_actions(); ?>
895
- </div>
896
- <?php
897
- $this->extra_tablenav( $which );
898
- $this->pagination( $which );
899
- ?>
900
 
901
- <br class="clear"/>
 
 
902
  </div>
903
- <?php
 
 
 
 
 
 
 
904
  }
905
 
906
  /**
907
  * Extra controls to be displayed between bulk actions and pagination
908
  *
909
- * @since 3.1.0
910
- * @access protected
 
911
  */
912
- function extra_tablenav( $which ) {
913
- }
914
 
915
  /**
916
- * Generate the <tbody> part of the table
917
  *
918
- * @since 3.1.0
919
- * @access protected
920
  */
921
- function display_rows_or_placeholder() {
922
-
923
  if ( $this->has_items() ) {
924
  $this->display_rows();
925
  } else {
926
- list( $columns, $hidden ) = $this->get_column_info();
927
  echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
928
  $this->no_items();
929
  echo '</td></tr>';
@@ -933,85 +1174,113 @@ class ITSEC_WP_List_Table {
933
  /**
934
  * Generate the table rows
935
  *
936
- * @since 3.1.0
937
- * @access protected
938
  */
939
- function display_rows() {
940
-
941
- foreach ( $this->items as $item ) {
942
  $this->single_row( $item );
943
- }
944
  }
945
 
946
  /**
947
  * Generates content for a single row of the table
948
  *
949
- * @since 3.1.0
950
- * @access protected
951
  *
952
  * @param object $item The current item
953
  */
954
- function single_row( $item ) {
955
-
956
- static $row_class = '';
957
- $row_class = ( $row_class == '' ? ' class="alternate"' : '' );
958
-
959
- echo '<tr' . $row_class . '>';
960
  $this->single_row_columns( $item );
961
  echo '</tr>';
962
  }
963
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  /**
965
  * Generates the columns for a single row of the table
966
  *
967
- * @since 3.1.0
968
- * @access protected
969
  *
970
  * @param object $item The current item
971
  */
972
- function single_row_columns( $item ) {
973
-
974
- list( $columns, $hidden ) = $this->get_column_info();
975
 
976
  foreach ( $columns as $column_name => $column_display_name ) {
977
- $class = "class='$column_name column-$column_name'";
 
 
 
978
 
979
- $style = '';
980
  if ( in_array( $column_name, $hidden ) ) {
981
- $style = ' style="display:none;"';
982
  }
983
 
984
- $attributes = "$class$style";
 
 
 
 
985
 
986
- if ( 'cb' == $column_name ) {
987
  echo '<th scope="row" class="check-column">';
988
  echo $this->column_cb( $item );
989
  echo '</th>';
 
 
 
 
 
 
 
 
990
  } elseif ( method_exists( $this, 'column_' . $column_name ) ) {
991
  echo "<td $attributes>";
992
  echo call_user_func( array( $this, 'column_' . $column_name ), $item );
 
993
  echo "</td>";
994
  } else {
995
  echo "<td $attributes>";
996
  echo $this->column_default( $item, $column_name );
 
997
  echo "</td>";
998
  }
999
  }
1000
  }
1001
 
1002
  /**
1003
- * Handle an incoming ajax request (called from admin-ajax.php)
1004
  *
1005
- * @since 3.1.0
1006
- * @access public
 
 
 
 
1007
  */
1008
- function ajax_response() {
 
 
1009
 
 
 
 
 
 
 
1010
  $this->prepare_items();
1011
 
1012
- extract( $this->_args );
1013
- extract( $this->_pagination_args, EXTR_SKIP );
1014
-
1015
  ob_start();
1016
  if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
1017
  $this->display_rows();
@@ -1023,25 +1292,25 @@ class ITSEC_WP_List_Table {
1023
 
1024
  $response = array( 'rows' => $rows );
1025
 
1026
- if ( isset( $total_items ) ) {
1027
- $response['total_items_i18n'] = sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) );
 
 
 
1028
  }
1029
-
1030
- if ( isset( $total_pages ) ) {
1031
- $response['total_pages'] = $total_pages;
1032
- $response['total_pages_i18n'] = number_format_i18n( $total_pages );
1033
  }
1034
 
1035
- die( json_encode( $response ) );
1036
  }
1037
 
1038
  /**
1039
  * Send required variables to JavaScript land
1040
  *
1041
- * @access private
1042
  */
1043
- function _js_vars() {
1044
-
1045
  $args = array(
1046
  'class' => get_class( $this ),
1047
  'screen' => array(
@@ -1050,6 +1319,6 @@ class ITSEC_WP_List_Table {
1050
  )
1051
  );
1052
 
1053
- printf( "<script type='text/javascript'>list_args = %s;</script>\n", json_encode( $args ) );
1054
  }
1055
  }
1
  <?php
2
  /**
3
+ * Administration API: ITSEC_WP_List_Table class
4
  *
5
+ * @package WordPress
6
+ * @subpackage List_Table
7
+ * @since 3.1.0
8
  */
9
 
10
  /**
11
  * Base class for displaying a list of items in an ajaxified HTML table.
12
  *
13
+ * @since 3.1.0
14
+ * @access private
 
 
15
  */
16
  class ITSEC_WP_List_Table {
17
 
18
  /**
19
+ * The current list of items.
20
  *
21
+ * @since 3.1.0
22
  * @var array
 
23
  */
24
+ public $items;
25
 
26
  /**
27
+ * Various information about the current table.
28
  *
29
+ * @since 3.1.0
30
  * @var array
 
31
  */
32
+ protected $_args;
33
 
34
  /**
35
+ * Various information needed for displaying the pagination.
36
  *
37
+ * @since 3.1.0
38
  * @var array
 
39
  */
40
+ protected $_pagination_args = array();
41
 
42
  /**
43
+ * The current screen.
44
  *
45
+ * @since 3.1.0
46
  * @var object
 
47
  */
48
+ protected $screen;
49
 
50
  /**
51
+ * Cached bulk actions.
52
  *
53
+ * @since 3.1.0
54
  * @var array
 
55
  */
56
+ private $_actions;
57
 
58
  /**
59
+ * Cached pagination output.
60
  *
61
+ * @since 3.1.0
62
  * @var string
 
63
  */
64
+ private $_pagination;
65
+
66
+ /**
67
+ * The view switcher modes.
68
+ *
69
+ * @since 4.1.0
70
+ * @var array
71
+ */
72
+ protected $modes = array();
73
+
74
+ /**
75
+ * Stores the value returned by ->get_column_info().
76
+ *
77
+ * @since 4.1.0
78
+ * @var array
79
+ */
80
+ protected $_column_headers;
81
 
82
  /**
83
+ * {@internal Missing Summary}
84
  *
85
+ * @var array
86
+ */
87
+ protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
88
+
89
+ /**
90
+ * {@internal Missing Summary}
91
  *
92
+ * @var array
93
  */
94
+ protected $compat_methods = array( 'set_pagination_args', 'get_views', 'get_bulk_actions', 'bulk_actions',
95
+ 'row_actions', 'months_dropdown', 'view_switcher', 'comments_bubble', 'get_items_per_page', 'pagination',
96
+ 'get_sortable_columns', 'get_column_info', 'get_table_classes', 'display_tablenav', 'extra_tablenav',
97
+ 'single_row_columns' );
98
 
99
+ /**
100
+ * Constructor.
101
+ *
102
+ * The child class should call this constructor from its own constructor to override
103
+ * the default $args.
104
+ *
105
+ * @since 3.1.0
106
+ *
107
+ * @param array|string $args {
108
+ * Array or string of arguments.
109
+ *
110
+ * @type string $plural Plural value used for labels and the objects being listed.
111
+ * This affects things such as CSS class-names and nonces used
112
+ * in the list table, e.g. 'posts'. Default empty.
113
+ * @type string $singular Singular label for an object being listed, e.g. 'post'.
114
+ * Default empty
115
+ * @type bool $ajax Whether the list table supports Ajax. This includes loading
116
+ * and sorting data, for example. If true, the class will call
117
+ * the _js_vars() method in the footer to provide variables
118
+ * to any scripts handling Ajax events. Default false.
119
+ * @type string $screen String containing the hook name used to determine the current
120
+ * screen. If left null, the current screen will be automatically set.
121
+ * Default null.
122
+ * }
123
+ */
124
+ public function __construct( $args = array() ) {
125
  $args = wp_parse_args( $args, array(
126
+ 'plural' => '',
127
  'singular' => '',
128
+ 'ajax' => false,
129
+ 'screen' => null,
130
  ) );
131
 
132
  $this->screen = convert_to_screen( $args['screen'] );
133
 
134
  add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
135
 
136
+ if ( !$args['plural'] )
137
  $args['plural'] = $this->screen->base;
 
138
 
139
+ $args['plural'] = sanitize_key( $args['plural'] );
140
  $args['singular'] = sanitize_key( $args['singular'] );
141
 
142
  $this->_args = $args;
145
  // wp_enqueue_script( 'list-table' );
146
  add_action( 'admin_footer', array( $this, '_js_vars' ) );
147
  }
148
+
149
+ if ( empty( $this->modes ) ) {
150
+ $this->modes = array(
151
+ 'list' => __( 'List View' ),
152
+ 'excerpt' => __( 'Excerpt View' )
153
+ );
154
+ }
155
  }
156
 
157
  /**
158
+ * Make private properties readable for backward compatibility.
159
  *
160
+ * @since 4.0.0
161
  *
162
+ * @param string $name Property to get.
163
+ * @return mixed Property.
 
164
  */
165
+ public function __get( $name ) {
166
+ if ( in_array( $name, $this->compat_fields ) ) {
167
+ return $this->$name;
168
+ }
169
+ }
170
 
171
+ /**
172
+ * Make private properties settable for backward compatibility.
173
+ *
174
+ * @since 4.0.0
175
+ *
176
+ * @param string $name Property to check if set.
177
+ * @param mixed $value Property value.
178
+ * @return mixed Newly-set property.
179
+ */
180
+ public function __set( $name, $value ) {
181
+ if ( in_array( $name, $this->compat_fields ) ) {
182
+ return $this->$name = $value;
183
+ }
184
  }
185
 
186
  /**
187
+ * Make private properties checkable for backward compatibility.
188
+ *
189
+ * @since 4.0.0
190
+ *
191
+ * @param string $name Property to check if set.
192
+ * @return bool Whether the property is set.
193
+ */
194
+ public function __isset( $name ) {
195
+ if ( in_array( $name, $this->compat_fields ) ) {
196
+ return isset( $this->$name );
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Make private properties un-settable for backward compatibility.
202
  *
203
+ * @since 4.0.0
204
  *
205
+ * @param string $name Property to unset.
206
+ */
207
+ public function __unset( $name ) {
208
+ if ( in_array( $name, $this->compat_fields ) ) {
209
+ unset( $this->$name );
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Make private/protected methods readable for backward compatibility.
215
+ *
216
+ * @since 4.0.0
217
+ *
218
+ * @param callable $name Method to call.
219
+ * @param array $arguments Arguments to pass when calling.
220
+ * @return mixed|bool Return value of the callback, false otherwise.
221
+ */
222
+ public function __call( $name, $arguments ) {
223
+ if ( in_array( $name, $this->compat_methods ) ) {
224
+ return call_user_func_array( array( $this, $name ), $arguments );
225
+ }
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Checks the current user's permissions
231
+ *
232
+ * @since 3.1.0
233
  * @abstract
234
  */
235
+ public function ajax_user_can() {
236
+ die( 'function ITSEC_WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
237
+ }
238
 
239
+ /**
240
+ * Prepares the list of items for displaying.
241
+ * @uses ITSEC_WP_List_Table::set_pagination_args()
242
+ *
243
+ * @since 3.1.0
244
+ * @abstract
245
+ */
246
+ public function prepare_items() {
247
+ die( 'function ITSEC_WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
248
  }
249
 
250
  /**
251
  * An internal method that sets all the necessary pagination arguments
252
  *
253
+ * @since 3.1.0
254
  *
255
+ * @param array|string $args Array or string of arguments with information about the pagination.
256
  */
257
+ protected function set_pagination_args( $args ) {
 
258
  $args = wp_parse_args( $args, array(
259
  'total_items' => 0,
260
  'total_pages' => 0,
261
+ 'per_page' => 0,
262
  ) );
263
 
264
+ if ( !$args['total_pages'] && $args['per_page'] > 0 )
265
  $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
 
266
 
267
+ // Redirect if page number is invalid and headers are not already sent.
268
+ if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
269
  wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
270
  exit;
271
  }
274
  }
275
 
276
  /**
277
+ * Access the pagination args.
278
  *
279
+ * @since 3.1.0
 
280
  *
281
+ * @param string $key Pagination argument to retrieve. Common values include 'total_items',
282
+ * 'total_pages', 'per_page', or 'infinite_scroll'.
283
+ * @return int Number of items that correspond to the given pagination argument.
284
  */
285
+ public function get_pagination_arg( $key ) {
286
+ if ( 'page' === $key ) {
 
287
  return $this->get_pagenum();
288
  }
289
 
295
  /**
296
  * Whether the table has items to display or not
297
  *
298
+ * @since 3.1.0
 
299
  *
300
  * @return bool
301
  */
302
+ public function has_items() {
303
+ return !empty( $this->items );
 
304
  }
305
 
306
  /**
307
  * Message to be displayed when there are no items
308
  *
309
+ * @since 3.1.0
 
310
  */
311
+ public function no_items() {
 
312
  _e( 'No items found.' );
313
  }
314
 
315
  /**
316
+ * Displays the search box.
317
  *
318
+ * @since 3.1.0
 
319
  *
320
+ * @param string $text The 'submit' button label.
321
+ * @param string $input_id ID attribute value for the search input field.
322
  */
323
+ public function search_box( $text, $input_id ) {
324
+ if ( empty( $_REQUEST['s'] ) && !$this->has_items() )
 
325
  return;
 
326
 
327
  $input_id = $input_id . '-search-input';
328
 
329
+ if ( ! empty( $_REQUEST['orderby'] ) )
330
  echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
331
+ if ( ! empty( $_REQUEST['order'] ) )
 
332
  echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
333
+ if ( ! empty( $_REQUEST['post_mime_type'] ) )
 
334
  echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
335
+ if ( ! empty( $_REQUEST['detached'] ) )
 
336
  echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
337
+ ?>
338
+ <p class="search-box">
339
+ <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
340
+ <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
341
+ <?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
342
+ </p>
343
+ <?php
 
344
  }
345
 
346
  /**
347
  * Get an associative array ( id => link ) with the list
348
  * of views available on this table.
349
  *
350
+ * @since 3.1.0
 
351
  *
352
  * @return array
353
  */
354
+ protected function get_views() {
 
355
  return array();
356
  }
357
 
358
  /**
359
  * Display the list of views available on this table.
360
  *
361
+ * @since 3.1.0
 
362
  */
363
+ public function views() {
 
364
  $views = $this->get_views();
365
  /**
366
+ * Filters the list of available list table views.
367
  *
368
+ * The dynamic portion of the hook name, `$this->screen->id`, refers
369
  * to the ID of the current screen, usually a string.
370
  *
371
  * @since 3.5.0
374
  */
375
  $views = apply_filters( "views_{$this->screen->id}", $views );
376
 
377
+ if ( empty( $views ) )
378
  return;
379
+
380
+ $this->screen->render_screen_reader_content( 'heading_views' );
381
 
382
  echo "<ul class='subsubsub'>\n";
383
  foreach ( $views as $class => $view ) {
384
+ $views[ $class ] = "\t<li class='$class'>$view";
385
  }
386
  echo implode( " |</li>\n", $views ) . "</li>\n";
387
  echo "</ul>";
391
  * Get an associative array ( option_name => option_title ) with the list
392
  * of bulk actions available on this table.
393
  *
394
+ * @since 3.1.0
 
395
  *
396
  * @return array
397
  */
398
+ protected function get_bulk_actions() {
 
399
  return array();
400
  }
401
 
402
  /**
403
  * Display the bulk actions dropdown.
404
  *
405
+ * @since 3.1.0
406
+ *
407
+ * @param string $which The location of the bulk actions: 'top' or 'bottom'.
408
+ * This is designated as optional for backward compatibility.
409
  */
410
+ protected function bulk_actions( $which = '' ) {
 
411
  if ( is_null( $this->_actions ) ) {
412
+ $this->_actions = $this->get_bulk_actions();
413
  /**
414
+ * Filters the list table Bulk Actions drop-down.
415
  *
416
+ * The dynamic portion of the hook name, `$this->screen->id`, refers
417
  * to the ID of the current screen, usually a string.
418
  *
419
  * This filter can currently only be used to remove bulk actions.
423
  * @param array $actions An array of the available bulk actions.
424
  */
425
  $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
426
+ $two = '';
 
427
  } else {
428
  $two = '2';
429
  }
430
 
431
+ if ( empty( $this->_actions ) )
432
  return;
 
433
 
434
+ echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
435
+ echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
436
+ echo '<option value="-1">' . __( 'Bulk Actions' ) . "</option>\n";
437
 
438
  foreach ( $this->_actions as $name => $title ) {
439
+ $class = 'edit' === $name ? ' class="hide-if-no-js"' : '';
440
 
441
+ echo "\t" . '<option value="' . $name . '"' . $class . '>' . $title . "</option>\n";
442
  }
443
 
444
  echo "</select>\n";
445
 
446
+ submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
447
  echo "\n";
448
  }
449
 
450
  /**
451
  * Get the current action selected from the bulk actions dropdown.
452
  *
453
+ * @since 3.1.0
 
454
  *
455
+ * @return string|false The action name or False if no action was selected
456
  */
457
+ public function current_action() {
458
+ if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) )
459
+ return false;
460
 
461
+ if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] )
462
  return $_REQUEST['action'];
 
463
 
464
+ if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] )
465
  return $_REQUEST['action2'];
 
466
 
467
  return false;
468
  }
470
  /**
471
  * Generate row actions div
472
  *
473
+ * @since 3.1.0
 
 
 
 
474
  *
475
+ * @param array $actions The list of actions
476
+ * @param bool $always_visible Whether the actions should be always visible
477
  * @return string
478
  */
479
+ protected function row_actions( $actions, $always_visible = false ) {
 
480
  $action_count = count( $actions );
481
+ $i = 0;
482
 
483
+ if ( !$action_count )
484
  return '';
 
485
 
486
  $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
487
  foreach ( $actions as $action => $link ) {
491
  }
492
  $out .= '</div>';
493
 
494
+ $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
495
+
496
  return $out;
497
  }
498
 
499
  /**
500
  * Display a monthly dropdown for filtering items
501
  *
502
+ * @since 3.1.0
503
+ *
504
+ * @global wpdb $wpdb
505
+ * @global WP_Locale $wp_locale
506
+ *
507
+ * @param string $post_type
508
  */
509
+ protected function months_dropdown( $post_type ) {
 
510
  global $wpdb, $wp_locale;
511
 
512
+ /**
513
+ * Filters whether to remove the 'Months' drop-down from the post list table.
514
+ *
515
+ * @since 4.2.0
516
+ *
517
+ * @param bool $disable Whether to disable the drop-down. Default false.
518
+ * @param string $post_type The post type.
519
+ */
520
+ if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
521
+ return;
522
+ }
523
+
524
+ $extra_checks = "AND post_status != 'auto-draft'";
525
+ if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
526
+ $extra_checks .= " AND post_status != 'trash'";
527
+ } elseif ( isset( $_GET['post_status'] ) ) {
528
+ $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
529
+ }
530
+
531
  $months = $wpdb->get_results( $wpdb->prepare( "
532
  SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
533
  FROM $wpdb->posts
534
  WHERE post_type = %s
535
+ $extra_checks
536
  ORDER BY post_date DESC
537
  ", $post_type ) );
538
 
539
  /**
540
+ * Filters the 'Months' drop-down results.
541
  *
542
  * @since 3.7.0
543
  *
548
 
549
  $month_count = count( $months );
550
 
551
+ if ( !$month_count || ( 1 == $month_count && 0 == $months[0]->month ) )
552
  return;
 
553
 
554
  $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
555
+ ?>
556
+ <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label>
557
+ <select name="m" id="filter-by-date">
558
+ <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
559
+ <?php
560
+ foreach ( $months as $arc_row ) {
561
+ if ( 0 == $arc_row->year )
562
+ continue;
563
 
564
+ $month = zeroise( $arc_row->month, 2 );
565
+ $year = $arc_row->year;
566
 
567
+ printf( "<option %s value='%s'>%s</option>\n",
568
+ selected( $m, $year . $month, false ),
569
+ esc_attr( $arc_row->year . $month ),
570
+ /* translators: 1: month name, 2: 4-digit year */
571
+ sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
572
+ );
573
+ }
574
+ ?>
575
  </select>
576
+ <?php
577
  }
578
 
579
  /**
580
  * Display a view switcher
581
  *
582
+ * @since 3.1.0
583
+ *
584
+ * @param string $current_mode
585
  */
586
+ protected function view_switcher( $current_mode ) {
587
+ ?>
588
+ <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
 
 
 
 
 
 
589
  <div class="view-switch">
590
+ <?php
591
+ foreach ( $this->modes as $mode => $title ) {
592
+ $classes = array( 'view-' . $mode );
593
+ if ( $current_mode === $mode )
594
+ $classes[] = 'current';
595
+ printf(
596
+ "<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",
597
+ esc_url( add_query_arg( 'mode', $mode ) ),
598
+ implode( ' ', $classes ),
599
+ $title
600
+ );
601
  }
602
+ ?>
603
  </div>
604
+ <?php
605
  }
606
 
607
  /**
608
  * Display a comment count bubble
609
  *
610
+ * @since 3.1.0
 
611
  *
612
+ * @param int $post_id The post ID.
613
+ * @param int $pending_comments Number of pending comments.
614
  */
615
+ protected function comments_bubble( $post_id, $pending_comments ) {
616
+ $approved_comments = get_comments_number();
617
 
618
+ $approved_comments_number = number_format_i18n( $approved_comments );
619
+ $pending_comments_number = number_format_i18n( $pending_comments );
620
 
621
+ $approved_only_phrase = sprintf( _n( '%s comment', '%s comments', $approved_comments ), $approved_comments_number );
622
+ $approved_phrase = sprintf( _n( '%s approved comment', '%s approved comments', $approved_comments ), $approved_comments_number );
623
+ $pending_phrase = sprintf( _n( '%s pending comment', '%s pending comments', $pending_comments ), $pending_comments_number );
624
 
625
+ // No comments at all.
626
+ if ( ! $approved_comments && ! $pending_comments ) {
627
+ printf( '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
628
+ __( 'No comments' )
629
+ );
630
+ // Approved comments have different display depending on some conditions.
631
+ } elseif ( $approved_comments ) {
632
+ printf( '<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
633
+ esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'approved' ), admin_url( 'edit-comments.php' ) ) ),
634
+ $approved_comments_number,
635
+ $pending_comments ? $approved_phrase : $approved_only_phrase
636
+ );
637
+ } else {
638
+ printf( '<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
639
+ $approved_comments_number,
640
+ $pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
641
+ );
642
+ }
643
 
644
  if ( $pending_comments ) {
645
+ printf( '<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
646
+ esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'moderated' ), admin_url( 'edit-comments.php' ) ) ),
647
+ $pending_comments_number,
648
+ $pending_phrase
649
+ );
650
+ } else {
651
+ printf( '<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
652
+ $pending_comments_number,
653
+ $approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
654
+ );
655
  }
656
  }
657
 
658
  /**
659
  * Get the current page number
660
  *
661
+ * @since 3.1.0
 
662
  *
663
  * @return int
664
  */
665
+ public function get_pagenum() {
 
666
  $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
667
 
668
+ if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] )
669
  $pagenum = $this->_pagination_args['total_pages'];
 
670
 
671
  return max( 1, $pagenum );
672
  }
674
  /**
675
  * Get number of items to display on a single page
676
  *
677
+ * @since 3.1.0
 
678
  *
679
+ * @param string $option
680
+ * @param int $default
681
  * @return int
682
  */
683
+ protected function get_items_per_page( $option, $default = 20 ) {
 
684
  $per_page = (int) get_user_option( $option );
685
+ if ( empty( $per_page ) || $per_page < 1 )
686
  $per_page = $default;
 
687
 
688
  /**
689
+ * Filters the number of items to be displayed on each page of the list table.
690
  *
691
+ * The dynamic hook name, $option, refers to the `per_page` option depending
692
+ * on the type of list table in use. Possible values include: 'edit_comments_per_page',
693
+ * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
694
+ * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
695
+ * 'edit_{$post_type}_per_page', etc.
696
  *
697
  * @since 2.9.0
698
  *
699
  * @param int $per_page Number of items to be displayed. Default 20.
700
  */
701
+ return (int) apply_filters( "{$option}", $per_page );
 
702
  }
703
 
704
  /**
705
  * Display the pagination.
706
  *
707
+ * @since 3.1.0
708
+ *
709
+ * @param string $which
710
  */
711
+ protected function pagination( $which ) {
 
712
  if ( empty( $this->_pagination_args ) ) {
713
  return;
714
  }
715
 
716
+ $total_items = $this->_pagination_args['total_items'];
717
+ $total_pages = $this->_pagination_args['total_pages'];
718
+ $infinite_scroll = false;
719
+ if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
720
+ $infinite_scroll = $this->_pagination_args['infinite_scroll'];
721
+ }
722
 
723
+ if ( 'top' === $which && $total_pages > 1 ) {
724
+ $this->screen->render_screen_reader_content( 'heading_pagination' );
725
+ }
726
+
727
+ $output = '<span class="displaying-num">' . sprintf( _n( '%s item', '%s items', $total_items ), number_format_i18n( $total_items ) ) . '</span>';
728
 
729
  $current = $this->get_pagenum();
730
+ $removable_query_args = wp_removable_query_args();
731
+ $removable_query_args[] = 'id';
732
 
733
  $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
734
 
735
+ $current_url = remove_query_arg( $removable_query_args, $current_url );
736
 
737
  $page_links = array();
738
 
739
+ $total_pages_before = '<span class="paging-input">';
740
+ $total_pages_after = '</span></span>';
741
+
742
+ $disable_first = $disable_last = $disable_prev = $disable_next = false;
743
+
744
+ if ( $current == 1 ) {
745
+ $disable_first = true;
746
+ $disable_prev = true;
747
+ }
748
+ if ( $current == 2 ) {
749
+ $disable_first = true;
750
  }
751
+ if ( $current == $total_pages ) {
752
+ $disable_last = true;
753
+ $disable_next = true;
754
+ }
755
+ if ( $current == $total_pages - 1 ) {
756
+ $disable_last = true;
757
  }
758
 
759
+ if ( $disable_first ) {
760
+ $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&laquo;</span>';
761
+ } else {
762
+ $page_links[] = sprintf( "<a class='first-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
763
+ esc_url( remove_query_arg( 'paged', $current_url ) ),
764
+ __( 'First page' ),
765
+ '&laquo;'
766
+ );
767
+ }
 
 
 
 
768
 
769
+ if ( $disable_prev ) {
770
+ $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&lsaquo;</span>';
771
  } else {
772
+ $page_links[] = sprintf( "<a class='prev-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
773
+ esc_url( add_query_arg( 'paged', max( 1, $current-1 ), $current_url ) ),
774
+ __( 'Previous page' ),
775
+ '&lsaquo;'
776
  );
777
  }
778
 
779
+ if ( 'bottom' === $which ) {
780
+ $html_current_page = $current;
781
+ $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
782
+ } else {
783
+ $html_current_page = sprintf( "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
784
+ '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
785
+ $current,
786
+ strlen( $total_pages )
787
+ );
788
+ }
789
  $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
790
+ $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging' ), $html_current_page, $html_total_pages ) . $total_pages_after;
791
 
792
+ if ( $disable_next ) {
793
+ $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&rsaquo;</span>';
794
+ } else {
795
+ $page_links[] = sprintf( "<a class='next-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
796
+ esc_url( add_query_arg( 'paged', min( $total_pages, $current+1 ), $current_url ) ),
797
+ __( 'Next page' ),
798
+ '&rsaquo;'
799
+ );
800
+ }
801
 
802
+ if ( $disable_last ) {
803
+ $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&raquo;</span>';
804
+ } else {
805
+ $page_links[] = sprintf( "<a class='last-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
806
+ esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
807
+ __( 'Last page' ),
808
+ '&raquo;'
809
+ );
810
+ }
811
 
812
  $pagination_links_class = 'pagination-links';
813
  if ( ! empty( $infinite_scroll ) ) {
814
+ $pagination_links_class .= ' hide-if-js';
815
  }
816
  $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
817
 
820
  } else {
821
  $page_class = ' no-pages';
822
  }
 
823
  $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
824
 
825
  echo $this->_pagination;
829
  * Get a list of columns. The format is:
830
  * 'internal-name' => 'Title'
831
  *
832
+ * @since 3.1.0
 
833
  * @abstract
834
  *
835
  * @return array
836
  */
837
+ public function get_columns() {
838
+ die( 'function ITSEC_WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
 
839
  }
840
 
841
  /**
846
  *
847
  * The second format will make the initial sorting order be descending
848
  *
849
+ * @since 3.1.0
 
850
  *
851
  * @return array
852
  */
853
+ protected function get_sortable_columns() {
 
854
  return array();
855
  }
856
 
857
+ /**
858
+ * Gets the name of the default primary column.
859
+ *
860
+ * @since 4.3.0
861
+ *
862
+ * @return string Name of the default primary column, in this case, an empty string.
863
+ */
864
+ protected function get_default_primary_column_name() {
865
+ $columns = $this->get_columns();
866
+ $column = '';
867
+
868
+ if ( empty( $columns ) ) {
869
+ return $column;
870
+ }
871
+
872
+ // We need a primary defined so responsive views show something,
873
+ // so let's fall back to the first non-checkbox column.
874
+ foreach ( $columns as $col => $column_name ) {
875
+ if ( 'cb' === $col ) {
876
+ continue;
877
+ }
878
+
879
+ $column = $col;
880
+ break;
881
+ }
882
+
883
+ return $column;
884
+ }
885
+
886
+ /**
887
+ * Public wrapper for ITSEC_WP_List_Table::get_default_primary_column_name().
888
+ *
889
+ * @since 4.4.0
890
+ *
891
+ * @return string Name of the default primary column.
892
+ */
893
+ public function get_primary_column() {
894
+ return $this->get_primary_column_name();
895
+ }
896
+
897
+ /**
898
+ * Gets the name of the primary column.
899
+ *
900
+ * @since 4.3.0
901
+ *
902
+ * @return string The name of the primary column.
903
+ */
904
+ protected function get_primary_column_name() {
905
+ $columns = get_column_headers( $this->screen );
906
+ $default = $this->get_default_primary_column_name();
907
+
908
+ // If the primary column doesn't exist fall back to the
909
+ // first non-checkbox column.
910
+ if ( ! isset( $columns[ $default ] ) ) {
911
+ $default = ITSEC_WP_List_Table::get_default_primary_column_name();
912
+ }
913
+
914
+ /**
915
+ * Filters the name of the primary column for the current list table.
916
+ *
917
+ * @since 4.3.0
918
+ *
919
+ * @param string $default Column name default for the specific list table, e.g. 'name'.
920
+ * @param string $context Screen ID for specific list table, e.g. 'plugins'.
921
+ */
922
+ $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
923
+
924
+ if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
925
+ $column = $default;
926
+ }
927
+
928
+ return $column;
929
+ }
930
+
931
  /**
932
  * Get a list of all, hidden and sortable columns, with filter applied
933
  *
934
+ * @since 3.1.0
 
935
  *
936
  * @return array
937
  */
938
+ protected function get_column_info() {
939
+ // $_column_headers is already set / cached
940
+ if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
941
+ // Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
942
+ // In 4.3, we added a fourth argument for primary column.
943
+ $column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
944
+ foreach ( $this->_column_headers as $key => $value ) {
945
+ $column_headers[ $key ] = $value;
946
+ }
947
 
948
+ return $column_headers;
 
949
  }
950
 
951
  $columns = get_column_headers( $this->screen );
952
+ $hidden = get_hidden_columns( $this->screen );
953
 
954
  $sortable_columns = $this->get_sortable_columns();
955
  /**
956
+ * Filters the list table sortable columns for a specific screen.
957
  *
958
+ * The dynamic portion of the hook name, `$this->screen->id`, refers
959
  * to the ID of the current screen, usually a string.
960
  *
961
  * @since 3.5.0
966
 
967
  $sortable = array();
968
  foreach ( $_sortable as $id => $data ) {
969
+ if ( empty( $data ) )
970
  continue;
 
971
 
972
  $data = (array) $data;
973
+ if ( !isset( $data[1] ) )
974
  $data[1] = false;
 
975
 
976
  $sortable[$id] = $data;
977
  }
978
 
979
+ $primary = $this->get_primary_column_name();
980
+ $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
981
 
982
  return $this->_column_headers;
983
  }
985
  /**
986
  * Return number of visible columns
987
  *
988
+ * @since 3.1.0
 
989
  *
990
  * @return int
991
  */
992
+ public function get_column_count() {
 
993
  list ( $columns, $hidden ) = $this->get_column_info();
994
  $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
 
995
  return count( $columns ) - count( $hidden );
996
  }
997
 
998
  /**
999
  * Print column headers, accounting for hidden and sortable columns.
1000
  *
1001
+ * @since 3.1.0
1002
+ *
1003
+ * @staticvar int $cb_counter
1004
  *
1005
  * @param bool $with_id Whether to set the id attribute or not
1006
  */
1007
+ public function print_column_headers( $with_id = true ) {
1008
+ list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
 
1009
 
1010
  $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
1011
  $current_url = remove_query_arg( 'paged', $current_url );
1016
  $current_orderby = '';
1017
  }
1018
 
1019
+ if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
1020
  $current_order = 'desc';
1021
  } else {
1022
  $current_order = 'asc';
1025
  if ( ! empty( $columns['cb'] ) ) {
1026
  static $cb_counter = 1;
1027
  $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
1028
+ . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
1029
+ $cb_counter++;
1030
  }
1031
 
1032
  foreach ( $columns as $column_key => $column_display_name ) {
1033
  $class = array( 'manage-column', "column-$column_key" );
1034
 
 
1035
  if ( in_array( $column_key, $hidden ) ) {
1036
+ $class[] = 'hidden';
1037
  }
1038
 
1039
+ if ( 'cb' === $column_key )
 
 
1040
  $class[] = 'check-column';
1041
+ elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) )
1042
  $class[] = 'num';
1043
+
1044
+ if ( $column_key === $primary ) {
1045
+ $class[] = 'column-primary';
1046
  }
1047
 
1048
  if ( isset( $sortable[$column_key] ) ) {
1049
  list( $orderby, $desc_first ) = $sortable[$column_key];
1050
 
1051
+ if ( $current_orderby === $orderby ) {
1052
+ $order = 'asc' === $current_order ? 'desc' : 'asc';
1053
  $class[] = 'sorted';
1054
  $class[] = $current_order;
1055
  } else {
1056
+ $order = $desc_first ? 'desc' : 'asc';
1057
  $class[] = 'sortable';
1058
  $class[] = $desc_first ? 'asc' : 'desc';
1059
  }
1061
  $column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
1062
  }
1063
 
1064
+ $tag = ( 'cb' === $column_key ) ? 'td' : 'th';
1065
+ $scope = ( 'th' === $tag ) ? 'scope="col"' : '';
1066
  $id = $with_id ? "id='$column_key'" : '';
1067
 
1068
+ if ( !empty( $class ) )
1069
  $class = "class='" . join( ' ', $class ) . "'";
 
1070
 
1071
+ echo "<$tag $scope $id $class>$column_display_name</$tag>";
1072
  }
1073
  }
1074
 
1075
  /**
1076
  * Display the table
1077
  *
1078
+ * @since 3.1.0
 
1079
  */
1080
+ public function display() {
1081
+ $singular = $this->_args['singular'];
 
1082
 
1083
  $this->display_tablenav( 'top' );
1084
 
1085
+ $this->screen->render_screen_reader_content( 'heading_list' );
1086
+ ?>
1087
+ <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
1088
+ <thead>
1089
+ <tr>
1090
+ <?php $this->print_column_headers(); ?>
1091
+ </tr>
1092
+ </thead>
1093
+
1094
+ <tbody id="the-list"<?php
1095
+ if ( $singular ) {
1096
+ echo " data-wp-lists='list:$singular'";
1097
+ } ?>>
1098
+ <?php $this->display_rows_or_placeholder(); ?>
1099
+ </tbody>
1100
+
1101
+ <tfoot>
1102
+ <tr>
1103
+ <?php $this->print_column_headers( false ); ?>
1104
+ </tr>
1105
+ </tfoot>
1106
+
1107
+ </table>
1108
+ <?php
1109
  $this->display_tablenav( 'bottom' );
1110
  }
1111
 
1112
  /**
1113
+ * Get a list of CSS classes for the ITSEC_WP_List_Table table tag.
1114
  *
1115
+ * @since 3.1.0
 
1116
  *
1117
+ * @return array List of CSS classes for the table tag.
1118
  */
1119
+ protected function get_table_classes() {
1120
+ return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
 
1121
  }
1122
 
1123
  /**
1124
  * Generate the table navigation above or below the table
1125
  *
1126
+ * @since 3.1.0
1127
+ * @param string $which
1128
  */
1129
+ protected function display_tablenav( $which ) {
1130
+ if ( 'top' === $which ) {
 
1131
  wp_nonce_field( 'bulk-' . $this->_args['plural'] );
1132
  }
1133
  ?>
1134
+ <div class="tablenav <?php echo esc_attr( $which ); ?>">
 
 
 
 
 
 
 
 
1135
 
1136
+ <?php if ( $this->has_items() ): ?>
1137
+ <div class="alignleft actions bulkactions">
1138
+ <?php $this->bulk_actions( $which ); ?>
1139
  </div>
1140
+ <?php endif;
1141
+ $this->extra_tablenav( $which );
1142
+ $this->pagination( $which );
1143
+ ?>
1144
+
1145
+ <br class="clear" />
1146
+ </div>
1147
+ <?php
1148
  }
1149
 
1150
  /**
1151
  * Extra controls to be displayed between bulk actions and pagination
1152
  *
1153
+ * @since 3.1.0
1154
+ *
1155
+ * @param string $which
1156
  */
1157
+ protected function extra_tablenav( $which ) {}
 
1158
 
1159
  /**
1160
+ * Generate the tbody element for the list table.
1161
  *
1162
+ * @since 3.1.0
 
1163
  */
1164
+ public function display_rows_or_placeholder() {
 
1165
  if ( $this->has_items() ) {
1166
  $this->display_rows();
1167
  } else {
 
1168
  echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
1169
  $this->no_items();
1170
  echo '</td></tr>';
1174
  /**
1175
  * Generate the table rows
1176
  *
1177
+ * @since 3.1.0
 
1178
  */
1179
+ public function display_rows() {
1180
+ foreach ( $this->items as $item )
 
1181
  $this->single_row( $item );
 
1182
  }
1183
 
1184
  /**
1185
  * Generates content for a single row of the table
1186
  *
1187
+ * @since 3.1.0
 
1188
  *
1189
  * @param object $item The current item
1190
  */
1191
+ public function single_row( $item ) {
1192
+ echo '<tr>';
 
 
 
 
1193
  $this->single_row_columns( $item );
1194
  echo '</tr>';
1195
  }
1196
 
1197
+ /**
1198
+ *
1199
+ * @param object $item
1200
+ * @param string $column_name
1201
+ */
1202
+ protected function column_default( $item, $column_name ) {}
1203
+
1204
+ /**
1205
+ *
1206
+ * @param object $item
1207
+ */
1208
+ protected function column_cb( $item ) {}
1209
+
1210
  /**
1211
  * Generates the columns for a single row of the table
1212
  *
1213
+ * @since 3.1.0
 
1214
  *
1215
  * @param object $item The current item
1216
  */
1217
+ protected function single_row_columns( $item ) {
1218
+ list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
 
1219
 
1220
  foreach ( $columns as $column_name => $column_display_name ) {
1221
+ $classes = "$column_name column-$column_name";
1222
+ if ( $primary === $column_name ) {
1223
+ $classes .= ' has-row-actions column-primary';
1224
+ }
1225
 
 
1226
  if ( in_array( $column_name, $hidden ) ) {
1227
+ $classes .= ' hidden';
1228
  }
1229
 
1230
+ // Comments column uses HTML in the display name with screen reader text.
1231
+ // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
1232
+ $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"';
1233
+
1234
+ $attributes = "class='$classes' $data";
1235
 
1236
+ if ( 'cb' === $column_name ) {
1237
  echo '<th scope="row" class="check-column">';
1238
  echo $this->column_cb( $item );
1239
  echo '</th>';
1240
+ } elseif ( method_exists( $this, '_column_' . $column_name ) ) {
1241
+ echo call_user_func(
1242
+ array( $this, '_column_' . $column_name ),
1243
+ $item,
1244
+ $classes,
1245
+ $data,
1246
+ $primary
1247
+ );
1248
  } elseif ( method_exists( $this, 'column_' . $column_name ) ) {
1249
  echo "<td $attributes>";
1250
  echo call_user_func( array( $this, 'column_' . $column_name ), $item );
1251
+ echo $this->handle_row_actions( $item, $column_name, $primary );
1252
  echo "</td>";
1253
  } else {
1254
  echo "<td $attributes>";
1255
  echo $this->column_default( $item, $column_name );
1256
+ echo $this->handle_row_actions( $item, $column_name, $primary );
1257
  echo "</td>";
1258
  }
1259
  }
1260
  }
1261
 
1262
  /**
1263
+ * Generates and display row actions links for the list table.
1264
  *
1265
+ * @since 4.3.0
1266
+ *
1267
+ * @param object $item The item being acted upon.
1268
+ * @param string $column_name Current column name.
1269
+ * @param string $primary Primary column name.
1270
+ * @return string The row actions HTML, or an empty string if the current column is the primary column.
1271
  */
1272
+ protected function handle_row_actions( $item, $column_name, $primary ) {
1273
+ return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
1274
+ }
1275
 
1276
+ /**
1277
+ * Handle an incoming ajax request (called from admin-ajax.php)
1278
+ *
1279
+ * @since 3.1.0
1280
+ */
1281
+ public function ajax_response() {
1282
  $this->prepare_items();
1283
 
 
 
 
1284
  ob_start();
1285
  if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
1286
  $this->display_rows();
1292
 
1293
  $response = array( 'rows' => $rows );
1294
 
1295
+ if ( isset( $this->_pagination_args['total_items'] ) ) {
1296
+ $response['total_items_i18n'] = sprintf(
1297
+ _n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
1298
+ number_format_i18n( $this->_pagination_args['total_items'] )
1299
+ );
1300
  }
1301
+ if ( isset( $this->_pagination_args['total_pages'] ) ) {
1302
+ $response['total_pages'] = $this->_pagination_args['total_pages'];
1303
+ $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
 
1304
  }
1305
 
1306
+ die( wp_json_encode( $response ) );
1307
  }
1308
 
1309
  /**
1310
  * Send required variables to JavaScript land
1311
  *
 
1312
  */
1313
+ public function _js_vars() {
 
1314
  $args = array(
1315
  'class' => get_class( $this ),
1316
  'screen' => array(
1319
  )
1320
  );
1321
 
1322
+ printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
1323
  }
1324
  }
core/lib/debug.php ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Debug {
4
+ public static function print_r( $data, $args = array() ) {
5
+ echo "<style>.wp-admin .it-debug-print-r { margin-left: 170px; } .wp-admin #wpcontent .it-debug-print-r { margin-left: 0; }</style>\n";
6
+ echo "<pre style='color:black;background:white;padding:15px;font-family:\"Courier New\",Courier,monospace;font-size:12px;white-space:pre-wrap;text-align:left;max-width:100%;' class='it-debug-print-r'>";
7
+ echo self::get_print_r( $data, $args );
8
+ echo "</pre>\n";
9
+ }
10
+
11
+ public static function get_print_r( $data, $args = array() ) {
12
+ if ( is_bool( $args ) ) {
13
+ $args = array( 'expand_objects' => $args );
14
+ } else if ( is_numeric( $args ) ) {
15
+ $args = array( 'max_depth' => $args );
16
+ } else if ( ! is_array( $args ) ) {
17
+ $args = array();
18
+ }
19
+
20
+ // Create a deep copy so that variables aren't needlessly manipulated.
21
+ $data = unserialize( serialize( $data ) );
22
+
23
+
24
+ $default_args = array(
25
+ 'expand_objects' => true,
26
+ 'max_depth' => 10,
27
+ );
28
+ $args = array_merge( $default_args, $args );
29
+
30
+ if ( $args['max_depth'] < 1 ) {
31
+ $args['max_depth'] = 100;
32
+ }
33
+
34
+
35
+ return self::inspect_dive( $data, $args['expand_objects'], $args['max_depth'] );
36
+ }
37
+
38
+ public static function backtrace( $args = array() ) {
39
+ if ( is_string( $args ) ) {
40
+ $args = array( 'description' => $args );
41
+ } else if ( is_bool( $args ) ) {
42
+ $args = array( 'expand_objects' => $args );
43
+ } else if ( is_numeric( $args ) ) {
44
+ $args = array( 'max_depth' => $args );
45
+ } else if ( ! is_array( $args ) ) {
46
+ $args = array();
47
+ }
48
+
49
+ $default_args = array(
50
+ 'description' => '',
51
+ 'expand_objects' => false,
52
+ 'max_depth' => 3,
53
+ 'type' => '',
54
+ );
55
+ $args = array_merge( $default_args, $args );
56
+
57
+
58
+ if ( isset( $args['offset'] ) ) {
59
+ $args['offset']++;
60
+ } else {
61
+ $args['offset'] = 1;
62
+ }
63
+
64
+ $backtrace = self::get_backtrace( $args );
65
+
66
+ if ( 'string' == $args['type'] ) {
67
+ echo $backtrace;
68
+ } else {
69
+ $args['max_depth']++;
70
+ self::print_r( $backtrace, $args );
71
+ }
72
+ }
73
+
74
+ public static function get_backtrace( $args = array() ) {
75
+ if ( is_bool( $args ) ) {
76
+ $args = array( 'expand_objects' => $args );
77
+ } else if ( ! is_array( $args ) ) {
78
+ $args = array();
79
+ }
80
+
81
+ $default_args = array(
82
+ 'expand_objects' => false,
83
+ 'limit' => 0,
84
+ 'offset' => 0,
85
+ 'type' => 'array', // 'array' or 'string'
86
+ );
87
+ $args = array_merge( $default_args, $args );
88
+
89
+
90
+ $backtrace = debug_backtrace();
91
+ unset( $backtrace[0] );
92
+
93
+ if ( $args['offset'] > 0 ) {
94
+ $backtrace = array_slice( $backtrace, $args['offset'] );
95
+ }
96
+ if ( $args['limit'] > 0 ) {
97
+ $backtrace = array_slice( $backtrace, 0, $args['limit'] );
98
+ }
99
+
100
+ $backtrace = array_values( $backtrace );
101
+
102
+
103
+ if ( 'string' == $args['type'] ) {
104
+ $string_backtrace = '';
105
+
106
+ foreach ( $backtrace as $trace ) {
107
+ $string_backtrace .= self::get_backtrace_description( $trace, $args ) . "\n";
108
+ }
109
+
110
+ $backtrace = $string_backtrace;
111
+ }
112
+
113
+
114
+ return $backtrace;
115
+ }
116
+
117
+ private static function get_backtrace_description( $backtrace, $backtrace_args = array() ) {
118
+ $default_backtrace_args = array(
119
+ 'remove_abspath' => true,
120
+ );
121
+ $backtrace_args = array_merge( $default_backtrace_args, $backtrace_args );
122
+
123
+
124
+ extract( $backtrace );
125
+
126
+
127
+ $args = self::flatten_backtrace_description_args( $args );
128
+
129
+ if ( $backtrace_args['remove_abspath'] && isset( $file ) ) {
130
+ $file = preg_replace( '/^' . preg_quote( ABSPATH, '/' ) . '/', '', $file );
131
+ }
132
+
133
+
134
+ if ( isset( $class ) && isset( $type ) && isset( $function ) && isset( $args ) ) {
135
+ return "<strong>$class$type$function(</strong>$args<strong>)</strong>";
136
+ } else if ( isset( $function ) && isset( $args ) ) {
137
+ if ( isset( $file ) && isset( $line ) ) {
138
+ return "<strong>$function(</strong>$args<strong>)</strong> on line $line of $file";
139
+ }
140
+
141
+ return "<strong>$function(</strong>$args<strong>)</strong>";
142
+ }
143
+
144
+
145
+ return 'String!';
146
+ }
147
+
148
+ private static function flatten_backtrace_description_args( $args, $max_depth = 2, $depth = 0 ) {
149
+ if ( is_string( $args ) ) {
150
+ return "'$args'";
151
+ } else if ( is_int( $args ) ) {
152
+ return "(int) $args";
153
+ } else if ( is_float( $args ) ) {
154
+ return "(float) $args";
155
+ } else if ( is_bool( $args ) ) {
156
+ return '(bool) ' . ( $args ? 'true' : 'false' );
157
+ } else if ( is_object( $args ) ) {
158
+ return '(object) ' . get_class( $args );
159
+ } else if ( ! is_array( $args ) ) {
160
+ return '[unknown]';
161
+ }
162
+
163
+ if ( $depth === $max_depth ) {
164
+ if ( empty( $args ) ) {
165
+ return 'array()';
166
+ } else {
167
+ return 'array( ' . count( $args ) . ' )';
168
+ }
169
+ }
170
+
171
+
172
+ $flat_args = array();
173
+
174
+ foreach ( $args as $arg ) {
175
+ $flat_args[] = self::flatten_backtrace_description_args( $arg, $max_depth, $depth + 1 );
176
+ }
177
+
178
+ $args = implode( ', ', $flat_args );
179
+
180
+ if ( ! empty( $args ) ) {
181
+ $args = " $args ";
182
+ }
183
+
184
+ if ( 0 === $depth ) {
185
+ return $args;
186
+ }
187
+
188
+ return "array($args)";
189
+ }
190
+
191
+ private static function pad( $depth, $pad = ' ' ) {
192
+ $retval = '';
193
+
194
+ for ( $x = 0; $x <= $depth; $x++ ) {
195
+ $retval .= $pad;
196
+ }
197
+
198
+ return $retval;
199
+ }
200
+
201
+ private static function is_callable_function( $function ) {
202
+ if ( ! is_callable( $function ) ) {
203
+ return false;
204
+ }
205
+
206
+ if ( ! isset( $GLOBALS['itsec_debug_cached_values'] ) ) {
207
+ $GLOBALS['itsec_debug_cached_values'] = array();
208
+ }
209
+
210
+ if ( ! isset( $GLOBALS['itsec_debug_cached_values']['ini_get:disable_functions'] ) ) {
211
+ $GLOBALS['itsec_debug_cached_values']['var:disable_functions'] = preg_split( '/\s*,\s*/', (string) ini_get( 'disable_functions' ) );
212
+ }
213
+
214
+ if ( in_array( $function, $GLOBALS['itsec_debug_cached_values']['var:disable_functions'] ) ) {
215
+ return false;
216
+ }
217
+
218
+ if ( ! isset( $GLOBALS['itsec_debug_cached_values']['ini_get:suhosin.executor.func.blacklist'] ) ) {
219
+ $GLOBALS['itsec_debug_cached_values']['ini_get:suhosin.executor.func.blacklist'] = preg_split( '/\s*,\s*/', (string) ini_get( 'suhosin.executor.func.blacklist' ) );
220
+ }
221
+
222
+ if ( in_array( $function, $GLOBALS['itsec_debug_cached_values']['ini_get:suhosin.executor.func.blacklist'] ) ) {
223
+ return false;
224
+ }
225
+
226
+ return true;
227
+ }
228
+
229
+ private static function inspect_dive( $data, $expand_objects, $max_depth, $depth = 0, $show_array_header = true ) {
230
+ $pad = self::pad( $depth, ' ' );
231
+
232
+ if ( is_string( $data ) ) {
233
+ if ( empty( $data ) ) {
234
+ return "<strong>[empty string]</strong>";
235
+ } else {
236
+ if ( self::is_callable_function( 'mb_detect_encoding' ) && ( 'UTF-8' !== mb_detect_encoding( $data, 'UTF-8', true ) ) && self::is_callable_function( 'utf8_encode' ) ) {
237
+ $data = utf8_encode( $data );
238
+ }
239
+
240
+ $flags = ENT_COMPAT;
241
+
242
+ if ( defined( 'ENT_HTML401' ) ) {
243
+ $flags |= ENT_HTML401;
244
+ }
245
+
246
+ return htmlspecialchars( $data, $flags, 'UTF-8', false );
247
+ }
248
+ }
249
+
250
+ if ( is_bool( $data ) ) {
251
+ return ( $data ) ? '<strong>[boolean] true</strong>' : '<strong>[boolean] false</strong>';
252
+ }
253
+
254
+ if ( is_null( $data ) ) {
255
+ return '<strong>null</strong>';
256
+ }
257
+
258
+ if ( is_object( $data ) ) {
259
+ $class_name = get_class( $data );
260
+ $retval = "<strong>Object</strong> $class_name";
261
+
262
+ if ( ! $expand_objects || ( $depth == $max_depth ) ) {
263
+ return $retval;
264
+ }
265
+
266
+ $vars = get_object_vars( $data );
267
+
268
+ if ( empty( $vars ) ) {
269
+ $vars = '';
270
+ } else {
271
+ $vars = self::inspect_dive( $vars, $expand_objects, $max_depth, $depth, false );
272
+ }
273
+
274
+ $retval .= "$vars";
275
+
276
+ return $retval;
277
+ }
278
+
279
+ if ( is_array( $data ) ) {
280
+ $retval = ( $show_array_header ) ? '<strong>Array</strong>' : '';
281
+
282
+ if ( empty( $data ) ) {
283
+ return "$retval()";
284
+ }
285
+ if ( $depth == $max_depth ) {
286
+ return "$retval( " . count( $data ) . " )";
287
+ }
288
+
289
+ $max = 0;
290
+
291
+ foreach ( array_keys( $data ) as $index ) {
292
+ if ( strlen( $index ) > $max ) {
293
+ $max = strlen( $index );
294
+ }
295
+ }
296
+
297
+ foreach ( $data as $index => $val ) {
298
+ $spaces = self::pad( $max - strlen( $index ), ' ' );
299
+ $retval .= "\n$pad" . htmlspecialchars( $index ) . "$spaces <strong>=&gt;</strong> " . self::inspect_dive( $val, $expand_objects, $max_depth, $depth + 1 );
300
+ }
301
+
302
+ return $retval;
303
+ }
304
+
305
+ return '<strong>[' . gettype( $data ) . ']</strong> ' . $data;
306
+ }
307
+ }
core/lib/form.php CHANGED
@@ -13,7 +13,7 @@ final class ITSEC_Form {
13
  $this->options =& $options;
14
  }
15
 
16
- public static function get_post_data() {
17
  $remove_vars = array( 'itsec-nonce', '_wp_http_referer' );
18
  $data = $_POST;
19
 
@@ -49,10 +49,8 @@ final class ITSEC_Form {
49
  if ( is_null( $value ) ) {
50
  ITSEC_Form::add_array_value( $data, $index, $default );
51
  }
52
- } else {
53
- if ( ! is_array( $value ) ) {
54
- ITSEC_Form::add_array_value( $data, $index, $default );
55
- }
56
  }
57
  }
58
 
@@ -87,6 +85,14 @@ final class ITSEC_Form {
87
  unset( $data['--itsec-form-convert-to-array'] );
88
  }
89
 
 
 
 
 
 
 
 
 
90
  return $data;
91
  }
92
 
@@ -359,6 +365,7 @@ final class ITSEC_Form {
359
  }
360
 
361
  $options['type'] = 'password';
 
362
  $this->add_custom_input( $var, $options );
363
  }
364
 
@@ -435,6 +442,16 @@ final class ITSEC_Form {
435
  $this->add_custom_input( $var, $options );
436
  }
437
 
 
 
 
 
 
 
 
 
 
 
438
  public function add_hidden( $var, $options = array() ) {
439
  if ( ! is_array( $options ) ) {
440
  $options = array( 'value' => $options );
@@ -592,7 +609,7 @@ final class ITSEC_Form {
592
  }
593
  } else if ( 'radio' === $options['type'] ) {
594
  if ( ! isset( $this->tracked_strings[$options['name']] ) ) {
595
- echo '<input type="hidden" name="--itsec-form-tracked-empty-strings[]" value="' . esc_attr( $options['name'] ) . '" />' . "\n";
596
  $this->tracked_strings[$options['name']] = true;
597
  }
598
  }
13
  $this->options =& $options;
14
  }
15
 
16
+ public static function get_post_data( $desired_inputs = false ) {
17
  $remove_vars = array( 'itsec-nonce', '_wp_http_referer' );
18
  $data = $_POST;
19
 
49
  if ( is_null( $value ) ) {
50
  ITSEC_Form::add_array_value( $data, $index, $default );
51
  }
52
+ } else if ( ! is_array( $value ) ) {
53
+ ITSEC_Form::add_array_value( $data, $index, $default );
 
 
54
  }
55
  }
56
 
85
  unset( $data['--itsec-form-convert-to-array'] );
86
  }
87
 
88
+ if ( is_array( $desired_inputs ) ) {
89
+ foreach ( array_keys( $data ) as $key ) {
90
+ if ( ! in_array( $key, $desired_inputs ) ) {
91
+ unset( $data[$key] );
92
+ }
93
+ }
94
+ }
95
+
96
  return $data;
97
  }
98
 
365
  }
366
 
367
  $options['type'] = 'password';
368
+
369
  $this->add_custom_input( $var, $options );
370
  }
371
 
442
  $this->add_custom_input( $var, $options );
443
  }
444
 
445
+ public function add_number( $var, $options = array() ) {
446
+ if ( ! is_array( $options ) ) {
447
+ $options = array( 'value' => $options );
448
+ }
449
+
450
+ $options['type'] = 'number';
451
+
452
+ $this->add_custom_input( $var, $options );
453
+ }
454
+
455
  public function add_hidden( $var, $options = array() ) {
456
  if ( ! is_array( $options ) ) {
457
  $options = array( 'value' => $options );
609
  }
610
  } else if ( 'radio' === $options['type'] ) {
611
  if ( ! isset( $this->tracked_strings[$options['name']] ) ) {
612
+ echo '<input type="hidden" name="--itsec-form-tracked-strings[]" value="' . esc_attr( $options['name'] ) . '" />' . "\n";
613
  $this->tracked_strings[$options['name']] = true;
614
  }
615
  }
core/lib/log-util.php ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Log_Util {
4
+ public static function get_type_counts( $min_timestamp = 0 ) {
5
+ global $wpdb;
6
+
7
+
8
+ $where = 'parent_id=0';
9
+ $prepare_args = array();
10
+
11
+ if ( $min_timestamp > 0 ) {
12
+ $where .= ' AND init_timestamp>%s';
13
+ $prepare_args[] = date( 'Y-m-d H:i:s', $min_timestamp );
14
+ }
15
+
16
+ $query = "SELECT type, COUNT(*) AS count FROM `{$wpdb->base_prefix}itsec_logs` WHERE $where GROUP BY type";
17
+
18
+ if ( ! empty( $prepare_args ) ) {
19
+ $query = $wpdb->prepare( $query, $prepare_args );
20
+ }
21
+
22
+ $results = $wpdb->get_results( $query, ARRAY_A );
23
+
24
+ $counts = array();
25
+
26
+ foreach ( $results as $result ) {
27
+ if ( 'process-start' === $result['type'] ) {
28
+ $result['type'] = 'process';
29
+ }
30
+
31
+ if ( isset( $counts[$result['type']] ) ) {
32
+ $counts[$result['type']] += $result['count'];
33
+ } else {
34
+ $counts[$result['type']] = $result['count'];
35
+ }
36
+ }
37
+
38
+ return $counts;
39
+ }
40
+
41
+ public static function get_entries( $filters = array(), $limit = 100, $page = 1, $sort_by_column = 'timestamp', $sort_direction = 'DESC', $columns = false ) {
42
+ global $wpdb;
43
+
44
+
45
+ $get_count = false;
46
+ $min_timestamp = false;
47
+
48
+ if ( isset( $filters['__get_count'] ) ) {
49
+ if ( $filters['__get_count'] ) {
50
+ $get_count = true;
51
+ }
52
+
53
+ unset( $filters['__get_count'] );
54
+ }
55
+
56
+ if ( isset( $filters['__min_timestamp'] ) ) {
57
+ $min_timestamp = $filters['__min_timestamp'];
58
+ unset( $filters['__min_timestamp'] );
59
+ }
60
+
61
+ $limit = max( 0, min( 100, intval( $limit ) ) );
62
+ $page = max( 1, intval( $page ) );
63
+
64
+ $sort_direction = strtoupper( $sort_direction );
65
+ if ( ! in_array( $sort_direction, array( 'DESC', 'ASC' ) ) ) {
66
+ $sort_direction = 'DESC';
67
+ }
68
+
69
+
70
+ $valid_columns = array(
71
+ 'id',
72
+ 'parent_id',
73
+ 'module',
74
+ 'type',
75
+ 'code',
76
+ 'timestamp',
77
+ 'init_timestamp',
78
+ 'remote_ip',
79
+ 'user_id',
80
+ 'url',
81
+ 'memory_current',
82
+ 'memory_peak',
83
+ );
84
+
85
+ if ( false === $columns ) {
86
+ $columns = $valid_columns;
87
+ } else if ( 'all' === $columns ) {
88
+ $columns = array_merge( $valid_columns, array( 'data' ) );
89
+ }
90
+
91
+
92
+ if ( $get_count ) {
93
+ $query = "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_logs`";
94
+ } else {
95
+ $query = "SELECT " . implode( ', ', $columns ) . " FROM `{$wpdb->base_prefix}itsec_logs`";
96
+ }
97
+
98
+ $prepare_args = array();
99
+
100
+
101
+ $where_entries = array();
102
+
103
+ if ( ! isset( $filters['parent_id'] ) ) {
104
+ $filters['parent_id'] = 0;
105
+ }
106
+
107
+ foreach ( (array) $filters as $column => $value ) {
108
+ if ( preg_match( '/^(.+)_not$/', $column, $match ) ) {
109
+ $not = true;
110
+ $column = $match[1];
111
+ } else {
112
+ $not = false;
113
+ }
114
+
115
+ if ( preg_match( '/^(.+)_(min|max)$/', $column, $match ) ) {
116
+ if ( ! in_array( $match[1], $valid_columns ) ) {
117
+ continue;
118
+ }
119
+
120
+ if ( 'min' === $match[2] ) {
121
+ $where_entries[] = "'$column'>=%s";
122
+ $prepare_args[] = $value;
123
+ } else {
124
+ $where_entries[] = "'column'<=%s";
125
+ $prepare_args[] = $value;
126
+ }
127
+ } else if ( ! in_array( $column, $valid_columns ) ) {
128
+ continue;
129
+ } else if ( is_array( $value ) ) {
130
+ if ( ! empty( $value ) ) {
131
+ if ( $not ) {
132
+ $where_entries[] = "$column NOT IN (" . implode( ', ', array_fill( 0, count( $value ), '%s' ) ) . ")";
133
+ } else {
134
+ $where_entries[] = "$column IN (" . implode( ', ', array_fill( 0, count( $value ), '%s' ) ) . ")";
135
+ }
136
+ $prepare_args = array_merge( $prepare_args, $value );
137
+ }
138
+ } else if ( false !== strpos( $value, '%' ) ) {
139
+ if ( $not ) {
140
+ $where_entries[] = "$column NOT LIKE %s";
141
+ } else {
142
+ $where_entries[] = "$column LIKE %s";
143
+ }
144
+ $prepare_args[] = $value;
145
+ } else {
146
+ if ( $not ) {
147
+ $where_entries[] = "$column<>%s";
148
+ } else {
149
+ $where_entries[] = "$column=%s";
150
+ }
151
+ $prepare_args[] = $value;
152
+ }
153
+ }
154
+
155
+ if ( false !== $min_timestamp ) {
156
+ $where_entries[] = 'init_timestamp>%s';
157
+ $prepare_args[] = date( 'Y-m-d H:i:s', $min_timestamp );
158
+ }
159
+
160
+ $query .= ' WHERE ' . implode( ' AND ', $where_entries );
161
+
162
+
163
+ if ( ! $get_count ) {
164
+ if ( ! is_array( $sort_by_column ) ) {
165
+ $sort_by_column = array( "$sort_by_column $sort_direction" );
166
+ }
167
+
168
+ $query .= ' ORDER BY ' . implode( ', ', $sort_by_column );
169
+
170
+
171
+ if ( $limit > 0 ) {
172
+ $offset = ( $page - 1 ) * $limit;
173
+ $query .= " LIMIT $offset,$limit";
174
+ }
175
+ }
176
+
177
+ $query = $wpdb->prepare( $query, $prepare_args );
178
+
179
+
180
+ if ( $get_count ) {
181
+ return intval( $wpdb->get_var( $query ) );
182
+ }
183
+
184
+ $rows = $wpdb->get_results( $query, ARRAY_A );
185
+
186
+ if ( is_null( $rows ) ) {
187
+ return new WP_Error( 'itsec-log-util-failed-query', sprintf( esc_html__( 'A query failure prevented the log data from being accessed: %s', 'better-wp-security' ), $wpdb->last_error ) );
188
+ }
189
+
190
+ foreach ( $rows as $index => $row ) {
191
+ if ( ! isset( $row['data'] ) ) {
192
+ break;
193
+ }
194
+
195
+ $data = unserialize( $row['data'] );
196
+
197
+ if ( false !== $data || 'b:0;' === $row['data'] ) {
198
+ $rows[$index]['data'] = $data;
199
+ }
200
+ }
201
+
202
+ return $rows;
203
+ }
204
+
205
+ public static function get_logs_page_screen_options() {
206
+ $defaults = array(
207
+ 'per_page' => 20,
208
+ 'default_view' => 'important',
209
+ 'color' => true,
210
+ 'show_debug' => false,
211
+ 'show_process' => false,
212
+ 'last_seen' => 0,
213
+ );
214
+
215
+ $options = get_user_option( 'itsec_logs_page_screen_options' );
216
+
217
+ if ( is_array( $options ) ) {
218
+ $options = array_merge( $defaults, $options );
219
+ } else {
220
+ $options = $defaults;
221
+
222
+ if ( $user = wp_get_current_user() ) {
223
+ update_user_option( $user->ID, 'itsec_logs_page_screen_options', $options, true );
224
+ }
225
+ }
226
+
227
+ return $options;
228
+ }
229
+
230
+ public static function set_logs_page_screen_options( $options ) {
231
+ if ( ! $user = wp_get_current_user() ) {
232
+ return;
233
+ }
234
+
235
+ if ( isset( $options['per_page'] ) && ( $options['per_page'] < 1 || $options['per_page'] > 999 ) ) {
236
+ unset( $options['per_page'] );
237
+ }
238
+ if ( isset( $options['default_view'] ) && ! in_array( $options['default_view'], array( 'important', 'all', 'critical-issue' ) ) ) {
239
+ unset( $options['default_view'] );
240
+ }
241
+ if ( isset( $options['last_seen'] ) ) {
242
+ $options['last_seen'] = intval( $options['last_seen'] );
243
+
244
+ if ( $options['last_seen'] < 0 || $options['last_seen'] > ITSEC_Core::get_current_time_gmt() ) {
245
+ unset( $options['last_seen'] );
246
+ }
247
+ }
248
+
249
+ $options = array_merge( self::get_logs_page_screen_options(), $options );
250
+
251
+ update_user_option( $user->ID, 'itsec_logs_page_screen_options', $options, true );
252
+ }
253
+
254
+ public static function has_old_log_entries() {
255
+ global $wpdb;
256
+
257
+ if ( ! $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}itsec_log'" ) ) {
258
+ return false;
259
+ }
260
+
261
+ $num_entries = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}itsec_log" );
262
+
263
+ if ( empty( $num_entries ) ) {
264
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}itsec_log" );
265
+ }
266
+
267
+ return true;
268
+ }
269
+
270
+ public static function migrate_old_log_entries() {
271
+ global $wpdb;
272
+
273
+ $max = 50;
274
+ $num_entries = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}itsec_log" );
275
+ $num_loops = min( $max, $num_entries );
276
+
277
+ for ( $count = 1; $count <= $num_loops; $count++ ) {
278
+ $old_entry = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}itsec_log ORDER BY log_date_gmt LIMIT 1", ARRAY_A );
279
+ $entry = self::get_new_log_entry_from_old( $old_entry );
280
+
281
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_logs", $entry );
282
+ $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}itsec_log WHERE log_id=%d", $old_entry['log_id'] ) );
283
+ }
284
+
285
+ if ( $num_entries > $max ) {
286
+ return false;
287
+ }
288
+
289
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}itsec_log" );
290
+
291
+ return true;
292
+ }
293
+
294
+ public static function get_new_log_entry_from_old( $old_entry ) {
295
+ $old_entry['log_data'] = unserialize( $old_entry['log_data'] );
296
+
297
+ $entry = array(
298
+ 'module' => $old_entry['log_type'],
299
+ 'code' => $old_entry['log_function'],
300
+ 'type' => 'notice',
301
+ 'user_id' => $old_entry['log_user'],
302
+ 'timestamp' => $old_entry['log_date_gmt'],
303
+ 'init_timestamp' => $old_entry['log_date_gmt'],
304
+ 'remote_ip' => $old_entry['log_host'],
305
+ 'url' => $old_entry['log_url'],
306
+ );
307
+
308
+ if ( 'lockout' === $old_entry['log_type'] ) {
309
+ if ( isset( $old_entry['log_data']['expires'] ) ) {
310
+ $entry['type'] = 'action';
311
+
312
+ if ( empty( $old_entry['log_host'] ) ) {
313
+ $entry['code'] = 'user-lockout';
314
+ } else {
315
+ $entry['code'] = 'host-lockout';
316
+ }
317
+ } else {
318
+ if ( empty( $old_entry['log_host'] ) ) {
319
+ $entry['code'] = 'whitelisted-host-triggered-user-lockout';
320
+ } else {
321
+ $entry['code'] = 'whitelisted-host-triggered-host-lockout';
322
+ }
323
+ }
324
+ } else if ( 'file_change' === $old_entry['log_type'] ) {
325
+ $entry['type'] = 'warning';
326
+ $entry['code'] = 'changes-found';
327
+ $entry['data'] = $old_entry['log_data'];
328
+ } else if ( 'malware' === $old_entry['log_type'] ) {
329
+ $entry['code'] = 'scan';
330
+ $entry['data'] = array( 'results' => $old_entry['log_data'] );
331
+ } else if ( 'backup' === $old_entry['log_type'] ) {
332
+ $entry['code'] = 'details';
333
+ } else if ( 'four_oh_four' === $old_entry['log_type'] ) {
334
+ $entry['code'] = 'found_404';
335
+ } else if ( 'ipcheck' === $old_entry['log_type'] ) {
336
+ if ( empty( $old_entry['log_data'] ) ) {
337
+ $entry['code'] = 'failed-login-by-blocked-ip';
338
+ } else {
339
+ $entry['type'] = 'action';
340
+ $entry['code'] = 'ip-blocked';
341
+ }
342
+ } else if ( 'brute_force' === $old_entry['log_type'] ) {
343
+ if ( 'admin' === $old_entry['log_username'] ) {
344
+ $entry['code'] = 'auto-ban-admin-username';
345
+ } else {
346
+ $entry['code'] = 'invalid-login';
347
+ }
348
+ } else if ( 'away_mode' === $old_entry['log_type'] ) {
349
+ $entry['code'] = 'away-mode-active';
350
+ } else if ( 'recaptcha' === $old_entry['log_type'] ) {
351
+ $entry['code'] = 'failed-validation';
352
+ } else if ( 'user_logging' === $old_entry['log_type'] ) {
353
+ if ( isset( $old_entry['log_data']['post'] ) ) {
354
+ $entry['code'] = 'post-status-changed';
355
+ } else if ( empty( $old_entry['log_username'] ) ) {
356
+ $entry['code'] = 'user-logged-out';
357
+ } else {
358
+ $entry['code'] = 'user-logged-in';
359
+ }
360
+ }
361
+
362
+ if ( isset( $entry['data'] ) ) {
363
+ $entry['data'] = serialize( $entry['data'] );
364
+ }
365
+
366
+ return $entry;
367
+ }
368
+ }
core/lib/log.php ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Log {
4
+ /* Critical issues are very important events that administrators should be notified about, such as finding malware
5
+ * on the site or detecting a security breach.
6
+ */
7
+ public static function add_critical_issue( $module, $code, $data = false ) {
8
+ return self::add( $module, $code, $data, 'critical-issue' );
9
+ }
10
+
11
+ /* Actions are noteworthy automated events that change the functionality of the site based upon certain criteria,
12
+ * such as locking out an IP address due to bruteforce attempts.
13
+ */
14
+ public static function add_action( $module, $code, $data = false ) {
15
+ return self::add( $module, $code, $data, 'action' );
16
+ }
17
+
18
+ /* Fatal errors are critical problems detected in the code that could and should be reserved for very rare but
19
+ * highly problematic situations, such as a catch handler in a try/catch block or a shutdown handler running before
20
+ * a process finishes.
21
+ */
22
+ public static function add_fatal_error( $module, $code, $data = false ) {
23
+ return self::add( $module, $code, $data, 'fatal' );
24
+ }
25
+
26
+ /* Errors are events that indicate a failure of some sort, such as failure to write to a file or an inability to
27
+ * request a remote URL.
28
+ */
29
+ public static function add_error( $module, $code, $data = false ) {
30
+ return self::add( $module, $code, $data, 'error' );
31
+ }
32
+
33
+ /* Warnings are noteworthy events that might indicate an issue, such as finding changed files.
34
+ */
35
+ public static function add_warning( $module, $code, $data = false ) {
36
+ return self::add( $module, $code, $data, 'warning' );
37
+ }
38
+
39
+ /* Notices keep track of events that should be tracked but do not necessarily indicate an issue, such as requests
40
+ * for files that do not exist and completed scans that did not find any issues.
41
+ */
42
+ public static function add_notice( $module, $code, $data = false ) {
43
+ return self::add( $module, $code, $data, 'notice' );
44
+ }
45
+
46
+ /* Debug events are to be used in situations where extra information about a specific process could be helpful to
47
+ * have when investigating an issue but the information would typically be uninteresting to the user, such as
48
+ * noting the use of a compatibility function.
49
+ */
50
+ public static function add_debug( $module, $code, $data = false ) {
51
+ return self::add( $module, $code, $data, 'debug' );
52
+ }
53
+
54
+ /* Process events allow for creating single entries that have a start, zero or more updates, and a stopping point.
55
+ * This allows for benchmarking performance of long-running code in addition to finding issues such as terminated
56
+ * execution due to the missing process-stop entry.
57
+ */
58
+ public static function add_process_start( $module, $code, $data = false ) {
59
+ $id = self::add( $module, $code, $data, 'process-start' );
60
+
61
+ return compact( 'module', 'code', 'id' );
62
+ }
63
+
64
+ public static function add_process_update( $reference, $data = false ) {
65
+ self::add( $reference['module'], $reference['code'], $data, 'process-update', $reference['id'] );
66
+ }
67
+
68
+ public static function add_process_stop( $reference, $data = false ) {
69
+ self::add( $reference['module'], $reference['code'], $data, 'process-stop', $reference['id'] );
70
+ }
71
+
72
+ private static function add( $module, $code, $data, $type, $parent_id = 0 ) {
73
+ $data = array(
74
+ 'parent_id' => $parent_id,
75
+ 'module' => $module,
76
+ 'code' => $code,
77
+ 'data' => $data,
78
+ 'type' => $type,
79
+ 'timestamp' => gmdate( 'Y-m-d H:i:s' ),
80
+ 'init_timestamp' => gmdate( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
81
+ 'memory_current' => memory_get_usage(),
82
+ 'memory_peak' => memory_get_peak_usage(),
83
+ 'url' => ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
84
+ 'blog_id' => get_current_blog_id(),
85
+ 'user_id' => get_current_user_id(),
86
+ 'remote_ip' => ITSEC_Lib::get_ip(),
87
+ );
88
+
89
+ $log_type = ITSEC_Modules::get_setting( 'global', 'log_type' );
90
+
91
+ if ( 'database' === $log_type ) {
92
+ $id = self::add_to_db( $data );
93
+ } else if ( 'file' === $log_type ) {
94
+ $id = self::add_to_file( $data );
95
+ } else {
96
+ $id = self::add_to_db( $data );
97
+ self::add_to_file( $data, $id );
98
+ }
99
+
100
+ return $id;
101
+ }
102
+
103
+ private static function add_to_db( $data ) {
104
+ global $wpdb;
105
+
106
+ $format = array();
107
+
108
+ foreach ( $data as $key => $value ) {
109
+ if ( is_int( $value ) ) {
110
+ $format[] = '%d';
111
+ } else {
112
+ $format[] = '%s';
113
+
114
+ if ( ! is_string( $value ) ) {
115
+ $data[$key] = serialize( $value );
116
+ }
117
+ }
118
+ }
119
+
120
+ $result = $wpdb->insert( "{$wpdb->base_prefix}itsec_logs", $data, $format );
121
+
122
+ if ( false === $result ) {
123
+ error_log( "Failed to insert log entry: {$wpdb->last_error}" );
124
+ return new WP_Error( 'itsec-log-failed-db-insert', sprintf( esc_html__( 'Failed to insert log entry: %s', 'better-wp-security' ), $wpdb->last_error ) );
125
+ }
126
+
127
+ return $wpdb->insert_id;
128
+ }
129
+
130
+ private static function add_to_file( $data, $id = false ) {
131
+ if ( false === $id ) {
132
+ $id = microtime( true );
133
+ }
134
+
135
+
136
+ $file = self::get_log_file_path();
137
+
138
+ if ( is_wp_error( $file ) ) {
139
+ return $file;
140
+ }
141
+
142
+
143
+ $entries = array();
144
+
145
+ foreach ( $data as $value ) {
146
+ if ( is_object( $value ) || is_array( $value ) ) {
147
+ $value = serialize( $value );
148
+ } else {
149
+ $value = (string) $value;
150
+ }
151
+
152
+ $value = str_replace( '"', '""', $value );
153
+
154
+ if ( preg_match( '/[", ]/', $value ) ) {
155
+ $value = "\"$value\"";
156
+ }
157
+
158
+ $entries[] = $value;
159
+ }
160
+
161
+ $entry = implode( ',', $entries ) . "\n";
162
+
163
+
164
+ $result = file_put_contents( $file, $entry, FILE_APPEND );
165
+
166
+ if ( false === $result ) {
167
+ return new WP_Error( 'itsec-log-failed-to-write-to-file', __( 'Unable to write to the log file. This could indicate that there is no space available, that there is a permissions issue, or that the server is not configured properly.', 'better-wp-security' ) );
168
+ }
169
+
170
+
171
+ return $id;
172
+ }
173
+
174
+ public static function get_log_file_path() {
175
+ static $log_file = false;
176
+
177
+ if ( false !== $log_file ) {
178
+ return $log_file;
179
+ }
180
+
181
+ $log_location = ITSEC_Modules::get_setting( 'global', 'log_location' );
182
+ $log_info = ITSEC_Modules::get_setting( 'global', 'log_info' );
183
+
184
+ if ( empty( $log_info ) ) {
185
+ $log_info = substr( sanitize_title( get_bloginfo( 'name' ) ), 0, 20 ) . '-' . wp_generate_password( 30, false );
186
+
187
+ ITSEC_Modules::set_setting( 'global', 'log_info', $log_info );
188
+ }
189
+
190
+ $log_file = "$log_location/event-log-$log_info.log";
191
+
192
+ if ( ! file_exists( $log_file ) ) {
193
+ $header = "parent_id,module,code,data,type,timestamp,init_timestamp,memory_current,memory_peak,user_id,remote_ip\n";
194
+
195
+ file_put_contents( $log_file, $header );
196
+ }
197
+
198
+ return $log_file;
199
+ }
200
+
201
+ public static function get_entries( $filters = array(), $limit = 0, $page = 1, $sort_by_column = 'id', $sort_direction = 'DESC', $columns = false ) {
202
+ require_once( dirname( __FILE__ ) . '/log-util.php' );
203
+
204
+ return ITSEC_Log_Util::get_entries( $filters, $limit, $page, $sort_by_column, $sort_direction, $columns );
205
+ }
206
+
207
+ public static function get_entry( $id ) {
208
+ require_once( dirname( __FILE__ ) . '/log-util.php' );
209
+
210
+ $entries = ITSEC_Log_Util::get_entries( array( 'id' => $id ), 0, 1, 'id', 'DESC', 'all' );
211
+
212
+ return $entries[0];
213
+ }
214
+
215
+ public static function get_number_of_entries( $filters = array() ) {
216
+ $filters['__get_count'] = true;
217
+ return self::get_entries( $filters );
218
+ }
219
+
220
+ public static function get_type_counts( $min_timestamp = 0 ) {
221
+ require_once( dirname( __FILE__ ) . '/log-util.php' );
222
+
223
+ return ITSEC_Log_Util::get_type_counts( $min_timestamp );
224
+ }
225
+
226
+ public static function get_types_for_display() {
227
+ return array(
228
+ 'critical-issue' => esc_html__( 'Critical Issue', 'better-wp-security' ),
229
+ 'action' => esc_html__( 'Action', 'better-wp-security' ),
230
+ 'fatal-error' => esc_html__( 'Fatal Error', 'better-wp-security' ),
231
+ 'error' => esc_html__( 'Error', 'better-wp-security' ),
232
+ 'warning' => esc_html__( 'Warning', 'better-wp-security' ),
233
+ 'notice' => esc_html__( 'Notice', 'better-wp-security' ),
234
+ 'debug' => esc_html__( 'Debug', 'better-wp-security' ),
235
+ 'process-start' => esc_html__( 'Process', 'better-wp-security' ),
236
+ );
237
+ }
238
+ }
core/lib/schema.php ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Schema {
4
+ /**
5
+ * Creates appropriate database tables.
6
+ *
7
+ * Uses dbdelta to create database tables either on activation or in the event that one is missing.
8
+ *
9
+ * @since 3.9.0
10
+ *
11
+ * @return void
12
+ */
13
+ public static function create_database_tables() {
14
+ global $wpdb;
15
+
16
+ $charset_collate = $wpdb->get_charset_collate();
17
+
18
+ $tables = "
19
+ CREATE TABLE {$wpdb->base_prefix}itsec_logs (
20
+ id bigint(20) unsigned NOT NULL auto_increment,
21
+ parent_id bigint(20) unsigned NOT NULL default '0',
22
+ module varchar(50) NOT NULL default '',
23
+ code varchar(100) NOT NULL default '',
24
+ data longtext NOT NULL default '',
25
+ type varchar(20) NOT NULL default 'notice',
26
+ timestamp datetime NOT NULL default '0000-00-00 00:00:00',
27
+ init_timestamp datetime NOT NULL default '0000-00-00 00:00:00',
28
+ memory_current bigint(20) unsigned NOT NULL default '0',
29
+ memory_peak bigint(20) unsigned NOT NULL default '0',
30
+ url varchar(500) NOT NULL default '',
31
+ blog_id bigint(20) NOT NULL default '0',
32
+ user_id bigint(20) unsigned NOT NULL default '0',
33
+ remote_ip varchar(50) NOT NULL default '',
34
+ PRIMARY KEY (id),
35
+ KEY module (module),
36
+ KEY code (code),
37
+ KEY type (type),
38
+ KEY timestamp (timestamp),
39
+ KEY user_id (user_id),
40
+ KEY blog_id (blog_id)
41
+ ) $charset_collate;
42
+
43
+ CREATE TABLE {$wpdb->base_prefix}itsec_lockouts (
44
+ lockout_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
45
+ lockout_type varchar(20) NOT NULL,
46
+ lockout_start datetime NOT NULL,
47
+ lockout_start_gmt datetime NOT NULL,
48
+ lockout_expire datetime NOT NULL,
49
+ lockout_expire_gmt datetime NOT NULL,
50
+ lockout_host varchar(40),
51
+ lockout_user bigint(20) UNSIGNED,
52
+ lockout_username varchar(60),
53
+ lockout_active int(1) NOT NULL DEFAULT 1,
54
+ PRIMARY KEY (lockout_id),
55
+ KEY lockout_expire_gmt (lockout_expire_gmt),
56
+ KEY lockout_host (lockout_host),
57
+ KEY lockout_user (lockout_user),
58
+ KEY lockout_username (lockout_username),
59
+ KEY lockout_active (lockout_active)
60
+ ) $charset_collate;
61
+
62
+ CREATE TABLE {$wpdb->base_prefix}itsec_temp (
63
+ temp_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
64
+ temp_type varchar(20) NOT NULL,
65
+ temp_date datetime NOT NULL,
66
+ temp_date_gmt datetime NOT NULL,
67
+ temp_host varchar(40),
68
+ temp_user bigint(20) UNSIGNED,
69
+ temp_username varchar(60),
70
+ PRIMARY KEY (temp_id),
71
+ KEY temp_date_gmt (temp_date_gmt),
72
+ KEY temp_host (temp_host),
73
+ KEY temp_user (temp_user),
74
+ KEY temp_username (temp_username)
75
+ ) $charset_collate;";
76
+
77
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
78
+ dbDelta( $tables );
79
+ }
80
+
81
+ public static function remove_database_tables() {
82
+ global $wpdb;
83
+
84
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}itsec_logs;" );
85
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}itsec_log;" );
86
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}itsec_lockouts;" );
87
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}itsec_temp;" );
88
+ }
89
+ }
core/lockout.php CHANGED
@@ -87,9 +87,6 @@ final class ITSEC_Lockout {
87
  //Process clear lockout form
88
  add_action( 'itsec_admin_init', array( $this, 'release_lockout' ) );
89
 
90
- //Register Logger
91
- add_filter( 'itsec_logger_modules', array( $this, 'register_logger' ) );
92
-
93
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
94
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
95
 
@@ -98,6 +95,8 @@ final class ITSEC_Lockout {
98
 
99
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
100
  add_filter( 'itsec_lockout_notification_strings', array( $this, 'notification_strings' ) );
 
 
101
  }
102
 
103
  public function init_settings_page() {
@@ -160,7 +159,11 @@ final class ITSEC_Lockout {
160
  $user_check = false;
161
  $host_check = false;
162
 
163
- if ( $user !== false && $user !== '' && $user !== null ) {
 
 
 
 
164
 
165
  $user = get_userdata( intval( $user ) );
166
  $user_id = $user->ID;
@@ -280,7 +283,9 @@ final class ITSEC_Lockout {
280
  /**
281
  * This persists a lockout to storage or performs a permanent ban if appropriate.
282
  *
283
- * The user will be immediately locked out by this method if their IP is not whitelisted.
 
 
284
  *
285
  * @since 4.0
286
  *
@@ -289,133 +294,106 @@ final class ITSEC_Lockout {
289
  *
290
  * @return void
291
  */
292
- public function do_lockout( $module, $username = null ) {
293
-
294
- global $wpdb, $itsec_globals;
295
 
296
  if ( ! isset( $this->lockout_modules[$module] ) ) {
297
  return;
298
  }
299
 
 
 
300
  $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
301
 
302
- $lock_host = null;
303
- $lock_user = null;
304
- $lock_username = null;
 
305
  $options = $this->lockout_modules[$module];
306
 
307
- $host = ITSEC_Lib::get_ip();
 
 
 
 
 
308
 
309
  if ( isset( $options['host'] ) && $options['host'] > 0 ) {
 
310
 
311
- $wpdb->insert(
312
- $wpdb->base_prefix . 'itsec_temp',
313
- array(
314
- 'temp_type' => $options['type'],
315
- 'temp_date' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
316
- 'temp_date_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
317
- 'temp_host' => $host,
318
- )
319
- );
320
 
321
  $host_count = $wpdb->get_var(
322
  $wpdb->prepare(
323
- "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_host`= %s;",
324
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * 60 ) ),
325
  $host
326
  )
327
  );
328
 
329
  if ( $host_count >= $options['host'] ) {
330
-
331
  $lock_host = $host;
332
-
333
  }
334
-
335
  }
336
 
337
- if ( $username !== null && isset( $options['user'] ) && $options['user'] > 0 ) {
 
 
338
 
339
- $user_id = username_exists( sanitize_text_field( $username ) );
 
 
 
340
 
341
- if ( $user_id !== false ) {
342
-
343
- $wpdb->insert(
344
- $wpdb->base_prefix . 'itsec_temp',
345
- array(
346
- 'temp_type' => $options['type'],
347
- 'temp_date' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
348
- 'temp_date_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
349
- 'temp_user' => intval( $user_id ),
350
- 'temp_username' => sanitize_text_field( $username ),
351
- )
352
- );
353
 
354
  $user_count = $wpdb->get_var(
355
  $wpdb->prepare(
356
- "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_temp` WHERE `temp_date_gmt` > '%s' AND (`temp_username`= %s OR `temp_user`= %d);",
357
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * 60 ) ),
358
- sanitize_text_field( $username ),
359
- intval( $user_id )
360
  )
361
  );
362
 
363
  if ( $user_count >= $options['user'] ) {
364
-
365
- $lock_user = $user_id;
366
-
367
  }
368
-
369
  } else {
 
 
370
 
371
- $username = sanitize_text_field( $username );
372
-
373
- $wpdb->insert(
374
- $wpdb->base_prefix . 'itsec_temp',
375
- array(
376
- 'temp_type' => $options['type'],
377
- 'temp_date' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
378
- 'temp_date_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
379
- 'temp_username' => $username,
380
- )
381
- );
382
 
383
  $user_count = $wpdb->get_var(
384
  $wpdb->prepare(
385
- "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_username`= %s;",
386
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * 60 ) ),
387
  $username
388
  )
389
  );
390
 
391
  if ( $user_count >= $options['user'] ) {
392
-
393
  $lock_username = $username;
394
-
395
  }
396
-
397
  }
398
-
399
  }
400
 
 
401
  $error = $wpdb->last_error;
402
 
 
403
  if ( strlen( trim( $error ) ) > 0 ) {
404
  ITSEC_Lib::create_database_tables();
405
  }
406
 
407
- if ( ! ITSEC_Lib::is_ip_whitelisted( $host ) && ( $lock_host !== null || $lock_user !== null || $lock_username !== null ) ) {
408
-
409
- $this->lockout( $options['type'], $options['reason'], $lock_host, $lock_user, $lock_username );
410
-
411
- } elseif ( $lock_host !== null || $lock_user !== null ) {
412
-
413
- global $itsec_logger;
414
-
415
- $itsec_logger->log_event( 'lockout', 10, array( __( 'A whitelisted host has triggered a lockout condition but was not locked out.', 'better-wp-security' ) ), sanitize_text_field( $host ) );
416
 
 
 
417
  }
418
-
419
  }
420
 
421
  /**
@@ -737,12 +715,10 @@ final class ITSEC_Lockout {
737
  * @return bool
738
  */
739
  public function is_visitor_temp_whitelisted() {
740
- global $itsec_globals;
741
-
742
  $whitelist = $this->get_temp_whitelist();
743
  $ip = ITSEC_Lib::get_ip();
744
 
745
- if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > $itsec_globals['current_time'] ) {
746
  return true;
747
  }
748
 
@@ -759,213 +735,158 @@ final class ITSEC_Lockout {
759
  *
760
  * @since 4.0
761
  *
762
- * @param string $type The type of lockout (for user reference)
763
- * @param string $reason Reason for lockout, for notifications
764
- * @param string $host Host to lock out
765
- * @param int $user user id to lockout
766
- * @param string $username username to lockout
767
  *
768
  * @return void
769
  */
770
- private function lockout( $type, $reason, $host = null, $user = null, $username = null ) {
 
771
 
772
- global $wpdb, $itsec_logger, $itsec_globals;
773
 
774
- $host_expiration = null;
775
- $user_expiration = null;
776
- $username = sanitize_text_field( trim( $username ) );
777
- $lock = 'lockout_' . $host . $user . $username;
778
 
779
  // Acquire a lock to prevent a lockout being created more than once by a particularly fast attacker.
780
- if ( ITSEC_Lib::get_lock( $lock, 180 ) ) {
781
-
782
- //Do we have a good host to lock out or not
783
- if ( ! is_null( $host ) && ITSEC_Lib::is_ip_whitelisted( sanitize_text_field( $host ) ) === false && ITSEC_Lib_IP_Tools::validate( $host ) ) {
784
- $good_host = sanitize_text_field( $host );
785
- } else {
786
- $good_host = false;
787
- }
788
-
789
- //Do we have a valid user to lockout or not
790
- if ( $user !== null && ITSEC_Lib::user_id_exists( intval( $user ) ) === true ) {
791
- $good_user = intval( $user );
792
- } else {
793
- $good_user = false;
794
- }
795
-
796
- //Do we have a valid username to lockout or not
797
- if ( $username !== null && $username != '' ) {
798
- $good_username = $username;
799
- } else {
800
- $good_username = false;
801
- }
802
-
803
- $blacklist_host = false; //assume we're not permanently blcking the host
804
-
805
- //Sanitize the data for later
806
- $type = sanitize_text_field( $type );
807
- $reason = sanitize_text_field( $reason );
808
-
809
- //handle a permanent host ban (if needed)
810
- if ( ITSEC_Modules::get_setting( 'global', 'blacklist' ) && $good_host !== false ) { //permanent blacklist
811
-
812
- $blacklist_period = ITSEC_Modules::get_setting( 'global', 'blacklist_period', 7 );
813
- $blacklist_seconds = $blacklist_period * DAY_IN_SECONDS;
814
-
815
- $host_count = 1 + $wpdb->get_var(
816
- $wpdb->prepare(
817
- "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_expire_gmt` > %s AND `lockout_host`= %s;",
818
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - $blacklist_seconds ),
819
- $host
820
- )
821
- );
822
-
823
- if ( $host_count >= ITSEC_Modules::get_setting( 'global', 'blacklist_count' ) && ITSEC_Files::can_write_to_files() ) {
824
 
825
- $host_expiration = false;
826
 
827
- $this->blacklist_ip( sanitize_text_field( $host ) );
828
 
829
- $blacklist_host = true; //flag it so we don't do a temp ban as well
 
830
 
831
- }
 
 
 
 
 
 
 
 
832
 
833
- }
834
 
835
- //We have temp bans to perform
836
- if ( $good_host !== false || $good_user !== false || $good_username || $good_username !== false ) {
 
 
 
837
 
838
- if ( ITSEC_Lib::is_ip_whitelisted( sanitize_text_field( $host ) ) ) {
 
 
 
 
 
 
839
 
840
- $whitelisted = true;
841
- $expiration = date( 'Y-m-d H:i:s', 1 );
842
- $expiration_gmt = date( 'Y-m-d H:i:s', 1 );
843
 
 
 
844
  } else {
845
-
846
- $whitelisted = false;
847
- $exp_seconds = ITSEC_Modules::get_setting( 'global', 'lockout_period' ) * MINUTE_IN_SECONDS;
848
- $expiration = date( 'Y-m-d H:i:s', $itsec_globals['current_time'] + $exp_seconds );
849
- $expiration_gmt = date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] + $exp_seconds );
850
-
851
  }
 
 
852
 
853
- if ( $good_host !== false && $blacklist_host === false ) { //temp lockout host
854
-
855
- $host_expiration = $expiration;
856
-
857
- $wpdb->insert(
858
- $wpdb->base_prefix . 'itsec_lockouts',
859
- array(
860
- 'lockout_type' => $type,
861
- 'lockout_start' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
862
- 'lockout_start_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
863
- 'lockout_expire' => $expiration,
864
- 'lockout_expire_gmt' => $expiration_gmt,
865
- 'lockout_host' => sanitize_text_field( $host ),
866
- )
867
- );
868
 
869
- $itsec_logger->log_event( 'lockout', 10, array(
870
- 'expires' => $expiration, 'expires_gmt' => $expiration_gmt, 'type' => $type
871
- ), sanitize_text_field( $host ) );
872
 
873
- }
 
 
 
 
874
 
875
- if ( $good_user !== false ) { //blacklist host and temp lockout user
876
-
877
- $user_expiration = $expiration;
878
-
879
- $wpdb->insert(
880
- $wpdb->base_prefix . 'itsec_lockouts',
881
- array(
882
- 'lockout_type' => $type,
883
- 'lockout_start' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
884
- 'lockout_start_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
885
- 'lockout_expire' => $expiration,
886
- 'lockout_expire_gmt' => $expiration_gmt,
887
- 'lockout_host' => '',
888
- 'lockout_user' => intval( $user ),
889
- )
890
- );
891
-
892
- if ( $whitelisted === false ) {
893
- $itsec_logger->log_event( 'lockout', 10, array(
894
- 'expires' => $expiration, 'expires_gmt' => $expiration_gmt, 'type' => $type
895
- ), '', '', intval( $user ) );
896
- } else {
897
- $itsec_logger->log_event( 'lockout', 10, array(
898
- __( 'White Listed', 'better-wp-security' ), 'type' => $type
899
- ), '', '', intval( $user ) );
900
- }
901
 
902
- }
 
 
903
 
904
- if ( $good_username !== false ) { //blacklist host and temp lockout username
905
-
906
- $user_expiration = $expiration;
907
-
908
- $wpdb->insert(
909
- $wpdb->base_prefix . 'itsec_lockouts',
910
- array(
911
- 'lockout_type' => $type,
912
- 'lockout_start' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
913
- 'lockout_start_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
914
- 'lockout_expire' => $expiration,
915
- 'lockout_expire_gmt' => $expiration_gmt,
916
- 'lockout_host' => '',
917
- 'lockout_username' => $username,
918
- )
919
- );
920
-
921
- if ( $whitelisted === false ) {
922
- $itsec_logger->log_event( 'lockout', 10, array(
923
- 'expires' => $expiration, 'expires_gmt' => $expiration_gmt, 'type' => $type
924
- ), '', '', $username );
925
- } else {
926
- $itsec_logger->log_event( 'lockout', 10, array(
927
- __( 'White Listed', 'better-wp-security' ), 'type' => $type
928
- ), '', '', $username );
929
- }
930
 
931
- }
 
 
 
932
 
933
- if ( $whitelisted === false ) {
 
 
 
934
 
935
- $this->send_lockout_email( $good_host, $good_user, $good_username, $host_expiration, $user_expiration, $reason );
936
 
937
- $lock_context = array(
938
- 'type' => $type,
939
- );
 
940
 
941
- if ( $user ) {
942
- $lock_context['user'] = get_userdata( $user );
943
- } elseif ( $username ) {
944
- $lock_context['username'] = $username;
945
- }
946
 
947
- if ( $good_host !== false ) {
948
 
949
- ITSEC_Lib::release_lock( $lock );
950
- $this->execute_lock( $lock_context );
 
951
 
952
- } else {
 
 
 
 
953
 
954
- $lock_context['user_lock'] = true;
 
 
955
 
956
- ITSEC_Lib::release_lock( $lock );
957
- $this->execute_lock( $lock_context );
 
958
 
959
- }
 
 
 
 
 
 
 
 
 
 
960
 
961
- }
 
962
 
 
 
 
 
 
 
 
 
 
963
  }
964
 
965
- ITSEC_Lib::release_lock( $lock );
966
-
967
  }
968
-
969
  }
970
 
971
  /**
@@ -1012,30 +933,10 @@ final class ITSEC_Lockout {
1012
  * @return void
1013
  */
1014
  public function purge_lockouts() {
1015
-
1016
  global $wpdb;
1017
 
1018
- $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_expire_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( ( ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) + 1 ) * DAY_IN_SECONDS ) ) . "';" );
1019
- $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_temp` WHERE `temp_date_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS ) . "';" );
1020
-
1021
- }
1022
-
1023
- /**
1024
- * Register 404 and file change detection for logger
1025
- *
1026
- * @param array $logger_modules array of logger modules
1027
- *
1028
- * @return array
1029
- */
1030
- public function register_logger( $logger_modules ) {
1031
-
1032
- $logger_modules['lockout'] = array(
1033
- 'type' => 'lockout',
1034
- 'function' => __( 'Host or User Lockout', 'better-wp-security' ),
1035
- );
1036
-
1037
- return $logger_modules;
1038
-
1039
  }
1040
 
1041
  /**
@@ -1255,4 +1156,44 @@ final class ITSEC_Lockout {
1255
 
1256
  }
1257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1258
  }
87
  //Process clear lockout form
88
  add_action( 'itsec_admin_init', array( $this, 'release_lockout' ) );
89
 
 
 
 
90
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
91
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
92
 
95
 
96
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
97
  add_filter( 'itsec_lockout_notification_strings', array( $this, 'notification_strings' ) );
98
+
99
+ add_filter( 'itsec_logs_prepare_lockout_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
100
  }
101
 
102
  public function init_settings_page() {
159
  $user_check = false;
160
  $host_check = false;
161
 
162
+ if ( is_object( $user ) && is_a( $user, 'WP_User' ) ) {
163
+
164
+ $user_id = $user->ID;
165
+
166
+ } else if ( ! empty( $user ) ) {
167
 
168
  $user = get_userdata( intval( $user ) );
169
  $user_id = $user->ID;
283
  /**
284
  * This persists a lockout to storage or performs a permanent ban if appropriate.
285
  *
286
+ * Each module registers lockout settings that determine if a lockout event applies to the hostname, user, or
287
+ * username. In addition, these settings determine how many lockout events of a specific kind trigger an actual
288
+ * lockout by calling $this->lockout().
289
  *
290
  * @since 4.0
291
  *
294
  *
295
  * @return void
296
  */
297
+ public function do_lockout( $module, $username = false ) {
298
+ global $wpdb;
 
299
 
300
  if ( ! isset( $this->lockout_modules[$module] ) ) {
301
  return;
302
  }
303
 
304
+
305
+ // TODO: Ensure that this is not needed and remove it.
306
  $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
307
 
308
+
309
+ $lock_host = false;
310
+ $lock_user_id = false;
311
+ $lock_username = false;
312
  $options = $this->lockout_modules[$module];
313
 
314
+ $lockout_event_data = array(
315
+ 'temp_type' => $options['type'],
316
+ 'temp_date' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
317
+ 'temp_date_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
318
+ );
319
+
320
 
321
  if ( isset( $options['host'] ) && $options['host'] > 0 ) {
322
+ $host = ITSEC_Lib::get_ip();
323
 
324
+ $host_lockout_event_data = $lockout_event_data;
325
+ $host_lockout_event_data['temp_host'] = $host;
326
+
327
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $host_lockout_event_data );
 
 
 
 
 
328
 
329
  $host_count = $wpdb->get_var(
330
  $wpdb->prepare(
331
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_host` = %s",
332
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
333
  $host
334
  )
335
  );
336
 
337
  if ( $host_count >= $options['host'] ) {
 
338
  $lock_host = $host;
 
339
  }
 
340
  }
341
 
342
+ if ( false !== $username && isset( $options['user'] ) && $options['user'] > 0 ) {
343
+ $username = sanitize_text_field( $username );
344
+ $user_id = username_exists( $username );
345
 
346
+ if ( false !== $user_id ) {
347
+ $user_lockout_event_data = $lockout_event_data;
348
+ $user_lockout_event_data['temp_user'] = $user_id;
349
+ $user_lockout_event_data['temp_username'] = $username;
350
 
351
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  $user_count = $wpdb->get_var(
354
  $wpdb->prepare(
355
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND (`temp_username` = %s OR `temp_user` = %d)",
356
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
357
+ $username,
358
+ $user_id
359
  )
360
  );
361
 
362
  if ( $user_count >= $options['user'] ) {
363
+ $lock_user_id = $user_id;
 
 
364
  }
 
365
  } else {
366
+ $user_lockout_event_data = $lockout_event_data;
367
+ $user_lockout_event_data['temp_username'] = $username;
368
 
369
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
 
 
 
 
 
 
 
 
 
 
370
 
371
  $user_count = $wpdb->get_var(
372
  $wpdb->prepare(
373
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_username` = %s",
374
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
375
  $username
376
  )
377
  );
378
 
379
  if ( $user_count >= $options['user'] ) {
 
380
  $lock_username = $username;
 
381
  }
 
382
  }
 
383
  }
384
 
385
+
386
  $error = $wpdb->last_error;
387
 
388
+ // TODO: Confirm that this scenario can never happen and remove this
389
  if ( strlen( trim( $error ) ) > 0 ) {
390
  ITSEC_Lib::create_database_tables();
391
  }
392
 
 
 
 
 
 
 
 
 
 
393
 
394
+ if ( false !== $lock_host || false !== $lock_user_id || false !== $lock_username ) {
395
+ $this->lockout( $module, $lock_host, $lock_user_id, $lock_username );
396
  }
 
397
  }
398
 
399
  /**
715
  * @return bool
716
  */
717
  public function is_visitor_temp_whitelisted() {
 
 
718
  $whitelist = $this->get_temp_whitelist();
719
  $ip = ITSEC_Lib::get_ip();
720
 
721
+ if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > ITSEC_Core::get_current_time() ) {
722
  return true;
723
  }
724
 
735
  *
736
  * @since 4.0
737
  *
738
+ * @param string $module The module triggering the lockout.
739
+ * @param string|bool $host Host to lock out or false if the host should not be locked out.
740
+ * @param int|bool $user_id User ID to lockout or false if the host should not be locked out.
741
+ * @param string|bool $username Username to lockout or false if the host should not be locked out.
 
742
  *
743
  * @return void
744
  */
745
+ private function lockout( $module, $host, $user_id, $username ) {
746
+ global $wpdb;
747
 
 
748
 
749
+ $lock = "lockout_$host$user_id$username";
 
 
 
750
 
751
  // Acquire a lock to prevent a lockout being created more than once by a particularly fast attacker.
752
+ if ( ! ITSEC_Lib::get_lock( $lock, 180 ) ) {
753
+ return;
754
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
 
 
756
 
757
+ $module_details = $this->lockout_modules[$module];
758
 
759
+ $whitelisted = ITSEC_Lib::is_ip_whitelisted( $host );
760
+ $blacklisted = false;
761
 
762
+ $log_data = array(
763
+ 'module' => $module,
764
+ 'host' => $host,
765
+ 'user_id' => $user_id,
766
+ 'username' => $username,
767
+ 'module_details' => $module_details,
768
+ 'whitelisted' => $whitelisted,
769
+ 'blacklisted' => false,
770
+ );
771
 
 
772
 
773
+ // Do a permanent ban if enabled and settings criteria are met.
774
+ if ( ITSEC_Modules::get_setting( 'global', 'blacklist' ) && false !== $host ) {
775
+ $blacklist_count = ITSEC_Modules::get_setting( 'global', 'blacklist_count' );
776
+ $blacklist_period = ITSEC_Modules::get_setting( 'global', 'blacklist_period', 7 );
777
+ $blacklist_seconds = $blacklist_period * DAY_IN_SECONDS;
778
 
779
+ $host_count = 1 + $wpdb->get_var(
780
+ $wpdb->prepare(
781
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` > %s AND `lockout_host`= %s",
782
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - $blacklist_seconds ),
783
+ $host
784
+ )
785
+ );
786
 
787
+ if ( $host_count >= $blacklist_count ) {
788
+ $blacklisted = true;
789
+ $log_data['blacklisted'] = true;
790
 
791
+ if ( $whitelisted ) {
792
+ ITSEC_Log::add_notice( 'lockout', 'whitelisted-host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
793
  } else {
794
+ $this->blacklist_ip( $host );
795
+ ITSEC_Log::add_action( 'lockout', 'host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
 
 
 
 
796
  }
797
+ }
798
+ }
799
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
800
 
801
+ $host_expiration = false;
802
+ $user_expiration = false;
 
803
 
804
+ $lockouts_data = array(
805
+ 'lockout_type' => $module_details['type'],
806
+ 'lockout_start' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
807
+ 'lockout_start_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
808
+ );
809
 
810
+ if ( $whitelisted ) {
811
+ $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', 1 );
812
+ $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', 1 );
813
+ } else {
814
+ $exp_seconds = ITSEC_Modules::get_setting( 'global', 'lockout_period' ) * MINUTE_IN_SECONDS;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
 
816
+ $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() + $exp_seconds );
817
+ $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() + $exp_seconds );
818
+ }
819
 
820
+ if ( false !== $host && ! $blacklisted ) {
821
+ $host_expiration = $lockouts_data['lockout_expire'];
822
+ $this->add_lockout_to_db( 'host', $host, $whitelisted, $lockouts_data, $log_data );
823
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
+ if ( false !== $user_id ) {
826
+ $user_expiration = $lockouts_data['lockout_expire'];
827
+ $this->add_lockout_to_db( 'user', $user_id, $whitelisted, $lockouts_data, $log_data );
828
+ }
829
 
830
+ if ( false !== $username ) {
831
+ $user_expiration = $lockouts_data['lockout_expire'];
832
+ $this->add_lockout_to_db( 'username', $username, $whitelisted, $lockouts_data, $log_data );
833
+ }
834
 
 
835
 
836
+ if ( $whitelisted ) {
837
+ // No need to send an email notice when the host is whitelisted.
838
+ ITSEC_Lib::release_lock( $lock );
839
+ }
840
 
 
 
 
 
 
841
 
842
+ $this->send_lockout_email( $host, $user_id, $username, $host_expiration, $user_expiration, $module_details['reason'] );
843
 
844
+ $lock_context = array(
845
+ 'type' => $module_details['type'],
846
+ );
847
 
848
+ if ( false !== $user_id ) {
849
+ $lock_context['user'] = get_userdata( $user_id );
850
+ } else if ( false !== $username ) {
851
+ $lock_context['username'] = $username;
852
+ }
853
 
854
+ if ( false === $host ) {
855
+ $lock_context['user_lock'] = true;
856
+ }
857
 
858
+ ITSEC_Lib::release_lock( $lock );
859
+ $this->execute_lock( $lock_context );
860
+ }
861
 
862
+ /**
863
+ * Adds a record of a lockout event to the database and log the event.
864
+ *
865
+ * @param string $type The type of lockout: "host", "user", "username".
866
+ * @param string|int $id The value for the type: host's IP, user's ID, username.
867
+ * @param bool $whitelisted Whether or not the host triggering the event is whitelisted.
868
+ * @param array $lockout_data Array of base data to be inserted.
869
+ * @param array $log_data Array of data to be logged for the event.
870
+ */
871
+ private function add_lockout_to_db( $type, $id, $whitelisted, $lockout_data, $log_data ) {
872
+ global $wpdb;
873
 
874
+ $lockout_data["lockout_$type"] = $id;
875
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_lockouts", $lockout_data );
876
 
877
+ if ( $whitelisted ) {
878
+ ITSEC_Log::add_notice( 'lockout', "whitelisted-host-triggered-$type-lockout", array_merge( $log_data, $lockout_data ) );
879
+ } else {
880
+ if ( 'host' === $type ) {
881
+ $code = "host-lockout::{$log_data['host']}";
882
+ } else if ( 'user' === $type ) {
883
+ $code = "user-lockout::{$log_data['user_id']}";
884
+ } else if ( 'username' === $type ) {
885
+ $code = "username-lockout::{$log_data['username']}";
886
  }
887
 
888
+ ITSEC_Log::add_action( 'lockout', $code, array_merge( $log_data, $lockout_data ) );
 
889
  }
 
890
  }
891
 
892
  /**
933
  * @return void
934
  */
935
  public function purge_lockouts() {
 
936
  global $wpdb;
937
 
938
+ $wpdb->query( "DELETE FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( ( ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) + 1 ) * DAY_IN_SECONDS ) ) . "';" );
939
+ $wpdb->query( "DELETE FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS ) . "';" );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
940
  }
941
 
942
  /**
1156
 
1157
  }
1158
 
1159
+ public function filter_entry_for_list_display( $entry, $code, $data ) {
1160
+ $entry['module_display'] = esc_html__( 'Lockout', 'better-wp-security' );
1161
+
1162
+ if ( 'whitelisted-host-triggered-blacklist' === $code ) {
1163
+ $entry['description'] = esc_html__( 'Whitelisted Host Triggered Blacklist', 'better-wp-security' );
1164
+ } else if ( 'host-triggered-blacklist' === $code ) {
1165
+ $entry['description'] = esc_html__( 'Host Triggered Blacklist', 'better-wp-security' );
1166
+ } else if ( 'whitelisted-host-triggered-host-lockout' === $code ) {
1167
+ $entry['description'] = esc_html__( 'Whitelisted Host Triggered Host Lockout', 'better-wp-security' );
1168
+ } else if ( 'host-lockout' === $code ) {
1169
+ if ( isset( $data[0] ) ) {
1170
+ $entry['description'] = sprintf( wp_kses( __( 'Host Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1171
+ } else {
1172
+ $entry['description'] = esc_html__( 'Host Lockout', 'better-wp-security' );
1173
+ }
1174
+ } else if ( 'whitelisted-host-triggered-user-lockout' === $code ) {
1175
+ $entry['description'] = esc_html__( 'Whitelisted Host Triggered User Lockout', 'better-wp-security' );
1176
+ } else if ( 'user-lockout' === $code ) {
1177
+ if ( isset( $data[0] ) ) {
1178
+ $user = get_user_by( 'id', $data[0] );
1179
+ }
1180
+
1181
+ if ( isset( $user ) && false !== $user ) {
1182
+ $entry['description'] = sprintf( wp_kses( __( 'User Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $user->user_login );
1183
+ } else {
1184
+ $entry['description'] = esc_html__( 'User Lockout', 'better-wp-security' );
1185
+ }
1186
+ } else if ( 'whitelisted-host-triggered-username-lockout' === $code ) {
1187
+ $entry['description'] = esc_html__( 'Whitelisted Host Triggered Username Lockout', 'better-wp-security' );
1188
+ } else if ( 'username-lockout' === $code ) {
1189
+ if ( isset( $data[0] ) ) {
1190
+ $entry['description'] = sprintf( wp_kses( __( 'Username Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1191
+ } else {
1192
+ $entry['description'] = esc_html__( 'Username Lockout', 'better-wp-security' );
1193
+ }
1194
+ }
1195
+
1196
+ return $entry;
1197
+ }
1198
+
1199
  }
core/logger-all-logs.php DELETED
@@ -1,260 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Log tables for Authentication Module
5
- *
6
- * @package iThemes-Security
7
- * @subpackage Authentication
8
- * @since 4.0
9
- */
10
- final class ITSEC_Logger_All_Logs extends ITSEC_WP_List_Table {
11
-
12
- function __construct() {
13
-
14
- parent::__construct(
15
- array(
16
- 'singular' => 'itsec_raw_log_item',
17
- 'plural' => 'itsec_raw_log_items',
18
- 'ajax' => true
19
- )
20
- );
21
-
22
- }
23
-
24
- /**
25
- * Define type column
26
- *
27
- * @param array $item array of row data
28
- *
29
- * @return string formatted output
30
- *
31
- **/
32
- function column_time( $item ) {
33
-
34
- return $item['time'];
35
-
36
- }
37
-
38
- /**
39
- * Define function column
40
- *
41
- * @param array $item array of row data
42
- *
43
- * @return string formatted output
44
- *
45
- **/
46
- function column_function( $item ) {
47
-
48
- return $item['function'];
49
-
50
- }
51
-
52
- /**
53
- * Define priority column
54
- *
55
- * @param array $item array of row data
56
- *
57
- * @return string formatted output
58
- *
59
- **/
60
- function column_priority( $item ) {
61
-
62
- return $item['priority'];
63
-
64
- }
65
-
66
- /**
67
- * Define host column
68
- *
69
- * @param array $item array of row data
70
- *
71
- * @return string formatted output
72
- *
73
- **/
74
- function column_host( $item ) {
75
- require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
76
-
77
- $r = array();
78
- if ( ! is_array( $item['host'] ) ) {
79
- $item['host'] = array( $item['host'] );
80
- }
81
- foreach ( $item['host'] as $host ) {
82
- if ( ITSEC_Lib_IP_Tools::validate( $host ) ) {
83
- $r[] = '<a href="' . esc_url( ITSEC_Lib::get_trace_ip_link( $host ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $host ) . '</a>';
84
- }
85
- }
86
- $return = implode( '<br />', $r );
87
-
88
- return $return;
89
-
90
- }
91
-
92
- /**
93
- * Define username column
94
- *
95
- * @param array $item array of row data
96
- *
97
- * @return string formatted output
98
- *
99
- **/
100
- function column_user( $item ) {
101
-
102
- if ( 0 != $item['user_id'] ) {
103
- return '<a href="' . esc_url( admin_url( 'user-edit.php?user_id=' . $item['user_id'] ) ) . '" target="_blank" rel="noopener noreferrer">' . $item['user'] . '</a>';
104
- } else {
105
- return $item['user'];
106
- }
107
-
108
- }
109
-
110
- /**
111
- * Define url column
112
- *
113
- * @param array $item array of row data
114
- *
115
- * @return string formatted output
116
- *
117
- **/
118
- function column_url( $item ) {
119
-
120
- return $item['url'];
121
-
122
- }
123
-
124
- /**
125
- * Define referrer column
126
- *
127
- * @param array $item array of row data
128
- *
129
- * @return string formatted output
130
- *
131
- **/
132
- function column_referrer( $item ) {
133
-
134
- return $item['referrer'];
135
-
136
- }
137
-
138
- /**
139
- * Define data column
140
- *
141
- * @param array $item array of row data
142
- *
143
- * @return string formatted output
144
- *
145
- **/
146
- function column_data( $item ) {
147
-
148
- global $itsec_logger;
149
-
150
- $raw_data = maybe_unserialize( $item['data'] );
151
-
152
- $data = apply_filters( "itsec_logger_filter_{$item['type']}_data_column_details", '', $raw_data );
153
-
154
- if ( empty( $data ) ) {
155
- if ( is_array( $raw_data ) && sizeof( $raw_data ) > 0 ) {
156
-
157
- $data = $itsec_logger->print_array( $raw_data, true );
158
-
159
- } elseif ( ! is_array( $raw_data ) ) {
160
-
161
- $data = sanitize_text_field( $raw_data );
162
-
163
- } else {
164
-
165
- $data = '';
166
-
167
- }
168
- }
169
-
170
- if ( strlen( $data ) > 1 ) {
171
-
172
- $content = '<div class="itsec-all-log-dialog" id="itsec-log-all-row-' . $item['id'] . '" style="display:none;">';
173
- $content .= $data;
174
- $content .= '</div>';
175
-
176
- $content .= '<a href="itsec-log-all-row-' . $item['id'] . '" class="dialog">' . __( 'Details', 'better-wp-security' ) . '</a>';
177
-
178
- return $content;
179
-
180
- } else {
181
-
182
- return '';
183
-
184
- }
185
-
186
- }
187
-
188
- /**
189
- * Define Columns
190
- *
191
- * @return array array of column titles
192
- */
193
- public function get_columns() {
194
-
195
- return array(
196
- 'function' => __( 'Function', 'better-wp-security' ),
197
- 'priority' => __( 'Priority', 'better-wp-security' ),
198
- 'time' => __( 'Time', 'better-wp-security' ),
199
- 'host' => __( 'Host', 'better-wp-security' ),
200
- 'user' => __( 'User', 'better-wp-security' ),
201
- 'url' => __( 'URL', 'better-wp-security' ),
202
- 'referrer' => __( 'Referrer', 'better-wp-security' ),
203
- 'data' => __( 'Data', 'better-wp-security' ),
204
- );
205
-
206
- }
207
-
208
- /**
209
- * Prepare data for table
210
- *
211
- * @return void
212
- */
213
- public function prepare_items() {
214
-
215
- global $itsec_logger, $wpdb;
216
-
217
- $columns = $this->get_columns();
218
- $hidden = array();
219
- $this->_column_headers = array( $columns, $hidden, false );
220
- $per_page = 20; //20 items per page
221
- $current_page = $this->get_pagenum();
222
- $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_log`;" );
223
-
224
- $items = $itsec_logger->get_events( 'all', array(), $per_page, ( ( $current_page - 1 ) * $per_page ), 'log_date' );
225
-
226
- $table_data = array();
227
-
228
- $count = 0;
229
-
230
- foreach ( $items as $item ) { //loop through and group 404s
231
-
232
- $table_data[ $count ]['id'] = $count;
233
- $table_data[ $count ]['type'] = sanitize_text_field( $item['log_type'] );
234
- $table_data[ $count ]['function'] = sanitize_text_field( $item['log_function'] );
235
- $table_data[ $count ]['priority'] = sanitize_text_field( $item['log_priority'] );
236
- $table_data[ $count ]['time'] = sanitize_text_field( $item['log_date'] );
237
- $table_data[ $count ]['host'] = sanitize_text_field( $item['log_host'] );
238
- $table_data[ $count ]['user'] = sanitize_text_field( $item['log_username'] );
239
- $table_data[ $count ]['user_id'] = sanitize_text_field( $item['log_user'] );
240
- $table_data[ $count ]['url'] = sanitize_text_field( $item['log_url'] );
241
- $table_data[ $count ]['referrer'] = sanitize_text_field( $item['log_referrer'] );
242
- $table_data[ $count ]['data'] = $item['log_data'];
243
-
244
- $count ++;
245
-
246
- }
247
-
248
- $this->items = $table_data;
249
-
250
- $this->set_pagination_args(
251
- array(
252
- 'total_items' => $total_items,
253
- 'per_page' => $per_page,
254
- 'total_pages' => ceil( $total_items / $per_page )
255
- )
256
- );
257
-
258
- }
259
-
260
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/logger.php DELETED
@@ -1,554 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Handles the writing, maintenance and display of log files
5
- *
6
- * @package iThemes-Security
7
- * @since 4.0
8
- */
9
- final class ITSEC_Logger {
10
-
11
- private
12
- $log_file,
13
- $logger_displays,
14
- $logger_modules;
15
-
16
- /**
17
- * @access private
18
- *
19
- * @var array Events that need to be logged to a file but couldn't
20
- */
21
- private $events_to_log_to_file = array();
22
-
23
- function __construct() {
24
-
25
- $this->logger_modules = array(); //array to hold information on modules using this feature
26
- $this->logger_displays = array(); //array to hold metabox information
27
-
28
- add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
29
- add_action( 'plugins_loaded', array( $this, 'write_pending_events_to_file' ) );
30
-
31
- //Run database cleanup daily with cron
32
- if ( ! wp_next_scheduled( 'itsec_purge_logs' ) ) {
33
- wp_schedule_event( time(), 'daily', 'itsec_purge_logs' );
34
- }
35
-
36
- add_action( 'itsec_purge_logs', array( $this, 'purge_logs' ) );
37
-
38
- }
39
-
40
- /**
41
- * Gets events from the logs for a specified module
42
- *
43
- * @param string $module module or type of events to fetch
44
- * @param array $params array of extra query parameters
45
- * @param int $limit the maximum number of rows to retrieve
46
- * @param int $offset the offset of the data
47
- * @param string $order order by column
48
- * @param bool $direction false for descending or true for ascending
49
- *
50
- * @return bool|mixed false on error, null if no events or array of events
51
- */
52
- public function get_events( $module, $params = array(), $limit = null, $offset = null, $order = null, $direction = false ) {
53
-
54
- global $wpdb;
55
-
56
- if ( isset( $module ) !== true || strlen( $module ) < 1 ) {
57
- return false;
58
- }
59
-
60
- if ( sizeof( $params ) > 0 || $module != 'all' ) {
61
- $where = " WHERE ";
62
- } else {
63
- $where = '';
64
- }
65
-
66
- $param_search = '';
67
-
68
- if ( $module == 'all' ) {
69
-
70
- $module_sql = '';
71
- $and = '';
72
-
73
- } else {
74
-
75
- $module_sql = "`log_type` = '" . esc_sql( $module ) . "'";
76
- $and = ' AND ';
77
-
78
- }
79
-
80
- if ( $direction === false ) {
81
-
82
- $order_direction = ' DESC';
83
-
84
- } else {
85
-
86
- $order_direction = ' ASC';
87
-
88
- }
89
-
90
- if ( $order !== null ) {
91
-
92
- $order_statement = ' ORDER BY `' . esc_sql( $order ) . '`';
93
-
94
- } else {
95
-
96
- $order_statement = ' ORDER BY `log_id`';
97
-
98
- }
99
-
100
- if ( $limit !== null ) {
101
-
102
- if ( $offset !== null ) {
103
-
104
- $result_limit = ' LIMIT ' . absint( $offset ) . ', ' . absint( $limit );
105
-
106
- } else {
107
-
108
- $result_limit = ' LIMIT ' . absint( $limit );
109
-
110
- }
111
-
112
- } else {
113
-
114
- $result_limit = '';
115
-
116
- }
117
-
118
- if ( sizeof( $params ) > 0 ) {
119
-
120
- foreach ( $params as $field => $value ) {
121
-
122
- if ( gettype( $value ) != 'integer' ) {
123
- $param_search .= $and . "`" . esc_sql( $field ) . "`='" . esc_sql( $value ) . "'";
124
- } else {
125
- $param_search .= $and . "`" . esc_sql( $field ) . "`=" . esc_sql( $value ) . "";
126
- }
127
-
128
- }
129
-
130
- }
131
-
132
- $items = $wpdb->get_results( "SELECT * FROM `" . $wpdb->base_prefix . "itsec_log`" . $where . $module_sql . $param_search . $order_statement . $order_direction . $result_limit . ";", ARRAY_A );
133
-
134
- return $items;
135
-
136
- }
137
-
138
- /**
139
- * Logs events sent by other modules or systems
140
- *
141
- * @param string $module the module requesting the log entry
142
- * @param int $priority the priority of the log entry (1-10)
143
- * @param array $data extra data to log (non-indexed data would be good here)
144
- * @param string $host the remote host triggering the event
145
- * @param string $username the username triggering the event
146
- * @param string $user the user id triggering the event
147
- * @param string $url the url triggering the event
148
- * @param string $referrer the referrer to the url (if applicable)
149
- *
150
- * @return void
151
- */
152
- public function log_event( $module, $priority = 5, $data = array(), $host = '', $username = '', $user = '', $url = '', $referrer = '' ) {
153
-
154
- if ( isset( $this->logger_modules[ $module ] ) ) {
155
- $type = ITSEC_Modules::get_setting( 'global', 'log_type' );
156
-
157
- if ( 'database' === $type || 'both' === $type ) {
158
- $this->log_event_to_db( $module, $priority, $data, $host, $username, $user, $url, $referrer );
159
- }
160
-
161
- if ( 'file' === $type || 'both' === $type ) {
162
- $this->log_event_to_file( $module, $priority, $data, $host, $username, $user, $url, $referrer );
163
- }
164
-
165
- }
166
-
167
- do_action( 'itsec_log_event', $module, $priority, $data, $host, $username, $user, $url, $referrer );
168
-
169
- }
170
-
171
- private function log_event_to_db( $module, $priority, $data, $host, $username, $user, $url, $referrer ) {
172
- global $wpdb, $itsec_globals;
173
-
174
- $options = $this->logger_modules[ $module ];
175
-
176
- $values = array(
177
- 'log_type' => $options['type'],
178
- 'log_priority' => intval( $priority ),
179
- 'log_function' => $options['function'],
180
- 'log_date' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
181
- 'log_date_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
182
- 'log_host' => sanitize_text_field( $host ),
183
- 'log_username' => sanitize_text_field( $username ),
184
- 'log_user' => intval( $user ),
185
- 'log_url' => $url,
186
- 'log_referrer' => $referrer,
187
- 'log_data' => serialize( $data ),
188
- );
189
-
190
- $columns = '`' . implode( '`, `', array_keys( $values ) ) . '`';
191
- $placeholders = '%s, %d, %s, %s, %s, %s, %s, %s, %s, %s, %s';
192
-
193
- $query_format = "INSERT INTO `{$wpdb->base_prefix}itsec_log` ($columns) VALUES ($placeholders)";
194
-
195
- $cached_show_errors_setting = $wpdb->hide_errors();
196
- $result = $wpdb->query( $wpdb->prepare( $query_format, $values ) );
197
-
198
- if ( ! $result ) {
199
- $wpdb->show_errors();
200
-
201
- ITSEC_Lib::create_database_tables();
202
-
203
- // Attempt the query again. Since errors will now be shown, a remaining issue will be display an error.
204
- $result = $wpdb->query( $wpdb->prepare( $query_format, $values ) );
205
- }
206
-
207
- // Set $wpdb->show_errors back to its original setting.
208
- $wpdb->show_errors( $cached_show_errors_setting );
209
- }
210
-
211
- private function log_event_to_file( $module, $priority = 5, $data = array(), $host = '', $username = '', $user = '', $url = '', $referrer = '' ) {
212
- global $itsec_globals;
213
-
214
- // If the file can't be prepared, store the events up to write later (at plugins_loaded)
215
- if ( false === $this->prepare_log_file() ) {
216
- $this->events_to_log_to_file[] = compact( 'module', 'priority', 'data', 'host', 'username', 'user', 'url', 'referrer' );
217
- return;
218
- }
219
-
220
- $options = $this->logger_modules[ $module ];
221
-
222
- $file_data = $this->sanitize_array( $data, true );
223
-
224
- $message =
225
- $options['type'] . ',' .
226
- intval( $priority ) . ',' .
227
- $options['function'] . ',' .
228
- date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ) . ',' .
229
- date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . ',' .
230
- sanitize_text_field( $host ) . ',' .
231
- sanitize_text_field( $username ) . ',' .
232
- ( intval( $user ) === 0 ? '' : intval( $user ) ) . ',' .
233
- esc_sql( $url ) . ',' .
234
- esc_sql( $referrer ) . ',' .
235
- maybe_serialize( $file_data );
236
-
237
- $this->add_to_log_file( $message );
238
-
239
- }
240
-
241
- public function write_pending_events_to_file() {
242
- foreach ( $this->events_to_log_to_file as $event ) {
243
- call_user_func_array( array( $this, 'log_event_to_file' ), $event );
244
- }
245
- }
246
-
247
- /**
248
- * Displays into box for logs page
249
- *
250
- * @since 4.0
251
- *
252
- * @return void
253
- */
254
- public function metabox_all_logs() {
255
-
256
- $log_filter = isset( $_GET['itsec_log_filter'] ) ? sanitize_text_field( $_GET['itsec_log_filter'] ) : 'all-log-data';
257
- $callback = null;
258
-
259
- echo '<p>' . __( 'To adjust logging options visit the global settings page.', 'better-wp-security' ) . '</p>';
260
-
261
- echo '<label for="itsec_log_filter"><strong>' . __( 'Select Filter: ', 'better-wp-security' ) . '</strong></label>';
262
- echo '<select id="itsec_log_filter" name="itsec_log_filter">';
263
- echo '<option value="all-log-data" ' . selected( $log_filter, 'all-log-data' ) . '>' . __( 'All Log Data', 'better-wp-security' ) . '</option>';
264
-
265
- if ( sizeof( $this->logger_displays ) > 0 ) {
266
-
267
- foreach ( $this->logger_displays as $display ) {
268
-
269
- if ( $display['module'] === $log_filter ) {
270
- $callback = $display['callback'];
271
- }
272
-
273
- echo '<option value="' . $display['module'] . '" ' . selected( $log_filter, $display['module'] ) . '>' . $display['title'] . '</option>';
274
-
275
- }
276
-
277
- }
278
-
279
- echo '</select>';
280
-
281
- if ( $log_filter === 'all-log-data' || $callback === null ) {
282
-
283
- $this->all_logs_content();
284
-
285
- } else {
286
-
287
- call_user_func_array( $callback, array() );
288
-
289
- }
290
-
291
- }
292
-
293
- /**
294
- * A better print array function to display array data in the logs
295
- *
296
- * @since 4.2
297
- *
298
- * @param array $array_items array to print or return
299
- * @param bool $return true to return the data false to echo it
300
- */
301
- public function print_array( $array_items, $return = true ) {
302
-
303
- $items = '';
304
-
305
- //make sure we're working with an array
306
- if ( ! is_array( $array_items ) ) {
307
- return false;
308
- }
309
-
310
- if ( sizeof( $array_items ) > 0 ) {
311
-
312
- $items .= '<ul>';
313
-
314
- foreach ( $array_items as $key => $item ) {
315
-
316
- if ( is_array( $item ) ) {
317
-
318
- $items .= '<li>';
319
-
320
- if ( ! is_numeric( $key ) ) {
321
- $items .= '<h3>' . esc_html( $key ) . '</h3>';
322
- }
323
-
324
- $items .= $this->print_array( $item, true ) . PHP_EOL;
325
-
326
- $items .= '</li>';
327
-
328
- } else {
329
-
330
- if ( strlen( trim( $item ) ) > 0 ) {
331
- $items .= '<li><h3>' . esc_html( $key ) . ' = ' . esc_html( $item ) . '</h3></li>' . PHP_EOL;
332
- }
333
-
334
- }
335
-
336
- }
337
-
338
- $items .= '</ul>';
339
-
340
- }
341
-
342
- return $items;
343
-
344
- }
345
-
346
- /**
347
- * Purges database logs and rotates file logs (when needed)
348
- *
349
- * @return void
350
- */
351
- public function purge_logs( $purge_all = false ) {
352
-
353
- global $wpdb, $itsec_globals, $itsec_clear_all_logs;
354
-
355
- if ( true === $purge_all ) {
356
-
357
- if ( ! wp_verify_nonce( $_POST['wp_nonce'], 'itsec_clear_logs' ) ) {
358
- return;
359
- }
360
-
361
- $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log`;" );
362
-
363
- } else {
364
-
365
- //Clean up the database log first
366
- $type = ITSEC_Modules::get_setting( 'global', 'log_type' );
367
-
368
- if ( 'database' === $type || 'both' === $type ) {
369
-
370
- $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log` WHERE `log_date_gmt` < '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] - ( ITSEC_Modules::get_setting( 'global', 'log_rotation' ) * DAY_IN_SECONDS ) ) . "';" );
371
-
372
- } else {
373
-
374
- $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log`;" );
375
-
376
- }
377
-
378
- $this->rotate_log();
379
-
380
- }
381
-
382
- }
383
-
384
- /**
385
- * Register modules that will use the logger service
386
- *
387
- * @return void
388
- */
389
- public function register_modules() {
390
-
391
- $this->logger_modules = apply_filters( 'itsec_logger_modules', $this->logger_modules );
392
- $this->logger_displays = apply_filters( 'itsec_logger_displays', $this->logger_displays );
393
-
394
- }
395
-
396
- /**
397
- * Rotates the event-log.log file when called
398
- *
399
- * Adapted from http://www.phpclasses.org/browse/file/49471.html
400
- *
401
- * @return void
402
- */
403
- private function rotate_log() {
404
- $log_file = $this->get_log_file();
405
- $max_size = 1024 * 1024 * 10; // 10MiB
406
-
407
- if ( ! @file_exists( $log_file ) || @filesize( $log_file ) < $max_size ) {
408
- return;
409
- }
410
-
411
- // rotate
412
- $path_info = pathinfo( $log_file );
413
- $base_directory = $path_info['dirname'];
414
- $base_name = $path_info['basename'];
415
- $num_map = array();
416
-
417
- foreach ( new DirectoryIterator( $base_directory ) as $fInfo ) {
418
-
419
- if ( $fInfo->isDot() || ! $fInfo->isFile() ) {
420
- continue;
421
- }
422
-
423
- if ( preg_match( '/^' . $base_name . '\.?([0-9]*)$/', $fInfo->getFilename(), $matches ) ) {
424
-
425
- $num = $matches[1];
426
- $old_file = $fInfo->getFilename();
427
-
428
- if ( $num == '' ) {
429
- $num = - 1;
430
- }
431
-
432
- $num_map[ $num ] = $old_file;
433
-
434
- }
435
-
436
- }
437
-
438
- krsort( $num_map );
439
-
440
- foreach ( $num_map as $num => $old_file ) {
441
-
442
- $new_file = $num + 1;
443
- @rename( $base_directory . DIRECTORY_SEPARATOR . $old_file, $log_file . '.' . $new_file );
444
-
445
- }
446
-
447
- $this->prepare_log_file();
448
-
449
- }
450
-
451
- /**
452
- * Sanitizes strings in a given array recursively
453
- *
454
- * @param array $array array to sanitize
455
- * @param bool $to_string true if output should be string or false for array output
456
- *
457
- * @return mixed sanitized array or string
458
- */
459
- private function sanitize_array( $array, $to_string = false ) {
460
-
461
- $sanitized_array = array();
462
- $string = '';
463
-
464
- //Loop to sanitize each piece of data
465
- foreach ( $array as $key => $value ) {
466
-
467
- if ( is_array( $value ) ) {
468
-
469
- if ( $to_string === false ) {
470
- $sanitized_array[ esc_sql( $key ) ] = $this->sanitize_array( $value );
471
- } else {
472
- $string .= esc_sql( $key ) . '=' . $this->sanitize_array( $value, true );
473
- }
474
-
475
- } else {
476
-
477
- $sanitized_array[ esc_sql( $key ) ] = esc_sql( $value );
478
-
479
- $string .= esc_sql( $key ) . '=' . esc_sql( $value );
480
-
481
- }
482
-
483
- }
484
-
485
- if ( $to_string === false ) {
486
- return $sanitized_array;
487
- } else {
488
- return $string;
489
- }
490
-
491
- }
492
-
493
- private function get_log_file() {
494
- if ( isset( $this->log_file ) ) {
495
- return $this->log_file;
496
- }
497
-
498
- $log_location = ITSEC_Modules::get_setting( 'global', 'log_location' );
499
- $log_info = ITSEC_Modules::get_setting( 'global', 'log_info' );
500
-
501
- if ( empty( $log_info ) ) {
502
- // We need wp_generate_password() to create a cryptographically secure file name
503
- if ( ! function_exists( 'wp_generate_password' ) ) {
504
- $this->log_file = false;
505
- return false;
506
- }
507
-
508
- $log_info = substr( sanitize_title( get_bloginfo( 'name' ) ), 0, 20 ) . '-' . wp_generate_password( 30, false );
509
-
510
- ITSEC_Modules::set_setting( 'global', 'log_info', $log_info );
511
- }
512
-
513
- $this->log_file = "$log_location/event-log-$log_info.log";
514
-
515
- return $this->log_file;
516
- }
517
-
518
- /**
519
- * Creates a new log file and adds header information (if needed)
520
- *
521
- * @return void
522
- */
523
- private function prepare_log_file() {
524
- $log_file = $this->get_log_file();
525
-
526
- // We can't prepare a file if we can't get the file name
527
- if ( false === $log_file ) {
528
- return false;
529
- }
530
-
531
- if ( ! file_exists( $log_file ) ) { //only if current log file doesn't exist
532
-
533
- $header = 'log_type,log_priority,log_function,log_date,log_date_gmt,log_host,log_username,log_user,log_url,log_referrer,log_data';
534
-
535
- $this->add_to_log_file( $header );
536
-
537
- }
538
-
539
- return true;
540
-
541
- }
542
-
543
- private function add_to_log_file( $details ) {
544
- $log_file = $this->get_log_file();
545
-
546
- if ( false === $log_file ) {
547
- return false;
548
- }
549
-
550
- @error_log( $details . PHP_EOL, 3, $log_file );
551
-
552
- return true;
553
- }
554
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/class-itsec-four-oh-four-log.php DELETED
@@ -1,233 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Log 404 errors for Intrusion Detection Module
5
- *
6
- * @package iThemes-Security
7
- * @subpackage Intrusion-Detection
8
- * @since 4.0
9
- */
10
- final class ITSEC_Four_Oh_Four_Log extends ITSEC_WP_List_Table {
11
-
12
- function __construct() {
13
-
14
- parent::__construct(
15
- array(
16
- 'singular' => 'itsec_four_oh_four_log_item',
17
- 'plural' => 'itsec_four_oh_four_log_items',
18
- 'ajax' => true
19
- )
20
- );
21
-
22
- }
23
-
24
- /**
25
- * Define first time column
26
- *
27
- * @param array $item array of row data
28
- *
29
- * @return string formatted output
30
- *
31
- **/
32
- function column_first_time( $item ) {
33
-
34
- return $item['first_time'];
35
-
36
- }
37
-
38
- /**
39
- * Define time column
40
- *
41
- * @param array $item array of row data
42
- *
43
- * @return string formatted output
44
- *
45
- **/
46
- function column_last_time( $item ) {
47
-
48
- return $item['last_time'];
49
-
50
- }
51
-
52
- /**
53
- * Define count column
54
- *
55
- * @param array $item array of row data
56
- *
57
- * @return string formatted output
58
- *
59
- **/
60
- function column_count( $item ) {
61
-
62
- return $item['count'];
63
-
64
- }
65
-
66
- /**
67
- * Define uri column
68
- *
69
- * @param array $item array of row data
70
- *
71
- * @return string formatted output
72
- *
73
- **/
74
- function column_uri( $item ) {
75
-
76
- global $itsec_logger;
77
-
78
- $items = $itsec_logger->get_events( 'four_oh_four', array( 'log_url' => $item['uri'] ) );
79
-
80
- echo '<a href="itsec_404_details_' . $item['id'] . '" class="dialog">' . $item['uri'] . '</a>';
81
-
82
- echo '<div id="itsec_404_details_' . $item['id'] . '" style="display:none;">';
83
-
84
- echo '<h3>' . __( 'Details for ' . $item['uri'], 'better-wp-security' ) . '</h3>';
85
-
86
- echo '<ol class="file_change_detail_list">';
87
-
88
- foreach ( $items as $item => $details ) {
89
- $data = maybe_unserialize( $details['log_data'] );
90
- echo '<li class="404_detail"><strong>' . __( 'Time', 'better-wp-security' ) . '</strong>: ' . $details['log_date'] . '<br /><strong>' . __( 'Host', 'better-wp-security' ) . '</strong>: ' . $details['log_host'] . '<br /><strong>' . __( 'Referrer', 'better-wp-security' ) . '</strong>: ' . esc_html( $details['log_referrer'] ) . '<br /><strong>' . __( 'Query', 'better-wp-security' ) . '</strong>: ' . esc_html( $data['query_string'] ) . '</li>';
91
- }
92
-
93
- echo '</ol>';
94
-
95
- echo '</div>';
96
-
97
- }
98
-
99
- /**
100
- * Define Columns
101
- *
102
- * @return array array of column titles
103
- */
104
- public function get_columns() {
105
-
106
- return array(
107
- 'uri' => __( 'Location', 'better-wp-security' ),
108
- 'count' => __( 'Count', 'better-wp-security' ),
109
- 'first_time' => __( 'First Recorded', 'better-wp-security' ),
110
- 'last_time' => __( 'Last Recorded', 'better-wp-security' ),
111
- );
112
-
113
- }
114
-
115
- /**
116
- * Define Sortable Columns
117
- *
118
- * @return array of column titles that can be sorted
119
- */
120
- public function get_sortable_columns() {
121
-
122
- $order = ( empty( $_GET['order'] ) ) ? false : true;
123
-
124
- $sortable_columns = array(
125
- 'uri' => array( 'uri', $order ),
126
- 'count' => array( 'count', $order ),
127
- 'first_time' => array( 'first_time', $order ),
128
- 'last_time' => array( 'last_time', $order ),
129
- );
130
-
131
- return $sortable_columns;
132
-
133
- }
134
-
135
- /**
136
- * Prepare data for table
137
- *
138
- * @return void
139
- */
140
- public function prepare_items() {
141
-
142
- global $itsec_logger;
143
-
144
- $columns = $this->get_columns();
145
- $hidden = array();
146
- $sortable = $this->get_sortable_columns();
147
- $this->_column_headers = array( $columns, $hidden, $sortable );
148
-
149
- $items = $itsec_logger->get_events( 'four_oh_four' );
150
-
151
- $table_data = array();
152
-
153
- foreach ( $items as $item ) { //loop through and group 404s
154
-
155
- if ( isset( $table_data[ $item['log_url'] ] ) ) {
156
-
157
- $table_data[ $item['log_url'] ]['id'] = $item['log_id'];
158
- $table_data[ $item['log_url'] ]['count'] = $table_data[ $item['log_url'] ]['count'] + 1;
159
- $table_data[ $item['log_url'] ]['last_time'] = strtotime( $table_data[ $item['log_url'] ]['last_time'] ) > strtotime( $item['log_date'] ) ? $table_data[ $item['log_url'] ]['last_time'] : sanitize_text_field( $item['log_date'] );
160
- $table_data[ $item['log_url'] ]['first_time'] = strtotime( $table_data[ $item['log_url'] ]['first_time'] ) < strtotime( $item['log_date'] ) ? $table_data[ $item['log_url'] ]['first_time'] : sanitize_text_field( $item['log_date'] );
161
- $table_data[ $item['log_url'] ]['uri'] = sanitize_text_field( $item['log_url'] );
162
-
163
- } else {
164
-
165
- $table_data[ $item['log_url'] ]['id'] = $item['log_id'];
166
- $table_data[ $item['log_url'] ]['count'] = 1;
167
- $table_data[ $item['log_url'] ]['last_time'] = sanitize_text_field( $item['log_date'] );
168
- $table_data[ $item['log_url'] ]['first_time'] = sanitize_text_field( $item['log_date'] );
169
- $table_data[ $item['log_url'] ]['uri'] = sanitize_text_field( $item['log_url'] );
170
-
171
- }
172
-
173
- }
174
-
175
- usort( $table_data, array( $this, 'sortrows' ) );
176
-
177
- $per_page = 20; //20 items per page
178
- $current_page = $this->get_pagenum();
179
- $total_items = count( $table_data );
180
-
181
- $table_data = array_slice( $table_data, ( ( $current_page - 1 ) * $per_page ), $per_page );
182
-
183
- $this->items = $table_data;
184
-
185
- $this->set_pagination_args(
186
- array(
187
- 'total_items' => $total_items,
188
- 'per_page' => $per_page,
189
- 'total_pages' => ceil( $total_items / $per_page )
190
- )
191
- );
192
-
193
- }
194
-
195
- /**
196
- * Sorts rows by count in descending order
197
- *
198
- * @param array $a first array to compare
199
- * @param array $b second array to compare
200
- *
201
- * @return int comparison result
202
- */
203
- function sortrows( $a, $b ) {
204
-
205
- // If no sort, default to count
206
- $orderby = ( ! empty( $_GET['orderby'] ) ) ? esc_attr( $_GET['orderby'] ) : 'last_time';
207
-
208
- // If no order, default to desc
209
- $order = ( ! empty( $_GET['order'] ) ) ? esc_attr( $_GET['order'] ) : 'desc';
210
-
211
- if ( $orderby == 'count' ) {
212
-
213
- if ( intval( $a[ $orderby ] ) < intval( $b[ $orderby ] ) ) {
214
- $result = - 1;
215
- } elseif ( intval( $a[ $orderby ] ) === intval( $b[ $orderby ] ) ) {
216
- $result = 0;
217
- } else {
218
- $result = 1;
219
- }
220
-
221
- } else {
222
-
223
- // Determine sort order
224
- $result = strcmp( $a[ $orderby ], $b[ $orderby ] );
225
-
226
- }
227
-
228
- // Send final sort direction to usort
229
- return ( $order === 'asc' ) ? $result : - $result;
230
-
231
- }
232
-
233
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/class-itsec-four-oh-four.php CHANGED
@@ -9,8 +9,6 @@ class ITSEC_Four_Oh_Four {
9
  $this->settings = ITSEC_Modules::get_settings( '404-detection' );
10
 
11
  add_filter( 'itsec_lockout_modules', array( $this, 'register_lockout' ) );
12
- add_filter( 'itsec_logger_modules', array( $this, 'register_logger' ) );
13
- add_filter( 'itsec_logger_displays', array( $this, 'register_logger_displays' ) );
14
 
15
  add_action( 'wp', array( $this, 'check_404' ), 9999 );
16
  }
@@ -23,7 +21,7 @@ class ITSEC_Four_Oh_Four {
23
  public function check_404() {
24
 
25
  /** @var ITSEC_Lockout $itsec_lockout */
26
- global $itsec_logger, $itsec_lockout;
27
 
28
  if ( ! is_404() ) {
29
  return;
@@ -31,27 +29,14 @@ class ITSEC_Four_Oh_Four {
31
 
32
  $uri = explode( '?', $_SERVER['REQUEST_URI'] );
33
 
34
- if ( ! is_array( $this->settings['white_list'] ) || in_array( $uri[0], $this->settings['white_list'] ) ) {
35
- // Invalid settings or white listed page.
36
  return;
37
  }
38
 
39
- $itsec_logger->log_event(
40
- 'four_oh_four',
41
- 3,
42
- array(
43
- 'query_string' => isset( $uri[1] ) ? esc_sql( $uri[1] ) : '',
44
- ),
45
- ITSEC_Lib::get_ip(),
46
- '',
47
- '',
48
- esc_sql( $uri[0] ),
49
- isset( $_SERVER['HTTP_REFERER'] ) ? esc_sql( $_SERVER['HTTP_REFERER'] ) : ''
50
- );
51
-
52
- $path_info = pathinfo( $uri[0] );
53
 
54
- if ( ! isset( $path_info['extension'] ) || ( is_array( $this->settings['types'] ) && ! in_array( '.' . $path_info['extension'], $this->settings['types'] ) ) ) {
55
 
56
  $itsec_lockout->do_lockout( 'four_oh_four' );
57
 
@@ -79,61 +64,4 @@ class ITSEC_Four_Oh_Four {
79
 
80
  }
81
 
82
- /**
83
- * Register 404 and file change detection for logger
84
- *
85
- * @param array $logger_modules array of logger modules
86
- *
87
- * @return array array of logger modules
88
- */
89
- public function register_logger( $logger_modules ) {
90
-
91
- $logger_modules['four_oh_four'] = array(
92
- 'type' => 'four_oh_four',
93
- 'function' => __( '404 Error', 'better-wp-security' ),
94
- );
95
-
96
- return $logger_modules;
97
-
98
- }
99
-
100
- /**
101
- * Array of displays for the logs screen
102
- *
103
- * @since 4.0
104
- *
105
- * @param array $logger_displays metabox array
106
- *
107
- * @return array metabox array
108
- */
109
- public function register_logger_displays( $logger_displays ) {
110
-
111
- $logger_displays[] = array(
112
- 'module' => 'four_oh_four',
113
- 'title' => __( '404 Errors Found', 'better-wp-security' ),
114
- 'callback' => array( $this, 'logs_metabox_content' )
115
- );
116
-
117
- return $logger_displays;
118
-
119
- }
120
-
121
- /**
122
- * Render the settings metabox
123
- *
124
- * @return void
125
- */
126
- public function logs_metabox_content() {
127
-
128
- if ( ! class_exists( 'ITSEC_Four_Oh_Four_Log' ) ) {
129
- require( dirname( __FILE__ ) . '/class-itsec-four-oh-four-log.php' );
130
- }
131
-
132
- $log_display = new ITSEC_Four_Oh_Four_Log();
133
-
134
- $log_display->prepare_items();
135
- $log_display->display();
136
-
137
- }
138
-
139
  }
9
  $this->settings = ITSEC_Modules::get_settings( '404-detection' );
10
 
11
  add_filter( 'itsec_lockout_modules', array( $this, 'register_lockout' ) );
 
 
12
 
13
  add_action( 'wp', array( $this, 'check_404' ), 9999 );
14
  }
21
  public function check_404() {
22
 
23
  /** @var ITSEC_Lockout $itsec_lockout */
24
+ global $itsec_lockout;
25
 
26
  if ( ! is_404() ) {
27
  return;
29
 
30
  $uri = explode( '?', $_SERVER['REQUEST_URI'] );
31
 
32
+ if ( in_array( '/' . ITSEC_Lib::get_request_path(), $this->settings['white_list'] ) ) {
33
+ // white listed page.
34
  return;
35
  }
36
 
37
+ ITSEC_Log::add_notice( 'four_oh_four', 'found_404', array( 'SERVER' => $_SERVER ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ if ( ! in_array( '.' . pathinfo( $uri[0], PATHINFO_EXTENSION ), $this->settings['types'] ) ) {
40
 
41
  $itsec_lockout->do_lockout( 'four_oh_four' );
42
 
64
 
65
  }
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
core/modules/404-detection/logs.php ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Four_Oh_Four_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_four_oh_four_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ add_filter( 'itsec_logs_prepare_four_oh_four_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ }
8
+
9
+ public function filter_entry_for_list_display( $entry ) {
10
+ $entry['module_display'] = esc_html__( '404 Detection', 'better-wp-security' );
11
+
12
+ if ( 'found_404' === $entry['code'] ) {
13
+ $entry['description'] = $entry['url'];
14
+ }
15
+
16
+ return $entry;
17
+ }
18
+
19
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
20
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
21
+
22
+ $details['module']['content'] = $entry['module_display'];
23
+ $details['description']['content'] = $entry['description'];
24
+
25
+ return $details;
26
+ }
27
+ }
28
+ new ITSEC_Four_Oh_Four_Logs();
core/modules/away-mode/class-itsec-away-mode.php CHANGED
@@ -5,7 +5,6 @@ final class ITSEC_Away_Mode {
5
  public function run() {
6
 
7
  //Execute away mode functions on admin init
8
- add_filter( 'itsec_logger_modules', array( $this, 'register_logger' ) );
9
  add_action( 'itsec_admin_init', array( $this, 'run_active_check' ) );
10
  add_action( 'login_init', array( $this, 'run_active_check' ) );
11
 
@@ -78,49 +77,15 @@ final class ITSEC_Away_Mode {
78
  * @return void
79
  */
80
  public function run_active_check() {
 
81
 
82
- global $itsec_logger;
83
-
84
- //execute lockout if applicable
85
- if ( self::is_active() ) {
86
-
87
- $itsec_logger->log_event(
88
- 'away_mode',
89
- 5,
90
- array(
91
- __( 'A host was prevented from accessing the dashboard due to away-mode restrictions being in effect', 'better-wp-security' ),
92
- ),
93
- ITSEC_Lib::get_ip(),
94
- '',
95
- '',
96
- '',
97
- ''
98
- );
99
 
100
  wp_redirect( get_option( 'siteurl' ) );
101
  wp_clear_auth_cookie();
102
  die();
103
-
104
  }
105
-
106
- }
107
-
108
- /**
109
- * Register 404 and file change detection for logger
110
- *
111
- * @param array $logger_modules array of logger modules
112
- *
113
- * @return array array of logger modules
114
- */
115
- public function register_logger( $logger_modules ) {
116
-
117
- $logger_modules['away_mode'] = array(
118
- 'type' => 'away_mode',
119
- 'function' => __( 'Away Mode Triggered', 'better-wp-security' ),
120
- );
121
-
122
- return $logger_modules;
123
-
124
  }
125
 
126
  /**
5
  public function run() {
6
 
7
  //Execute away mode functions on admin init
 
8
  add_action( 'itsec_admin_init', array( $this, 'run_active_check' ) );
9
  add_action( 'login_init', array( $this, 'run_active_check' ) );
10
 
77
  * @return void
78
  */
79
  public function run_active_check() {
80
+ $away_mode_details = self::is_active( true );
81
 
82
+ if ( $away_mode_details['active'] ) {
83
+ ITSEC_Log::add_notice( 'away_mode', 'away-mode-active', array( 'login_details' => ITSEC_Lib::get_login_details(), 'away_mode_details' => $away_mode_details ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  wp_redirect( get_option( 'siteurl' ) );
86
  wp_clear_auth_cookie();
87
  die();
 
88
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
 
91
  /**
core/modules/away-mode/logs.php ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Away_Mode_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_away_mode_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ }
7
+
8
+ public function filter_entry_for_list_display( $entry ) {
9
+ $entry['module_display'] = esc_html__( 'Away Mode', 'better-wp-security' );
10
+
11
+ if ( 'away-mode-active' === $entry['code'] ) {
12
+ $entry['description'] = esc_html__( 'Access Blocked', 'better-wp-security' );
13
+ }
14
+
15
+ return $entry;
16
+ }
17
+ }
18
+ new ITSEC_Away_Mode_Logs();
core/modules/backup/class-itsec-backup.php CHANGED
@@ -35,7 +35,6 @@ class ITSEC_Backup {
35
  $this->settings = ITSEC_Modules::get_settings( 'backup' );
36
 
37
  add_action( 'itsec_execute_backup_cron', array( $this, 'do_backup' ) );
38
- add_filter( 'itsec_logger_modules', array( $this, 'register_logger' ) );
39
 
40
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
41
  add_filter( 'itsec_backup_notification_strings', array( $this, 'notification_strings' ) );
@@ -109,7 +108,7 @@ class ITSEC_Backup {
109
  * @return void
110
  */
111
  private function execute_backup( $one_time = false ) {
112
- global $wpdb, $itsec_logger;
113
 
114
 
115
 
@@ -205,6 +204,8 @@ class ITSEC_Backup {
205
  @fwrite( $fh, PHP_EOL . PHP_EOL );
206
  @fclose( $fh );
207
 
 
 
208
  if ( $this->settings['zip'] ) {
209
  if ( ! class_exists( 'PclZip' ) ) {
210
  require( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
@@ -246,27 +247,27 @@ class ITSEC_Backup {
246
  }
247
 
248
 
249
- $status = __( 'Success', 'better-wp-security' );
250
- $details = __( 'saved locally', 'better-wp-security' );
 
 
 
251
 
252
  if ( 0 === $this->settings['method'] ) {
253
  if ( false === $mail_success ) {
254
- $status = __( 'Error', 'better-wp-security' );
255
- $details = __( 'saved locally but email to backup recipients could not be sent.', 'better-wp-security' );
256
  } else {
257
- $details = __( 'emailed to backup recipients and saved locally', 'better-wp-security' );
258
  }
259
  } else if ( 1 === $this->settings['method'] ) {
260
  if ( false === $mail_success ) {
261
- $status = __( 'Error', 'better-wp-security' );
262
- $details = __( 'email to backup recipients could not be sent.', 'better-wp-security' );
263
  } else {
264
- $details = __( 'emailed to backup recipients', 'better-wp-security' );
265
  }
 
 
266
  }
267
-
268
- $data = compact( 'status', 'details' );
269
- $itsec_logger->log_event( 'backup', 3, array( $data ) );
270
  }
271
 
272
  private function send_mail( $file ) {
@@ -298,28 +299,6 @@ class ITSEC_Backup {
298
  return $nc->send( 'backup', $mail );
299
  }
300
 
301
- /**
302
- * Register backups for logger.
303
- *
304
- * Adds the backup module to ITSEC_Logger.
305
- *
306
- * @since 4.0.0
307
- *
308
- * @param array $logger_modules array of logger modules
309
- *
310
- * @return array array of logger modules
311
- */
312
- public function register_logger( $logger_modules ) {
313
-
314
- $logger_modules['backup'] = array(
315
- 'type' => 'backup',
316
- 'function' => __( 'Database Backup Executed', 'better-wp-security' ),
317
- );
318
-
319
- return $logger_modules;
320
-
321
- }
322
-
323
  /**
324
  * Register the events.
325
  *
35
  $this->settings = ITSEC_Modules::get_settings( 'backup' );
36
 
37
  add_action( 'itsec_execute_backup_cron', array( $this, 'do_backup' ) );
 
38
 
39
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
40
  add_filter( 'itsec_backup_notification_strings', array( $this, 'notification_strings' ) );
108
  * @return void
109
  */
110
  private function execute_backup( $one_time = false ) {
111
+ global $wpdb;
112
 
113
 
114
 
204
  @fwrite( $fh, PHP_EOL . PHP_EOL );
205
  @fclose( $fh );
206
 
207
+ $backup_file = $file;
208
+
209
  if ( $this->settings['zip'] ) {
210
  if ( ! class_exists( 'PclZip' ) ) {
211
  require( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
247
  }
248
 
249
 
250
+ $log_data = array(
251
+ 'settings' => $this->settings,
252
+ 'mail_success' => $mail_success,
253
+ 'file' => $backup_file,
254
+ );
255
 
256
  if ( 0 === $this->settings['method'] ) {
257
  if ( false === $mail_success ) {
258
+ ITSEC_Log::add_warning( 'backup', 'email-failed-file-stored', $log_data );
 
259
  } else {
260
+ ITSEC_Log::add_notice( 'backup', 'email-succeeded-file-stored', $log_data );
261
  }
262
  } else if ( 1 === $this->settings['method'] ) {
263
  if ( false === $mail_success ) {
264
+ ITSEC_Log::add_error( 'backup', 'email-failed', $log_data );
 
265
  } else {
266
+ ITSEC_Log::add_notice( 'backup', 'email-succeeded', $log_data );
267
  }
268
+ } else {
269
+ ITSEC_Log::add_notice( 'backup', 'file-stored', $log_data );
270
  }
 
 
 
271
  }
272
 
273
  private function send_mail( $file ) {
299
  return $nc->send( 'backup', $mail );
300
  }
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  /**
303
  * Register the events.
304
  *
core/modules/backup/js/settings-page.js CHANGED
@@ -1,53 +1,53 @@
1
  jQuery( document ).ready( function () {
2
  var $container = jQuery( '#wpcontent' );
3
-
4
-
5
  $container.on( 'click', '#itsec-backup-reset_backup_location', function( e ) {
6
  e.preventDefault();
7
-
8
  jQuery( '#itsec-backup-location' ).val( itsec_backup.default_backup_location );
9
  } );
10
-
11
  $container.on( 'change', '#itsec-backup-method', function( e ) {
12
  var method = jQuery(this).val();
13
-
14
  if ( 1 == method ) {
15
  jQuery( '.itsec-backup-method-file-content' ).hide();
16
  } else {
17
  jQuery( '.itsec-backup-method-file-content' ).show();
18
  }
19
  } );
20
-
21
  jQuery( '#itsec-backup-method' ).trigger( 'change' );
22
-
23
-
24
  jQuery( '#itsec-backup-exclude' ).multiSelect( {
25
  selectableHeader: '<div class="custom-header">' + itsec_backup.available_tables_label + '</div>',
26
  selectionHeader: '<div class="custom-header">' + itsec_backup.excluded_tables_label + '</div>',
27
  keepOrder: true
28
  } );
29
-
30
-
31
  jQuery( '#itsec-backup-create_backup' ).click(function( e ) {
32
  e.preventDefault();
33
-
34
  var originalButtonLabel = jQuery( '#itsec-backup-create_backup' ).attr( 'value' );
35
-
36
  jQuery( '#itsec-backup-create_backup' )
37
  .removeClass( 'button-primary' )
38
  .addClass( 'button-secondary' )
39
  .attr( 'value', itsec_backup.creating_backup_text )
40
  .prop( 'disabled', true );
41
-
42
  jQuery( '#itsec_backup_status' ).html( '' );
43
-
44
  var data = {
45
  'method': 'create-backup'
46
  };
47
-
48
- itsecSettingsPage.sendModuleAJAXRequest( 'backup', data, function( results ) {
49
  jQuery( '#itsec_backup_status' ).html( results.response );
50
-
51
  jQuery( '#itsec-backup-create_backup' )
52
  .removeClass( 'button-secondary' )
53
  .addClass( 'button-primary' )
1
  jQuery( document ).ready( function () {
2
  var $container = jQuery( '#wpcontent' );
3
+
4
+
5
  $container.on( 'click', '#itsec-backup-reset_backup_location', function( e ) {
6
  e.preventDefault();
7
+
8
  jQuery( '#itsec-backup-location' ).val( itsec_backup.default_backup_location );
9
  } );
10
+
11
  $container.on( 'change', '#itsec-backup-method', function( e ) {
12
  var method = jQuery(this).val();
13
+
14
  if ( 1 == method ) {
15
  jQuery( '.itsec-backup-method-file-content' ).hide();
16
  } else {
17
  jQuery( '.itsec-backup-method-file-content' ).show();
18
  }
19
  } );
20
+
21
  jQuery( '#itsec-backup-method' ).trigger( 'change' );
22
+
23
+
24
  jQuery( '#itsec-backup-exclude' ).multiSelect( {
25
  selectableHeader: '<div class="custom-header">' + itsec_backup.available_tables_label + '</div>',
26
  selectionHeader: '<div class="custom-header">' + itsec_backup.excluded_tables_label + '</div>',
27
  keepOrder: true
28
  } );
29
+
30
+
31
  jQuery( '#itsec-backup-create_backup' ).click(function( e ) {
32
  e.preventDefault();
33
+
34
  var originalButtonLabel = jQuery( '#itsec-backup-create_backup' ).attr( 'value' );
35
+
36
  jQuery( '#itsec-backup-create_backup' )
37
  .removeClass( 'button-primary' )
38
  .addClass( 'button-secondary' )
39
  .attr( 'value', itsec_backup.creating_backup_text )
40
  .prop( 'disabled', true );
41
+
42
  jQuery( '#itsec_backup_status' ).html( '' );
43
+
44
  var data = {
45
  'method': 'create-backup'
46
  };
47
+
48
+ itsecUtil.sendModuleAJAXRequest( 'backup', data, function( results ) {
49
  jQuery( '#itsec_backup_status' ).html( results.response );
50
+
51
  jQuery( '#itsec-backup-create_backup' )
52
  .removeClass( 'button-secondary' )
53
  .addClass( 'button-primary' )
core/modules/backup/logs.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Backup_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_backup_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ add_filter( 'itsec_logs_prepare_backup_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ }
8
+
9
+ public function filter_entry_for_list_display( $entry ) {
10
+ $entry['module_display'] = esc_html__( 'Database Backups', 'better-wp-security' );
11
+
12
+ if ( 'email-failed-file-stored' === $entry['code'] ) {
13
+ $entry['description'] = esc_html__( 'File Created but Email Send Failed', 'better-wp-security' );
14
+ } else if ( 'email-succeeded-file-stored' === $entry['code'] ) {
15
+ $entry['description'] = esc_html__( 'File Created and Emails Sent', 'better-wp-security' );
16
+ } else if ( 'email-failed' === $entry['code'] ) {
17
+ $entry['description'] = esc_html__( 'Email Send Failed', 'better-wp-security' );
18
+ } else if ( 'email-succeeded' === $entry['code'] ) {
19
+ $entry['description'] = esc_html__( 'Email Send Succeeded', 'better-wp-security' );
20
+ } else if ( 'file-stored' === $entry['code'] ) {
21
+ $entry['description'] = esc_html__( 'File Created', 'better-wp-security' );
22
+ } else if ( 'details' === $entry['code'] ) {
23
+ $entry['description'] = esc_html__( 'Details', 'better-wp-security' );
24
+ }
25
+
26
+ return $entry;
27
+ }
28
+
29
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
30
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
31
+
32
+ $details['module']['content'] = $entry['module_display'];
33
+ $details['description']['content'] = $entry['description'];
34
+
35
+ return $details;
36
+ }
37
+ }
38
+ new ITSEC_Backup_Logs();
core/modules/brute-force/class-itsec-brute-force-log.php DELETED
@@ -1,186 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Log tables for Authentication Module
5
- *
6
- * @package iThemes-Security
7
- * @subpackage Authentication
8
- * @since 4.0
9
- */
10
- final class ITSEC_Brute_Force_Log extends ITSEC_WP_List_Table {
11
-
12
- function __construct() {
13
-
14
- parent::__construct(
15
- array(
16
- 'singular' => 'itsec_brute_force_log_item',
17
- 'plural' => 'itsec_brute_force_log_items',
18
- 'ajax' => true
19
- )
20
- );
21
-
22
- }
23
-
24
- /**
25
- * Define time column
26
- *
27
- * @param array $item array of row data
28
- *
29
- * @return string formatted output
30
- *
31
- **/
32
- function column_time( $item ) {
33
-
34
- return $item['time'];
35
-
36
- }
37
-
38
- /**
39
- * Define host column
40
- *
41
- * @param array $item array of row data
42
- *
43
- * @return string formatted output
44
- *
45
- **/
46
- function column_host( $item ) {
47
- require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
48
-
49
- $r = array();
50
- if ( ! is_array( $item['host'] ) ) {
51
- $item['host'] = array( $item['host'] );
52
- }
53
- foreach ( $item['host'] as $host ) {
54
- if ( ITSEC_Lib_IP_Tools::validate( $host ) ) {
55
- $r[] = '<a href="' . esc_url( ITSEC_Lib::get_trace_ip_link( $host ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $host ) . '</a>';
56
- }
57
- }
58
- $return = implode( '<br />', $r );
59
-
60
- return $return;
61
-
62
- }
63
-
64
- /**
65
- * Define added column
66
- *
67
- * @param array $item array of row data
68
- *
69
- * @return string formatted output
70
- *
71
- **/
72
- function column_user( $item ) {
73
-
74
- return $item['user'];
75
-
76
- }
77
-
78
- /**
79
- * Define Columns
80
- *
81
- * @return array array of column titles
82
- */
83
- public function get_columns() {
84
-
85
- return array(
86
- 'time' => __( 'Time', 'better-wp-security' ),
87
- 'host' => __( 'Host', 'better-wp-security' ),
88
- 'user' => __( 'Username', 'better-wp-security' ),
89
- );
90
-
91
- }
92
-
93
- /**
94
- * Define Sortable Columns
95
- *
96
- * @return array of column titles that can be sorted
97
- */
98
- public function get_sortable_columns() {
99
-
100
- $order = ( empty( $_GET['order'] ) ) ? false : true;
101
-
102
- $sortable_columns = array(
103
- 'time' => array( 'time', $order ),
104
- 'host' => array( 'host', $order ),
105
- 'user' => array( 'user', $order ),
106
- );
107
-
108
- return $sortable_columns;
109
-
110
- }
111
-
112
- /**
113
- * Prepare data for table
114
- *
115
- * @return void
116
- */
117
- public function prepare_items() {
118
-
119
- global $itsec_logger;
120
-
121
- $columns = $this->get_columns();
122
- $hidden = array();
123
- $sortable = $this->get_sortable_columns();
124
- $this->_column_headers = array( $columns, $hidden, $sortable );
125
-
126
- $items = $itsec_logger->get_events( 'brute_force' );
127
-
128
- $table_data = array();
129
-
130
- $count = 0;
131
-
132
- foreach ( $items as $item ) { //loop through and group 404s
133
-
134
- $table_data[$count]['time'] = sanitize_text_field( $item['log_date'] );
135
- $table_data[$count]['host'] = sanitize_text_field( $item['log_host'] );
136
- $table_data[$count]['user'] = sanitize_text_field( $item['log_username'] );
137
-
138
- $count ++;
139
-
140
- }
141
-
142
- usort( $table_data, array( $this, 'sortrows' ) );
143
-
144
- $per_page = 20; //20 items per page
145
- $current_page = $this->get_pagenum();
146
- $total_items = count( $table_data );
147
-
148
- $table_data = array_slice( $table_data, ( ( $current_page - 1 ) * $per_page ), $per_page );
149
-
150
- $this->items = $table_data;
151
-
152
- $this->set_pagination_args(
153
- array(
154
- 'total_items' => $total_items,
155
- 'per_page' => $per_page,
156
- 'total_pages' => ceil( $total_items / $per_page )
157
- )
158
- );
159
-
160
- }
161
-
162
- /**
163
- * Sorts rows by count in descending order
164
- *
165
- * @param array $a first array to compare
166
- * @param array $b second array to compare
167
- *
168
- * @return int comparison result
169
- */
170
- function sortrows( $a, $b ) {
171
-
172
- // If no sort, default to count
173
- $orderby = ( ! empty( $_GET['orderby'] ) ) ? esc_attr( $_GET['orderby'] ) : 'time';
174
-
175
- // If no order, default to desc
176
- $order = ( ! empty( $_GET['order'] ) ) ? esc_attr( $_GET['order'] ) : 'desc';
177
-
178
- // Determine sort order
179
- $result = strcmp( $a[$orderby], $b[$orderby] );
180
-
181
- // Send final sort direction to usort
182
- return ( $order === 'asc' ) ? $result : - $result;
183
-
184
- }
185
-
186
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/brute-force/class-itsec-brute-force.php CHANGED
@@ -10,20 +10,14 @@ class ITSEC_Brute_Force {
10
 
11
  $this->settings = ITSEC_Modules::get_settings( 'brute-force' );
12
 
13
- add_action( 'wp_login', array( $this, 'wp_login' ), 10, 2 );
14
- add_action( 'itsec-handle-failed-login', array( $this, 'handle_failed_login' ), 10, 2 );
15
-
16
- add_filter( 'itsec_logger_displays', array( $this, 'itsec_logger_displays' ) ); //adds logs metaboxes
17
-
18
- add_filter( 'authenticate', array( $this, 'authenticate' ), 10, 3 );
19
  add_filter( 'itsec_lockout_modules', array( $this, 'itsec_lockout_modules' ) );
20
- add_filter( 'itsec_logger_modules', array( $this, 'itsec_logger_modules' ) );
21
  add_filter( 'jetpack_get_default_modules', array( $this, 'jetpack_get_default_modules' ) ); //disable jetpack protect via Geoge Stephanis
22
 
23
  }
24
 
25
  /**
26
- * Sends to lockout class when login form isn't completely filled out and process xml_rpc username
27
  *
28
  * @since 4.0
29
  *
@@ -34,39 +28,46 @@ class ITSEC_Brute_Force {
34
  * @return user object or WordPress error
35
  */
36
  public function authenticate( $user, $username = '', $password = '' ) {
37
-
38
  /** @var ITSEC_Lockout $itsec_lockout */
39
- /** @var ITSEC_Logger $itsec_logger */
40
- global $itsec_lockout, $itsec_logger;
41
-
42
- //Look for the "admin" user name and ban it if it is set to auto-ban
43
- if ( isset( $this->settings['auto_ban_admin'] ) && $this->settings['auto_ban_admin'] === true && 'admin' === $username ) {
44
-
45
- $itsec_logger->log_event( 'brute_force', 5, array(), ITSEC_Lib::get_ip(), $username );
46
-
47
- $itsec_lockout->do_lockout( 'brute_force_admin_user', $username );
48
 
 
 
 
49
  }
50
 
51
- //Execute brute force if username or password are empty
52
- if ( isset( $_POST['wp-submit'] ) && ( empty( $username ) || empty( $password ) ) ) {
53
-
54
- $user_id = username_exists( $username );
55
 
56
- if ( $user_id === false || $user_id === null ) {
 
57
 
58
- $itsec_lockout->check_lockout( false, $username );
 
59
 
 
60
  } else {
 
61
 
62
- $itsec_lockout->check_lockout( $user_id );
 
 
 
63
 
64
- }
 
 
 
 
 
65
 
66
- $itsec_logger->log_event( 'brute_force', 5, array(), ITSEC_Lib::get_ip(), $username, intval( $user_id ) );
67
-
68
- $itsec_lockout->do_lockout( 'brute_force', $username );
69
 
 
 
 
 
 
70
  }
71
 
72
  return $user;
@@ -104,26 +105,6 @@ class ITSEC_Brute_Force {
104
 
105
  }
106
 
107
- /**
108
- * Register Brute Force for logger
109
- *
110
- * @since 4.0
111
- *
112
- * @param array $logger_modules array of logger modules
113
- *
114
- * @return array array of logger modules
115
- */
116
- public function itsec_logger_modules( $logger_modules ) {
117
-
118
- $logger_modules['brute_force'] = array(
119
- 'type' => 'brute_force',
120
- 'function' => __( 'Invalid Login Attempt', 'better-wp-security' ),
121
- );
122
-
123
- return $logger_modules;
124
-
125
- }
126
-
127
  /**
128
  * Disables the jetpack protect module
129
  *
@@ -141,106 +122,4 @@ class ITSEC_Brute_Force {
141
 
142
  }
143
 
144
- /**
145
- * Make sure user isn't already locked out even on successful form submission
146
- *
147
- * @since 4.0
148
- *
149
- * @param string $username the username attempted
150
- * @param object wp_user the user
151
- *
152
- * @return void
153
- */
154
- public function wp_login( $username, $user = null ) {
155
-
156
- /** @var ITSEC_Lockout $itsec_lockout */
157
- global $itsec_lockout;
158
-
159
- if ( ! $user === null ) {
160
-
161
- $itsec_lockout->check_lockout( $user );
162
-
163
- } elseif ( is_user_logged_in() ) {
164
-
165
- $current_user = wp_get_current_user();
166
-
167
- $itsec_lockout->check_lockout( $current_user->ID );
168
-
169
- }
170
-
171
- }
172
-
173
- /**
174
- * Sends to lockout class when username and password are filled out and wrong
175
- *
176
- * @since 4.0
177
- *
178
- * @param string $username the username attempted
179
- *
180
- * @return void
181
- */
182
- public function handle_failed_login( $username, $details ) {
183
-
184
- /** @var ITSEC_Lockout $itsec_lockout */
185
- global $itsec_lockout, $itsec_logger;
186
-
187
- $user_id = username_exists( $username );
188
-
189
- if ( 'admin' === $username && $this->settings['auto_ban_admin'] && empty( $user_id ) ) {
190
- $itsec_logger->log_event( 'brute_force', 5, $details, ITSEC_Lib::get_ip(), $username );
191
- $itsec_lockout->do_lockout( 'brute_force_admin_user', $username );
192
-
193
- return;
194
- }
195
-
196
- if ( empty( $user_id ) ) {
197
- $itsec_lockout->check_lockout( false, $username, 'brute_force' );
198
- } else {
199
- $itsec_lockout->check_lockout( $user_id, false, 'brute_force' );
200
- }
201
-
202
- $itsec_logger->log_event( 'brute_force', 5, $details, ITSEC_Lib::get_ip(), $username, intval( $user_id ) );
203
- $itsec_lockout->do_lockout( 'brute_force', $username );
204
- }
205
-
206
- /**
207
- * Array of metaboxes for the logs screen
208
- *
209
- * @since 4.0
210
- *
211
- * @param object $displays metabox array
212
- *
213
- * @return array metabox array
214
- */
215
- public function itsec_logger_displays( $displays ) {
216
-
217
- $displays[] = array(
218
- 'module' => 'brute_force',
219
- 'title' => __( 'Invalid Login Attempts', 'better-wp-security' ),
220
- 'callback' => array( $this, 'logs_metabox_content' ),
221
- );
222
-
223
- return $displays;
224
-
225
- }
226
-
227
- /**
228
- * Render the settings metabox
229
- *
230
- * @since 4.0
231
- *
232
- * @return void
233
- */
234
- public function logs_metabox_content() {
235
-
236
- if ( ! class_exists( 'ITSEC_Brute_Force_Log' ) ) {
237
- require( dirname( __FILE__ ) . '/class-itsec-brute-force-log.php' );
238
- }
239
-
240
- $log_display = new ITSEC_Brute_Force_Log();
241
- $log_display->prepare_items();
242
- $log_display->display();
243
-
244
- }
245
-
246
  }
10
 
11
  $this->settings = ITSEC_Modules::get_settings( 'brute-force' );
12
 
13
+ add_filter( 'authenticate', array( $this, 'authenticate' ), 10000, 3 ); // Set a very late priority so that we run after actual authentication takes place.
 
 
 
 
 
14
  add_filter( 'itsec_lockout_modules', array( $this, 'itsec_lockout_modules' ) );
 
15
  add_filter( 'jetpack_get_default_modules', array( $this, 'jetpack_get_default_modules' ) ); //disable jetpack protect via Geoge Stephanis
16
 
17
  }
18
 
19
  /**
20
+ * Handle brute force lockout conditions when the site is handling authentication.
21
  *
22
  * @since 4.0
23
  *
28
  * @return user object or WordPress error
29
  */
30
  public function authenticate( $user, $username = '', $password = '' ) {
 
31
  /** @var ITSEC_Lockout $itsec_lockout */
32
+ global $itsec_lockout;
 
 
 
 
 
 
 
 
33
 
34
+ if ( is_wp_error( $user ) && $user->get_error_codes() == array( 'empty_username', 'empty_password' ) ) {
35
+ // This is not an authentication attempt. It is simply the login page loading.
36
+ return $user;
37
  }
38
 
39
+ if ( is_wp_error( $user ) || null == $user ) {
40
+ // Failed authentication.
 
 
41
 
42
+ $details = ITSEC_Lib::get_login_details();
43
+ $SERVER = $_SERVER;
44
 
45
+ if ( 'admin' === $username && $this->settings['auto_ban_admin'] ) {
46
+ ITSEC_Log::add_notice( 'brute_force', 'auto-ban-admin-username', compact( 'details', 'user', 'username', 'SERVER' ) );
47
 
48
+ $itsec_lockout->do_lockout( 'brute_force_admin_user', $username );
49
  } else {
50
+ $user_id = false;
51
 
52
+ if ( empty( $username ) ) {
53
+ $itsec_lockout->check_lockout( false, false, 'brute_force_empty_username' );
54
+ } else {
55
+ $user_id = username_exists( $username );
56
 
57
+ if ( empty( $user_id ) ) {
58
+ $itsec_lockout->check_lockout( false, $username, 'brute_force_invalid_username' );
59
+ } else {
60
+ $itsec_lockout->check_lockout( $user_id, false, 'brute_force_invalid_password' );
61
+ }
62
+ }
63
 
64
+ ITSEC_Log::add_notice( 'brute_force', 'invalid-login', compact( 'details', 'user', 'username', 'user_id', 'SERVER' ) );
 
 
65
 
66
+ $itsec_lockout->do_lockout( 'brute_force', $username );
67
+ }
68
+ } else {
69
+ // Successful authentication. Check to ensure that they are not locked out.
70
+ $itsec_lockout->check_lockout( $user, false, 'brute_force_host_lockout' );
71
  }
72
 
73
  return $user;
105
 
106
  }
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  /**
109
  * Disables the jetpack protect module
110
  *
122
 
123
  }
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
core/modules/brute-force/logs.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Brute_Force_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_brute_force_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ add_filter( 'itsec_logs_prepare_brute_force_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ }
8
+
9
+ public function filter_entry_for_list_display( $entry ) {
10
+ $entry['module_display'] = esc_html__( 'Brute Force', 'better-wp-security' );
11
+
12
+ if ( 'invalid-login' === $entry['code'] ) {
13
+ $entry['description'] = esc_html__( 'Invalid Login', 'better-wp-security' );
14
+ } else if ( 'auto-ban-admin-username' === $entry['code'] ) {
15
+ $entry['description'] = esc_html__( 'Banned Use of "admin" Username', 'better-wp-security' );
16
+ }
17
+
18
+ return $entry;
19
+ }
20
+
21
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
22
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
23
+
24
+ $details['module']['content'] = $entry['module_display'];
25
+ $details['description']['content'] = $entry['description'];
26
+
27
+ if ( isset( $entry['data']['details'] ) ) {
28
+ if ( 'xmlrpc' === $entry['data']['details']['source'] ) {
29
+ $source = esc_html__( 'XMLRPC Authentication', 'better-wp-security' );
30
+ } else if ( 'rest_api' === $entry['data']['details']['source'] ) {
31
+ $source = esc_html__( 'REST API Authentication', 'better-wp-security' );
32
+ }
33
+ }
34
+
35
+ if ( ! isset( $source ) ) {
36
+ $source = esc_html__( 'Login Page', 'better-wp-security' );
37
+ }
38
+
39
+ $details['source'] = array(
40
+ 'header' => esc_html__( 'Login Source' ),
41
+ 'content' => $source,
42
+ );
43
+
44
+ return $details;
45
+ }
46
+ }
47
+ new ITSEC_Brute_Force_Logs();
core/modules/file-change/class-itsec-file-change-log.php DELETED
@@ -1,255 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Display File Change Log for Intrusion Detection Module
5
- *
6
- * @package iThemes-Security
7
- * @subpackage Intrusion-Detection
8
- * @since 4.0
9
- */
10
- final class ITSEC_File_Change_Log extends ITSEC_WP_List_Table {
11
-
12
- function __construct() {
13
-
14
- parent::__construct(
15
- array(
16
- 'singular' => 'itsec_file_change_log_item',
17
- 'plural' => 'itsec_file_change_log_items',
18
- 'ajax' => true
19
- )
20
- );
21
-
22
- }
23
-
24
- /**
25
- * Define time column
26
- *
27
- * @param array $item array of row data
28
- *
29
- * @return string formatted output
30
- *
31
- **/
32
- function column_time( $item ) {
33
-
34
- return $item['time'];
35
-
36
- }
37
-
38
- /**
39
- * Define added column
40
- *
41
- * @param array $item array of row data
42
- *
43
- * @return string formatted output
44
- *
45
- **/
46
- function column_added( $item ) {
47
-
48
- return $item['added'];
49
-
50
- }
51
-
52
- /**
53
- * Define removed column
54
- *
55
- * @param array $item array of row data
56
- *
57
- * @return string formatted output
58
- *
59
- **/
60
- function column_removed( $item ) {
61
-
62
- return $item['removed'];
63
-
64
- }
65
-
66
- /**
67
- * Define changed column
68
- *
69
- * @param array $item array of row data
70
- *
71
- * @return string formatted output
72
- *
73
- **/
74
- function column_changed( $item ) {
75
-
76
- return $item['changed'];
77
-
78
- }
79
-
80
- /**
81
- * Define memory used column
82
- *
83
- * @param array $item array of row data
84
- *
85
- * @return string formatted output
86
- *
87
- **/
88
- function column_memory( $item ) {
89
-
90
- return $item['memory'] . __( 'MB', 'better-wp-security' );
91
-
92
- }
93
-
94
- /**
95
- * Define detail column
96
- *
97
- * @param array $item array of row data
98
- *
99
- * @return string formatted output
100
- *
101
- **/
102
- function column_detail( $item ) {
103
-
104
- if ( $item['added'] > 0 || $item['removed'] > 0 || $item['changed'] > 0 ) {
105
-
106
- echo '<a href="itsec-log-file-change-row-' . $item['detail'] . '" class="dialog">' . __( 'Details', 'better-wp-security' ) . '</a>';
107
-
108
- echo '<div id="itsec-log-file-change-row-' . $item['detail'] . '" style="display:none;">';
109
-
110
- echo '<h3>' . __( 'Files Added', 'better-wp-security' ) . '</h3>';
111
-
112
- echo '<ol class="file_change_detail_list">';
113
-
114
- if ( sizeof( $item['added_detail'] ) > 0 ) {
115
-
116
- foreach ( $item['added_detail'] as $file => $details ) {
117
- echo '<li class="file_change_detail"><strong>' . __( 'File', 'better-wp-security' ) . '</strong>: ' . esc_html( $file ) . '<br /><strong>' . __( 'Date', 'better-wp-security' ) . '</strong>: ' . date( 'l F jS, Y \a\t g:i a e', ( isset( $details['mod_date'] ) ? $details['mod_date'] : $details['d'] ) ) . '</li>';
118
- }
119
-
120
- } else {
121
-
122
- echo '<li class="file_change_detail">' . __( 'There are no added files to report', 'better-wp-security' ) . '</li>';
123
-
124
- }
125
-
126
- echo '</ol>';
127
-
128
- echo '<h3>' . __( 'Files Removed', 'better-wp-security' ) . '</h3>';
129
-
130
- echo '<ol class="file_change_detail_list">';
131
-
132
- if ( sizeof( $item['removed_detail'] ) > 0 ) {
133
-
134
- foreach ( $item['removed_detail'] as $file => $details ) {
135
- echo '<li class="file_change_detail"><strong>' . __( 'File', 'better-wp-security' ) . '</strong>:' . esc_html( $file ) . '<br /><strong>' . __( 'Date', 'better-wp-security' ) . '</strong>: ' . date( 'l F jS, Y \a\t g:i a e', ( isset( $details['mod_date'] ) ? $details['mod_date'] : $details['d'] ) ) . '</li>';
136
- }
137
-
138
- } else {
139
-
140
- echo '<li class="file_change_detail">' . __( 'There are no deleted files to report', 'better-wp-security' ) . '</li>';
141
-
142
- }
143
-
144
- echo '</ol>';
145
-
146
- echo '<h3>' . __( 'Files Changed', 'better-wp-security' ) . '</h3>';
147
-
148
- echo '<ol class="file_change_detail_list">';
149
-
150
- if ( sizeof( $item['changed_detail'] ) > 0 ) {
151
-
152
- foreach ( $item['changed_detail'] as $file => $details ) {
153
- echo '<li class="file_change_detail"><strong>' . __( 'File', 'better-wp-security' ) . '</strong>: ' . esc_html( $file ) . '<br /><strong>' . __( 'Date', 'better-wp-security' ) . '</strong>: ' . date( 'l F jS, Y \a\t g:i a e', ( isset( $details['mod_date'] ) ? $details['mod_date'] : $details['d'] ) ) . '</li>';
154
- }
155
-
156
- } else {
157
-
158
- echo '<li class="file_change_detail">' . __( 'There are no changed files to report', 'better-wp-security' ) . '</li>';
159
-
160
- }
161
-
162
- echo '</ol>';
163
- echo '</div>';
164
-
165
- }
166
-
167
- }
168
-
169
- /**
170
- * Define Columns
171
- *
172
- * @return array array of column titles
173
- */
174
- public function get_columns() {
175
-
176
- return array(
177
- 'time' => __( 'Check Time', 'better-wp-security' ),
178
- 'added' => __( 'Files Added', 'better-wp-security' ),
179
- 'removed' => __( 'Files Deleted', 'better-wp-security' ),
180
- 'changed' => __( 'Files Changed', 'better-wp-security' ),
181
- 'memory' => __( 'Memory Used', 'better-wp-security' ),
182
- 'detail' => __( 'Details', 'better-wp-security' ),
183
- );
184
-
185
- }
186
-
187
- /**
188
- * Prepare data for table
189
- *
190
- * @return void
191
- */
192
- public function prepare_items() {
193
-
194
- global $itsec_logger;
195
-
196
- $columns = $this->get_columns();
197
- $hidden = array();
198
- $this->_column_headers = array( $columns, $hidden, false );
199
-
200
- $items = $itsec_logger->get_events( 'file_change' );
201
-
202
- $table_data = array();
203
-
204
- $count = 0;
205
-
206
- //Loop through results and take data we need
207
- foreach ( $items as $item ) {
208
-
209
- $data = maybe_unserialize( $item['log_data'] );
210
-
211
- $table_data[$count]['time'] = $item['log_date'];
212
- $table_data[$count]['detail'] = $item['log_id'];
213
- $table_data[$count]['added'] = isset( $data['added'] ) ? sizeof( $data['added'] ) : 0;
214
- $table_data[$count]['removed'] = isset( $data['removed'] ) ? sizeof( $data['removed'] ) : 0;
215
- $table_data[$count]['changed'] = isset( $data['changed'] ) ? sizeof( $data['changed'] ) : 0;
216
- $table_data[$count]['memory'] = isset( $data['memory'] ) ? $data['memory'] : 0;
217
- $table_data[$count]['added_detail'] = $data['added'];
218
- $table_data[$count]['removed_detail'] = $data['removed'];
219
- $table_data[$count]['changed_detail'] = $data['changed'];
220
-
221
- $count ++;
222
-
223
- }
224
-
225
- usort( $table_data, array( $this, 'sortrows' ) );
226
-
227
- $this->items = $table_data;
228
-
229
- }
230
-
231
- /**
232
- * Sorts rows by count in descending order
233
- *
234
- * @param array $a first array to compare
235
- * @param array $b second array to compare
236
- *
237
- * @return int comparison result
238
- */
239
- function sortrows( $a, $b ) {
240
-
241
- // If no sort, default to count
242
- $orderby = 'time';
243
-
244
- // If no order, default to desc
245
- $order = 'desc';
246
-
247
- // Determine sort order
248
- $result = strcmp( $a[$orderby], $b[$orderby] );
249
-
250
- // Send final sort direction to usort
251
- return ( $order === 'asc' ) ? $result : - $result;
252
-
253
- }
254
-
255
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/file-change/class-itsec-file-change.php CHANGED
@@ -26,8 +26,6 @@ class ITSEC_File_Change {
26
 
27
  add_action( 'itsec_execute_file_check_cron', array( $this, 'run_scan' ) ); //Action to execute during a cron run.
28
 
29
- add_filter( 'itsec_logger_displays', array( $this, 'itsec_logger_displays' ) ); //adds logs metaboxes
30
- add_filter( 'itsec_logger_modules', array( $this, 'itsec_logger_modules' ) );
31
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
32
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
33
  add_filter( 'itsec_file-change_notification_strings', array( $this, 'register_notification_strings' ) );
@@ -56,102 +54,6 @@ class ITSEC_File_Change {
56
  $scheduler->schedule( $interval, 'file-change' );
57
  }
58
 
59
- /**
60
- * Register file change detection for logger
61
- *
62
- * Registers the file change detection module with the core logger functionality.
63
- *
64
- * @since 4.0.0
65
- *
66
- * @param array $logger_modules array of logger modules
67
- *
68
- * @return array array of logger modules
69
- */
70
- public function itsec_logger_modules( $logger_modules ) {
71
-
72
- $logger_modules['file_change'] = array(
73
- 'type' => 'file_change',
74
- 'function' => __( 'File Changes Detected', 'better-wp-security' ),
75
- );
76
-
77
- return $logger_modules;
78
-
79
- }
80
-
81
- /**
82
- * Array of displays for the logs screen
83
- *
84
- * Registers the custom log page with the core plugin to allow for access from the log page's
85
- * dropdown menu.
86
- *
87
- * @since 4.0.0
88
- *
89
- * @param array $displays metabox array
90
- *
91
- * @return array metabox array
92
- */
93
- public function itsec_logger_displays( $displays ) {
94
-
95
- $displays[] = array(
96
- 'module' => 'file_change',
97
- 'title' => __( 'File Change History', 'better-wp-security' ),
98
- 'callback' => array( $this, 'logs_metabox_content' )
99
- );
100
-
101
- return $displays;
102
-
103
- }
104
-
105
- /**
106
- * Render the file change log metabox
107
- *
108
- * Displays a metabox on the logs page, when filtered, showing all file change items.
109
- *
110
- * @since 4.0.0
111
- *
112
- * @return void
113
- */
114
- public function logs_metabox_content() {
115
-
116
- if ( ! class_exists( 'ITSEC_File_Change_Log' ) ) {
117
- require( dirname( __FILE__ ) . '/class-itsec-file-change-log.php' );
118
- }
119
-
120
-
121
- $settings = ITSEC_Modules::get_settings( 'file-change' );
122
-
123
-
124
- // If we're splitting the file check run it every 6 hours. Else daily.
125
- if ( isset( $settings['split'] ) && true === $settings['split'] ) {
126
-
127
- $interval = 12342;
128
-
129
- } else {
130
-
131
- $interval = 86400;
132
-
133
- }
134
-
135
- $next_run_raw = $settings['last_run'] + $interval;
136
-
137
- if ( date( 'j', $next_run_raw ) == date( 'j', ITSEC_Core::get_current_time() ) ) {
138
- $next_run_day = __( 'Today', 'better-wp-security' );
139
- } else {
140
- $next_run_day = __( 'Tomorrow', 'better-wp-security' );
141
- }
142
-
143
- $next_run = $next_run_day . ' at ' . date( 'g:i a', $next_run_raw );
144
-
145
- echo '<p>' . __( 'Next automatic scan at: ', 'better-wp-security' ) . '<strong>' . $next_run . '*</strong></p>';
146
- echo '<p><em>*' . __( 'Automatic file change scanning is triggered by a user visiting your page and may not happen exactly at the time listed.', 'better-wp-security' ) . '</em>';
147
-
148
- $log_display = new ITSEC_File_Change_Log();
149
-
150
- $log_display->prepare_items();
151
- $log_display->display();
152
-
153
- }
154
-
155
  /**
156
  * Register verbs for Sync.
157
  *
26
 
27
  add_action( 'itsec_execute_file_check_cron', array( $this, 'run_scan' ) ); //Action to execute during a cron run.
28
 
 
 
29
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
30
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
31
  add_filter( 'itsec_file-change_notification_strings', array( $this, 'register_notification_strings' ) );
54
  $scheduler->schedule( $interval, 'file-change' );
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  /**
58
  * Register verbs for Sync.
59
  *
core/modules/file-change/js/settings-page.js CHANGED
@@ -34,23 +34,23 @@ jQuery( document ).ready( function ( $ ) {
34
  */
35
  $( document ).on( 'click', '#itsec-file-change-one_time_check', function( e ) {
36
  e.preventDefault();
37
-
38
  //let user know we're working
39
  $( '#itsec-file-change-one_time_check' )
40
  .removeClass( 'button-primary' )
41
  .addClass( 'button-secondary' )
42
  .attr( 'value', itsec_file_change_settings.scanning_button_text )
43
  .prop( 'disabled', true );
44
-
45
  var data = {
46
  'method': 'one-time-scan'
47
  };
48
-
49
  $( '#itsec_file_change_status' ).html( '' );
50
-
51
- itsecSettingsPage.sendModuleAJAXRequest( 'file-change', data, function( results ) {
52
  $( '#itsec_file_change_status' ).html( '' );
53
-
54
  if ( false === results.response ) {
55
  $( '#itsec_file_change_status' ).append( '<div class="updated fade inline"><p><strong>' + itsec_file_change_settings.no_changes + '</strong></p></div>' );
56
  } else if ( true === results.response ) {
@@ -64,7 +64,7 @@ jQuery( document ).ready( function ( $ ) {
64
  } else {
65
  $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + itsec_file_change_settings.unknown_error + '</strong></p></div>' );
66
  }
67
-
68
  $( '#itsec-file-change-one_time_check' )
69
  .removeClass( 'button-secondary' )
70
  .addClass( 'button-primary' )
34
  */
35
  $( document ).on( 'click', '#itsec-file-change-one_time_check', function( e ) {
36
  e.preventDefault();
37
+
38
  //let user know we're working
39
  $( '#itsec-file-change-one_time_check' )
40
  .removeClass( 'button-primary' )
41
  .addClass( 'button-secondary' )
42
  .attr( 'value', itsec_file_change_settings.scanning_button_text )
43
  .prop( 'disabled', true );
44
+
45
  var data = {
46
  'method': 'one-time-scan'
47
  };
48
+
49
  $( '#itsec_file_change_status' ).html( '' );
50
+
51
+ itsecUtil.sendModuleAJAXRequest( 'file-change', data, function( results ) {
52
  $( '#itsec_file_change_status' ).html( '' );
53
+
54
  if ( false === results.response ) {
55
  $( '#itsec_file_change_status' ).append( '<div class="updated fade inline"><p><strong>' + itsec_file_change_settings.no_changes + '</strong></p></div>' );
56
  } else if ( true === results.response ) {
64
  } else {
65
  $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + itsec_file_change_settings.unknown_error + '</strong></p></div>' );
66
  }
67
+
68
  $( '#itsec-file-change-one_time_check' )
69
  .removeClass( 'button-secondary' )
70
  .addClass( 'button-primary' )
core/modules/file-change/logs.php ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_File_Change_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_file_change_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
6
+ add_filter( 'itsec_logs_prepare_file_change_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ }
8
+
9
+ public function filter_entry_for_list_display( $entry, $code, $code_data ) {
10
+ $entry['module_display'] = esc_html__( 'File Change', 'better-wp-security' );
11
+
12
+ if ( 'scan' === $code && 'process-start' === $entry['type'] ) {
13
+ $entry['description'] = esc_html__( 'Scan Performance', 'better-wp-security' );
14
+ } else if ( 'no-changes-found' === $code ) {
15
+ $entry['description'] = esc_html__( 'No Changes Found', 'better-wp-security' );
16
+ } else if ( 'changes-found' === $code ) {
17
+ if ( isset( $code_data[0] ) ) {
18
+ $entry['description'] = sprintf( esc_html__( '%1$d Added, %2$d Removed, %3$d Changed', 'better-wp-security' ), $code_data[0], $code_data[1], $code_data[2] );
19
+ } else {
20
+ $entry['description'] = esc_html__( 'Changes Found', 'better-wp-security' );
21
+ }
22
+ }
23
+
24
+ return $entry;
25
+ }
26
+
27
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
28
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
29
+
30
+ $details['module']['content'] = $entry['module_display'];
31
+ $details['description']['content'] = $entry['description'];
32
+
33
+ if ( 'process-start' !== $entry['type'] ) {
34
+ $details['memory'] = array(
35
+ 'header' => esc_html__( 'Memory Used', 'better-wp-security' ),
36
+ 'content' => sprintf( esc_html_x( '%s MB', 'Megabytes of memory used', 'better-wp-security' ), $entry['data']['memory'] ),
37
+ );
38
+
39
+ $types = array(
40
+ 'added' => esc_html__( 'Added', 'better-wp-security' ),
41
+ 'removed' => esc_html__( 'Removed', 'better-wp-security' ),
42
+ 'changed' => esc_html__( 'Changed', 'better-wp-security' ),
43
+ );
44
+
45
+ foreach ( $types as $type => $header ) {
46
+ $details[$type] = array(
47
+ 'header' => $header,
48
+ 'content' => '<pre>' . implode( "\n", array_keys( $entry['data'][$type] ) ) . '</pre>',
49
+ );
50
+ }
51
+ }
52
+
53
+ return $details;
54
+ }
55
+ }
56
+ new ITSEC_File_Change_Logs();
core/modules/file-change/scanner.php CHANGED
@@ -11,15 +11,6 @@ final class ITSEC_File_Change_Scanner {
11
  */
12
  private $excludes;
13
 
14
- /**
15
- * Flag to indicate if a file change scan is in process
16
- *
17
- * @since 4.0.0
18
- * @access private
19
- * @var bool
20
- */
21
- private $running;
22
-
23
  /**
24
  * The module's saved options
25
  *
@@ -29,21 +20,12 @@ final class ITSEC_File_Change_Scanner {
29
  */
30
  private $settings;
31
 
32
- private static $instance = false;
33
-
34
 
35
- private function __construct() {
36
 
37
- $this->settings = ITSEC_Modules::get_settings( 'file-change' );
38
- $this->running = false;
39
- $this->excludes = array(
40
- 'file_change.lock',
41
- ITSEC_Modules::get_setting( 'backup', 'location' ),
42
- ITSEC_Modules::get_setting( 'global', 'log_location' ),
43
- '.lock',
44
- );
45
 
46
- }
47
 
48
  /**
49
  * Executes file checking
@@ -67,247 +49,287 @@ final class ITSEC_File_Change_Scanner {
67
  return self::$instance->execute_file_check( $scheduled_call, $return_data );
68
  }
69
 
70
- public function execute_file_check( $scheduled_call = true, $return_data = false ) {
71
 
72
- global $itsec_logger;
 
 
73
 
74
- if ( false === $this->running ) {
75
 
76
- $this->running = true;
77
- $send_email = true;
78
 
79
- ITSEC_Lib::set_minimum_memory_limit( '256M' );
80
 
81
- if ( ITSEC_Lib::get_lock( 'file_change', 300 ) ) { //make sure it isn't already running
82
 
83
- define( 'ITSEC_DOING_FILE_CHECK', true );
84
 
85
- //figure out what chunk we're on
86
- if ( isset( $this->settings['split'] ) && true === $this->settings['split'] ) {
 
87
 
88
- if ( isset( $this->settings['last_chunk'] ) && false !== $this->settings['last_chunk'] && $this->settings['last_chunk'] < 6 ) {
 
 
89
 
90
- $chunk = $this->settings['last_chunk'] + 1;
 
 
 
91
 
92
- } else {
93
 
94
- $chunk = 0;
95
 
96
- }
97
 
98
- } else {
99
 
100
- $chunk = false;
 
101
 
102
- }
 
 
 
 
103
 
104
- if ( false !== $chunk ) {
105
 
106
- $db_field = 'itsec_local_file_list_' . $chunk;
107
 
108
- } else {
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- $db_field = 'itsec_local_file_list';
111
 
112
- }
113
 
114
- //set base memory
115
- $memory_used = @memory_get_peak_usage();
116
 
117
- $logged_files = get_site_option( $db_field );
 
 
118
 
119
- //if there are no old files old file list is an empty array
120
- if ( false === $logged_files ) {
121
 
122
- $send_email = false;
123
 
124
- $logged_files = array();
 
 
125
 
126
- if ( is_multisite() ) {
127
 
128
- add_site_option( $db_field, $logged_files );
129
 
130
- } else {
131
 
132
- add_option( $db_field, $logged_files, '', 'no' );
133
 
134
- }
 
135
 
136
- }
 
 
 
 
 
 
 
 
137
 
138
- do_action( 'itsec-file-change-start-scan' );
139
- $current_files = $this->scan_files( '', $scheduled_call, $chunk ); //scan current files
140
- do_action( 'itsec-file-change-end-scan' );
141
 
142
- $files_added = @array_diff_assoc( $current_files, $logged_files ); //files added
143
- $files_removed = @array_diff_assoc( $logged_files, $current_files ); //files deleted
144
- $current_minus_added = @array_diff_key( $current_files, $files_added ); //remove all added files from current filelist
145
- $logged_minus_deleted = @array_diff_key( $logged_files, $files_removed ); //remove all deleted files from old file list
146
- $files_changed = array(); //array of changed files
147
 
148
- do_action( 'itsec-file-change-start-hash-comparisons' );
149
 
150
- //compare file hashes and mod dates
151
- foreach ( $current_minus_added as $current_file => $current_attr ) {
152
 
153
- if ( array_key_exists( $current_file, $logged_minus_deleted ) ) {
 
 
154
 
155
- //if attributes differ added to changed files array
156
- if (
157
- (
158
- (
159
- isset( $current_attr['mod_date'] ) &&
160
- 0 != strcmp( $current_attr['mod_date'], $logged_minus_deleted[ $current_file ]['mod_date'] )
161
- ) ||
162
- 0 != strcmp( $current_attr['d'], $logged_minus_deleted[ $current_file ]['d'] )
163
- ) ||
164
- (
165
- (
166
- isset( $current_attr['hash'] ) &&
167
- 0 != strcmp( $current_attr['hash'], $logged_minus_deleted[ $current_file ]['hash'] ) ) ||
168
- 0 != strcmp( $current_attr['h'], $logged_minus_deleted[ $current_file ]['h'] )
169
- )
170
- ) {
171
 
172
- $remote_check = apply_filters( 'itsec_process_changed_file', true, $current_file, $current_attr['h'] ); //hook to run actions on a changed file at time of discovery
173
 
174
- if ( true === $remote_check ) { //don't list the file if it matches the WordPress.org hash
 
 
 
 
175
 
176
- $files_changed[ $current_file ]['h'] = isset( $current_attr['hash'] ) ? $current_attr['hash'] : $current_attr['h'];
177
- $files_changed[ $current_file ]['d'] = isset( $current_attr['mod_date'] ) ? $current_attr['mod_date'] : $current_attr['d'];
178
 
179
- }
 
180
 
181
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  }
184
 
185
  }
186
 
187
- //get count of changes
188
- $files_added_count = sizeof( $files_added );
189
- $files_deleted_count = sizeof( $files_removed );
190
- $files_changed_count = sizeof( $files_changed );
191
 
192
- if ( 0 < $files_added_count ) {
193
 
194
- $files_added = apply_filters( 'itsec_process_added_files', $files_added ); //hook to run actions on all files added
195
- $files_added_count = sizeof( $files_added );
196
 
197
- }
 
 
 
198
 
199
- if ( 0 < $files_deleted_count ) {
200
- do_action( 'itsec_process_removed_files', $files_removed ); //hook to run actions on all files removed
201
- }
202
 
203
- do_action( 'itsec-file-change-end-hash-comparisons' );
 
204
 
205
- //create single array of all changes
206
- $full_change_list = array(
207
- 'added' => $files_added,
208
- 'removed' => $files_removed,
209
- 'changed' => $files_changed,
210
- );
211
 
212
- $this->settings['latest_changes'] = array(
213
- 'added' => count( $files_added ),
214
- 'removed' => count( $files_removed ),
215
- 'changed' => count( $files_changed ),
216
- );
217
 
218
- update_site_option( $db_field, $current_files );
219
 
220
- //Cleanup variables when we're done with them
221
- unset( $files_added );
222
- unset( $files_removed );
223
- unset( $files_changed );
224
- unset( $current_files );
225
 
226
- $this->settings['last_run'] = ITSEC_Core::get_current_time();
227
- $this->settings['last_chunk'] = $chunk;
228
 
229
- ITSEC_Modules::set_settings( 'file-change', $this->settings );
 
 
 
 
 
230
 
231
- //get new max memory
232
- $check_memory = @memory_get_peak_usage();
233
- if ( $check_memory > $memory_used ) {
234
- $memory_used = $check_memory - $memory_used;
235
- }
236
 
237
- $full_change_list['memory'] = round( ( $memory_used / 1000000 ), 2 );
238
 
239
- $itsec_logger->log_event(
240
- 'file_change',
241
- 8,
242
- $full_change_list
243
- );
244
 
245
- if (
246
- true === $send_email &&
247
- false !== $scheduled_call &&
248
- (
249
- 0 < $files_added_count ||
250
- 0 < $files_changed_count ||
251
- 0 < $files_deleted_count
252
- )
253
- ) {
254
 
255
- $email_details = array(
256
- $files_added_count,
257
- $files_deleted_count,
258
- $files_changed_count,
259
- $full_change_list
260
- );
261
 
262
- $this->send_notification_email( $email_details );
263
- }
264
 
265
- if (
266
- function_exists( 'get_current_screen' ) &&
267
- (
268
- ! isset( get_current_screen()->id ) ||
269
- false === strpos( get_current_screen()->id, 'security_page_toplevel_page_itsec_logs' )
270
- ) &&
271
- ! empty( $this->settings['notify_admin'] )
272
- ) {
273
- ITSEC_Modules::set_setting( 'file-change', 'show_warning', true );
274
- }
275
 
276
- ITSEC_Lib::release_lock( 'file_change' );
 
 
 
 
277
 
278
- if ( $files_added_count > 0 || $files_changed_count > 0 || $files_deleted_count > 0 ) {
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- $this->running = false;
 
281
 
282
- //There were changes found
283
- if ( $return_data ) {
 
 
 
 
 
 
 
 
 
284
 
285
- return $full_change_list;
 
 
 
 
286
 
287
- } else {
288
 
289
- return true;
290
 
291
- }
292
 
293
- } else {
294
 
295
- $this->running = false;
 
296
 
297
- return false; //No changes were found
298
 
299
- }
 
 
300
 
301
  }
302
 
303
- $this->running = false;
304
 
305
- return -1; //An error occured
306
 
307
  }
308
 
309
- return -1;
310
-
311
  }
312
 
313
  /**
@@ -328,73 +350,53 @@ final class ITSEC_File_Change_Scanner {
328
  }
329
 
330
  /**
331
- * Check file list
332
  *
333
- * Checks if given file should be included in file check based on exclude/include options
 
334
  *
335
- * @since 4.0.0
336
  *
337
  * @access private
338
  *
339
- * @param string $file path of file to check from site root
 
340
  *
341
- * @return bool true if file should be checked false if not
342
  */
343
- private function is_checkable_file( $file ) {
344
 
345
- //get file list from last check
346
- $file_list = $this->settings['file_list'];
347
- $type_list = $this->settings['types'];
348
-
349
- //Make sure the file list is an array
350
- if ( ! is_array( $file_list ) ) {
351
- $file_list = array();
352
- }
353
 
354
- //lets check the absolute path too for excludes just to be sure
355
- $abs_file = ITSEC_Lib::get_home_path() . $file;
356
 
357
- //assume not a directory and not checked
358
- $flag = false;
359
-
360
- if ( is_array( $this->excludes ) && ( in_array( $file, $this->excludes ) || in_array( $abs_file, $this->excludes ) ) ) {
361
- return false;
362
- }
363
-
364
- if ( in_array( $file, $file_list ) ) {
365
- $flag = true;
366
- }
367
 
368
- if ( ! is_dir( $file ) ) {
369
 
370
- $path_info = pathinfo( $file );
371
 
372
- if ( isset( $path_info['extension'] ) && in_array( '.' . $path_info['extension'], $this->excludes ) ) {
 
 
 
 
373
 
374
- return false;
375
-
376
- }
377
-
378
- if ( isset( $path_info['extension'] ) && in_array( '.' . $path_info['extension'], $type_list ) ) {
379
- $flag = true;
380
  }
381
 
382
  }
383
 
384
- if ( 'exclude' === $this->settings['method'] ) {
385
-
386
- if ( true === $flag ) { //if exclude reverse
387
- return false;
388
- } else {
389
- return true;
390
- }
391
-
392
- } else { //return flag
393
-
394
- return $flag;
395
-
396
- }
397
 
 
398
  }
399
 
400
  /**
@@ -407,89 +409,63 @@ final class ITSEC_File_Change_Scanner {
407
  *
408
  * @access private
409
  *
410
- * @param string $path [optional] path to scan, defaults to WordPress root
411
- * @param bool $scheduled_call is this a scheduled call
412
- * @param mixed $chunk the current chunk or false
413
  *
414
  * @return array array of files found and their information
415
  *
416
  */
417
- private function scan_files( $path = '', $scheduled_call, $chunk ) {
418
-
419
- if ( $chunk !== false ) {
420
-
421
- $content_dir = explode( '/', WP_CONTENT_DIR );
422
- $plugin_dir = explode( '/', WP_PLUGIN_DIR );
423
-
424
- $dirs = array(
425
- 'wp-admin/',
426
- WPINC . '/',
427
- $content_dir[ sizeof( $content_dir ) - 1 ] . '/',
428
- $content_dir[ sizeof( $content_dir ) - 1 ] . '/uploads/',
429
- $content_dir[ sizeof( $content_dir ) - 1 ] . '/themes/',
430
- $content_dir[ sizeof( $content_dir ) - 1 ] . '/' . $plugin_dir[ sizeof( $plugin_dir ) - 1 ] . '/',
431
- ''
432
- );
433
-
434
- $path = $dirs[ $chunk ];
435
-
436
- unset( $dirs[ $chunk ] );
437
-
438
- $this->excludes = $dirs;
439
-
440
  }
441
 
442
- $data = array();
443
-
444
- $clean_path = sanitize_text_field( $path );
445
-
446
- if ( $directory_handle = @opendir( ITSEC_Lib::get_home_path() . $clean_path ) ) { //get the directory
447
-
448
- while ( false !== ( $item = @readdir( $directory_handle ) ) ) { // loop through dirs
449
-
450
- if ( '.' != $item && '..' != $item ) { //don't scan parents
451
 
452
- $relname = $path . $item;
453
-
454
- $absname = ITSEC_Lib::get_home_path() . $relname;
455
-
456
- if ( is_dir( $absname ) && 'dir' == filetype( $absname ) ) {
457
 
458
- $is_dir = true;
459
- $check_name = trailingslashit( $relname );
 
460
 
461
- } else {
462
 
463
- $is_dir = false;
464
- $check_name = $relname;
465
 
466
- }
 
 
467
 
468
- if ( true === $this->is_checkable_file( $check_name ) ) { //make sure the user wants this file scanned
469
 
470
- if ( true === $is_dir ) { //if directory scan it
 
471
 
472
- $data = array_merge( $data, $this->scan_files( $relname . '/', $scheduled_call, false ) );
473
 
474
- } else { //is file so add to array
 
 
 
 
475
 
476
- $data[ $relname ] = array();
477
- $data[ $relname ]['d'] = @filemtime( $absname );
478
- $data[ $relname ]['h'] = @md5_file( $absname );
479
 
480
- }
481
 
482
- }
483
 
 
 
 
484
  }
485
 
 
 
 
 
486
  }
487
 
488
- @closedir( $directory_handle ); //close the directory we're working with
489
-
490
  }
491
 
492
- return $data; // return the files we found in this dir
 
 
493
 
494
  }
495
 
11
  */
12
  private $excludes;
13
 
 
 
 
 
 
 
 
 
 
14
  /**
15
  * The module's saved options
16
  *
20
  */
21
  private $settings;
22
 
23
+ private $home_path;
 
24
 
25
+ private static $instance = false;
26
 
 
 
 
 
 
 
 
 
27
 
28
+ private function __construct() {}
29
 
30
  /**
31
  * Executes file checking
49
  return self::$instance->execute_file_check( $scheduled_call, $return_data );
50
  }
51
 
52
+ private function execute_file_check( $scheduled_call, $return_data ) {
53
 
54
+ if ( ! ITSEC_Lib::get_lock( 'file_change', 300 ) ) {
55
+ return -1;
56
+ }
57
 
 
58
 
59
+ $process_id = ITSEC_Log::add_process_start( 'file_change', 'scan' );
 
60
 
 
61
 
62
+ $this->home_path = untrailingslashit( ITSEC_Lib::get_home_path() );
63
 
64
+ $this->settings = ITSEC_Modules::get_settings( 'file-change' );
65
 
66
+ if ( ! in_array( '.lock', $this->settings['types'] ) ) {
67
+ $this->settings['types'][] = '.lock';
68
+ }
69
 
70
+ foreach ( $this->settings['file_list'] as $index => $path ) {
71
+ $this->settings['file_list'][$index] = untrailingslashit( $path );
72
+ }
73
 
74
+ $this->excludes = array(
75
+ ITSEC_Modules::get_setting( 'backup', 'location' ),
76
+ ITSEC_Modules::get_setting( 'global', 'log_location' ),
77
+ );
78
 
 
79
 
80
+ $send_email = true;
81
 
82
+ ITSEC_Lib::set_minimum_memory_limit( '512M' );
83
 
84
+ define( 'ITSEC_DOING_FILE_CHECK', true );
85
 
86
+ //figure out what chunk we're on
87
+ if ( $this->settings['split'] ) {
88
 
89
+ if ( false === $this->settings['last_chunk'] || $this->settings['last_chunk'] > 5 ) {
90
+ $chunk = 0;
91
+ } else {
92
+ $chunk = $this->settings['last_chunk'] + 1;
93
+ }
94
 
95
+ $db_field = 'itsec_local_file_list_' . $chunk;
96
 
 
97
 
98
+ $content_dir = explode( '/', WP_CONTENT_DIR );
99
+ $plugin_dir = explode( '/', WP_PLUGIN_DIR );
100
+ $wp_upload_dir = ITSEC_Core::get_wp_upload_dir();
101
+
102
+ $dirs = array(
103
+ 'wp-admin',
104
+ WPINC,
105
+ WP_CONTENT_DIR,
106
+ ITSEC_Core::get_wp_upload_dir(),
107
+ WP_CONTENT_DIR . '/themes',
108
+ WP_PLUGIN_DIR,
109
+ ''
110
+ );
111
 
112
+ $path = $dirs[ $chunk ];
113
 
114
+ unset( $dirs[ $chunk ] );
115
 
116
+ $this->excludes = array_merge( $this->excludes, $dirs );
 
117
 
118
+ foreach ( $this->excludes as $index => $path ) {
119
+ $path = untrailingslashit( $path );
120
+ $path = preg_replace( '/^' . preg_quote( ABSPATH, '/' ) . '/', '', $path );
121
 
122
+ $this->excludes[$index] = $path;
123
+ }
124
 
125
+ } else {
126
 
127
+ $chunk = false;
128
+ $db_field = 'itsec_local_file_list';
129
+ $path = '';
130
 
131
+ }
132
 
 
133
 
134
+ $memory_used = @memory_get_peak_usage();
135
 
136
+ $logged_files = get_site_option( $db_field );
137
 
138
+ //if there are no old files old file list is an empty array
139
+ if ( false === $logged_files ) {
140
 
141
+ $send_email = false;
142
+
143
+ $logged_files = array();
144
+
145
+ if ( is_multisite() ) {
146
+
147
+ add_site_option( $db_field, $logged_files );
148
+
149
+ } else {
150
 
151
+ add_option( $db_field, $logged_files, '', 'no' );
 
 
152
 
153
+ }
 
 
 
 
154
 
155
+ }
156
 
157
+ ITSEC_Log::add_process_update( $process_id, array( 'status' => 'init_complete', 'settings' => $this->settings, 'excludes' => $this->excludes, 'path' => $path, 'scheduled_call' => $scheduled_call, 'chunk' => $chunk ) );
 
158
 
159
+ do_action( 'itsec-file-change-start-scan' );
160
+ $current_files = $this->scan_files( $path );
161
+ do_action( 'itsec-file-change-end-scan' );
162
 
163
+ ITSEC_Log::add_process_update( $process_id, array( 'status' => 'file_scan_complete' ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
 
165
 
166
+ $files_added = @array_diff_assoc( $current_files, $logged_files ); //files added
167
+ $files_removed = @array_diff_assoc( $logged_files, $current_files ); //files deleted
168
+ $current_minus_added = @array_diff_key( $current_files, $files_added ); //remove all added files from current filelist
169
+ $logged_minus_deleted = @array_diff_key( $logged_files, $files_removed ); //remove all deleted files from old file list
170
+ $files_changed = array(); //array of changed files
171
 
172
+ do_action( 'itsec-file-change-start-hash-comparisons' );
 
173
 
174
+ //compare file hashes and mod dates
175
+ foreach ( $current_minus_added as $current_file => $current_attr ) {
176
 
177
+ if ( array_key_exists( $current_file, $logged_minus_deleted ) ) {
178
+
179
+ //if attributes differ added to changed files array
180
+ if (
181
+ (
182
+ (
183
+ isset( $current_attr['mod_date'] ) &&
184
+ 0 != strcmp( $current_attr['mod_date'], $logged_minus_deleted[ $current_file ]['mod_date'] )
185
+ ) ||
186
+ 0 != strcmp( $current_attr['d'], $logged_minus_deleted[ $current_file ]['d'] )
187
+ ) ||
188
+ (
189
+ (
190
+ isset( $current_attr['hash'] ) &&
191
+ 0 != strcmp( $current_attr['hash'], $logged_minus_deleted[ $current_file ]['hash'] ) ) ||
192
+ 0 != strcmp( $current_attr['h'], $logged_minus_deleted[ $current_file ]['h'] )
193
+ )
194
+ ) {
195
+
196
+ $remote_check = apply_filters( 'itsec_process_changed_file', true, $current_file, $current_attr['h'] ); //hook to run actions on a changed file at time of discovery
197
+
198
+ if ( true === $remote_check ) { //don't list the file if it matches the WordPress.org hash
199
+
200
+ $files_changed[ $current_file ]['h'] = isset( $current_attr['hash'] ) ? $current_attr['hash'] : $current_attr['h'];
201
+ $files_changed[ $current_file ]['d'] = isset( $current_attr['mod_date'] ) ? $current_attr['mod_date'] : $current_attr['d'];
202
 
203
  }
204
 
205
  }
206
 
207
+ }
 
 
 
208
 
209
+ }
210
 
 
 
211
 
212
+ //get count of changes
213
+ $files_added_count = count( $files_added );
214
+ $files_deleted_count = count( $files_removed );
215
+ $files_changed_count = count( $files_changed );
216
 
217
+ if ( $files_added_count > 0 ) {
 
 
218
 
219
+ $files_added = apply_filters( 'itsec_process_added_files', $files_added ); //hook to run actions on all files added
220
+ $files_added_count = count( $files_added );
221
 
222
+ }
 
 
 
 
 
223
 
224
+ if ( $files_deleted_count > 0 ) {
225
+ do_action( 'itsec_process_removed_files', $files_removed ); //hook to run actions on all files removed
226
+ }
 
 
227
 
228
+ do_action( 'itsec-file-change-end-hash-comparisons' );
229
 
230
+ ITSEC_Log::add_process_update( $process_id, array( 'status' => 'hash_comparisons_complete' ) );
 
 
 
 
231
 
 
 
232
 
233
+ //create single array of all changes
234
+ $full_change_list = array(
235
+ 'added' => $files_added,
236
+ 'removed' => $files_removed,
237
+ 'changed' => $files_changed,
238
+ );
239
 
240
+ $this->settings['latest_changes'] = array(
241
+ 'added' => count( $files_added ),
242
+ 'removed' => count( $files_removed ),
243
+ 'changed' => count( $files_changed ),
244
+ );
245
 
246
+ update_site_option( $db_field, $current_files );
247
 
 
 
 
 
 
248
 
249
+ //Cleanup variables when we're done with them
250
+ unset( $files_added );
251
+ unset( $files_removed );
252
+ unset( $files_changed );
253
+ unset( $current_files );
 
 
 
 
254
 
255
+ $this->settings['last_run'] = ITSEC_Core::get_current_time();
256
+ $this->settings['last_chunk'] = $chunk;
 
 
 
 
257
 
258
+ ITSEC_Modules::set_settings( 'file-change', $this->settings );
 
259
 
260
+ //get new max memory
261
+ $check_memory = @memory_get_peak_usage();
262
+ if ( $check_memory > $memory_used ) {
263
+ $memory_used = $check_memory - $memory_used;
264
+ }
265
+
266
+ $full_change_list['memory'] = round( ( $memory_used / 1000000 ), 2 );
 
 
 
267
 
268
+ if ( $files_added_count > 0 || $files_changed_count > 0 || $files_deleted_count > 0 ) {
269
+ $found_changes = true;
270
+ } else {
271
+ $found_changes = false;
272
+ }
273
 
274
+ if (
275
+ $found_changes &&
276
+ $send_email &&
277
+ ! $scheduled_call &&
278
+ $this->settings['email']
279
+ ) {
280
+
281
+ $email_details = array(
282
+ $files_added_count,
283
+ $files_deleted_count,
284
+ $files_changed_count,
285
+ $full_change_list
286
+ );
287
 
288
+ $this->send_notification_email( $email_details );
289
+ }
290
 
291
+ if (
292
+ $found_changes &&
293
+ $this->settings['notify_admin'] &&
294
+ function_exists( 'get_current_screen' ) &&
295
+ (
296
+ ! isset( get_current_screen()->id ) ||
297
+ false === strpos( get_current_screen()->id, 'security_page_toplevel_page_itsec_logs' )
298
+ )
299
+ ) {
300
+ ITSEC_Modules::set_setting( 'file-change', 'show_warning', true );
301
+ }
302
 
303
+ if ( $found_changes ) {
304
+ ITSEC_Log::add_warning( 'file_change', "changes-found::$files_added_count,$files_deleted_count,$files_changed_count", $full_change_list );
305
+ } else {
306
+ ITSEC_Log::add_notice( 'file_change', 'no-changes-found', $full_change_list );
307
+ }
308
 
309
+ ITSEC_Lib::release_lock( 'file_change' );
310
 
 
311
 
312
+ ITSEC_Log::add_process_stop( $process_id );
313
 
314
+ if ( $files_added_count > 0 || $files_changed_count > 0 || $files_deleted_count > 0 ) {
315
 
316
+ //There were changes found
317
+ if ( $return_data ) {
318
 
319
+ return $full_change_list;
320
 
321
+ } else {
322
+
323
+ return true;
324
 
325
  }
326
 
327
+ } else {
328
 
329
+ return false; //No changes were found
330
 
331
  }
332
 
 
 
333
  }
334
 
335
  /**
350
  }
351
 
352
  /**
353
+ * Builds table section for file report
354
  *
355
+ * Builds the individual table areas for files added, changed and deleted that goes in the file
356
+ * change notification emails.
357
  *
358
+ * @since 4.6.0
359
  *
360
  * @access private
361
  *
362
+ * @param string $title User readable title to display
363
+ * @param array $files array of files to build the report on
364
  *
365
+ * @return string the markup with the given files to be added to the report
366
  */
367
+ private function build_table_section( $title, $files ) {
368
 
369
+ $section = '<h4>' . __( 'Files', 'better-wp-security' ) . ' ' . $title . '</h4>';
370
+ $section .= '<table border="1" style="width: 100%; text-align: center;">' . PHP_EOL;
371
+ $section .= '<tr>' . PHP_EOL;
372
+ $section .= '<th>' . __( 'File', 'better-wp-security' ) . '</th>' . PHP_EOL;
373
+ $section .= '<th>' . __( 'Modified', 'better-wp-security' ) . '</th>' . PHP_EOL;
374
+ $section .= '<th>' . __( 'File Hash', 'better-wp-security' ) . '</th>' . PHP_EOL;
375
+ $section .= '</tr>' . PHP_EOL;
 
376
 
377
+ if ( empty( $files ) ) {
 
378
 
379
+ $section .= '<tr>' . PHP_EOL;
380
+ $section .= '<td colspan="3">' . __( 'No files were changed.', 'better-wp-security' ) . '</td>' . PHP_EOL;
381
+ $section .= '</tr>' . PHP_EOL;
 
 
 
 
 
 
 
382
 
383
+ } else {
384
 
385
+ foreach ( $files as $item => $attr ) {
386
 
387
+ $section .= '<tr>' . PHP_EOL;
388
+ $section .= '<td>' . $item . '</td>' . PHP_EOL;
389
+ $section .= '<td>' . date( 'l F jS, Y \a\t g:i a e', ( isset( $attr['mod_date'] ) ? $attr['mod_date'] : $attr['d'] ) ) . '</td>' . PHP_EOL;
390
+ $section .= '<td>' . ( isset( $attr['hash'] ) ? $attr['hash'] : $attr['h'] ) . '</td>' . PHP_EOL;
391
+ $section .= '</tr>' . PHP_EOL;
392
 
 
 
 
 
 
 
393
  }
394
 
395
  }
396
 
397
+ $section .= '</table>' . PHP_EOL;
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
+ return $section;
400
  }
401
 
402
  /**
409
  *
410
  * @access private
411
  *
412
+ * @param string $path Path to scan. Defaults to WordPress root
 
 
413
  *
414
  * @return array array of files found and their information
415
  *
416
  */
417
+ private function scan_files( $path ) {
418
+ if ( in_array( $path, $this->excludes ) ) {
419
+ return array();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  }
421
 
 
 
 
 
 
 
 
 
 
422
 
423
+ $abspath = "{$this->home_path}/$path";
424
+ $data = array();
 
 
 
425
 
426
+ if ( false === ( $dh = @opendir( $abspath ) ) ) {
427
+ return $data;
428
+ }
429
 
 
430
 
431
+ while ( false !== ( $item = @readdir( $dh ) ) ) {
 
432
 
433
+ if ( '.' === $item || '..' === $item ) {
434
+ continue;
435
+ }
436
 
 
437
 
438
+ $relname = "$path/$item";
439
+ $absname = "$abspath/$item";
440
 
 
441
 
442
+ // Efficient but difficult to grock way to skip an item if it is in the file_list and the method is
443
+ // exclude or if it is not in the file_list and the method is include.
444
+ if ( in_array( $relname, $this->settings['file_list'] ) xor 'include' === $this->settings['method'] ) {
445
+ continue;
446
+ }
447
 
 
 
 
448
 
449
+ if ( is_dir( $absname ) && 'dir' === filetype( $absname ) ) {
450
 
451
+ $data = array_merge( $data, $this->scan_files( $relname ) );
452
 
453
+ } else {
454
+ if ( in_array( '.' . pathinfo( $item, PATHINFO_EXTENSION ), $this->settings['types'] ) ) {
455
+ continue;
456
  }
457
 
458
+ $data[ substr( $relname, 1 ) ] = array(
459
+ 'd' => @filemtime( $absname ),
460
+ 'h' => @md5_file( $absname ),
461
+ );
462
  }
463
 
 
 
464
  }
465
 
466
+ @closedir( $dh );
467
+
468
+ return $data;
469
 
470
  }
471
 
core/modules/global/js/settings-page.js CHANGED
@@ -1,6 +1,6 @@
1
  function itsec_change_show_error_codes( args ) {
2
  var show = args[0];
3
-
4
  if ( show ) {
5
  jQuery( 'body' ).addClass( 'itsec-show-error-codes' );
6
  } else {
@@ -10,7 +10,7 @@ function itsec_change_show_error_codes( args ) {
10
 
11
  function itsec_change_write_files( args ) {
12
  var enabled = args[0];
13
-
14
  if ( enabled ) {
15
  jQuery( 'body' ).removeClass( 'itsec-write-files-disabled' ).addClass( 'itsec-write-files-enabled' );
16
  } else {
@@ -18,21 +18,40 @@ function itsec_change_write_files( args ) {
18
  }
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  jQuery( document ).ready(function() {
22
  var $container = jQuery( '#wpcontent' );
23
-
24
  $container.on( 'click', '#itsec-global-add-to-whitelist', function( e ) {
25
  e.preventDefault();
26
-
27
  var whitelist = jQuery( '#itsec-global-lockout_white_list' ).val();
28
  whitelist = whitelist.trim();
29
  whitelist += "\n" + itsec_global_settings_page.ip;
30
  jQuery( '#itsec-global-lockout_white_list' ).val( whitelist );
31
  } );
32
-
33
  $container.on( 'click', '#itsec-global-reset-log-location', function( e ) {
34
  e.preventDefault();
35
-
36
  jQuery( '#itsec-global-log_location' ).val( itsec_global_settings_page.log_location );
37
  } );
 
 
 
 
38
  });
1
  function itsec_change_show_error_codes( args ) {
2
  var show = args[0];
3
+
4
  if ( show ) {
5
  jQuery( 'body' ).addClass( 'itsec-show-error-codes' );
6
  } else {
10
 
11
  function itsec_change_write_files( args ) {
12
  var enabled = args[0];
13
+
14
  if ( enabled ) {
15
  jQuery( 'body' ).removeClass( 'itsec-write-files-disabled' ).addClass( 'itsec-write-files-enabled' );
16
  } else {
18
  }
19
  }
20
 
21
+ var itsec_log_type_changed = function() {
22
+ var type = jQuery( '#itsec-global-log_type' ).val();
23
+
24
+ if ( 'both' === type ) {
25
+ jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
26
+ jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
27
+ } else if ( 'file' === type ) {
28
+ jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).hide();
29
+ jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
30
+ } else {
31
+ jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
32
+ jQuery( '#itsec-global-log_location' ).parents( 'tr' ).hide();
33
+ }
34
+ };
35
+
36
  jQuery( document ).ready(function() {
37
  var $container = jQuery( '#wpcontent' );
38
+
39
  $container.on( 'click', '#itsec-global-add-to-whitelist', function( e ) {
40
  e.preventDefault();
41
+
42
  var whitelist = jQuery( '#itsec-global-lockout_white_list' ).val();
43
  whitelist = whitelist.trim();
44
  whitelist += "\n" + itsec_global_settings_page.ip;
45
  jQuery( '#itsec-global-lockout_white_list' ).val( whitelist );
46
  } );
47
+
48
  $container.on( 'click', '#itsec-global-reset-log-location', function( e ) {
49
  e.preventDefault();
50
+
51
  jQuery( '#itsec-global-log_location' ).val( itsec_global_settings_page.log_location );
52
  } );
53
+
54
+ $container.on( 'change', '#itsec-global-log_type', itsec_log_type_changed );
55
+
56
+ itsec_log_type_changed();
57
  });
core/modules/global/settings.php CHANGED
@@ -15,7 +15,7 @@ final class ITSEC_Global_Settings_New extends ITSEC_Settings {
15
  'blacklist_period' => 7,
16
  'lockout_period' => 15,
17
  'lockout_white_list' => array(),
18
- 'log_rotation' => 14,
19
  'log_type' => 'database',
20
  'log_location' => ITSEC_Core::get_storage_dir( 'logs' ),
21
  'log_info' => '',
15
  'blacklist_period' => 7,
16
  'lockout_period' => 15,
17
  'lockout_white_list' => array(),
18
+ 'log_rotation' => 60,
19
  'log_type' => 'database',
20
  'log_location' => ITSEC_Core::get_storage_dir( 'logs' ),
21
  'log_info' => '',
core/modules/ipcheck/class-itsec-ipcheck.php CHANGED
@@ -15,10 +15,7 @@ class ITSEC_IPCheck {
15
  private $settings;
16
 
17
  public function run() {
18
- add_action( 'wp_login', array( $this, 'wp_login' ), 10, 2 );
19
- add_action( 'itsec-handle-failed-login', array( $this, 'handle_failed_login' ), 10, 2 );
20
-
21
- add_filter( 'itsec_logger_modules', array( $this, 'itsec_logger_modules' ) );
22
  }
23
 
24
  private function load_settings() {
@@ -27,44 +24,27 @@ class ITSEC_IPCheck {
27
  }
28
  }
29
 
30
- private function set_cache( $ip, $response ) {
31
- $cache = $this->get_cache( $ip );
32
- $time = ITSEC_Core::get_current_time_gmt();
33
-
34
- if ( isset( $response['block'] ) ) {
35
- $cache['block'] = (boolean) $response['block'];
36
- }
37
-
38
- if ( isset( $response['cache_ttl'] ) ) {
39
- $cache['cache_ttl'] = intval( $response['cache_ttl'] ) + $time;
40
- } else if ( 0 === $cache['cache_ttl'] ) {
41
- $cache['cache_ttl'] = $time + HOUR_IN_SECONDS;
42
- }
43
 
44
- if ( isset( $response['report_ttl'] ) ) {
45
- $cache['report_ttl'] = intval( $response['report_ttl'] ) + $time;
 
46
  }
47
 
48
- $transient_time = max( $cache['cache_ttl'], $cache['report_ttl'] ) - $time;
49
-
50
-
51
- set_site_transient( "itsec_ipcheck_$ip", $cache, $transient_time );
52
- }
53
-
54
- private function get_cache( $ip ) {
55
- $cache = get_site_transient( "itsec_ipcheck_$ip" );
56
-
57
- $defaults = array(
58
- 'block' => false,
59
- 'cache_ttl' => 0,
60
- 'report_ttl' => 0,
61
- );
62
 
63
- if ( ! is_array( $cache ) ) {
64
- return $defaults;
 
 
 
 
 
 
65
  }
66
 
67
- return array_merge( $defaults, $cache );
68
  }
69
 
70
  /**
@@ -74,8 +54,8 @@ class ITSEC_IPCheck {
74
  *
75
  * @return bool true if banned, false otherwise.
76
  */
77
- private function is_ip_banned( $ip = false ) {
78
- return $this->get_server_response( 'check-ip', $ip );
79
  }
80
 
81
  /**
@@ -85,14 +65,11 @@ class ITSEC_IPCheck {
85
  *
86
  * @return bool true if banned, false otherwise.
87
  */
88
- private function report_ip( $ip = false ) {
89
- return $this->get_server_response( 'report-ip', $ip );
90
  }
91
 
92
- private function get_server_response( $action, $ip = false ) {
93
- global $itsec_logger;
94
-
95
-
96
  $this->load_settings();
97
 
98
  if ( empty( $this->settings['api_key'] ) || empty( $this->settings['api_secret'] ) ) {
@@ -100,9 +77,7 @@ class ITSEC_IPCheck {
100
  }
101
 
102
 
103
- if ( false === $ip ) {
104
- $ip = ITSEC_Lib::get_ip();
105
- }
106
 
107
  require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
108
 
@@ -167,7 +142,7 @@ class ITSEC_IPCheck {
167
  'type' => 'host',
168
  );
169
 
170
- $itsec_logger->log_event( 'lockout', 10, $data, $ip );
171
 
172
  return true;
173
  }
@@ -175,6 +150,46 @@ class ITSEC_IPCheck {
175
  return false;
176
  }
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  /**
179
  * Calculates the HMAC of a string using SHA1.
180
  *
@@ -200,62 +215,4 @@ class ITSEC_IPCheck {
200
  return base64_encode( $hmac );
201
  }
202
 
203
- /**
204
- * Register IPCheck for logger
205
- *
206
- * @since 4.5
207
- *
208
- * @param array $logger_modules array of logger modules
209
- *
210
- * @return array array of logger modules
211
- */
212
- public function itsec_logger_modules( $logger_modules ) {
213
- $logger_modules['ipcheck'] = array(
214
- 'type' => 'ipcheck',
215
- 'function' => __( 'IP Flagged by Network Brute Force Protection', 'better-wp-security' ),
216
- );
217
-
218
- return $logger_modules;
219
- }
220
-
221
- /**
222
- * Make sure user isn't already locked out even on successful form submission
223
- *
224
- * @since 4.5
225
- *
226
- * @return void
227
- */
228
- public function wp_login() {
229
-
230
- /** @var ITSEC_Lockout $itsec_lockout */
231
- global $itsec_logger, $itsec_lockout;
232
-
233
- $this->load_settings();
234
-
235
- if ( $this->settings['enable_ban'] && $this->is_ip_banned() ) {
236
- $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
237
- $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
238
- }
239
- }
240
-
241
- /**
242
- * Sends to lockout class when username and password are filled out and wrong
243
- *
244
- * @since 4.5
245
- *
246
- * @return void
247
- */
248
- public function handle_failed_login( $username, $details ) {
249
-
250
- /** @var ITSEC_Lockout $itsec_lockout */
251
- global $itsec_logger, $itsec_lockout;
252
-
253
- $this->load_settings();
254
-
255
- if ( $this->settings['enable_ban'] && $this->report_ip() ) {
256
- $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
257
- $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
258
- }
259
- }
260
-
261
  }
15
  private $settings;
16
 
17
  public function run() {
18
+ add_filter( 'authenticate', array( $this, 'filter_authenticate' ), 10000, 3 ); // Set a very late priority so that we run after actual authentication takes place.
 
 
 
19
  }
20
 
21
  private function load_settings() {
24
  }
25
  }
26
 
27
+ public function filter_authenticate( $user, $username, $password ) {
28
+ global $itsec_lockout;
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ if ( is_wp_error( $user ) && $user->get_error_codes() == array( 'empty_username', 'empty_password' ) ) {
31
+ // This is not an authentication attempt. It is simply the login page loading.
32
+ return $user;
33
  }
34
 
35
+ $this->load_settings();
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ if ( is_wp_error( $user ) || null == $user ) {
38
+ if ( $this->report_ip() && $this->settings['enable_ban'] ) {
39
+ ITSEC_Log::add_notice( 'ipcheck', 'failed-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
40
+ $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
41
+ }
42
+ } else if ( $this->settings['enable_ban'] && $this->is_ip_banned() ) {
43
+ ITSEC_Log::add_critical_issue( 'ipcheck', 'successful-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
44
+ $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
45
  }
46
 
47
+ return $user;
48
  }
49
 
50
  /**
54
  *
55
  * @return bool true if banned, false otherwise.
56
  */
57
+ private function is_ip_banned() {
58
+ return $this->get_server_response( 'check-ip' );
59
  }
60
 
61
  /**
65
  *
66
  * @return bool true if banned, false otherwise.
67
  */
68
+ private function report_ip() {
69
+ return $this->get_server_response( 'report-ip' );
70
  }
71
 
72
+ private function get_server_response( $action ) {
 
 
 
73
  $this->load_settings();
74
 
75
  if ( empty( $this->settings['api_key'] ) || empty( $this->settings['api_secret'] ) ) {
77
  }
78
 
79
 
80
+ $ip = ITSEC_Lib::get_ip();
 
 
81
 
82
  require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
83
 
142
  'type' => 'host',
143
  );
144
 
145
+ ITSEC_Log::add_action( 'ipcheck', 'ip-blocked', $data );
146
 
147
  return true;
148
  }
150
  return false;
151
  }
152
 
153
+ private function set_cache( $ip, $response ) {
154
+ $cache = $this->get_cache( $ip );
155
+ $time = ITSEC_Core::get_current_time_gmt();
156
+
157
+ if ( isset( $response['block'] ) ) {
158
+ $cache['block'] = (boolean) $response['block'];
159
+ }
160
+
161
+ if ( isset( $response['cache_ttl'] ) ) {
162
+ $cache['cache_ttl'] = intval( $response['cache_ttl'] ) + $time;
163
+ } else if ( 0 === $cache['cache_ttl'] ) {
164
+ $cache['cache_ttl'] = $time + HOUR_IN_SECONDS;
165
+ }
166
+
167
+ if ( isset( $response['report_ttl'] ) ) {
168
+ $cache['report_ttl'] = intval( $response['report_ttl'] ) + $time;
169
+ }
170
+
171
+ $transient_time = max( $cache['cache_ttl'], $cache['report_ttl'] ) - $time;
172
+
173
+
174
+ set_site_transient( "itsec_ipcheck_$ip", $cache, $transient_time );
175
+ }
176
+
177
+ private function get_cache( $ip ) {
178
+ $cache = get_site_transient( "itsec_ipcheck_$ip" );
179
+
180
+ $defaults = array(
181
+ 'block' => false,
182
+ 'cache_ttl' => 0,
183
+ 'report_ttl' => 0,
184
+ );
185
+
186
+ if ( ! is_array( $cache ) ) {
187
+ return $defaults;
188
+ }
189
+
190
+ return array_merge( $defaults, $cache );
191
+ }
192
+
193
  /**
194
  * Calculates the HMAC of a string using SHA1.
195
  *
215
  return base64_encode( $hmac );
216
  }
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  }
core/modules/ipcheck/js/settings-page.js CHANGED
@@ -20,7 +20,7 @@ jQuery( document ).ready(function ( $ ) {
20
 
21
  $( '#itsec-network-brute-force-reset-status' ).html( '' );
22
 
23
- itsecSettingsPage.sendModuleAJAXRequest( 'network-brute-force', data, function( results ) {
24
  $( '#itsec-network-brute-force-reset-status' ).html( '' );
25
 
26
  if ( true !== results.response ) {
20
 
21
  $( '#itsec-network-brute-force-reset-status' ).html( '' );
22
 
23
+ itsecUtil.sendModuleAJAXRequest( 'network-brute-force', data, function( results ) {
24
  $( '#itsec-network-brute-force-reset-status' ).html( '' );
25
 
26
  if ( true !== results.response ) {
core/modules/ipcheck/logs.php ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_IPCheck_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_ipcheck_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ add_filter( 'itsec_logs_prepare_ipcheck_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ }
8
+
9
+ public function filter_entry_for_list_display( $entry ) {
10
+ $entry['module_display'] = esc_html__( 'Network Brute Force', 'better-wp-security' );
11
+
12
+ if ( 'ip-blocked' === $entry['code'] ) {
13
+ $entry['description'] = esc_html__( 'IP Blocked', 'better-wp-security' );
14
+ } else if ( 'successful-login-by-blocked-ip' === $entry['code'] ) {
15
+ $entry['description'] = esc_html__( 'Blocked Host Attempted Login With Good Credentials', 'better-wp-security' );
16
+ } else if ( 'failed-login-by-blocked-ip' === $entry['code'] ) {
17
+ $entry['description'] = esc_html__( 'Blocked Host Attempted Login', 'better-wp-security' );
18
+ }
19
+
20
+ return $entry;
21
+ }
22
+
23
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
24
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
25
+
26
+ $details['module']['content'] = $entry['module_display'];
27
+ $details['description']['content'] = $entry['description'];
28
+
29
+ if ( isset( $entry['data']['expires_gmt'] ) ) {
30
+ $timestamp = strtotime( $entry['data']['expires_gmt'] );
31
+ $datetime = date( 'Y-m-d H:i:s', $timestamp + ITSEC_Core::get_time_offset() );
32
+
33
+ $details['expiration'] = array(
34
+ 'header' => esc_html__( 'Block Expiration', 'better-wp-security' ),
35
+ 'content' => $datetime,
36
+ );
37
+ }
38
+
39
+ if ( isset( $entry['data']['details'] ) && isset( $entry['data']['details']['source'] ) ) {
40
+ if ( 'xmlrpc' === $entry['data']['details']['source'] ) {
41
+ $source = esc_html__( 'XMLRPC Authentication', 'better-wp-security' );
42
+ } else if ( 'rest_api' === $entry['data']['details']['source'] ) {
43
+ $source = esc_html__( 'REST API Authentication', 'better-wp-security' );
44
+ } else {
45
+ $source = esc_html__( 'Login Page', 'better-wp-security' );
46
+ }
47
+
48
+ $details['source'] = array(
49
+ 'header' => esc_html__( 'Login Source' ),
50
+ 'content' => $source,
51
+ );
52
+ }
53
+
54
+ return $details;
55
+ }
56
+ }
57
+ new ITSEC_IPCheck_Logs();
core/modules/malware/class-itsec-malware-log.php DELETED
@@ -1,340 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Log malware scan reports
5
- *
6
- * @package iThemes-Security
7
- * @subpackage Intrusion-Detection
8
- * @since 4.4
9
- */
10
- final class ITSEC_Malware_Log extends ITSEC_WP_List_Table {
11
-
12
- function __construct() {
13
-
14
- parent::__construct(
15
- array(
16
- 'singular' => 'itsec_malware_log_item',
17
- 'plural' => 'itsec_malware_log_items',
18
- 'ajax' => true
19
- )
20
- );
21
-
22
- }
23
-
24
- /**
25
- * Define first time column
26
- *
27
- * @param array $item array of row data
28
- *
29
- * @return string formatted output
30
- *
31
- **/
32
- function column_details( $item ) {
33
-
34
- $content = '';
35
-
36
- if ( ! empty( $item['results'] ) && in_array( $item['results'], array( 'clean', 'warn', 'error' ) ) ) {
37
- // Results for Sucuri scans.
38
-
39
- require_once( dirname( __FILE__ ) . '/class-itsec-malware-scan-results-template.php' );
40
-
41
- $content .= '<a href="itsec-log-malware-row-' . $item['count'] . '" class="dialog">' . __( 'Details', 'better-wp-security' ) . '</a>';
42
- $content .= '<div id="itsec-log-malware-row-' . $item['count'] . '" style="display:none;">';
43
- $content .= "<div class='itsec-malware-scan-results-wrapper'>\n";
44
-
45
- $content .= ITSEC_Malware_Scan_Results_Template::get_html( $item['data'], true );
46
-
47
- $content .= "</div>\n";
48
- $content .= "</div>\n";
49
- } else if ( isset( $item['report'] ) ) {
50
- // Results for legacy VirusTotal scans.
51
-
52
- $content .= '<a href="itsec-log-malware-row-' . $item['count'] . '" class="dialog">' . __( 'Details', 'better-wp-security' ) . '</a>';
53
-
54
- $content .= '<div id="itsec-log-malware-row-' . $item['count'] . '" style="display:none;">';
55
-
56
- if ( isset( $item['report']['resource'] ) ) {
57
- $content .= '<strong>' . __( 'Resource Scanned', 'better-wp-security' ) . ':</strong> ' . $item['report']['resource'] . '<br />';
58
- }
59
-
60
- if ( isset( $item['report']['results'] ) ) {
61
-
62
- if ( isset( $item['report']['results']['total'] ) ) {
63
- $content .= '<strong>' . __( 'Total Scans', 'better-wp-security' ) . ':</strong> ' . $item['report']['results']['total'] . '<br />';
64
- }
65
-
66
- if ( isset( $item['report']['results']['positives'] ) ) {
67
- $content .= '<strong>' . __( 'Problems Found', 'better-wp-security' ) . ':</strong> ' . $item['report']['results']['positives'] . '<br />';
68
- }
69
-
70
- if ( isset( $item['report']['results']['scans'] ) ) {
71
-
72
- $content .= '<h3>' . __( 'Scan Details', 'better-wp-security' ) . '</h3>';
73
-
74
- $content .= '<ol class="malware_detail_list">';
75
-
76
- foreach ( $item['report']['results']['scans'] as $service => $results ) {
77
- $content .= '<li class="malware_detail"><strong>' . $service . '</strong>: ' . $results['result'] . ' ' . $results['detected'] . '<br /></li>';
78
- }
79
-
80
- $content .= '</ol>';
81
-
82
- }
83
-
84
- }
85
-
86
- $content .= '</div>';
87
-
88
- }
89
-
90
- return $content;
91
-
92
- }
93
-
94
- /**
95
- * Define results column
96
- *
97
- * @param array $item array of row data
98
- *
99
- * @return string formatted output
100
- *
101
- **/
102
- function column_results( $item ) {
103
-
104
- $content = '';
105
-
106
- if ( ! empty( $item['results'] ) && in_array( $item['results'], array( 'clean', 'warn', 'error' ) ) ) {
107
- // Results for Sucuri scans.
108
- if ( 'error' === $item['results'] ) {
109
- $content = '<span style="color: red">' . __( 'Error', 'better-wp-security' ) . '</span>';
110
- } else if ( 'warn' === $item['results'] ) {
111
- $content = '<span style="color: red">' . __( 'Warning', 'better-wp-security' ) . '</span>';
112
- } else {
113
- $content = '<span style="color: green">' . __( 'Clean', 'better-wp-security' ) . '</span>';
114
- }
115
- } else if ( isset( $item['report']['results']['positives'] ) ) {
116
- // Results for legacy VirusTotal scans.
117
-
118
- if ( $item['report']['results']['positives'] == 0 ) {
119
- $content = '<span style="color: green">' . __( 'Clean', 'better-wp-security' ) . '</span>';
120
- } else {
121
- $content = '<span style="color: red">' . __( 'Issues Detected', 'better-wp-security' ) . '</span>';
122
- }
123
-
124
- }
125
-
126
- return $content;
127
-
128
- }
129
-
130
- /**
131
- * Define time column
132
- *
133
- * @param array $item array of row data
134
- *
135
- * @return string formatted output
136
- *
137
- **/
138
- function column_time( $item ) {
139
-
140
- return $item['time'];
141
-
142
- }
143
-
144
- /**
145
- * Define count column
146
- *
147
- * @param array $item array of row data
148
- *
149
- * @return string formatted output
150
- *
151
- **/
152
- function column_user( $item ) {
153
-
154
- $user = get_user_by( 'login', $item['user'] );
155
-
156
- if ( $user !== false ) {
157
-
158
- return '<a href="/wp-admin/user-edit.php?user_id=' . $user->ID . '">' . $item['user'] . '</a>';
159
-
160
- } else {
161
-
162
- return $item['user'];
163
-
164
- }
165
-
166
- }
167
-
168
- /**
169
- * Define uri column
170
- *
171
- * @param array $item array of row data
172
- *
173
- * @return string formatted output
174
- *
175
- **/
176
- function column_action( $item ) {
177
-
178
- return $item['action'];
179
-
180
- }
181
-
182
- /**
183
- * Define Columns
184
- *
185
- * @return array array of column titles
186
- */
187
- public function get_columns() {
188
-
189
- return array(
190
- 'time' => __( 'Time', 'better-wp-security' ),
191
- 'action' => __( 'Action', 'better-wp-security' ),
192
- 'results' => __( 'Results', 'better-wp-security' ),
193
- 'user' => __( 'User', 'better-wp-security' ),
194
- 'details' => __( 'Details', 'better-wp-security' ),
195
- );
196
-
197
- }
198
-
199
- /**
200
- * Prepare data for table
201
- *
202
- * @return void
203
- */
204
- public function prepare_items() {
205
- global $itsec_logger, $wpdb;
206
-
207
- $columns = $this->get_columns();
208
- $hidden = array();
209
- $this->_column_headers = array( $columns, $hidden, false );
210
- $per_page = 20;
211
- $current_page = $this->get_pagenum();
212
- $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM `" . $wpdb->base_prefix . "itsec_log` WHERE `log_type`='malware'" );
213
-
214
- $items = $itsec_logger->get_events( 'malware', array(), $per_page, ( ( $current_page - 1 ) * $per_page ), 'log_date' );
215
-
216
- $table_data = array();
217
-
218
- $count = 0;
219
-
220
- foreach ( $items as $item ) { //loop through and group 404s
221
-
222
- $log_data = maybe_unserialize( $item['log_data'] );
223
-
224
- $table_data[$count]['time'] = sanitize_text_field( $item['log_date'] );
225
- $table_data[$count]['host'] = sanitize_text_field( $item['log_host'] );
226
-
227
- if ( strlen( trim( sanitize_text_field( $item['log_username'] ) ) ) > 0 ) {
228
-
229
- $table_data[$count]['user'] = sanitize_text_field( $item['log_username'] );
230
-
231
- } elseif ( intval( $item['log_user'] ) > 0 && ITSEC_Lib::user_id_exists( $item['log_user'] ) ) {
232
-
233
- $user = get_user_by( 'id', $item['log_user'] );
234
-
235
- $table_data[$count]['user'] = $user->data->user_login;
236
-
237
- } else {
238
-
239
- $table_data[$count]['user'] = '';
240
-
241
- }
242
-
243
- $table_data[$count]['action'] = ( is_array( $log_data ) && isset( $log_data['type'] ) ) ? sanitize_text_field( $log_data['type'] ) : __( 'Malware Scan Report', 'better-wp-security' );
244
-
245
- if ( is_wp_error( $log_data ) ) {
246
- $table_data[$count]['results'] = 'error';
247
- $table_data[$count]['data'] = $log_data;
248
- } else if ( isset( $log_data['SCAN']['SITE'] ) ) {
249
- // New log data from Sucuri scan.
250
-
251
- if (
252
- empty( $log_data['SYSTEM']['WARN'] ) &&
253
- empty( $log_data['MALWARE']['WARN'] ) &&
254
- empty( $log_data['BLACKLIST']['WARN'] )
255
- ) {
256
- $table_data[$count]['results'] = 'clean';
257
- } else {
258
- $table_data[$count]['results'] = 'warn';
259
- }
260
-
261
- $table_data[$count]['data'] = $log_data;
262
- } else {
263
- // Legacy log data from VirusTotal scan.
264
-
265
- if ( isset( $log_data['resource'] ) ) {
266
-
267
- $table_data[$count]['report'] = array(
268
- 'resource' => $log_data['resource'],
269
- );
270
-
271
- } else {
272
-
273
- $table_data[$count]['report'] = array();
274
-
275
- }
276
-
277
- if ( isset( $log_data['report'] ) ) {
278
- $table_data[$count]['report']['results'] = $log_data['report'];
279
- }
280
-
281
- }
282
-
283
- $table_data[$count]['count'] = $count;
284
-
285
- $count ++;
286
-
287
- }
288
-
289
- // usort( $table_data, array( $this, 'sortrows' ) );
290
-
291
- $this->items = $table_data;
292
-
293
- $this->set_pagination_args(
294
- array(
295
- 'total_items' => $total_items,
296
- 'per_page' => $per_page,
297
- 'total_pages' => ceil( $total_items / $per_page )
298
- )
299
- );
300
- }
301
-
302
- /**
303
- * Sorts rows by count in descending order
304
- *
305
- * @param array $a first array to compare
306
- * @param array $b second array to compare
307
- *
308
- * @return int comparison result
309
- */
310
- function sortrows( $a, $b ) {
311
-
312
- // If no sort, default to count
313
- $orderby = ( ! empty( $_GET['orderby'] ) ) ? esc_attr( $_GET['orderby'] ) : 'time';
314
-
315
- // If no order, default to desc
316
- $order = ( ! empty( $_GET['order'] ) ) ? esc_attr( $_GET['order'] ) : 'desc';
317
-
318
- if ( $orderby == 'count' ) {
319
-
320
- if ( intval( $a[$orderby] ) < intval( $b[$orderby] ) ) {
321
- $result = - 1;
322
- } elseif ( intval( $a[$orderby] ) === intval( $b[$orderby] ) ) {
323
- $result = 0;
324
- } else {
325
- $result = 1;
326
- }
327
-
328
- } else {
329
-
330
- // Determine sort order
331
- $result = strcmp( $a[$orderby], $b[$orderby] );
332
-
333
- }
334
-
335
- // Send final sort direction to usort
336
- return ( $order === 'asc' ) ? $result : - $result;
337
-
338
- }
339
-
340
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/malware/class-itsec-malware-scan-results-template.php CHANGED
@@ -2,8 +2,21 @@
2
 
3
  class ITSEC_Malware_Scan_Results_Template {
4
  public static function get_html( $results, $show_error_details = false ) {
 
 
 
 
 
 
 
 
 
5
  $html = "<div class='itsec-malware-scan-results'>\n";
6
 
 
 
 
 
7
  if ( is_wp_error( $results ) ) {
8
  $html .= self::get_wp_error_details( $results, $show_error_details );
9
  } else {
2
 
3
  class ITSEC_Malware_Scan_Results_Template {
4
  public static function get_html( $results, $show_error_details = false ) {
5
+
6
+ if ( is_wp_error( $results ) && ( $edata = $results->get_error_data() ) && ! empty( $edata['itsec_site'] ) ) {
7
+ $site_url = $edata['itsec_site']['url'];
8
+ } elseif ( is_array( $results ) && ! empty( $results['itsec_site'] ) ) {
9
+ $site_url = $results['itsec_site']['url'];
10
+ } else {
11
+ $site_url = '';
12
+ }
13
+
14
  $html = "<div class='itsec-malware-scan-results'>\n";
15
 
16
+ if ( $site_url ) {
17
+ $html .= '<h4>' . sprintf( esc_html__( 'Site: %s', 'better-wp-security' ), $site_url ) . '</h4>';
18
+ }
19
+
20
  if ( is_wp_error( $results ) ) {
21
  $html .= self::get_wp_error_details( $results, $show_error_details );
22
  } else {
core/modules/malware/class-itsec-malware-scanner.php CHANGED
@@ -3,21 +3,40 @@
3
  final class ITSEC_Malware_Scanner {
4
  protected static $transient_name = 'itsec_cached_sucuri_scan';
5
 
6
- public static function scan() {
7
-
8
- /** @var ITSEC_Logger $itsec_logger */
9
- global $itsec_logger;
10
 
 
 
 
 
 
11
 
12
- $results = self::get_scan_results();
13
 
14
- if ( is_array( $results ) && isset( $results['cached'] ) && $results['cached'] ) {
15
  return $results;
16
  }
17
 
18
-
19
- $user = wp_get_current_user();
20
- $itsec_logger->log_event( 'malware', 3, $results, ITSEC_Lib::get_ip(), $user->user_login, $user->ID );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  return $results;
23
  }
@@ -36,6 +55,10 @@ final class ITSEC_Malware_Scanner {
36
 
37
  $code = $results->get_error_code();
38
 
 
 
 
 
39
  // Networking error probably due to a server issue.
40
  if ( strpos( $code, 'itsec' ) === false ) {
41
  return false;
@@ -55,74 +78,165 @@ final class ITSEC_Malware_Scanner {
55
  return true;
56
  }
57
 
58
- protected static function get_scan_results() {
 
 
 
 
 
 
 
 
 
59
 
60
  $response = get_site_transient( self::$transient_name );
61
  $cached = true;
62
 
 
 
63
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE' ) && ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE ) {
 
64
  $cached = false;
65
  $response = false;
66
  }
67
 
68
-
69
  if ( false === $response ) {
70
  $cached = false;
71
 
72
- $scanner_url = 'https://sitecheck.sucuri.net/';
73
  $site_url = apply_filters( 'itsec_test_malware_scan_site_url', get_site_url() );
74
 
75
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SITE_URL' ) ) {
 
76
  $site_url = ITSEC_TEST_MALWARE_SCAN_SITE_URL;
77
  }
78
 
79
- $site_url = preg_replace( '|^https?://|i', '', $site_url );
80
-
81
- $query_args = array(
82
- 'scan' => $site_url,
83
- 'p' => 'ithemes',
84
- 'clear' => 1,
85
- 'json' => 1,
86
- 'time' => time(),
87
- );
88
 
89
- $key = apply_filters( 'itsec_sucuri_key', '' );
90
-
91
- if ( defined( 'ITSEC_SUCURI_KEY' ) ) {
92
- $key = ITSEC_SUCURI_KEY;
93
- }
94
-
95
- if ( ! empty( $key ) ) {
96
- $query_args['k'] = $key;
97
  }
 
 
 
98
 
99
- $scan_url = "$scanner_url?" . http_build_query( $query_args, '', '&' );
100
 
101
- $req_args = array(
102
- 'connect_timeout' => 30, // Placeholder for when WordPress implements support.
103
- 'timeout' => 300,
104
- );
105
 
106
- if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
107
- $req_args['sslverify'] = false;
108
 
109
- // Ensure that another plugin isn't preventing the disabling of sslverify from working.
110
- add_filter( 'https_local_ssl_verify', '__return_false', 999999 );
111
- add_filter( 'https_ssl_verify', '__return_false', 999999 );
112
- }
 
 
 
 
 
 
 
113
 
114
- $response = wp_remote_get( $scan_url, $req_args );
 
 
 
 
115
 
116
- if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
117
- remove_filter( 'https_local_ssl_verify', '__return_false', 999999 );
118
- remove_filter( 'https_ssl_verify', '__return_false', 999999 );
119
- }
120
 
121
  if ( is_wp_error( $response ) ) {
122
  return $response;
123
  }
 
 
 
 
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  if ( isset( $response['body'] ) ) {
128
  $body = $response['body'];
@@ -136,14 +250,14 @@ final class ITSEC_Malware_Scanner {
136
 
137
  $body = @json_decode( $body, true );
138
 
139
- if ( is_null( $body ) && isset( $response['headers'], $response['headers']['content-type'] ) ) {
140
  if ( 'application/json' === $response['headers']['content-type'] ) {
141
  return new WP_Error( 'itsec-malware-scanner-invalid-json-data-in-scan-response', __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The response indicates that the encoding is JSON, but the data could not be decoded. This problem could be due to a temporary Sucuri server issue or a compatibility issue on your server. If the problem continues, please contact iThemes Security support.', 'better-wp-security' ), $response );
142
  } else {
143
  return new WP_Error( 'itsec-malware-scanner-invalid-content-type-in-scan-response', sprintf( __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The data received from the Sucuri server could not be decoded. In addition, a content type of <code>%s</code> was received when a content type of <code>application/json</code> was expected. This could indicate a temporary issue with the Sucuri servers.', 'better-wp-security' ), esc_html( $response['headers']['content-type'] ) ), $response );
144
  }
145
  } else if ( ! is_array( $body ) ) {
146
- if ( 'ERROR' === substr( $response['body'], 0, 5 ) ) {
147
  return new WP_Error( 'itsec-malware-scanner-error-received', sprintf( __( 'The scan did not complete successfully. Sucuri sent the following error: %s', 'better-wp-security' ), '<code>' . $response['body'] . '</code>' ), $response );
148
  }
149
 
@@ -154,15 +268,6 @@ final class ITSEC_Malware_Scanner {
154
  return new WP_Error( 'itsec-malware-scanner-wp-remote-get-response-malformed', __( 'The scan failed due to an unexpected technical error. The response from the wp_remote_get function is missing some critical information that is needed in order to properly process the response from Sucuri\'s servers. This could indicate a plugin/theme compatibility issue or a problem in WordPress.', 'better-wp-security' ), $response );
155
  }
156
 
157
-
158
- if ( ! $cached ) {
159
- set_site_transient( self::$transient_name, $response, 10 * MINUTE_IN_SECONDS );
160
- }
161
-
162
- if ( is_array( $body ) ) {
163
- $body['cached'] = $cached;
164
- }
165
-
166
  return $body;
167
  }
168
  }
3
  final class ITSEC_Malware_Scanner {
4
  protected static $transient_name = 'itsec_cached_sucuri_scan';
5
 
6
+ public static function scan( $site_id = 0 ) {
7
+ $process_id = ITSEC_Log::add_process_start( 'malware', 'scan', compact( 'site_id' ) );
 
 
8
 
9
+ if ( $site_id && ! is_main_site( $site_id ) ) {
10
+ $results = self::scan_sub_site( $site_id, $process_id );
11
+ } else {
12
+ $results = self::scan_main_site( $process_id );
13
+ }
14
 
15
+ ITSEC_Log::add_process_stop( $process_id, compact( 'results' ) );
16
 
17
+ if ( is_array( $results ) && ! empty( $results['cached'] ) ) {
18
  return $results;
19
  }
20
 
21
+ if ( is_wp_error( $results ) ) {
22
+ if ( self::is_sucuri_error( $results ) ) {
23
+ ITSEC_Log::add_warning( 'malware', 'scan-failure-server-error', compact( 'results' ) );
24
+ } else {
25
+ ITSEC_Log::add_warning( 'malware', 'scan-failure-client-error', compact( 'results' ) );
26
+ }
27
+ } else if ( ! empty( $results['SYSTEM']['ERROR'] ) ) {
28
+ ITSEC_Log::add_warning( 'malware', 'sucuri-system-error', compact( 'results' ) );
29
+ } else if ( ! empty( $results['MALWARE']['WARN'] ) ) {
30
+ if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
31
+ ITSEC_Log::add_critical_issue( 'malware', 'found-malware-and-on-blacklist', compact( 'results' ) );
32
+ } else {
33
+ ITSEC_Log::add_critical_issue( 'malware', 'found-malware', compact( 'results' ) );
34
+ }
35
+ } else if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
36
+ ITSEC_Log::add_critical_issue( 'malware', 'on-blacklist', compact( 'results' ) );
37
+ } else {
38
+ ITSEC_Log::add_notice( 'malware', 'clean', compact( 'results' ) );
39
+ }
40
 
41
  return $results;
42
  }
55
 
56
  $code = $results->get_error_code();
57
 
58
+ if ( 'http_request_failed' === $code && strpos( $results->get_error_message(), 'cURL error 52:' ) !== false ) {
59
+ return true;
60
+ }
61
+
62
  // Networking error probably due to a server issue.
63
  if ( strpos( $code, 'itsec' ) === false ) {
64
  return false;
78
  return true;
79
  }
80
 
81
+ /**
82
+ * Scan the main site of the network.
83
+ *
84
+ * This handles caching and filter overrides for URLs.
85
+ *
86
+ * @param array $process_id
87
+ *
88
+ * @return array|WP_Error
89
+ */
90
+ protected static function scan_main_site( $process_id ) {
91
 
92
  $response = get_site_transient( self::$transient_name );
93
  $cached = true;
94
 
95
+ ITSEC_Log::add_process_update( $process_id, array( 'cached-response' => $response ) );
96
+
97
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE' ) && ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE ) {
98
+ ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-skip-cache', 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE ) );
99
  $cached = false;
100
  $response = false;
101
  }
102
 
 
103
  if ( false === $response ) {
104
  $cached = false;
105
 
 
106
  $site_url = apply_filters( 'itsec_test_malware_scan_site_url', get_site_url() );
107
 
108
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SITE_URL' ) ) {
109
+ ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-force-site-url', 'original-site-url' => $site_url, 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SITE_URL', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SITE_URL ) );
110
  $site_url = ITSEC_TEST_MALWARE_SCAN_SITE_URL;
111
  }
112
 
113
+ $response = self::scan_url( $site_url, $process_id );
 
 
 
 
 
 
 
 
114
 
115
+ if ( ! is_wp_error( $response ) ) {
116
+ set_site_transient( self::$transient_name, array(
117
+ 'body' => $response['body'],
118
+ 'headers' => $response['headers'],
119
+ 'response' => $response['response'],
120
+ ), MINUTE_IN_SECONDS * 10 );
 
 
121
  }
122
+ } else {
123
+ ITSEC_Log::add_process_update( $process_id, array( 'action' => 'using-cached-response' ) );
124
+ }
125
 
126
+ $results = self::parse_response( $response );
127
 
128
+ if ( ! is_wp_error( $results ) ) {
129
+ $results['cached'] = $cached;
130
+ }
 
131
 
132
+ return $results;
133
+ }
134
 
135
+ /**
136
+ * Scan a sub site.
137
+ *
138
+ * These requests are not cached.
139
+ *
140
+ * @param int $site_id
141
+ * @param array $process_id
142
+ *
143
+ * @return array|WP_Error
144
+ */
145
+ protected static function scan_sub_site( $site_id, $process_id ) {
146
 
147
+ $url = get_site_url( $site_id );
148
+ $record = array(
149
+ 'url' => $url,
150
+ 'id' => $site_id,
151
+ );
152
 
153
+ if ( $url ) {
154
+ $response = self::scan_url( $url, $process_id );
 
 
155
 
156
  if ( is_wp_error( $response ) ) {
157
  return $response;
158
  }
159
+
160
+ $results = self::parse_response( $response );
161
+ } else {
162
+ $results = new WP_Error( 'itsec-malware-scanner-invalid-site', sprintf( __( 'Failed to scan site #%d because it does not exist.', 'better-wp-security' ), $site_id ) );
163
  }
164
 
165
+ if ( is_wp_error( $results ) ) {
166
+ $results->add_data( array_merge( $results->get_error_data(), array( 'itsec_site' => $record ) ) );
167
+ } else {
168
+ $results['itsec_site'] = $record;
169
+ }
170
+
171
+ return $results;
172
+ }
173
+
174
+ /**
175
+ * Request a Sucuri Malware Scan for a URL.
176
+ *
177
+ * @param string $site_url
178
+ * @param array $process_id
179
+ *
180
+ * @return array|WP_Error
181
+ */
182
+ protected static function scan_url( $site_url, $process_id ) {
183
+
184
+ $site_url = preg_replace( '|^https?://|i', '', $site_url );
185
+
186
+ $query_args = array(
187
+ 'scan' => $site_url,
188
+ 'p' => 'ithemes',
189
+ 'clear' => 1,
190
+ 'json' => 1,
191
+ 'time' => time(),
192
+ );
193
+
194
+ $key = apply_filters( 'itsec_sucuri_key', '' );
195
+
196
+ if ( defined( 'ITSEC_SUCURI_KEY' ) ) {
197
+ $key = ITSEC_SUCURI_KEY;
198
+ }
199
+
200
+ if ( ! empty( $key ) ) {
201
+ $query_args['k'] = $key;
202
+ }
203
+
204
+ $scanner_url = 'https://sitecheck.sucuri.net/';
205
+ $scan_url = "$scanner_url?" . http_build_query( $query_args, '', '&' );
206
+
207
+ $req_args = array(
208
+ 'connect_timeout' => 30, // Placeholder for when WordPress implements support.
209
+ 'timeout' => 300,
210
+ );
211
+
212
+ if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
213
+ $req_args['sslverify'] = false;
214
+
215
+ // Ensure that another plugin isn't preventing the disabling of sslverify from working.
216
+ add_filter( 'https_local_ssl_verify', '__return_false', 999999 );
217
+ add_filter( 'https_ssl_verify', '__return_false', 999999 );
218
+ }
219
+
220
+ $response = wp_remote_get( $scan_url, $req_args );
221
+
222
+ if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
223
+ remove_filter( 'https_local_ssl_verify', '__return_false', 999999 );
224
+ remove_filter( 'https_ssl_verify', '__return_false', 999999 );
225
+ }
226
+
227
+ ITSEC_Log::add_process_update( $process_id, compact( 'scan_url', 'req_args', 'response' ) );
228
+
229
+ return $response;
230
+ }
231
+
232
+ /**
233
+ * Parse the response from Sucuri to get either a WP_Error instance or the scan results.
234
+ *
235
+ * @param array $response
236
+ *
237
+ * @return array|WP_Error
238
+ */
239
+ protected static function parse_response( $response ) {
240
 
241
  if ( isset( $response['body'] ) ) {
242
  $body = $response['body'];
250
 
251
  $body = @json_decode( $body, true );
252
 
253
+ if ( is_null( $body ) && isset( $response['headers']['content-type'] ) ) {
254
  if ( 'application/json' === $response['headers']['content-type'] ) {
255
  return new WP_Error( 'itsec-malware-scanner-invalid-json-data-in-scan-response', __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The response indicates that the encoding is JSON, but the data could not be decoded. This problem could be due to a temporary Sucuri server issue or a compatibility issue on your server. If the problem continues, please contact iThemes Security support.', 'better-wp-security' ), $response );
256
  } else {
257
  return new WP_Error( 'itsec-malware-scanner-invalid-content-type-in-scan-response', sprintf( __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The data received from the Sucuri server could not be decoded. In addition, a content type of <code>%s</code> was received when a content type of <code>application/json</code> was expected. This could indicate a temporary issue with the Sucuri servers.', 'better-wp-security' ), esc_html( $response['headers']['content-type'] ) ), $response );
258
  }
259
  } else if ( ! is_array( $body ) ) {
260
+ if ( 0 === strpos( $response['body'], 'ERROR' ) ) {
261
  return new WP_Error( 'itsec-malware-scanner-error-received', sprintf( __( 'The scan did not complete successfully. Sucuri sent the following error: %s', 'better-wp-security' ), '<code>' . $response['body'] . '</code>' ), $response );
262
  }
263
 
268
  return new WP_Error( 'itsec-malware-scanner-wp-remote-get-response-malformed', __( 'The scan failed due to an unexpected technical error. The response from the wp_remote_get function is missing some critical information that is needed in order to properly process the response from Sucuri\'s servers. This could indicate a plugin/theme compatibility issue or a problem in WordPress.', 'better-wp-security' ), $response );
269
  }
270
 
 
 
 
 
 
 
 
 
 
271
  return $body;
272
  }
273
  }
core/modules/malware/class-itsec-malware.php CHANGED
@@ -5,10 +5,6 @@ class ITSEC_Malware {
5
  function run() {
6
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
7
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
8
-
9
- add_filter( 'itsec_logger_displays', array( $this, 'itsec_logger_displays' ) );
10
- add_filter( 'itsec_logger_modules', array( $this, 'itsec_logger_modules' ) );
11
- add_filter( 'itsec_logger_filter_malware_data_column_details', array( $this, 'filter_logger_data_column_details' ), 10, 2 );
12
  }
13
 
14
  /**
@@ -38,74 +34,4 @@ class ITSEC_Malware {
38
  return $verbs;
39
  }
40
 
41
- /**
42
- * Register malware for logger
43
- *
44
- * @since 4.4
45
- *
46
- * @param array $logger_modules array of logger modules
47
- *
48
- * @return array array of logger modules
49
- */
50
- public function itsec_logger_modules( $logger_modules ) {
51
- $logger_modules['malware'] = array(
52
- 'type' => 'malware',
53
- 'function' => __( 'Malware Scan', 'better-wp-security' ),
54
- );
55
-
56
- return $logger_modules;
57
-
58
- }
59
-
60
- /**
61
- * Array of metaboxes for the logs screen
62
- *
63
- * @since 4.4
64
- *
65
- * @param array $displays metabox array
66
- *
67
- * @return array metabox array
68
- */
69
- public function itsec_logger_displays( $displays ) {
70
- $displays[] = array(
71
- 'module' => 'malware',
72
- 'title' => __( 'Malware Scan', 'better-wp-security' ),
73
- 'callback' => array( $this, 'logs_metabox_content' )
74
- );
75
-
76
- return $displays;
77
- }
78
-
79
- /**
80
- * Render the settings metabox
81
- *
82
- * @return void
83
- */
84
- public function logs_metabox_content() {
85
-
86
- if ( ! class_exists( 'ITSEC_Malware_Log' ) ) {
87
- require( dirname( __FILE__ ) . '/class-itsec-malware-log.php' );
88
- }
89
-
90
- $log_display = new ITSEC_Malware_Log();
91
-
92
- $log_display->prepare_items();
93
- $log_display->display();
94
-
95
- }
96
-
97
- public function filter_logger_data_column_details( $details, $data ) {
98
- if ( is_wp_error( $data ) || ( ! empty( $data ) && ! empty( $data['SCAN']['SITE'] ) ) ) {
99
- // Results for Sucuri scans.
100
-
101
- require_once( dirname( __FILE__ ) . '/class-itsec-malware-scan-results-template.php' );
102
-
103
- $details = "<div class='itsec-malware-scan-results-wrapper'>\n";
104
- $details .= ITSEC_Malware_Scan_Results_Template::get_html( $data, true );
105
- $details .= "</div>\n";
106
- }
107
-
108
- return $details;
109
- }
110
-
111
  }
5
  function run() {
6
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
7
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
 
 
 
 
8
  }
9
 
10
  /**
34
  return $verbs;
35
  }
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
core/modules/malware/css/malware.css CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  .itsec-malware-scan-results ul {
2
  margin-left: 20px;
3
  }
1
+ .itsec-malware-scan-results h4 {
2
+ margin-top: 5px;
3
+ }
4
  .itsec-malware-scan-results ul {
5
  margin-left: 20px;
6
  }
core/modules/malware/css/settings.css CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  .itsec-malware-scan-results ul {
2
  margin-left: 20px;
3
  }
@@ -21,6 +25,9 @@
21
  .itsec-malware-scan-results-section:last-child {
22
  border-bottom-color: #ddd;
23
  }
 
 
 
24
  .itsec-malware-scan-clean,
25
  .itsec-malware-scan-warn,
26
  .itsec-malware-scan-error {
1
+ .itsec-malware-scan-results h4 {
2
+ margin-top: 5px;
3
+ }
4
+
5
  .itsec-malware-scan-results ul {
6
  margin-left: 20px;
7
  }
25
  .itsec-malware-scan-results-section:last-child {
26
  border-bottom-color: #ddd;
27
  }
28
+ .form-table td .itsec-malware-scan-results-section p {
29
+ margin: 1em 0;
30
+ }
31
  .itsec-malware-scan-clean,
32
  .itsec-malware-scan-warn,
33
  .itsec-malware-scan-error {
core/modules/malware/js/malware.js CHANGED
@@ -8,7 +8,7 @@
8
 
9
  bindEvents: function() {
10
  $('#itsec-malware-scan').on('click', this.startScan);
11
- $('.itsec-malware-scan-results-wrapper').on('click', '.itsec-malware-scan-toggle-details', this.toggleDetails);
12
  },
13
 
14
  toggleDetails: function( event ) {
8
 
9
  bindEvents: function() {
10
  $('#itsec-malware-scan').on('click', this.startScan);
11
+ $(document).on('click', '.itsec-malware-scan-results .itsec-malware-scan-toggle-details', this.toggleDetails);
12
  },
13
 
14
  toggleDetails: function( event ) {
core/modules/malware/js/settings-page.js CHANGED
@@ -5,18 +5,18 @@
5
  init: function() {
6
  this.bindEvents();
7
  },
8
-
9
  bindEvents: function() {
10
  $( document ).on( 'click', '#itsec-malware-scan-start', this.startScan );
11
  $( document ).on( 'click', '.itsec-malware-scan-results-wrapper .itsec-malware-scan-toggle-details', this.toggleDetails );
12
  },
13
-
14
  toggleDetails: function( e ) {
15
  e.preventDefault();
16
-
17
  var $container = $(this).parents( '.itsec-malware-scan-results-section' );
18
  var $details = $container.find( '.itsec-malware-scan-details' );
19
-
20
  if ( $details.is(':visible') ) {
21
  $(this).html( itsecMalwareScanData.showDetailsText );
22
  $details.hide();
@@ -25,33 +25,33 @@
25
  $details.show();
26
  }
27
  },
28
-
29
  startScan: function( e ) {
30
  e.preventDefault();
31
-
32
  itsecMalwareScanData.originalSubmitButtonText = $(this).val();
33
-
34
  $(this)
35
  .prop( 'disabled', true )
36
  .val( itsecMalwareScanData.clickedButtonText );
37
-
38
  var data = {
39
  'action': 'run-scan'
40
  };
41
-
42
- itsecSettingsPage.sendWidgetAJAXRequest( 'malware-scan', data, itsecMalwareScan.handleResponse );
43
  },
44
-
45
  handleResponse: function( results ) {
46
  $('#itsec-malware-scan-start').hide();
47
 
48
  if ( results.response && results.response.length ) {
49
  $('.itsec-malware-scan-results-wrapper').html( results.response );
50
  }
51
-
52
  if ( results.errors.length > 0 ) {
53
  var message;
54
-
55
  $.each( results.errors, function( index, error ) {
56
  message = '<div class="notice notice-error notice-alt"><p><strong>' + error + '</strong></p></div>';
57
  $('.itsec-malware-scan-results-wrapper').append( message );
@@ -66,7 +66,7 @@
66
  }
67
  },
68
  };
69
-
70
  $(document).ready(function() {
71
  itsecMalwareScan.init();
72
  });
5
  init: function() {
6
  this.bindEvents();
7
  },
8
+
9
  bindEvents: function() {
10
  $( document ).on( 'click', '#itsec-malware-scan-start', this.startScan );
11
  $( document ).on( 'click', '.itsec-malware-scan-results-wrapper .itsec-malware-scan-toggle-details', this.toggleDetails );
12
  },
13
+
14
  toggleDetails: function( e ) {
15
  e.preventDefault();
16
+
17
  var $container = $(this).parents( '.itsec-malware-scan-results-section' );
18
  var $details = $container.find( '.itsec-malware-scan-details' );
19
+
20
  if ( $details.is(':visible') ) {
21
  $(this).html( itsecMalwareScanData.showDetailsText );
22
  $details.hide();
25
  $details.show();
26
  }
27
  },
28
+
29
  startScan: function( e ) {
30
  e.preventDefault();
31
+
32
  itsecMalwareScanData.originalSubmitButtonText = $(this).val();
33
+
34
  $(this)
35
  .prop( 'disabled', true )
36
  .val( itsecMalwareScanData.clickedButtonText );
37
+
38
  var data = {
39
  'action': 'run-scan'
40
  };
41
+
42
+ itsecUtil.sendWidgetAJAXRequest( 'malware-scan', data, itsecMalwareScan.handleResponse );
43
  },
44
+
45
  handleResponse: function( results ) {
46
  $('#itsec-malware-scan-start').hide();
47
 
48
  if ( results.response && results.response.length ) {
49
  $('.itsec-malware-scan-results-wrapper').html( results.response );
50
  }
51
+
52
  if ( results.errors.length > 0 ) {
53
  var message;
54
+
55
  $.each( results.errors, function( index, error ) {
56
  message = '<div class="notice notice-error notice-alt"><p><strong>' + error + '</strong></p></div>';
57
  $('.itsec-malware-scan-results-wrapper').append( message );
66
  }
67
  },
68
  };
69
+
70
  $(document).ready(function() {
71
  itsecMalwareScan.init();
72
  });
core/modules/malware/logs.php ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Malware_Logs {
4
+ public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_malware_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
+ add_filter( 'itsec_logs_prepare_malware_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+
8
+ if ( did_action( 'admin_enqueue_scripts' ) ) {
9
+ $this->enqueue();
10
+ } else {
11
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
12
+ }
13
+ }
14
+
15
+ public function filter_entry_for_list_display( $entry ) {
16
+ $entry['module_display'] = esc_html__( 'Malware Scan', 'better-wp-security' );
17
+
18
+ if ( 'scan' === $entry['code'] && 'process-start' === $entry['type'] ) {
19
+ $entry['description'] = esc_html__( 'Scan Performance', 'better-wp-security' );
20
+ } else if ( 'clean' === $entry['code'] ) {
21
+ $entry['description'] = esc_html__( 'Clean', 'better-wp-security' );
22
+ } else if ( 'scan-failure-server-error' === $entry['code'] ) {
23
+ $entry['description'] = esc_html__( 'Scan Failed: Sucuri Error', 'better-wp-security' );
24
+ } else if ( 'scan-failure-client-error' === $entry['code'] ) {
25
+ $entry['description'] = esc_html__( 'Scan Failed: Site Error', 'better-wp-security' );
26
+ } else if ( 'sucuri-system-error' === $entry['code'] ) {
27
+ $entry['description'] = esc_html__( 'Scan Failed: Sucuri Error', 'better-wp-security' );
28
+ } else if ( 'found-malware-and-on-blacklist' === $entry['code'] ) {
29
+ $entry['description'] = esc_html__( 'Malware Found, Site on Blacklist', 'better-wp-security' );
30
+ } else if ( 'found-malware' === $entry['code'] ) {
31
+ $entry['description'] = esc_html__( 'Malware Found', 'better-wp-security' );
32
+ } else if ( 'on-blacklist' === $entry['code'] ) {
33
+ $entry['description'] = esc_html__( 'Site on Blacklist', 'better-wp-security' );
34
+ } else if ( 'scan' === $entry['code'] ) {
35
+ $entry['description'] = esc_html__( 'Scan', 'better-wp-security' );
36
+ }
37
+
38
+ return $entry;
39
+ }
40
+
41
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
42
+ require_once( dirname( __FILE__ ) . '/class-itsec-malware-scan-results-template.php' );
43
+
44
+
45
+ $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
46
+
47
+ $details['module']['content'] = $entry['module_display'];
48
+ $details['description']['content'] = $entry['description'];
49
+
50
+ if ( 'process-start' !== $entry['type'] ) {
51
+ $details['results'] = array(
52
+ 'header' => esc_html__( 'Results', 'better-wp-security' ),
53
+ 'content' => ITSEC_Malware_Scan_Results_Template::get_html( $entry['data']['results'], true ),
54
+ );
55
+ }
56
+
57
+ return $details;
58
+ }
59
+
60
+ public function enqueue() {
61
+ wp_enqueue_script( 'itsec-malware-scan-logs', plugin_dir_url( __FILE__ ) . 'js/malware.js', array( 'jquery' ), 3 );
62
+ }
63
+ }
64
+ new ITSEC_Malware_Logs();
core/modules/malware/sync-verbs/itsec-get-malware-scan-log.php CHANGED
@@ -6,27 +6,12 @@ class Ithemes_Sync_Verb_ITSEC_Get_Malware_Scan_Log extends Ithemes_Sync_Verb {
6
 
7
  public $default_arguments = array(
8
  'count' => 10,
 
9
  );
10
 
11
  public function run( $arguments ) {
12
  $arguments = Ithemes_Sync_Functions::merge_defaults( $arguments, $this->default_arguments );
13
 
14
- global $itsec_logger;
15
-
16
- $items = $itsec_logger->get_events( 'malware', array(), $arguments['count'] );
17
- $response = array();
18
-
19
- foreach ( $items as $item ) {
20
- $item['log_data'] = maybe_unserialize( $item['log_data'] );
21
-
22
- if ( ! is_array( $item['log_data'] ) || ! isset( $item['log_data']['SCAN']['SITE'] ) ) {
23
- // Don't return old scan data.
24
- continue;
25
- }
26
-
27
- $response[] = $item;
28
- }
29
-
30
- return $response;
31
  }
32
  }
6
 
7
  public $default_arguments = array(
8
  'count' => 10,
9
+ 'page' => 1,
10
  );
11
 
12
  public function run( $arguments ) {
13
  $arguments = Ithemes_Sync_Functions::merge_defaults( $arguments, $this->default_arguments );
14
 
15
+ return ITSEC_Log::get_entries( array( 'module' => 'malware' ), $arguments['count'], $arguments['page'], 'timestamp', 'DESC', 'all' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
  }
core/modules/notification-center/js/settings-page.js CHANGED
@@ -7,7 +7,7 @@ jQuery( function ( $ ) {
7
  $( document ).on( 'itsec-dismiss-notice', '.itsec-notification-center-mail-errors-container .notice.itsec-is-dismissible', function () {
8
  var errorId = $( this ).data( 'id' );
9
 
10
- itsecSettingsPage.sendModuleAJAXRequest( 'notification-center', { method: 'dismiss-mail-error', mail_error: errorId }, function ( r ) {
11
  if ( r.response && r.response.status === 'all-cleared' ) {
12
  jQuery( '#itsec-module-card-notification-center' ).removeClass( 'itsec-module-status--warning' );
13
  }
@@ -41,4 +41,4 @@ jQuery( function ( $ ) {
41
  initializeHiding();
42
  }
43
  } );
44
- } );
7
  $( document ).on( 'itsec-dismiss-notice', '.itsec-notification-center-mail-errors-container .notice.itsec-is-dismissible', function () {
8
  var errorId = $( this ).data( 'id' );
9
 
10
+ itsecUtil.sendModuleAJAXRequest( 'notification-center', { method: 'dismiss-mail-error', mail_error: errorId }, function ( r ) {
11
  if ( r.response && r.response.status === 'all-cleared' ) {
12
  jQuery( '#itsec-module-card-notification-center' ).removeClass( 'itsec-module-status--warning' );
13
  }
41
  initializeHiding();
42
  }
43
  } );
44
+ } );
core/modules/security-check/js/settings-page.js CHANGED
@@ -15,7 +15,7 @@ jQuery( document ).ready( function ( $ ) {
15
  'method': 'secure-site'
16
  };
17
 
18
- itsecSettingsPage.sendModuleAJAXRequest( 'security-check', data, function( results ) {
19
  $( '#itsec-security-check-secure_site' )
20
  .addClass( 'button-primary' )
21
  .removeClass( 'button-secondary' )
@@ -62,7 +62,7 @@ jQuery( document ).ready( function ( $ ) {
62
  .attr( 'value', $( this ).data( 'clicked-value' ) )
63
  }
64
 
65
- var ajaxFunction = itsecSettingsPage.sendModuleAJAXRequest;
66
 
67
  if ( 'undefined' !== typeof itsecSecurityCheckAJAXRequest ) {
68
  ajaxFunction = itsecSecurityCheckAJAXRequest;
@@ -108,6 +108,6 @@ jQuery( document ).ready( function ( $ ) {
108
  /*
109
  function itsecSecurityCheckAJAXRequest( type, data, callback ) {
110
  console.log( 'Override called' );
111
- itsecSettingsPage.sendModuleAJAXRequest( type, data, callback );
112
  }
113
  */
15
  'method': 'secure-site'
16
  };
17
 
18
+ itsecUtil.sendModuleAJAXRequest( 'security-check', data, function( results ) {
19
  $( '#itsec-security-check-secure_site' )
20
  .addClass( 'button-primary' )
21
  .removeClass( 'button-secondary' )
62
  .attr( 'value', $( this ).data( 'clicked-value' ) )
63
  }
64
 
65
+ var ajaxFunction = itsecUtil.sendModuleAJAXRequest;
66
 
67
  if ( 'undefined' !== typeof itsecSecurityCheckAJAXRequest ) {
68
  ajaxFunction = itsecSecurityCheckAJAXRequest;
108
  /*
109
  function itsecSecurityCheckAJAXRequest( type, data, callback ) {
110
  console.log( 'Override called' );
111
+ itsecUtil.sendModuleAJAXRequest( type, data, callback );
112
  }
113
  */
core/setup.php CHANGED
@@ -143,6 +143,10 @@ final class ITSEC_Setup {
143
  ITSEC_Lib::schedule_cron_test();
144
  }
145
 
 
 
 
 
146
  // Update stored build number.
147
  ITSEC_Modules::set_setting( 'global', 'build', ITSEC_Core::get_plugin_build() );
148
  }
@@ -184,8 +188,8 @@ final class ITSEC_Setup {
184
  }
185
 
186
  private static function uninstall() {
187
-
188
- global $wpdb;
189
 
190
  ITSEC_Modules::run_uninstall();
191
 
@@ -195,16 +199,8 @@ final class ITSEC_Setup {
195
  delete_site_option( 'itsec-storage' );
196
  delete_site_option( 'itsec_active_modules' );
197
 
198
- $wpdb->query( "DROP TABLE IF EXISTS " . $wpdb->base_prefix . "itsec_log;" );
199
- $wpdb->query( "DROP TABLE IF EXISTS " . $wpdb->base_prefix . "itsec_lockouts;" );
200
- $wpdb->query( "DROP TABLE IF EXISTS " . $wpdb->base_prefix . "itsec_temp;" );
201
-
202
- if ( is_dir( ITSEC_Core::get_storage_dir() ) ) {
203
- require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-directory.php' );
204
-
205
- ITSEC_Lib_Directory::remove( ITSEC_Core::get_storage_dir() );
206
- }
207
-
208
  ITSEC_Lib::clear_caches();
209
 
210
  }
143
  ITSEC_Lib::schedule_cron_test();
144
  }
145
 
146
+ if ( $build < 4081 ) {
147
+ ITSEC_Core::get_scheduler()->register_events();
148
+ }
149
+
150
  // Update stored build number.
151
  ITSEC_Modules::set_setting( 'global', 'build', ITSEC_Core::get_plugin_build() );
152
  }
188
  }
189
 
190
  private static function uninstall() {
191
+ require_once( ITSEC_Core::get_core_dir() . '/lib/schema.php' );
192
+ require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-directory.php' );
193
 
194
  ITSEC_Modules::run_uninstall();
195
 
199
  delete_site_option( 'itsec-storage' );
200
  delete_site_option( 'itsec_active_modules' );
201
 
202
+ ITSEC_Schema::remove_database_tables();
203
+ ITSEC_Lib_Directory::remove( ITSEC_Core::get_storage_dir() );
 
 
 
 
 
 
 
 
204
  ITSEC_Lib::clear_caches();
205
 
206
  }
history.txt CHANGED
@@ -717,3 +717,7 @@
717
  Bug Fix: Update to the REST API "Restricted Access" feature to protect against methods to work around the restricted access.
718
  Bug Fix: Prevent login page being hidden when following the "Confirm Email Address" notification URL.
719
  Bug Fix: Hide Backend notifications not being properly sent when first enabled.
 
 
 
 
717
  Bug Fix: Update to the REST API "Restricted Access" feature to protect against methods to work around the restricted access.
718
  Bug Fix: Prevent login page being hidden when following the "Confirm Email Address" notification URL.
719
  Bug Fix: Hide Backend notifications not being properly sent when first enabled.
720
+ 6.9.0 - 2018-02-12 - Chris Jean & Timothy Jacobs
721
+ Enhancement: Updated logging system to keep track of more information and have more options to filter and sort log entries.
722
+ Enhancement: Improved efficiency of File Change Detection scanning.
723
+ Bug Fix: Fixed issue that could register loading the logging page as a failed login attempt on some sites.
readme.txt CHANGED
@@ -2,8 +2,8 @@
2
  Contributors: ithemes, chrisjean, gerroald, mattdanner, timothyblynjacobs
3
  Tags: security, security plugin, malware, hack, secure, block, SSL, admin, htaccess, lockdown, login, protect, protection, anti virus, attack, injection, login security, maintenance, permissions, prevention, authentication, administration, password, brute force, ban, permissions, bots, user agents, xml rpc, security log
4
  Requires at least: 4.7
5
- Tested up to: 4.9.2
6
- Stable tag: 6.8.1
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -188,6 +188,11 @@ Free support may be available with the help of the community in the <a href="htt
188
 
189
  == Changelog ==
190
 
 
 
 
 
 
191
  = 6.8.1 =
192
  * Enhancement: Display user lockouts in Lockout Sidebar.
193
  * Bug Fix: Load translations on the plugins_loaded hook.
@@ -400,5 +405,5 @@ Free support may be available with the help of the community in the <a href="htt
400
 
401
  == Upgrade Notice ==
402
 
403
- = 6.8.1 =
404
- Version 6.8.1 contains important bug fixes and improvements. It is recommended for all users.
2
  Contributors: ithemes, chrisjean, gerroald, mattdanner, timothyblynjacobs
3
  Tags: security, security plugin, malware, hack, secure, block, SSL, admin, htaccess, lockdown, login, protect, protection, anti virus, attack, injection, login security, maintenance, permissions, prevention, authentication, administration, password, brute force, ban, permissions, bots, user agents, xml rpc, security log
4
  Requires at least: 4.7
5
+ Tested up to: 4.9.4
6
+ Stable tag: 6.9.0
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
188
 
189
  == Changelog ==
190
 
191
+ = 6.9.0 =
192
+ * Enhancement: Updated logging system to keep track of more information and have more options to filter and sort log entries.
193
+ * Enhancement: Improved efficiency of File Change Detection scanning.
194
+ * Bug Fix: Fixed issue that could register loading the logging page as a failed login attempt on some sites.
195
+
196
  = 6.8.1 =
197
  * Enhancement: Display user lockouts in Lockout Sidebar.
198
  * Bug Fix: Load translations on the plugins_loaded hook.
405
 
406
  == Upgrade Notice ==
407
 
408
+ = 6.9.0 =
409
+ Version 6.9.0 contains an improved logging system and bug fixes. It is recommended for all users.