PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | core/admin/notification.php +89 -10 1.0.60 → 1.0.64 View file →
@@ -53,8 +53,9 @@
53 53 )
54 54 )
55 55 ->init();
56 56
57 + add_filter( 'rest_pre_dispatch', array( $this, 'check_notification_flow_permission' ), 10, 3 );
57 58 add_filter( 'ens_tt_available_actions', array( $this, 'register_triggers' ) );
58 59 add_filter( 'timetics_notification_sdk_email_body', array( $this, 'wrap_email_body' ), 10, 1 );
59 60 add_filter( 'timetics_notification_sdk_to_emails', array( $this, 'expand_custom_email' ), 10, 2 );
60 61
@@ -61,8 +62,33 @@
61 62 add_action( 'admin_init', array( 'Timetics\Core\Admin\Notification_Seeder', 'maybe_seed' ), 20 );
62 63 }
63 64
64 65 /**
66 + * Block non-admins from the SDK notification-flow REST endpoints.
67 + *
68 + * The SDK's FlowAPI registers these routes with permission_callback => true,
69 + * so we enforce the capability here from the consumer plugin.
70 + *
71 + * @param mixed $result Short-circuit value (null to continue).
72 + * @param \WP_REST_Server $server
73 + * @param \WP_REST_Request $request
74 + * @return mixed WP_Error on failure, original $result otherwise.
75 + */
76 + public function check_notification_flow_permission( $result, $server, $request ) {
77 + if ( strpos( $request->get_route(), '/timetics/v1/notification-flow' ) === 0 ) {
78 + if ( ! current_user_can( 'manage_options' ) ) {
79 + return new \WP_Error(
80 + 'rest_forbidden',
81 + __( 'Sorry, you are not allowed to do that.', 'timetics' ),
82 + array( 'status' => 403 )
83 + );
84 + }
85 + }
86 +
87 + return $result;
88 + }
89 +
90 + /**
65 91 * Register the 3 booking triggers with their tag fields and receivers.
66 92 *
67 93 * @param array $actions
68 94 * @return array
@@ -150,19 +176,23 @@
150 176 'value' => 'custom_email',
151 177 ),
152 178 );
153 179
180 + // Resolves against hook_data['meeting_date_timestamp'] (see get_hook_data),
181 + // a true UTC timestamp, so a delay node anchored to it schedules correctly.
182 + $delay_dependencies = array(
183 + array(
184 + 'label' => __( 'Meeting Date', 'timetics' ),
185 + 'value' => 'meeting_date',
186 + ),
187 + );
188 +
154 189 $actions = array(
155 190 array(
156 191 'trigger_label' => __( 'After Booking Confirmation', 'timetics' ),
157 192 'trigger_value' => 'booking_created',
158 193 'trigger_data' => $trigger_data,
159 - 'delay_dependencies' => array(
160 - array(
161 - 'label' => __( 'Meeting Date', 'timetics' ),
162 - 'value' => 'meeting_date',
163 - ),
164 - ),
194 + 'delay_dependencies' => $delay_dependencies,
165 195 'email_receivers' => $email_receivers,
166 196 ),
167 197 array(
168 198 'trigger_label' => __( 'After Booking Cancellation', 'timetics' ),
@@ -167,9 +197,15 @@
167 197 array(
168 198 'trigger_label' => __( 'After Booking Cancellation', 'timetics' ),
169 199 'trigger_value' => 'booking_canceled',
170 200 'trigger_data' => $trigger_data,
171 - 'delay_dependencies' => array(),
201 + // Offering Meeting Date here as well, not just on booking_created.
202 + // An empty list left a delay node with nothing to anchor to, and
203 + // the SDK then falls back to current_time( 'timestamp' ) — which is
204 + // UTC plus the site's GMT offset, handed straight to
205 + // wp_schedule_single_event() where a true UTC timestamp is expected.
206 + // The email then fires GMT-offset hours away from the intended time.
207 + 'delay_dependencies' => $delay_dependencies,
172 208 'email_receivers' => $email_receivers,
173 209 ),
174 210 array(
175 211 'trigger_label' => __( 'After Booking Rescheduled', 'timetics' ),
@@ -174,9 +210,9 @@
174 210 array(
175 211 'trigger_label' => __( 'After Booking Rescheduled', 'timetics' ),
176 212 'trigger_value' => 'booking_rescheduled',
177 213 'trigger_data' => $trigger_data,
178 - 'delay_dependencies' => array(),
214 + 'delay_dependencies' => $delay_dependencies,
179 215 'email_receivers' => $email_receivers,
180 216 ),
181 217 );
182 218
@@ -259,11 +295,12 @@
259 295 $meeting = new Appointment( $booking->get_appointment() );
260 296 $staff = new Staff( $booking->get_staff_id() );
261 297 $customer = new Customer( $booking->get_customer_id() );
262 298
263 - $formatted = timetics_format_email_datetime( $booking->get_start_date(), $booking->get_start_time() );
264 - $booking_timestamp = strtotime( $booking->get_start_date() . ' ' . $booking->get_start_time() );
299 + $formatted = timetics_format_email_datetime( $booking->get_start_date(), $booking->get_start_time() );
265 300
301 + $booking_timestamp = self::get_booking_timestamp( $booking );
302 +
266 303 /* Pull the Google Meet link from the stored calendar event so it can be inserted as the {%meeting_meet_link%} tag in automation emails.
267 304 */
268 305 $calendar_event = $booking->get_event();
269 306 $meet_link = ( is_array( $calendar_event ) && ! empty( $calendar_event['hangoutLink'] ) ) ? $calendar_event['hangoutLink'] : '';
@@ -280,8 +317,17 @@
280 317 }
281 318 }
282 319
283 320 return array(
321 + // The booking id lets delayed flows be re-validated or re-scheduled
322 + // when the booking changes after the flow started. `post_id` is the
323 + // key the email-notification-sdk itself looks for; `booking_id` is
324 + // the readable alias used inside Timetics.
325 + 'post_id' => $booking->get_id(),
326 + 'booking_id' => $booking->get_id(),
327 + // Not $booking->get_status(): the cancel-by-delete path fires this
328 + // trigger after the post row is gone, where get_post() returns null.
329 + 'booking_status' => (string) get_post_status( $booking->get_id() ),
284 330 'customer_email' => $customer->get_email(),
285 331 'host_email' => $staff->get_email(),
286 332 'custom_email' => apply_filters( 'timetics_custom_notification_email', timetics_get_option( 'custom_notification_email', get_option( 'admin_email' ) ) ),
287 333 'meeting_title' => $meeting->get_name(),
@@ -296,6 +342,39 @@
296 342 'login_url' => wp_login_url(),
297 343 'login_username' => $customer->get_email(),
298 344 'set_password_url' => $set_password_url,
299 345 );
346 + }
347 +
348 + /**
349 + * Resolve a booking's start date/time to a real UTC timestamp.
350 + *
351 + * The stored date and time are wall-clock values in the booking's own
352 + * timezone, so they must be interpreted in that timezone — strtotime()
353 + * would read them as server time and shift every delay by the offset.
354 + *
355 + * @param Booking $booking
356 + * @return int Unix timestamp, 0 when the booking has no start date.
357 + */
358 + public static function get_booking_timestamp( Booking $booking ) {
359 + $date = $booking->get_start_date();
360 + $time = $booking->get_start_time();
361 +
362 + if ( ! $date ) {
363 + return 0;
364 + }
365 +
366 + $timezone = $booking->get_timezone();
367 +
368 + if ( ! $timezone || ! timetics_is_valid_timezone( $timezone ) ) {
369 + $timezone = timetics_reminder_fallback_timezone();
370 + }
371 +
372 + try {
373 + $datetime = new \DateTime( $date . ' ' . $time, new \DateTimeZone( $timezone ) );
374 + } catch ( \Exception $e ) {
375 + return 0;
376 + }
377 +
378 + return $datetime->getTimestamp();
300 379 }
301 380 }