PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.12
Booktics – Appointment Booking Calendar for Service Businesses v1.0.12
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.12, at core/module/google-calendar/Controller/calendar-auth-controller.php

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