PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
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
← All changes | core/module/google-calendar/Controller/calendar-auth-controller.php +283 -80 1.0.0 → 1.0.28 View file →
@@ -4,13 +4,13 @@
4 4 *
5 5 * @package BookticsGoogleCalendar\Google\Controller
6 6 */
7 7
8 -namespace BookticsGoogleCalendar\Google\Controller;
8 +namespace Booktics\Module\Google_Calendar\Controller;
9 9
10 -use BookticsGoogleCalendar\Abstracts\Base_Rest_Controller;
11 -use Google\Client as Google_Client;
12 -use Google\Service\Calendar;
10 +if ( ! defined( 'ABSPATH' ) ) exit;
11 +
12 +use Booktics\Abstracts\Base_Rest_Controller;
13 13 use Exception;
14 14 use WP_REST_Request;
15 15 use WP_REST_Server;
16 16 use WP_Error;
@@ -40,110 +40,232 @@
40 40 */
41 41 const BGC_USER_META_TOKEN = 'booktics_google_auth_token';
42 42
43 43 /**
44 - * Google Client instance.
44 + * Google OAuth configuration.
45 45 *
46 - * @var Google_Client
46 + * @var array
47 47 */
48 - private $client;
48 + private $oauth_config;
49 49
50 50 /**
51 51 * Initialize the class and set up hooks.
52 52 */
53 53 public function __construct() {
54 - $this->client = $this->initialize_google_client();
55 - add_action( 'init', array( $this, 'handle_google_auth_callback' ) );
56 -
54 + $this->oauth_config = $this->get_oauth_config();
55 + add_action( 'template_redirect', array( $this, 'handle_google_auth_callback' ) );
56 + add_action( 'admin_notices', array( $this, 'permalink_warning_notice' ) );
57 +
57 58 // In your addon's main file or a dedicated class
58 - add_filter('booktics_team_member_data', function($team_member, $team_member_id) {
59 - $team_member['google_calendar_connected'] = $this->team_member_is_connected( $team_member_id );
59 + add_filter(
60 + 'booktics_team_member_data',
61 + function ( $team_member, $team_member_id ) {
62 + $team_member['google_calendar_connected'] = $this->team_member_is_connected( $team_member_id );
60 63
61 - return $team_member;
62 - }, 10, 2);
64 + return $team_member;
65 + },
66 + 10,
67 + 2
68 + );
63 69 }
64 70
65 71 /**
66 - * Initialize Google Client with default configuration
72 + * Warn the admin when the permalink structure breaks the OAuth callback.
67 73 *
68 - * @return Google_Client
74 + * The Google callback lives at a pretty path ( booktics-integration/
75 + * google-auth ), which WordPress only routes when permalinks are not set to
76 + * Plain. On Plain that URL 404s and the connection silently fails, so a
77 + * heads-up is shown — but only once Google is actually configured, to keep
78 + * the notice off screens where it would be noise.
79 + *
80 + * @return void
69 81 */
70 - private function initialize_google_client() {
71 - $client = new Google_Client();
72 - $client->setApplicationName( 'Booktics Google Calendar' );
73 - $client->setScopes( array( Calendar::CALENDAR ) );
74 - $client->setAuthConfig(
75 - array(
76 - 'client_id' => booktics_get_option( 'google_client_id' ),
77 - 'client_secret' => booktics_get_option( 'google_secret_key' ),
82 + public function permalink_warning_notice() {
83 + if ( ! current_user_can( 'manage_options' ) ) {
84 + return;
85 + }
86 +
87 + // Plain permalinks store an empty structure.
88 + if ( get_option( 'permalink_structure' ) ) {
89 + return;
90 + }
91 +
92 + if ( ! booktics_get_option( 'google_client_id' ) ) {
93 + return;
94 + }
95 +
96 + printf(
97 + '<div class="notice notice-warning"><p>%s</p></div>',
98 + wp_kses(
99 + sprintf(
100 + /* translators: %s: link to the WordPress permalink settings screen. */
101 + __( 'Booktics: Google Calendar sign-in needs a permalink structure other than <strong>Plain</strong>. Open <a href="%s">Settings &rarr; Permalinks</a> and pick <strong>Post name</strong>, or the Google connection will fail.', 'booktics' ),
102 + esc_url( admin_url( 'options-permalink.php' ) )
103 + ),
104 + array(
105 + 'strong' => array(),
106 + 'a' => array( 'href' => array() ),
107 + )
78 108 )
79 109 );
110 + }
80 111
81 - $client->setRedirectUri( booktics_get_option( 'google_redirect_url' ) );
82 - $client->setAccessType( 'offline' );
83 - $client->setPrompt( 'consent' );
84 - $client->setIncludeGrantedScopes( true );
85 -
86 - return $client;
112 + /**
113 + * Get OAuth configuration for Google API
114 + *
115 + * @return array
116 + */
117 + private function get_oauth_config() {
118 + return array(
119 + 'client_id' => booktics_get_option( 'google_client_id' ),
120 + 'client_secret' => booktics_get_option( 'google_secret_key' ),
121 + 'redirect_uri' => booktics_get_option( 'google_redirect_url' ),
122 + 'scope' => 'https://www.googleapis.com/auth/calendar',
123 + 'access_type' => 'offline',
124 + 'prompt' => 'consent',
125 + );
87 126 }
88 127
89 128 /**
90 - * Get Google Client with user token
129 + * Get valid access token for user, refreshing if necessary
91 130 *
92 131 * @param int $user_id
93 - * @return Google_Client
132 + * @return string|false Access token or false on failure
94 133 */
95 - private function get_google_client( $user_id = null ) {
134 + private function get_valid_access_token( $user_id = null ) {
96 135 if ( null === $user_id ) {
97 136 $user_id = get_current_user_id();
98 137 }
99 138
100 139 $token = get_user_meta( $user_id, self::BGC_USER_META_TOKEN, true );
101 - if ( $token ) {
102 - $this->client->setAccessToken( $token );
140 + if ( ! $token ) {
141 + return false;
142 + }
103 143
104 - // Refresh token if expired
105 - if ( $this->client->isAccessTokenExpired() ) {
106 - if ( $this->client->getRefreshToken() ) {
107 - $new_token = $this->client->fetchAccessTokenWithRefreshToken( $this->client->getRefreshToken() );
108 - update_user_meta( $user_id, self::BGC_USER_META_TOKEN, $new_token );
109 - } else {
110 - delete_user_meta( $user_id, self::BGC_USER_META_TOKEN );
111 - throw new Exception( 'Session expired. Please reconnect your Google account.' );
112 - }
144 + // Handle both string (old format) and array (new format) tokens
145 + if ( is_string( $token ) ) {
146 + $token = json_decode( $token, true );
147 + }
148 +
149 + if ( ! is_array( $token ) || empty( $token['access_token'] ) ) {
150 + return false;
151 + }
152 +
153 + // Check if token needs refreshing
154 + if ( $this->is_token_expired( $token ) ) {
155 + $refreshed_token = $this->refresh_access_token( $token, $user_id );
156 + if ( ! $refreshed_token ) {
157 + delete_user_meta( $user_id, self::BGC_USER_META_TOKEN );
158 + return false;
113 159 }
160 + return $refreshed_token['access_token'];
114 161 }
115 162
116 - return $this->client;
163 + return $token['access_token'];
117 164 }
118 165
119 166 /**
167 + * Check if token is expired
168 + *
169 + * @param array $token
170 + * @return bool
171 + */
172 + private function is_token_expired( $token ) {
173 + if ( empty( $token['expires_at'] ) ) {
174 + return true;
175 + }
176 +
177 + $expires_at = is_numeric( $token['expires_at'] ) ? $token['expires_at'] : strtotime( $token['expires_at'] );
178 + $buffer_time = 300; // 5 minutes buffer
179 +
180 + return ( time() + $buffer_time ) >= $expires_at;
181 + }
182 +
183 + /**
184 + * Refresh access token using refresh token
185 + *
186 + * @param array $token
187 + * @param int $user_id
188 + * @return array|false New token array or false on failure
189 + */
190 + private function refresh_access_token( $token, $user_id ) {
191 + if ( empty( $token['refresh_token'] ) ) {
192 + return false;
193 + }
194 +
195 + $response = wp_remote_post(
196 + 'https://oauth2.googleapis.com/token',
197 + array(
198 + 'body' => array(
199 + 'client_id' => $this->oauth_config['client_id'],
200 + 'client_secret' => $this->oauth_config['client_secret'],
201 + 'refresh_token' => $token['refresh_token'],
202 + 'grant_type' => 'refresh_token',
203 + ),
204 + )
205 + );
206 +
207 + if ( is_wp_error( $response ) ) {
208 + return false;
209 + }
210 +
211 + $body = wp_remote_retrieve_body( $response );
212 + $new_token = json_decode( $body, true );
213 +
214 + if ( isset( $new_token['error'] ) ) {
215 + return false;
216 + }
217 +
218 + // Merge with existing token to preserve refresh_token
219 + $refreshed_token = array_merge( $token, $new_token );
220 +
221 + // Set expiry time
222 + if ( isset( $refreshed_token['expires_in'] ) ) {
223 + $refreshed_token['expires_at'] = time() + $refreshed_token['expires_in'];
224 + }
225 +
226 + // Save the refreshed token
227 + update_user_meta( $user_id, self::BGC_USER_META_TOKEN, $refreshed_token );
228 +
229 + return $refreshed_token;
230 + }
231 +
232 + /**
120 233 * Handle direct OAuth callback from Google.
121 234 */
122 235 public function handle_google_auth_callback() {
236 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
123 237 if ( ! isset( $_GET['code'] ) ) {
124 238 return;
125 239 }
126 -
240 +
241 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
127 242 if ( isset( $_GET['state'] ) ) {
128 - $decoded_state = base64_decode($_GET['state']);
129 - $state_data = json_decode($decoded_state, true);
130 - $team_member_id = isset($state_data['team_member_id']) ? $state_data['team_member_id'] : null;
243 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
244 + $decoded_state = base64_decode( sanitize_text_field( wp_unslash( $_GET['state'] ) ) );
245 + $state_data = json_decode( $decoded_state, true );
246 + $team_member_id = isset( $state_data['team_member_id'] ) ? $state_data['team_member_id'] : null;
131 247 }
132 248
133 - $code = sanitize_text_field( $_GET['code'] );
134 -
249 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
250 + $code = sanitize_text_field( wp_unslash( $_GET['code'] ) );
251 +
135 252 if ( ! isset( $team_member_id ) ) {
136 253 $team_member_id = get_current_user_id();
137 254 }
138 255
139 256 try {
140 - $tokens = $this->client->fetchAccessTokenWithAuthCode( $code );
257 + $tokens = $this->exchange_code_for_tokens( $code );
141 258
142 - if ( isset( $tokens['error'] ) ) {
259 + if ( ! $tokens || isset( $tokens['error'] ) ) {
143 260 throw new Exception( $tokens['error_description'] ?? 'Authentication failed' );
144 261 }
145 262
263 + // Set expiry time
264 + if ( isset( $tokens['expires_in'] ) ) {
265 + $tokens['expires_at'] = time() + $tokens['expires_in'];
266 + }
267 +
146 268 update_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, $tokens );
147 269
148 270 if ( current_user_can( 'manage_options' ) ) {
149 271 booktics_update_option( 'google_calendar_connected', 1 );
@@ -148,15 +270,15 @@
148 270 if ( current_user_can( 'manage_options' ) ) {
149 271 booktics_update_option( 'google_calendar_connected', 1 );
150 272 }
151 273
152 - $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();
274 + $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();
153 275
154 276 wp_safe_redirect( $redirect_url );
155 277 exit;
156 278
157 279 } catch ( Exception $e ) {
158 - $redirect_url = isset($team_member_id) ? $this->get_redirect_url_for_team_member($team_member_id) : $this->get_redirect_url_for_admin();
280 + $redirect_url = isset( $team_member_id ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
159 281
160 282 wp_safe_redirect( $redirect_url );
161 283 exit;
162 284 }
@@ -161,13 +283,41 @@
161 283 exit;
162 284 }
163 285 }
164 286
287 + /**
288 + * Exchange authorization code for access tokens
289 + *
290 + * @param string $code
291 + * @return array|false
292 + */
293 + private function exchange_code_for_tokens( $code ) {
294 + $response = wp_remote_post(
295 + 'https://oauth2.googleapis.com/token',
296 + array(
297 + 'body' => array(
298 + 'client_id' => $this->oauth_config['client_id'],
299 + 'client_secret' => $this->oauth_config['client_secret'],
300 + 'code' => $code,
301 + 'grant_type' => 'authorization_code',
302 + 'redirect_uri' => $this->oauth_config['redirect_uri'],
303 + ),
304 + )
305 + );
306 +
307 + if ( is_wp_error( $response ) ) {
308 + return false;
309 + }
310 +
311 + $body = wp_remote_retrieve_body( $response );
312 + return json_decode( $body, true );
313 + }
314 +
165 315 private function get_redirect_url_for_admin() {
166 316 return home_url( '/wp-admin/admin.php?page=booktics#/settings?tab=integration' );
167 317 }
168 318
169 - private function get_redirect_url_for_team_member($team_member_id) {
319 + private function get_redirect_url_for_team_member( $team_member_id ) {
170 320 return home_url( "/wp-admin/admin.php?page=booktics#/team-members/update/{$team_member_id}?tab=integration" );
171 321 }
172 322
173 323 /**
@@ -172,9 +322,9 @@
172 322
173 323 /**
174 324 * Register the routes for the controller.
175 325 */
176 - public function register_routes() {
326 + public function register_routes(): void {
177 327 // Get Google auth URL
178 328 register_rest_route(
179 329 $this->namespace,
180 330 $this->rest_base . '/connect',
@@ -287,21 +437,23 @@
287 437 public function get_admin_auth_url( $request ) {
288 438 $data = json_decode( $request->get_body(), true );
289 439 $google_client_id = $data['google_client_id'];
290 440 $google_client_secret = $data['google_secret_key'];
441 + $block_booking_during_google_events = isset( $data['block_booking_during_google_events'] ) ? $data['block_booking_during_google_events'] : false;
291 442
292 443 // save credentials in options
293 444 booktics_update_option( 'google_client_id', $google_client_id );
294 445 booktics_update_option( 'google_secret_key', $google_client_secret );
446 + booktics_update_option( 'block_booking_during_google_events', $block_booking_during_google_events );
295 447
296 - $this->client->setClientId( $google_client_id );
297 - $this->client->setClientSecret( $google_client_secret );
298 - $this->client->setRedirectUri( booktics_get_option( 'google_redirect_url' ) );
448 + // Update OAuth config
449 + $this->oauth_config['client_id'] = $google_client_id;
450 + $this->oauth_config['client_secret'] = $google_client_secret;
299 451
300 - booktics_update_option('google_calendar_connected', 1);
452 + $auth_url = $this->create_auth_url();
301 453
302 454 return $this->response(
303 - array( 'auth_url' => $this->client->createAuthUrl() ),
455 + array( 'auth_url' => $auth_url ),
304 456 __( 'Google authentication URL generated successfully.', 'booktics' )
305 457 );
306 458 }
307 459
@@ -314,18 +466,15 @@
314 466 public function get_auth_url( $request ) {
315 467 try {
316 468 // Get any parameters you want to pass through
317 469 $state_params = array(
318 - 'team_member_id' => $request->get_param('team_member_id'),
470 + 'team_member_id' => absint( $request->get_param( 'team_member_id' ) ),
319 471 );
320 -
472 +
321 473 // Convert to JSON and base64 encode to make it URL-safe
322 - $state = base64_encode(json_encode($state_params));
323 -
324 - // Set the state parameter
325 - $this->client->setState($state);
474 + $state = base64_encode( wp_json_encode( $state_params ) );
326 475
327 - $auth_url = $this->client->createAuthUrl();
476 + $auth_url = $this->create_auth_url( $state );
328 477
329 478 if ( empty( $auth_url ) ) {
330 479 return $this->error(
331 480 __( 'Failed to generate Google authentication URL.', 'booktics' ),
@@ -349,8 +498,31 @@
349 498 }
350 499 }
351 500
352 501 /**
502 + * Create OAuth authorization URL
503 + *
504 + * @param string $state Optional state parameter
505 + * @return string
506 + */
507 + private function create_auth_url( $state = '' ) {
508 + $params = array(
509 + 'client_id' => $this->oauth_config['client_id'],
510 + 'redirect_uri' => $this->oauth_config['redirect_uri'],
511 + 'scope' => $this->oauth_config['scope'],
512 + 'response_type' => 'code',
513 + 'access_type' => $this->oauth_config['access_type'],
514 + 'prompt' => $this->oauth_config['prompt'],
515 + );
516 +
517 + if ( ! empty( $state ) ) {
518 + $params['state'] = $state;
519 + }
520 +
521 + return 'https://accounts.google.com/o/oauth2/auth?' . http_build_query( $params );
522 + }
523 +
524 + /**
353 525 * Revoke Google authentication.
354 526 *
355 527 * @param WP_REST_Request $request
356 528 * @return WP_Error|WP_HTTP_Response
@@ -357,10 +529,10 @@
357 529 */
358 530 public function revoke_auth( $request ) {
359 531 try {
360 532 $team_member_id = $this->get_team_member_id( $request );
361 -
362 - if ( current_user_can('manage_options') && $team_member_id == get_current_user_id() ) {
533 +
534 + if ( current_user_can( 'manage_options' ) && (int)$team_member_id == get_current_user_id() ) {
363 535 booktics_update_option( 'google_calendar_connected', 0 );
364 536 }
365 537
366 538 // Get the token before revoking
@@ -372,10 +544,17 @@
372 544 __( 'Google account was not connected.', 'booktics' )
373 545 );
374 546 }
375 547
548 + // Handle both string (old format) and array (new format) tokens
549 + if ( is_string( $token ) ) {
550 + $token = json_decode( $token, true );
551 + }
552 +
376 553 // Revoke the token
377 - $this->client->revokeToken( $token['access_token'] );
554 + if ( isset( $token['access_token'] ) ) {
555 + $this->revoke_token( $token['access_token'] );
556 + }
378 557
379 558 // Delete the token
380 559 delete_user_meta( $team_member_id, self::BGC_USER_META_TOKEN );
381 560
@@ -394,8 +573,27 @@
394 573 }
395 574 }
396 575
397 576 /**
577 + * Revoke access token at Google
578 + *
579 + * @param string $access_token
580 + * @return bool
581 + */
582 + private function revoke_token( $access_token ) {
583 + $response = wp_remote_post(
584 + 'https://oauth2.googleapis.com/revoke',
585 + array(
586 + 'body' => array(
587 + 'token' => $access_token,
588 + ),
589 + )
590 + );
591 +
592 + return ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200;
593 + }
594 +
595 + /**
398 596 * Check authentication status.
399 597 *
400 598 * @param WP_REST_Request $request
401 599 * @return WP_Error|WP_HTTP_Response
@@ -406,17 +604,22 @@
406 604 $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
407 605 $is_authenticated = false;
408 606
409 607 if ( $token ) {
410 - $this->client->setAccessToken( $token );
411 - $is_authenticated = ! $this->client->isAccessTokenExpired();
608 + // Handle both string (old format) and array (new format) tokens
609 + if ( is_string( $token ) ) {
610 + $token = json_decode( $token, true );
611 + }
412 612
413 - // If token is expired but we have a refresh token, try to refresh
414 - if ( ! $is_authenticated && $this->client->getRefreshToken() ) {
415 - $new_token = $this->client->fetchAccessTokenWithRefreshToken( $this->client->getRefreshToken() );
416 - if ( ! isset( $new_token['error'] ) ) {
417 - update_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, $new_token );
418 - $is_authenticated = true;
613 + if ( is_array( $token ) && isset( $token['access_token'] ) ) {
614 + $is_authenticated = ! $this->is_token_expired( $token );
615 +
616 + // If token is expired but we have a refresh token, try to refresh
617 + if ( ! $is_authenticated && isset( $token['refresh_token'] ) ) {
618 + $new_token = $this->refresh_access_token( $token, $team_member_id );
619 + if ( $new_token ) {
620 + $is_authenticated = true;
621 + }
419 622 }
420 623 }
421 624 }
422 625