'RS256', 'typ' => 'JWT' ] ) ); $claims = self::base64url_encode( wp_json_encode( [ 'iss' => $creds['client_email'], 'scope' => self::SCOPE, 'aud' => self::TOKEN_URL, 'iat' => $now, 'exp' => $now + 3600, ] ) ); $signing_input = $header . '.' . $claims; $signature = ''; $key = openssl_pkey_get_private( $creds['private_key'] ); if ( ! $key || ! openssl_sign( $signing_input, $signature, $key, OPENSSL_ALGO_SHA256 ) ) { return new \WP_Error( 'chargeforms_sheets_bad_credentials', __( 'Could not sign a request with this service account\'s private key — the JSON key may be corrupted.', 'chargeforms' ) ); } $jwt = $signing_input . '.' . self::base64url_encode( $signature ); $response = wp_remote_post( self::TOKEN_URL, [ 'timeout' => 12, 'body' => [ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt, ], ] ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code >= 400 || empty( $data['access_token'] ) ) { $message = is_array( $data ) && ! empty( $data['error_description'] ) ? $data['error_description'] : __( 'Google rejected this service account (check that the Sheets API is enabled for this project).', 'chargeforms' ); return new \WP_Error( 'chargeforms_sheets_auth_failed', $message, [ 'status' => $code ] ); } // 60s safety margin so a token already mid-flight to Google never expires between the cache check and the actual API call. set_transient( $cache_key, $data['access_token'], max( 60, (int) ( $data['expires_in'] ?? 3600 ) - 60 ) ); return $data['access_token']; } private static function request( $access_token, $method, $path, $body = null ) { $args = [ 'method' => $method, 'timeout' => 12, 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ]; if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( 'https://sheets.googleapis.com/v4/spreadsheets' . $path, $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code >= 400 ) { $message = is_array( $data ) && ! empty( $data['error']['message'] ) ? $data['error']['message'] : sprintf( /* translators: %d: HTTP status code */ __( 'Google Sheets returned an unexpected error (HTTP %d).', 'chargeforms' ), $code ); return new \WP_Error( 'chargeforms_sheets_api_error', $message, [ 'status' => $code, 'body' => $data ] ); } return is_array( $data ) ? $data : []; } /** Connection test — confirms the token works AND the spreadsheet is actually shared with this service account (a very common setup mistake: creating the account but forgetting to share the sheet). */ public static function ping( $access_token, $spreadsheet_id ) { $result = self::request( $access_token, 'GET', '/' . rawurlencode( $spreadsheet_id ) . '?fields=spreadsheetId' ); return is_wp_error( $result ) ? $result : true; } /** @return array|\WP_Error Row arrays, or [] if the range is empty. */ public static function get_values( $access_token, $spreadsheet_id, $range ) { $result = self::request( $access_token, 'GET', '/' . rawurlencode( $spreadsheet_id ) . '/values/' . rawurlencode( $range ) ); if ( is_wp_error( $result ) ) { return $result; } return $result['values'] ?? []; } /** Overwrites the given range verbatim — used only to write the one-time header row, never submission data (which always goes through append_row()). */ public static function update_values( $access_token, $spreadsheet_id, $range, array $rows ) { $result = self::request( $access_token, 'PUT', '/' . rawurlencode( $spreadsheet_id ) . '/values/' . rawurlencode( $range ) . '?valueInputOption=USER_ENTERED', [ 'values' => $rows ] ); return is_wp_error( $result ) ? $result : true; } /** Appends after the last row with data in `$range`'s sheet — Sheets' own table-detection, not a row number ChargeForms has to track itself. */ public static function append_row( $access_token, $spreadsheet_id, $range, array $row ) { $result = self::request( $access_token, 'POST', '/' . rawurlencode( $spreadsheet_id ) . '/values/' . rawurlencode( $range ) . ':append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS', [ 'values' => [ $row ] ] ); return is_wp_error( $result ) ? $result : true; } }