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 +678 -108 1.0.1 → 1.0.64 View file →
@@ -7,11 +7,15 @@
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;
15 +use Timetics\Core\Staffs\Staff;
13 16 use Timetics\Utils\Singleton;
17 +use WP_Error;
14 18 use WP_HTTP_Response;
15 19 use WP_REST_Request;
16 20
17 21 /**
@@ -63,9 +67,9 @@
63 67 [
64 68 'methods' => \WP_REST_Server::CREATABLE,
65 69 'callback' => [$this, 'create_item'],
66 70 'permission_callback' => function () {
67 - return current_user_can( 'edit_meeting' );
71 + return current_user_can( 'read_meeting' );
68 72 },
69 73 ],
70 74 [
71 75 'methods' => \WP_REST_Server::DELETABLE,
@@ -70,9 +74,9 @@
70 74 [
71 75 'methods' => \WP_REST_Server::DELETABLE,
72 76 'callback' => [$this, 'bulk_delete'],
73 77 'permission_callback' => function () {
74 - return current_user_can( 'edit_meeting' );
78 + return current_user_can( 'read_meeting' );
75 79 },
76 80 ],
77 81 ] );
78 82
@@ -91,18 +95,14 @@
91 95 ],
92 96 [
93 97 'methods' => \WP_REST_Server::EDITABLE,
94 98 'callback' => [$this, 'update_item'],
95 - 'permission_callback' => function () {
96 - return current_user_can( 'edit_meeting' );
97 - },
99 + 'permission_callback' => [ $this, 'update_item_permissions_check' ],
98 100 ],
99 101 [
100 102 'methods' => \WP_REST_Server::DELETABLE,
101 103 'callback' => [$this, 'delete_item'],
102 - 'permission_callback' => function () {
103 - return current_user_can( 'edit_meeting' );
104 - },
104 + 'permission_callback' => [$this, 'delete_item_permissions_check' ],
105 105 ],
106 106 ] );
107 107
108 108 register_rest_route( $this->namespace, $this->rest_base . '/search', [
@@ -109,9 +109,11 @@
109 109 [
110 110 'methods' => \WP_REST_Server::READABLE,
111 111 'callback' => [$this, 'search_items'],
112 112 'permission_callback' => function () {
113 - 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' );
114 116 },
115 117 ],
116 118 ] );
117 119
@@ -119,9 +121,9 @@
119 121 [
120 122 'methods' => \WP_REST_Server::READABLE,
121 123 'callback' => [$this, 'filter_items'],
122 124 'permission_callback' => function () {
123 - return current_user_can( 'edit_posts' );
125 + return true;
124 126 },
125 127 ],
126 128 ] );
127 129
@@ -146,23 +148,66 @@
146 148 public function get_items( $request ) {
147 149
148 150 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
149 151 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
152 + $type = ! empty( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';
150 153
151 - $args = [
152 - 'posts_per_page' => $per_page,
153 - 'paged' => $paged,
154 - ];
154 + $restrict_to_own = ! current_user_can( 'edit_meeting' );
155 + $current_user_id = get_current_user_id();
155 156
156 - if ( ! current_user_can( 'edit_meeting' ) ) {
157 - $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 + );
158 168 }
159 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 +
160 183 $appoint = Appointment::all( $args );
184 + $matched = $appoint['items'];
185 + $total = $appoint['total'];
161 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 +
162 207 $items = [];
163 208
164 - foreach ( $appoint['items'] as $item ) {
209 + foreach ( $matched as $item ) {
165 210 $items[] = $this->prepare_item( $item->ID );
166 211 }
167 212
168 213 $data = [
@@ -168,9 +213,9 @@
168 213 $data = [
169 214 'success' => 1,
170 215 'status_code' => 200,
171 216 'data' => [
172 - 'total' => $appoint['total'],
217 + 'total' => $total,
173 218 'items' => $items,
174 219 ],
175 220 ];
176 221
@@ -184,22 +229,35 @@
184 229 *
185 230 * @return JSON
186 231 */
187 232 public function search_items( $request ) {
233 +
188 234 // Prepare search args.
189 - $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
190 - $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
191 - $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' );
192 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 +
193 255 // Get search.
194 256 $appointments = new \WP_Query(
195 - array(
196 - 'post_type' => 'timetics-appointment',
197 - 'posts_per_page' => $per_page,
198 - 'paged' => $paged,
199 - 'orderby' => 'ID',
200 - 'order' => 'DESC',
201 -
257 + array_merge(
258 + $query_args,
259 + array(
202 260 // @codingStandardsIgnoreStart
203 261 'meta_query' => array(
204 262 'relation' => 'OR',
205 263 array(
@@ -233,15 +291,38 @@
233 291 'compare' => 'LIKE',
234 292 ),
235 293 ),
236 294 // @codingStandardsIgnoreEnd
295 + )
237 296 )
238 297 );
239 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 +
240 321 // Prepare items for response.
241 322 $items = [];
242 323
243 - foreach ( $appointments->posts as $item ) {
324 + foreach ( $matched as $item ) {
244 325 $items[] = $this->prepare_item( $item->ID );
245 326 }
246 327
247 328 $data = [
@@ -247,9 +328,9 @@
247 328 $data = [
248 329 'success' => 1,
249 330 'status' => 200,
250 331 'data' => [
251 - 'total' => $appointments->found_posts,
332 + 'total' => $total,
252 333 'items' => $items,
253 334 ],
254 335 ];
255 336
@@ -256,26 +337,52 @@
256 337 return rest_ensure_response( $data );
257 338 }
258 339
259 340 public function filter_items( $request ) {
341 +
260 342 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
261 343 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
262 - $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : 0;
263 - $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : 'enabled';
344 + $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : '';
345 + $category = ! empty( $request['category'] ) ? intval( $request['category'] ) : 0;
346 + $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : '';
264 347
348 + $restrict_to_enabled = ! current_user_can( 'edit_meeting' );
349 +
265 350 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
266 351 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
267 352
268 353 $appoint = Appointment::all( [
269 - 'posts_per_page' => $per_page,
270 - 'paged' => $paged,
354 + 'posts_per_page' => $restrict_to_enabled ? -1 : $per_page,
355 + 'paged' => $restrict_to_enabled ? 1 : $paged,
271 356 'visibility' => $visibility,
272 357 'staff' => $staff,
358 + 'category' => $category,
273 359 ] );
274 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 +
275 382 $items = [];
276 383
277 - foreach ( $appoint['items'] as $item ) {
384 + foreach ( $matched as $item ) {
278 385 $items[] = $this->prepare_item( $item->ID );
279 386 }
280 387
281 388 $data = [
@@ -281,9 +388,9 @@
281 388 $data = [
282 389 'success' => 1,
283 390 'status' => 200,
284 391 'data' => [
285 - 'total' => $appoint['total'],
392 + 'total' => $total,
286 393 'items' => $items,
287 394 ],
288 395 ];
289 396
@@ -297,8 +404,64 @@
297 404 *
298 405 * @return JSON Newly created appointment data
299 406 */
300 407 public function create_item( $request ) {
408 +
409 + /**
410 + * Added temporary for leagacy sass. It will remove in future.
411 + */
412 +
413 + $meetings_count = Appointment::all();
414 + $data = json_decode( $request->get_body(), true );
415 +
416 + $response = [
417 + 'success' => 0,
418 + 'status_code' => 403,
419 + 'message' => esc_html__( 'Something went wrong', 'timetics' ),
420 + 'data' => [],
421 + ];
422 +
423 + if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
424 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
425 + }
426 +
427 + if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
428 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ), 403 );
429 + }
430 +
431 + $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : '';
432 +
433 + if ( apply_filters( 'timetics/staff/appointment/type_check', false, $type ) == true ) {
434 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'type_check' ), 403 );
435 + }
436 +
437 + $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
438 +
439 + if ( apply_filters( 'timetics/staff/appointment/category_check', false, $categories ) == true ) {
440 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'category_check' ), 403 );
441 + }
442 +
443 + $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : [];
444 +
445 + if ( apply_filters( 'timetics/staff/appointment/staff_check', false, $staff ) == true ) {
446 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'staff_check' ), 403 );
447 + }
448 +
449 + $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
450 +
451 + if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
452 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ), 403 );
453 + }
454 +
455 + $recurring_limit = ! empty( $data['recurring_limit'] ) ? $data['recurring_limit'] : [];
456 +
457 + if ( apply_filters( 'timetics/staff/appointment/recurring_limit_check', false, $recurring_limit ) == true ) {
458 +
459 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'recurring_limit_check' ), 403 );
460 + }
461 +
462 + // End
463 +
301 464 return $this->save_appointment( $request );
302 465 }
303 466
304 467 /**
@@ -308,21 +471,82 @@
308 471 *
309 472 * @return JSON Updated appointment data
310 473 */
311 474 public function update_item( $request ) {
475 +
312 476 $appointment_id = (int) $request['appointment_id'];
313 477 $appoint = new Appointment( $appointment_id );
314 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 +
492 + $data = json_decode( $request->get_body(), true );
493 +
494 + /**
495 + * Added temporary for leagacy sass. It will remove in future.
496 + */
497 + $response = [
498 + 'success' => 0,
499 + 'status_code' => 502,
500 + 'message' => esc_html__( 'Something went wrong', 'timetics' ),
501 + 'data' => [],
502 + ];
503 +
504 + if ( !empty($data['availability']) && $data['availability'] && apply_filters('timetics/staff/meeting/availability', false)) {
505 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'availability_update' ), 403 );
506 + }
507 +
508 + if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
509 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
510 + }
511 +
512 + $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
513 +
514 + if ( apply_filters( 'timetics/staff/appointment/category_check', false, $categories ) == true ) {
515 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'category_check' ), 403 );
516 + }
517 +
518 + $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : [];
519 +
520 + if ( apply_filters( 'timetics/staff/appointment/staff_check', false, $staff ) == true ) {
521 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'staff_check' ), 403 );
522 + }
523 +
524 + $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
525 +
526 + if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
527 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ), 403 );
528 + }
529 +
530 + $recurring_limit = ! empty( $data['recurring_limit'] ) ? $data['recurring_limit'] : [];
531 +
532 + if ( apply_filters( 'timetics/staff/appointment/recurring_limit_check', false, $recurring_limit ) == true ) {
533 +
534 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'recurring_limit_check' ), 403 );
535 + }
536 +
537 + // End.
538 +
315 539 if ( ! $appoint->is_appointment() ) {
316 540
317 - $data = [
541 + $response = [
318 542 'success' => 0,
319 543 'status_code' => 404,
320 - 'message' => __( 'Invalid appointment id.', 'timetics' ),
544 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
321 545 'data' => [],
322 546 ];
323 547
324 - return new WP_HTTP_Response( $data, 404 );
548 + return new WP_HTTP_Response( $response, 404 );
325 549 }
326 550
327 551 return $this->save_appointment( $request, $appointment_id );
328 552 }
@@ -327,8 +551,67 @@
327 551 return $this->save_appointment( $request, $appointment_id );
328 552 }
329 553
330 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 + /**
331 614 * Get single appointment
332 615 *
333 616 * @param WP_Rest_Requesr $request
334 617 *
@@ -341,9 +624,9 @@
341 624 if ( ! $appoint->is_appointment() ) {
342 625
343 626 $data = [
344 627 'status_code' => 404,
345 - 'message' => __( 'Invalid appointment id.', 'timetics' ),
628 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
346 629 'data' => [],
347 630 ];
348 631
349 632 return new WP_HTTP_Response( $data, 404 );
@@ -348,11 +631,28 @@
348 631
349 632 return new WP_HTTP_Response( $data, 404 );
350 633 }
351 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 +
352 652 $response = [
353 653 'status_code' => 200,
354 - 'message' => __( 'Successfully retrieved appointments', 'timetics' ),
654 + 'message' => esc_html__( 'Successfully retrieved appointments', 'timetics' ),
355 655 'data' => $this->prepare_item( $appoint ),
356 656 ];
357 657
358 658 return rest_ensure_response( $response );
@@ -365,15 +665,27 @@
365 665 *
366 666 * @return
367 667 */
368 668 public function delete_item( $request ) {
669 +
369 670 $appoinment_id = (int) $request['appointment_id'];
370 671 $appoint = new Appointment( $appoinment_id );
371 672
673 + $current_user_id = get_current_user_id();
674 +
675 + if ( $appoint->get_author() != $current_user_id ) {
676 + $data = [
677 + 'success' => 0,
678 + 'message' => __( 'You are not allowed to delete this meeting.', 'timetics' ),
679 + ];
680 +
681 + return new WP_HTTP_Response( $data, 403 );
682 + }
683 +
372 684 if ( ! $appoint->is_appointment() ) {
373 685 return [
374 686 'status_code' => 404,
375 - 'message' => __( 'Invalid appointment id.', 'timetics' ),
687 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
376 688 'data' => [],
377 689 ];
378 690 }
379 691
@@ -380,9 +692,9 @@
380 692 $appoint->delete();
381 693
382 694 $response = [
383 695 'status_code' => 201,
384 - 'message' => __( 'Successfully deleted appointment', 'timetics' ),
696 + 'message' => esc_html__( 'Successfully deleted appointment', 'timetics' ),
385 697 'data' => [
386 698 'item' => $appoinment_id,
387 699 ],
388 700 ];
@@ -390,8 +702,19 @@
390 702 return rest_ensure_response( $response );
391 703 }
392 704
393 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 + /**
394 717 * Delete multiples
395 718 *
396 719 * @param WP_Rest_Request $request
397 720 *
@@ -397,18 +720,28 @@
397 720 *
398 721 * @return JSON
399 722 */
400 723 public function bulk_delete( $request ) {
724 +
401 725 $appointments = json_decode( $request->get_body(), true );
726 + $appointments = is_array( $appointments ) ? $appointments : [];
402 727
403 - foreach ( $appointments as $appoint ) {
404 - $appoint = new Appointment( $appoint );
728 + $current_user_id = get_current_user_id();
729 + $is_admin = current_user_can( 'manage_options' );
405 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 +
406 739 if ( ! $appoint->is_appointment() ) {
407 740 $data = [
408 741 'success' => 0,
409 742 'status' => 404,
410 - 'message' => __( 'Invalid appointment id.', 'timetics' ),
743 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
411 744 'data' => [],
412 745 ];
413 746
414 747 return new WP_HTTP_Response( $data, 404 );
@@ -413,8 +746,23 @@
413 746
414 747 return new WP_HTTP_Response( $data, 404 );
415 748 }
416 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 ) {
417 765 $appoint->delete();
418 766 }
419 767
420 768 return rest_ensure_response( [
@@ -419,9 +767,9 @@
419 767
420 768 return rest_ensure_response( [
421 769 'success' => 1,
422 770 'status' => 201,
423 - 'message' => __( 'Successfully deleted all appointments', 'timetics' ),
771 + 'message' => esc_html__( 'Successfully deleted all appointments', 'timetics' ),
424 772 'data' => [
425 773 'items' => $appointments,
426 774 ],
427 775 ] );
@@ -436,8 +784,32 @@
436 784 *
437 785 * @return JSON
438 786 */
439 787 public function duplicate_item( $request ) {
788 +
789 + /**
790 + * Added temporary for leagacy sass. It will remove in future.
791 + */
792 +
793 + $meetings_count = Appointment::all();
794 +
795 + $response = [
796 + 'success' => 0,
797 + 'status_code' => 502,
798 + 'message' => esc_html__( 'Something went wrong', 'timetics' ),
799 + 'data' => [],
800 + ];
801 +
802 + if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
803 + return rest_ensure_response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ) );
804 + }
805 +
806 + $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
807 +
808 + if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
809 + return rest_ensure_response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ) );
810 + }
811 +
440 812 $appoinment_id = (int) $request['appointment_id'];
441 813 $appoint = new Appointment( $appoinment_id );
442 814
443 815 if ( ! $appoint->is_appointment() ) {
@@ -444,9 +816,9 @@
444 816 return new WP_HTTP_Response(
445 817 [
446 818 'success' => 0,
447 819 'status_code' => 404,
448 - 'message' => __( 'Invalid appointment id.', 'timetics' ),
820 + 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
449 821 'data' => [],
450 822 ],
451 823 404
452 824 );
@@ -458,9 +830,9 @@
458 830
459 831 $response = [
460 832 'success' => 1,
461 833 'status_code' => 201,
462 - 'message' => __( 'Successfully duplicated appointment', 'timetics' ),
834 + 'message' => esc_html__( 'Successfully duplicated appointment', 'timetics' ),
463 835 'data' => $item,
464 836 ];
465 837
466 838 return rest_ensure_response( $response );
@@ -478,31 +850,91 @@
478 850 $appoint = new Appointment( $id );
479 851
480 852 $data = json_decode( $request->get_body(), true );
481 853
482 - $name = ! empty( $data['name'] ) ? sanitize_text_field( $data['name'] ) : $appoint->get_name();
483 - $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : $appoint->get_type();
484 - $description = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : '';
485 - $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : $appoint->get_staff();
486 - $locations = ! empty( $data['locations'] ) ? $data['locations'] : $appoint->get_locations();
487 - $duration = ! empty( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : '';
488 - $schedule = ! empty( $data['schedule'] ) ? $data['schedule'] : $appoint->get_schedule();
489 - $price = ! empty( $data['price'] ) ? sanitize_text_field( $data['price'] ) : '';
490 - $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
491 - $buffer_time = ! empty( $data['buffer_time'] ) ? $data['buffer_time'] : '';
492 - $timezone = ! empty( $data['timezone'] ) ? $data['timezone'] : '';
493 - $availability = ! empty( $data['availability'] ) ? $data['availability'] : '';
494 - $visibility = ! empty( $data['visibility'] ) ? strtolower( $data['visibility'] ) : 'enabled';
495 - $notifications = ! empty( $data['notifications'] ) ? $data['notifications'] : '';
496 - $capacity = ! empty( $data['capacity'] ) ? intval( $data['capacity'] ) : 1;
497 - $action = $id ? 'update' : 'created';
854 + $data = apply_filters( 'timetics_meeting_data', $data );
498 855
856 + $name = ! empty( $data['name'] ) ? sanitize_text_field( $data['name'] ) : $appoint->get_name();
857 + $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : $appoint->get_type();
858 + $description = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : '';
859 + $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : $appoint->get_staff();
860 + $locations = ! empty( $data['locations'] ) ? $data['locations'] : $appoint->get_locations();
861 + $duration = ! empty( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : '';
862 + $schedule = ! empty( $data['schedule'] ) ? $data['schedule'] : $appoint->get_schedule();
863 + $blocked_schedule = ! empty( $data['blocked_schedule'] ) ? $data['blocked_schedule'] : $appoint->get_blocked_schedule();
864 + $price = ! empty( $data['price'] ) ? $data['price'] : '';
865 + $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
866 + $buffer_time = ! empty( $data['buffer_time'] ) ? $data['buffer_time'] : '';
867 + $timezone = ! empty( $data['timezone'] ) ? $data['timezone'] : '';
868 + $availability = ! empty( $data['availability'] ) ? $data['availability'] : '';
869 + $visibility = ! empty( $data['visibility'] ) ? strtolower( $data['visibility'] ) : 'enabled';
870 + $notifications = ! empty( $data['notifications'] ) ? $data['notifications'] : '';
871 + $fleunt_crm_webhook = ! empty( $data['fleunt_crm_webhook'] ) ? $data['fleunt_crm_webhook'] : '';
872 + $fluent_hook_overwrite = ! empty( $data['fluent_hook_overwrite'] ) ? (bool) $data['fluent_hook_overwrite'] : false;
873 + $pabbly_hook_overwrite = ! empty( $data['pabbly_hook_overwrite'] ) ? (bool) $data['pabbly_hook_overwrite'] : false;
874 + $zapier_hook_overwrite = ! empty( $data['zapier_hook_overwrite'] ) ? (bool) $data['zapier_hook_overwrite'] : false;
875 + $pabbly_webook = ! empty( $data['pabbly_webook'] ) ? $data['pabbly_webook'] : '';
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'] ) : '';
879 + $min_notice_time = ! empty( $data['min_notice_time'] ) ? $data['min_notice_time'] : '';
880 + $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
881 + $guest_enabled = ! empty( $data['guest_enabled'] ) ? intval( $data['guest_enabled'] ) : false;
882 + $guest_limit = ! empty( $data['guest_limit'] ) ? intval( $data['guest_limit'] ) : 1;
883 + $capacity = ! empty( $data['capacity'] ) ? intval( $data['capacity'] ) : 1;
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';
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 +
907 + if ( $id ) {
908 + $dulicate = $appoint->get_duplicate_nuber();
909 + if ( $dulicate && strpos( $name, '-Duplicate' ) == 0 ) {
910 + $appoint->update([
911 + 'duplicate' => 0
912 + ]);
913 + }
914 + }
915 +
916 + if ( is_array( $price ) ) {
917 + $ticket_quantity = 0;
918 + foreach ( $price as &$ticket ) {
919 + if ( empty( $ticket['ticket_price'] ) ) {
920 + $ticket['ticket_price'] = 0;
921 + }
922 + if ( ! empty( $ticket['ticket_quantity'] ) ) {
923 + $ticket_quantity += intval( $ticket['ticket_quantity'] );
924 + }
925 + }
926 +
927 + $capacity = $ticket_quantity;
928 + }
929 +
499 930 $validate_data = [
500 - 'name' => $name,
501 - 'type' => $type,
502 - 'locations' => $locations,
503 - 'schedule' => $schedule,
504 - 'staff' => $staff,
931 + 'name' => $name,
932 + 'type' => $type,
933 + 'locations' => $locations,
934 + 'schedule' => $schedule,
935 + 'blocked_schedule' => $blocked_schedule,
936 + 'staff' => $staff,
505 937
506 938 ];
507 939 // Validate input data.
508 940 $validate = $this->validate( $validate_data, [
@@ -512,8 +944,25 @@
512 944 'schedule',
513 945 'staff',
514 946 ] );
515 947
948 + if ( ! timetics_is_valid_timezone( $timezone ) ) {
949 + return new WP_Error( 'timezone_error', __( 'Your timezone is invaid.', 'timetics' ) );
950 + }
951 +
952 + $lolcation_errors = $this->get_location_errors( $locations, $staff );
953 +
954 + if ( $lolcation_errors ) {
955 + $data = [
956 + 'status_code' => 409,
957 + 'success' => 0,
958 + 'message' => $lolcation_errors,
959 + 'data' => [],
960 + ];
961 +
962 + return new WP_HTTP_Response( $data, 409 );
963 + }
964 +
516 965 if ( is_wp_error( $validate ) ) {
517 966 $data = [
518 967 'status_code' => 409,
519 968 'success' => 0,
@@ -524,29 +973,57 @@
524 973 return new WP_HTTP_Response( $data, 409 );
525 974 }
526 975
527 976 // Save appointment.
977 + $appointment_data = [
978 + 'name' => str_replace( '-Duplicate', '', $name ),
979 + 'description' => $description,
980 + 'type' => $type,
981 + 'locations' => $locations,
982 + 'staff' => $staff,
983 + 'duration' => $duration,
984 + 'price' => $price,
985 + 'capacity' => $capacity,
986 + 'schedule' => $schedule,
987 + 'blocked_schedule' => $blocked_schedule,
988 + 'categories' => $categories,
989 + 'timezone' => $timezone,
990 + 'availability' => $availability,
991 + 'visibility' => $visibility,
992 + 'buffer_time' => $buffer_time,
993 + 'notifications' => $notifications,
994 + 'fleunt_crm_webhook' => $fleunt_crm_webhook,
995 + 'fluent_hook_overwrite' => $fluent_hook_overwrite,
996 + 'pabbly_hook_overwrite' => $pabbly_hook_overwrite,
997 + 'zapier_hook_overwrite' => $zapier_hook_overwrite,
998 + 'pabbly_webook' => $pabbly_webook,
999 + 'zapier_webook' => $zapier_webook,
1000 + 'flowmattic_hook_overwrite' => $flowmattic_hook_overwrite,
1001 + 'flowmattic_webhook' => $flowmattic_webhook,
1002 + 'min_notice_time' => $min_notice_time,
1003 + 'custom_fields' => $custom_fields,
1004 + 'guest_enabled' => $guest_enabled,
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,
528 1010
529 - $appoint->set_props( [
530 - 'name' => $name,
531 - 'description' => $description,
532 - 'type' => $type,
533 - 'locations' => $locations,
534 - 'staff' => $staff,
535 - 'duration' => $duration,
536 - 'price' => $price,
537 - 'capacity' => $capacity,
538 - 'schedule' => $schedule,
539 - 'categories' => $categories,
540 - 'timezone' => $timezone,
541 - 'availability' => $availability,
542 - 'visibility' => $visibility,
543 - 'buffer_time' => $buffer_time,
544 - 'notifications' => $notifications,
545 - ] );
1011 + ];
546 1012
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 );
1017 +
1018 + $appoint->set_props( $appointment_data );
547 1019 $appoint->save();
548 1020
1021 + // Assign meeting category.
1022 + wp_set_post_terms( $appoint->get_id(), $categories, 'timetics-meeting-category' );
1023 +
1024 + do_action( 'timetics_meeting_after_insert', $appoint, $data );
1025 +
549 1026 // Prepare response data.
550 1027 $item = $this->prepare_item( $appoint );
551 1028
552 1029 $response = [
@@ -551,9 +1028,10 @@
551 1028
552 1029 $response = [
553 1030 'status_code' => 201,
554 1031 'success' => 1,
555 - 'message' => sprintf( __( 'Succssfully %s appointment', 'timetics' ), $action ),
1032 + /* translators: %s: Action performed (created, updated, etc.) */
1033 + 'message' => sprintf( esc_html__( 'Successfully %s meeting', 'timetics' ), $action ),
556 1034 'data' => $item,
557 1035 ];
558 1036
559 1037 return rest_ensure_response( $response );
@@ -565,32 +1043,124 @@
565 1043 * @param integer $appoinment_id
566 1044 *
567 1045 * @return array
568 1046 */
569 - public function prepare_item( $appoint_id ) {
1047 + public function prepare_item( $appoint_id, $timezone = '' ) {
570 1048 $appointment = new Appointment( $appoint_id );
571 1049 $dulicate = $appointment->get_duplicate_nuber();
1050 +
572 1051 $dulicate_text = $dulicate ? ' -Duplicate' : '';
573 -
574 - return [
575 - 'id' => $appointment->get_id(),
576 - 'name' => $appointment->get_name() . $dulicate_text,
577 - 'image' => $appointment->get_image(),
1052 + $custom_fields = $appointment->get_custom_fields();
1053 + $data = [
1054 + 'id' => $appointment->get_id(),
1055 + 'name' => $appointment->get_name() . $dulicate_text,
1056 + 'image' => $appointment->get_image(),
578 1057 // 'link' => $appointment->get_link(),
579 - 'description' => $appointment->get_description(),
580 - 'type' => $appointment->get_type(),
581 - 'locations' => $appointment->get_locations(),
582 - 'schedule' => $appointment->get_schedule(),
583 - 'price' => $appointment->get_price(),
584 - 'categories' => $appointment->get_categories(),
585 - 'staff' => $appointment->get_staff(),
586 - 'buffer_time' => $appointment->get_buffer_time(),
587 - 'timezone' => $appointment->get_timezone(),
588 - 'availability' => $appointment->get_availability(),
589 - 'visibility' => $appointment->get_visibility(),
590 - 'duration' => $appointment->get_duration(),
591 - 'notifications' => $appointment->get_notifications(),
592 - 'capacity' => $appointment->get_capacity(),
1058 + 'description' => $appointment->get_description(),
1059 + 'type' => $appointment->get_type(),
1060 + 'locations' => $appointment->get_locations(),
1061 + 'schedule' => $appointment->get_schedule(),
1062 + 'blocked_schedule' => $appointment->get_blocked_schedule(),
1063 + 'price' => $appointment->get_price(),
1064 + 'categories' => $appointment->get_category_ids(),
1065 + 'staff' => $appointment->get_staff(),
1066 + 'buffer_time' => $appointment->get_buffer_time(),
1067 + 'timezone' => $appointment->get_timezone(),
1068 + 'availability' => $appointment->get_availability(),
1069 + 'visibility' => $appointment->get_visibility(),
1070 + 'duration' => $appointment->get_duration(),
1071 + 'notifications' => $appointment->get_notifications(),
1072 + 'capacity' => $appointment->get_capacity(),
1073 + 'fluent_hook_overwrite' => $appointment->get_fluent_hook_overwrite(),
1074 + 'fleunt_crm_webhook' => $appointment->get_fleunt_crm_webhook(),
1075 + 'pabbly_hook_overwrite' => $appointment->get_pabbly_hook_overwrite(),
1076 + 'pabbly_webook' => $appointment->get_pabbly_webook(),
1077 + 'zapier_hook_overwrite' => $appointment->get_zapier_hook_overwrite(),
1078 + 'zapier_webook' => $appointment->get_zapier_webook(),
1079 + 'flowmattic_hook_overwrite' => $appointment->get_flowmattic_hook_overwrite(),
1080 + 'flowmattic_webhook' => $appointment->get_flowmattic_webhook(),
1081 + 'min_notice_time' => $appointment->get_min_notice_time(),
1082 + 'custom_fields' => $custom_fields ?: [],
1083 + 'permalink' => get_permalink( $appointment->get_id() ),
1084 + 'guest_enabled' => $appointment->get_guest_enabled(),
1085 + 'guest_limit' => $appointment->get_guest_limit(),
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(),
593 1091 ];
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 +
1120 + return apply_filters( 'timetics_meeting_json_data', $data, $appointment );
594 1121 }
595 1122
1123 + /**
1124 + * Validate location with zoom and google connection
1125 + *
1126 + * @param array $locations
1127 + * @param array $staffs
1128 + *
1129 + * @return array
1130 + */
1131 + public function get_location_errors( $locations, $staffs ) {
1132 +
1133 + $errors = [];
1134 +
1135 + foreach ( $staffs as $staff_id ) {
1136 + $staff = new Staff( $staff_id );
1137 + foreach ( $locations as $location ) {
1138 + switch ( $location['location_type'] ) {
1139 + case 'google-meet':
1140 + if ( ! timetics_is_google_meet_connected( $staff_id ) ) {
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' ) );
1142 + }
1143 + break;
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 +
1156 + if ( ! timetics_is_zoom_connected( $staff_id ) ) {
1157 + $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'is not connected to zoom. Please connect to zoom then try again', 'timetics' ) );
1158 + }
1159 + break;
1160 + }
1161 + }
1162 + }
1163 +
1164 + return $errors;
1165 + }
596 1166 }