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/appointments/api-appointment.php +353 -50 1.0.12 → 1.0.64 View file →
@@ -7,8 +7,10 @@
7 7 * @package Timetics
8 8 */
9 9 namespace Timetics\Core\Appointments;
10 10
11 +defined( 'ABSPATH' ) || exit;
12 +
11 13 use Timetics\Base\Api;
12 14 use Timetics\Core\Appointments\Appointment;
13 15 use Timetics\Core\Staffs\Staff;
14 16 use Timetics\Utils\Singleton;
@@ -93,18 +95,14 @@
93 95 ],
94 96 [
95 97 'methods' => \WP_REST_Server::EDITABLE,
96 98 'callback' => [$this, 'update_item'],
97 - 'permission_callback' => function () {
98 - return current_user_can( 'read_meeting' );
99 - },
99 + 'permission_callback' => [ $this, 'update_item_permissions_check' ],
100 100 ],
101 101 [
102 102 'methods' => \WP_REST_Server::DELETABLE,
103 103 'callback' => [$this, 'delete_item'],
104 - 'permission_callback' => function () {
105 - return current_user_can( 'read_meeting' );
106 - },
104 + 'permission_callback' => [$this, 'delete_item_permissions_check' ],
107 105 ],
108 106 ] );
109 107
110 108 register_rest_route( $this->namespace, $this->rest_base . '/search', [
@@ -111,9 +109,11 @@
111 109 [
112 110 'methods' => \WP_REST_Server::READABLE,
113 111 'callback' => [$this, 'search_items'],
114 112 'permission_callback' => function () {
115 - return current_user_can( 'edit_posts' );
113 + // edit_meeting is admin-only in this plugin (see get_items()) —
114 + // staff need manage_timetics to search their own meetings at all.
115 + return current_user_can( 'manage_timetics' ) || current_user_can( 'manage_options' );
116 116 },
117 117 ],
118 118 ] );
119 119
@@ -150,23 +150,64 @@
150 150 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
151 151 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
152 152 $type = ! empty( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';
153 153
154 - $args = [
155 - 'posts_per_page' => $per_page,
156 - 'paged' => $paged,
157 - 'type' => $type,
158 - ];
154 + $restrict_to_own = ! current_user_can( 'edit_meeting' );
155 + $current_user_id = get_current_user_id();
159 156
160 - if ( ! current_user_can( 'edit_meeting' ) ) {
161 - $args['staff'] = get_current_user_id();
157 + if ( $restrict_to_own && ! $current_user_id ) {
158 + return rest_ensure_response(
159 + [
160 + 'success' => 1,
161 + 'status_code' => 200,
162 + 'data' => [
163 + 'total' => 0,
164 + 'items' => [],
165 + ],
166 + ]
167 + );
162 168 }
163 169
170 + $args = [ 'type' => $type ];
171 +
172 + if ( $restrict_to_own ) {
173 + // The staff meta_query is a LIKE match against a serialized array
174 + // and isn't safe as the access boundary (staff id 5 also matches
175 + // a meeting assigned to staff 55) — fetch broadly and enforce
176 + // real ownership below instead of filtering in SQL.
177 + $args['posts_per_page'] = -1;
178 + } else {
179 + $args['posts_per_page'] = $per_page;
180 + $args['paged'] = $paged;
181 + }
182 +
164 183 $appoint = Appointment::all( $args );
184 + $matched = $appoint['items'];
185 + $total = $appoint['total'];
165 186
187 + if ( $restrict_to_own ) {
188 + $matched = array_values(
189 + array_filter(
190 + $matched,
191 + function ( $item ) use ( $current_user_id ) {
192 + return in_array( $current_user_id, ( new Appointment( $item->ID ) )->get_staff_ids(), true );
193 + }
194 + )
195 + );
196 +
197 + $total = count( $matched );
198 +
199 + // A per_page value of -1 means "all items". Passing it directly to
200 + // array_slice() excludes the last item, which leaves a staff member
201 + // with a single assigned meeting with an empty meeting list.
202 + if ( -1 !== $per_page ) {
203 + $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
204 + }
205 + }
206 +
166 207 $items = [];
167 208
168 - foreach ( $appoint['items'] as $item ) {
209 + foreach ( $matched as $item ) {
169 210 $items[] = $this->prepare_item( $item->ID );
170 211 }
171 212
172 213 $data = [
@@ -172,9 +213,9 @@
172 213 $data = [
173 214 'success' => 1,
174 215 'status_code' => 200,
175 216 'data' => [
176 - 'total' => $appoint['total'],
217 + 'total' => $total,
177 218 'items' => $items,
178 219 ],
179 220 ];
180 221
@@ -188,22 +229,35 @@
188 229 *
189 230 * @return JSON
190 231 */
191 232 public function search_items( $request ) {
233 +
192 234 // Prepare search args.
193 - $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
194 - $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
195 - $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
235 + $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
236 + $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
237 + $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
238 + $restrict_to_own = ! current_user_can( 'manage_options' );
196 239
240 + $query_args = array(
241 + 'post_type' => 'timetics-appointment',
242 + 'orderby' => 'ID',
243 + 'order' => 'DESC',
244 + );
245 +
246 + if ( $restrict_to_own ) {
247 + // Same LIKE-isn't-a-boundary caveat as get_items() — fetch broadly
248 + // and enforce real ownership below instead of filtering in SQL.
249 + $query_args['posts_per_page'] = -1;
250 + } else {
251 + $query_args['posts_per_page'] = $per_page;
252 + $query_args['paged'] = $paged;
253 + }
254 +
197 255 // Get search.
198 256 $appointments = new \WP_Query(
199 - array(
200 - 'post_type' => 'timetics-appointment',
201 - 'posts_per_page' => $per_page,
202 - 'paged' => $paged,
203 - 'orderby' => 'ID',
204 - 'order' => 'DESC',
205 -
257 + array_merge(
258 + $query_args,
259 + array(
206 260 // @codingStandardsIgnoreStart
207 261 'meta_query' => array(
208 262 'relation' => 'OR',
209 263 array(
@@ -237,15 +291,38 @@
237 291 'compare' => 'LIKE',
238 292 ),
239 293 ),
240 294 // @codingStandardsIgnoreEnd
295 + )
241 296 )
242 297 );
243 298
299 + $matched = $appointments->posts;
300 + $total = $appointments->found_posts;
301 +
302 + if ( $restrict_to_own ) {
303 + $current_user_id = get_current_user_id();
304 +
305 + $matched = array_values(
306 + array_filter(
307 + $matched,
308 + function ( $item ) use ( $current_user_id ) {
309 + return in_array( $current_user_id, ( new Appointment( $item->ID ) )->get_staff_ids(), true );
310 + }
311 + )
312 + );
313 +
314 + $total = count( $matched );
315 +
316 + if ( -1 !== $per_page ) {
317 + $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
318 + }
319 + }
320 +
244 321 // Prepare items for response.
245 322 $items = [];
246 323
247 - foreach ( $appointments->posts as $item ) {
324 + foreach ( $matched as $item ) {
248 325 $items[] = $this->prepare_item( $item->ID );
249 326 }
250 327
251 328 $data = [
@@ -251,9 +328,9 @@
251 328 $data = [
252 329 'success' => 1,
253 330 'status' => 200,
254 331 'data' => [
255 - 'total' => $appointments->found_posts,
332 + 'total' => $total,
256 333 'items' => $items,
257 334 ],
258 335 ];
259 336
@@ -260,28 +337,52 @@
260 337 return rest_ensure_response( $data );
261 338 }
262 339
263 340 public function filter_items( $request ) {
341 +
264 342 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
265 343 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
266 - $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : 0;
344 + $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : '';
267 345 $category = ! empty( $request['category'] ) ? intval( $request['category'] ) : 0;
268 - $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : 'enabled';
346 + $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : '';
269 347
348 + $restrict_to_enabled = ! current_user_can( 'edit_meeting' );
349 +
270 350 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
271 351 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
272 352
273 353 $appoint = Appointment::all( [
274 - 'posts_per_page' => $per_page,
275 - 'paged' => $paged,
354 + 'posts_per_page' => $restrict_to_enabled ? -1 : $per_page,
355 + 'paged' => $restrict_to_enabled ? 1 : $paged,
276 356 'visibility' => $visibility,
277 357 'staff' => $staff,
278 358 'category' => $category,
279 359 ] );
280 360
361 + $matched = $appoint['items'];
362 + $total = $appoint['total'];
363 +
364 + if ( $restrict_to_enabled ) {
365 + // Public route — never show a disabled meeting type, regardless
366 + // of what visibility was requested. A blank/missing visibility
367 + // meta (legacy rows) is treated as visible, matching the default
368 + // used when saving an appointment.
369 + $matched = array_values(
370 + array_filter(
371 + $matched,
372 + function ( $item ) {
373 + return 'disabled' !== strtolower( (string) ( new Appointment( $item->ID ) )->get_visibility() );
374 + }
375 + )
376 + );
377 +
378 + $total = count( $matched );
379 + $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
380 + }
381 +
281 382 $items = [];
282 383
283 - foreach ( $appoint['items'] as $item ) {
384 + foreach ( $matched as $item ) {
284 385 $items[] = $this->prepare_item( $item->ID );
285 386 }
286 387
287 388 $data = [
@@ -287,9 +388,9 @@
287 388 $data = [
288 389 'success' => 1,
289 390 'status' => 200,
290 391 'data' => [
291 - 'total' => $appoint['total'],
392 + 'total' => $total,
292 393 'items' => $items,
293 394 ],
294 395 ];
295 396
@@ -303,8 +404,9 @@
303 404 *
304 405 * @return JSON Newly created appointment data
305 406 */
306 407 public function create_item( $request ) {
408 +
307 409 /**
308 410 * Added temporary for leagacy sass. It will remove in future.
309 411 */
310 412
@@ -369,11 +471,25 @@
369 471 *
370 472 * @return JSON Updated appointment data
371 473 */
372 474 public function update_item( $request ) {
475 +
373 476 $appointment_id = (int) $request['appointment_id'];
374 477 $appoint = new Appointment( $appointment_id );
375 478
479 + // Handler must not rely solely on permission_callback having run.
480 + if ( ! $this->can_edit_appointment( $appointment_id ) ) {
481 + return new WP_HTTP_Response(
482 + [
483 + 'success' => 0,
484 + 'status_code' => 403,
485 + 'message' => esc_html__( 'You are not allowed to edit this appointment.', 'timetics' ),
486 + 'data' => [],
487 + ],
488 + 403
489 + );
490 + }
491 +
376 492 $data = json_decode( $request->get_body(), true );
377 493
378 494 /**
379 495 * Added temporary for leagacy sass. It will remove in future.
@@ -435,8 +551,67 @@
435 551 return $this->save_appointment( $request, $appointment_id );
436 552 }
437 553
438 554 /**
555 + * Update permission check
556 + *
557 + * @param WP_Rest_Request $request
558 + *
559 + * @return bool
560 + */
561 + public function update_item_permissions_check( $request ) {
562 + return $this->can_edit_appointment( (int) $request['appointment_id'] );
563 + }
564 +
565 + /**
566 + * Object-level authorization for editing an appointment. Called from
567 + * both the route's permission_callback and update_item() itself, so
568 + * the handler never relies solely on the callback having run.
569 + *
570 + * @param int $appointment_id
571 + *
572 + * @return bool
573 + */
574 + private function can_edit_appointment( $appointment_id ) {
575 + if ( current_user_can( 'manage_options' ) ) {
576 + return true;
577 + }
578 +
579 + $current_user_id = get_current_user_id();
580 +
581 + if ( $current_user_id <= 0 ) {
582 + return false;
583 + }
584 +
585 + $appointment = new Appointment( $appointment_id );
586 +
587 + if ( ! $appointment->is_appointment() ) {
588 + return false;
589 + }
590 +
591 + $staff_ids = array_map( 'intval', $appointment->get_staff_ids() );
592 + $author = $appointment->get_author();
593 +
594 + // read_meeting only proves "is staff," not ownership — must not bypass the checks below.
595 + return in_array( $current_user_id, $staff_ids, true )
596 + || $author === $current_user_id;
597 + }
598 +
599 + /**
600 + * True only for the meeting's owner (author) or an administrator.
601 + * Used to gate fields an assigned-but-non-owning staff member must
602 + * not be able to change (staff list, visibility, webhooks).
603 + *
604 + * @param Appointment $appointment
605 + *
606 + * @return bool
607 + */
608 + private function is_appointment_owner( $appointment ) {
609 + return current_user_can( 'manage_options' )
610 + || (int) $appointment->get_author() === get_current_user_id();
611 + }
612 +
613 + /**
439 614 * Get single appointment
440 615 *
441 616 * @param WP_Rest_Requesr $request
442 617 *
@@ -456,8 +631,25 @@
456 631
457 632 return new WP_HTTP_Response( $data, 404 );
458 633 }
459 634
635 + $current_user_id = get_current_user_id();
636 + $is_privileged = current_user_can( 'edit_meeting' )
637 + || $current_user_id == $appoint->get_author()
638 + || in_array( $current_user_id, $appoint->get_staff_ids(), true );
639 +
640 + // This route is public — a disabled meeting type isn't meant to be
641 + // reachable by guessing its id, only owner/staff/admin can still see it.
642 + if ( ! $is_privileged && 'disabled' === strtolower( (string) $appoint->get_visibility() ) ) {
643 + $data = [
644 + 'status_code' => 404,
645 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
646 + 'data' => [],
647 + ];
648 +
649 + return new WP_HTTP_Response( $data, 404 );
650 + }
651 +
460 652 $response = [
461 653 'status_code' => 200,
462 654 'message' => esc_html__( 'Successfully retrieved appointments', 'timetics' ),
463 655 'data' => $this->prepare_item( $appoint ),
@@ -473,8 +665,9 @@
473 665 *
474 666 * @return
475 667 */
476 668 public function delete_item( $request ) {
669 +
477 670 $appoinment_id = (int) $request['appointment_id'];
478 671 $appoint = new Appointment( $appoinment_id );
479 672
480 673 $current_user_id = get_current_user_id();
@@ -509,8 +702,19 @@
509 702 return rest_ensure_response( $response );
510 703 }
511 704
512 705 /**
706 + * Delete item permission check
707 + *
708 + * @param WP_Rest_Request $request
709 + *
710 + * @return bool
711 + */
712 + public function delete_item_permissions_check( $request ) {
713 + return $this->can_edit_appointment( (int) $request['appointment_id'] );
714 + }
715 +
716 + /**
513 717 * Delete multiples
514 718 *
515 719 * @param WP_Rest_Request $request
516 720 *
@@ -516,13 +720,23 @@
516 720 *
517 721 * @return JSON
518 722 */
519 723 public function bulk_delete( $request ) {
724 +
520 725 $appointments = json_decode( $request->get_body(), true );
726 + $appointments = is_array( $appointments ) ? $appointments : [];
521 727
522 - foreach ( $appointments as $appoint ) {
523 - $appoint = new Appointment( $appoint );
728 + $current_user_id = get_current_user_id();
729 + $is_admin = current_user_can( 'manage_options' );
524 730
731 + $to_delete = [];
732 +
733 + // Validate every id — existence and ownership — before deleting any
734 + // of them. The route only checks read_meeting, which every staff
735 + // account has, so ownership has to be enforced here per appointment.
736 + foreach ( $appointments as $appoint_id ) {
737 + $appoint = new Appointment( $appoint_id );
738 +
525 739 if ( ! $appoint->is_appointment() ) {
526 740 $data = [
527 741 'success' => 0,
528 742 'status' => 404,
@@ -532,8 +746,23 @@
532 746
533 747 return new WP_HTTP_Response( $data, 404 );
534 748 }
535 749
750 + if ( ! $is_admin && $appoint->get_author() != $current_user_id ) {
751 + $data = [
752 + 'success' => 0,
753 + 'status' => 403,
754 + 'message' => esc_html__( 'You are not allowed to delete one or more of the selected appointments.', 'timetics' ),
755 + 'data' => [],
756 + ];
757 +
758 + return new WP_HTTP_Response( $data, 403 );
759 + }
760 +
761 + $to_delete[] = $appoint;
762 + }
763 +
764 + foreach ( $to_delete as $appoint ) {
536 765 $appoint->delete();
537 766 }
538 767
539 768 return rest_ensure_response( [
@@ -555,8 +784,9 @@
555 784 *
556 785 * @return JSON
557 786 */
558 787 public function duplicate_item( $request ) {
788 +
559 789 /**
560 790 * Added temporary for leagacy sass. It will remove in future.
561 791 */
562 792
@@ -643,31 +873,49 @@
643 873 $pabbly_hook_overwrite = ! empty( $data['pabbly_hook_overwrite'] ) ? (bool) $data['pabbly_hook_overwrite'] : false;
644 874 $zapier_hook_overwrite = ! empty( $data['zapier_hook_overwrite'] ) ? (bool) $data['zapier_hook_overwrite'] : false;
645 875 $pabbly_webook = ! empty( $data['pabbly_webook'] ) ? $data['pabbly_webook'] : '';
646 876 $zapier_webook = ! empty( $data['zapier_webook'] ) ? $data['zapier_webook'] : '';
877 + $flowmattic_hook_overwrite = ! empty( $data['flowmattic_hook_overwrite'] ) ? (bool) $data['flowmattic_hook_overwrite'] : false;
878 + $flowmattic_webhook = ! empty( $data['flowmattic_webhook'] ) ? esc_url_raw( $data['flowmattic_webhook'] ) : '';
647 879 $min_notice_time = ! empty( $data['min_notice_time'] ) ? $data['min_notice_time'] : '';
648 880 $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
649 881 $guest_enabled = ! empty( $data['guest_enabled'] ) ? intval( $data['guest_enabled'] ) : false;
650 882 $guest_limit = ! empty( $data['guest_limit'] ) ? intval( $data['guest_limit'] ) : 1;
651 883 $capacity = ! empty( $data['capacity'] ) ? intval( $data['capacity'] ) : 1;
652 - $action = $id ? 'update' : 'created';
884 + $appointment_id = ! empty( $data['appointment'] ) ? intval( $data['appointment'] ) : 1;
885 + $action = $id ? 'updated' : 'created';
886 + $buffer_time_before_value = ! empty( $data['buffer_time_before_value'] ) ? $data['buffer_time_before_value'] : 0;
887 + $buffer_time_before_unit = ! empty( $data['buffer_time_before_unit'] ) ? $data['buffer_time_before_unit'] : 'min';
888 + $buffer_time_after_value = ! empty( $data['buffer_time_after_value'] ) ? $data['buffer_time_after_value'] : 0;
889 + $buffer_time_after_unit = ! empty( $data['buffer_time_after_unit'] ) ? $data['buffer_time_after_unit'] : 'min';
653 890
891 + // Assigned-but-non-owning staff may edit their meeting's schedule/details,
892 + // but must not rename it, reassign staff, change visibility, or touch webhook integrations.
893 + if ( $id && ! $this->is_appointment_owner( $appoint ) ) {
894 + $name = $appoint->get_name();
895 + $description = $appoint->get_description();
896 + $staff = $appoint->get_staff();
897 + $visibility = $appoint->get_visibility();
898 + $notifications = $appoint->get_notifications();
899 + $fleunt_crm_webhook = $appoint->get_fleunt_crm_webhook();
900 + $fluent_hook_overwrite = $appoint->get_fluent_hook_overwrite();
901 + $pabbly_hook_overwrite = $appoint->get_pabbly_hook_overwrite();
902 + $zapier_hook_overwrite = $appoint->get_zapier_hook_overwrite();
903 + $pabbly_webook = $appoint->get_pabbly_webook();
904 + $zapier_webook = $appoint->get_zapier_webook();
905 + }
906 +
654 907 if ( $id ) {
655 - $current_user_id = get_current_user_id();
656 -
657 - if ( $appoint->get_author() != $current_user_id ) {
658 - $data = [
659 - 'success' => 0,
660 - 'message' => __( 'You are not allowed to update this meeting.', 'timetics' ),
661 - ];
662 -
663 - return new WP_HTTP_Response( $data, 403 );
908 + $dulicate = $appoint->get_duplicate_nuber();
909 + if ( $dulicate && strpos( $name, '-Duplicate' ) == 0 ) {
910 + $appoint->update([
911 + 'duplicate' => 0
912 + ]);
664 913 }
665 914 }
666 915
667 916 if ( is_array( $price ) ) {
668 917 $ticket_quantity = 0;
669 -
670 918 foreach ( $price as &$ticket ) {
671 919 if ( empty( $ticket['ticket_price'] ) ) {
672 920 $ticket['ticket_price'] = 0;
673 921 }
@@ -726,9 +974,9 @@
726 974 }
727 975
728 976 // Save appointment.
729 977 $appointment_data = [
730 - 'name' => $name,
978 + 'name' => str_replace( '-Duplicate', '', $name ),
731 979 'description' => $description,
732 980 'type' => $type,
733 981 'locations' => $locations,
734 982 'staff' => $staff,
@@ -748,16 +996,25 @@
748 996 'pabbly_hook_overwrite' => $pabbly_hook_overwrite,
749 997 'zapier_hook_overwrite' => $zapier_hook_overwrite,
750 998 'pabbly_webook' => $pabbly_webook,
751 999 'zapier_webook' => $zapier_webook,
1000 + 'flowmattic_hook_overwrite' => $flowmattic_hook_overwrite,
1001 + 'flowmattic_webhook' => $flowmattic_webhook,
752 1002 'min_notice_time' => $min_notice_time,
753 1003 'custom_fields' => $custom_fields,
754 1004 'guest_enabled' => $guest_enabled,
755 1005 'guest_limit' => $guest_limit,
1006 + 'buffer_time_before_value' => $buffer_time_before_value,
1007 + 'buffer_time_before_unit' => $buffer_time_before_unit,
1008 + 'buffer_time_after_value' => $buffer_time_after_value,
1009 + 'buffer_time_after_unit' => $buffer_time_after_unit,
756 1010
757 1011 ];
758 1012
759 - $appointment_data = apply_filters( 'timetics_meeting_insert_data', $data, $appointment_data );
1013 + // The sanitised array is the filterable value; $data (raw body) is only
1014 + // a reference arg. Getting this order backwards silently discards every
1015 + // sanitizer/intval() above and lets the caller write arbitrary post meta.
1016 + $appointment_data = apply_filters( 'timetics_meeting_insert_data', $appointment_data, $data );
760 1017
761 1018 $appoint->set_props( $appointment_data );
762 1019 $appoint->save();
763 1020
@@ -771,9 +1028,10 @@
771 1028
772 1029 $response = [
773 1030 'status_code' => 201,
774 1031 'success' => 1,
775 - 'message' => sprintf( esc_html__( 'Succssfully %s appointment', 'timetics' ), $action ),
1032 + /* translators: %s: Action performed (created, updated, etc.) */
1033 + 'message' => sprintf( esc_html__( 'Successfully %s meeting', 'timetics' ), $action ),
776 1034 'data' => $item,
777 1035 ];
778 1036
779 1037 return rest_ensure_response( $response );
@@ -788,8 +1046,9 @@
788 1046 */
789 1047 public function prepare_item( $appoint_id, $timezone = '' ) {
790 1048 $appointment = new Appointment( $appoint_id );
791 1049 $dulicate = $appointment->get_duplicate_nuber();
1050 +
792 1051 $dulicate_text = $dulicate ? ' -Duplicate' : '';
793 1052 $custom_fields = $appointment->get_custom_fields();
794 1053 $data = [
795 1054 'id' => $appointment->get_id(),
@@ -816,8 +1075,10 @@
816 1075 'pabbly_hook_overwrite' => $appointment->get_pabbly_hook_overwrite(),
817 1076 'pabbly_webook' => $appointment->get_pabbly_webook(),
818 1077 'zapier_hook_overwrite' => $appointment->get_zapier_hook_overwrite(),
819 1078 'zapier_webook' => $appointment->get_zapier_webook(),
1079 + 'flowmattic_hook_overwrite' => $appointment->get_flowmattic_hook_overwrite(),
1080 + 'flowmattic_webhook' => $appointment->get_flowmattic_webhook(),
820 1081 'min_notice_time' => $appointment->get_min_notice_time(),
821 1082 'custom_fields' => $custom_fields ?: [],
822 1083 'permalink' => get_permalink( $appointment->get_id() ),
823 1084 'guest_enabled' => $appointment->get_guest_enabled(),
@@ -822,10 +1083,41 @@
822 1083 'permalink' => get_permalink( $appointment->get_id() ),
823 1084 'guest_enabled' => $appointment->get_guest_enabled(),
824 1085 'guest_limit' => $appointment->get_guest_limit(),
825 1086 'author' => $appointment->get_author(),
1087 + 'buffer_time_before_value' => $appointment->get_buffer_time_before_value(),
1088 + 'buffer_time_before_unit' => $appointment->get_buffer_time_before_unit(),
1089 + 'buffer_time_after_value' => $appointment->get_buffer_time_after_value(),
1090 + 'buffer_time_after_unit' => $appointment->get_buffer_time_after_unit(),
826 1091 ];
827 1092
1093 + // Strip webhook URLs, notifications, and staff PII for non-privileged callers — several read routes here are public.
1094 + if ( ! current_user_can( 'edit_meeting' ) ) {
1095 + unset(
1096 + $data['notifications'],
1097 + $data['fluent_hook_overwrite'],
1098 + $data['fleunt_crm_webhook'],
1099 + $data['pabbly_hook_overwrite'],
1100 + $data['pabbly_webook'],
1101 + $data['zapier_hook_overwrite'],
1102 + $data['zapier_webook'],
1103 + $data['author']
1104 + );
1105 +
1106 + if ( ! empty( $data['staff'] ) && is_array( $data['staff'] ) ) {
1107 + $data['staff'] = array_map(
1108 + function ( $staff ) {
1109 + return [
1110 + 'id' => $staff['id'] ?? 0,
1111 + 'full_name' => $staff['full_name'] ?? '',
1112 + 'image' => $staff['image'] ?? '',
1113 + ];
1114 + },
1115 + $data['staff']
1116 + );
1117 + }
1118 + }
1119 +
828 1120 return apply_filters( 'timetics_meeting_json_data', $data, $appointment );
829 1121 }
830 1122
831 1123 /**
@@ -849,8 +1141,19 @@
849 1141 $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'is not connected to google meet. Please connect to google meet then try again', 'timetics' ) );
850 1142 }
851 1143 break;
852 1144 case 'zoom':
1145 + // Check if zoom addon plugin is active
1146 + if ( ! is_plugin_active( 'timetics-zoom-addon/timetics-zoom-addon.php' ) ) {
1147 + $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'Zoom addon plugin is not active. Please activate the plugin then try again', 'timetics' ) );
1148 + break;
1149 + }
1150 +
1151 + // if zoom_connection_type is server_to_server then no need to check for zoom connection
1152 + if ( timetics_get_option( 'zoom_connection_type' ) == 'server_to_server' ) {
1153 + break;
1154 + }
1155 +
853 1156 if ( ! timetics_is_zoom_connected( $staff_id ) ) {
854 1157 $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'is not connected to zoom. Please connect to zoom then try again', 'timetics' ) );
855 1158 }
856 1159 break;