| @@ -24,8 +24,28 @@ | ||
| 24 | 24 | */ |
| 25 | 25 | private $table = 'kit_form_entries'; |
| 26 | 26 | |
| 27 | 27 | /** |
| 28 | + * Holds the columns that entries can be ordered by. | |
| 29 | + * | |
| 30 | + * @since 3.4.6 | |
| 31 | + * | |
| 32 | + * @var array | |
| 33 | + */ | |
| 34 | + private $order_by_columns = array( | |
| 35 | + 'id', | |
| 36 | + 'post_id', | |
| 37 | + 'first_name', | |
| 38 | + 'email', | |
| 39 | + 'form_id', | |
| 40 | + 'tag_id', | |
| 41 | + 'sequence_id', | |
| 42 | + 'created_at', | |
| 43 | + 'updated_at', | |
| 44 | + 'api_result', | |
| 45 | + ); | |
| 46 | + | |
| 47 | + /** | |
| 28 | 48 | * Create database table. |
| 29 | 49 | * |
| 30 | 50 | * @since 3.0.0 |
| 31 | 51 | * |
| @@ -35,10 +55,10 @@ | ||
| 35 | 55 | |
| 36 | 56 | global $wpdb; |
| 37 | 57 | |
| 38 | 58 | // Create database table. |
| 39 | - $query = $wpdb->prepare( | |
| 40 | - "CREATE TABLE IF NOT EXISTS %i ( | |
| 59 | + // The table name isn't prepared, as the %i placeholder requires WordPress 6.2+. | |
| 60 | + $query = "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}{$this->table}` ( | |
| 41 | 61 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, |
| 42 | 62 | `post_id` int(11) NOT NULL, |
| 43 | 63 | `first_name` varchar(191) NOT NULL DEFAULT '', |
| 44 | 64 | `email` varchar(191) NOT NULL DEFAULT '', |
| @@ -57,11 +77,9 @@ | ||
| 57 | 77 | KEY `form_id` (`form_id`), |
| 58 | 78 | KEY `tag_id` (`tag_id`), |
| 59 | 79 | KEY `sequence_id` (`sequence_id`), |
| 60 | 80 | KEY `api_result` (`api_result`) |
| 61 | - )", | |
| 62 | - $wpdb->prefix . $this->table | |
| 63 | - ); | |
| 81 | + )"; | |
| 64 | 82 | $query .= ' ' . $wpdb->get_charset_collate() . ' AUTO_INCREMENT=1'; |
| 65 | 83 | $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 66 | 84 | |
| 67 | 85 | } |
| @@ -157,9 +175,9 @@ | ||
| 157 | 175 | array( 'id' => $id ) |
| 158 | 176 | ); |
| 159 | 177 | |
| 160 | 178 | // Return the entry ID. |
| 161 | - return $wpdb->insert_id; | |
| 179 | + return (int) $id; | |
| 162 | 180 | |
| 163 | 181 | } |
| 164 | 182 | |
| 165 | 183 | /** |
| @@ -197,10 +215,9 @@ | ||
| 197 | 215 | |
| 198 | 216 | // Check if an entry already exists for the given Post ID and Email. |
| 199 | 217 | $id = $wpdb->get_var( |
| 200 | 218 | $wpdb->prepare( |
| 201 | - 'SELECT id FROM %i WHERE post_id = %d AND email = %s', | |
| 202 | - $wpdb->prefix . $this->table, | |
| 219 | + "SELECT id FROM `{$wpdb->prefix}{$this->table}` WHERE post_id = %d AND email = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 203 | 220 | $entry['post_id'], |
| 204 | 221 | $entry['email'] |
| 205 | 222 | ) |
| 206 | 223 | ); |
| @@ -263,12 +280,12 @@ | ||
| 263 | 280 | return ''; |
| 264 | 281 | } |
| 265 | 282 | |
| 266 | 283 | $csv = array( |
| 267 | - '"' . implode( '","', array_keys( $entries[0] ) ) . '"', | |
| 284 | + $this->get_csv_row( array_keys( $entries[0] ) ), | |
| 268 | 285 | ); |
| 269 | 286 | foreach ( $entries as $entry ) { |
| 270 | - $csv[] = '"' . implode( '","', $entry ) . '"'; | |
| 287 | + $csv[] = $this->get_csv_row( $entry ); | |
| 271 | 288 | } |
| 272 | 289 | |
| 273 | 290 | return implode( "\n", $csv ); |
| 274 | 291 | |
| @@ -274,8 +291,35 @@ | ||
| 274 | 291 | |
| 275 | 292 | } |
| 276 | 293 | |
| 277 | 294 | /** |
| 295 | + * Returns a CSV row for the given values, escaping double quotes and | |
| 296 | + * values that spreadsheet applications would run as a formula. | |
| 297 | + * | |
| 298 | + * @since 3.4.6 | |
| 299 | + * | |
| 300 | + * @param array $values Values. | |
| 301 | + * @return string | |
| 302 | + */ | |
| 303 | + private function get_csv_row( $values ) { | |
| 304 | + | |
| 305 | + $row = array(); | |
| 306 | + foreach ( $values as $value ) { | |
| 307 | + $value = (string) $value; | |
| 308 | + | |
| 309 | + // Prefix values starting with a formula character, so they're treated as text. | |
| 310 | + if ( $value !== '' && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) { | |
| 311 | + $value = "'" . $value; | |
| 312 | + } | |
| 313 | + | |
| 314 | + $row[] = '"' . str_replace( '"', '""', $value ) . '"'; | |
| 315 | + } | |
| 316 | + | |
| 317 | + return implode( ',', $row ); | |
| 318 | + | |
| 319 | + } | |
| 320 | + | |
| 321 | + /** | |
| 278 | 322 | * Searches entries by the given key/value pairs |
| 279 | 323 | * |
| 280 | 324 | * @since 3.0.0 |
| 281 | 325 | * |
| @@ -291,12 +335,9 @@ | ||
| 291 | 335 | |
| 292 | 336 | global $wpdb; |
| 293 | 337 | |
| 294 | 338 | // Prepare query. |
| 295 | - $query = $wpdb->prepare( | |
| 296 | - 'SELECT * FROM %i', | |
| 297 | - $wpdb->prefix . $this->table | |
| 298 | - ); | |
| 339 | + $query = "SELECT * FROM `{$wpdb->prefix}{$this->table}`"; | |
| 299 | 340 | |
| 300 | 341 | // Build where clauses. |
| 301 | 342 | $where_clauses = $this->build_where_clauses( $search, $api_result ); |
| 302 | 343 | |
| @@ -304,15 +345,15 @@ | ||
| 304 | 345 | if ( count( $where_clauses ) ) { |
| 305 | 346 | $query .= ' WHERE ' . implode( ' AND ', $where_clauses ); |
| 306 | 347 | } |
| 307 | 348 | |
| 349 | + // Fallback to ordering by created_at if the order by column is invalid. | |
| 350 | + if ( ! in_array( $order_by, $this->order_by_columns, true ) ) { | |
| 351 | + $order_by = 'created_at'; | |
| 352 | + } | |
| 353 | + | |
| 308 | 354 | // Order. |
| 309 | - $query .= $wpdb->prepare( | |
| 310 | - ' ORDER BY %i.%i', | |
| 311 | - $wpdb->prefix . $this->table, | |
| 312 | - $order_by | |
| 313 | - ); | |
| 314 | - $query .= ' ' . ( strtolower( $order ) === 'asc' ? 'ASC' : 'DESC' ); | |
| 355 | + $query .= " ORDER BY `{$order_by}` " . ( strtolower( $order ) === 'asc' ? 'ASC' : 'DESC' ); | |
| 315 | 356 | |
| 316 | 357 | // Limit. |
| 317 | 358 | if ( $page > 0 && $per_page > 0 ) { |
| 318 | 359 | $query .= $wpdb->prepare( ' LIMIT %d, %d', ( ( $page - 1 ) * $per_page ), $per_page ); |
| @@ -336,13 +377,9 @@ | ||
| 336 | 377 | |
| 337 | 378 | global $wpdb; |
| 338 | 379 | |
| 339 | 380 | // Prepare query. |
| 340 | - $query = $wpdb->prepare( | |
| 341 | - 'SELECT COUNT(%i.id) FROM %i', | |
| 342 | - $wpdb->prefix . $this->table, | |
| 343 | - $wpdb->prefix . $this->table | |
| 344 | - ); | |
| 381 | + $query = "SELECT COUNT(id) FROM `{$wpdb->prefix}{$this->table}`"; | |
| 345 | 382 | |
| 346 | 383 | // Build where clauses. |
| 347 | 384 | $where_clauses = $this->build_where_clauses( $search, $api_result ); |
| 348 | 385 | |
| @@ -374,10 +411,10 @@ | ||
| 374 | 411 | // Add search clause. |
| 375 | 412 | if ( $search ) { |
| 376 | 413 | $where_clauses[] = $wpdb->prepare( |
| 377 | 414 | '(first_name LIKE %s OR email LIKE %s)', |
| 378 | - '%' . $search . '%', | |
| 379 | - '%' . $search . '%' | |
| 415 | + '%' . $wpdb->esc_like( $search ) . '%', | |
| 416 | + '%' . $wpdb->esc_like( $search ) . '%' | |
| 380 | 417 | ); |
| 381 | 418 | } |
| 382 | 419 | |
| 383 | 420 | // Add API result clause. |
| @@ -418,14 +455,22 @@ | ||
| 418 | 455 | * |
| 419 | 456 | * @since 3.0.0 |
| 420 | 457 | * |
| 421 | 458 | * @param array $ids Entry IDs. |
| 422 | - * @return bool Success | |
| 459 | + * @return int|bool Number of entries deleted, or false on error | |
| 423 | 460 | */ |
| 424 | 461 | public function delete_by_ids( $ids ) { |
| 425 | 462 | |
| 426 | 463 | global $wpdb; |
| 427 | 464 | |
| 465 | + // Map IDs as integers. | |
| 466 | + $ids = array_values( array_filter( array_map( 'absint', $ids ) ) ); | |
| 467 | + | |
| 468 | + // Bail if no IDs are provided. | |
| 469 | + if ( empty( $ids ) ) { | |
| 470 | + return 0; | |
| 471 | + } | |
| 472 | + | |
| 428 | 473 | return $wpdb->query( |
| 429 | 474 | $wpdb->prepare( |
| 430 | 475 | sprintf( |
| 431 | 476 | 'DELETE FROM %s WHERE id IN (%s)', |
| @@ -448,14 +493,9 @@ | ||
| 448 | 493 | public function delete_all() { |
| 449 | 494 | |
| 450 | 495 | global $wpdb; |
| 451 | 496 | |
| 452 | - return $wpdb->query( | |
| 453 | - $wpdb->prepare( | |
| 454 | - 'TRUNCATE TABLE %i', | |
| 455 | - $wpdb->prefix . $this->table | |
| 456 | - ) | |
| 457 | - ); | |
| 497 | + return $wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}{$this->table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 458 | 498 | |
| 459 | 499 | } |
| 460 | 500 | |
| 461 | 501 | } |