PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Controllers / NotificationCountsController.php

NotificationCountsController.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Controllers/NotificationCountsController.php

192 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use Yatra\Database\Tables\BookingsTable;
10 use Yatra\Database\Tables\BookingPaymentsTable;
11 use Yatra\Database\Tables\EnquiriesTable;
12 use Yatra\Database\Tables\ReviewsTable;
13
14 /**
15 * Admin sidebar "new since you last looked" counters.
16 *
17 * Powers the red count badges on the Bookings / Payments / Abandoned Recovery
18 * sidebar items. Purely additive and read-only against the existing tables:
19 *
20 * - The "last seen" marker for each section lives in `wp_options`
21 * (`yatra_<section>_last_seen_id`). NO existing table is altered.
22 * - A badge count = number of rows in that section's table with `id` greater
23 * than the stored marker (i.e. created after the admin last opened the page).
24 * - Opening a page calls mark-seen, which bumps the marker to the current
25 * MAX(id) so the badge clears.
26 *
27 * Abandoned Recovery is a Pro feature, so its count/marker are contributed by
28 * Pro through the `yatra_admin_new_counts` filter and `yatra_admin_mark_seen`
29 * action — this controller knows nothing about Pro.
30 */
31 class NotificationCountsController extends BaseController
32 {
33 /** section => wp_option name (sections handled directly by Free core). */
34 private const CORE_SECTIONS = [
35 'bookings' => 'yatra_bookings_last_seen_id',
36 'payments' => 'yatra_payments_last_seen_id',
37 'enquiries' => 'yatra_enquiries_last_seen_id',
38 'reviews' => 'yatra_reviews_last_seen_id',
39 ];
40
41 public function register_routes(): void
42 {
43 register_rest_route($this->namespace, '/admin/new-counts', [
44 'methods' => 'GET',
45 'callback' => [$this, 'get_counts'],
46 'permission_callback' => [$this, 'check_permission'],
47 ]);
48
49 register_rest_route($this->namespace, '/admin/mark-seen', [
50 'methods' => 'POST',
51 'callback' => [$this, 'mark_seen'],
52 'permission_callback' => [$this, 'check_permission'],
53 'args' => [
54 'section' => [
55 'required' => true,
56 'type' => 'string',
57 ],
58 ],
59 ]);
60 }
61
62 /**
63 * Anyone who can reach the Yatra admin can read these counters. The sidebar
64 * already hides items the user lacks the cap for, so unseen-counts for a
65 * section they can't view are simply never displayed.
66 */
67 public function check_permission(?WP_REST_Request $request = null): bool
68 {
69 $cap = (string) apply_filters('yatra_admin_menu_cap', 'yatra_access_admin');
70
71 return current_user_can($cap) || current_user_can('manage_options');
72 }
73
74 /**
75 * GET /admin/new-counts → { success, counts: { bookings, payments, abandoned? } }
76 */
77 public function get_counts(WP_REST_Request $request): WP_REST_Response
78 {
79 $counts = [
80 'bookings' => $this->newCount(BookingsTable::getTableName(), self::CORE_SECTIONS['bookings']),
81 'payments' => $this->newCount(BookingPaymentsTable::getTableName(), self::CORE_SECTIONS['payments']),
82 'enquiries' => $this->newCount(EnquiriesTable::getTableName(), self::CORE_SECTIONS['enquiries']),
83 'reviews' => $this->newCount(ReviewsTable::getTableName(), self::CORE_SECTIONS['reviews']),
84 ];
85
86 /**
87 * Let Pro (and future modules) contribute their own section counts,
88 * e.g. 'abandoned'. Values are normalised to non-negative ints below.
89 *
90 * @param array<string,int> $counts section => new-row count
91 */
92 $counts = (array) apply_filters('yatra_admin_new_counts', $counts);
93
94 $counts = array_map(static function ($v): int {
95 return max(0, (int) $v);
96 }, $counts);
97
98 return new WP_REST_Response(['success' => true, 'counts' => $counts], 200);
99 }
100
101 /**
102 * POST /admin/mark-seen { section } → bumps that section's marker to MAX(id).
103 */
104 public function mark_seen(WP_REST_Request $request): WP_REST_Response
105 {
106 $section = sanitize_key((string) $request->get_param('section'));
107
108 $tableMap = [
109 'bookings' => BookingsTable::getTableName(),
110 'payments' => BookingPaymentsTable::getTableName(),
111 'enquiries' => EnquiriesTable::getTableName(),
112 'reviews' => ReviewsTable::getTableName(),
113 ];
114
115 if (isset(self::CORE_SECTIONS[$section], $tableMap[$section])) {
116 $this->updateMarker(self::CORE_SECTIONS[$section], $tableMap[$section]);
117 } else {
118 /**
119 * Non-core sections (e.g. Pro's 'abandoned') persist their own marker.
120 *
121 * @param string $section sanitised section key
122 */
123 do_action('yatra_admin_mark_seen', $section);
124 }
125
126 return new WP_REST_Response(['success' => true, 'section' => $section], 200);
127 }
128
129 /**
130 * New-row count for a section, with two go-live safeguards:
131 *
132 * - Missing table (fresh / partial install): returns 0 instead of letting
133 * `SELECT COUNT(*)` raise a "table doesn't exist" DB error into debug.log.
134 * - First run on a site (marker never set): seeds the marker to the current
135 * MAX(id) and returns 0, so pre-existing history is NOT reported as "new".
136 * Only rows created after the feature goes live are counted.
137 *
138 * Table name comes from our own *Table::getTableName(), never user input.
139 */
140 private function newCount(string $table, string $option): int
141 {
142 if (!$this->tableExists($table)) {
143 return 0;
144 }
145
146 $marker = get_option($option, null);
147 if ($marker === null) {
148 // Never seen on this site → seed to current max, report nothing new.
149 $this->updateMarker($option, $table);
150 return 0;
151 }
152
153 global $wpdb;
154
155 return (int) $wpdb->get_var(
156 $wpdb->prepare("SELECT COUNT(*) FROM `{$table}` WHERE id > %d", (int) $marker)
157 );
158 }
159
160 /** Bump a section's marker to the current highest id (badge → 0). */
161 private function updateMarker(string $option, string $table): void
162 {
163 if (!$this->tableExists($table)) {
164 return;
165 }
166
167 global $wpdb;
168
169 $maxId = (int) $wpdb->get_var("SELECT COALESCE(MAX(id), 0) FROM `{$table}`");
170 update_option($option, $maxId, false);
171 }
172
173 /**
174 * Does a table exist? Memoised per request (these endpoints touch the same
175 * tables 4–5×) so we never spam SHOW TABLES or COUNT a missing table.
176 *
177 * @var array<string,bool>
178 */
179 private static $tableExistsCache = [];
180
181 private function tableExists(string $table): bool
182 {
183 if (!array_key_exists($table, self::$tableExistsCache)) {
184 global $wpdb;
185 $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
186 self::$tableExistsCache[$table] = ($found === $table);
187 }
188
189 return self::$tableExistsCache[$table];
190 }
191 }
192