exists) { $credentialManager = new PBXCredentialManager(); $credential = $credentialManager->ensurePrimaryCredential(); } if (!$credential || !$credential->isValid()) { throw new PBXConnectionException('No valid PBX API credentials available'); } $this->baseUrl = rtrim($credential->api_url, '/'); $this->clientId = $credential->api_user; $this->clientSecret = $credential->getDecryptedSecret(); $credential->markAsUsed(); } // ------------------------------------------------------------------------- // OAuth 2.0 Token Management // ------------------------------------------------------------------------- /** * Obtain an OAuth 2.0 Bearer token using the client_credentials grant. * Tokens are cached in-memory for the lifetime of this object. * * @param string $scope FreePBX scope key: 'rest', 'gql', or 'rest gql' * @return string * @throws PBXConnectionException */ public function getToken(string $scope = 'rest gql'): string { $cacheKey = md5($scope); if ( isset($this->tokenCache[$cacheKey]) && $this->tokenCache[$cacheKey]['expires_at'] > time() + 60 ) { return $this->tokenCache[$cacheKey]['token']; } $tokenUrl = $this->baseUrl . '/admin/api/api/token'; try { $response = Http::timeout($this->timeout) ->asForm() ->post($tokenUrl, [ 'grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'scope' => $scope, ]); if (!$response->successful()) { throw new PBXConnectionException( 'FreePBX token request failed (' . $response->status() . '): ' . $response->body() ); } $data = $response->json(); if (empty($data['access_token'])) { throw new PBXConnectionException( 'FreePBX token response missing access_token: ' . $response->body() ); } $this->tokenCache[$cacheKey] = [ 'token' => $data['access_token'], 'expires_at' => time() + ($data['expires_in'] ?? 3600), ]; Log::debug('PBX OAuth token acquired', ['scope' => $scope]); return $this->tokenCache[$cacheKey]['token']; } catch (PBXConnectionException $e) { throw $e; } catch (\Exception $e) { throw new PBXConnectionException( 'Failed to obtain FreePBX OAuth token: ' . $e->getMessage(), 0, $e ); } } // ------------------------------------------------------------------------- // GraphQL API (/admin/api/api/gql) // ------------------------------------------------------------------------- /** * Execute a GraphQL query or mutation against the FreePBX API. * * @param string $query GraphQL query/mutation string * @param array $variables GraphQL variables * @return array Decoded JSON response * @throws PBXException */ public function graphql(string $query, array $variables = []): array { $token = $this->getToken('gql'); $url = $this->baseUrl . '/admin/api/api/gql'; try { $response = Http::timeout($this->timeout) ->withToken($token) ->post($url, array_filter([ 'query' => $query, 'variables' => $variables ?: null, ])); if (!$response->successful()) { $this->handleErrorResponse($response, 'POST', 'gql'); } $data = $response->json() ?? []; if (!empty($data['errors'])) { $messages = implode('; ', array_column($data['errors'], 'message')); Log::error('FreePBX GraphQL errors', [ 'query' => substr($query, 0, 200), 'errors' => $data['errors'], ]); throw new PBXException('FreePBX GraphQL error: ' . $messages); } return $data['data'] ?? []; } catch (PBXException $e) { throw $e; } catch (\Illuminate\Http\Client\ConnectionException $e) { throw new PBXConnectionException( 'Failed to connect to FreePBX GraphQL API: ' . $e->getMessage(), 0, $e ); } catch (\Exception $e) { throw new PBXException( 'FreePBX GraphQL request failed: ' . $e->getMessage(), 0, $e ); } } // ------------------------------------------------------------------------- // REST API (/admin/api/api/rest/{freepbxModule}/{route}) // ------------------------------------------------------------------------- /** * Make a GET request to a FreePBX REST module endpoint. * * @param string $module FreePBX module name (e.g. 'core', 'userman', 'framework') * @param string $route Route within the module (e.g. 'users', 'users/1000') * @param array $query Query-string parameters * @return array * @throws PBXException */ public function get(string $module, string $route = '', array $query = []): array { return $this->restRequest('GET', $module, $route, [], $query); } /** * Make a POST request to a FreePBX REST module endpoint. */ public function post(string $module, string $route, array $data = []): array { return $this->restRequest('POST', $module, $route, $data); } /** * Make a PUT request to a FreePBX REST module endpoint. */ public function put(string $module, string $route, array $data = []): array { return $this->restRequest('PUT', $module, $route, $data); } /** * Make a DELETE request to a FreePBX REST module endpoint. */ public function delete(string $module, string $route): array { return $this->restRequest('DELETE', $module, $route); } protected function restRequest( string $method, string $module, string $route = '', array $data = [], array $query = [] ): array { $token = $this->getToken('rest'); $path = '/admin/api/api/rest/' . trim($module, '/') . ('' !== $route ? '/' . ltrim($route, '/') : ''); $url = $this->baseUrl . $path; try { $response = Http::timeout($this->timeout) ->withToken($token) ->send($method, $url, [ 'json' => $data ?: null, 'query' => $query ?: null, ]); if (!$response->successful()) { $this->handleErrorResponse($response, $method, $path); } return $response->json() ?? []; } catch (PBXException $e) { throw $e; } catch (\Illuminate\Http\Client\ConnectionException $e) { throw new PBXConnectionException( 'Failed to connect to FreePBX REST API: ' . $e->getMessage(), 0, $e, ['path' => $path] ); } catch (\Exception $e) { throw new PBXException( 'FreePBX REST request failed: ' . $e->getMessage(), 0, $e, ['path' => $path] ); } } protected function handleErrorResponse($response, string $method, string $endpoint): void { $status = $response->status(); $body = $response->json() ?? []; $message = $body['message'] ?? $body['error'] ?? $response->body(); Log::error('PBX API error response', [ 'method' => $method, 'endpoint' => $endpoint, 'status' => $status, 'body' => $body, ]); throw new PBXException( "PBX API Error ({$status}): {$message}", $status, null, ['method' => $method, 'endpoint' => $endpoint, 'response' => $body] ); } // ------------------------------------------------------------------------- // Convenience helpers // ------------------------------------------------------------------------- /** * Test connectivity by calling the lightweight /framework/version REST endpoint. */ public function testConnection(): bool { try { $this->get('framework', 'version'); return true; } catch (\Exception $e) { Log::warning('PBX API connection test failed', ['error' => $e->getMessage()]); return false; } } /** * Trigger a FreePBX "Apply Config" / dialplan reload via GraphQL. * * The doreload mutation requires an input argument (even though the input type is empty). * See: FreePBX API test ModuleAdminGqlApiTest.php:394 — doreload(input: { }) */ public function applyChanges(): bool { try { $this->graphql( 'mutation DoReload($input: doreloadInput!) { doreload(input: $input) { status message } }', ['input' => new \stdClass()] ); return true; } catch (\Exception $e) { Log::error('Failed to apply PBX configuration changes', ['error' => $e->getMessage()]); throw new PBXException('Failed to apply configuration changes: ' . $e->getMessage(), 0, $e); } } }