PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.0
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.0
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
notificationx / includes / Core / Rest / Popup.php

Popup.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.3.0, at includes/Core/Rest/Popup.php

516 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace NotificationX\Core\Rest;
4
5 use NotificationX\GetInstance;
6 use NotificationX\Core\PopupNotification;
7 use NotificationX\Extensions\Popup\PopupNotification as PopupPopupNotification;
8 use NotificationX\NotificationX;
9 use WP_REST_Server;
10
11 /**
12 * @method static Popup get_instance($args = null)
13 */
14 class Popup {
15 /**
16 * Instance of Popup
17 *
18 * @var Popup
19 */
20 use GetInstance;
21 public $namespace;
22 public $rest_base;
23 public $id = 'popup_notification';
24
25 /**
26 * Sources that share the popup-submit form pipeline.
27 * Entries from these sources are listed/managed together in the Feedback Entries screen.
28 */
29 private function form_sources() {
30 return [ 'popup_notification', 'exit_intent_custom' ];
31 }
32
33 /**
34 * Constructor.
35 *
36 * @since 4.7.0
37 *
38 * @param string $post_type Post type.
39 */
40 public function __construct() {
41 $this->namespace = 'notificationx/v1';
42 $this->rest_base = 'popup-submit';
43 add_action('rest_api_init', [$this, 'register_routes']);
44 }
45
46
47 /**
48 * Registers the routes for the objects of the controller.
49 *
50 * @since 3.1.12
51 *
52 * @see register_rest_route()
53 */
54 public function register_routes() {
55 register_rest_route('notificationx/v1', '/popup-submit', [
56 'methods' => 'POST',
57 'callback' => [ $this , 'handle_popup_submission' ],
58 'permission_callback' => '__return_true',
59 'args' => [
60 'nx_id' => [
61 'required' => true,
62 'type' => 'integer',
63 'sanitize_callback' => 'absint',
64 ],
65 'email' => [
66 'type' => 'string',
67 'sanitize_callback' => 'sanitize_email',
68 ],
69 'message' => [
70 'type' => 'string',
71 'sanitize_callback' => 'sanitize_textarea_field',
72 ],
73 'name' => [
74 'type' => 'string',
75 'sanitize_callback' => 'sanitize_textarea_field',
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 ],
88 'timestamp' => [
89 'type' => 'integer',
90 'sanitize_callback' => 'absint',
91 ],
92 ],
93 ]);
94
95 // Feedback entries endpoint
96 register_rest_route('notificationx/v1', '/feedback-entries', [
97 'methods' => 'GET',
98 'callback' => [$this, 'get_feedback_entries'],
99 'permission_callback' => function() {
100 return current_user_can('read_notificationx');
101 },
102 'args' => [
103 'page' => [
104 'default' => 1,
105 'type' => 'integer',
106 'minimum' => 1,
107 ],
108 'per_page' => [
109 'default' => 20,
110 'type' => 'integer',
111 'minimum' => 1,
112 'maximum' => 200,
113 ],
114 's' => [
115 'default' => '',
116 'type' => 'string',
117 'sanitize_callback' => 'sanitize_text_field',
118 ],
119 'notification_id' => [
120 'default' => '',
121 'type' => 'string',
122 'sanitize_callback' => 'sanitize_text_field',
123 ],
124 ],
125 ]);
126
127 // Delete feedback entry endpoint
128 register_rest_route('notificationx/v1', '/feedback-entries/(?P<id>\d+)', [
129 'methods' => 'DELETE',
130 'callback' => [$this, 'delete_feedback_entry'],
131 'permission_callback' => function() {
132 return current_user_can('edit_notificationx');
133 },
134 'args' => [
135 'id' => [
136 'required' => true,
137 'type' => 'integer',
138 ],
139 ],
140 ]);
141
142 // Bulk delete feedback entries endpoint
143 register_rest_route('notificationx/v1', '/feedback-entries/bulk-delete', [
144 'methods' => 'POST',
145 'callback' => [$this, 'bulk_delete_feedback_entries'],
146 'permission_callback' => function() {
147 return current_user_can('edit_notificationx');
148 },
149 'args' => [
150 'ids' => [
151 'required' => true,
152 'type' => 'array',
153 'items' => [
154 'type' => 'integer',
155 ],
156 ],
157 ],
158 ]);
159
160 // Export feedback entries endpoint
161 register_rest_route('notificationx/v1', '/feedback-entries/export', [
162 'methods' => 'POST',
163 'callback' => [$this, 'export_feedback_entries'],
164 'permission_callback' => function() {
165 return current_user_can('read_notificationx');
166 },
167 'args' => [
168 's' => [
169 'required' => false,
170 'type' => 'string',
171 ],
172 'notification_id' => [
173 'required' => false,
174 'type' => 'string',
175 ],
176 ],
177 ]);
178 }
179
180 /**
181 * Handle popup form submission
182 *
183 * @param WP_REST_Request $request
184 * @return WP_REST_Response
185 */
186 public function handle_popup_submission($request) {
187 $popup = PopupPopupNotification::get_instance();
188 return $popup->handle_popup_submission($request);
189 }
190
191
192 /**
193 * Get feedback entries
194 *
195 * @param WP_REST_Request $request
196 * @return WP_REST_Response
197 */
198 public function get_feedback_entries($request) {
199 global $wpdb;
200
201 $table_name = $wpdb->prefix . 'nx_entries';
202
203 // Get pagination parameters
204 $page = $request->get_param('page') ?: 1;
205 $per_page = $request->get_param('per_page') ?: 20;
206 $search = $request->get_param('s') ?: '';
207 $notification_id = $request->get_param('notification_id') ?: '';
208 $offset = ($page - 1) * $per_page;
209
210 // Build WHERE clause — include both popup and exit-intent submissions
211 $sources = $this->form_sources();
212 $src_placeholders = implode(',', array_fill(0, count($sources), '%s'));
213 $where_conditions = ["e.source IN ({$src_placeholders})"];
214 $where_values = $sources;
215
216 // Add notification filter
217 if (!empty($notification_id)) {
218 $where_conditions[] = "e.nx_id = %d";
219 $where_values[] = intval($notification_id);
220 }
221
222 // Add search functionality
223 if (!empty($search)) {
224 $where_conditions[] = "(e.data LIKE %s OR e.created_at LIKE %s)";
225 $search_term = '%' . $wpdb->esc_like($search) . '%';
226 $where_values[] = $search_term;
227 $where_values[] = $search_term;
228 }
229
230 $where_clause = implode(' AND ', $where_conditions);
231
232 // Get total count for pagination
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.
235 "SELECT COUNT(*) FROM {$table_name} e WHERE {$where_clause}",
236 ...$where_values
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.
239 $total_items = (int) $wpdb->get_var($total_query);
240
241 // Get paginated entries with notification information
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.
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.
246 "SELECT e.*, p.title as notification_name, p.nx_id as notification_id
247 FROM {$table_name} e
248 LEFT JOIN {$posts_table} p ON e.nx_id = p.nx_id
249 WHERE {$where_clause}
250 ORDER BY e.created_at DESC
251 LIMIT %d OFFSET %d",
252 ...array_merge($where_values, [$per_page, $offset])
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.
256 $entries = $wpdb->get_results($entries_query, ARRAY_A);
257
258 $formatted_entries = [];
259 foreach ($entries as $entry) {
260 $data = maybe_unserialize($entry['data']);
261 $formatted_entries[] = [
262 'id' => $entry['entry_id'],
263 'date' => $entry['created_at'],
264 'name' => $data['name'] ?? '',
265 'email' => $data['email'] ?? '',
266 'message' => $data['message'] ?? '',
267 'title' => $data['title'] ?? '',
268 'theme' => $data['theme'] ?? '',
269 'ip' => $data['ip'] ?? '',
270 'notification_name' => $entry['notification_name'] ?? '',
271 'notification_id' => $entry['notification_id'] ?? 0,
272 'nx_id' => $entry['nx_id'] ?? 0,
273 ];
274 }
275
276 return new \WP_REST_Response([
277 'entries' => $formatted_entries,
278 'total' => $total_items,
279 'page' => $page,
280 'per_page' => $per_page,
281 'total_pages' => ceil($total_items / $per_page),
282 ], 200);
283 }
284
285 /**
286 * Delete feedback entry
287 *
288 * @param WP_REST_Request $request
289 * @return WP_REST_Response
290 */
291 public function delete_feedback_entry($request) {
292 global $wpdb;
293
294 $entry_id = $request->get_param('id');
295 $table_name = $wpdb->prefix . 'nx_entries';
296
297 $sources = $this->form_sources();
298 $src_placeholders = implode(',', array_fill(0, count($sources), '%s'));
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.
301 "DELETE FROM {$table_name} WHERE entry_id = %d AND source IN ({$src_placeholders})",
302 array_merge([$entry_id], $sources)
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.
305 $result = $wpdb->query($delete_query);
306
307 if ($result === false) {
308 return new \WP_REST_Response([
309 'success' => false,
310 'message' => __('Failed to delete entry', 'notificationx'),
311 ], 500);
312 }
313
314 return new \WP_REST_Response([
315 'success' => true,
316 'message' => __('Entry deleted successfully', 'notificationx'),
317 ], 200);
318 }
319
320 /**
321 * Bulk delete feedback entries
322 *
323 * @param WP_REST_Request $request
324 * @return WP_REST_Response
325 */
326 public function bulk_delete_feedback_entries($request) {
327 global $wpdb;
328
329 $entry_ids = $request->get_param('ids');
330 $table_name = $wpdb->prefix . 'nx_entries';
331
332 if (empty($entry_ids) || !is_array($entry_ids)) {
333 return new \WP_REST_Response([
334 'success' => false,
335 'message' => __('No entries selected for deletion', 'notificationx'),
336 ], 400);
337 }
338
339 // Sanitize entry IDs
340 $entry_ids = array_map('absint', $entry_ids);
341 $entry_ids = array_filter($entry_ids); // Remove any zero values
342
343 if (empty($entry_ids)) {
344 return new \WP_REST_Response([
345 'success' => false,
346 'message' => __('Invalid entry IDs provided', 'notificationx'),
347 ], 400);
348 }
349
350 // Create placeholders for the IN clauses
351 $placeholders = implode(',', array_fill(0, count($entry_ids), '%d'));
352 $sources = $this->form_sources();
353 $src_placeholders = implode(',', array_fill(0, count($sources), '%s'));
354
355 // Prepare the query with source filter
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.
358 "DELETE FROM {$table_name} WHERE entry_id IN ({$placeholders}) AND source IN ({$src_placeholders})",
359 array_merge($entry_ids, $sources)
360 );
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.
363 $result = $wpdb->query($query);
364
365 if ($result === false) {
366 return new \WP_REST_Response([
367 'success' => false,
368 'message' => __('Failed to delete entries', 'notificationx'),
369 ], 500);
370 }
371
372 return new \WP_REST_Response([
373 'success' => true,
374 'message' => sprintf(
375 /* translators: %d: Number of entries deleted */
376 _n('%d entry deleted successfully', '%d entries deleted successfully', $result, 'notificationx'),
377 $result
378 ),
379 'deleted_count' => $result,
380 ], 200);
381 }
382
383 /**
384 * Export feedback entries
385 *
386 * @param WP_REST_Request $request
387 * @return WP_REST_Response
388 */
389 public function export_feedback_entries($request) {
390 global $wpdb;
391
392 $table_name = $wpdb->prefix . 'nx_entries';
393 $search = $request->get_param('s') ?: '';
394 $notification_id = $request->get_param('notification_id') ?: '';
395
396 // Build WHERE clause — include both popup and exit-intent submissions
397 $sources = $this->form_sources();
398 $src_placeholders = implode(',', array_fill(0, count($sources), '%s'));
399 $where_conditions = ["e.source IN ({$src_placeholders})"];
400 $where_values = $sources;
401
402 // Add notification filter if provided
403 if (!empty($notification_id)) {
404 $where_conditions[] = "e.nx_id = %d";
405 $where_values[] = intval($notification_id);
406 }
407
408 // Add search functionality if provided
409 if (!empty($search)) {
410 $where_conditions[] = "(e.data LIKE %s OR e.created_at LIKE %s)";
411 $search_term = '%' . $wpdb->esc_like($search) . '%';
412 $where_values[] = $search_term;
413 $where_values[] = $search_term;
414 }
415
416 $where_clause = implode(' AND ', $where_conditions);
417
418 // Get all entries for export (no pagination)
419 $posts_table = $wpdb->prefix . 'nx_posts';
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.
422 "SELECT e.entry_id, e.nx_id, e.data, e.created_at, p.title as notification_name
423 FROM {$table_name} e
424 LEFT JOIN {$posts_table} p ON e.nx_id = p.nx_id
425 WHERE {$where_clause}
426 ORDER BY e.created_at DESC",
427 ...$where_values
428 );
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.
431 $entries = $wpdb->get_results($query, ARRAY_A);
432 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
433
434 if (empty($entries)) {
435 return new \WP_REST_Response([
436 'success' => false,
437 'message' => __('No entries found to export', 'notificationx'),
438 ], 404);
439 }
440
441 // Generate CSV content
442 $csv_data = $this->generate_csv_data($entries);
443
444 // Generate filename
445 $filename = 'notificationx-feedback-entries-' . gmdate('Y-m-d-H-i-s') . '.csv';
446
447 return new \WP_REST_Response([
448 'success' => true,
449 'csv_content' => $csv_data,
450 'filename' => $filename,
451 'total_entries' => count($entries),
452 /* translators: %d: number of entries prepared for export */
453 'message' => sprintf(__('Successfully prepared %d entries for export', 'notificationx'), count($entries))
454 ], 200);
455 }
456
457 /**
458 * Generate CSV data from entries
459 *
460 * @param array $entries
461 * @return string
462 */
463 private function generate_csv_data($entries) {
464 $csv_data = [];
465 $is_pro = NotificationX::is_pro();
466
467 // CSV Headers
468 $csv_headers = [
469 __('No', 'notificationx'),
470 __('Date', 'notificationx'),
471 __('NotificationX Title', 'notificationx'),
472 ];
473
474 if ($is_pro) {
475 $csv_headers[] = __('Name', 'notificationx');
476 $csv_headers[] = __('Email Address', 'notificationx');
477 }
478
479 $csv_headers[] = __('Message', 'notificationx');
480
481 $csv_data[] = $csv_headers;
482
483 // Add data rows
484 $counter = 1;
485 foreach ($entries as $entry) {
486 $data = maybe_unserialize($entry['data']);
487 $date = new \DateTime($entry['created_at']);
488
489 $row = [
490 $counter++,
491 $date->format('F j, Y'),
492 /* translators: %d: notification ID */
493 $entry['notification_name'] ?: sprintf(__('Notification #%d', 'notificationx'), $entry['nx_id']),
494 ];
495
496 if ($is_pro) {
497 $row[] = $data['name'] ?? '';
498 $row[] = $data['email'] ?? '';
499 }
500
501 $row[] = $data['message'] ?? '';
502
503 $csv_data[] = $row;
504 }
505
506 // Convert array to CSV string
507 $csv_content = '';
508 foreach ($csv_data as $row) {
509 $csv_content .= '"' . implode('","', array_map(function($field) {
510 return str_replace('"', '""', $field); // Escape quotes
511 }, $row)) . '"' . "\n";
512 }
513
514 return $csv_content;
515 }
516 }