| 1 |
<?php |
| 2 |
/** |
| 3 |
* Shared query/formatting for the entry-reading abilities (list + export). |
| 4 |
* |
| 5 |
* Entries are the submissions collected by NotificationX form notifications — |
| 6 |
* the Popup and Exit Intent forms — and stored in the `nx_entries` table. They |
| 7 |
* are the same rows surfaced on the admin "Feedback Entries" screen and its CSV |
| 8 |
* export, so this trait mirrors that feature exactly: the same two form sources, |
| 9 |
* the same search/notification filters, and the same Free/Pro field split |
| 10 |
* (message on Free; name + email added when Pro is active). |
| 11 |
* |
| 12 |
* @package NotificationX\Abilities\Read |
| 13 |
*/ |
| 14 |
|
| 15 |
namespace NotificationX\Abilities\Read; |
| 16 |
|
| 17 |
if ( ! defined( 'ABSPATH' ) ) { |
| 18 |
exit; |
| 19 |
} |
| 20 |
|
| 21 |
trait EntriesReader { |
| 22 |
|
| 23 |
/** |
| 24 |
* The notification sources whose submissions are treated as form entries — |
| 25 |
* identical to the admin Feedback Entries screen. |
| 26 |
* |
| 27 |
* @return string[] |
| 28 |
*/ |
| 29 |
protected function form_sources() { |
| 30 |
return array( 'popup_notification', 'exit_intent_custom' ); |
| 31 |
} |
| 32 |
|
| 33 |
/** |
| 34 |
* Whether Pro is active (unlocks the name + email fields, exactly like the |
| 35 |
* admin CSV export's `is_pro()` branch). |
| 36 |
* |
| 37 |
* @return bool |
| 38 |
*/ |
| 39 |
protected function entries_is_pro() { |
| 40 |
return class_exists( '\NotificationXPro\NotificationX' ) && \NotificationX\NotificationX::is_pro(); |
| 41 |
} |
| 42 |
|
| 43 |
/** |
| 44 |
* Query form entries with optional notification/source/search filters and |
| 45 |
* optional pagination. |
| 46 |
* |
| 47 |
* @param array $args nx_id, source, search. |
| 48 |
* @param int|null $limit Max rows (null = no limit, for export). |
| 49 |
* @param int $offset Row offset. |
| 50 |
* @return array{0: array[], 1: int} [rows, total_matching] |
| 51 |
*/ |
| 52 |
protected function query_entries( $args, $limit = null, $offset = 0 ) { |
| 53 |
global $wpdb; |
| 54 |
|
| 55 |
$entries_table = $wpdb->prefix . 'nx_entries'; |
| 56 |
$posts_table = $wpdb->prefix . 'nx_posts'; |
| 57 |
|
| 58 |
// Default to both form sources; allow narrowing to one of them. |
| 59 |
$sources = $this->form_sources(); |
| 60 |
if ( ! empty( $args['source'] ) && in_array( $args['source'], $sources, true ) ) { |
| 61 |
$sources = array( $args['source'] ); |
| 62 |
} |
| 63 |
|
| 64 |
$placeholders = implode( ',', array_fill( 0, count( $sources ), '%s' ) ); |
| 65 |
$where = array( "e.source IN ({$placeholders})" ); |
| 66 |
$values = $sources; |
| 67 |
|
| 68 |
if ( ! empty( $args['nx_id'] ) ) { |
| 69 |
$where[] = 'e.nx_id = %d'; |
| 70 |
$values[] = (int) $args['nx_id']; |
| 71 |
} |
| 72 |
if ( isset( $args['search'] ) && '' !== $args['search'] ) { |
| 73 |
$where[] = '(e.data LIKE %s OR e.created_at LIKE %s)'; |
| 74 |
$like = '%' . $wpdb->esc_like( (string) $args['search'] ) . '%'; |
| 75 |
$values[] = $like; |
| 76 |
$values[] = $like; |
| 77 |
} |
| 78 |
|
| 79 |
$where_sql = implode( ' AND ', $where ); |
| 80 |
|
| 81 |
// Total matching (before pagination). |
| 82 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Prepared below; only $wpdb->prefix table names are interpolated. |
| 83 |
$total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$entries_table} e WHERE {$where_sql}", ...$values ) ); |
| 84 |
|
| 85 |
$limit_sql = ''; |
| 86 |
$page_values = $values; |
| 87 |
if ( null !== $limit ) { |
| 88 |
$limit_sql = ' LIMIT %d OFFSET %d'; |
| 89 |
$page_values[] = (int) $limit; |
| 90 |
$page_values[] = max( 0, (int) $offset ); |
| 91 |
} |
| 92 |
|
| 93 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Prepared below; only $wpdb->prefix table names are interpolated. |
| 94 |
$rows = $wpdb->get_results( |
| 95 |
$wpdb->prepare( |
| 96 |
"SELECT e.entry_id, e.nx_id, e.source, e.data, e.created_at, p.title AS notification_name |
| 97 |
FROM {$entries_table} e |
| 98 |
LEFT JOIN {$posts_table} p ON e.nx_id = p.nx_id |
| 99 |
WHERE {$where_sql} |
| 100 |
ORDER BY e.created_at DESC{$limit_sql}", |
| 101 |
...$page_values |
| 102 |
), |
| 103 |
ARRAY_A |
| 104 |
); |
| 105 |
|
| 106 |
return array( is_array( $rows ) ? $rows : array(), $total ); |
| 107 |
} |
| 108 |
|
| 109 |
/** |
| 110 |
* Flatten a raw entry row into the response shape. `message` is always |
| 111 |
* present; `name` and `email` are included only when Pro is active. |
| 112 |
* |
| 113 |
* @param array $row Raw DB row (entry_id, nx_id, source, data, created_at, notification_name). |
| 114 |
* @param bool $is_pro Whether to expose the Pro contact fields. |
| 115 |
* @return array |
| 116 |
*/ |
| 117 |
protected function map_entry_row( $row, $is_pro ) { |
| 118 |
$data = maybe_unserialize( isset( $row['data'] ) ? $row['data'] : '' ); |
| 119 |
if ( ! is_array( $data ) ) { |
| 120 |
$data = array(); |
| 121 |
} |
| 122 |
|
| 123 |
$nx_id = isset( $row['nx_id'] ) ? (int) $row['nx_id'] : 0; |
| 124 |
$title = ! empty( $row['notification_name'] ) |
| 125 |
? $row['notification_name'] |
| 126 |
/* translators: %d: notification id. */ |
| 127 |
: sprintf( __( 'Notification #%d', 'notificationx' ), $nx_id ); |
| 128 |
|
| 129 |
$entry = array( |
| 130 |
'entry_id' => isset( $row['entry_id'] ) ? (int) $row['entry_id'] : 0, |
| 131 |
'nx_id' => $nx_id, |
| 132 |
'notification' => $title, |
| 133 |
'source' => isset( $row['source'] ) ? $row['source'] : '', |
| 134 |
'created_at' => isset( $row['created_at'] ) ? $row['created_at'] : '', |
| 135 |
); |
| 136 |
|
| 137 |
if ( $is_pro ) { |
| 138 |
$entry['name'] = isset( $data['name'] ) ? $data['name'] : ''; |
| 139 |
$entry['email'] = isset( $data['email'] ) ? $data['email'] : ''; |
| 140 |
} |
| 141 |
|
| 142 |
$entry['message'] = isset( $data['message'] ) ? $data['message'] : ''; |
| 143 |
|
| 144 |
return $entry; |
| 145 |
} |
| 146 |
} |
| 147 |
|