| @@ -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 | } |