diff --git a/classes/class-log.php b/classes/class-log.php index 3ba17fa69..93c11172b 100644 --- a/classes/class-log.php +++ b/classes/class-log.php @@ -163,6 +163,8 @@ function ( $var ) { * @return bool */ public function is_record_excluded( $connector, $context, $action, $user = null, $ip = null ) { + $exclude_record = false; + if ( is_null( $user ) ) { $user = wp_get_current_user(); } @@ -191,64 +193,25 @@ public function is_record_excluded( $connector, $context, $action, $user = null, $exclude_settings = isset( $this->plugin->settings->options['exclude_rules'] ) ? $this->plugin->settings->options['exclude_rules'] : array(); if ( is_multisite() && $this->plugin->is_network_activated() && ! is_network_admin() ) { - $multisite_options = (array) get_site_option( 'wp_stream_network', array() ); - $multisite_exclude_settings = isset( $multisite_options['exclude_rules'] ) ? $multisite_options['exclude_rules'] : array(); - - if ( ! empty( $multisite_exclude_settings ) ) { - foreach ( $multisite_exclude_settings['exclude_row'] as $key => $rule ) { - $exclude_settings['exclude_row'][] = $multisite_exclude_settings['exclude_row'][ $key ]; - $exclude_settings['author_or_role'][] = $multisite_exclude_settings['author_or_role'][ $key ]; - $exclude_settings['connector'][] = $multisite_exclude_settings['connector'][ $key ]; - $exclude_settings['context'][] = $multisite_exclude_settings['context'][ $key ]; - $exclude_settings['action'][] = $multisite_exclude_settings['action'][ $key ]; - $exclude_settings['ip_address'][] = $multisite_exclude_settings['ip_address'][ $key ]; - } - } + $multisite_options = (array) get_site_option( 'wp_stream_network', array() ); + $exclude_settings = isset( $multisite_options['exclude_rules'] ) ? $multisite_options['exclude_rules'] : array(); } - $exclude_record = false; + foreach ( $this->exclude_rules_by_rows( $exclude_settings ) as $exclude_rule ) { + $exclude = array( + 'connector' => ! empty( $exclude_rule['connector'] ) ? $exclude_rule['connector'] : null, + 'context' => ! empty( $exclude_rule['context'] ) ? $exclude_rule['context'] : null, + 'action' => ! empty( $exclude_rule['action'] ) ? $exclude_rule['action'] : null, + 'ip_address' => ! empty( $exclude_rule['ip_address'] ) ? $exclude_rule['ip_address'] : null, + 'author' => is_numeric( $exclude_rule['author_or_role'] ) ? absint( $exclude_rule['author_or_role'] ) : null, + 'role' => ( ! empty( $exclude_rule['author_or_role'] ) && ! is_numeric( $exclude_rule['author_or_role'] ) ) ? $exclude_rule['author_or_role'] : null, + ); - if ( isset( $exclude_settings['exclude_row'] ) && ! empty( $exclude_settings['exclude_row'] ) ) { - foreach ( $exclude_settings['exclude_row'] as $key => $value ) { - // Prepare values. - $author_or_role = isset( $exclude_settings['author_or_role'][ $key ] ) ? $exclude_settings['author_or_role'][ $key ] : ''; - $connector = isset( $exclude_settings['connector'][ $key ] ) ? $exclude_settings['connector'][ $key ] : ''; - $context = isset( $exclude_settings['context'][ $key ] ) ? $exclude_settings['context'][ $key ] : ''; - $action = isset( $exclude_settings['action'][ $key ] ) ? $exclude_settings['action'][ $key ] : ''; - $ip_address = isset( $exclude_settings['ip_address'][ $key ] ) ? $exclude_settings['ip_address'][ $key ] : ''; - - $exclude = array( - 'connector' => ! empty( $connector ) ? $connector : null, - 'context' => ! empty( $context ) ? $context : null, - 'action' => ! empty( $action ) ? $action : null, - 'ip_address' => ! empty( $ip_address ) ? $ip_address : null, - 'author' => is_numeric( $author_or_role ) ? absint( $author_or_role ) : null, - 'role' => ( ! empty( $author_or_role ) && ! is_numeric( $author_or_role ) ) ? $author_or_role : null, - ); - - $exclude_rules = array_filter( $exclude, 'strlen' ); - - if ( ! empty( $exclude_rules ) ) { - $matches_exclusion_rule = true; - - foreach ( $exclude_rules as $exclude_key => $exclude_value ) { - if ( 'ip_address' === $exclude_key ) { - $ip_addresses = explode( ',', $exclude_value ); - if ( ! in_array( $record['ip_address'], $ip_addresses, true ) ) { - $matches_exclusion_rule = false; - break; - } - } elseif ( $record[ $exclude_key ] !== $exclude_value ) { - $matches_exclusion_rule = false; - break; - } - } - - if ( $matches_exclusion_rule ) { - $exclude_record = true; - break; - } - } + $exclude_rules = array_filter( $exclude, 'strlen' ); + + if ( $this->record_matches_rules( $record, $exclude_rules ) ) { + $exclude_record = true; + break; } } @@ -265,6 +228,73 @@ public function is_record_excluded( $connector, $context, $action, $user = null, return apply_filters( 'wp_stream_is_record_excluded', $exclude_record, $record ); } + /** + * Check if a record to stored matches certain rules. + * + * @param array $record List of record parameters. + * @param array $exclude_rules List of record exclude rules. + * + * @return boolean + */ + public function record_matches_rules( $record, $exclude_rules ) { + foreach ( $exclude_rules as $exclude_key => $exclude_value ) { + if ( ! isset( $record[ $exclude_key ] ) ) { + continue; + } + + if ( 'ip_address' === $exclude_key ) { + $ip_addresses = explode( ',', $exclude_value ); + + if ( in_array( $record['ip_address'], $ip_addresses, true ) ) { + return true; + } + } elseif ( $record[ $exclude_key ] === $exclude_value ) { + return true; + } + } + + return false; + } + + /** + * Get all exclude rules by row because we store them by rule instead. + * + * @param array $rules List of rules indexed by rule ID. + * + * @return array + */ + public function exclude_rules_by_rows( $rules ) { + $excludes = array(); + + // TODO: Move these to where the settings are generated to ensure they're in sync. + $rule_keys = array( + 'exclude_row', + 'author_or_role', + 'connector', + 'context', + 'action', + 'ip_address', + ); + + if ( empty( $rules['exclude_row'] ) ) { + return array(); + } + + foreach ( array_keys( $rules['exclude_row'] ) as $row_id ) { + $excludes[ $row_id ] = array(); + + foreach ( $rule_keys as $rule_key ) { + if ( isset( $rules[ $rule_key ][ $row_id ] ) ) { + $excludes[ $row_id ][ $rule_key ] = $rules[ $rule_key ][ $row_id ]; + } else { + $excludes[ $row_id ][ $rule_key ] = null; + } + } + } + + return $excludes; + } + /** * Helper function to send a full backtrace of calls to the PHP error log for debugging * diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 693c88f9a..ca067eb77 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -9,7 +9,7 @@ class Plugin { * * @const string */ - const VERSION = '3.4.1'; + const VERSION = '3.4.2'; /** * WP-CLI command diff --git a/classes/class-settings.php b/classes/class-settings.php index 94f1f694a..fa00b7b20 100644 --- a/classes/class-settings.php +++ b/classes/class-settings.php @@ -551,12 +551,12 @@ public function sanitize_settings( $input ) { // Support all values in multidimentional arrays too. array_walk_recursive( $output[ $name ], - function ( &$v, $k ) { - $v = trim( $v ); + function ( &$v ) { + $v = sanitize_text_field( trim( $v ) ); } ); } else { - $output[ $name ] = trim( $input[ $name ] ); + $output[ $name ] = sanitize_text_field( trim( $input[ $name ] ) ); } } } @@ -842,8 +842,13 @@ public function render_field( $field ) { $exclude_rows = array(); + // Account for when no rules have been added yet. + if ( ! is_array( $current_value ) ) { + $current_value = array(); + } + // Prepend an empty row. - $current_value['exclude_row'] = array( 'helper' => '' ) + ( isset( $current_value['exclude_row'] ) ? $current_value['exclude_row'] : array() ); + $current_value['exclude_row'] = ( isset( $current_value['exclude_row'] ) ? $current_value['exclude_row'] : array() ) + array( 'helper' => '' ); foreach ( $current_value['exclude_row'] as $key => $value ) { // Prepare values. diff --git a/readme.txt b/readme.txt index 8d306403f..d76a33608 100644 --- a/readme.txt +++ b/readme.txt @@ -1,9 +1,9 @@ === Stream === -Contributors: lukecarbis, fjarrett, stream, xwp +Contributors: lukecarbis, fjarrett, stream, xwp, kasparsd Tags: wp stream, stream, activity, logs, track Requires at least: 4.5 Tested up to: 5.2 -Stable tag: 3.4.1 +Stable tag: 3.4.2 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -87,6 +87,11 @@ Thank you for wanting to make Stream better for everyone! == Changelog == += 3.4.2 - September 26, 2019 = + +* Fix: Visiting the plugin settings page no longer produces PHP warnings for undefined variables [#1031](https://github.com/xwp/stream/issues/1031). +* Fix: The IP address based exclude rules now stay with the same ruleset when saving [#1035](https://github.com/xwp/stream/issues/1035). Previously IP addresses would jump to the previous rule which didn't have an IP address based conditional. + = 3.4.1 - July 25, 2019 = * Fix: Allow tracking cron events even when the default WordPress front-end cron runner is disabled via `DISABLE_WP_CRON`. See [#959], props [@khromov](https://github.com/khromov) and [@tareiking](https://github.com/tareiking). diff --git a/stream.php b/stream.php index cb225b030..b4ede6005 100644 --- a/stream.php +++ b/stream.php @@ -3,7 +3,7 @@ * Plugin Name: Stream * Plugin URI: https://wp-stream.com/ * Description: Stream tracks logged-in user activity so you can monitor every change made on your WordPress site in beautifully organized detail. All activity is organized by context, action and IP address for easy filtering. Developers can extend Stream with custom connectors to log any kind of action. - * Version: 3.4.1 + * Version: 3.4.2 * Author: XWP * Author URI: https://xwp.co/ * License: GPLv2+ diff --git a/tests/tests/test-class-log.php b/tests/tests/test-class-log.php index 52d21610d..cf0e03892 100644 --- a/tests/tests/test-class-log.php +++ b/tests/tests/test-class-log.php @@ -63,6 +63,103 @@ public function test_field_lengths() { // Test action length // Test IP length - + + } + + public function test_can_map_exclude_rules_settings_to_rows() { + $rules_settings = array( + 'exclude_row' => array( + null, + null, + ), + 'action' => array( + 'one', + null, + 'three' + ) + ); + + $this->assertEquals( + array( + array( + 'exclude_row' => null, + 'action' => 'one', + 'author_or_role' => null, + 'connector' => null, + 'context' => null, + 'ip_address' => null, + ), + array( + 'exclude_row' => null, + 'action' => null, + 'author_or_role' => null, + 'connector' => null, + 'context' => null, + 'ip_address' => null, + ) + ), + $this->plugin->log->exclude_rules_by_rows( $rules_settings ) + ); + } + + public function test_can_match_record_exclude() { + $rules = array( + 'action' => 'mega_action', + ); + + $this->assertTrue( + $this->plugin->log->record_matches_rules( + array( + 'action' => 'mega_action', + 'ip_address' => '1.1.1.1', + ), + $rules + ), + 'Record action is the same' + ); + + $this->assertFalse( + $this->plugin->log->record_matches_rules( + array( + 'action' => 'different_action', + ), + $rules + ), + 'Record action is different' + ); + } + + public function test_can_match_record_id_address() { + $this->assertFalse( + $this->plugin->log->record_matches_rules( + array( + 'ip_address' => '1.1.1.1', + ), + array( + 'ip_address' => '8.8.8.8', + ) + ), + 'Record IP address is different' + ); + + $this->assertTrue( $this->plugin->log->record_matches_rules( + array( + 'ip_address' => '1.1.1.1', + ), + array( + 'ip_address' => '1.1.1.1', + ), + 'Record and rule IP addresses match' + ) ); + + $this->assertTrue( $this->plugin->log->record_matches_rules( + array( + 'ip_address' => '1.1.1.1', + ), + array( + 'ip_address' => '8.8.8.8,1.1.1.1', + ), + 'Record IP address is one of the IP addresses in the rule' + ) ); } } diff --git a/ui/js/exclude.js b/ui/js/exclude.js index 1b6416f90..a813818a7 100644 --- a/ui/js/exclude.js +++ b/ui/js/exclude.js @@ -1,10 +1,13 @@ /* globals jQuery, ajaxurl, wp_stream_regenerate_alt_rows */ jQuery( function( $ ) { - var initSettingsSelect2 = function() { + var $excludeRows = $( '.stream-exclude-list tbody tr:not(.hidden)' ); + var $placeholderRow = $( '.stream-exclude-list tr.helper' ); + + var initSettingsSelect2 = function( $rowsWithSelect2 ) { var $input_user; - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.connector_or_context' ).each( + $( 'select.select2-select.connector_or_context', $rowsWithSelect2 ).each( function( k, el ) { $( el ).select2( { @@ -64,7 +67,7 @@ jQuery( } ); - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.action' ).each( + $( 'select.select2-select.action', $rowsWithSelect2 ).each( function( k, el ) { $( el ).select2( { @@ -74,7 +77,7 @@ jQuery( } ); - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.author_or_role' ).each( + $( 'select.select2-select.author_or_role', $rowsWithSelect2 ).each( function( k, el ) { $input_user = $( el ); @@ -173,7 +176,7 @@ jQuery( } ); - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.ip_address' ).each( + $( 'select.select2-select.ip_address', $rowsWithSelect2 ).each( function( k, el ) { var $input_ip = $( el ), searchTerm = ''; @@ -271,28 +274,30 @@ jQuery( } ); - $( '.stream-exclude-list tr:not(.hidden) .exclude_rules_remove_rule_row' ).on( - 'click', function() { + $( '.exclude_rules_remove_rule_row', $rowsWithSelect2 ).on( + 'click', function( e ) { var $thisRow = $( this ).closest( 'tr' ); $thisRow.remove(); recalculate_rules_found(); recalculate_rules_selected(); + + e.preventDefault(); } ); }; - initSettingsSelect2(); + initSettingsSelect2( $excludeRows ); - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.author_or_role' ).each( + $( 'select.select2-select.author_or_role', $excludeRows ).each( function() { var $option = $( '' ).val( $( this ).data( 'selected-id' ) ); $( this ).append( $option ).trigger( 'change' ); } ); - $( '.stream-exclude-list tr:not(.hidden) select.select2-select.connector_or_context' ).each( + $( 'select.select2-select.connector_or_context', $excludeRows ).each( function() { var parts = [ $( this ).siblings( '.connector' ).val(), @@ -307,25 +312,12 @@ jQuery( $( '#exclude_rules_new_rule' ).on( 'click', function() { - var $excludeList = $( 'table.stream-exclude-list' ); - - $( 'tr:not(.hidden) select.select2-select', $excludeList ).each( - function() { - $( this ).select2( 'destroy' ); - } - ); - - var $lastRow = $( 'tr', $excludeList ).last(), - $newRow = $lastRow.clone(); + var $newRow = $placeholderRow.clone(); $newRow.removeAttr( 'class' ); - $( '.stream-exclude-list tbody :input' ).off(); - $( ':input', $newRow ).off().val( '' ); - - $lastRow.after( $newRow ); - - initSettingsSelect2(); + $newRow.insertBefore( $placeholderRow ); + initSettingsSelect2( $newRow ); recalculate_rules_found(); recalculate_rules_selected(); } @@ -369,6 +361,12 @@ jQuery( $( '.stream-exclude-list tbody tr:not(.hidden) select.select2-select.ip_address', this ).each( function() { var firstSelected = $( 'option:selected', this ).first(); + + // Ugly hack to ensure we always pass an empty value or the order of rows gets messed up. + if ( ! firstSelected.length ) { + $( this ).append( '' ); + } + $( 'option:selected:not(:first)', this ).each( function() { firstSelected.attr( 'value', firstSelected.attr( 'value' ) + ',' + $( this ).attr( 'value' ) );