PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.38
Timetics – Appointment Booking Calendar & Scheduling v1.0.38
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 1.0.22 All 64 releases
← All changes | core/appointments/api-appointment.php +57 -279 1.0.621.0.38 View file →
@@ -7,10 +7,8 @@
7 7 * @package Timetics
8 8 */
9 9 namespace Timetics\Core\Appointments;
10 10
11 -defined( 'ABSPATH' ) || exit;
12 -
13 11 use Timetics\Base\Api;
14 12 use Timetics\Core\Appointments\Appointment;
15 13 use Timetics\Core\Staffs\Staff;
16 14 use Timetics\Utils\Singleton;
@@ -109,11 +107,9 @@
109 107 [
110 108 'methods' => \WP_REST_Server::READABLE,
111 109 'callback' => [$this, 'search_items'],
112 110 'permission_callback' => function () {
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' );
111 + return current_user_can( 'edit_posts' );
116 112 },
117 113 ],
118 114 ] );
119 115
@@ -150,64 +146,23 @@
150 146 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
151 147 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
152 148 $type = ! empty( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';
153 149
154 - $restrict_to_own = ! current_user_can( 'edit_meeting' );
155 - $current_user_id = get_current_user_id();
150 + $args = [
151 + 'posts_per_page' => $per_page,
152 + 'paged' => $paged,
153 + 'type' => $type,
154 + ];
156 155
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 - );
156 + if ( ! current_user_can( 'edit_meeting' ) ) {
157 + $args['staff'] = get_current_user_id();
168 158 }
169 159
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 -
183 160 $appoint = Appointment::all( $args );
184 - $matched = $appoint['items'];
185 - $total = $appoint['total'];
186 161
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 -
207 162 $items = [];
208 163
209 - foreach ( $matched as $item ) {
164 + foreach ( $appoint['items'] as $item ) {
210 165 $items[] = $this->prepare_item( $item->ID );
211 166 }
212 167
213 168 $data = [
@@ -213,9 +168,9 @@
213 168 $data = [
214 169 'success' => 1,
215 170 'status_code' => 200,
216 171 'data' => [
217 - 'total' => $total,
172 + 'total' => $appoint['total'],
218 173 'items' => $items,
219 174 ],
220 175 ];
221 176
@@ -231,33 +186,21 @@
231 186 */
232 187 public function search_items( $request ) {
233 188
234 189 // Prepare search args.
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' );
190 + $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
191 + $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
192 + $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
239 193
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 -
255 194 // Get search.
256 195 $appointments = new \WP_Query(
257 - array_merge(
258 - $query_args,
259 - array(
196 + array(
197 + 'post_type' => 'timetics-appointment',
198 + 'posts_per_page' => $per_page,
199 + 'paged' => $paged,
200 + 'orderby' => 'ID',
201 + 'order' => 'DESC',
202 +
260 203 // @codingStandardsIgnoreStart
261 204 'meta_query' => array(
262 205 'relation' => 'OR',
263 206 array(
@@ -291,38 +234,15 @@
291 234 'compare' => 'LIKE',
292 235 ),
293 236 ),
294 237 // @codingStandardsIgnoreEnd
295 - )
296 238 )
297 239 );
298 240
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 -
321 241 // Prepare items for response.
322 242 $items = [];
323 243
324 - foreach ( $matched as $item ) {
244 + foreach ( $appointments->posts as $item ) {
325 245 $items[] = $this->prepare_item( $item->ID );
326 246 }
327 247
328 248 $data = [
@@ -328,9 +248,9 @@
328 248 $data = [
329 249 'success' => 1,
330 250 'status' => 200,
331 251 'data' => [
332 - 'total' => $total,
252 + 'total' => $appointments->found_posts,
333 253 'items' => $items,
334 254 ],
335 255 ];
336 256
@@ -340,49 +260,26 @@
340 260 public function filter_items( $request ) {
341 261
342 262 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
343 263 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
344 - $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : '';
264 + $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : 0;
345 265 $category = ! empty( $request['category'] ) ? intval( $request['category'] ) : 0;
346 266 $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : '';
347 267
348 - $restrict_to_enabled = ! current_user_can( 'edit_meeting' );
349 -
350 268 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
351 269 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
352 270
353 271 $appoint = Appointment::all( [
354 - 'posts_per_page' => $restrict_to_enabled ? -1 : $per_page,
355 - 'paged' => $restrict_to_enabled ? 1 : $paged,
272 + 'posts_per_page' => $per_page,
273 + 'paged' => $paged,
356 274 'visibility' => $visibility,
357 275 'staff' => $staff,
358 276 'category' => $category,
359 277 ] );
360 278
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 -
382 279 $items = [];
383 280
384 - foreach ( $matched as $item ) {
281 + foreach ( $appoint['items'] as $item ) {
385 282 $items[] = $this->prepare_item( $item->ID );
386 283 }
387 284
388 285 $data = [
@@ -388,9 +285,9 @@
388 285 $data = [
389 286 'success' => 1,
390 287 'status' => 200,
391 288 'data' => [
392 - 'total' => $total,
289 + 'total' => $appoint['total'],
393 290 'items' => $items,
394 291 ],
395 292 ];
396 293
@@ -475,21 +372,8 @@
475 372
476 373 $appointment_id = (int) $request['appointment_id'];
477 374 $appoint = new Appointment( $appointment_id );
478 375
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 376 $data = json_decode( $request->get_body(), true );
493 377
494 378 /**
495 379 * Added temporary for leagacy sass. It will remove in future.
@@ -558,60 +442,28 @@
558 442 *
559 443 * @return bool
560 444 */
561 445 public function update_item_permissions_check( $request ) {
562 - return $this->can_edit_appointment( (int) $request['appointment_id'] );
563 - }
446 + $appoinment_id = (int) $request['appointment_id'];
447 + $appointment = new Appointment( $appoinment_id );
564 448
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 -
449 + $staff_ids = $appointment->get_staff_ids();
450 + $author = $appointment->get_author();
579 451 $current_user_id = get_current_user_id();
580 452
581 - if ( $current_user_id <= 0 ) {
582 - return false;
453 + if (
454 + current_user_can( 'manage_options' )
455 + || current_user_can( 'read_meeting' )
456 + || in_array( $current_user_id, $staff_ids )
457 + || $author == $current_user_id
458 + ) {
459 + return true;
583 460 }
584 461
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;
462 + return false;
597 463 }
598 464
599 465 /**
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 - /**
614 466 * Get single appointment
615 467 *
616 468 * @param WP_Rest_Requesr $request
617 469 *
@@ -631,25 +483,8 @@
631 483
632 484 return new WP_HTTP_Response( $data, 404 );
633 485 }
634 486
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 -
652 487 $response = [
653 488 'status_code' => 200,
654 489 'message' => esc_html__( 'Successfully retrieved appointments', 'timetics' ),
655 490 'data' => $this->prepare_item( $appoint ),
@@ -709,9 +544,23 @@
709 544 *
710 545 * @return bool
711 546 */
712 547 public function delete_item_permissions_check( $request ) {
713 - return $this->can_edit_appointment( (int) $request['appointment_id'] );
548 + $appoinment_id = (int) $request['appointment_id'];
549 + $appointment = new Appointment( $appoinment_id );
550 +
551 + $staff_ids = $appointment->get_staff_ids();
552 + $author = $appointment->get_author();
553 + $current_user_id = get_current_user_id();
554 +
555 + if (
556 + current_user_can( 'manage_options' )
557 + || $author == $current_user_id
558 + ) {
559 + return true;
560 + }
561 +
562 + return false;
714 563 }
715 564
716 565 /**
717 566 * Delete multiples
@@ -722,21 +571,12 @@
722 571 */
723 572 public function bulk_delete( $request ) {
724 573
725 574 $appointments = json_decode( $request->get_body(), true );
726 - $appointments = is_array( $appointments ) ? $appointments : [];
727 575
728 - $current_user_id = get_current_user_id();
729 - $is_admin = current_user_can( 'manage_options' );
576 + foreach ( $appointments as $appoint ) {
577 + $appoint = new Appointment( $appoint );
730 578
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 -
739 579 if ( ! $appoint->is_appointment() ) {
740 580 $data = [
741 581 'success' => 0,
742 582 'status' => 404,
@@ -746,23 +586,8 @@
746 586
747 587 return new WP_HTTP_Response( $data, 404 );
748 588 }
749 589
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 ) {
765 590 $appoint->delete();
766 591 }
767 592
768 593 return rest_ensure_response( [
@@ -885,24 +710,8 @@
885 710 $buffer_time_before_unit = ! empty( $data['buffer_time_before_unit'] ) ? $data['buffer_time_before_unit'] : 'min';
886 711 $buffer_time_after_value = ! empty( $data['buffer_time_after_value'] ) ? $data['buffer_time_after_value'] : 0;
887 712 $buffer_time_after_unit = ! empty( $data['buffer_time_after_unit'] ) ? $data['buffer_time_after_unit'] : 'min';
888 713
889 - // Assigned-but-non-owning staff may edit their meeting's schedule/details,
890 - // but must not rename it, reassign staff, change visibility, or touch webhook integrations.
891 - if ( $id && ! $this->is_appointment_owner( $appoint ) ) {
892 - $name = $appoint->get_name();
893 - $description = $appoint->get_description();
894 - $staff = $appoint->get_staff();
895 - $visibility = $appoint->get_visibility();
896 - $notifications = $appoint->get_notifications();
897 - $fleunt_crm_webhook = $appoint->get_fleunt_crm_webhook();
898 - $fluent_hook_overwrite = $appoint->get_fluent_hook_overwrite();
899 - $pabbly_hook_overwrite = $appoint->get_pabbly_hook_overwrite();
900 - $zapier_hook_overwrite = $appoint->get_zapier_hook_overwrite();
901 - $pabbly_webook = $appoint->get_pabbly_webook();
902 - $zapier_webook = $appoint->get_zapier_webook();
903 - }
904 -
905 714 if ( $id ) {
906 715 $dulicate = $appoint->get_duplicate_nuber();
907 716 if ( $dulicate && strpos( $name, '-Duplicate' ) == 0 ) {
908 717 $appoint->update([
@@ -1005,12 +814,9 @@
1005 814 'buffer_time_after_unit' => $buffer_time_after_unit,
1006 815
1007 816 ];
1008 817
1009 - // The sanitised array is the filterable value; $data (raw body) is only
1010 - // a reference arg. Getting this order backwards silently discards every
1011 - // sanitizer/intval() above and lets the caller write arbitrary post meta.
1012 - $appointment_data = apply_filters( 'timetics_meeting_insert_data', $appointment_data, $data );
818 + $appointment_data = apply_filters( 'timetics_meeting_insert_data', $data, $appointment_data );
1013 819
1014 820 $appoint->set_props( $appointment_data );
1015 821 $appoint->save();
1016 822
@@ -1024,9 +830,8 @@
1024 830
1025 831 $response = [
1026 832 'status_code' => 201,
1027 833 'success' => 1,
1028 - /* translators: %s: Action performed (created, updated, etc.) */
1029 834 'message' => sprintf( esc_html__( 'Successfully %s meeting', 'timetics' ), $action ),
1030 835 'data' => $item,
1031 836 ];
1032 837
@@ -1082,35 +887,8 @@
1082 887 'buffer_time_before_unit' => $appointment->get_buffer_time_before_unit(),
1083 888 'buffer_time_after_value' => $appointment->get_buffer_time_after_value(),
1084 889 'buffer_time_after_unit' => $appointment->get_buffer_time_after_unit(),
1085 890 ];
1086 -
1087 - // Strip webhook URLs, notifications, and staff PII for non-privileged callers — several read routes here are public.
1088 - if ( ! current_user_can( 'edit_meeting' ) ) {
1089 - unset(
1090 - $data['notifications'],
1091 - $data['fluent_hook_overwrite'],
1092 - $data['fleunt_crm_webhook'],
1093 - $data['pabbly_hook_overwrite'],
1094 - $data['pabbly_webook'],
1095 - $data['zapier_hook_overwrite'],
1096 - $data['zapier_webook'],
1097 - $data['author']
1098 - );
1099 -
1100 - if ( ! empty( $data['staff'] ) && is_array( $data['staff'] ) ) {
1101 - $data['staff'] = array_map(
1102 - function ( $staff ) {
1103 - return [
1104 - 'id' => $staff['id'] ?? 0,
1105 - 'full_name' => $staff['full_name'] ?? '',
1106 - 'image' => $staff['image'] ?? '',
1107 - ];
1108 - },
1109 - $data['staff']
1110 - );
1111 - }
1112 - }
1113 891
1114 892 return apply_filters( 'timetics_meeting_json_data', $data, $appointment );
1115 893 }
1116 894