| @@ -5,8 +5,10 @@ | ||
| 5 | 5 | * @package Timetics |
| 6 | 6 | */ |
| 7 | 7 | namespace Timetics\Core\Integrations\Google; |
| 8 | 8 | |
| 9 | +defined( 'ABSPATH' ) || exit; | |
| 10 | + | |
| 9 | 11 | use Exception; |
| 10 | 12 | use InvalidArgumentException; |
| 11 | 13 | |
| 12 | 14 | /** |
| @@ -93,8 +95,9 @@ | ||
| 93 | 95 | 'scope' => urlencode_deep( $this->auth_scope ), |
| 94 | 96 | 'redirect_uri' => $this->redirect_uri, |
| 95 | 97 | 'response_type' => 'code', |
| 96 | 98 | 'access_type' => 'offline', |
| 99 | + 'prompt' => 'consent', | |
| 97 | 100 | ); |
| 98 | 101 | |
| 99 | 102 | if ( ! empty( $state ) ) { |
| 100 | 103 | $params['state'] = $state; |
| @@ -144,9 +147,11 @@ | ||
| 144 | 147 | * Fetch access token |
| 145 | 148 | * |
| 146 | 149 | * @param string $code |
| 147 | 150 | * |
| 148 | - * @return void | |
| 151 | + * @return array Token data on success. | |
| 152 | + * @throws \InvalidArgumentException When the code is empty. | |
| 153 | + * @throws \Exception On transport failure or a non-200 token response. | |
| 149 | 154 | */ |
| 150 | 155 | public function fetch_access_token_with_auth_code( $code ) { |
| 151 | 156 | if ( strlen( $code ) === 0 ) { |
| 152 | 157 | throw new InvalidArgumentException( 'Invalid code' ); |
| @@ -161,16 +166,21 @@ | ||
| 161 | 166 | ); |
| 162 | 167 | |
| 163 | 168 | $response = wp_remote_post( self::TIMETICS_TOKEN_URI, array( 'body' => $args ) ); |
| 164 | 169 | |
| 165 | - $status_code = wp_remote_retrieve_response_code( $response ); | |
| 170 | + if ( is_wp_error( $response ) ) { | |
| 171 | + throw new Exception( esc_html( $response->get_error_message() ) ); | |
| 172 | + } | |
| 166 | 173 | |
| 167 | - if ( 200 != $status_code ) { | |
| 168 | - return false; | |
| 174 | + $status_code = (int) wp_remote_retrieve_response_code( $response ); | |
| 175 | + $body = wp_remote_retrieve_body( $response ); | |
| 176 | + $data = json_decode( $body, true ); | |
| 177 | + | |
| 178 | + if ( 200 !== $status_code || empty( $data['access_token'] ) ) { | |
| 179 | + $message = ! empty( $data['error_description'] ) ? $data['error_description'] : __( 'Failed to obtain Google access token.', 'timetics' ); | |
| 180 | + throw new Exception( esc_html( $message ) ); | |
| 169 | 181 | } |
| 170 | 182 | |
| 171 | - $data = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 172 | - | |
| 173 | 183 | return $data; |
| 174 | 184 | } |
| 175 | 185 | |
| 176 | 186 | /** |
| @@ -175,11 +185,21 @@ | ||
| 175 | 185 | |
| 176 | 186 | /** |
| 177 | 187 | * Fetch access token by using refresh token |
| 178 | 188 | * |
| 179 | - * @return void | |
| 189 | + * @param string $refresh_token | |
| 190 | + * | |
| 191 | + * @return array|\WP_Error Token data on success, or a WP_Error whose error_data carries a boolean `transient` flag. | |
| 180 | 192 | */ |
| 181 | 193 | public function fetch_access_token_with_refresh_token( $refresh_token ) { |
| 194 | + if ( empty( $refresh_token ) ) { | |
| 195 | + return new \WP_Error( | |
| 196 | + 'timetics_google_no_refresh_token', | |
| 197 | + 'No Google refresh token is available for this account.', | |
| 198 | + array( 'transient' => false ) | |
| 199 | + ); | |
| 200 | + } | |
| 201 | + | |
| 182 | 202 | $args = array( |
| 183 | 203 | 'client_id' => $this->client_id, |
| 184 | 204 | 'client_secret' => $this->client_secrete, |
| 185 | 205 | 'refresh_token' => $refresh_token, |
| @@ -188,17 +208,47 @@ | ||
| 188 | 208 | ); |
| 189 | 209 | |
| 190 | 210 | $response = wp_remote_post( self::TIMETICS_TOKEN_URI, array( 'body' => $args ) ); |
| 191 | 211 | |
| 192 | - $status_code = wp_remote_retrieve_response_code( $response ); | |
| 212 | + // Transport-level failure (DNS, timeout, connection refused). Transient: | |
| 213 | + // the credential is fine, Google was just briefly unreachable. | |
| 214 | + if ( is_wp_error( $response ) ) { | |
| 215 | + return new \WP_Error( | |
| 216 | + 'timetics_google_refresh_http_error', | |
| 217 | + $response->get_error_message(), | |
| 218 | + array( 'transient' => true ) | |
| 219 | + ); | |
| 220 | + } | |
| 193 | 221 | |
| 194 | - if ( 200 != $status_code ) { | |
| 195 | - return false; | |
| 222 | + $status_code = (int) wp_remote_retrieve_response_code( $response ); | |
| 223 | + $body = wp_remote_retrieve_body( $response ); | |
| 224 | + $data = json_decode( $body, true ); | |
| 225 | + | |
| 226 | + if ( 200 === $status_code && ! empty( $data['access_token'] ) ) { | |
| 227 | + return $data; | |
| 196 | 228 | } |
| 197 | 229 | |
| 198 | - $data = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 230 | + $google_error = isset( $data['error'] ) ? $data['error'] : ''; | |
| 231 | + $transient = ( 'invalid_grant' !== $google_error ); | |
| 199 | 232 | |
| 200 | - return $data; | |
| 233 | + if ( 429 === $status_code || $status_code >= 500 || 0 === $status_code ) { | |
| 234 | + $transient = true; | |
| 235 | + } | |
| 236 | + | |
| 237 | + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { | |
| 238 | + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is guarded by WP_DEBUG checks. | |
| 239 | + error_log( sprintf( 'Timetics Google token refresh failed (HTTP %d): %s', $status_code, $body ) ); | |
| 240 | + } | |
| 241 | + | |
| 242 | + return new \WP_Error( | |
| 243 | + 'timetics_google_refresh_failed', | |
| 244 | + ! empty( $data['error_description'] ) ? $data['error_description'] : 'Failed to refresh Google access token.', | |
| 245 | + [ | |
| 246 | + 'transient' => $transient, | |
| 247 | + 'status_code' => $status_code, | |
| 248 | + 'error' => $google_error, | |
| 249 | + ] | |
| 250 | + ); | |
| 201 | 251 | } |
| 202 | 252 | |
| 203 | 253 | /** |
| 204 | 254 | * Revoke authorization |