PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.20
Booktics – Appointment Booking Calendar for Service Businesses v1.0.20
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 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.2 1.0.3 All 31 releases
booktics / core / module / google-calendar / Controller / calendar-auth-controller.php

calendar-auth-controller.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.20, at core/module/google-calendar/Controller/calendar-auth-controller.php

625 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Calendar Auth Controller
4 *
5 * @package BookticsGoogleCalendar\Google\Controller
6 */
7
8 namespace Booktics\Module\Google_Calendar\Controller;
9
10 if ( ! defined( 'ABSPATH' ) ) exit;
11
12 use Booktics\Abstracts\Base_Rest_Controller;
13 use Exception;
14 use WP_REST_Request;
15 use WP_REST_Server;
16 use WP_Error;
17 use WP_HTTP_Response;
18
19 /**
20 * Class Calendar_Auth_Controller
21 */
22 class Calendar_Auth_Controller extends Base_Rest_Controller {
23
24 /**
25 * The namespace of this controller's route.
26 *
27 * @var string
28 */
29 protected $namespace = 'booktics/v1';
30
31 /**
32 * The base of this controller's route.
33 *
34 * @var string
35 */
36 protected $rest_base = 'settings/google';
37
38 /**
39 * User meta key for storing Google tokens
40 */
41 const BGC_USER_META_TOKEN = 'booktics_google_auth_token';
42
43 /**
44 * Google OAuth configuration.
45 *
46 * @var array
47 */
48 private $oauth_config;
49
50 /**
51 * Initialize the class and set up hooks.
52 */
53 public function __construct() {
54 $this->oauth_config = $this->get_oauth_config();
55 add_action( 'template_redirect', array( $this, 'handle_google_auth_callback' ) );
56
57 // In your addon's main file or a dedicated class
58 add_filter(
59 'booktics_team_member_data',
60 function ( $team_member, $team_member_id ) {
61 $team_member['google_calendar_connected'] = $this->team_member_is_connected( $team_member_id );
62
63 return $team_member;
64 },
65 10,
66 2
67 );
68 }
69
70 /**
71 * Get OAuth configuration for Google API
72 *
73 * @return array
74 */
75 private function get_oauth_config() {
76 return array(
77 'client_id' => booktics_get_option( 'google_client_id' ),
78 'client_secret' => booktics_get_option( 'google_secret_key' ),
79 'redirect_uri' => booktics_get_option( 'google_redirect_url' ),
80 'scope' => 'https://www.googleapis.com/auth/calendar',
81 'access_type' => 'offline',
82 'prompt' => 'consent',
83 );
84 }
85
86 /**
87 * Get valid access token for user, refreshing if necessary
88 *
89 * @param int $user_id
90 * @return string|false Access token or false on failure
91 */
92 private function get_valid_access_token( $user_id = null ) {
93 if ( null === $user_id ) {
94 $user_id = get_current_user_id();
95 }
96
97 $token = get_user_meta( $user_id, self::BGC_USER_META_TOKEN, true );
98 if ( ! $token ) {
99 return false;
100 }
101
102 // Handle both string (old format) and array (new format) tokens
103 if ( is_string( $token ) ) {
104 $token = json_decode( $token, true );
105 }
106
107 if ( ! is_array( $token ) || empty( $token['access_token'] ) ) {
108 return false;
109 }
110
111 // Check if token needs refreshing
112 if ( $this->is_token_expired( $token ) ) {
113 $refreshed_token = $this->refresh_access_token( $token, $user_id );
114 if ( ! $refreshed_token ) {
115 delete_user_meta( $user_id, self::BGC_USER_META_TOKEN );
116 return false;
117 }
118 return $refreshed_token['access_token'];
119 }
120
121 return $token['access_token'];
122 }
123
124 /**
125 * Check if token is expired
126 *
127 * @param array $token
128 * @return bool
129 */
130 private function is_token_expired( $token ) {
131 if ( empty( $token['expires_at'] ) ) {
132 return true;
133 }
134
135 $expires_at = is_numeric( $token['expires_at'] ) ? $token['expires_at'] : strtotime( $token['expires_at'] );
136 $buffer_time = 300; // 5 minutes buffer
137
138 return ( time() + $buffer_time ) >= $expires_at;
139 }
140
141 /**
142 * Refresh access token using refresh token
143 *
144 * @param array $token
145 * @param int $user_id
146 * @return array|false New token array or false on failure
147 */
148 private function refresh_access_token( $token, $user_id ) {
149 if ( empty( $token['refresh_token'] ) ) {
150 return false;
151 }
152
153 $response = wp_remote_post(
154 'https://oauth2.googleapis.com/token',
155 array(
156 'body' => array(
157 'client_id' => $this->oauth_config['client_id'],
158 'client_secret' => $this->oauth_config['client_secret'],
159 'refresh_token' => $token['refresh_token'],
160 'grant_type' => 'refresh_token',
161 ),
162 )
163 );
164
165 if ( is_wp_error( $response ) ) {
166 return false;
167 }
168
169 $body = wp_remote_retrieve_body( $response );
170 $new_token = json_decode( $body, true );
171
172 if ( isset( $new_token['error'] ) ) {
173 return false;
174 }
175
176 // Merge with existing token to preserve refresh_token
177 $refreshed_token = array_merge( $token, $new_token );
178
179 // Set expiry time
180 if ( isset( $refreshed_token['expires_in'] ) ) {
181 $refreshed_token['expires_at'] = time() + $refreshed_token['expires_in'];
182 }
183
184 // Save the refreshed token
185 update_user_meta( $user_id, self::BGC_USER_META_TOKEN, $refreshed_token );
186
187 return $refreshed_token;
188 }
189
190 /**
191 * Handle direct OAuth callback from Google.
192 */
193 public function handle_google_auth_callback() {
194 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
195 if ( ! isset( $_GET['code'] ) ) {
196 return;
197 }
198
199 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
200 if ( isset( $_GET['state'] ) ) {
201 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
202 $decoded_state = base64_decode( sanitize_text_field( wp_unslash( $_GET['state'] ) ) );
203 $state_data = json_decode( $decoded_state, true );
204 $team_member_id = isset( $state_data['team_member_id'] ) ? $state_data['team_member_id'] : null;
205 }
206
207 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
208 $code = sanitize_text_field( wp_unslash( $_GET['code'] ) );
209
210 if ( ! isset( $team_member_id ) ) {
211 $team_member_id = get_current_user_id();
212 }
213
214 try {
215 $tokens = $this->exchange_code_for_tokens( $code );
216
217 if ( ! $tokens || isset( $tokens['error'] ) ) {
218 throw new Exception( $tokens['error_description'] ?? 'Authentication failed' );
219 }
220
221 // Set expiry time
222 if ( isset( $tokens['expires_in'] ) ) {
223 $tokens['expires_at'] = time() + $tokens['expires_in'];
224 }
225
226 update_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, $tokens );
227
228 if ( current_user_can( 'manage_options' ) ) {
229 booktics_update_option( 'google_calendar_connected', 1 );
230 }
231
232 $redirect_url = isset( $team_member_id ) && (int)$team_member_id != get_current_user_id() ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
233
234 wp_safe_redirect( $redirect_url );
235 exit;
236
237 } catch ( Exception $e ) {
238 $redirect_url = isset( $team_member_id ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
239
240 wp_safe_redirect( $redirect_url );
241 exit;
242 }
243 }
244
245 /**
246 * Exchange authorization code for access tokens
247 *
248 * @param string $code
249 * @return array|false
250 */
251 private function exchange_code_for_tokens( $code ) {
252 $response = wp_remote_post(
253 'https://oauth2.googleapis.com/token',
254 array(
255 'body' => array(
256 'client_id' => $this->oauth_config['client_id'],
257 'client_secret' => $this->oauth_config['client_secret'],
258 'code' => $code,
259 'grant_type' => 'authorization_code',
260 'redirect_uri' => $this->oauth_config['redirect_uri'],
261 ),
262 )
263 );
264
265 if ( is_wp_error( $response ) ) {
266 return false;
267 }
268
269 $body = wp_remote_retrieve_body( $response );
270 return json_decode( $body, true );
271 }
272
273 private function get_redirect_url_for_admin() {
274 return home_url( '/wp-admin/admin.php?page=booktics#/settings?tab=integration' );
275 }
276
277 private function get_redirect_url_for_team_member( $team_member_id ) {
278 return home_url( "/wp-admin/admin.php?page=booktics#/team-members/update/{$team_member_id}?tab=integration" );
279 }
280
281 /**
282 * Register the routes for the controller.
283 */
284 public function register_routes(): void {
285 // Get Google auth URL
286 register_rest_route(
287 $this->namespace,
288 $this->rest_base . '/connect',
289 array(
290 array(
291 'methods' => WP_REST_Server::READABLE,
292 'callback' => array( $this, 'get_auth_url' ),
293 'permission_callback' => array( $this, 'permission_check' ),
294 'args' => array(
295 'team_member_id' => array(
296 'description' => __( 'ID of the team member to connect', 'booktics' ),
297 'type' => 'integer',
298 'required' => false,
299 ),
300 ),
301 ),
302 )
303 );
304
305 // Revoke authentication
306 register_rest_route(
307 $this->namespace,
308 $this->rest_base . '/revoke',
309 array(
310 array(
311 'methods' => WP_REST_Server::DELETABLE,
312 'callback' => array( $this, 'revoke_auth' ),
313 'permission_callback' => array( $this, 'permission_check' ),
314 'args' => array(
315 'team_member_id' => array(
316 'description' => __( 'ID of the team member to disconnect', 'booktics' ),
317 'type' => 'integer',
318 'required' => false,
319 ),
320 ),
321 ),
322 )
323 );
324
325 // Google connect for admin
326 register_rest_route(
327 $this->namespace,
328 $this->rest_base . '/admin/connect',
329 array(
330 array(
331 'methods' => WP_REST_Server::CREATABLE,
332 'callback' => array( $this, 'get_admin_auth_url' ),
333 'permission_callback' => function () {
334 return current_user_can( 'manage_options' );
335 },
336 ),
337 )
338 );
339
340 // Check authentication status
341 register_rest_route(
342 $this->namespace,
343 $this->rest_base . '/check',
344 array(
345 array(
346 'methods' => WP_REST_Server::READABLE,
347 'callback' => array( $this, 'check_auth' ),
348 'permission_callback' => array( $this, 'permission_check' ),
349 'args' => array(
350 'team_member_id' => array(
351 'description' => __( 'ID of the team member to check', 'booktics' ),
352 'type' => 'integer',
353 'required' => false,
354 ),
355 ),
356 ),
357 )
358 );
359 }
360
361 /**
362 * Check if a given request has permission.
363 *
364 * @return bool|WP_Error
365 */
366 public function permission_check( $request ) {
367 if ( ! is_user_logged_in() ) {
368 return $this->error(
369 __( 'You must be logged in to access this endpoint.', 'booktics' ),
370 401,
371 'permission_error'
372 );
373 }
374
375 // Get team member ID from request
376 $team_member_id = $this->get_team_member_id( $request );
377
378 // If no team member ID specified, allow if user can manage options
379 if ( ! $team_member_id ) {
380 return current_user_can( 'manage_options' );
381 }
382
383 // If team member ID is specified, check if current user can edit that user
384 if ( get_current_user_id() !== $team_member_id && ! current_user_can( 'edit_user', $team_member_id ) ) {
385 return $this->error(
386 __( 'You do not have permission to access this user\'s Google Calendar settings.', 'booktics' ),
387 403,
388 'permission_error'
389 );
390 }
391
392 return true;
393 }
394
395 public function get_admin_auth_url( $request ) {
396 $data = json_decode( $request->get_body(), true );
397 $google_client_id = $data['google_client_id'];
398 $google_client_secret = $data['google_secret_key'];
399 $block_booking_during_google_events = isset( $data['block_booking_during_google_events'] ) ? $data['block_booking_during_google_events'] : false;
400
401 // save credentials in options
402 booktics_update_option( 'google_client_id', $google_client_id );
403 booktics_update_option( 'google_secret_key', $google_client_secret );
404 booktics_update_option( 'block_booking_during_google_events', $block_booking_during_google_events );
405
406 // Update OAuth config
407 $this->oauth_config['client_id'] = $google_client_id;
408 $this->oauth_config['client_secret'] = $google_client_secret;
409
410 $auth_url = $this->create_auth_url();
411
412 return $this->response(
413 array( 'auth_url' => $auth_url ),
414 __( 'Google authentication URL generated successfully.', 'booktics' )
415 );
416 }
417
418 /**
419 * Get Google OAuth URL.
420 *
421 * @param WP_REST_Request $request
422 * @return WP_Error|WP_HTTP_Response
423 */
424 public function get_auth_url( $request ) {
425 try {
426 // Get any parameters you want to pass through
427 $state_params = array(
428 'team_member_id' => absint( $request->get_param( 'team_member_id' ) ),
429 );
430
431 // Convert to JSON and base64 encode to make it URL-safe
432 $state = base64_encode( wp_json_encode( $state_params ) );
433
434 $auth_url = $this->create_auth_url( $state );
435
436 if ( empty( $auth_url ) ) {
437 return $this->error(
438 __( 'Failed to generate Google authentication URL.', 'booktics' ),
439 500,
440 'auth_url_error'
441 );
442 }
443
444 return $this->response(
445 array( 'auth_url' => $auth_url ),
446 __( 'Google authentication URL generated successfully.', 'booktics' )
447 );
448
449 } catch ( Exception $e ) {
450 return $this->error(
451 /* translators: %s: Error message */
452 sprintf( __( 'Failed to generate Google auth URL: %s', 'booktics' ), $e->getMessage() ),
453 500,
454 'auth_error'
455 );
456 }
457 }
458
459 /**
460 * Create OAuth authorization URL
461 *
462 * @param string $state Optional state parameter
463 * @return string
464 */
465 private function create_auth_url( $state = '' ) {
466 $params = array(
467 'client_id' => $this->oauth_config['client_id'],
468 'redirect_uri' => $this->oauth_config['redirect_uri'],
469 'scope' => $this->oauth_config['scope'],
470 'response_type' => 'code',
471 'access_type' => $this->oauth_config['access_type'],
472 'prompt' => $this->oauth_config['prompt'],
473 );
474
475 if ( ! empty( $state ) ) {
476 $params['state'] = $state;
477 }
478
479 return 'https://accounts.google.com/o/oauth2/auth?' . http_build_query( $params );
480 }
481
482 /**
483 * Revoke Google authentication.
484 *
485 * @param WP_REST_Request $request
486 * @return WP_Error|WP_HTTP_Response
487 */
488 public function revoke_auth( $request ) {
489 try {
490 $team_member_id = $this->get_team_member_id( $request );
491
492 if ( current_user_can( 'manage_options' ) && (int)$team_member_id == get_current_user_id() ) {
493 booktics_update_option( 'google_calendar_connected', 0 );
494 }
495
496 // Get the token before revoking
497 $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
498
499 if ( empty( $token ) ) {
500 return $this->response(
501 array( 'revoked' => true ),
502 __( 'Google account was not connected.', 'booktics' )
503 );
504 }
505
506 // Handle both string (old format) and array (new format) tokens
507 if ( is_string( $token ) ) {
508 $token = json_decode( $token, true );
509 }
510
511 // Revoke the token
512 if ( isset( $token['access_token'] ) ) {
513 $this->revoke_token( $token['access_token'] );
514 }
515
516 // Delete the token
517 delete_user_meta( $team_member_id, self::BGC_USER_META_TOKEN );
518
519 return $this->response(
520 array( 'revoked' => true ),
521 __( 'Google authentication revoked successfully.', 'booktics' )
522 );
523
524 } catch ( Exception $e ) {
525 return $this->error(
526 /* translators: %s: Error message */
527 sprintf( __( 'Failed to revoke Google authentication: %s', 'booktics' ), $e->getMessage() ),
528 500,
529 'revoke_error'
530 );
531 }
532 }
533
534 /**
535 * Revoke access token at Google
536 *
537 * @param string $access_token
538 * @return bool
539 */
540 private function revoke_token( $access_token ) {
541 $response = wp_remote_post(
542 'https://oauth2.googleapis.com/revoke',
543 array(
544 'body' => array(
545 'token' => $access_token,
546 ),
547 )
548 );
549
550 return ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200;
551 }
552
553 /**
554 * Check authentication status.
555 *
556 * @param WP_REST_Request $request
557 * @return WP_Error|WP_HTTP_Response
558 */
559 public function check_auth( $request ) {
560 try {
561 $team_member_id = $this->get_team_member_id( $request );
562 $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
563 $is_authenticated = false;
564
565 if ( $token ) {
566 // Handle both string (old format) and array (new format) tokens
567 if ( is_string( $token ) ) {
568 $token = json_decode( $token, true );
569 }
570
571 if ( is_array( $token ) && isset( $token['access_token'] ) ) {
572 $is_authenticated = ! $this->is_token_expired( $token );
573
574 // If token is expired but we have a refresh token, try to refresh
575 if ( ! $is_authenticated && isset( $token['refresh_token'] ) ) {
576 $new_token = $this->refresh_access_token( $token, $team_member_id );
577 if ( $new_token ) {
578 $is_authenticated = true;
579 }
580 }
581 }
582 }
583
584 return $this->response(
585 array( 'authenticated' => $is_authenticated ),
586 $is_authenticated
587 ? __( 'Google account is connected.', 'booktics' )
588 : __( 'Google account is not connected.', 'booktics' )
589 );
590
591 } catch ( Exception $e ) {
592 return $this->error(
593 /* translators: %s: Error message */
594 sprintf( __( 'Failed to check authentication status: %s', 'booktics' ), $e->getMessage() ),
595 500,
596 'auth_check_error'
597 );
598 }
599 }
600
601 private function get_team_member_id( $request ) {
602 $data = $request->get_method() === 'GET'
603 ? $request->get_query_params()
604 : json_decode( $request->get_body(), true );
605
606 if ( isset( $data['team_member_id'] ) ) {
607 return absint( $data['team_member_id'] );
608 }
609
610 return get_current_user_id();
611 }
612
613 /**
614 * Check if team member is connected
615 *
616 * @param int $team_member_id team member id or user id
617 * @return int 1 if connected, 0 if not
618 */
619 private function team_member_is_connected( $team_member_id ) {
620 $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
621
622 return empty( $token ) ? 0 : 1;
623 }
624 }
625