<?php
/**
* Figma REST API client.
*
* Uses wp_remote_get/wp_remote_post for HTTP so it respects WordPress proxy
* settings and SSL configuration. Implements a simple cooldown between
* requests to stay under Figma's 30 req/min rate limit.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class FTE_Figma_Client {
private string $token;
private string $base_url = 'https://api.figma.com';
private float $last_request_time = 0;
private int $min_interval_ms = 2100; // ~28 req/min
public function __construct( string $token ) {
$this->token = $token;
}
// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------
/**
* Get the current user's profile (for token validation).
*
* @return array|WP_Error { id, handle, email, img_url }
*/
public function get_me() {
$url = $this->base_url . '/v1/me';
return $this->request( $url );
}
/**
* Fetch a full Figma file.
*
* @param string $file_key Figma file key.
* @param int|null $depth Optional tree depth limit.
* @return array|WP_Error Decoded JSON or error.
*/
public function get_file( string $file_key, ?int $depth = null ) {
$params = [];
if ( $depth !== null ) {
$params['depth'] = $depth;
}
$url = $this->base_url . '/v1/files/' . urlencode( $file_key ) . '?' . http_build_query( $params );
return $this->request( $url );
}
/**
* Fetch specific nodes from a file.
*
* @param string $file_key Figma file key.
* @param string[] $node_ids Array of node IDs.
* @return array|WP_Error
*/
public function get_nodes( string $file_key, array $node_ids ) {
$params = [
'ids' => implode( ',', $node_ids ),
];
$url = $this->base_url . '/v1/files/' . urlencode( $file_key ) . '/nodes?' . http_build_query( $params );
return $this->request( $url );
}
/**
* Export nodes as images.
*
* @param string $file_key Figma file key.
* @param string[] $node_ids Node IDs to export.
* @param string $format png|svg|jpg|pdf.
* @param float $scale Export scale (1–4).
* @return array Map of nodeId => URL.
*/
public function export_images( string $file_key, array $node_ids, string $format = 'png', float $scale = 2.0 ): array {
$all_urls = [];
// Figma caps at 100 IDs per request
$batches = array_chunk( $node_ids, 100 );
foreach ( $batches as $batch ) {
$params = [
'ids' => implode( ',', $batch ),
'format' => $format,
'scale' => $scale,
];
$url = $this->base_url . '/v1/images/' . urlencode( $file_key ) . '?' . http_build_query( $params );
$result = $this->request( $url );
if ( is_wp_error( $result ) ) {
continue; // Skip failed batches, log if needed
}
foreach ( ( $result['images'] ?? [] ) as $id => $image_url ) {
if ( $image_url ) {
$all_urls[ $id ] = $image_url;
}
}
}
return $all_urls;
}
/**
* Get permanent URLs for image fills (uploaded assets).
*
* @param string $file_key
* @return array Map of imageRef => URL.
*/
public function get_image_fills( string $file_key ): array {
$url = $this->base_url . '/v1/files/' . urlencode( $file_key ) . '/images';
$result = $this->request( $url );
if ( is_wp_error( $result ) ) {
return [];
}
return $result['meta']['images'] ?? [];
}
// -----------------------------------------------------------------------
// HTTP layer
// -----------------------------------------------------------------------
/**
* @param string $url Full API URL.
* @return array|WP_Error
*/
private function request( string $url ) {
$this->rate_limit_wait();
$response = wp_remote_get( $url, [
'headers' => [
'X-Figma-Token' => $this->token,
'Content-Type' => 'application/json',
],
'timeout' => 60,
] );
$this->last_request_time = microtime( true ) * 1000;
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
return new WP_Error(
'figma_api_error',
sprintf( 'Figma API returned HTTP %d: %s', $code, substr( $body, 0, 200 ) )
);
}
$decoded = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'figma_json_error', 'Invalid JSON from Figma API' );
}
return $decoded;
}
/**
* Simple rate limit: sleep if the last request was less than
* min_interval_ms ago.
*/
private function rate_limit_wait(): void {
if ( $this->last_request_time > 0 ) {
$now = microtime( true ) * 1000;
$elapsed = $now - $this->last_request_time;
if ( $elapsed < $this->min_interval_ms ) {
usleep( (int) ( ( $this->min_interval_ms - $elapsed ) * 1000 ) );
}
}
}
}