<?php
/**
* Plugin Name: Figma to Elementor
* Plugin URI: https://marketbiz.co.il
* Description: Converts Figma designs into Elementor legacy Section/Column templates.
* Version: 1.0.0
* Author: MarketBiz
* Author URI: https://marketbiz.co.il
* License: GPL-2.0+
* Text Domain: figma-to-elementor
* Requires PHP: 7.4
*
* This plugin fetches a Figma file via the REST API, converts the design
* into Elementor's legacy (Sections & Columns) JSON format, resolves image
* assets, and saves the result as an importable Elementor template.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'FTE_VERSION', '1.0.0' );
define( 'FTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'FTE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'FTE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
// ---------------------------------------------------------------------------
// Autoload includes
// ---------------------------------------------------------------------------
require_once FTE_PLUGIN_DIR . 'includes/class-figma-client.php';
require_once FTE_PLUGIN_DIR . 'includes/class-row-detector.php';
require_once FTE_PLUGIN_DIR . 'includes/class-widget-mapper.php';
require_once FTE_PLUGIN_DIR . 'includes/class-converter.php';
require_once FTE_PLUGIN_DIR . 'includes/class-elementor-emitter.php';
require_once FTE_PLUGIN_DIR . 'includes/class-image-resolver.php';
require_once FTE_PLUGIN_DIR . 'includes/class-template-importer.php';
require_once FTE_PLUGIN_DIR . 'includes/class-admin-page.php';
// ---------------------------------------------------------------------------
// Boot
// ---------------------------------------------------------------------------
/**
* Main plugin class — singleton.
*/
final class Figma_To_Elementor {
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'admin_menu', [ $this, 'register_admin_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'wp_ajax_fte_convert', [ $this, 'ajax_convert' ] );
add_action( 'wp_ajax_fte_fetch_pages', [ $this, 'ajax_fetch_pages' ] );
add_action( 'wp_ajax_fte_test_token', [ $this, 'ajax_test_token' ] );
add_action( 'wp_ajax_fte_diagnose', [ $this, 'ajax_diagnose' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
}
// -----------------------------------------------------------------------
// Admin menu
// -----------------------------------------------------------------------
public function register_admin_menu() {
add_menu_page(
__( 'Figma to Elementor', 'figma-to-elementor' ),
__( 'Figma → Elementor', 'figma-to-elementor' ),
'manage_options',
'figma-to-elementor',
[ $this, 'render_admin_page' ],
'dashicons-layout',
59
);
}
public function render_admin_page() {
FTE_Admin_Page::render();
}
// -----------------------------------------------------------------------
// Settings (stores the Figma token in wp_options)
// -----------------------------------------------------------------------
public function register_settings() {
register_setting( 'fte_settings_group', 'fte_figma_token', [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
] );
}
// -----------------------------------------------------------------------
// Admin assets
// -----------------------------------------------------------------------
public function enqueue_admin_assets( $hook ) {
if ( 'toplevel_page_figma-to-elementor' !== $hook ) {
return;
}
wp_enqueue_style(
'fte-admin',
FTE_PLUGIN_URL . 'assets/css/admin.css',
[],
FTE_VERSION
);
wp_enqueue_script(
'fte-admin',
FTE_PLUGIN_URL . 'assets/js/admin.js',
[ 'jquery' ],
FTE_VERSION,
true
);
wp_localize_script( 'fte-admin', 'fteAjax', [
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'fte_nonce' ),
] );
}
// -----------------------------------------------------------------------
// AJAX: Full diagnostic — tests every layer independently
// -----------------------------------------------------------------------
public function ajax_diagnose() {
check_ajax_referer( 'fte_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized', 403 );
}
$diag = [];
// 1. Check token is stored
$token = get_option( 'fte_figma_token', '' );
$diag['token_saved'] = ! empty( $token );
$diag['token_length'] = strlen( $token );
$diag['token_prefix'] = substr( $token, 0, 5 ) . '...';
// 2. Test basic outbound HTTPS (to a known endpoint)
$test_response = wp_remote_get( 'https://httpbin.org/get', [ 'timeout' => 10 ] );
if ( is_wp_error( $test_response ) ) {
$diag['outbound_https'] = 'FAIL: ' . $test_response->get_error_message();
} else {
$diag['outbound_https'] = 'OK (HTTP ' . wp_remote_retrieve_response_code( $test_response ) . ')';
}
// 3. Test connectivity to api.figma.com specifically
$figma_response = wp_remote_get( 'https://api.figma.com/v1/me', [
'timeout' => 15,
'headers' => [
'X-Figma-Token' => $token,
'Content-Type' => 'application/json',
],
] );
if ( is_wp_error( $figma_response ) ) {
$diag['figma_api'] = 'FAIL: ' . $figma_response->get_error_message();
$diag['figma_error_code'] = $figma_response->get_error_code();
$diag['figma_error_data'] = $figma_response->get_error_data();
} else {
$code = wp_remote_retrieve_response_code( $figma_response );
$body = wp_remote_retrieve_body( $figma_response );
$diag['figma_api'] = 'HTTP ' . $code;
$diag['figma_response_body'] = substr( $body, 0, 500 );
if ( $code === 200 ) {
$data = json_decode( $body, true );
$diag['figma_user'] = ( $data['handle'] ?? '?' ) . ' (' . ( $data['email'] ?? '?' ) . ')';
}
}
// 4. Test file access (if file_key provided)
$raw_input = sanitize_text_field( $_POST['file_key'] ?? '' );
if ( ! empty( $raw_input ) ) {
$parsed = self::parse_figma_input( $raw_input );
$diag['parsed_file_key'] = $parsed['file_key'];
$diag['parsed_node_id'] = $parsed['node_id'];
if ( ! empty( $parsed['file_key'] ) ) {
$file_url = 'https://api.figma.com/v1/files/' . urlencode( $parsed['file_key'] ) . '?depth=1';
$file_response = wp_remote_get( $file_url, [
'timeout' => 30,
'headers' => [
'X-Figma-Token' => $token,
'Content-Type' => 'application/json',
],
] );
if ( is_wp_error( $file_response ) ) {
$diag['file_fetch'] = 'FAIL: ' . $file_response->get_error_message();
} else {
$fcode = wp_remote_retrieve_response_code( $file_response );
$fbody = wp_remote_retrieve_body( $file_response );
$diag['file_fetch'] = 'HTTP ' . $fcode;
$diag['file_response'] = substr( $fbody, 0, 300 );
}
}
}
// 5. PHP/server info
$diag['php_version'] = PHP_VERSION;
$diag['wp_version'] = get_bloginfo( 'version' );
$diag['curl_installed'] = function_exists( 'curl_version' ) ? curl_version()['version'] : 'NOT AVAILABLE';
$diag['ssl_available'] = extension_loaded( 'openssl' ) ? 'Yes' : 'No';
wp_send_json_success( $diag );
}
// -----------------------------------------------------------------------
// AJAX: Test token — hits GET /v1/me to verify the token is valid
// -----------------------------------------------------------------------
public function ajax_test_token() {
check_ajax_referer( 'fte_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized', 403 );
}
$token = get_option( 'fte_figma_token', '' );
if ( empty( $token ) ) {
wp_send_json_error( 'No token saved. Enter your Figma token and click Save first.' );
}
$client = new FTE_Figma_Client( $token );
$result = $client->get_me();
if ( is_wp_error( $result ) ) {
$msg = $result->get_error_message();
// Give specific guidance based on error
if ( strpos( $msg, '403' ) !== false ) {
wp_send_json_error( 'Token rejected (HTTP 403). The token is invalid or expired. Generate a new one at figma.com → Settings → Personal Access Tokens.' );
}
wp_send_json_error( 'Connection failed: ' . $msg );
}
$email = $result['email'] ?? 'unknown';
$handle = $result['handle'] ?? 'unknown';
wp_send_json_success( [
'email' => $email,
'handle' => $handle,
'message' => sprintf( 'Token is valid! Connected as %s (%s).', $handle, $email ),
] );
}
// -----------------------------------------------------------------------
// AJAX: Fetch pages/frames from Figma file (for the dropdown)
// -----------------------------------------------------------------------
public function ajax_fetch_pages() {
check_ajax_referer( 'fte_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized', 403 );
}
$token = get_option( 'fte_figma_token', '' );
$raw_input = sanitize_text_field( $_POST['file_key'] ?? '' );
$parsed = self::parse_figma_input( $raw_input );
$file_key = $parsed['file_key'];
if ( empty( $token ) || empty( $file_key ) ) {
wp_send_json_error( 'Token and file key are required. Enter a Figma file key or paste a full Figma URL.' );
}
$client = new FTE_Figma_Client( $token );
$result = $client->get_file( $file_key, 2 ); // depth=2 for pages+frame names only
if ( is_wp_error( $result ) ) {
$msg = $result->get_error_message();
// Give actionable guidance for common errors
if ( strpos( $msg, '404' ) !== false ) {
wp_send_json_error(
'File not found (HTTP 404). This usually means: (1) The file key is wrong — check your URL. '
. '(2) Your Figma token does not have access to this file — the file may be in a different team/org. '
. '(3) The token is expired — try generating a new one. '
. 'Use the "Test Connection" button to verify your token works.'
);
}
if ( strpos( $msg, '403' ) !== false ) {
wp_send_json_error( 'Access denied (HTTP 403). Your token is invalid or expired. Generate a new one at figma.com → Settings → Personal Access Tokens.' );
}
wp_send_json_error( $msg );
}
// Extract page → frame list
$pages = [];
$document = $result['document'] ?? [];
foreach ( ( $document['children'] ?? [] ) as $page ) {
$frames = [];
foreach ( ( $page['children'] ?? [] ) as $frame ) {
$frames[] = [
'id' => $frame['id'],
'name' => $frame['name'],
];
}
$pages[] = [
'id' => $page['id'],
'name' => $page['name'],
'frames' => $frames,
];
}
wp_send_json_success( [
'file_name' => $result['name'] ?? 'Untitled',
'pages' => $pages,
'parsed_file_key' => $file_key,
'parsed_node_id' => $parsed['node_id'] ?? '',
] );
}
// -----------------------------------------------------------------------
// URL / File Key Parser
// -----------------------------------------------------------------------
/**
* Extract file_key and optional node_id from a raw user input.
*
* Handles all Figma URL formats:
* https://www.figma.com/file/XXXXXX/Name
* https://www.figma.com/design/XXXXXX/Name
* https://www.figma.com/proto/XXXXXX/Name?node-id=1-2
* https://figma.com/file/XXXXXX/...
* or just a bare file key: XXXXXX
*
* Node IDs in URLs use dashes (1-2) but the API uses colons (1:2).
*/
public static function parse_figma_input( string $input ): array {
$input = trim( $input );
$result = [
'file_key' => '',
'node_id' => '',
];
// Pattern: Figma URL containing /file/, /design/, or /proto/
if ( preg_match( '#figma\.com/(?:file|design|proto)/([a-zA-Z0-9]+)#', $input, $m ) ) {
$result['file_key'] = $m[1];
// Extract node-id query param if present (URL uses dashes: 1-2)
if ( preg_match( '#[?&]node-id=([0-9]+-[0-9]+)#', $input, $nm ) ) {
// Convert URL dash format (1-2) to API colon format (1:2)
$result['node_id'] = str_replace( '-', ':', $nm[1] );
}
} else {
// Assume bare file key — strip any whitespace or trailing slashes
$result['file_key'] = preg_replace( '/[^a-zA-Z0-9]/', '', $input );
}
return $result;
}
// -----------------------------------------------------------------------
// AJAX: Run the full conversion pipeline
// -----------------------------------------------------------------------
public function ajax_convert() {
check_ajax_referer( 'fte_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized', 403 );
}
$token = get_option( 'fte_figma_token', '' );
$raw_input = sanitize_text_field( $_POST['file_key'] ?? '' );
$parsed = self::parse_figma_input( $raw_input );
$file_key = $parsed['file_key'];
$node_id = sanitize_text_field( $_POST['node_id'] ?? '' );
$title = sanitize_text_field( $_POST['title'] ?? 'Imported from Figma' );
// If no node_id from the dropdown, try the one parsed from the URL
if ( empty( $node_id ) && ! empty( $parsed['node_id'] ) ) {
$node_id = $parsed['node_id'];
}
if ( empty( $token ) || empty( $file_key ) ) {
wp_send_json_error( 'Token and file key are required.' );
}
// Increase time limit for large files
set_time_limit( 120 );
try {
$client = new FTE_Figma_Client( $token );
// Step 1: Fetch the target node
if ( ! empty( $node_id ) ) {
$node_data = $client->get_nodes( $file_key, [ $node_id ] );
if ( is_wp_error( $node_data ) ) {
wp_send_json_error( 'Figma API error: ' . $node_data->get_error_message() );
}
$target = $node_data['nodes'][ $node_id ]['document'] ?? null;
} else {
$file_data = $client->get_file( $file_key );
if ( is_wp_error( $file_data ) ) {
wp_send_json_error( 'Figma API error: ' . $file_data->get_error_message() );
}
// Default: first frame on first page
$target = $file_data['document']['children'][0]['children'][0] ?? null;
}
if ( ! $target ) {
wp_send_json_error( 'Could not find the target frame in the Figma file.' );
}
// Step 2: Convert
$converter = new FTE_Converter();
$logical_rows = $converter->convert( $target );
// Step 3: Emit Elementor JSON
$emitter = new FTE_Elementor_Emitter();
$template = $emitter->emit_template( $logical_rows, $title );
// Step 4: Resolve images
$resolver = new FTE_Image_Resolver( $client, $file_key );
$resolved = $resolver->resolve( $template );
// Step 5: Validate
$errors = $emitter->validate( $template );
// Step 6: Import into Elementor
$importer = new FTE_Template_Importer();
$template_id = $importer->import( $template, $title );
if ( is_wp_error( $template_id ) ) {
wp_send_json_error( 'Import failed: ' . $template_id->get_error_message() );
}
wp_send_json_success( [
'template_id' => $template_id,
'edit_url' => admin_url( "post.php?post={$template_id}&action=elementor" ),
'sections' => count( $template['content'] ),
'images_resolved' => count( $resolved ),
'validation_errors' => $errors,
] );
} catch ( \Exception $e ) {
wp_send_json_error( 'Conversion error: ' . $e->getMessage() );
}
}
}
// Initialize
Figma_To_Elementor::instance();