PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / trunk
Timetics – Appointment Booking Calendar & Scheduling vtrunk
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 1.0.22 1.0.23 1.0.24 All 62 releases
timetics / core / admin / notification.php

notification.php in Timetics – Appointment Booking Calendar & Scheduling trunk, at core/admin/notification.php

381 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Notification class — boots themewinter/email-notification-sdk and registers
4 * Timetics triggers so the React automation builder can wire up email flows.
5 *
6 * @package Timetics
7 */
8
9 namespace Timetics\Core\Admin;
10
11 use Ens\Core\SDK;
12 use Timetics\Core\Appointments\Appointment;
13 use Timetics\Core\Bookings\Booking;
14 use Timetics\Core\Customers\Customer;
15 use Timetics\Core\Staffs\Staff;
16 use Timetics\Utils\Singleton;
17
18 defined( 'ABSPATH' ) || exit;
19
20 require_once __DIR__ . '/notification-seeder.php';
21
22 class Notification {
23
24 use Singleton;
25
26 /**
27 * Boot SDK and register trigger definitions.
28 *
29 * @return void
30 */
31 public function init() {
32 if ( ! class_exists( SDK::class ) ) {
33 return;
34 }
35
36 SDK::get_instance()
37 ->setup(
38 array(
39 'plugin_name' => 'Timetics',
40 'plugin_slug' => 'timetics',
41 'general_prefix' => 'tt',
42 'hook_prefix' => 'timetics',
43 'text_domain' => 'timetics',
44 'admin_script_handler' => 'timetics-dashboard-scripts',
45 'sub_menu_filter_hook' => 'timetics_menu',
46 'sub_menu_details' => array(
47 'id' => 'timetics-automation',
48 'title' => __( 'Automation', 'timetics' ),
49 'link' => '/automation',
50 'capability' => apply_filters( 'timetics_menu_permission_automation', 'manage_options' ),
51 'position' => apply_filters( 'timetics_menu_position_automation', 11 ),
52 ),
53 )
54 )
55 ->init();
56
57 add_filter( 'rest_pre_dispatch', array( $this, 'check_notification_flow_permission' ), 10, 3 );
58 add_filter( 'ens_tt_available_actions', array( $this, 'register_triggers' ) );
59 add_filter( 'timetics_notification_sdk_email_body', array( $this, 'wrap_email_body' ), 10, 1 );
60 add_filter( 'timetics_notification_sdk_to_emails', array( $this, 'expand_custom_email' ), 10, 2 );
61
62 add_action( 'admin_init', array( 'Timetics\Core\Admin\Notification_Seeder', 'maybe_seed' ), 20 );
63 }
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 /**
91 * Register the 3 booking triggers with their tag fields and receivers.
92 *
93 * @param array $actions
94 * @return array
95 */
96 public function register_triggers( $actions ) {
97 $trigger_data = array(
98 array(
99 'label' => __( 'Meeting Title', 'timetics' ),
100 'value' => 'meeting_title',
101 'type' => 'string',
102 ),
103 array(
104 'label' => __( 'Meeting Date', 'timetics' ),
105 'value' => 'meeting_date',
106 'type' => 'date',
107 ),
108 array(
109 'label' => __( 'Meeting Time', 'timetics' ),
110 'value' => 'meeting_time',
111 'type' => 'string',
112 ),
113 array(
114 'label' => __( 'Meeting Location', 'timetics' ),
115 'value' => 'meeting_location',
116 'type' => 'string',
117 ),
118 array(
119 'label' => __( 'Google Meet Link', 'timetics' ),
120 'value' => 'meeting_meet_link',
121 'type' => 'string',
122 ),
123 array(
124 'label' => __( 'Meeting Duration', 'timetics' ),
125 'value' => 'meeting_duration',
126 'type' => 'string',
127 ),
128 array(
129 'label' => __( 'Host Name', 'timetics' ),
130 'value' => 'host_name',
131 'type' => 'string',
132 ),
133 array(
134 'label' => __( 'Host Email', 'timetics' ),
135 'value' => 'host_email',
136 'type' => 'string',
137 ),
138 array(
139 'label' => __( 'Customer Name', 'timetics' ),
140 'value' => 'customer_name',
141 'type' => 'string',
142 ),
143 array(
144 'label' => __( 'Customer Email', 'timetics' ),
145 'value' => 'customer_email',
146 'type' => 'string',
147 ),
148 array(
149 'label' => __( 'Login URL', 'timetics' ),
150 'value' => 'login_url',
151 'type' => 'string',
152 ),
153 array(
154 'label' => __( 'Login Username', 'timetics' ),
155 'value' => 'login_username',
156 'type' => 'string',
157 ),
158 array(
159 'label' => __( 'Set Password URL', 'timetics' ),
160 'value' => 'set_password_url',
161 'type' => 'string',
162 ),
163 );
164
165 $email_receivers = array(
166 array(
167 'label' => __( 'Customer Email', 'timetics' ),
168 'value' => 'customer_email',
169 ),
170 array(
171 'label' => __( 'Host Email', 'timetics' ),
172 'value' => 'host_email',
173 ),
174 array(
175 'label' => __( 'Custom Email', 'timetics' ),
176 'value' => 'custom_email',
177 ),
178 );
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
189 $actions = array(
190 array(
191 'trigger_label' => __( 'After Booking Confirmation', 'timetics' ),
192 'trigger_value' => 'booking_created',
193 'trigger_data' => $trigger_data,
194 'delay_dependencies' => $delay_dependencies,
195 'email_receivers' => $email_receivers,
196 ),
197 array(
198 'trigger_label' => __( 'After Booking Cancellation', 'timetics' ),
199 'trigger_value' => 'booking_canceled',
200 'trigger_data' => $trigger_data,
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,
208 'email_receivers' => $email_receivers,
209 ),
210 array(
211 'trigger_label' => __( 'After Booking Rescheduled', 'timetics' ),
212 'trigger_value' => 'booking_rescheduled',
213 'trigger_data' => $trigger_data,
214 'delay_dependencies' => $delay_dependencies,
215 'email_receivers' => $email_receivers,
216 ),
217 );
218
219 return $actions;
220 }
221
222 /**
223 * Expand a comma-separated custom_email setting into an array of addresses.
224 *
225 * Only expands when the email value matches hook_data['custom_email'],
226 * so customer/host emails pass through untouched.
227 *
228 * Hooked to `timetics_notification_sdk_to_emails`.
229 *
230 * @param string $email Raw email value being sent.
231 * @param array $hook_data Full hook data payload.
232 * @return string|array Single email or array of emails.
233 */
234 public function expand_custom_email( $email, $hook_data ) {
235 if ( ! isset( $hook_data['custom_email'] ) || $email !== $hook_data['custom_email'] ) {
236 return $email;
237 }
238
239 if ( strpos( $email, ',' ) === false ) {
240 return sanitize_email( $email );
241 }
242
243 return array_values(
244 array_filter(
245 array_map( 'sanitize_email', array_map( 'trim', explode( ',', $email ) ) )
246 )
247 );
248 }
249
250 /**
251 * Wrap the SDK email body in Timetics' branded HTML template.
252 *
253 * Hooked to `notification_sdk_email_body`.
254 *
255 * @param string $message Raw email body HTML from the SDK.
256 * @return string Full HTML email.
257 */
258 public function wrap_email_body( $message ) {
259 $template_path = TIMETICS_PLUGIN_DIR . '/templates/emails/email-notification-wrapper.html';
260
261 if ( ! file_exists( $template_path ) ) {
262 return $message;
263 }
264
265 $template = file_get_contents( $template_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
266 $primary_color = timetics_get_option( 'primary_color', '#3161F1' );
267 $company_name = timetics_get_option( 'business_name', get_bloginfo( 'name' ) );
268
269 $show_powered_by = apply_filters( 'timetics_show_email_powered_by', true );
270 $powered_by_html = '';
271 if ( $show_powered_by ) {
272 $powered_by_html = '<p style="margin:8px 0 0;font-size:11px;color:#b0bec9;font-style:italic;">'
273 . esc_html__( 'Powered by Timetics', 'timetics' )
274 . '</p>';
275 }
276
277 $variables = array(
278 '{{MESSAGE}}' => wp_kses_post( $message ),
279 '{{COMPANY_NAME}}' => esc_html( $company_name ),
280 '{{PRIMARY_COLOR}}' => esc_attr( $primary_color ),
281 '{%powered_by_section%}' => $powered_by_html,
282 );
283
284 return str_replace( array_keys( $variables ), array_values( $variables ), $template );
285 }
286
287 /**
288 * Build the hook data payload from a Booking object.
289 * Called from any file that needs to fire a timetics_gln_hook action.
290 *
291 * @param Booking $booking
292 * @return array
293 */
294 public static function get_hook_data( Booking $booking ) {
295 $meeting = new Appointment( $booking->get_appointment() );
296 $staff = new Staff( $booking->get_staff_id() );
297 $customer = new Customer( $booking->get_customer_id() );
298
299 $formatted = timetics_format_email_datetime( $booking->get_start_date(), $booking->get_start_time() );
300
301 $booking_timestamp = self::get_booking_timestamp( $booking );
302
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.
304 */
305 $calendar_event = $booking->get_event();
306 $meet_link = ( is_array( $calendar_event ) && ! empty( $calendar_event['hangoutLink'] ) ) ? $calendar_event['hangoutLink'] : '';
307
308 $set_password_url = '';
309 $customer_user = get_user_by( 'email', $customer->get_email() );
310 if ( $customer_user ) {
311 $reset_key = get_password_reset_key( $customer_user );
312 if ( ! is_wp_error( $reset_key ) ) {
313 $set_password_url = network_site_url(
314 'wp-login.php?action=rp&key=' . $reset_key . '&login=' . rawurlencode( $customer_user->user_login ),
315 'login'
316 );
317 }
318 }
319
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() ),
330 'customer_email' => $customer->get_email(),
331 'host_email' => $staff->get_email(),
332 'custom_email' => apply_filters( 'timetics_custom_notification_email', timetics_get_option( 'custom_notification_email', get_option( 'admin_email' ) ) ),
333 'meeting_title' => $meeting->get_name(),
334 'meeting_date' => $formatted['date'],
335 'meeting_date_timestamp' => $booking_timestamp,
336 'meeting_time' => $formatted['time'],
337 'meeting_location' => $booking->get_location(),
338 'meeting_meet_link' => $meet_link,
339 'meeting_duration' => $meeting->get_duration(),
340 'host_name' => $staff->get_display_name(),
341 'customer_name' => $customer->get_display_name(),
342 'login_url' => wp_login_url(),
343 'login_username' => $customer->get_email(),
344 'set_password_url' => $set_password_url,
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();
379 }
380 }
381