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 +498 -64 1.0.10 → 1.0.64 View file →
@@ -7,10 +7,13 @@
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;
14 17 use WP_Error;
15 18 use WP_HTTP_Response;
16 19 use WP_REST_Request;
@@ -92,18 +95,14 @@
92 95 ],
93 96 [
94 97 'methods' => \WP_REST_Server::EDITABLE,
95 98 'callback' => [$this, 'update_item'],
96 - 'permission_callback' => function () {
97 - return current_user_can( 'read_meeting' );
98 - },
99 + 'permission_callback' => [ $this, 'update_item_permissions_check' ],
99 100 ],
100 101 [
101 102 'methods' => \WP_REST_Server::DELETABLE,
102 103 'callback' => [$this, 'delete_item'],
103 - 'permission_callback' => function () {
104 - return current_user_can( 'read_meeting' );
105 - },
104 + 'permission_callback' => [$this, 'delete_item_permissions_check' ],
106 105 ],
107 106 ] );
108 107
109 108 register_rest_route( $this->namespace, $this->rest_base . '/search', [
@@ -110,9 +109,11 @@
110 109 [
111 110 'methods' => \WP_REST_Server::READABLE,
112 111 'callback' => [$this, 'search_items'],
113 112 'permission_callback' => function () {
114 - 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' );
115 116 },
116 117 ],
117 118 ] );
118 119
@@ -149,23 +150,64 @@
149 150 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
150 151 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
151 152 $type = ! empty( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';
152 153
153 - $args = [
154 - 'posts_per_page' => $per_page,
155 - 'paged' => $paged,
156 - 'type' => $type,
157 - ];
154 + $restrict_to_own = ! current_user_can( 'edit_meeting' );
155 + $current_user_id = get_current_user_id();
158 156
159 - if ( ! current_user_can( 'edit_meeting' ) ) {
160 - $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 + );
161 168 }
162 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 +
163 183 $appoint = Appointment::all( $args );
184 + $matched = $appoint['items'];
185 + $total = $appoint['total'];
164 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 +
165 207 $items = [];
166 208
167 - foreach ( $appoint['items'] as $item ) {
209 + foreach ( $matched as $item ) {
168 210 $items[] = $this->prepare_item( $item->ID );
169 211 }
170 212
171 213 $data = [
@@ -171,9 +213,9 @@
171 213 $data = [
172 214 'success' => 1,
173 215 'status_code' => 200,
174 216 'data' => [
175 - 'total' => $appoint['total'],
217 + 'total' => $total,
176 218 'items' => $items,
177 219 ],
178 220 ];
179 221
@@ -187,22 +229,35 @@
187 229 *
188 230 * @return JSON
189 231 */
190 232 public function search_items( $request ) {
233 +
191 234 // Prepare search args.
192 - $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
193 - $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
194 - $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' );
195 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 +
196 255 // Get search.
197 256 $appointments = new \WP_Query(
198 - array(
199 - 'post_type' => 'timetics-appointment',
200 - 'posts_per_page' => $per_page,
201 - 'paged' => $paged,
202 - 'orderby' => 'ID',
203 - 'order' => 'DESC',
204 -
257 + array_merge(
258 + $query_args,
259 + array(
205 260 // @codingStandardsIgnoreStart
206 261 'meta_query' => array(
207 262 'relation' => 'OR',
208 263 array(
@@ -236,15 +291,38 @@
236 291 'compare' => 'LIKE',
237 292 ),
238 293 ),
239 294 // @codingStandardsIgnoreEnd
295 + )
240 296 )
241 297 );
242 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 +
243 321 // Prepare items for response.
244 322 $items = [];
245 323
246 - foreach ( $appointments->posts as $item ) {
324 + foreach ( $matched as $item ) {
247 325 $items[] = $this->prepare_item( $item->ID );
248 326 }
249 327
250 328 $data = [
@@ -250,9 +328,9 @@
250 328 $data = [
251 329 'success' => 1,
252 330 'status' => 200,
253 331 'data' => [
254 - 'total' => $appointments->found_posts,
332 + 'total' => $total,
255 333 'items' => $items,
256 334 ],
257 335 ];
258 336
@@ -259,28 +337,52 @@
259 337 return rest_ensure_response( $data );
260 338 }
261 339
262 340 public function filter_items( $request ) {
341 +
263 342 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
264 343 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
265 - $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : 0;
344 + $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : '';
266 345 $category = ! empty( $request['category'] ) ? intval( $request['category'] ) : 0;
267 - $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : 'enabled';
346 + $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : '';
268 347
348 + $restrict_to_enabled = ! current_user_can( 'edit_meeting' );
349 +
269 350 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
270 351 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
271 352
272 353 $appoint = Appointment::all( [
273 - 'posts_per_page' => $per_page,
274 - 'paged' => $paged,
354 + 'posts_per_page' => $restrict_to_enabled ? -1 : $per_page,
355 + 'paged' => $restrict_to_enabled ? 1 : $paged,
275 356 'visibility' => $visibility,
276 357 'staff' => $staff,
277 358 'category' => $category,
278 359 ] );
279 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 +
280 382 $items = [];
281 383
282 - foreach ( $appoint['items'] as $item ) {
384 + foreach ( $matched as $item ) {
283 385 $items[] = $this->prepare_item( $item->ID );
284 386 }
285 387
286 388 $data = [
@@ -286,9 +388,9 @@
286 388 $data = [
287 389 'success' => 1,
288 390 'status' => 200,
289 391 'data' => [
290 - 'total' => $appoint['total'],
392 + 'total' => $total,
291 393 'items' => $items,
292 394 ],
293 395 ];
294 396
@@ -302,37 +404,64 @@
302 404 *
303 405 * @return JSON Newly created appointment data
304 406 */
305 407 public function create_item( $request ) {
408 +
306 409 /**
307 410 * Added temporary for leagacy sass. It will remove in future.
308 411 */
309 - $args['staff'] = get_current_user_id();
310 - $meetings_count = Appointment::all( $args );
412 +
413 + $meetings_count = Appointment::all();
311 414 $data = json_decode( $request->get_body(), true );
312 415
313 416 $response = [
314 417 'success' => 0,
315 - 'status_code' => 502,
418 + 'status_code' => 403,
316 419 'message' => esc_html__( 'Something went wrong', 'timetics' ),
317 420 'data' => [],
318 421 ];
319 422
320 423 if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
321 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'price_check' ) );
424 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
322 425 }
323 426
324 427 if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
325 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'count_check' ) );
428 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ), 403 );
326 429 }
327 430
328 431 $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : '';
329 432
330 433 if ( apply_filters( 'timetics/staff/appointment/type_check', false, $type ) == true ) {
331 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'type_check' ) );
332 - } // End
434 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'type_check' ), 403 );
435 + }
333 436
437 + $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
334 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 +
335 464 return $this->save_appointment( $request );
336 465 }
337 466
338 467 /**
@@ -342,11 +471,25 @@
342 471 *
343 472 * @return JSON Updated appointment data
344 473 */
345 474 public function update_item( $request ) {
475 +
346 476 $appointment_id = (int) $request['appointment_id'];
347 477 $appoint = new Appointment( $appointment_id );
348 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 +
349 492 $data = json_decode( $request->get_body(), true );
350 493
351 494 /**
352 495 * Added temporary for leagacy sass. It will remove in future.
@@ -357,12 +500,43 @@
357 500 'message' => esc_html__( 'Something went wrong', 'timetics' ),
358 501 'data' => [],
359 502 ];
360 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 +
361 508 if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
362 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'price_check' ) );
363 - } // End.
509 + return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
510 + }
364 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 +
365 539 if ( ! $appoint->is_appointment() ) {
366 540
367 541 $response = [
368 542 'success' => 0,
@@ -377,8 +551,67 @@
377 551 return $this->save_appointment( $request, $appointment_id );
378 552 }
379 553
380 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 + /**
381 614 * Get single appointment
382 615 *
383 616 * @param WP_Rest_Requesr $request
384 617 *
@@ -398,8 +631,25 @@
398 631
399 632 return new WP_HTTP_Response( $data, 404 );
400 633 }
401 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 +
402 652 $response = [
403 653 'status_code' => 200,
404 654 'message' => esc_html__( 'Successfully retrieved appointments', 'timetics' ),
405 655 'data' => $this->prepare_item( $appoint ),
@@ -415,11 +665,23 @@
415 665 *
416 666 * @return
417 667 */
418 668 public function delete_item( $request ) {
669 +
419 670 $appoinment_id = (int) $request['appointment_id'];
420 671 $appoint = new Appointment( $appoinment_id );
421 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 +
422 684 if ( ! $appoint->is_appointment() ) {
423 685 return [
424 686 'status_code' => 404,
425 687 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
@@ -440,8 +702,19 @@
440 702 return rest_ensure_response( $response );
441 703 }
442 704
443 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 + /**
444 717 * Delete multiples
445 718 *
446 719 * @param WP_Rest_Request $request
447 720 *
@@ -447,13 +720,23 @@
447 720 *
448 721 * @return JSON
449 722 */
450 723 public function bulk_delete( $request ) {
724 +
451 725 $appointments = json_decode( $request->get_body(), true );
726 + $appointments = is_array( $appointments ) ? $appointments : [];
452 727
453 - foreach ( $appointments as $appoint ) {
454 - $appoint = new Appointment( $appoint );
728 + $current_user_id = get_current_user_id();
729 + $is_admin = current_user_can( 'manage_options' );
455 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 +
456 739 if ( ! $appoint->is_appointment() ) {
457 740 $data = [
458 741 'success' => 0,
459 742 'status' => 404,
@@ -463,8 +746,23 @@
463 746
464 747 return new WP_HTTP_Response( $data, 404 );
465 748 }
466 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 ) {
467 765 $appoint->delete();
468 766 }
469 767
470 768 return rest_ensure_response( [
@@ -486,15 +784,14 @@
486 784 *
487 785 * @return JSON
488 786 */
489 787 public function duplicate_item( $request ) {
788 +
490 789 /**
491 790 * Added temporary for leagacy sass. It will remove in future.
492 791 */
493 792
494 - $args['staff'] = get_current_user_id();
495 - $meetings_count = Appointment::all( $args );
496 - $data = json_decode( $request->get_body(), true );
793 + $meetings_count = Appointment::all();
497 794
498 795 $response = [
499 796 'success' => 0,
500 797 'status_code' => 502,
@@ -501,21 +798,17 @@
501 798 'message' => esc_html__( 'Something went wrong', 'timetics' ),
502 799 'data' => [],
503 800 ];
504 801
505 - if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
506 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'price_check' ) );
507 - }
508 -
509 802 if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
510 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'count_check' ) );
803 + return rest_ensure_response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ) );
511 804 }
512 805
513 - $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : '';
806 + $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
514 807
515 - if ( apply_filters( 'timetics/staff/appointment/type_check', false, $type ) == true ) {
516 - return rest_ensure_response( apply_filter( 'timetics/admin/appointment/error_data', $response, 'type_check' ) );
517 - } // End.
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 + }
518 811
519 812 $appoinment_id = (int) $request['appointment_id'];
520 813 $appoint = new Appointment( $appoinment_id );
521 814
@@ -580,18 +873,54 @@
580 873 $pabbly_hook_overwrite = ! empty( $data['pabbly_hook_overwrite'] ) ? (bool) $data['pabbly_hook_overwrite'] : false;
581 874 $zapier_hook_overwrite = ! empty( $data['zapier_hook_overwrite'] ) ? (bool) $data['zapier_hook_overwrite'] : false;
582 875 $pabbly_webook = ! empty( $data['pabbly_webook'] ) ? $data['pabbly_webook'] : '';
583 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'] ) : '';
584 879 $min_notice_time = ! empty( $data['min_notice_time'] ) ? $data['min_notice_time'] : '';
585 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;
586 883 $capacity = ! empty( $data['capacity'] ) ? intval( $data['capacity'] ) : 1;
587 - $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';
588 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 +
589 916 if ( is_array( $price ) ) {
590 917 $ticket_quantity = 0;
591 -
592 - foreach( $price as $ticket ) {
593 - if ( ! empty( $ticket['ticket_quantity'] ) ) {
918 + foreach ( $price as &$ticket ) {
919 + if ( empty( $ticket['ticket_price'] ) ) {
920 + $ticket['ticket_price'] = 0;
921 + }
922 + if ( ! empty( $ticket['ticket_quantity'] ) ) {
594 923 $ticket_quantity += intval( $ticket['ticket_quantity'] );
595 924 }
596 925 }
597 926
@@ -619,8 +948,21 @@
619 948 if ( ! timetics_is_valid_timezone( $timezone ) ) {
620 949 return new WP_Error( 'timezone_error', __( 'Your timezone is invaid.', 'timetics' ) );
621 950 }
622 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 +
623 965 if ( is_wp_error( $validate ) ) {
624 966 $data = [
625 967 'status_code' => 409,
626 968 'success' => 0,
@@ -632,9 +974,9 @@
632 974 }
633 975
634 976 // Save appointment.
635 977 $appointment_data = [
636 - 'name' => $name,
978 + 'name' => str_replace( '-Duplicate', '', $name ),
637 979 'description' => $description,
638 980 'type' => $type,
639 981 'locations' => $locations,
640 982 'staff' => $staff,
@@ -654,14 +996,25 @@
654 996 'pabbly_hook_overwrite' => $pabbly_hook_overwrite,
655 997 'zapier_hook_overwrite' => $zapier_hook_overwrite,
656 998 'pabbly_webook' => $pabbly_webook,
657 999 'zapier_webook' => $zapier_webook,
1000 + 'flowmattic_hook_overwrite' => $flowmattic_hook_overwrite,
1001 + 'flowmattic_webhook' => $flowmattic_webhook,
658 1002 'min_notice_time' => $min_notice_time,
659 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,
660 1010
661 1011 ];
662 1012
663 - $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 );
664 1017
665 1018 $appoint->set_props( $appointment_data );
666 1019 $appoint->save();
667 1020
@@ -675,9 +1028,10 @@
675 1028
676 1029 $response = [
677 1030 'status_code' => 201,
678 1031 'success' => 1,
679 - '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 ),
680 1034 'data' => $item,
681 1035 ];
682 1036
683 1037 return rest_ensure_response( $response );
@@ -692,8 +1046,9 @@
692 1046 */
693 1047 public function prepare_item( $appoint_id, $timezone = '' ) {
694 1048 $appointment = new Appointment( $appoint_id );
695 1049 $dulicate = $appointment->get_duplicate_nuber();
1050 +
696 1051 $dulicate_text = $dulicate ? ' -Duplicate' : '';
697 1052 $custom_fields = $appointment->get_custom_fields();
698 1053 $data = [
699 1054 'id' => $appointment->get_id(),
@@ -720,13 +1075,92 @@
720 1075 'pabbly_hook_overwrite' => $appointment->get_pabbly_hook_overwrite(),
721 1076 'pabbly_webook' => $appointment->get_pabbly_webook(),
722 1077 'zapier_hook_overwrite' => $appointment->get_zapier_hook_overwrite(),
723 1078 'zapier_webook' => $appointment->get_zapier_webook(),
1079 + 'flowmattic_hook_overwrite' => $appointment->get_flowmattic_hook_overwrite(),
1080 + 'flowmattic_webhook' => $appointment->get_flowmattic_webhook(),
724 1081 'min_notice_time' => $appointment->get_min_notice_time(),
725 1082 'custom_fields' => $custom_fields ?: [],
726 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(),
727 1091 ];
728 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 +
729 1120 return apply_filters( 'timetics_meeting_json_data', $data, $appointment );
730 1121 }
731 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 + }
732 1166 }