| | |
| | | |
| | | trait OAuth |
| | | { |
| | | use _Base, Admin; |
| | | protected bool $hasOAuth = true; |
| | | use _Base, Admin, ServiceMap; |
| | | protected OAuthURLs $oauth; |
| | | /** |
| | | * Child classes must set this (e.g. 'https://api.example.com/v1/') |
| | | */ |
| | |
| | | protected int $oauthTTL = 300; |
| | | protected array $oathScopes = []; |
| | | |
| | | protected ?RateLimits $oauthLimiter; |
| | | |
| | | abstract protected function getOAuthRequestHeaders(): array; |
| | | abstract protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array; |
| | | protected function initializeOAuthRateLimiter():void |
| | | { |
| | | $key = $this->service_name; |
| | | if (!is_null($this->userID)) { |
| | | $key .= '_'.$this->userID; |
| | | } |
| | | $key .= '_oauth'; |
| | | $this->oauthLimiter = new RateLimits($key, ['s' => 1, 'm' => 10, 'h' => 100]); |
| | | } |
| | | /** |
| | | * @return array Defaults to the base getRequestHeaders, but can optionally be overridden |
| | | */ |
| | | protected function getOAuthRequestHeaders(): array |
| | | { |
| | | return $this->getRequestHeaders(); |
| | | } |
| | | public function getOAuthBaseURL():?string |
| | | { |
| | | return $this->baseOAuthUrl; |
| | | } |
| | | |
| | | /** |
| | | * Default to the base handleResponse method, but extensions can modify accordingly |
| | | * @param array|WP_Error $response |
| | | * @param string $method |
| | | * @param string $endpoint |
| | | * @return array |
| | | */ |
| | | protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array |
| | | { |
| | | return $this->handleResponse($response, $method, $endpoint); |
| | | } |
| | | abstract protected function refreshAccessToken(OAuthCredentials $credentials): array; |
| | | |
| | | function addOAuthActions():void |
| | | { |
| | | $actions = [ |
| | | [ |
| | | 'name' => 'Connect to Service', |
| | | 'label' => 'Connect', |
| | | 'icon' => 'plugs-connected', |
| | | 'action' => [$this, 'handleOAuthConnect'] |
| | | ], |
| | | [ |
| | | 'name' => 'Disconnect from Service', |
| | | 'icon' => 'plugs-disconnected', |
| | | 'action' => [$this, 'handleOAuthDisconnect'] |
| | | ], |
| | | [ |
| | | 'name' => 'Refresh Token', |
| | | 'icon' => 'arrows-clockwise', |
| | | 'action' => [$this, 'handleOAuthRefreshToken'] |
| | | ] |
| | | ]; |
| | | foreach ($actions as $a) { |
| | | $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null); |
| | | if ($this->isSetUp()) { |
| | | $actions = [ |
| | | [ |
| | | 'name' => 'Connect to Service', |
| | | 'label' => 'Connect', |
| | | 'icon' => 'plugs-connected', |
| | | 'action' => [$this, 'handleOAuthConnect'] |
| | | ], |
| | | [ |
| | | 'name' => 'Disconnect from Service', |
| | | 'label' => 'Disconnect', |
| | | 'icon' => 'plugs-disconnected', |
| | | 'action' => [$this, 'handleOAuthDisconnect'] |
| | | ], |
| | | // [ |
| | | // 'name' => 'Refresh Token', |
| | | // 'icon' => 'arrows-clockwise', |
| | | // 'action' => [$this, 'handleOAuthRefreshToken'] |
| | | // ] |
| | | ]; |
| | | foreach ($actions as $a) { |
| | | $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null); |
| | | } |
| | | } |
| | | } |
| | | public function hasOAuthCredentials(?int $userID = null):bool |
| | | { |
| | | if ($userID !== $this->userID) { |
| | | $this->switchUser($userID); |
| | | } |
| | | return $this->hasValidOAuth(); |
| | | } |
| | | public function handleOAuthConnect():array |
| | | { |
| | | $url = $this->getOAuthUrl(); |
| | | if ($url) { |
| | | return [ |
| | | 'success' => true, |
| | | 'redirect' => $url, |
| | | ]; |
| | | } |
| | | return $this->response(false, 'Failed to generate OAuth URL'); |
| | | } |
| | | |
| | | public function handleOAuthDisconnect():array |
| | | { |
| | | $revoked = $this->revokeOAuth(); |
| | | return $this->response($revoked, $revoked ? |
| | | 'Successfully disconnected from '.$this->title : |
| | | 'Failed to disconnect from '.$this->title); |
| | | } |
| | | |
| | | public function revokeOAuth():bool |
| | | { |
| | | if (empty($this->oauth->getRevoke())) { |
| | | error_log('No revoke url set for '.$this->service_name); |
| | | return false; |
| | | } |
| | | $credentials = $this->loadCredentials(); |
| | | if (empty($credentials['access_token'])) { |
| | | error_log('Could not send revoke request, no access_token for '.$this->service_name); |
| | | } else { |
| | | $response = $this->postOAuthRequest($this->oauth->getRevoke(), [ |
| | | 'body' => [ |
| | | 'token' => $credentials['access_token'] |
| | | ] |
| | | ]); |
| | | $empty = new OAuthCredentials([]); |
| | | foreach ($empty->toArray() as $key => $value) { |
| | | if (array_key_exists($key, $credentials)) { |
| | | unset($credentials[$key]); |
| | | } |
| | | } |
| | | |
| | | return Auth::getInstance()->storeCredentials($this->service_name, $credentials, $this->userID); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | protected function registerOAuthCallbacks():void |
| | | { |
| | |
| | | return $this->handleOAuthResponse($error, $method, $endpoint); |
| | | } |
| | | |
| | | if (!empty($this->requiredScopes) && !$credentials->hasScopes($this->requiredScopes)) { |
| | | $error = new WP_Error('insufficient_scope', "Missing required scopes for {$this->service_name}"); |
| | | return $this->handleOAuthResponse($error, $method, $endpoint); |
| | | } |
| | | |
| | | $url = rtrim($this->baseOAuthUrl, '/') . '/' . ltrim($endpoint, '/'); |
| | | |
| | |
| | | } |
| | | |
| | | // Get integration instance |
| | | $integration = JVB()->connect($this->service_name, $user_id); |
| | | $integration = $this->getService($this->service_name); |
| | | |
| | | if (!$integration) { |
| | | if (!$integration || !method_exists($integration, 'switchUser')) { |
| | | wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error'); |
| | | } |
| | | |
| | | $integration->switchUser($user_id); |
| | | |
| | | |
| | | // Exchange code for tokens |
| | | $result = $integration->handleOAuthCode($code, $state); |
| | |
| | | $credentials = $this->loadCredentials(); |
| | | $hasCredentials = $this->hasValidOAuth(); |
| | | $returnURL = $this->getReturnURL(); |
| | | |
| | | if ($hasCredentials) { |
| | | if (!$this->isSetUp()) { |
| | | return ''; |
| | | } |
| | | if (!$hasCredentials) { |
| | | return sprintf( |
| | | '<div class="oauth-connect"> |
| | | <h3>Connect to your %s account</h3> |
| | | <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>', |
| | | <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a> |
| | | </div>', |
| | | $this->title, |
| | | $this->getOAuthURL($returnURL), |
| | | esc_attr($this->service_name), |
| | | $this->outputIcon($this->icon) |
| | | ); |
| | | } |
| | | $form = sprintf( |
| | | '<details open> |
| | | } else { |
| | | $form = sprintf( |
| | | '<details open> |
| | | <summary>Connected Account |
| | | <div class="connection-status connected"> |
| | | <span class="status-indicator">●</span> |
| | |
| | | </summary> |
| | | <span class="label">OAuth Redirect URL:</span> |
| | | <code>%s</code>', |
| | | $this->getRedirectUri() |
| | | ); |
| | | if (!empty($credentials['updated_at'])) { |
| | | $form .= sprintf( |
| | | '<div class="oauth-meta"><small>Token expires: %s</small></div>', |
| | | array_key_exists('expires_at', $credentials) ? |
| | | $this->getRedirectUri() |
| | | ); |
| | | if (!empty($credentials['updated_at'])) { |
| | | $form .= sprintf( |
| | | '<div class="oauth-meta"><small>Token expires: %s</small></div>', |
| | | array_key_exists('expires_at', $credentials) ? |
| | | human_time_diff($credentials['expires_at']) : |
| | | 'Never' |
| | | ); |
| | | ); |
| | | } |
| | | // |
| | | if (method_exists($this, 'renderOAuthConnectedOptions')) { |
| | | $form .= $this->renderOAuthConnectedOptions(); |
| | | } |
| | | $form .= '</details>'; |
| | | return $form; |
| | | } |
| | | // |
| | | if (method_exists($this, 'renderOAuthConnectedOptions')) { |
| | | $form .= $this->renderOAuthConnectedOptions(); |
| | | } |
| | | $form .= '</details'; |
| | | return $form; |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | public function getOAuthURL(?string $return_url = null):string |
| | | { |
| | | if (!$this->hasOAuth) { |
| | | return ''; |
| | | } |
| | | |
| | | $credentials = $this->loadCredentials(); |
| | | if (empty($credentials['authorize'])) { |
| | | if (empty($this->oauth->getAuthorize())) { |
| | | $this->logError('getOAuthUrl', 'OAuth authorize URL not configured'); |
| | | return ''; |
| | | } |
| | |
| | | 'client_id' => $credentials['client_id']??$credentials['application_id']??$credentials['app_id']??'', |
| | | 'redirect_url' => $this->getRedirectUri(), |
| | | 'response_type' => 'code', |
| | | 'scope' => implode(' ', $credentials['scopes']??[]) |
| | | 'scope' => implode(' ', $this->oauthScopes??[]) |
| | | ]; |
| | | $state_key = wp_generate_password(32, false); |
| | | $user_id = $this->userID??0; |
| | |
| | | $params = $this->addOAuthParams($params); |
| | | } |
| | | |
| | | return $credentials['authorize'] . '?' . http_build_query($params); |
| | | return $this->oauth->getAuthorize() . '?' . http_build_query($params); |
| | | } |
| | | |
| | | protected function getRedirectUri():string |
| | | { |
| | | if ($this->hasOAuth) { |
| | | return admin_url('admin-ajax.php?action='.BASE . $this->service_name . '_oauth_callback'); |
| | | } |
| | | return rest_url('jvb/v1/oauth/callback?service=' . $this->service_name); |
| | | return admin_url('admin-ajax.php?action='.BASE . $this->service_name . '_oauth_callback'); |
| | | } |
| | | |
| | | /**************************************************** |
| | | * UTILITY |
| | | ****************************************************/ |
| | | protected function getAccessToken():string |
| | | { |
| | | return $this->loadCredentials()['access_token']??''; |
| | | } |
| | | } |