Stream - Version 3.2.0

Version Description

  • March 15, 2017 =

  • New: Stream now support alternate Database Drivers. (#889)

  • Fix: Exclude dropdown menus (e5c8677, 3626ba8, e923a92)

  • Fix: Prevent loading of connectors on frontend (ed3a635)

  • Fix: Customizer performance issue (#898)

  • Fix: Various Network Admin bugs (#899)

  • Tweak: Codeclimate & Editorconfig support (#896)

  • Tweak: Better DB migration support (#905)

Download this release

Release Info

Developer lukecarbis
Plugin Icon 128x128 Stream
Version 3.2.0
Comparing to
See all releases

Code changes from version 3.1.1 to 3.2.0

.codeclimate.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ engines:
3
+ csslint:
4
+ enabled: true
5
+ duplication:
6
+ enabled: true
7
+ config:
8
+ languages:
9
+ - javascript
10
+ - php
11
+ fixme:
12
+ enabled: true
13
+ phpcodesniffer:
14
+ enabled: true
15
+ config:
16
+ standard: "WordPress-Core,WordPress-Docs,WordPress-Extra"
17
+ phpmd:
18
+ enabled: true
19
+ checks:
20
+ Controversial/CamelCaseParameterName:
21
+ enabled: false
22
+ Controversial/CamelCaseMethodName:
23
+ enabled: false
24
+ Controversial/CamelCasePropertyName:
25
+ enabled: false
26
+ Controversial/CamelCaseVariableName:
27
+ enabled: false
28
+ CleanCode/ElseExpression:
29
+ enabled: false
30
+ eslint:
31
+ enabled: true
32
+ channel: "eslint-2"
33
+ scss-lint:
34
+ enabled: true
35
+ markdownlint:
36
+ enabled: true
37
+ ratings:
38
+ paths:
39
+ - "**.css"
40
+ - "**.scss"
41
+ - "**.inc"
42
+ - "**.js"
43
+ - "**.jsx"
44
+ - "**.module"
45
+ - "**.php"
46
+ - "**.md"
47
+ - "**.py"
48
+ - "**.rb"
49
+ exclude_paths:
50
+ - "**.png"
51
+ - "**.jpg"
52
+ - "**.gif"
53
+ - "gulpfile.js"
54
+ - "composer.lock"
55
+ - "phpcs.xml"
56
+ - "**.json"
57
+ - "**.pot"
58
+ - "**.txt"
59
+ - "**-min.js"
60
+ - "**-min.css"
61
+ - "**.min.js"
62
+ - "**.min.css"
63
+ - "**.dist"
64
+ - "**.sh"
.editorconfig ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is for unifying the coding style for different editors and IDEs
2
+ # editorconfig.org
3
+
4
+ # WordPress Coding Standards
5
+ # https://make.wordpress.org/core/handbook/coding-standards/
6
+
7
+ root = true
8
+
9
+ [*]
10
+ charset = utf-8
11
+ end_of_line = lf
12
+ insert_final_newline = true
13
+ trim_trailing_whitespace = true
14
+ indent_style = tab
15
+
16
+ [{.jshintrc,*.json,*.yml}]
17
+ indent_style = space
18
+ indent_size = 2
19
+
20
+ [{*.txt,wp-config-sample.php}]
21
+ end_of_line = crlf
.mailmap DELETED
@@ -1,4 +0,0 @@
1
- Luke Carbis <luke@xwp.co> <Luke.Carbis@news.com.au>
2
- Luke Carbis <luke@xwp.co> <luke.carbis@x-team.com.au>
3
- Luke Carbis <luke@xwp.co> <luke.carbis@x-team.com>
4
- Luke Carbis <luke@xwp.co> <luke@carbis.com.au>
 
 
 
 
alerts/class-alert-trigger-author.php CHANGED
@@ -107,8 +107,9 @@ class Alert_Trigger_Author extends Alert_Trigger {
107
 
108
  foreach ( $users as $user ) {
109
  $all_records[] = array(
110
- 'id' => $user->id,
111
- 'text' => $user->get_display_name(),
 
112
  );
113
  }
114
  return $all_records;
107
 
108
  foreach ( $users as $user ) {
109
  $all_records[] = array(
110
+ 'id' => $user->id,
111
+ 'value' => $user->id,
112
+ 'text' => $user->get_display_name(),
113
  );
114
  }
115
  return $all_records;
alerts/class-alert-trigger-context.php CHANGED
@@ -115,14 +115,14 @@ class Alert_Trigger_Context extends Alert_Trigger {
115
  if ( isset( $context_data['children'] ) ) {
116
  $child_values = array();
117
  foreach ( $context_data['children'] as $child_id => $child_value ) {
118
- $child_values[] = array( 'id' => $context_id . '-' . $child_id, 'text' => $child_value, 'parent' => $context_id );
119
  }
120
  }
121
  if ( isset( $context_data['label'] ) ) {
122
- $context_values[] = array( 'id' => $context_id, 'text' => $context_data['label'], 'children' => $child_values );
123
  }
124
  } else {
125
- $context_values[] = array( 'id' => $context_id, 'text' => $context_data );
126
  }
127
  }
128
  return $context_values;
115
  if ( isset( $context_data['children'] ) ) {
116
  $child_values = array();
117
  foreach ( $context_data['children'] as $child_id => $child_value ) {
118
+ $child_values[] = array( 'value' => $context_id . '-' . $child_id, 'id' => $context_id . '-' . $child_id, 'text' => $child_value, 'parent' => $context_id );
119
  }
120
  }
121
  if ( isset( $context_data['label'] ) ) {
122
+ $context_values[] = array( 'value' => $context_id, 'id' => $context_id, 'text' => $context_data['label'], 'children' => $child_values );
123
  }
124
  } else {
125
+ $context_values[] = array( 'value' => $context_id, 'id' => $context_id, 'text' => $context_data );
126
  }
127
  }
128
  return $context_values;
classes/class-admin.php CHANGED
@@ -162,8 +162,7 @@ class Admin {
162
  add_action( 'wp_ajax_wp_stream_reset', array( $this, 'wp_ajax_reset' ) );
163
 
164
  // Uninstall Streams and Deactivate plugin.
165
- $uninstall = new Uninstall( $this->plugin );
166
- add_action( 'wp_ajax_wp_stream_uninstall', array( $uninstall, 'uninstall' ) );
167
 
168
  // Auto purge setup.
169
  add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
@@ -346,8 +345,8 @@ class Admin {
346
  * @return void
347
  */
348
  public function admin_enqueue_scripts( $hook ) {
349
- wp_register_script( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/js/select2.js', array( 'jquery' ), '3.5.2', true );
350
- wp_register_style( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/css/select2.css', array(), '3.5.2' );
351
  wp_register_script( 'wp-stream-timeago', $this->plugin->locations['url'] . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
352
 
353
  $locale = strtolower( substr( get_locale(), 0, 2 ) );
@@ -442,7 +441,7 @@ class Admin {
442
  }
443
 
444
  $screen = get_current_screen();
445
- if ( is_admin() && 'post' === $screen->base && Alerts::POST_TYPE === $screen->post_type ) {
446
  return true;
447
  }
448
 
162
  add_action( 'wp_ajax_wp_stream_reset', array( $this, 'wp_ajax_reset' ) );
163
 
164
  // Uninstall Streams and Deactivate plugin.
165
+ $uninstall = $this->plugin->db->driver->purge_storage( $this->plugin );
 
166
 
167
  // Auto purge setup.
168
  add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
345
  * @return void
346
  */
347
  public function admin_enqueue_scripts( $hook ) {
348
+ wp_register_script( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/js/select2.full.min.js', array( 'jquery' ), '3.5.2', true );
349
+ wp_register_style( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/css/select2.min.css', array(), '3.5.2' );
350
  wp_register_script( 'wp-stream-timeago', $this->plugin->locations['url'] . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
351
 
352
  $locale = strtolower( substr( get_locale(), 0, 2 ) );
441
  }
442
 
443
  $screen = get_current_screen();
444
+ if ( is_admin() && Alerts::POST_TYPE === $screen->post_type ) {
445
  return true;
446
  }
447
 
classes/class-alerts-list.php CHANGED
@@ -334,6 +334,10 @@ class Alerts_List {
334
  * @return array
335
  */
336
  function save_alert_inline_edit( $data, $postarr ) {
 
 
 
 
337
  $post_id = $postarr['ID'];
338
  $post_type = wp_stream_filter_input( INPUT_POST, 'post_type' );
339
  if ( Alerts::POST_TYPE !== $post_type ) {
334
  * @return array
335
  */
336
  function save_alert_inline_edit( $data, $postarr ) {
337
+ if ( did_action( 'customize_preview_init' ) ) {
338
+ return $data;
339
+ }
340
+
341
  $post_id = $postarr['ID'];
342
  $post_type = wp_stream_filter_input( INPUT_POST, 'post_type' );
343
  if ( Alerts::POST_TYPE !== $post_type ) {
classes/class-alerts.php CHANGED
@@ -426,8 +426,7 @@ class Alerts {
426
  }
427
 
428
  // Get the first existing Site in the Network.
429
- // @todo: Switch to use wp_stream_get_sites()
430
- $sites = wp_get_sites(
431
  array(
432
  'limit' => 5, // Limit the size of the query.
433
  )
@@ -436,8 +435,8 @@ class Alerts {
436
  $site_id = '1';
437
 
438
  // Function wp_get_sites() can return an empty array if the network is too large.
439
- if ( ! empty( $sites ) && ! empty( $sites[0]['blog_id'] ) ) {
440
- $site_id = $sites[0]['blog_id'];
441
  }
442
 
443
  $new_url = get_admin_url( $site_id, $page );
426
  }
427
 
428
  // Get the first existing Site in the Network.
429
+ $sites = wp_stream_get_sites(
 
430
  array(
431
  'limit' => 5, // Limit the size of the query.
432
  )
435
  $site_id = '1';
436
 
437
  // Function wp_get_sites() can return an empty array if the network is too large.
438
+ if ( ! empty( $sites ) && ! empty( $sites[0]->blog_id ) ) {
439
+ $site_id = $sites[0]->blog_id;
440
  }
441
 
442
  $new_url = get_admin_url( $site_id, $page );
classes/class-connector.php CHANGED
@@ -30,6 +30,20 @@ abstract class Connector {
30
  */
31
  public $prev_stream = null;
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  /**
34
  * Register all context hooks
35
  */
30
  */
31
  public $prev_stream = null;
32
 
33
+ /**
34
+ * Register connector in the WP Admin
35
+ *
36
+ * @var bool
37
+ */
38
+ public $register_admin = true;
39
+
40
+ /**
41
+ * Register connector in the WP Frontend
42
+ *
43
+ * @var bool
44
+ */
45
+ public $register_frontend = true;
46
+
47
  /**
48
  * Register all context hooks
49
  */
classes/class-connectors.php CHANGED
@@ -89,9 +89,22 @@ class Connectors {
89
  continue;
90
  }
91
  $class = new $class_name( $this->plugin->log );
92
- if ( ! method_exists( $class, 'is_dependency_satisfied' ) ) {
 
 
 
 
 
 
 
93
  continue;
94
  }
 
 
 
 
 
 
95
  if ( $class->is_dependency_satisfied() ) {
96
  $classes[ $class->name ] = $class;
97
  }
89
  continue;
90
  }
91
  $class = new $class_name( $this->plugin->log );
92
+
93
+ // Check if the Connector extends WP_Stream\Connector
94
+ if ( ! is_subclass_of( $class, 'WP_Stream\Connector' ) ) {
95
+ continue;
96
+ }
97
+
98
+ // Check if the Connector is allowed to be registered in the WP Admin
99
+ if ( is_admin() && ! $class->register_admin ) {
100
  continue;
101
  }
102
+
103
+ // Check if the Connector is allowed to be registered in the WP Frontend
104
+ if ( ! is_admin() && ! $class->register_frontend ) {
105
+ continue;
106
+ }
107
+
108
  if ( $class->is_dependency_satisfied() ) {
109
  $classes[ $class->name ] = $class;
110
  }
classes/class-db-driver-wpdb.php ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ class DB_Driver_WPDB implements DB_Driver {
5
+ /**
6
+ * Holds Query class
7
+ *
8
+ * @var Query
9
+ */
10
+ protected $query;
11
+
12
+ /**
13
+ * Hold records table name
14
+ *
15
+ * @var string
16
+ */
17
+ public $table;
18
+
19
+ /**
20
+ * Hold meta table name
21
+ *
22
+ * @var string
23
+ */
24
+ public $table_meta;
25
+
26
+ /**
27
+ * Class constructor.
28
+ */
29
+ public function __construct() {
30
+ $this->query = new Query( $this );
31
+
32
+ global $wpdb;
33
+ $prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix );
34
+
35
+ $this->table = $prefix . 'stream';
36
+ $this->table_meta = $prefix . 'stream_meta';
37
+
38
+ $wpdb->stream = $this->table;
39
+ $wpdb->streammeta = $this->table_meta;
40
+
41
+ // Hack for get_metadata
42
+ $wpdb->recordmeta = $this->table_meta;
43
+ }
44
+
45
+ /**
46
+ * Insert a record.
47
+ *
48
+ * @param array $data Data to insert.
49
+ *
50
+ * @return int
51
+ */
52
+ public function insert_record( $data ) {
53
+ global $wpdb;
54
+
55
+ if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
56
+ return false;
57
+ }
58
+
59
+ $meta = $data['meta'];
60
+ unset( $data['meta'] );
61
+
62
+ $result = $wpdb->insert( $this->table, $data );
63
+ if ( ! $result ) {
64
+ return false;
65
+ }
66
+
67
+ $record_id = $wpdb->insert_id;
68
+
69
+ // Insert record meta
70
+ foreach ( (array) $meta as $key => $vals ) {
71
+ foreach ( (array) $vals as $val ) {
72
+ $this->insert_meta( $record_id, $key, $val );
73
+ }
74
+ }
75
+
76
+ return $record_id;
77
+ }
78
+
79
+ /**
80
+ * Insert record meta
81
+ *
82
+ * @param int $record_id
83
+ * @param string $key
84
+ * @param string $val
85
+ *
86
+ * @return array
87
+ */
88
+ public function insert_meta( $record_id, $key, $val ) {
89
+ global $wpdb;
90
+
91
+ $result = $wpdb->insert(
92
+ $this->table_meta,
93
+ array(
94
+ 'record_id' => $record_id,
95
+ 'meta_key' => $key,
96
+ 'meta_value' => $val,
97
+ )
98
+ );
99
+
100
+ return $result;
101
+ }
102
+
103
+ /**
104
+ * Retrieve records
105
+ *
106
+ * @param array $args
107
+ *
108
+ * @return array
109
+ */
110
+ public function get_records( $args ) {
111
+ return $this->query->query( $args );
112
+ }
113
+
114
+ /**
115
+ * Returns array of existing values for requested column.
116
+ * Used to fill search filters with only used items, instead of all items.
117
+ *
118
+ * GROUP BY allows query to find just the first occurrence of each value in the column,
119
+ * increasing the efficiency of the query.
120
+ *
121
+ * @param string $column
122
+ *
123
+ * @return array
124
+ */
125
+ public function get_column_values( $column ) {
126
+ global $wpdb;
127
+ return (array) $wpdb->get_results(
128
+ "SELECT DISTINCT $column FROM $wpdb->stream", // @codingStandardsIgnoreLine can't prepare column name
129
+ 'ARRAY_A'
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Public getter to return table names
135
+ *
136
+ * @return array
137
+ */
138
+ public function get_table_names() {
139
+ return array(
140
+ $this->table,
141
+ $this->table_meta,
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Init storage.
147
+ *
148
+ * @param \WP_Stream\Plugin $plugin Instance of the plugin.
149
+ * @return \WP_Stream\Install
150
+ */
151
+ public function setup_storage( $plugin ) {
152
+ return new Install( $plugin );
153
+ }
154
+
155
+ /**
156
+ * Purge storage.
157
+ *
158
+ * @param \WP_Stream\Plugin $plugin Instance of the plugin.
159
+ * @return \WP_Stream\Uninstall
160
+ */
161
+ public function purge_storage( $plugin ) {
162
+ $uninstall = new Uninstall( $plugin );
163
+ add_action( 'wp_ajax_wp_stream_uninstall', array( $uninstall, 'uninstall' ) );
164
+
165
+ return $uninstall;
166
+ }
167
+
168
+ }
classes/class-db-driver.php ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ namespace WP_Stream;
3
+
4
+ interface DB_Driver {
5
+ /**
6
+ * Insert a record
7
+ *
8
+ * @param array $data
9
+ *
10
+ * @return int
11
+ */
12
+ public function insert_record( $data );
13
+
14
+ /**
15
+ * Retrieve records
16
+ *
17
+ * @param array $args
18
+ *
19
+ * @return array
20
+ */
21
+ public function get_records( $args );
22
+
23
+ /**
24
+ * Returns array of existing values for requested column.
25
+ * Used to fill search filters with only used items, instead of all items.
26
+ *
27
+ * @param string $column
28
+ *
29
+ * @return array
30
+ */
31
+ public function get_column_values( $column );
32
+
33
+ /**
34
+ * Public getter to return table names
35
+ *
36
+ * @return array
37
+ */
38
+ public function get_table_names();
39
+
40
+ /**
41
+ * Init storage.
42
+ *
43
+ * @param \WP_Stream\Plugin $plugin Instance of the plugin.
44
+ */
45
+ public function setup_storage( $plugin );
46
+
47
+ /**
48
+ * Purge storage.
49
+ *
50
+ * @param \WP_Stream\Plugin $plugin Instance of the plugin.
51
+ */
52
+ public function purge_storage( $plugin );
53
+ }
classes/class-db.php CHANGED
@@ -3,71 +3,26 @@ namespace WP_Stream;
3
 
4
  class DB {
5
  /**
6
- * Hold Plugin class
7
- * @var Plugin
8
- */
9
- public $plugin;
10
-
11
- /**
12
- * Hold Query class
13
- * @var Query
14
- */
15
- public $query;
16
-
17
- /**
18
- * Hold records table name
19
  *
20
- * @var string
21
  */
22
- public $table;
23
 
24
  /**
25
- * Hold meta table name
26
  *
27
- * @var string
28
  */
29
- public $table_meta;
30
 
31
  /**
32
  * Class constructor.
33
  *
34
- * @param Plugin $plugin The main Plugin class.
35
- */
36
- public function __construct( $plugin ) {
37
- $this->plugin = $plugin;
38
- $this->query = new Query( $this );
39
-
40
- global $wpdb;
41
-
42
- /**
43
- * Allows devs to alter the tables prefix, default to base_prefix
44
- *
45
- * @param string $prefix
46
- *
47
- * @return string
48
- */
49
- $prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix );
50
-
51
- $this->table = $prefix . 'stream';
52
- $this->table_meta = $prefix . 'stream_meta';
53
-
54
- $wpdb->stream = $this->table;
55
- $wpdb->streammeta = $this->table_meta;
56
-
57
- // Hack for get_metadata
58
- $wpdb->recordmeta = $this->table_meta;
59
- }
60
-
61
- /**
62
- * Public getter to return table names
63
- *
64
- * @return array
65
  */
66
- public function get_table_names() {
67
- return array(
68
- $this->table,
69
- $this->table_meta,
70
- );
71
  }
72
 
73
  /**
@@ -101,37 +56,34 @@ class DB {
101
  return false;
102
  }
103
 
104
- global $wpdb;
105
-
106
  $fields = array( 'object_id', 'site_id', 'blog_id', 'user_id', 'user_role', 'created', 'summary', 'ip', 'connector', 'context', 'action' );
107
  $data = array_intersect_key( $record, array_flip( $fields ) );
108
 
109
- $result = $wpdb->insert( $this->table, $data );
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- if ( 1 !== $result ) {
112
  /**
113
  * Fires on a record insertion error
114
  *
115
  * @param array $record
116
  * @param mixed $result
117
  */
118
- do_action( 'wp_stream_record_insert_error', $record, $result );
119
-
120
- return $result;
121
- }
122
-
123
- $record_id = $wpdb->insert_id;
124
 
125
- // Insert record meta
126
- foreach ( (array) $record['meta'] as $key => $vals ) {
127
- // If associative array, serialize it, otherwise loop on its members
128
- $vals = ( is_array( $vals ) && 0 !== key( $vals ) ) ? array( $vals ) : $vals;
129
-
130
- foreach ( (array) $vals as $val ) {
131
- $val = maybe_serialize( $val );
132
-
133
- $this->insert_meta( $record_id, $key, $val );
134
- }
135
  }
136
 
137
  /**
@@ -145,35 +97,11 @@ class DB {
145
  return absint( $record_id );
146
  }
147
 
148
- /**
149
- * Insert record meta
150
- *
151
- * @param int $record_id
152
- * @param string $key
153
- * @param string $val
154
- *
155
- * @return array
156
- */
157
- public function insert_meta( $record_id, $key, $val ) {
158
- global $wpdb;
159
-
160
- $result = $wpdb->insert(
161
- $this->table_meta,
162
- array(
163
- 'record_id' => $record_id,
164
- 'meta_key' => $key,
165
- 'meta_value' => $val,
166
- )
167
- );
168
-
169
- return $result;
170
- }
171
-
172
  /**
173
  * Returns array of existing values for requested column.
174
  * Used to fill search filters with only used items, instead of all items.
175
  *
176
- * GROUP BY allows query to find just the first occurance of each value in the column,
177
  * increasing the efficiency of the query.
178
  *
179
  * @see assemble_records
@@ -183,19 +111,14 @@ class DB {
183
  *
184
  * @return array
185
  */
186
- function existing_records( $column ) {
187
- global $wpdb;
188
-
189
  // Sanitize column
190
  $allowed_columns = array( 'ID', 'site_id', 'blog_id', 'object_id', 'user_id', 'user_role', 'created', 'summary', 'connector', 'context', 'action', 'ip' );
191
  if ( ! in_array( $column, $allowed_columns, true ) ) {
192
  return array();
193
  }
194
 
195
- $rows = $wpdb->get_results(
196
- "SELECT DISTINCT $column FROM $wpdb->stream", // @codingStandardsIgnoreLine can't prepare column name
197
- 'ARRAY_A'
198
- );
199
 
200
  if ( is_array( $rows ) && ! empty( $rows ) ) {
201
  $output_array = array();
@@ -211,19 +134,114 @@ class DB {
211
 
212
  $column = sprintf( 'stream_%s', $column );
213
 
214
- return isset( $this->plugin->connectors->term_labels[ $column ] ) ? $this->plugin->connectors->term_labels[ $column ] : array();
 
215
  }
216
 
217
  /**
218
- * Helper function for calling $this->query->query()
219
- *
220
- * @see Query->query()
221
  *
222
  * @param array Query args
223
  *
224
  * @return array Stream Records
225
  */
226
- function query( $args ) {
227
- return $this->query->query( $args );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  }
229
  }
3
 
4
  class DB {
5
  /**
6
+ * Hold the Driver class
 
 
 
 
 
 
 
 
 
 
 
 
7
  *
8
+ * @var DB_Driver
9
  */
10
+ public $driver;
11
 
12
  /**
13
+ * Number of records in last request
14
  *
15
+ * @var int
16
  */
17
+ protected $found_records_count = 0;
18
 
19
  /**
20
  * Class constructor.
21
  *
22
+ * @param DB_Driver $driver Driver we want to use.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  */
24
+ public function __construct( $driver ) {
25
+ $this->driver = $driver;
 
 
 
26
  }
27
 
28
  /**
56
  return false;
57
  }
58
 
 
 
59
  $fields = array( 'object_id', 'site_id', 'blog_id', 'user_id', 'user_role', 'created', 'summary', 'ip', 'connector', 'context', 'action' );
60
  $data = array_intersect_key( $record, array_flip( $fields ) );
61
 
62
+ $meta = array();
63
+ foreach ( (array) $record['meta'] as $key => $vals ) {
64
+ // If associative array, serialize it, otherwise loop on its members
65
+ $vals = ( is_array( $vals ) && 0 !== key( $vals ) ) ? array( $vals ) : $vals;
66
+
67
+ foreach ( (array) $vals as $num => $val ) {
68
+ $vals[ $num ] = maybe_serialize( $val );
69
+ }
70
+ $meta[ $key ] = $vals;
71
+ }
72
+
73
+ $data['meta'] = $meta;
74
+
75
+ $record_id = $this->driver->insert_record( $data );
76
 
77
+ if ( ! $record_id ) {
78
  /**
79
  * Fires on a record insertion error
80
  *
81
  * @param array $record
82
  * @param mixed $result
83
  */
84
+ do_action( 'wp_stream_record_insert_error', $record, false );
 
 
 
 
 
85
 
86
+ return false;
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  /**
97
  return absint( $record_id );
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  /**
101
  * Returns array of existing values for requested column.
102
  * Used to fill search filters with only used items, instead of all items.
103
  *
104
+ * GROUP BY allows query to find just the first occurrence of each value in the column,
105
  * increasing the efficiency of the query.
106
  *
107
  * @see assemble_records
111
  *
112
  * @return array
113
  */
114
+ public function existing_records( $column ) {
 
 
115
  // Sanitize column
116
  $allowed_columns = array( 'ID', 'site_id', 'blog_id', 'object_id', 'user_id', 'user_role', 'created', 'summary', 'connector', 'context', 'action', 'ip' );
117
  if ( ! in_array( $column, $allowed_columns, true ) ) {
118
  return array();
119
  }
120
 
121
+ $rows = $this->driver->get_column_values( $column );
 
 
 
122
 
123
  if ( is_array( $rows ) && ! empty( $rows ) ) {
124
  $output_array = array();
134
 
135
  $column = sprintf( 'stream_%s', $column );
136
 
137
+ $term_labels = wp_stream_get_instance()->connectors->term_labels;
138
+ return isset( $term_labels[ $column ] ) ? $term_labels[ $column ] : array();
139
  }
140
 
141
  /**
142
+ * Get stream records
 
 
143
  *
144
  * @param array Query args
145
  *
146
  * @return array Stream Records
147
  */
148
+ public function get_records( $args ) {
149
+ $defaults = array(
150
+ // Search param
151
+ 'search' => null,
152
+ 'search_field' => 'summary',
153
+ 'record_after' => null, // Deprecated, use date_after instead
154
+ // Date-based filters
155
+ 'date' => null, // Ex: 2015-07-01
156
+ 'date_from' => null, // Ex: 2015-07-01
157
+ 'date_to' => null, // Ex: 2015-07-01
158
+ 'date_after' => null, // Ex: 2015-07-01T15:19:21+00:00
159
+ 'date_before' => null, // Ex: 2015-07-01T15:19:21+00:00
160
+ // Record ID filters
161
+ 'record' => null,
162
+ 'record__in' => array(),
163
+ 'record__not_in' => array(),
164
+ // Pagination params
165
+ 'records_per_page' => get_option( 'posts_per_page', 20 ),
166
+ 'paged' => 1,
167
+ // Order
168
+ 'order' => 'desc',
169
+ 'orderby' => 'date',
170
+ // Fields selection
171
+ 'fields' => array(),
172
+ );
173
+
174
+ // Additional property fields
175
+ $properties = array(
176
+ 'user_id' => null,
177
+ 'user_role' => null,
178
+ 'ip' => null,
179
+ 'object_id' => null,
180
+ 'site_id' => null,
181
+ 'blog_id' => null,
182
+ 'connector' => null,
183
+ 'context' => null,
184
+ 'action' => null,
185
+ );
186
+
187
+ /**
188
+ * Filter allows additional query properties to be added
189
+ *
190
+ * @return array Array of query properties
191
+ */
192
+ $properties = apply_filters( 'wp_stream_query_properties', $properties );
193
+
194
+ // Add property fields to defaults, including their __in/__not_in variations
195
+ foreach ( $properties as $property => $default ) {
196
+ if ( ! isset( $defaults[ $property ] ) ) {
197
+ $defaults[ $property ] = $default;
198
+ }
199
+
200
+ $defaults[ "{$property}__in" ] = array();
201
+ $defaults[ "{$property}__not_in" ] = array();
202
+ }
203
+
204
+ $args = wp_parse_args( $args, $defaults );
205
+
206
+ /**
207
+ * Filter allows additional arguments to query $args
208
+ *
209
+ * @return array Array of query arguments
210
+ */
211
+ $args = apply_filters( 'wp_stream_query_args', $args );
212
+
213
+ $result = (array) $this->driver->get_records( $args );
214
+ $this->found_records_count = isset( $result['count'] ) ? $result['count'] : 0;
215
+
216
+ return empty( $result['items'] ) ? array() : $result['items'];
217
+ }
218
+
219
+ /**
220
+ * Helper function, backwards compatibility
221
+ *
222
+ * @param array $args Query args
223
+ *
224
+ * @return array Stream Records
225
+ */
226
+ public function query( $args ) {
227
+ return $this->get_records( $args );
228
+ }
229
+
230
+ /**
231
+ * Return the number of records found in last request
232
+ *
233
+ * return int
234
+ */
235
+ public function get_found_records_count() {
236
+ return $this->found_records_count;
237
+ }
238
+
239
+ /**
240
+ * Public getter to return table names
241
+ *
242
+ * @return array
243
+ */
244
+ public function get_table_names() {
245
+ return $this->driver->get_table_names();
246
  }
247
  }
classes/class-form-generator.php CHANGED
@@ -133,30 +133,30 @@ class Form_Generator {
133
  'text' => '',
134
  'children' => array(),
135
  ) );
136
- if ( empty( $parent['id'] ) ) {
137
  continue;
138
  }
139
  if ( is_array( $args['value'] ) ) {
140
- $selected = selected( in_array( $parent['id'], $args['value'], true ), true, false );
141
  } else {
142
- $selected = selected( $args['value'], $parent['id'], false );
143
  }
144
  $output .= sprintf(
145
  '<option class="parent" value="%1$s" %3$s>%2$s</option>',
146
- $parent['id'],
147
  $parent['text'],
148
  $selected
149
  );
150
- $values[] = $parent['id'];
151
  if ( ! empty( $parent['children'] ) ) {
152
  foreach ( $parent['children'] as $child ) {
153
  $output .= sprintf(
154
  '<option class="child" value="%1$s" %3$s>%2$s</option>',
155
- $child['id'],
156
  $child['text'],
157
- selected( $args['value'], $child['id'], false )
158
  );
159
- $values[] = $child['id'];
160
  }
161
  $output .= '</optgroup>';
162
  }
133
  'text' => '',
134
  'children' => array(),
135
  ) );
136
+ if ( empty( $parent['value'] ) ) {
137
  continue;
138
  }
139
  if ( is_array( $args['value'] ) ) {
140
+ $selected = selected( in_array( $parent['value'], $args['value'], true ), true, false );
141
  } else {
142
+ $selected = selected( $args['value'], $parent['value'], false );
143
  }
144
  $output .= sprintf(
145
  '<option class="parent" value="%1$s" %3$s>%2$s</option>',
146
+ $parent['value'],
147
  $parent['text'],
148
  $selected
149
  );
150
+ $values[] = $parent['value'];
151
  if ( ! empty( $parent['children'] ) ) {
152
  foreach ( $parent['children'] as $child ) {
153
  $output .= sprintf(
154
  '<option class="child" value="%1$s" %3$s>%2$s</option>',
155
+ $child['value'],
156
  $child['text'],
157
+ selected( $args['value'], $child['value'], false )
158
  );
159
+ $values[] = $child['value'];
160
  }
161
  $output .= '</optgroup>';
162
  }
classes/class-list-table.php CHANGED
@@ -206,8 +206,7 @@ class List_Table extends \WP_List_Table {
206
  }
207
  $args['records_per_page'] = apply_filters( 'stream_records_per_page', $args['records_per_page'] );
208
 
209
- $items = $this->plugin->db->query( $args );
210
-
211
  return $items;
212
  }
213
 
@@ -217,7 +216,7 @@ class List_Table extends \WP_List_Table {
217
  * @return integer
218
  */
219
  public function get_total_found_rows() {
220
- return $this->plugin->db->query->found_records;
221
  }
222
 
223
  function column_default( $item, $column_name ) {
206
  }
207
  $args['records_per_page'] = apply_filters( 'stream_records_per_page', $args['records_per_page'] );
208
 
209
+ $items = $this->plugin->db->get_records( $args );
 
210
  return $items;
211
  }
212
 
216
  * @return integer
217
  */
218
  public function get_total_found_rows() {
219
+ return $this->plugin->db->get_found_records_count();
220
  }
221
 
222
  function column_default( $item, $column_name ) {
classes/class-network.php CHANGED
@@ -51,6 +51,8 @@ class Network {
51
  /**
52
  * Workaround to get admin-ajax.php to know when the request is from the Network Admin
53
  *
 
 
54
  * @action init
55
  *
56
  * @see https://core.trac.wordpress.org/ticket/22589
@@ -64,7 +66,10 @@ class Network {
64
  preg_match( '#^' . network_admin_url() . '#i', $_SERVER['HTTP_REFERER'] )
65
  ) {
66
  define( 'WP_NETWORK_ADMIN', true );
 
67
  }
 
 
68
  }
69
 
70
  /**
@@ -136,6 +141,7 @@ class Network {
136
  }
137
 
138
  remove_submenu_page( $this->plugin->admin->records_page_slug, 'wp_stream_settings' );
 
139
 
140
  $this->plugin->admin->screen_id['network_settings'] = add_submenu_page(
141
  $this->plugin->admin->records_page_slug,
@@ -496,7 +502,7 @@ class Network {
496
  * @return mixed
497
  */
498
  public function network_admin_columns( $columns ) {
499
- if ( is_network_admin() ) {
500
  $columns = array_merge(
501
  array_slice( $columns, 0, -1 ),
502
  array(
51
  /**
52
  * Workaround to get admin-ajax.php to know when the request is from the Network Admin
53
  *
54
+ * @return bool
55
+ *
56
  * @action init
57
  *
58
  * @see https://core.trac.wordpress.org/ticket/22589
66
  preg_match( '#^' . network_admin_url() . '#i', $_SERVER['HTTP_REFERER'] )
67
  ) {
68
  define( 'WP_NETWORK_ADMIN', true );
69
+ return WP_NETWORK_ADMIN;
70
  }
71
+
72
+ return false;
73
  }
74
 
75
  /**
141
  }
142
 
143
  remove_submenu_page( $this->plugin->admin->records_page_slug, 'wp_stream_settings' );
144
+ remove_submenu_page( $this->plugin->admin->records_page_slug, 'edit.php?post_type=wp_stream_alerts' );
145
 
146
  $this->plugin->admin->screen_id['network_settings'] = add_submenu_page(
147
  $this->plugin->admin->records_page_slug,
502
  * @return mixed
503
  */
504
  public function network_admin_columns( $columns ) {
505
+ if ( is_network_admin() || $this->ajax_network_admin() ) {
506
  $columns = array_merge(
507
  array_slice( $columns, 0, -1 ),
508
  array(
classes/class-plugin.php CHANGED
@@ -7,7 +7,7 @@ class Plugin {
7
  *
8
  * @const string
9
  */
10
- const VERSION = '3.1.1';
11
 
12
  /**
13
  * WP-CLI command
@@ -83,14 +83,24 @@ class Plugin {
83
  require_once $this->locations['inc_dir'] . 'functions.php';
84
 
85
  // Load DB helper interface/class
86
- $driver = '\WP_Stream\DB';
87
- if ( class_exists( $driver ) ) {
88
- $this->db = new DB( $this );
 
 
 
89
  }
90
 
 
91
  if ( ! $this->db ) {
 
 
 
 
 
 
92
  wp_die(
93
- esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ),
94
  esc_html__( 'Stream DB Error', 'stream' )
95
  );
96
  }
@@ -107,12 +117,15 @@ class Plugin {
107
  // Add frontend indicator
108
  add_action( 'wp_head', array( $this, 'frontend_indicator' ) );
109
 
 
 
 
110
  // Load admin area classes
111
  if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
112
  $this->admin = new Admin( $this );
113
- $this->install = new Install( $this );
114
  } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
115
- $this->admin = new Admin( $this );
116
  }
117
 
118
  // Load WP-CLI command
@@ -221,4 +234,17 @@ class Plugin {
221
  public function get_version() {
222
  return self::VERSION;
223
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
7
  *
8
  * @const string
9
  */
10
+ const VERSION = '3.2.0';
11
 
12
  /**
13
  * WP-CLI command
83
  require_once $this->locations['inc_dir'] . 'functions.php';
84
 
85
  // Load DB helper interface/class
86
+ $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
87
+ $driver = null;
88
+
89
+ if ( class_exists( $driver_class ) ) {
90
+ $driver = new $driver_class();
91
+ $this->db = new DB( $driver );
92
  }
93
 
94
+ $error = false;
95
  if ( ! $this->db ) {
96
+ $error = esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' );
97
+ } elseif ( ! $driver instanceof DB_Driver ) {
98
+ $error = esc_html__( 'Stream: DB driver must implement DB Driver interface.', 'stream' );
99
+ }
100
+
101
+ if ( $error ) {
102
  wp_die(
103
+ esc_html( $error ),
104
  esc_html__( 'Stream DB Error', 'stream' )
105
  );
106
  }
117
  // Add frontend indicator
118
  add_action( 'wp_head', array( $this, 'frontend_indicator' ) );
119
 
120
+ // Change DB driver after plugin loaded if any add-ons want to replace
121
+ add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
122
+
123
  // Load admin area classes
124
  if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
125
  $this->admin = new Admin( $this );
126
+ $this->install = $driver->setup_storage( $this );
127
  } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
128
+ $this->admin = new Admin( $this, $driver );
129
  }
130
 
131
  // Load WP-CLI command
234
  public function get_version() {
235
  return self::VERSION;
236
  }
237
+
238
+ /**
239
+ * Change plugin database driver in case driver plugin loaded after stream
240
+ */
241
+ public function plugins_loaded() {
242
+ // Load DB helper interface/class
243
+ $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
244
+
245
+ if ( class_exists( $driver_class ) ) {
246
+ $driver = new $driver_class();
247
+ $this->db = new DB( $driver );
248
+ }
249
+ }
250
  }
classes/class-query.php CHANGED
@@ -2,11 +2,6 @@
2
  namespace WP_Stream;
3
 
4
  class Query {
5
- /**
6
- * @var DB
7
- */
8
- public $db;
9
-
10
  /**
11
  * Hold the number of records found
12
  *
@@ -14,15 +9,6 @@ class Query {
14
  */
15
  public $found_records = 0;
16
 
17
- /**
18
- * Class constructor.
19
- *
20
- * @param DB $db The parent database class.
21
- */
22
- public function __construct( $db ) {
23
- $this->db = $db;
24
- }
25
-
26
  /**
27
  * Query records
28
  *
@@ -33,70 +19,6 @@ class Query {
33
  public function query( $args ) {
34
  global $wpdb;
35
 
36
- $defaults = array(
37
- // Search param
38
- 'search' => null,
39
- 'search_field' => 'summary',
40
- 'record_after' => null, // Deprecated, use date_after instead
41
- // Date-based filters
42
- 'date' => null, // Ex: 2015-07-01
43
- 'date_from' => null, // Ex: 2015-07-01
44
- 'date_to' => null, // Ex: 2015-07-01
45
- 'date_after' => null, // Ex: 2015-07-01T15:19:21+00:00
46
- 'date_before' => null, // Ex: 2015-07-01T15:19:21+00:00
47
- // Record ID filters
48
- 'record' => null,
49
- 'record__in' => array(),
50
- 'record__not_in' => array(),
51
- // Pagination params
52
- 'records_per_page' => get_option( 'posts_per_page', 20 ),
53
- 'paged' => 1,
54
- // Order
55
- 'order' => 'desc',
56
- 'orderby' => 'date',
57
- // Fields selection
58
- 'fields' => array(),
59
- );
60
-
61
- // Additional property fields
62
- $properties = array(
63
- 'user_id' => null,
64
- 'user_role' => null,
65
- 'ip' => null,
66
- 'object_id' => null,
67
- 'site_id' => null,
68
- 'blog_id' => null,
69
- 'connector' => null,
70
- 'context' => null,
71
- 'action' => null,
72
- );
73
-
74
- /**
75
- * Filter allows additional query properties to be added
76
- *
77
- * @return array Array of query properties
78
- */
79
- $properties = apply_filters( 'wp_stream_query_properties', $properties );
80
-
81
- // Add property fields to defaults, including their __in/__not_in variations
82
- foreach ( $properties as $property => $default ) {
83
- if ( ! isset( $defaults[ $property ] ) ) {
84
- $defaults[ $property ] = $default;
85
- }
86
-
87
- $defaults[ "{$property}__in" ] = array();
88
- $defaults[ "{$property}__not_in" ] = array();
89
- }
90
-
91
- $args = wp_parse_args( $args, $defaults );
92
-
93
- /**
94
- * Filter allows additional arguments to query $args
95
- *
96
- * @return array Array of query arguments
97
- */
98
- $args = apply_filters( 'wp_stream_query_args', $args );
99
-
100
  $join = '';
101
  $where = '';
102
 
@@ -305,20 +227,19 @@ class Query {
305
  */
306
  $query = apply_filters( 'wp_stream_db_query', $query, $args );
307
 
 
308
  /**
309
  * QUERY THE DATABASE FOR RESULTS
310
  */
311
- $results = $wpdb->get_results( $query ); // @codingStandardsIgnoreLine $query already prepared
312
-
313
- // Hold the number of records found
314
- $this->found_records = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) );
315
 
316
  // Add meta to the records, when applicable
317
  if ( empty( $fields ) || in_array( 'meta', $fields, true ) ) {
318
- $results = $this->add_record_meta( $results );
319
  }
320
 
321
- return (array) $results;
322
  }
323
 
324
  /**
2
  namespace WP_Stream;
3
 
4
  class Query {
 
 
 
 
 
5
  /**
6
  * Hold the number of records found
7
  *
9
  */
10
  public $found_records = 0;
11
 
 
 
 
 
 
 
 
 
 
12
  /**
13
  * Query records
14
  *
19
  public function query( $args ) {
20
  global $wpdb;
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  $join = '';
23
  $where = '';
24
 
227
  */
228
  $query = apply_filters( 'wp_stream_db_query', $query, $args );
229
 
230
+ $result = array();
231
  /**
232
  * QUERY THE DATABASE FOR RESULTS
233
  */
234
+ $result['items'] = $wpdb->get_results( $query ); // @codingStandardsIgnoreLine $query already prepared
235
+ $result['count'] = $result['items'] ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : 0;
 
 
236
 
237
  // Add meta to the records, when applicable
238
  if ( empty( $fields ) || in_array( 'meta', $fields, true ) ) {
239
+ $result['items'] = $this->add_record_meta( $result['items'] );
240
  }
241
 
242
+ return $result;
243
  }
244
 
245
  /**
classes/class-settings.php CHANGED
@@ -84,7 +84,12 @@ class Settings {
84
  'message' => esc_html__( 'There was an error in the request', 'stream' ),
85
  );
86
 
87
- $search = wp_unslash( trim( wp_stream_filter_input( INPUT_POST, 'find' ) ) );
 
 
 
 
 
88
 
89
  $request = (object) array(
90
  'find' => $search,
@@ -123,6 +128,7 @@ class Settings {
123
 
124
  $response->status = true;
125
  $response->message = '';
 
126
  $response->users = array();
127
  $users_added_to_response = array();
128
 
84
  'message' => esc_html__( 'There was an error in the request', 'stream' ),
85
  );
86
 
87
+ $search = '';
88
+ $input = wp_stream_filter_input( INPUT_POST, 'find' );
89
+
90
+ if ( ! isset( $input['term'] ) ) {
91
+ $search = wp_unslash( trim( $input['term'] ) );
92
+ }
93
 
94
  $request = (object) array(
95
  'find' => $search,
128
 
129
  $response->status = true;
130
  $response->message = '';
131
+ $response->roles = $this->get_roles();
132
  $response->users = array();
133
  $users_added_to_response = array();
134
 
connectors/class-connector-blogs.php CHANGED
@@ -31,6 +31,13 @@ class Connector_Blogs extends Connector {
31
  'update_blog_public',
32
  );
33
 
 
 
 
 
 
 
 
34
  /**
35
  * Return translated connector label
36
  *
31
  'update_blog_public',
32
  );
33
 
34
+ /**
35
+ * Register connector in the WP Frontend
36
+ *
37
+ * @var bool
38
+ */
39
+ public $register_frontend = false;
40
+
41
  /**
42
  * Return translated connector label
43
  *
connectors/class-connector-editor.php CHANGED
@@ -23,6 +23,13 @@ class Connector_Editor extends Connector {
23
  */
24
  private $edited_file = array();
25
 
 
 
 
 
 
 
 
26
  /**
27
  * Register all context hooks
28
  *
23
  */
24
  private $edited_file = array();
25
 
26
+ /**
27
+ * Register connector in the WP Frontend
28
+ *
29
+ * @var bool
30
+ */
31
+ public $register_frontend = false;
32
+
33
  /**
34
  * Register all context hooks
35
  *
connectors/class-connector-installer.php CHANGED
@@ -26,6 +26,13 @@ class Connector_Installer extends Connector {
26
  '_core_updated_successfully',
27
  );
28
 
 
 
 
 
 
 
 
29
  /**
30
  * Return translated connector label
31
  *
26
  '_core_updated_successfully',
27
  );
28
 
29
+ /**
30
+ * Register connector in the WP Frontend
31
+ *
32
+ * @var bool
33
+ */
34
+ public $register_frontend = false;
35
+
36
  /**
37
  * Return translated connector label
38
  *
connectors/class-connector-jetpack.php CHANGED
@@ -33,6 +33,13 @@ class Connector_Jetpack extends Connector {
33
  'wp_ajax_jetpack_post_by_email_disable',
34
  );
35
 
 
 
 
 
 
 
 
36
  /**
37
  * Tracked option keys
38
  *
33
  'wp_ajax_jetpack_post_by_email_disable',
34
  );
35
 
36
+ /**
37
+ * Register connector in the WP Frontend
38
+ *
39
+ * @var bool
40
+ */
41
+ public $register_frontend = false;
42
+
43
  /**
44
  * Tracked option keys
45
  *
connectors/class-connector-menus.php CHANGED
@@ -20,6 +20,13 @@ class Connector_Menus extends Connector {
20
  'delete_nav_menu',
21
  );
22
 
 
 
 
 
 
 
 
23
  /**
24
  * Return translated connector label
25
  *
20
  'delete_nav_menu',
21
  );
22
 
23
+ /**
24
+ * Register connector in the WP Frontend
25
+ *
26
+ * @var bool
27
+ */
28
+ public $register_frontend = false;
29
+
30
  /**
31
  * Return translated connector label
32
  *
connectors/class-connector-posts.php CHANGED
@@ -19,6 +19,13 @@ class Connector_Posts extends Connector {
19
  'deleted_post',
20
  );
21
 
 
 
 
 
 
 
 
22
  /**
23
  * Return translated connector label
24
  *
19
  'deleted_post',
20
  );
21
 
22
+ /**
23
+ * Register connector in the WP Frontend
24
+ *
25
+ * @var bool
26
+ */
27
+ public $register_frontend = false;
28
+
29
  /**
30
  * Return translated connector label
31
  *
connectors/class-connector-settings.php CHANGED
@@ -81,6 +81,13 @@ class Connector_Settings extends Connector {
81
  'new_admin_email',
82
  );
83
 
 
 
 
 
 
 
 
84
  /**
85
  * Register all context hooks
86
  *
81
  'new_admin_email',
82
  );
83
 
84
+ /**
85
+ * Register connector in the WP Frontend
86
+ *
87
+ * @var bool
88
+ */
89
+ public $register_frontend = false;
90
+
91
  /**
92
  * Register all context hooks
93
  *
connectors/class-connector-taxonomies.php CHANGED
@@ -35,6 +35,13 @@ class Connector_Taxonomies extends Connector {
35
  */
36
  public $context_labels;
37
 
 
 
 
 
 
 
 
38
  /**
39
  * Return translated connector label
40
  *
35
  */
36
  public $context_labels;
37
 
38
+ /**
39
+ * Register connector in the WP Frontend
40
+ *
41
+ * @var bool
42
+ */
43
+ public $register_frontend = false;
44
+
45
  /**
46
  * Return translated connector label
47
  *
includes/db-updates.php CHANGED
@@ -60,31 +60,40 @@ function wp_stream_update_auto_300( $db_version, $current_version ) {
60
  $plugin = wp_stream_get_instance();
61
  $plugin->install->install( $current_version );
62
 
63
- $stream_entries = $wpdb->get_results( "SELECT * FROM {$wpdb->base_prefix}stream_tmp" );
 
64
 
65
- foreach ( $stream_entries as $entry ) {
66
- $context = $wpdb->get_row(
67
- $wpdb->prepare( "SELECT * FROM {$wpdb->base_prefix}stream_context_tmp WHERE record_id = %s LIMIT 1", $entry->ID )
68
- );
69
-
70
- $new_entry = array(
71
- 'site_id' => $entry->site_id,
72
- 'blog_id' => $entry->blog_id,
73
- 'user_id' => $entry->author,
74
- 'user_role' => $entry->author_role,
75
- 'summary' => $entry->summary,
76
- 'created' => $entry->created,
77
- 'connector' => $context->connector,
78
- 'context' => $context->context,
79
- 'action' => $context->action,
80
- 'ip' => $entry->ip,
81
- );
82
-
83
- if ( $entry->object_id && 0 !== $entry->object_id ) {
84
- $new_entry['object_id'] = $entry->object_id;
 
 
 
 
 
 
85
  }
86
 
87
- $wpdb->insert( $wpdb->base_prefix . 'stream', $new_entry );
 
 
88
  }
89
 
90
  $wpdb->query( "DROP TABLE {$wpdb->base_prefix}stream_tmp, {$wpdb->base_prefix}stream_context_tmp" );
60
  $plugin = wp_stream_get_instance();
61
  $plugin->install->install( $current_version );
62
 
63
+ $starting_row = 0;
64
+ $rows_per_round = 5000;
65
 
66
+ $stream_entries = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->base_prefix}stream_tmp LIMIT %d, %d", $starting_row, $rows_per_round ) );
67
+
68
+ while ( ! empty( $stream_entries ) ) {
69
+ foreach ( $stream_entries as $entry ) {
70
+ $context = $wpdb->get_row(
71
+ $wpdb->prepare( "SELECT * FROM {$wpdb->base_prefix}stream_context_tmp WHERE record_id = %s LIMIT 1", $entry->ID )
72
+ );
73
+
74
+ $new_entry = array(
75
+ 'site_id' => $entry->site_id,
76
+ 'blog_id' => $entry->blog_id,
77
+ 'user_id' => $entry->author,
78
+ 'user_role' => $entry->author_role,
79
+ 'summary' => $entry->summary,
80
+ 'created' => $entry->created,
81
+ 'connector' => $context->connector,
82
+ 'context' => $context->context,
83
+ 'action' => $context->action,
84
+ 'ip' => $entry->ip,
85
+ );
86
+
87
+ if ( $entry->object_id && 0 !== $entry->object_id ) {
88
+ $new_entry['object_id'] = $entry->object_id;
89
+ }
90
+
91
+ $wpdb->insert( $wpdb->base_prefix . 'stream', $new_entry );
92
  }
93
 
94
+ $starting_row += $rows_per_round;
95
+
96
+ $stream_entries = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->base_prefix}stream_tmp LIMIT %d, %d", $starting_row, $rows_per_round ) );
97
  }
98
 
99
  $wpdb->query( "DROP TABLE {$wpdb->base_prefix}stream_tmp, {$wpdb->base_prefix}stream_context_tmp" );
includes/functions.php CHANGED
@@ -89,14 +89,16 @@ function wp_stream_json_encode( $data, $options = 0, $depth = 512 ) {
89
  /**
90
  * Return an array of sites for a network in a way that is also backwards compatible
91
  *
 
 
92
  * @return array
93
  */
94
- function wp_stream_get_sites() {
95
  if ( function_exists( 'get_sites' ) ) {
96
- $sites = get_sites();
97
  } else {
98
  $sites = array();
99
- foreach ( wp_get_sites() as $site ) {
100
  $sites[] = WP_Site::get_instance( $site['blog_id'] );
101
  }
102
  }
89
  /**
90
  * Return an array of sites for a network in a way that is also backwards compatible
91
  *
92
+ * @param string|array $args
93
+ *
94
  * @return array
95
  */
96
+ function wp_stream_get_sites( $args = array() ) {
97
  if ( function_exists( 'get_sites' ) ) {
98
+ $sites = get_sites( $args );
99
  } else {
100
  $sites = array();
101
+ foreach ( wp_get_sites( $args ) as $site ) {
102
  $sites[] = WP_Site::get_instance( $site['blog_id'] );
103
  }
104
  }
readme.txt CHANGED
@@ -2,8 +2,8 @@
2
  Contributors: fjarrett, lukecarbis, stream, xwp
3
  Tags: actions, activity, activity log, activity logs, admin actions, analytics, audit, audit log, audit logs, blackbox, black box, change, changes, dashboard, log, logs, stream, tracking, troubleshooting, wp stream
4
  Requires at least: 3.9
5
- Tested up to: 4.6
6
- Stable tag: 3.1.1
7
  License: GPLv2 or later
8
  License URI: https://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -79,6 +79,16 @@ Thank you for wanting to make Stream better for everyone!
79
 
80
  == Changelog ==
81
 
 
 
 
 
 
 
 
 
 
 
82
  = 3.1.1 - October 31, 2016 =
83
 
84
  * Fix: Hotfix for Error Updating Stream DB.
2
  Contributors: fjarrett, lukecarbis, stream, xwp
3
  Tags: actions, activity, activity log, activity logs, admin actions, analytics, audit, audit log, audit logs, blackbox, black box, change, changes, dashboard, log, logs, stream, tracking, troubleshooting, wp stream
4
  Requires at least: 3.9
5
+ Tested up to: 4.8
6
+ Stable tag: 3.2
7
  License: GPLv2 or later
8
  License URI: https://www.gnu.org/licenses/gpl-2.0.html
9
 
79
 
80
  == Changelog ==
81
 
82
+ = 3.2.0 - March 15, 2017 =
83
+
84
+ * New: Stream now support alternate Database Drivers. ([#889](https://github.com/xwp/stream/pull/889))
85
+ * Fix: Exclude dropdown menus ([e5c8677](https://github.com/xwp/stream/commit/e5c8677), [3626ba8](https://github.com/xwp/stream/commit/3626ba8), [e923a92](https://github.com/xwp/stream/commit/e923a92))
86
+ * Fix: Prevent loading of connectors on frontend ([ed3a635](https://github.com/xwp/stream/commit/ed3a635))
87
+ * Fix: Customizer performance issue ([#898](https://github.com/xwp/stream/pull/898))
88
+ * Fix: Various Network Admin bugs ([#899](https://github.com/xwp/stream/pull/899))
89
+ * Tweak: Codeclimate & Editorconfig support ([#896](https://github.com/xwp/stream/pull/896))
90
+ * Tweak: Better DB migration support ([#905](https://github.com/xwp/stream/pull/905))
91
+
92
  = 3.1.1 - October 31, 2016 =
93
 
94
  * Fix: Hotfix for Error Updating Stream DB.
stream.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: Stream
4
  * Plugin URI: https://wp-stream.com/
5
  * 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.
6
- * Version: 3.1.1
7
  * Author: XWP
8
  * Author URI: https://xwp.co/
9
  * License: GPLv2+
3
  * Plugin Name: Stream
4
  * Plugin URI: https://wp-stream.com/
5
  * 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.
6
+ * Version: 3.2.0
7
  * Author: XWP
8
  * Author URI: https://xwp.co/
9
  * License: GPLv2+
ui/css/admin.css CHANGED
@@ -18,28 +18,33 @@
18
  overflow: visible;
19
  }
20
 
21
- .post-type-wp_stream_alerts .select2 .select2-selection {
 
22
  border-color: #ccc;
23
  background: #f7f7f7;
24
  -webkit-box-shadow: 0 1px 0 #ccc;
25
  box-shadow: 0 1px 0 #ccc;
26
  }
27
 
28
- .post-type-wp_stream_alerts .select2 .select2-selection--multiple {
 
29
  font-size: 0;
30
  min-height: 28px;
31
  }
32
 
33
- .post-type-wp_stream_alerts .select2-container.select2-container--focus .select2-selection--multiple {
 
34
  border: solid #ccc 1px;
35
  }
36
 
37
- .post-type-wp_stream_alerts .select2-container .select2-selection--multiple .select2-selection__choice {
 
38
  margin-top: 4px;
39
  margin-bottom: 3px;
40
  }
41
 
42
- .post-type-wp_stream_alerts .select2 .select2-selection .select2-selection__rendered {
 
43
  color: #555;
44
  }
45
 
@@ -470,11 +475,16 @@
470
  border: 1px solid rgba(160,0,0,0.75);
471
  }
472
 
473
- .post-type-wp_stream_alerts .select2-results__option .parent {
 
 
 
 
 
474
  font-weight: bold;
475
  }
476
 
477
- .post-type-wp_stream_alerts .select2-results__option .child {
478
  padding-left: 8px;
479
  }
480
 
18
  overflow: visible;
19
  }
20
 
21
+ .post-type-wp_stream_alerts .select2 .select2-selection,
22
+ .stream-exclude-list .select2 .select2-selection{
23
  border-color: #ccc;
24
  background: #f7f7f7;
25
  -webkit-box-shadow: 0 1px 0 #ccc;
26
  box-shadow: 0 1px 0 #ccc;
27
  }
28
 
29
+ .post-type-wp_stream_alerts .select2 .select2-selection--multiple,
30
+ .stream-exclude-list .select2 .select2-selection--multiple{
31
  font-size: 0;
32
  min-height: 28px;
33
  }
34
 
35
+ .post-type-wp_stream_alerts .select2-container.select2-container--focus .select2-selection--multiple,
36
+ .stream-exclude-list .select2-container.select2-container--focus .select2-selection--multiple{
37
  border: solid #ccc 1px;
38
  }
39
 
40
+ .post-type-wp_stream_alerts .select2-container .select2-selection--multiple .select2-selection__choice,
41
+ .stream-exclude-list .select2-container .select2-selection--multiple .select2-selection__choice{
42
  margin-top: 4px;
43
  margin-bottom: 3px;
44
  }
45
 
46
+ .post-type-wp_stream_alerts .select2 .select2-selection .select2-selection__rendered,
47
+ .stream-exclude-list .select2 .select2-selection .select2-selection__rendered{
48
  color: #555;
49
  }
50
 
475
  border: 1px solid rgba(160,0,0,0.75);
476
  }
477
 
478
+ .stream-exclude-list .icon-users {
479
+ top: -3px !important;
480
+ position: relative !important;
481
+ }
482
+
483
+ .wp_stream_screen .select2-results__option .parent {
484
  font-weight: bold;
485
  }
486
 
487
+ .wp_stream_screen .select2-results__option .child {
488
  padding-left: 8px;
489
  }
490
 
ui/js/alerts.js CHANGED
@@ -50,13 +50,12 @@ jQuery( function( $ ) {
50
 
51
  return null;
52
  }
53
- }).change(function () {
54
  var value = $( this ).val();
55
- if (value) {
56
  var parts = value.split( '-' );
57
  $( this ).siblings( '.connector' ).val( parts[0] );
58
  $( this ).siblings( '.context' ).val( parts[1] );
59
- // $(this).removeAttr('name');
60
  }
61
  });
62
 
@@ -64,13 +63,13 @@ jQuery( function( $ ) {
64
  $( el ).siblings( '.connector' ).val(),
65
  $( el ).siblings( '.context' ).val()
66
  ];
67
- if ('' === parts[1]) {
68
  parts.splice( 1, 1 );
69
  }
70
  $( el ).val( parts.join( '-' ) ).trigger( 'change' );
71
  });
72
 
73
- $target.find( 'select.select2-select:not(.connector_or_context)' ).each(function () {
74
  var element_id_split = $( this ).attr( 'id' ).split( '_' );
75
  var select_name = element_id_split[element_id_split.length - 1].charAt( 0 ).toUpperCase() +
76
  element_id_split[element_id_split.length - 1].slice( 1 );
@@ -169,14 +168,14 @@ jQuery( function( $ ) {
169
  // Color taken from /wp-admin/css/forms.css
170
  // #pass-strength-result.strong
171
  add_new_alert.css( 'background-color', '#C1E1B9' );
172
- setTimeout(function () {
173
  add_new_alert.css( 'background-color', current_bg_color );
174
  }, 250);
175
 
176
- $( '#wp_stream_alert_type' ).change( function () {
177
  loadAlertSettings( $( this ).val() );
178
  });
179
- add_new_alert.on( 'click', '.button-secondary.cancel', function () {
180
  $( '#add-new-alert' ).remove();
181
  });
182
  add_new_alert.on( 'click', '.button-primary.save', save_new_alert );
50
 
51
  return null;
52
  }
53
+ }).change( function() {
54
  var value = $( this ).val();
55
+ if ( value ) {
56
  var parts = value.split( '-' );
57
  $( this ).siblings( '.connector' ).val( parts[0] );
58
  $( this ).siblings( '.context' ).val( parts[1] );
 
59
  }
60
  });
61
 
63
  $( el ).siblings( '.connector' ).val(),
64
  $( el ).siblings( '.context' ).val()
65
  ];
66
+ if ( '' === parts[1] ) {
67
  parts.splice( 1, 1 );
68
  }
69
  $( el ).val( parts.join( '-' ) ).trigger( 'change' );
70
  });
71
 
72
+ $target.find( 'select.select2-select:not(.connector_or_context)' ).each( function() {
73
  var element_id_split = $( this ).attr( 'id' ).split( '_' );
74
  var select_name = element_id_split[element_id_split.length - 1].charAt( 0 ).toUpperCase() +
75
  element_id_split[element_id_split.length - 1].slice( 1 );
168
  // Color taken from /wp-admin/css/forms.css
169
  // #pass-strength-result.strong
170
  add_new_alert.css( 'background-color', '#C1E1B9' );
171
+ setTimeout( function() {
172
  add_new_alert.css( 'background-color', current_bg_color );
173
  }, 250);
174
 
175
+ $( '#wp_stream_alert_type' ).change( function() {
176
  loadAlertSettings( $( this ).val() );
177
  });
178
+ add_new_alert.on( 'click', '.button-secondary.cancel', function() {
179
  $( '#add-new-alert' ).remove();
180
  });
181
  add_new_alert.on( 'click', '.button-primary.save', save_new_alert );
ui/js/exclude.js CHANGED
@@ -48,6 +48,14 @@ jQuery( function( $ ) {
48
 
49
  return null;
50
  }
 
 
 
 
 
 
 
 
51
  });
52
  });
53
 
@@ -76,38 +84,10 @@ jQuery( function( $ ) {
76
  };
77
  },
78
  processResults: function( response ) {
79
- var roles = [];
80
-
81
- $( 'option', $input_user ).each( function() {
82
- if ( $( this ).val() === '' ) {
83
- return;
84
- }
85
- if ( ! $.isNumeric( $( this ).val() ) ) {
86
- roles.push({
87
- 'id' : $( this ).val(),
88
- 'text' : $( this ).text()
89
- });
90
- }
91
- });
92
-
93
- roles = $.grep(
94
- roles,
95
- function( role ) {
96
- var roleVal = $input_user.data( 'select2' ).dropdown.$search
97
- .val()
98
- .toLowerCase();
99
- var rolePos = role
100
- .text
101
- .toLowerCase()
102
- .indexOf( roleVal );
103
- return rolePos >= 0;
104
- }
105
- );
106
-
107
  var answer = {
108
  results: [
109
  { text: '', id: '' },
110
- { text: 'Roles', children: roles },
111
  { text: 'Users', children: [] }
112
  ]
113
  };
@@ -116,15 +96,23 @@ jQuery( function( $ ) {
116
  return answer;
117
  }
118
 
119
- $.each( response.data.users, function( k, user ) {
120
- if ( $.contains( roles, user.id ) ) {
121
- user.disabled = true;
122
- }
 
 
 
 
 
 
 
123
  });
124
 
 
125
  answer.results[ 2 ].children = response.data.users;
126
 
127
- // Notice we return the value of more so Select2 knows if more results can be loaded
128
  return answer;
129
  }
130
  },
@@ -263,7 +251,8 @@ jQuery( function( $ ) {
263
  initSettingsSelect2();
264
 
265
  $( '.stream-exclude-list tr:not(.hidden) select.select2-select.author_or_role' ).each( function() {
266
- $( this ).val( $( this ).data( 'selected-id' ) ).trigger( 'change' );
 
267
  });
268
 
269
  $( '.stream-exclude-list tr:not(.hidden) select.select2-select.connector_or_context' ).each( function() {
@@ -342,6 +331,40 @@ jQuery( function( $ ) {
342
  recalculate_rules_selected();
343
  });
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  function recalculate_rules_selected() {
346
  var $selectedRows = $( 'table.stream-exclude-list tbody tr:not( .hidden ) input.cb-select:checked' ),
347
  $deleteButton = $( '#exclude_rules_remove_rules' );
48
 
49
  return null;
50
  }
51
+ }).on( 'change', function() {
52
+ var row = $( this ).closest( 'tr' ),
53
+ connector = $( this ).val();
54
+ if ( connector && 0 < connector.indexOf( '-' ) ) {
55
+ var connector_split = connector.split( '-' );
56
+ connector = connector_split[0];
57
+ }
58
+ getActions( row, connector );
59
  });
60
  });
61
 
84
  };
85
  },
86
  processResults: function( response ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  var answer = {
88
  results: [
89
  { text: '', id: '' },
90
+ { text: 'Roles', children: [] },
91
  { text: 'Users', children: [] }
92
  ]
93
  };
96
  return answer;
97
  }
98
 
99
+ if ( undefined === response.data.users || undefined === response.data.roles ) {
100
+ return answer;
101
+ }
102
+
103
+ var roles = [];
104
+
105
+ $.each( response.data.roles, function( id, text ) {
106
+ roles.push({
107
+ 'id' : id,
108
+ 'text' : text
109
+ });
110
  });
111
 
112
+ answer.results[ 1 ].children = roles;
113
  answer.results[ 2 ].children = response.data.users;
114
 
115
+ // Return the value of more so Select2 knows if more results can be loaded
116
  return answer;
117
  }
118
  },
251
  initSettingsSelect2();
252
 
253
  $( '.stream-exclude-list tr:not(.hidden) select.select2-select.author_or_role' ).each( function() {
254
+ var $option = $('<option selected>' + $( this ).data( 'selected-text' ) + '</option>').val( $( this ).data( 'selected-id' ) );
255
+ $( this ).append( $option ).trigger( 'change' );
256
  });
257
 
258
  $( '.stream-exclude-list tr:not(.hidden) select.select2-select.connector_or_context' ).each( function() {
331
  recalculate_rules_selected();
332
  });
333
 
334
+ function getActions( row, connector ) {
335
+ var trigger_action = $( '.select2-select.action', row ),
336
+ action_value = trigger_action.val();
337
+
338
+ trigger_action.empty();
339
+ trigger_action.prop( 'disabled', true );
340
+
341
+ var placeholder = $( '<option/>', {value: '', text: ''} );
342
+ trigger_action.append( placeholder );
343
+
344
+ var data = {
345
+ 'action' : 'get_actions',
346
+ 'connector' : connector
347
+ };
348
+
349
+ $.post( window.ajaxurl, data, function( response ) {
350
+ var success = response.success,
351
+ actions = response.data;
352
+ if ( ! success ) {
353
+ return;
354
+ }
355
+ for ( var key in actions ) {
356
+ if ( actions.hasOwnProperty( key ) ) {
357
+ var value = actions[key];
358
+ var option = $( '<option/>', {value: key, text: value} );
359
+ trigger_action.append( option );
360
+ }
361
+ }
362
+ trigger_action.val( action_value );
363
+ trigger_action.prop( 'disabled', false );
364
+ $( document ).trigger( 'alert-actions-updated' );
365
+ });
366
+ }
367
+
368
  function recalculate_rules_selected() {
369
  var $selectedRows = $( 'table.stream-exclude-list tbody tr:not( .hidden ) input.cb-select:checked' ),
370
  $deleteButton = $( '#exclude_rules_remove_rules' );