| @@ -57,10 +57,11 @@ | ||
| 57 | 57 | 'callback' => [ $this , 'handle_popup_submission' ], |
| 58 | 58 | 'permission_callback' => '__return_true', |
| 59 | 59 | 'args' => [ |
| 60 | 60 | 'nx_id' => [ |
| 61 | - 'required' => true, | |
| 62 | - 'type' => 'string', | |
| 61 | + 'required' => true, | |
| 62 | + 'type' => 'integer', | |
| 63 | + 'sanitize_callback' => 'absint', | |
| 63 | 64 | ], |
| 64 | 65 | 'email' => [ |
| 65 | 66 | 'type' => 'string', |
| 66 | 67 | 'sanitize_callback' => 'sanitize_email', |
| @@ -72,10 +73,22 @@ | ||
| 72 | 73 | 'name' => [ |
| 73 | 74 | 'type' => 'string', |
| 74 | 75 | 'sanitize_callback' => 'sanitize_textarea_field', |
| 75 | 76 | ], |
| 77 | + // `title` and `theme` are persisted into the entry data and later | |
| 78 | + // shown in the admin Feedback Entries screen and CSV export, so | |
| 79 | + // sanitize them on the way in instead of storing raw input. | |
| 80 | + 'title' => [ | |
| 81 | + 'type' => 'string', | |
| 82 | + 'sanitize_callback' => 'sanitize_text_field', | |
| 83 | + ], | |
| 84 | + 'theme' => [ | |
| 85 | + 'type' => 'string', | |
| 86 | + 'sanitize_callback' => 'sanitize_text_field', | |
| 87 | + ], | |
| 76 | 88 | 'timestamp' => [ |
| 77 | - 'type' => 'integer', | |
| 89 | + 'type' => 'integer', | |
| 90 | + 'sanitize_callback' => 'absint', | |
| 78 | 91 | ], |
| 79 | 92 | ], |
| 80 | 93 | ]); |
| 81 | 94 | |
| @@ -217,16 +230,20 @@ | ||
| 217 | 230 | $where_clause = implode(' AND ', $where_conditions); |
| 218 | 231 | |
| 219 | 232 | // Get total count for pagination |
| 220 | 233 | $total_query = $wpdb->prepare( |
| 234 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- 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. | |
| 221 | 235 | "SELECT COUNT(*) FROM {$table_name} e WHERE {$where_clause}", |
| 222 | 236 | ...$where_values |
| 223 | 237 | ); |
| 238 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, 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. | |
| 224 | 239 | $total_items = (int) $wpdb->get_var($total_query); |
| 225 | 240 | |
| 226 | 241 | // Get paginated entries with notification information |
| 227 | 242 | $posts_table = $wpdb->prefix . 'nx_posts'; |
| 243 | + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- 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 | 244 | $entries_query = $wpdb->prepare( |
| 245 | + // phpcs:disable 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. | |
| 229 | 246 | "SELECT e.*, p.title as notification_name, p.nx_id as notification_id |
| 230 | 247 | FROM {$table_name} e |
| 231 | 248 | LEFT JOIN {$posts_table} p ON e.nx_id = p.nx_id |
| 232 | 249 | WHERE {$where_clause} |
| @@ -233,8 +250,10 @@ | ||
| 233 | 250 | ORDER BY e.created_at DESC |
| 234 | 251 | LIMIT %d OFFSET %d", |
| 235 | 252 | ...array_merge($where_values, [$per_page, $offset]) |
| 236 | 253 | ); |
| 254 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 255 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, 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. | |
| 237 | 256 | $entries = $wpdb->get_results($entries_query, ARRAY_A); |
| 238 | 257 | |
| 239 | 258 | $formatted_entries = []; |
| 240 | 259 | foreach ($entries as $entry) { |
| @@ -277,11 +296,13 @@ | ||
| 277 | 296 | |
| 278 | 297 | $sources = $this->form_sources(); |
| 279 | 298 | $src_placeholders = implode(',', array_fill(0, count($sources), '%s')); |
| 280 | 299 | $delete_query = $wpdb->prepare( |
| 300 | + // phpcs:ignore 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. | |
| 281 | 301 | "DELETE FROM {$table_name} WHERE entry_id = %d AND source IN ({$src_placeholders})", |
| 282 | 302 | array_merge([$entry_id], $sources) |
| 283 | 303 | ); |
| 304 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, 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. | |
| 284 | 305 | $result = $wpdb->query($delete_query); |
| 285 | 306 | |
| 286 | 307 | if ($result === false) { |
| 287 | 308 | return new \WP_REST_Response([ |
| @@ -332,12 +353,14 @@ | ||
| 332 | 353 | $src_placeholders = implode(',', array_fill(0, count($sources), '%s')); |
| 333 | 354 | |
| 334 | 355 | // Prepare the query with source filter |
| 335 | 356 | $query = $wpdb->prepare( |
| 357 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- 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. | |
| 336 | 358 | "DELETE FROM {$table_name} WHERE entry_id IN ({$placeholders}) AND source IN ({$src_placeholders})", |
| 337 | 359 | array_merge($entry_ids, $sources) |
| 338 | 360 | ); |
| 339 | 361 | |
| 362 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, 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. | |
| 340 | 363 | $result = $wpdb->query($query); |
| 341 | 364 | |
| 342 | 365 | if ($result === false) { |
| 343 | 366 | return new \WP_REST_Response([ |
| @@ -394,8 +417,9 @@ | ||
| 394 | 417 | |
| 395 | 418 | // Get all entries for export (no pagination) |
| 396 | 419 | $posts_table = $wpdb->prefix . 'nx_posts'; |
| 397 | 420 | $query = $wpdb->prepare( |
| 421 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- 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. | |
| 398 | 422 | "SELECT e.entry_id, e.nx_id, e.data, e.created_at, p.title as notification_name |
| 399 | 423 | FROM {$table_name} e |
| 400 | 424 | LEFT JOIN {$posts_table} p ON e.nx_id = p.nx_id |
| 401 | 425 | WHERE {$where_clause} |
| @@ -402,9 +426,11 @@ | ||
| 402 | 426 | ORDER BY e.created_at DESC", |
| 403 | 427 | ...$where_values |
| 404 | 428 | ); |
| 405 | 429 | |
| 430 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, 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. | |
| 406 | 431 | $entries = $wpdb->get_results($query, ARRAY_A); |
| 432 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare | |
| 407 | 433 | |
| 408 | 434 | if (empty($entries)) { |
| 409 | 435 | return new \WP_REST_Response([ |
| 410 | 436 | 'success' => false, |
| @@ -415,9 +441,9 @@ | ||
| 415 | 441 | // Generate CSV content |
| 416 | 442 | $csv_data = $this->generate_csv_data($entries); |
| 417 | 443 | |
| 418 | 444 | // Generate filename |
| 419 | - $filename = 'notificationx-feedback-entries-' . date('Y-m-d-H-i-s') . '.csv'; | |
| 445 | + $filename = 'notificationx-feedback-entries-' . gmdate('Y-m-d-H-i-s') . '.csv'; | |
| 420 | 446 | |
| 421 | 447 | return new \WP_REST_Response([ |
| 422 | 448 | 'success' => true, |
| 423 | 449 | 'csv_content' => $csv_data, |
| @@ -422,8 +448,9 @@ | ||
| 422 | 448 | 'success' => true, |
| 423 | 449 | 'csv_content' => $csv_data, |
| 424 | 450 | 'filename' => $filename, |
| 425 | 451 | 'total_entries' => count($entries), |
| 452 | + /* translators: %d: number of entries prepared for export */ | |
| 426 | 453 | 'message' => sprintf(__('Successfully prepared %d entries for export', 'notificationx'), count($entries)) |
| 427 | 454 | ], 200); |
| 428 | 455 | } |
| 429 | 456 | |
| @@ -461,8 +488,9 @@ | ||
| 461 | 488 | |
| 462 | 489 | $row = [ |
| 463 | 490 | $counter++, |
| 464 | 491 | $date->format('F j, Y'), |
| 492 | + /* translators: %d: notification ID */ | |
| 465 | 493 | $entry['notification_name'] ?: sprintf(__('Notification #%d', 'notificationx'), $entry['nx_id']), |
| 466 | 494 | ]; |
| 467 | 495 | |
| 468 | 496 | if ($is_pro) { |