· 6 years ago · Nov 01, 2019, 07:30 AM
1<?php
2// Exit if accessed directly.
3defined( 'ABSPATH' ) || exit;
4
5/**
6 * Google Calendar Synchronization.
7 */
8class WC_Appointments_GCal {
9
10 const TOKEN_TRANSIENT_TIME = 3500;
11
12 const DAYS_OF_WEEK = array(
13 1 => 'monday',
14 2 => 'tuesday',
15 3 => 'wednesday',
16 4 => 'thursday',
17 5 => 'friday',
18 6 => 'saturday',
19 7 => 'sunday',
20 );
21
22 /**
23 * If the service is currently is a syncing operation with google.
24 *
25 * @var bool
26 */
27 protected $syncing = false;
28
29 /**
30 * @var WC_Appointments_GCal The single instance of the class
31 */
32 protected static $_instance = null;
33
34 /**
35 * Main WC_Appointments_GCal Instance
36 */
37 public static function instance() {
38 if ( is_null( self::$_instance ) ) {
39 self::$_instance = new self();
40 }
41 return self::$_instance;
42 }
43
44 /**
45 * User ID not set by default.
46 */
47 private $user_id = null;
48
49 /**
50 * Init and hook in the integration.
51 */
52 public function __construct() {
53 // API.
54 $this->id = 'gcal';
55 $this->oauth_uri = 'https://accounts.google.com/o/oauth2/';
56 $this->calendars_uri = 'https://www.googleapis.com/calendar/v3/calendars/';
57 $this->calendars_list = 'https://www.googleapis.com/calendar/v3/users/me/calendarList';
58 $this->api_scope = 'https://www.googleapis.com/auth/calendar';
59 $this->redirect_uri = WC()->api_request_url( 'wc_appointments_oauth_redirect' );
60 $this->client_id = get_option( 'wc_appointments_gcal_client_id' );
61 $this->client_secret = get_option( 'wc_appointments_gcal_client_secret' );
62 $this->calendar_id = get_option( 'wc_appointments_gcal_calendar_id' );
63 $this->debug = get_option( 'wc_appointments_gcal_debug' );
64 $this->twoway = get_option( 'wc_appointments_gcal_twoway' );
65
66 // Oauth redirect.
67 add_action( 'woocommerce_api_wc_appointments_oauth_redirect', array( $this, 'oauth_redirect' ) );
68 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
69
70 // Run the schedule and Sync from GCal.
71 add_action( 'action_scheduler_after_process_queue', array( $this, 'sync_from_gcal' ) );
72
73 // Appointment update actions.
74 // Sync all statuses, but limit inside maybe_sync_to_gcal_from_status() function.
75 foreach ( get_wc_appointment_statuses() as $status ) {
76 add_action( 'woocommerce_appointment_' . $status, array( $this, 'sync_new_appointment' ) );
77 }
78
79 // Remove from Gcal.
80 add_action( 'woocommerce_appointment_cancelled', array( $this, 'remove_from_gcal' ) );
81
82 // Process edited appointment.
83 add_action( 'woocommerce_appointment_process_meta', array( $this, 'sync_edited_appointment' ) );
84
85 // Sync trashed/untrashed appointments.
86 add_action( 'trashed_post', array( $this, 'remove_from_gcal' ) );
87 add_action( 'untrashed_post', array( $this, 'sync_untrashed_appointment' ) );
88
89 // Sync availability to Gcal.
90 add_action( 'woocommerce_before_appointments_availability_object_save', array( $this, 'sync_availability' ) ); #'woocommerce_before_' . $object_type . '_object_save'
91 add_action( 'woocommerce_appointments_before_delete_appointment_availability', array( $this, 'delete_availability' ) );
92
93 // Active logs.
94 if ( class_exists( 'WC_Logger' ) ) {
95 $this->log = new WC_Logger();
96 } else {
97 $this->log = WC()->logger();
98 }
99 }
100
101 /**
102 * Set redirect_uri option.
103 */
104 public function set_redirect_uri( $option ) {
105 $this->redirect_uri = $option;
106 }
107
108 /**
109 * Get redirect_uri option.
110 */
111 public function get_redirect_uri() {
112 return $this->redirect_uri;
113 }
114
115 /**
116 * Set callback_uri option.
117 */
118 public function set_callback_uri( $option ) {
119 $this->callback_uri = $option;
120 }
121
122 /**
123 * Get callback_uri option.
124 */
125 public function get_callback_uri() {
126 return $this->callback_uri;
127 }
128
129 /**
130 * Set client_id option.
131 */
132 public function set_client_id( $option ) {
133 $this->client_id = $option;
134 }
135
136 /**
137 * Get client_id option.
138 */
139 public function get_client_id() {
140 return $this->client_id;
141 }
142
143 /**
144 * Set client_secret option.
145 */
146 public function set_client_secret( $option ) {
147 $this->client_secret = $option;
148 }
149
150 /**
151 * Get client_secret option.
152 */
153 public function get_client_secret() {
154 return $this->client_secret;
155 }
156
157 /**
158 * Set calendar_id option.
159 */
160 public function set_calendar_id( $option ) {
161 $this->calendar_id = $option;
162 }
163
164 /**
165 * Get calendar_id option.
166 */
167 public function get_calendar_id() {
168 return $this->calendar_id;
169 }
170
171 /**
172 * Set user_id option.
173 */
174 public function set_user_id( $option ) {
175 $this->user_id = $option;
176 $calendar_id = get_user_meta( $option, 'wc_appointments_gcal_calendar_id', true );
177 $calendar_id = $calendar_id ? $calendar_id : get_option( 'wc_appointments_gcal_calendar_id' );
178 $two_way = get_user_meta( $option, 'wc_appointments_gcal_twoway', true );
179 $two_way = $two_way ? $two_way : get_option( 'wc_appointments_gcal_twoway' );
180
181 $this->set_calendar_id( $calendar_id );
182 $this->set_twoway( $two_way );
183 }
184
185 /**
186 * Get user_id option.
187 */
188 public function get_user_id() {
189 return $this->user_id;
190 }
191
192 /**
193 * Set debug option.
194 */
195 public function set_debug( $option ) {
196 $this->debug = $option;
197 }
198
199 /**
200 * Get debug option.
201 */
202 public function get_debug() {
203 return $this->debug;
204 }
205
206 /**
207 * Set twoway option.
208 */
209 public function set_twoway( $option ) {
210 $this->twoway = $option;
211 }
212
213 /**
214 * Get twoway option.
215 */
216 public function get_twoway() {
217 return $this->twoway;
218 }
219
220 /**
221 * Get twoway option.
222 */
223 public function is_twoway_enabled() {
224 $twoway_enabled = ( 'two_way' !== $this->get_twoway() ) ? false : true;
225
226 return $twoway_enabled;
227 }
228
229 /**
230 * Display admin screen notices.
231 *
232 * @return string
233 */
234 public function admin_notices() {
235 $screen = get_current_screen();
236
237 $allowed_screens = array( 'user-edit', 'woocommerce_page_wc-settings' );
238
239 if ( in_array( $screen->id, $allowed_screens ) && isset( $_GET['wc_gcal_oauth'] ) ) {
240 if ( 'success' == $_GET['wc_gcal_oauth'] ) {
241 echo '<div class="updated fade"><p><strong>' . __( 'Google Calendar', 'woocommerce-appointments' ) . '</strong> ' . __( 'Account connected successfully!', 'woocommerce-appointments' ) . '</p></div>';
242 } else {
243 echo '<div class="error fade"><p><strong>' . __( 'Google Calendar', 'woocommerce-appointments' ) . '</strong> ' . __( 'Failed to connect to your account, please try again, if the problem persists, turn on Debug Log option and see what is happening.', 'woocommerce-appointments' ) . '</p></div>';
244 }
245 }
246
247 if ( in_array( $screen->id, $allowed_screens ) && isset( $_GET['wc_gcal_logout'] ) ) {
248 if ( 'success' == $_GET['wc_gcal_logout'] ) {
249 echo '<div class="updated fade"><p><strong>' . __( 'Google Calendar', 'woocommerce-appointments' ) . '</strong> ' . __( 'Account disconnected successfully!', 'woocommerce-appointments' ) . '</p></div>';
250 } else {
251 echo '<div class="error fade"><p><strong>' . __( 'Google Calendar', 'woocommerce-appointments' ) . '</strong> ' . __( 'Failed to disconnect to your account, please try again, if the problem persists, turn on Debug Log option and see what is happening.', 'woocommerce-appointments' ) . '</p></div>';
252 }
253 }
254 }
255
256 /**
257 * Get Access Token.
258 *
259 * @param string $code Authorization code.
260 *
261 * @return string Access token.
262 */
263 public function get_access_token( $code = '', $user_id = '' ) {
264 $user_id = $user_id ? $user_id : '';
265 $user_id = $this->get_user_id() ? $this->get_user_id() : $user_id;
266
267 // Check roles if user is shop staff.
268 if ( $user_id ) {
269 $user_meta = get_userdata( $user_id );
270 if ( isset( $user_meta->roles ) && ! in_array( 'shop_staff', (array) $user_meta->roles ) ) {
271 return;
272 }
273 }
274
275 // Get access token.
276 if ( $user_id ) {
277 $access_token = get_transient( 'wc_appointments_gcal_access_token_' . $user_id );
278 } else {
279 $access_token = get_transient( 'wc_appointments_gcal_access_token' );
280 }
281
282 // Get refresh token.
283 if ( $user_id ) {
284 $refresh_token = get_user_meta( $user_id, 'wc_appointments_gcal_refresh_token', true );
285 } else {
286 $refresh_token = get_option( 'wc_appointments_gcal_refresh_token' );
287 }
288
289 if ( ! $code && $refresh_token ) {
290 $data = array(
291 'client_id' => $this->get_client_id(),
292 'client_secret' => $this->get_client_secret(),
293 'refresh_token' => $refresh_token,
294 'grant_type' => 'refresh_token',
295 );
296
297 $params = array(
298 'body' => http_build_query( $data ),
299 'sslverify' => false,
300 'timeout' => 60,
301 'headers' => array(
302 'Content-Type' => 'application/x-www-form-urlencoded',
303 ),
304 );
305
306 $response = wp_safe_remote_post( $this->oauth_uri . 'token', $params );
307
308 if ( ! is_wp_error( $response ) && 200 == $response['response']['code'] && 'OK' == $response['response']['message'] ) {
309 $response_data = json_decode( $response['body'] );
310 $access_token = sanitize_text_field( $response_data->access_token );
311
312 // Set the transient.
313 if ( $user_id ) {
314 set_transient( 'wc_appointments_gcal_access_token_' . $user_id, $access_token, self::TOKEN_TRANSIENT_TIME );
315 if ( 'yes' === $this->get_debug() ) {
316 #$this->log->add( $this->id, 'Google API Access Token for staff #' . $user_id . ' generated successfully: ' . $access_token ); #debug
317 }
318 } else {
319 set_transient( 'wc_appointments_gcal_access_token', $access_token, self::TOKEN_TRANSIENT_TIME );
320 if ( 'yes' === $this->get_debug() ) {
321 #$this->log->add( $this->id, 'Google API Access Token generated successfully: ' . $access_token ); #debug
322 }
323 }
324
325 return $access_token;
326 } else {
327 if ( 'yes' === $this->get_debug() ) {
328 #$this->log->add( $this->id, 'Error while generating the Access Token: ' . var_export( $response['response'], true ) ); #debug
329 }
330 }
331 } elseif ( '' !== $code ) {
332 if ( 'yes' === $this->get_debug() ) {
333 #$this->log->add( $this->id, 'Renewing the Access Token...' ); #debug
334 }
335
336 $data = array(
337 'code' => $code,
338 'client_id' => $this->get_client_id(),
339 'client_secret' => $this->get_client_secret(),
340 'redirect_uri' => $this->get_redirect_uri(),
341 'grant_type' => 'authorization_code',
342 );
343
344 $params = array(
345 'body' => http_build_query( $data ),
346 'sslverify' => false,
347 'timeout' => 60,
348 'headers' => array(
349 'Content-Type' => 'application/x-www-form-urlencoded',
350 ),
351 );
352
353 $response = wp_safe_remote_post( $this->oauth_uri . 'token', $params );
354
355 if ( ! is_wp_error( $response ) && 200 == $response['response']['code'] && 'OK' == $response['response']['message'] ) {
356 $response_data = json_decode( $response['body'] );
357 $access_token = sanitize_text_field( $response_data->access_token );
358
359 // Add refresh token.
360 if ( $user_id ) {
361 update_user_meta( $user_id, 'wc_appointments_gcal_refresh_token', $response_data->refresh_token );
362 } else {
363 update_option( 'wc_appointments_gcal_refresh_token', $response_data->refresh_token );
364 }
365
366 // Set the transient.
367 if ( $user_id ) {
368 set_transient( 'wc_appointments_gcal_access_token_' . $user_id, $access_token, self::TOKEN_TRANSIENT_TIME );
369 if ( 'yes' === $this->get_debug() ) {
370 #$this->log->add( $this->id, 'Google API Access Token for staff #' . $user_id . ' renewed successfully: ' . $access_token ); #debug
371 }
372 } else {
373 set_transient( 'wc_appointments_gcal_access_token', $access_token, self::TOKEN_TRANSIENT_TIME );
374 if ( 'yes' === $this->get_debug() ) {
375 #$this->log->add( $this->id, 'Google API Access Token renewed successfully: ' . $access_token ); #debug
376 }
377 }
378
379 return $access_token;
380 } else {
381 if ( 'yes' === $this->get_debug() ) {
382 #$this->log->add( $this->id, 'Error while renewing the Access Token: ' . var_export( $response['response'], true ) ); #debug
383 }
384 }
385 }
386
387 if ( 'yes' === $this->get_debug() ) {
388 #$this->log->add( $this->id, 'Failed to retrieve and generate the Access Token. Code: ' . $code . ', User: ' . $user_id . ', Refresh token: ' . $refresh_token ); #debug
389 }
390 }
391
392 /**
393 * OAuth Logout.
394 *
395 * @return bool
396 */
397 protected function oauth_logout( $user_id = '' ) {
398 $user_id = $user_id ? $user_id : '';
399 $user_id = $this->get_user_id() ? $this->get_user_id() : $user_id;
400
401 if ( 'yes' === $this->get_debug() ) {
402 $this->log->add( $this->id, 'Disconnecting from the Google Calendar app...' ); #debug
403 }
404
405 // Get the refresh token.
406 $refresh_token = $user_id ? get_user_meta( $user_id, 'wc_appointments_gcal_refresh_token', true ) : get_option( 'wc_appointments_gcal_refresh_token' );
407
408 if ( $refresh_token ) {
409 $params = array(
410 'sslverify' => false,
411 'timeout' => 60,
412 'headers' => array(
413 'Content-Type' => 'application/x-www-form-urlencoded',
414 ),
415 );
416
417 $response = wp_remote_get( $this->oauth_uri . 'revoke?token=' . $refresh_token, $params );
418
419 if ( ! is_wp_error( $response ) && 200 == $response['response']['code'] && 'OK' == $response['response']['message'] ) {
420 // Delete tokens.
421 if ( $user_id ) {
422 delete_user_meta( $user_id, 'wc_appointments_gcal_refresh_token' );
423 delete_transient( 'wc_appointments_gcal_access_token_' . $user_id );
424 } else {
425 delete_option( 'wc_appointments_gcal_refresh_token' );
426 delete_transient( 'wc_appointments_gcal_access_token' );
427 }
428
429 if ( 'yes' === $this->get_debug() ) {
430 $this->log->add( $this->id, 'Successfully disconnected from the Google Calendar app' ); #debug
431 }
432
433 return true;
434 } else {
435 if ( 'yes' === $this->get_debug() ) {
436 $this->log->add( $this->id, 'Error while disconnecting from the Google Calendar app: ' . var_export( $response['response'], true ) ); #debug
437 }
438 }
439 }
440
441 if ( 'yes' === $this->get_debug() ) {
442 $this->log->add( $this->id, 'Failed to disconnect from the Google Calendar app' ); #debug
443 }
444
445 return;
446 }
447
448 /**
449 * Process the oauth redirect.
450 *
451 * @return void
452 */
453 public function oauth_redirect() {
454 if ( ! current_user_can( 'manage_appointments' ) ) {
455 wp_die( __( 'Permission denied!', 'woocommerce-appointments' ) );
456 }
457
458 // User ID passed.
459 if ( isset( $_GET['state'] ) ) {
460
461 $user_id = absint( $_GET['state'] );
462 $admin_url = admin_url( 'user-edit.php' );
463 $redirect_args = array(
464 'user_id' => $_GET['state'],
465 );
466
467 } else {
468
469 $user_id = '';
470 $admin_url = admin_url( 'admin.php' );
471 $redirect_args = array(
472 'page' => 'wc-settings',
473 'tab' => 'appointments',
474 'section' => $this->id,
475 );
476
477 }
478
479 // OAuth.
480 if ( isset( $_GET['code'] ) ) {
481 $code = sanitize_text_field( $_GET['code'] );
482 $access_token = $this->get_access_token( $code, $user_id );
483
484 if ( ! $access_token ) {
485 $redirect_args['wc_gcal_oauth'] = 'fail';
486
487 wp_safe_redirect( add_query_arg( $redirect_args, $admin_url ), 301 );
488 exit;
489 } else {
490 $redirect_args['wc_gcal_oauth'] = 'success';
491
492 wp_safe_redirect( add_query_arg( $redirect_args, $admin_url ), 301 );
493 exit;
494 }
495 }
496
497 // Error.
498 if ( isset( $_GET['error'] ) ) {
499 $redirect_args['wc_gcal_oauth'] = 'fail';
500
501 wp_safe_redirect( add_query_arg( $redirect_args, $admin_url ), 301 );
502 exit;
503 }
504
505 // Logout.
506 if ( isset( $_GET['logout'] ) ) {
507 $logout = $this->oauth_logout( $user_id );
508 $redirect_args['wc_gcal_logout'] = ( isset( $logout ) && $logout ) ? 'success' : 'fail';
509
510 wp_safe_redirect( add_query_arg( $redirect_args, $admin_url ), 301 );
511 exit;
512 }
513
514 wp_die( __( 'Invalid request!', 'woocommerce-appointments' ) );
515 }
516
517 /**
518 * Get user calendars.
519 *
520 * @return array Calendar list
521 */
522 public function get_calendars() {
523 // Get all Google Calendars.
524 $google_calendars = array();
525
526 // Check if Authorized.
527 $access_token = $this->get_access_token();
528 if ( ! $access_token ) {
529 return;
530 }
531
532 // Connection params.
533 $params = array(
534 'method' => 'GET',
535 'sslverify' => false,
536 'timeout' => 60,
537 'headers' => array(
538 'Content-Type' => 'application/json',
539 'Authorization' => 'Bearer ' . $access_token,
540 ),
541 );
542
543 $response = wp_safe_remote_post( $this->calendars_list, $params );
544
545 if ( ! is_wp_error( $response ) && 200 == $response['response']['code'] && 'OK' == $response['response']['message'] ) {
546 // Get response data.
547 $response_data = json_decode( $response['body'], true );
548
549 // List calendars.
550 if ( is_array( $response_data['items'] ) && ! empty( $response_data['items'] ) ) {
551 foreach ( $response_data['items'] as $data ) {
552 $google_calendars[ $data['id'] ] = $data['summary'];
553 }
554 }
555 }
556
557 return $google_calendars;
558 }
559
560 /**
561 * Check if Google Calendar settings are supplied.
562 *
563 * @return bool True is calendar is set, false otherwise.
564 */
565 public function is_calendar_set() {
566 $client_id = $this->get_client_id();
567 $client_secret = $this->get_client_secret();
568 $calendar_id = $this->get_calendar_id();
569
570 return ! empty( $client_id ) && ! empty( $client_secret ) && ! empty( $calendar_id );
571 }
572
573 /**
574 * Makes an http request to the Google Calendar API.
575 *
576 * @param string $api_url API Url to make the request against
577 * @param array $params Array of parameters that will be used when making the request
578 * @version 3.5.6
579 * @since 3.5.6
580 * @return object Response object from the request
581 */
582 protected function make_gcal_request( $api_url, $params = array(), $staff_id = '' ) {
583 if ( ! isset( $api_url ) ) {
584 return;
585 }
586
587 // Check if Authorized.
588 $access_token = $this->get_access_token( '', $staff_id );
589 if ( ! $access_token ) {
590 return;
591 }
592
593 // Connection params.
594 $params['method'] = isset( $params['method'] ) ? strtoupper( $params['method'] ) : 'GET';
595 $params['sslverify'] = false;
596 $params['timeout'] = 60;
597 $params['headers'] = array(
598 'Content-Type' => 'application/json',
599 'Authorization' => 'Bearer ' . $access_token,
600 );
601
602 if ( isset( $params['querystring'] ) && is_array( $params['querystring'] ) ) {
603 $api_url .= '?' . http_build_query( wp_json_encode( $params['querystring'], JSON_UNESCAPED_SLASHES ) );
604 }
605
606 if ( in_array( $params['method'], array( 'GET', 'DELETE' ) ) ) {
607 unset( $params['body'] );
608 }
609
610 // Filter the gCal request.
611 $params = apply_filters( 'woocommerce_appointments_gcal_sync_parameters', $params, $api_url, $staff_id );
612
613 $response = wp_safe_remote_request( $api_url, $params );
614
615 // 200 = ok
616 // 204 = deleted
617 if ( ! is_wp_error( $response ) && 'OK' == $response['response']['message']
618 && in_array( $response['response']['code'], array( 200, 204 ) )
619 ) {
620 if ( 'yes' === $this->get_debug() ) {
621 #$this->log->add( $this->id, 'Google calendar request successful!' );
622 }
623 } elseif ( 410 === $response['response']['code'] ) {
624 $this->log->add( $this->id, 'Attempting to delete event that does not exist any more' ); #debug
625 } elseif ( 'yes' === $this->get_debug() ) {
626 $this->log->add( $this->id, 'Error while making Google Calendar request for ' . $api_url . ': ' . var_export( $response['response'], true ) ); #debug
627 }
628
629 return $response;
630 }
631
632 /**
633 * Is edited from post.php's meta box.
634 *
635 * @return bool
636 */
637 public function is_edited_from_meta_box() {
638 return (
639 ! empty( $_POST['wc_appointments_details_meta_box_nonce'] )
640 &&
641 wp_verify_nonce( $_POST['wc_appointments_details_meta_box_nonce'], 'wc_appointments_details_meta_box' )
642 );
643 }
644
645 /**
646 * Sync new Appointment with GCal.
647 *
648 * @param int $appointment_id Appointment ID
649 * @return void
650 */
651 public function sync_new_appointment( $appointment_id ) {
652 if ( $this->is_edited_from_meta_box() ) {
653 return;
654 }
655
656 $this->maybe_sync_to_gcal_from_status( $appointment_id );
657 }
658
659 /**
660 * Sync Appointment with GCal when appointment is edited.
661 *
662 * @param int $appointment_id Appointment ID
663 * @return void
664 */
665 public function sync_edited_appointment( $appointment_id ) {
666 if ( ! $this->is_edited_from_meta_box() ) {
667 return;
668 }
669
670 $this->maybe_sync_to_gcal_from_status( $appointment_id );
671 }
672
673 /**
674 * Sync Appointment with GCal when appointment is untrashed.
675 *
676 * @param int $appointment_id Appointment ID
677 *
678 * @return void
679 */
680 public function sync_untrashed_appointment( $appointment_id ) {
681 $this->maybe_sync_to_gcal_from_status( $appointment_id );
682 }
683
684 /**
685 * Maybe remove / sync appointment based on appointment status.
686 *
687 * @param int $appointment_id Appointment ID
688 * @return void
689 */
690 public function maybe_sync_to_gcal_from_status( $appointment_id ) {
691 global $wpdb;
692
693 // Check if Authorized.
694 $access_token = $this->get_access_token();
695 if ( ! $access_token ) {
696 return;
697 }
698
699 $status = $wpdb->get_var( $wpdb->prepare( "SELECT post_status FROM $wpdb->posts WHERE post_type = 'wc_appointment' AND ID = %d", $appointment_id ) );
700
701 if ( 'cancelled' === $status ) {
702 $this->remove_from_gcal( $appointment_id );
703 } elseif ( in_array( $status, apply_filters( 'woocommerce_appointments_gcal_sync_statuses', array( 'confirmed', 'paid', 'complete' ) ) ) ) {
704 $this->sync_to_gcal( $appointment_id );
705 } elseif ( 'unpaid' === $status ) { #Sync Cash on Delivery appointments.
706 $order_id = WC_Appointment_Data_Store::get_appointment_order_id( $appointment_id );
707 $order = wc_get_order( $order_id );
708 if ( is_a( $order, 'WC_Order' ) ) {
709 if ( 'cod' === $order->get_payment_method() ) {
710 $this->sync_to_gcal( $appointment_id );
711 }
712 }
713 }
714 }
715
716 /**
717 * Sync an event resource with Google Calendar.
718 * https://developers.google.com/google-apps/calendar/v3/reference/events
719 *
720 * @param int $appointment_id Appointment ID
721 * @param array $params Set of parameters to be passed to the http request
722 * @param array $data Optional set of data for writeable syncs
723 * @since 3.5.6
724 * @version 3.5.6
725 * @return object|boolean Parsed JSON data from the http request or false if error
726 */
727 public function sync_event_resource( $appointment_id = -1, $params = array(), $resource_params = array(), $data = array() ) {
728 if ( $appointment_id < 0 ) {
729 return;
730 }
731
732 $appointment = get_wc_appointment( $appointment_id );
733 $event_id = $resource_params['event_id'];
734 $staff_id = $resource_params['staff_id'];
735 $calendar_id = $resource_params['calendar_id'];
736 $api_url_ok = $this->calendars_uri . $calendar_id . '/events' . ( ( $event_id ) ? '/' . $event_id : '' );
737 $api_url = $api_url_ok.'/?sendUpdates=all';
738 $json_data = false;
739
740
741
742 if ( isset( $params['method'] ) && 'GET' !== $params['method'] ) {
743 $params['body'] = wp_json_encode( apply_filters( 'woocommerce_appointments_gcal_sync', $data, $appointment ) );
744 }
745
746 try {
747
748 $response = $this->make_gcal_request( $api_url, $params, $staff_id );
749 $json_data = json_decode( $response['body'], true );
750
751
752 $debug_export = var_export($appointment->get_staff_ids(), true);
753
754
755 if ( 'yes' === $this->get_debug() ) {
756 $this->log->add( $this->id, 'Synced appointment #' . $appointment->get_id() . ' with Google Calendar: ' . $calendar_id . ' - ' . $debug_export ); #debug
757 }
758
759 } catch ( Exception $e ) {
760 $json_data = false;
761
762 if ( 'yes' === $this->get_debug() ) {
763 $this->log->add( $this->id, 'Error while getting data for ' . $api_url . ': ' . print_r( $response, true ) ); #debug
764 }
765 }
766
767 return $json_data;
768
769 }
770
771 /**
772 * Sync Appointment to GCal
773 *
774 * @param int $appointment_id Appointment ID
775 * @return void
776 */
777 public function sync_to_gcal( $appointment_id, $appointment_staff_id = false, $staff_calendar_id = false ) {
778 if ( 'wc_appointment' !== get_post_type( $appointment_id ) ) {
779 return;
780 }
781
782 /**
783 * woocommerce_appointments_sync_to_gcal_start hook
784 */
785 do_action( 'woocommerce_appointments_sync_to_gcal_start', $appointment_id, $appointment_staff_id );
786
787 $appointment = get_wc_appointment( $appointment_id );
788 $staff_ids = $appointment->get_staff_ids();
789 if ( $appointment_staff_id ) {
790 $staff_event_ids = $appointment->get_google_calendar_staff_event_ids();
791 $event_id = isset( $staff_event_ids[ $appointment_staff_id ] ) ? $staff_event_ids[ $appointment_staff_id ] : '';
792 } else {
793 $event_id = $appointment->get_google_calendar_event_id();
794 }
795 $product = $appointment->get_product();
796 $product_id = $appointment->get_product_id();
797 $order = $appointment->get_order();
798 $customer = $appointment->get_customer();
799 $timezone = wc_appointment_get_timezone_string();
800 /* translators: 1: appointment ID */
801 $summary = sprintf( __( 'Appointment #%s', 'woocommerce-appointments' ), $appointment_id ) . ( $product ? ' - ' . html_entity_decode( $product->get_title() ) : '' );
802 $description = '';
803 $description_does_exist = false;
804 $description_has_been_edited = false;
805
806 // Add customer name.
807 if ( $customer && $customer->name ) {
808 $description .= sprintf( '%s: %s', __( 'Customer', 'woocommerce-appointments' ), $customer->name ) . PHP_EOL;
809 } else {
810 $description .= sprintf( '%s: %s', __( 'Customer', 'woocommerce-appointments' ), __( 'Guest', 'woocommerce-appointments' ) ) . PHP_EOL;
811 }
812
813 // Product name.
814 if ( is_object( $product ) ) {
815 $description .= sprintf( '%s: %s', __( 'Product', 'woocommerce-appointments' ), $product->get_title() ) . PHP_EOL;
816 }
817
818 // Appointment data.
819 $appointment_data = array(
820 __( 'Appointment ID', 'woocommerce-appointments' ) => $appointment_id,
821 __( 'When', 'woocommerce-appointments' ) => $appointment->get_start_date(),
822 __( 'Duration', 'woocommerce-appointments' ) => $appointment->get_duration(),
823 __( 'Providers', 'woocommerce-appointments' ) => $appointment->get_staff_members( true ),
824 );
825
826 foreach ( $appointment_data as $key => $value ) {
827 if ( empty( $value ) ) {
828 continue;
829 }
830
831 $description .= sprintf( '%1$s: %2$s', rawurldecode( html_entity_decode( $key ) ), rawurldecode( html_entity_decode( $value ) ) ) . PHP_EOL;
832 }
833
834 // Addons and other order items.
835 if ( is_a( $order, 'WC_Order' ) ) {
836 foreach ( $order->get_items() as $order_item_id => $order_item ) {
837 if ( $order_item_id !== WC_Appointment_Data_Store::get_appointment_order_item_id( $appointment_id ) ) {
838 continue;
839 }
840 foreach ( $order_item->get_meta_data() as $order_meta_data ) {
841 $the_meta_data = $order_meta_data->get_data();
842 if ( is_serialized( $the_meta_data['value'] ) ) {
843 continue;
844 }
845 if ( is_array( $the_meta_data['key'] ) ) {
846 continue;
847 }
848 if ( is_array( $the_meta_data['value'] ) && ! empty( $the_meta_data['value'] ) ) {
849 $onedimensional_arr = [];
850
851 foreach ( $the_meta_data['value'] as $meta_data_value ) {
852 // Skip deep arrays.
853 if ( is_array( $meta_data_value ) ) {
854 continue;
855 }
856 $onedimensional_arr[] = $meta_data_value;
857 }
858
859 $the_meta_data['value'] = implode( ', ', $onedimensional_arr );
860 }
861 // Fix for WooCommerce TM Extra Product Options plugin.
862 if ( '_tmcartepo_data' === $the_meta_data['key'] || '_tm_epo_product_original_price' === $the_meta_data['key'] || '_tm_epo' === $the_meta_data['key'] ) {
863 continue;
864 }
865
866 $description .= sprintf( '%s: %s', rawurldecode( html_entity_decode( $the_meta_data['key'] ) ), rawurldecode( html_entity_decode( $the_meta_data['value'] ) ) ) . PHP_EOL;
867 }
868 }
869 }
870
871 // Resource params.
872 $resource_params = array(
873 'event_id' => $event_id,
874 'staff_id' => $appointment_staff_id,
875 'calendar_id' => ( $staff_calendar_id ? $staff_calendar_id : $this->get_calendar_id() ),
876 );
877
878 // Update event.
879 if ( $event_id ) {
880 $response_data = $this->sync_event_resource(
881 $appointment_id,
882 array(
883 'method' => 'GET',
884 'querystring' => array(
885 'fields' => 'summary, description',
886 ),
887 ),
888 $resource_params
889 );
890
891 $description_does_exist = isset( $response_data['description'] ) && ( '' !== trim( $response_data['description'] ) );
892 $description_has_been_edited = isset( $response_data['description'] ) && $response_data['description'] !== $description;
893
894 // If the user edited the description on the Google Calendar side we want to keep that data intact.
895 if ( $description_does_exist && $description_has_been_edited ) {
896 $description = $response_data['description'];
897 }
898
899 $summary_does_exist = isset( $response_data['summary'] ) && ( '' !== trim( $response_data['summary'] ) );
900 $summary_has_been_edited = isset( $response_data['summary'] ) && $response_data['summary'] !== $summary;
901
902 // If the user edited the summary (event title) on the Google Calendar side we want to keep that data intact.
903 if ( $summary_does_exist && $summary_has_been_edited ) {
904 $summary = $response_data['summary'];
905 }
906 }
907
908 // Set the event data.
909 $data = array(
910 'summary' => wp_kses_post( $summary ),
911 'description' => wp_kses_post( $description ),
912 'location' => $customer->full_name,
913 'attendees' => array(
914 array(
915 'email' => $customer->email,
916 'displayName' => $customer->full_name
917 ),
918 ),
919 );
920
921 // Pass appointment ID.
922 $data['extendedProperties'] = array(
923 'shared' => array(
924 'appointment_id' => $appointment_id,
925 ),
926 );
927
928 // Set the event start and end dates.
929 if ( $appointment->is_all_day() ) {
930 $data['end'] = array(
931 'date' => date( 'Y-m-d', ( $appointment->get_end() + 1440 ) ),
932 );
933
934 $data['start'] = array(
935 'date' => date( 'Y-m-d', $appointment->get_start() ),
936 );
937 } else {
938 $data['end'] = array(
939 'dateTime' => date( 'Y-m-d\TH:i:s', $appointment->get_end() ),
940 'timeZone' => $timezone,
941 );
942
943 $data['start'] = array(
944 'dateTime' => date( 'Y-m-d\TH:i:s', $appointment->get_start() ),
945 'timeZone' => $timezone,
946 );
947 }
948
949 $response_data = $this->sync_event_resource(
950 $appointment_id,
951 array(
952 'method' => $event_id ? 'PUT' : 'POST',
953 ),
954 $resource_params,
955 $data
956 );
957
958 // Save event ID only when available.
959 if ( isset( $response_data['id'] ) ) {
960 if ( $appointment_staff_id ) {
961 $appointment->set_google_calendar_staff_event_ids( array( $appointment_staff_id => $response_data['id'] ) );
962 } else {
963 $appointment->set_google_calendar_event_id( wc_clean( $response_data['id'] ) );
964 }
965 }
966
967 // Save appointment also calls $appointment->status_transition() in which
968 // infinite loop could happens.
969 $appointment->skip_status_transition_events();
970 $appointment->save();
971
972 // Sync for each staff.
973 // Only when $appointment_staff_id is false,
974 // so it does not go into inifinite loop.
975 if ( $staff_ids && ! $appointment_staff_id ) {
976 $count_staff = 0;
977 foreach ( $staff_ids as $staff_id ) {
978 $calendar_id = get_user_meta( $staff_id, 'wc_appointments_gcal_calendar_id', true );
979 $staff_calendar_id = $calendar_id ? $calendar_id : '';
980 // Staff must have calendar ID set.
981 if ( $staff_calendar_id ) {
982 $this->sync_to_gcal( $appointment_id, $staff_id, $staff_calendar_id );
983 $count_staff++;
984 }
985 }
986
987 /*
988 // Don't delete event ID's from removed staff
989 // in case you add it back in future.
990 if ( ! $count_staff ) {
991 $appointment->set_google_calendar_staff_event_ids('');
992 $appointment->save();
993 }
994 */
995 }
996 }
997
998 /**
999 * Remove/cancel the appointment in GCal
1000 *
1001 * @param int $appointment_id Appointment ID
1002 * @return void
1003 */
1004 public function remove_from_gcal( $appointment_id, $appointment_staff_id = false, $staff_calendar_id = false ) {
1005 $appointment = get_wc_appointment( $appointment_id );
1006 if ( ! $appointment ) {
1007 return;
1008 }
1009 $staff_ids = $appointment->get_staff_ids();
1010
1011 if ( $appointment_staff_id ) {
1012 $staff_event_ids = $appointment->get_google_calendar_staff_event_ids();
1013 $event_id = isset( $staff_event_ids[ $appointment_staff_id ] ) ? $staff_event_ids[ $appointment_staff_id ] : '';
1014 } else {
1015 $event_id = $appointment->get_google_calendar_event_id();
1016 }
1017
1018 // Check if Authorized.
1019 $access_token = $this->get_access_token( '', $appointment_staff_id );
1020 if ( ! $access_token ) {
1021 return;
1022 }
1023
1024 // Calendar ID.
1025 $calendar_id = $staff_calendar_id ? $staff_calendar_id : $this->get_calendar_id();
1026
1027 // Stop here if calendar is not set.
1028 if ( ! $calendar_id ) {
1029 return;
1030 }
1031
1032 // Remove event.
1033 if ( $event_id ) {
1034 $api_url = $this->calendars_uri . $calendar_id . '/events/' . $event_id;
1035
1036 // Connection params.
1037 $params = array(
1038 'method' => 'DELETE',
1039 'sslverify' => false,
1040 'timeout' => 60,
1041 'headers' => array(
1042 'Content-Type' => 'application/json',
1043 'Authorization' => 'Bearer ' . $access_token,
1044 ),
1045 );
1046
1047 if ( 'yes' === $this->get_debug() ) {
1048 $this->log->add( $this->id, 'Removing appointment #' . $appointment_id . ' from Google Calendar: ' . $calendar_id );
1049 }
1050
1051 $response = wp_safe_remote_post( $api_url, $params );
1052
1053 if ( ! is_wp_error( $response ) && 204 == $response['response']['code'] ) {
1054 if ( 'yes' === $this->get_debug() ) {
1055 #$this->log->add( $this->id, 'Event #' . $event_id . ' removed successfully!' );
1056 }
1057 } else {
1058 if ( 'yes' === $this->get_debug() ) {
1059 $this->log->add( $this->id, 'Error while removing event #' . $event_id . ': from Google Calendar: ' . $calendar_id . ' : ' . var_export( $response['response'], true ) );
1060 }
1061 }
1062
1063 // Sync for each staff.
1064 // Only when $appointment_staff_id is false,
1065 // so it does not go into inifinite loop.
1066 if ( $staff_ids && ! $appointment_staff_id ) {
1067 $count_staff = 0;
1068 foreach ( $staff_ids as $staff_id ) {
1069 $calendar_id = get_user_meta( $staff_id, 'wc_appointments_gcal_calendar_id', true );
1070 $staff_calendar_id = $calendar_id ? $calendar_id : '';
1071 // Staff must have calendar ID set.
1072 if ( $staff_calendar_id ) {
1073 $this->remove_from_gcal( $appointment_id, $staff_id, $staff_calendar_id );
1074 $count_staff++;
1075 }
1076 }
1077 }
1078 }
1079 }
1080
1081 public function get_synced_staff_ids() {
1082 // Get all users set as staff.
1083 $all_staff = get_users(
1084 array(
1085 'role' => 'shop_staff',
1086 'orderby' => 'nicename',
1087 'order' => 'asc',
1088 'fields' => array( 'ID' ),
1089 )
1090 );
1091
1092 if ( $all_staff ) {
1093 $synced_ids = array();
1094 foreach ( $all_staff as $staff_id ) {
1095 $two_way = get_user_meta( $staff_id->ID, 'wc_appointments_gcal_twoway', true );
1096 $calendar_id = get_user_meta( $staff_id->ID, 'wc_appointments_gcal_calendar_id', true );
1097
1098 if ( 'two_way' === $two_way && $calendar_id ) {
1099 $synced_ids[] = absint( $staff_id->ID );
1100 }
1101 }
1102
1103 // Array of staff with sync enabled.
1104 if ( ! empty( $synced_ids ) ) {
1105 return $synced_ids;
1106 }
1107 }
1108
1109 return;
1110 }
1111
1112 public function get_sync_token() {
1113 if ( $this->get_user_id() ) {
1114 $sync_token = rawurlencode( get_transient( 'wc_appointments_gcal_sync_token' . $this->get_user_id() ) ); #get
1115 } else {
1116 $sync_token = rawurlencode( get_transient( 'wc_appointments_gcal_sync_token' ) ); #get
1117 }
1118
1119 return $sync_token;
1120 }
1121
1122 public function set_sync_token( $sync_token = 0 ) {
1123 if ( $this->get_user_id() ) {
1124 if ( $sync_token ) {
1125 set_transient( 'wc_appointments_gcal_sync_token' . $this->get_user_id(), $sync_token, self::TOKEN_TRANSIENT_TIME ); #update
1126 } else {
1127 delete_transient( 'wc_appointments_gcal_sync_token' . $this->get_user_id() ); #delete
1128 }
1129 } else {
1130 if ( $sync_token ) {
1131 set_transient( 'wc_appointments_gcal_sync_token', $sync_token, self::TOKEN_TRANSIENT_TIME ); #update
1132 } else {
1133 delete_transient( 'wc_appointments_gcal_sync_token' ); #delete
1134 }
1135 }
1136 }
1137
1138 /**
1139 * Sync back events from GCal.
1140 *
1141 * @return void
1142 */
1143 public function sync_from_gcal( $user_id = '' ) {
1144 // Get all staff with sync enabled.
1145 $synced_staff = $this->get_synced_staff_ids();
1146 if ( $synced_staff && ! $user_id ) {
1147 foreach ( $synced_staff as $synced_staff_id ) {
1148 $this->sync_from_gcal( $synced_staff_id );
1149 }
1150 }
1151
1152 // Set user id, calendar and 2-way sync.
1153 if ( $user_id ) {
1154 $this->set_user_id( $user_id );
1155 } else {
1156 $this->set_user_id( 0 ); #reset to global calendar sync.
1157 }
1158
1159 #error_log( 'works' );
1160
1161 // Two way sync not enabled.
1162 if ( ! $this->is_twoway_enabled() ) {
1163 return;
1164 }
1165
1166 // Check if Authorized and if calendar is set.
1167 $access_token = $this->get_access_token();
1168 $is_calendar_set = $this->is_calendar_set();
1169 if ( ! $access_token || ! $is_calendar_set ) {
1170 return;
1171 }
1172
1173 // Connection params.
1174 $params = array(
1175 'method' => 'GET',
1176 'sslverify' => false,
1177 'timeout' => 60,
1178 'headers' => array(
1179 'Content-Type' => 'application/json',
1180 'Authorization' => 'Bearer ' . $access_token,
1181 ),
1182 );
1183
1184 // Don't sync events older than now.
1185 $timeMin = new DateTime();
1186 $timeMin->setTimezone( new DateTimeZone( wc_appointment_get_timezone_string() ) );
1187 $timeMin = $timeMin->format( \DateTime::RFC3339 );
1188 $timeMin = rawurlencode( $timeMin );
1189
1190 // Don't sync events more than 1 year in future.
1191 $timeMax = new DateTime();
1192 $timeMax->setTimezone( new DateTimeZone( wc_appointment_get_timezone_string() ) );
1193 $timeMax->modify( '+1 year' );
1194 $timeMax = $timeMax->format( \DateTime::RFC3339 );
1195 $timeMax = rawurlencode( $timeMax );
1196
1197 // Get sync token.
1198 $sync_token = $this->get_sync_token();
1199 #$sync_token = false;
1200
1201 // maxResults 1000, 250 by default.
1202 if ( $this->get_sync_token() ) { #updated events only
1203 $response = wp_safe_remote_post( $this->calendars_uri . $this->get_calendar_id() . '/events' . "?singleEvents=false&showDeleted=true&syncToken=$sync_token", $params );
1204 } else { #full sync
1205 $response = wp_safe_remote_post( $this->calendars_uri . $this->get_calendar_id() . '/events' . "?singleEvents=false&showDeleted=true&maxResults=1000&timeMin=$timeMin&timeMax=$timeMax", $params );
1206 }
1207
1208 // If the syncToken expires, the server will respond with a 410 GONE response code.
1209 // Perform a full synchronization without any syncToken.
1210 if ( ! is_wp_error( $response ) && 410 == $response['response']['code'] ) {
1211 // Delete sync token.
1212 $this->set_sync_token( 0 );
1213
1214 // Perform a full synchronization without any syncToken.
1215 $response = wp_safe_remote_post( $this->calendars_uri . $this->get_calendar_id() . '/events' . "?singleEvents=false&showDeleted=true&maxResults=1000&timeMin=$timeMin&timeMax=$timeMax", $params );
1216 }
1217
1218 // Stop here when error spotted.
1219 if ( is_wp_error( $response ) ) {
1220 if ( 'yes' === $this->get_debug() ) {
1221 $this->log->add( $this->id, 'Error while performing sync from Google Calendar: ' . $this->get_calendar_id() . ': ' . var_export( $response['response'], true ) );
1222 }
1223 return;
1224 }
1225
1226 // Fetch the events.
1227 $this->gcal_fetch_events( $response );
1228 }
1229
1230 /**
1231 * Fetch the events and generate gcal availability.
1232 *
1233 * @param array $global_availability Availability rules.
1234 * @return void
1235 */
1236 public function gcal_fetch_events( $response ) {
1237 // Stop here if no $response.
1238 if ( ! $response ) {
1239 return;
1240 }
1241
1242 $this->syncing = true;
1243
1244 // Get gcals availability rules.
1245 $gcal_availability_rules = $this->gcal_availability_rules( $response );
1246
1247 // Make sure $gcal_availability_rules is array or object so count() works.
1248 $gcal_availability_rules = is_array( $gcal_availability_rules ) || is_object( $gcal_availability_rules ) ? $gcal_availability_rules : array();
1249
1250 // Last synced variables.
1251 // 0: current time in timestamp.
1252 // 1: number of events synced.
1253 $last_synced[] = absint( current_time( 'timestamp' ) );
1254 $last_synced[] = absint( count( $gcal_availability_rules ) );
1255
1256 // Save gcal availability.
1257 if ( $this->get_user_id() ) {
1258 update_user_meta( $this->get_user_id(), 'wc_appointments_gcal_availability_last_synced', $last_synced );
1259 } else {
1260 update_option( 'wc_appointments_gcal_availability_last_synced', $last_synced );
1261 }
1262
1263 $this->syncing = false;
1264 }
1265
1266 /**
1267 * Generate availability rules from GCal.
1268 *
1269 * @param array $response
1270 * @return void
1271 */
1272 public function gcal_availability_rules( $response ) {
1273 global $wpdb;
1274
1275 // Response error.
1276 if ( is_wp_error( $response ) || 200 !== $response['response']['code'] || 'OK' !== strtoupper( $response['response']['message'] ) ) {
1277 if ( 'yes' === $this->get_debug() ) {
1278 $this->log->add( $this->id, 'Error while performing sync from Google Calendar: ' . $this->get_calendar_id() . ': ' . var_export( $response['response'], true ) );
1279 }
1280 return;
1281 }
1282
1283 // Hook: woocommerce_appointments_sync_from_gcal_start
1284 do_action( 'woocommerce_appointments_sync_from_gcal_start', $response );
1285
1286 // Get site TimeZone.
1287 $wp_appointments_timezone = wc_appointment_get_timezone_string();
1288
1289 // Get response data.
1290 $response_data = json_decode( $response['body'], true );
1291
1292 // No events.
1293 if ( empty( $response_data['items'] ) || ! is_array( $response_data['items'] ) ) {
1294 return;
1295 }
1296
1297 // Set next sync token.
1298 $sync_token = isset( $response_data['nextSyncToken'] ) ? $response_data['nextSyncToken'] : '';
1299 if ( $sync_token ) {
1300 $this->set_sync_token( $sync_token );
1301 }
1302
1303 // Set event ids for counting later.
1304 $gcal_count = array();
1305
1306 /**
1307 * Availability Data store instance.
1308 *
1309 * @var WC_Appointments_Availability_Data_Store $availability_data_store
1310 */
1311 $availability_data_store = WC_Data_Store::load( WC_Appointments_Availability::DATA_STORE );
1312
1313 #update_option( 'xxx3', $response_data );
1314
1315 // Debug.
1316 if ( 'yes' === $this->get_debug() ) {
1317 if ( $this->get_user_id() ) {
1318 #$this->log->add( $this->id, 'List events from Google for staff #' . $this->get_user_id() . ':' . var_export( $response_data, true ) );
1319 } else {
1320 #$this->log->add( $this->id, 'List events from Google: ' . var_export( $response_data, true ) );
1321 }
1322 }
1323
1324 // Assemble events
1325 foreach ( $response_data['items'] as $event ) {
1326 // Check if all day event.
1327 // value = DATE for all day, otherwise time included.
1328 $all_day = isset( $event['start']['date'] ) && isset( $event['end']['date'] ) ? true : false;
1329
1330 if ( $all_day ) {
1331 // Get Start and end date information
1332 $dtstart = new DateTime( $event['start']['date'] );
1333 $dtend = new DateTime( $event['end']['date'] );
1334 $dtend->modify( '-1 second' ); #reduce 1 sec from end date.
1335 } else {
1336 // Get Start and end datetime information
1337 $dtstart = new DateTime( $event['start']['dateTime'] );
1338 $dtstart->setTimezone( new DateTimeZone( $wp_appointments_timezone ) );
1339 $dtend = new DateTime( $event['end']['dateTime'] );
1340 $dtend->setTimezone( new DateTimeZone( $wp_appointments_timezone ) );
1341 }
1342
1343 // Load all synced availabilities.
1344 $availabilities = $availability_data_store->get_all(
1345 array(
1346 array(
1347 'key' => 'event_id',
1348 'compare' => '=',
1349 'value' => $event['id'],
1350 ),
1351 )
1352 );
1353
1354 // No availabilities, check if an appointment matches the event.
1355 if ( empty( $availabilities ) ) {
1356
1357 // Debug.
1358 if ( 'yes' === $this->get_debug() ) {
1359 if ( $this->get_user_id() ) {
1360 #$this->log->add( $this->id, 'Availabilities for event #' . $event['id'] . ' for staff #' . $this->get_user_id() . ':' . var_export( $availabilities, true ) );
1361 } else {
1362 #$this->log->add( $this->id, 'Availabilities for event #' . $event['id'] . ': ' . var_export( $availabilities, true ) );
1363 }
1364 }
1365
1366 // Check if appointment ID is save in extendedProperties.
1367 $appointment_eid = 0;
1368 if ( isset( $event['extendedProperties']['shared']['appointment_id'] ) ) {
1369 $appointment_eid = absint( $event['extendedProperties']['shared']['appointment_id'] );
1370 $appointment_eid = is_string( get_post_status( $appointment_eid ) ) ? $appointment_eid : 0; #check if post exists.
1371 }
1372
1373 // Check if event is synced to any appointments.
1374 // @TODO eventually remove and only use extendedProperties.
1375 $args = array(
1376 'meta_query' => array(
1377 'relation' => 'OR',
1378 array(
1379 'key' => '_wc_appointments_gcal_event_id',
1380 'value' => $event['id'],
1381 ),
1382 array(
1383 'key' => '_wc_appointments_gcal_staff_event_ids',
1384 'value' => $event['id'],
1385 'compare' => 'LIKE',
1386 ),
1387 ),
1388 'no_found_rows' => true,
1389 'update_post_meta_cache' => false,
1390 'post_type' => 'wc_appointment',
1391 'posts_per_page' => '1',
1392 );
1393
1394 $get_appointments_uids = new WP_Query();
1395 $appointment_qids = $get_appointments_uids->query( $args );
1396 $appointment_qid = isset( $appointment_qids[0]->ID ) ? absint( $appointment_qids[0]->ID ) : '';
1397
1398 // Either appointment ID from extendedProperties or from saved appointments.
1399 $appointment_uid = $appointment_eid ? $appointment_eid : $appointment_qid;
1400
1401 if ( ! empty( $appointment_uid ) ) {
1402 // When event is deleted inside GCal set appointment status to cancelled and go to next event.
1403 if ( isset( $event['status'] ) && 'CANCELLED' === strtoupper( $event['status'] ) ) {
1404 // Get appointment object.
1405 $appointment = get_wc_appointment( $appointment_uid );
1406
1407 // Don't cancel trashed appointment.
1408 if ( 'trash' === $appointment->get_status() ) {
1409 continue;
1410 }
1411
1412 // Update appointment status to cancelled.
1413 $appointment->update_status( 'cancelled' );
1414 $appointment->save();
1415
1416 // Debug.
1417 if ( 'yes' === $this->get_debug() ) {
1418 if ( $this->get_user_id() ) {
1419 $this->log->add( $this->id, 'Successfully cancelled appointment #' . $appointment_uid . ' from Google Calendar event #' . $event['id'] . ' for staff #' . $this->get_user_id() );
1420 } else {
1421 $this->log->add( $this->id, 'Successfully cancelled appointment #' . $appointment_uid . ' from Google Calendar event #' . $event['id'] );
1422 }
1423 }
1424 // Update appointment data.
1425 } else {
1426 // Get appointment object.
1427 $appointment = get_wc_appointment( $appointment_uid );
1428
1429 // Skip to next event if appointment data is the same.
1430 if (
1431 absint( date( 'YmdHis', $appointment->get_start() ) ) === absint( $dtstart->format( 'YmdHis' ) ) &&
1432 absint( date( 'YmdHis', $appointment->get_end() ) ) === absint( $dtend->format( 'YmdHis' ) ) &&
1433 $appointment->get_google_calendar_event_id() === $event['id']
1434 ) {
1435 continue;
1436 }
1437
1438 // Prepare meta for updating.
1439 $meta_args = apply_filters(
1440 'wc_appointments_gcal_sync_order_itemmeta',
1441 array(
1442 '_appointment_start' => absint( $dtstart->format( 'YmdHis' ) ),
1443 '_appointment_end' => absint( $dtend->format( 'YmdHis' ) ),
1444 '_appointment_all_day' => intval( $all_day ),
1445 ),
1446 $appointment_uid,
1447 $event
1448 );
1449
1450 // Apply update from GCal.
1451 foreach ( $meta_args as $key => $value ) {
1452 update_post_meta( $appointment_uid, $key, $value );
1453 }
1454
1455 // Update appointment event ID if saved
1456 // in extendedProperties of the event.
1457 if ( $appointment_eid ) {
1458 update_post_meta( $appointment_uid, '_wc_appointments_gcal_event_id', $event['id'] );
1459 }
1460
1461 // Debug.
1462 if ( 'yes' === $this->get_debug() ) {
1463 if ( $this->get_user_id() ) {
1464 $this->log->add( $this->id, 'Successfully updated appointment #' . $appointment_uid . ' from Google Calendar event #' . $event['id'] . ' for staff #' . $this->get_user_id() );
1465 } else {
1466 $this->log->add( $this->id, 'Successfully updated appointment #' . $appointment_uid . ' from Google Calendar event #' . $event['id'] );
1467 }
1468 }
1469 }
1470
1471 // Go to next event.
1472 continue;
1473 }
1474
1475 // Check again if event is already synced.
1476 // @TODO remove duplicates more elegantly.
1477 $availabilities_recheck = $wpdb->get_row(
1478 $wpdb->prepare(
1479 "SELECT ID
1480 FROM {$wpdb->prefix}wc_appointments_availability
1481 WHERE `event_id` = %s
1482 ORDER BY ordering ASC",
1483 $event['id']
1484 ),
1485 ARRAY_A
1486 );
1487
1488 // If no availability found, just create one.
1489 if ( ! empty( $availabilities_recheck ) ) {
1490 continue;
1491 }
1492
1493 $availability = get_wc_appointments_availability();
1494 if ( 'CANCELLED' !== strtoupper( $event['status'] ) ) {
1495 $this->update_availability_from_event( $availability, $event );
1496 $availability->save();
1497
1498 // Debug.
1499 if ( 'yes' === $this->get_debug() ) {
1500 if ( $this->get_user_id() ) {
1501 $this->log->add( $this->id, 'Successfully created availability rule from Google Calendar event #' . $event['id'] . ' for staff #' . $this->get_user_id() );
1502 } else {
1503 $this->log->add( $this->id, 'Successfully created availability rule from Google Calendar event #' . $event['id'] );
1504 }
1505 }
1506 }
1507
1508 continue;
1509 }
1510
1511 // Don't save as availability rule if event is from appointment.
1512 if ( $appointment_eid ) {
1513 continue;
1514 }
1515
1516 // Loop through availability rules.
1517 // Update rules or delete them.
1518 foreach ( $availabilities as $availability ) {
1519 $event_date = new WC_DateTime( $event['updated'] );
1520 $availability_date = $availability->get_date_modified();
1521
1522 #$this->log->add( $this->id, 'Event #' . $event['id'] . ' date #' . var_export( $event_date, true ) );
1523 #$this->log->add( $this->id, 'Availability #' . $event['id'] . ' date #' . var_export( $availability_date, true ) );
1524 #$this->log->add( $this->id, 'Event #' . $event['id'] . ' :' . var_export( $event, true ) );
1525
1526 if ( $event_date > $availability_date ) {
1527 // Sync Google Event -> Availability.
1528 if ( 'CANCELLED' !== strtoupper( $event['status'] ) ) {
1529 $this->update_availability_from_event( $availability, $event );
1530 $availability->save();
1531
1532 // Debug.
1533 if ( 'yes' === $this->get_debug() ) {
1534 if ( $this->get_user_id() ) {
1535 $this->log->add( $this->id, 'Successfully updated availability rule from Google Calendar event #' . $event['id'] . ' for staff #' . $this->get_user_id() );
1536 } else {
1537 $this->log->add( $this->id, 'Successfully updated availability rule from Google Calendar event #' . $event['id'] );
1538 }
1539 }
1540 } else {
1541 // @TODO cancelled instances of recurring events should be available.
1542 $availability->delete();
1543
1544 // Debug.
1545 if ( 'yes' === $this->get_debug() ) {
1546 if ( $this->get_user_id() ) {
1547 $this->log->add( $this->id, 'Successfully deleted availability rule #' . $availability->get_id() . ' for staff #' . $this->get_user_id() );
1548 } else {
1549 $this->log->add( $this->id, 'Successfully deleted availability rule #' . $availability->get_id() );
1550 }
1551 }
1552 }
1553 }
1554 }
1555
1556 // Add event to counter.
1557 if ( 'CANCELLED' !== strtoupper( $event['status'] ) ) {
1558 $gcal_count[] = $event['id'];
1559 }
1560 }
1561
1562 if ( 'yes' === $this->get_debug() ) {
1563 if ( $this->get_user_id() ) {
1564 #$this->log->add( $this->id, 'Sync from Google Calendar for staff #' . $this->get_user_id() . ' is successful.' ); #debug
1565 } else {
1566 #$this->log->add( $this->id, 'Sync from Google Calendar is successful.' ); #debug
1567 }
1568 }
1569
1570 // Event ids for counting.
1571 return $gcal_count;
1572 }
1573
1574 /**
1575 * Update global availability object with data from google event object.
1576 *
1577 * @param WC_Appointments_Availability $availability WooCommerce Appointments Availability object.
1578 * @param array $event Google calendar event.
1579 * @param object $dtstart Google calendar event start date/time.
1580 * @param object $dtend Google calendar event end date/time.
1581 *
1582 * @return bool
1583 */
1584 private function update_availability_from_event( WC_Appointments_Availability $availability, $event ) {
1585 // Check if all day event.
1586 // value = DATE for all day, otherwise time included.
1587 $all_day = isset( $event['start']['date'] ) && isset( $event['end']['date'] ) ? true : false;
1588
1589 // Check if BUSY or FREE.
1590 // value = OPAQUE for busy, and TRANSPARENT for free
1591 #$yes_no = isset( $event['transparency'] ) && 'TRANSPARENT' === strtoupper( $event['transparency'] ) ? 'yes' : 'no';
1592 $yes_no = 'no';
1593
1594 // Get site TimeZone.
1595 $wp_appointments_timezone = wc_appointment_get_timezone_string();
1596
1597 if ( $all_day ) {
1598 // Get Start and end date information
1599 $dtstart = new DateTime( $event['start']['date'] );
1600 $dtend = new DateTime( $event['end']['date'] );
1601 $dtend->modify( '-1 second' ); #reduce 1 sec from end date.
1602 } else {
1603 // Get Start and end datetime information
1604 $dtstart = new DateTime( $event['start']['dateTime'] );
1605 $dtstart->setTimezone( new DateTimeZone( $wp_appointments_timezone ) );
1606 $dtend = new DateTime( $event['end']['dateTime'] );
1607 $dtend->setTimezone( new DateTimeZone( $wp_appointments_timezone ) );
1608 }
1609
1610 $availability->set_event_id( $event['id'] )
1611 ->set_title( $event['summary'] )
1612 ->set_appointable( $yes_no )
1613 ->set_priority( 5 )
1614 ->set_ordering( 0 );
1615
1616 if ( $this->get_user_id() ) {
1617 $availability->set_kind( 'availability#staff' );
1618 $availability->set_kind_id( $this->get_user_id() );
1619 } else {
1620 $availability->set_kind( 'availability#global' );
1621 }
1622
1623 if ( isset( $event['recurrence'] ) ) {
1624
1625 $availability->set_range_type( 'rrule' );
1626 $availability->set_rrule( join( "\n", $event['recurrence'] ) );
1627 if ( $all_day ) {
1628 $availability->set_from_range( $dtstart->format( 'Y-m-d' ) );
1629 $availability->set_to_range( $dtend->format( 'Y-m-d' ) );
1630 } else {
1631 $availability->set_from_range( $dtstart->format( \DateTime::RFC3339 ) );
1632 $availability->set_to_range( $dtend->format( \DateTime::RFC3339 ) );
1633 }
1634 } elseif ( $all_day ) {
1635
1636 $availability->set_range_type( 'custom' )
1637 ->set_from_range( $dtstart->format( 'Y-m-d' ) )
1638 ->set_to_range( $dtend->format( 'Y-m-d' ) );
1639
1640 } else {
1641
1642 $availability->set_range_type( 'custom:daterange' )
1643 ->set_from_date( $dtstart->format( 'Y-m-d' ) )
1644 ->set_to_date( $dtend->format( 'Y-m-d' ) )
1645 ->set_from_range( $dtstart->format( 'H:i' ) )
1646 ->set_to_range( $dtend->format( 'H:i' ) );
1647
1648 }
1649
1650 return true;
1651 }
1652
1653 /**
1654 * Maybe delete Global Availability from Google.
1655 *
1656 * @param WC_Appointments_Availability $availability Availability to delete.
1657 */
1658 public function delete_availability( WC_Appointments_Availability $availability ) {
1659 if ( $availability->get_event_id() ) {
1660 // Set staff ID and staff calendar ID
1661 // if event is from staff availability.
1662 if ( 'availability#staff' === $availability->get_kind() && $availability->get_kind_id() ) {
1663 $this->set_user_id( $availability->get_kind_id() );
1664 }
1665
1666 // Set parameters for gcal request.
1667 $calendar_id = $this->get_calendar_id() ? $this->get_calendar_id() : 0;
1668 $api_url = $this->calendars_uri . $calendar_id . '/events/' . $availability->get_event_id().'/?sendUpdates=all';
1669 $user_id = $this->get_user_id() ? $this->get_user_id() : 0;
1670 $params = array(
1671 'method' => 'DELETE',
1672 );
1673
1674 try {
1675
1676 $response = $this->make_gcal_request( $api_url, $params, $user_id );
1677
1678 // Event already deleted.
1679 if ( 410 === $response['response']['code'] ) {
1680 return;
1681 }
1682
1683 // Debug.
1684 if ( 'yes' === $this->get_debug() ) {
1685 if ( $this->get_user_id() ) {
1686 $this->log->add( $this->id, 'Successfully deleted event #' . $availability->get_event_id() . ' from Google for staff #' . $this->get_user_id() );
1687 } else {
1688 $this->log->add( $this->id, 'Successfully deleted event #' . $availability->get_event_id() . ' from Google' );
1689 }
1690 }
1691
1692 } catch ( Exception $e ) {
1693
1694 // Debug.
1695 if ( 'yes' === $this->get_debug() ) {
1696 if ( $this->get_user_id() ) {
1697 $this->log->add( $this->id, 'Error while deleting event #' . $availability->get_event_id() . ' from Google for staff #' . $this->get_user_id() . ':' . $e->getMessage() );
1698 } else {
1699 $this->log->add( $this->id, 'Error while deleting event #' . $availability->get_event_id() . ' from Google: ' . $e->getMessage() );
1700 }
1701 }
1702 }
1703 }
1704 }
1705
1706 /**
1707 * Sync Global Availability to Google.
1708 *
1709 * @param WC_Appointments_Availability $availability Global Availability object.
1710 */
1711 public function sync_availability( WC_Appointments_Availability $availability ) {
1712 if ( ! $availability->get_changes() ) {
1713 // nothing changed don't waste time syncing.
1714 return;
1715 }
1716
1717 if ( $this->syncing ) {
1718 // Event is coming from google don't send it back.
1719 return;
1720 }
1721
1722 if ( $availability->get_event_id() ) {
1723 // Set staff ID and staff calendar ID
1724 // if event is from staff availability.
1725 if ( 'availability#staff' === $availability->get_kind() && $availability->get_kind_id() ) {
1726 $this->set_user_id( $availability->get_kind_id() );
1727 }
1728
1729 // Set parameters for gcal request.
1730 $calendar_id = $this->get_calendar_id() ? $this->get_calendar_id() : 0;
1731 $api_url = $this->calendars_uri . $calendar_id . '/events/' . $availability->get_event_id().'?sendUpdates=all';
1732 $user_id = $this->get_user_id() ? $this->get_user_id() : 0;
1733 $params = array(
1734 'method' => 'GET',
1735 );
1736 $json_data = false;
1737 $event_data = false;
1738
1739 try {
1740
1741 $response = $this->make_gcal_request( $api_url, $params, $user_id );
1742 $json_data = json_decode( $response['body'], true );
1743
1744 // Debug.
1745 if ( 'yes' === $this->get_debug() ) {
1746 if ( $this->get_user_id() ) {
1747 #$this->log->add( $this->id, 'Successfully got event #' . $availability->get_event_id() . ' from Google for staff #' . $this->get_user_id() );
1748 } else {
1749 #$this->log->add( $this->id, 'Successfully got event #' . $availability->get_event_id() . ' from Google' );
1750 }
1751 }
1752
1753 } catch ( Exception $e ) {
1754
1755 // Debug.
1756 if ( 'yes' === $this->get_debug() ) {
1757 if ( $this->get_user_id() ) {
1758 $this->log->add( $this->id, 'Error while getting event #' . $availability->get_event_id() . ' from Google for staff #' . $this->get_user_id() . ':' . $e->getMessage() );
1759 } else {
1760 $this->log->add( $this->id, 'Error while getting event #' . $availability->get_event_id() . ' from Google: ' . $e->getMessage() );
1761 }
1762 }
1763 }
1764
1765 // Only update events created in Gcal.
1766 // @TODO maybe add site rules to gcal as new events.
1767 if ( $json_data ) {
1768 $event = $json_data;
1769 $event_data = $this->update_event_from_availability( $event, $availability );
1770
1771 // Skip update of 'rrule' type of rules.
1772 if ( $event_data ) {
1773
1774 // Set parameters for gcal request.
1775 $params = array(
1776 'method' => 'PUT',
1777 'body' => wp_json_encode( $event_data ),
1778 );
1779
1780 try {
1781
1782 $response = $this->make_gcal_request( $api_url, $params, $user_id );
1783
1784 // Debug.
1785 if ( 'yes' === $this->get_debug() ) {
1786 if ( $this->get_user_id() ) {
1787 $this->log->add( $this->id, 'Successfully updated event #' . $event_data['id'] . ' with Google for staff #' . $this->get_user_id() );
1788 } else {
1789 $this->log->add( $this->id, 'Successfully updated event #' . $event_data['id'] . ' with Google' );
1790 }
1791 }
1792
1793 } catch ( Exception $e ) {
1794
1795 // Debug.
1796 if ( 'yes' === $this->get_debug() ) {
1797 if ( $this->get_user_id() ) {
1798 $this->log->add( $this->id, 'Error while updating event #' . $event_data['id'] . ' with Google for staff #' . $this->get_user_id() . ':' . $e->getMessage() );
1799 } else {
1800 $this->log->add( $this->id, 'Error while updating event #' . $event_data['id'] . ' with Google: ' . $e->getMessage() );
1801 }
1802 }
1803 }
1804 }
1805 }
1806 }
1807 }
1808
1809 /**
1810 * Update google event object with data from global availability object.
1811 *
1812 * @param array $event Google calendar event.
1813 * @param WC_Appointments_Availability $availability WooCommerce Global Availability object.
1814 *
1815 * @return bool
1816 */
1817 private function update_event_from_availability( $event, WC_Appointments_Availability $availability ) {
1818 $timezone = wc_appointment_get_timezone_string();
1819 $start_date_time = new WC_DateTime();
1820 $end_date_time = new WC_DateTime();
1821
1822 $event['summary'] = $availability->get_title();
1823
1824 switch ( $availability->get_range_type() ) {
1825 case 'custom:daterange':
1826 $start_date_time = new WC_DateTime( $availability->get_from_date() . ' ' . $availability->get_from_range() );
1827 $event['start'] = array(
1828 'dateTime' => $start_date_time->format( 'Y-m-d\TH:i:s' ),
1829 'timeZone' => $timezone,
1830 );
1831
1832 $end_date_time = new WC_DateTime( $availability->get_to_date() . ' ' . $availability->get_to_range() );
1833 $event['end'] = array(
1834 'dateTime' => $end_date_time->format( 'Y-m-d\TH:i:s' ),
1835 'timeZone' => $timezone,
1836 );
1837
1838 break;
1839 case 'custom':
1840 $start_date_time = new WC_DateTime( $availability->get_from_range() );
1841 $event['start'] = array(
1842 'date' => $start_date_time->format( 'Y-m-d' ),
1843 );
1844
1845 $end_date_time = new WC_DateTime( $availability->get_to_range() );
1846 $end_date_time->add( new DateInterval( 'P1D' ) );
1847 $event['end'] = array(
1848 'date' => $end_date_time->format( 'Y-m-d' ),
1849 );
1850
1851 break;
1852 case 'months':
1853 $start_date_time->setDate(
1854 date( 'Y' ),
1855 $availability->get_from_range(),
1856 1
1857 );
1858
1859 $event['start'] = array(
1860 'date' => $start_date_time->format( 'Y-m-d' ),
1861 );
1862
1863 $number_of_months = 1 + intval( $availability->get_to_range() ) - intval( $availability->get_from_range() );
1864
1865 $end_date_time = $start_date_time->add( new DateInterval( 'P' . $number_of_months . 'M' ) );
1866
1867 $event['end'] = array(
1868 'date' => $end_date_time->format( 'Y-m-d' ),
1869 );
1870
1871 $event['recurrence'] = array( 'RRULE:FREQ=YEARLY' );
1872
1873 break;
1874 case 'weeks':
1875 $start_date_time->setDate(
1876 date( 'Y' ),
1877 1,
1878 1
1879 );
1880
1881 $end_date_time->setDate(
1882 date( 'Y' ),
1883 1,
1884 2
1885 );
1886
1887 $all_days = join( ',', array_keys( \RRule\RRule::$week_days ) );
1888 $week_numbers = join( ',', range( $availability->get_from_range(), $availability->get_to_range() ) );
1889 $rrule = "RRULE:FREQ=YEARLY;BYWEEKNO=$week_numbers;BYDAY=$all_days";
1890
1891 $event['start'] = array(
1892 'date' => $start_date_time->format( 'Y-m-d' ),
1893 );
1894
1895 $event['end'] = array(
1896 'date' => $end_date_time->format( 'Y-m-d' ),
1897 );
1898
1899 $event['recurrence'] = array( $rrule );
1900
1901 break;
1902 case 'days':
1903 $start_day = intval( $availability->get_from_range() );
1904 $end_day = intval( $availability->get_to_range() );
1905
1906 $start_date_time->modify( 'this ' . self::DAYS_OF_WEEK[ $start_day ] );
1907 $event['start'] = array(
1908 'date' => $start_date_time->format( 'Y-m-d' ),
1909 );
1910
1911 $end_date_time = $start_date_time->modify( 'this ' . self::DAYS_OF_WEEK[ $end_day ] );
1912
1913 $event['end'] = array(
1914 'date' => $end_date_time->format( 'Y-m-d' ),
1915 );
1916
1917 $event['recurrence'] = array( 'RRULE:FREQ=WEEKLY' );
1918
1919 break;
1920 case 'time:1':
1921 case 'time:2':
1922 case 'time:3':
1923 case 'time:4':
1924 case 'time:5':
1925 case 'time:6':
1926 case 'time:7':
1927 list( , $day_of_week ) = explode( ':', $availability->get_range_type() );
1928
1929 $start_date_time->modify( 'this ' . self::DAYS_OF_WEEK[ $day_of_week ] );
1930 $end_date_time->modify( 'this ' . self::DAYS_OF_WEEK[ $day_of_week ] );
1931 $rrule = 'RRULE:FREQ=WEEKLY';
1932
1933 // fall through please.
1934 case 'time':
1935 if ( ! isset( $rrule ) ) {
1936 $rrule = 'RRULE:FREQ=DAILY';
1937 }
1938
1939 list( $start_hour, $start_min ) = explode( ':', $availability->get_from_range() );
1940 $start_date_time->setTime( $start_hour, $start_min );
1941
1942 list( $end_hour, $end_min ) = explode( ':', $availability->get_to_range() );
1943 $end_date_time->setTime( $end_hour, $end_min );
1944
1945 $event['start'] = array(
1946 'dateTime' => $start_date_time->format( 'Y-m-d\TH:i:s' ),
1947 'timeZone' => $timezone,
1948 );
1949
1950 $event['end'] = array(
1951 'dateTime' => $end_date_time->format( 'Y-m-d\TH:i:s' ),
1952 'timeZone' => $timezone,
1953 );
1954
1955 $event['recurrence'] = array( $rrule );
1956
1957 break;
1958
1959 default:
1960 // That should be everything, anything else is not supported.
1961 return;
1962 }
1963
1964 return $event;
1965 }
1966}
1967
1968if ( ! function_exists( 'wc_appointments_gcal' ) ) {
1969 /**
1970 * Returns the main instance of WC_Appointments_GCal to prevent the need to use globals.
1971 *
1972 * @return WC_Appointments_GCal
1973 */
1974 function wc_appointments_gcal() {
1975 return WC_Appointments_GCal::instance();
1976 }
1977}
1978
1979// Action hook to initiate Gcal.
1980add_action( 'init', 'wc_appointments_gcal_init' );
1981
1982if ( ! function_exists( 'wc_appointments_gcal_init' ) ) {
1983 /**
1984 * Initiates wc_appointments_gcal() within Init hook.
1985 *
1986 * @return WC_Appointments_GCal
1987 */
1988 function wc_appointments_gcal_init() {
1989 return wc_appointments_gcal();
1990 }
1991}