*/ if ( ! defined( 'ABSPATH' ) ) { exit; } class Group107_AccessibleDocs_API_Handler { /** * Transient (per site) that caches the API access token. */ const TOKEN_TRANSIENT = 'accessible_docs_access_token'; /** * Send a request to the AccessibleDocs API (or to an upload URL returned by it). * * @param string $url The endpoint URL. * @param mixed $data Request body: array (JSON-encoded) or raw string. * @param string $method HTTP method. * @param array $headers HTTP headers. * @param string $data_type 'json' to JSON-encode $data and decode the response. * @return array { success: bool, code: int, data?: mixed, error?: string } */ public static function accessible_docs_api_request($url, $data, $method, $headers, $data_type = 'json') { // Add source identifier to all requests $headers['X-AccessibleDocs-Source'] = 'wordpress-plugin'; $args = array( 'method' => $method, 'headers' => $headers, 'timeout' => 180, ); if (!empty($data)) { $args['body'] = ('json' === $data_type) ? wp_json_encode($data) : $data; } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { return array( 'success' => false, 'code' => 0, 'error' => $response->get_error_message(), ); } $http_code = (int) wp_remote_retrieve_response_code($response); if ($http_code < 200 || $http_code > 299) { return array( 'success' => false, 'code' => $http_code, 'error' => "Server returned HTTP code {$http_code}", ); } $body = wp_remote_retrieve_body($response); if ('json' === $data_type) { $response_data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { $response_data = null; } } else { $response_data = $body; } return array( 'success' => true, 'code' => $http_code, 'data' => $response_data, ); } /** * Log in with the credentials from the plugin settings. * * @param string|null $email Email to use instead of the saved one (used to validate new settings). * @param string|null $password Password to use instead of the saved one. * @return array Response with data.accessToken on success. */ public static function accessible_docs_login_request($email = null, $password = null) { if (null === $email || null === $password) { $settings = get_option('accessible_docs_settings'); $email = $settings['email'] ?? ''; $password = $settings['password'] ?? ''; } if (empty($email) || empty($password)) { return [ 'success' => false, 'code' => 0, 'error' => __('Email or password is not set in the plugin settings.', 'accessible-docs'), ]; } $url = ACCESSIBLE_DOCS_API_URL . "/v1/login"; $data = [ "email" => $email, "password" => $password, ]; $headers = [ "Content-Type" => "application/json", ]; return self::accessible_docs_api_request($url, $data, 'POST', $headers, 'json'); } /** * Access token for the saved credentials, cached until shortly before it expires. * * @param bool $force_refresh Ignore the cached token. * @return string|false */ public static function get_access_token($force_refresh = false) { if (!$force_refresh) { $cached = get_transient(self::TOKEN_TRANSIENT); if (is_string($cached) && '' !== $cached) { return $cached; } } $login = self::accessible_docs_login_request(); if (empty($login['data']['accessToken'])) { return false; } $token = (string) $login['data']['accessToken']; $expires_in = isset($login['data']['expiresIn']) ? (int) $login['data']['expiresIn'] : 0; // Keep a safety margin; fall back to 15 minutes if the API does not say. $ttl = $expires_in > 120 ? $expires_in - 60 : 15 * MINUTE_IN_SECONDS; set_transient(self::TOKEN_TRANSIENT, $token, min($ttl, DAY_IN_SECONDS)); return $token; } /** * Forget the cached token (called when the credentials change). * * @return void */ public static function clear_access_token() { delete_transient(self::TOKEN_TRANSIENT); } /** * Authenticated JSON request. Logs in again once if the cached token was rejected. * * @param string $url Endpoint URL. * @param array $data Request body. * @param string $method HTTP method. * @return array Same shape as accessible_docs_api_request(); code 0 + error if login failed. */ private static function authorized_request($url, $data, $method) { foreach (array(false, true) as $force_refresh) { $token = self::get_access_token($force_refresh); if (!$token) { return array( 'success' => false, 'code' => 0, 'auth' => false, 'error' => __('Unable to retrieve access token', 'accessible-docs'), ); } $response = self::accessible_docs_api_request($url, $data, $method, array( "Authorization" => "Bearer " . $token, "Content-Type" => "application/json", ), 'json'); if (401 !== $response['code'] && 403 !== $response['code']) { return $response; } self::clear_access_token(); } return $response; } /** * Check the processing status of a document at AccessibleDocs. * * @param string $accessible_docs_file_id The ID of the document in the external system. * @return array { status: 'completed'|'in-process'|'not-found'|'error', error?: string } */ public static function accessible_docs_check_file_status($accessible_docs_file_id) { $url = ACCESSIBLE_DOCS_API_URL . "/v1/user/files/" . rawurlencode($accessible_docs_file_id); $response_data = self::authorized_request($url, [], 'GET'); if (404 === $response_data['code']) { return [ 'status' => 'not-found' ]; } if (!isset($response_data['data']['state'])) { return [ 'status' => 'error', 'error' => __('Failed to check status in the system', 'accessible-docs')]; } $status = ($response_data['data']['state'] === 'completed') ? 'completed' : 'in-process'; return [ 'status' => $status ]; } /** * Upload a file to the accessible docs API. * * @param string $email The email address for notifications. * @param int $file_id The WordPress attachment file ID. * @return array { status: 'success', data: array } or { status: 'error', error: string } */ public static function accessible_docs_upload_file($email, $file_id) { $file_path = get_attached_file($file_id); if (!$file_path || !file_exists($file_path)) { return [ 'status' => 'error', 'error' => __('The file you are trying to send was not found.', 'accessible-docs')]; } $data = [ 'name' => basename($file_path), 'externalNotification' => ['email' => $email], 'mime' => get_post_mime_type($file_id), 'processing' => 'auto', 'size' => filesize($file_path), 'tags' => [], ]; // Create the file entry $response_data = self::authorized_request(ACCESSIBLE_DOCS_API_URL . "/v1/user/files/", $data, 'POST'); if (empty($response_data['success']) || empty($response_data['data']['id']) || empty($response_data['data']['uploadUrl'])) { $error = (isset($response_data['auth']) && false === $response_data['auth']) ? __('Unable to retrieve access token', 'accessible-docs') : __('File failed to upload', 'accessible-docs'); return [ 'status' => 'error', 'error' => $error ]; } // Upload the file content if (!self::accessible_docs_upload_file_to_server($file_id, $response_data['data']['uploadUrl'])) { return [ 'status' => 'error', 'error' => __('File failed to upload', 'accessible-docs') ]; } return ['status' => 'success', 'data' => $response_data['data']]; } /** * Upload the file to the accessible docs server using the provided upload URL. * * @param int $file_id The WordPress attachment file ID. * @param string $url The URL to upload the file to. * @return bool True if the upload was successful, false otherwise. */ public static function accessible_docs_upload_file_to_server($file_id, $url) { // Get the local file path $file_path = get_attached_file($file_id); if (!$file_path || !file_exists($file_path)) { return false; // Return false if the file is not found } // Read file content $file_data = file_get_contents($file_path); if ($file_data === false) { return false; } // Get MIME type $mime_type = get_post_mime_type($file_id); // Set request headers $headers = [ "Content-Type" => $mime_type, ]; // Upload the file using PUT request $response = self::accessible_docs_api_request($url, $file_data, 'PUT', $headers, 'not-json'); return !empty($response['success']); } }