PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.1
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.1
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
← All changes | includes/Core/Database.php +30 -4 3.2.73.3.1 View file →
@@ -110,16 +110,19 @@
110 110 public function update_analytics( $col, $id, $date, $data = null ) {
111 111 $table_name = self::$table_stats;
112 112 $_data = is_null( $data ) ? 1 : intval( $data );
113 113
114 + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
114 115 return $this->wpdb->query( $this->wpdb->prepare( '
115 116 UPDATE %1$s
116 117 SET `%2$s` = `%3$s` + %4$s
117 118 WHERE nx_id = "%5$s"
118 119 AND created_at = "%6$s"',
120 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
119 121 $table_name, esc_sql( $col ), esc_sql( $col ), $_data, intval( $id ), $date
120 122 )
121 123 );
124 + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder
122 125 }
123 126
124 127 public function insert_post( $table_name, $post, $format = null ) {
125 128 $post = $this->serialize_data( $post );
@@ -131,9 +134,26 @@
131 134 if ( ! empty( $posts[0] ) ) {
132 135 $values = array();
133 136 $place_holders = array();
134 137 $_column = array_keys( $posts[0] );
135 - $columns = implode( ', ', $_column );
138 + /*
139 + * Column names are interpolated into the statement, so prepare() below
140 + * cannot protect them - it only binds the values. Callers such as the
141 + * /import REST route take these keys straight from a user-supplied JSON
142 + * payload, so accept identifiers only and quote them.
143 + */
144 + $_column = array_values(
145 + array_filter(
146 + $_column,
147 + function ( $col ) {
148 + return is_string( $col ) && preg_match( '/^[A-Za-z0-9_]+$/', $col );
149 + }
150 + )
151 + );
152 + if ( empty( $_column ) ) {
153 + return;
154 + }
155 + $columns = '`' . implode( '`, `', $_column ) . '`';
136 156 $query = "INSERT INTO $table_name ($columns) VALUES ";
137 157 foreach ( $posts as $key => $entry ) {
138 158 $entry = $this->serialize_data( $entry );
139 159 reset( $_column );
@@ -149,9 +169,11 @@
149 169 // $values = array_merge($values, array_values($entry));
150 170 $place_holders[] = '(' . implode( ', ', $_place_holders ) . ')'; /* In my case, i know they will always be integers */
151 171 }
152 172 $query .= implode( ', ', $place_holders );
173 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Values are bound by prepare() below; the column list is whitelisted to /^[A-Za-z0-9_]+$/ identifiers above, which is what stops the injection this used to allow.
153 174 $query = $this->wpdb->prepare( "$query", $values );
175 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared -- $query is the prepare()d statement built above, with the column list whitelisted to identifiers.
154 176 $this->wpdb->query( $query );
155 177 }
156 178 }
157 179
@@ -181,8 +203,9 @@
181 203 $query .= $this->get_where_query( $wheres );
182 204 if ( ! empty( $group_by_col ) ) {
183 205 $query .= " GROUP BY $group_by_col";
184 206 }
207 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
185 208 $posts = $this->wpdb->get_results( "$query $extra_query", ARRAY_A );
186 209 $posts = array_map( [ $this, 'unserialize_data' ], $posts );
187 210 return $posts;
188 211 }
@@ -224,8 +247,9 @@
224 247 $limit = absint( $limit );
225 248 $query = "DELETE FROM `$table_name` ";
226 249 $query .= $this->get_where_query( $wheres );
227 250 $query .= " LIMIT $limit";
251 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
228 252 return $this->wpdb->query( $query );
229 253 }
230 254
231 255 public function serialize_data( $post ) {
@@ -233,12 +257,12 @@
233 257 $post['data'] = maybe_serialize( $post['data'] );
234 258 }
235 259 // created_at and updated_at if not empty convert to mysql date
236 260 if ( ! empty( $post['created_at'] ) ) {
237 - $post['created_at'] = date( 'Y-m-d H:i:s', strtotime( $post['created_at'] ) );
261 + $post['created_at'] = gmdate( 'Y-m-d H:i:s', strtotime( $post['created_at'] ) );
238 262 }
239 263 if ( ! empty( $post['updated_at'] ) ) {
240 - $post['updated_at'] = date( 'Y-m-d H:i:s', strtotime( $post['updated_at'] ) );
264 + $post['updated_at'] = gmdate( 'Y-m-d H:i:s', strtotime( $post['updated_at'] ) );
241 265 }
242 266 return $post;
243 267 }
244 268
@@ -277,9 +301,9 @@
277 301 $value = $this->wpdb->prepare( '%s AND %s', $value[1], $value[2] );
278 302 } elseif ( in_array( $compare, [ '<', '<=', '>', '>=' ], true ) ) {
279 303 $value = "'" . esc_sql( $value[1] ) . "'";
280 304 } else {
281 - throw new \Exception( "Unknown parameter $compare.", 1 );
305 + throw new \Exception( esc_html( "Unknown parameter $compare." ), 1 );
282 306 }
283 307 } else {
284 308 $value = "'" . esc_sql( $value ) . "'"; // is_bool($value) ? $value :.
285 309 }
@@ -289,8 +313,9 @@
289 313 return $query;
290 314 }
291 315
292 316 public function update_option( $key, $value, $autoload = 'no' ) {
317 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
293 318 $is_exists = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}options WHERE option_name=%s LIMIT 1", $key ) );
294 319 if ( $is_exists ) {
295 320 if ( $is_exists->option_value == $value ) {
296 321 return;
@@ -304,8 +329,9 @@
304 329 );
305 330 }
306 331 }
307 332 public function get_option( $key, $default = false ) {
333 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
308 334 $results = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->options} WHERE option_name=%s LIMIT 1", $key ) );
309 335 if ( $results ) {
310 336 return ! empty( $results->option_value ) ? $results->option_value : $default;
311 337 }