Plugin Directory

Changeset 3445250


Ignore:
Timestamp:
01/23/2026 01:56:44 AM (2 months ago)
Author:
fahdi
Message:

Release v3.5.1: Caching Improvements

Location:
tablecrafter-wp-data-tables
Files:
3 added
8 edited
24 copied

Legend:

Unmodified
Added
Removed
  • tablecrafter-wp-data-tables/tags/3.5.0/assets/js/admin.js

    r3441951 r3445250  
    8080        });
    8181    }
     82
     83    // --- Airtable Integration (v3.5.0) ---
     84    const atBtn = document.getElementById('tc-airtable-btn');
     85    const atModal = document.getElementById('tc-airtable-modal');
     86    const atCancel = document.getElementById('tc-at-cancel');
     87    const atSave = document.getElementById('tc-at-save');
     88
     89    if (atBtn && atModal) {
     90        // Open Modal
     91        atBtn.addEventListener('click', function(e) {
     92            e.preventDefault();
     93            atModal.style.display = 'block';
     94        });
     95
     96        // Close Modal
     97        const closeModal = () => { atModal.style.display = 'none'; };
     98        atCancel.addEventListener('click', closeModal);
     99       
     100        // Close on overlay click
     101        atModal.querySelector('.tc-modal-overlay').addEventListener('click', closeModal);
     102
     103        // Save & Connect
     104        atSave.addEventListener('click', function() {
     105            const baseId = document.getElementById('tc-at-base').value.trim();
     106            const tableName = document.getElementById('tc-at-table').value.trim();
     107            const viewName = document.getElementById('tc-at-view').value.trim();
     108            const token = document.getElementById('tc-at-token').value.trim();
     109
     110            if (!baseId || !tableName || !token) {
     111                alert('Please fill in Base ID, Table Name, and Personal Access Token.');
     112                return;
     113            }
     114
     115            // 1. Save Token Securely via AJAX
     116            const originalText = atSave.textContent;
     117            atSave.textContent = 'Saving...';
     118            atSave.disabled = true;
     119
     120            const formData = new FormData();
     121            formData.append('action', 'tc_save_airtable_token');
     122            formData.append('token', token);
     123            formData.append('nonce', tablecrafterAdmin.nonce);
     124
     125            fetch(tablecrafterAdmin.ajaxUrl, {
     126                method: 'POST',
     127                body: formData
     128            })
     129            .then(response => response.json())
     130            .then(result => {
     131                if (result.success) {
     132                    // 2. Generate Secure URL (Token is stored server-side)
     133                    let url = `airtable://${baseId}/${tableName}`;
     134                    if (viewName) {
     135                        url += `?view=${encodeURIComponent(viewName)}`;
     136                    }
     137
     138                    // 3. Update Input & Preview
     139                    urlInput.value = url;
     140                    urlInput.dispatchEvent(new Event('input'));
     141                   
     142                    closeModal();
     143                   
     144                    // Clear sensitive input
     145                    document.getElementById('tc-at-token').value = '';
     146                   
     147                    setTimeout(() => previewBtn.click(), 100);
     148                } else {
     149                    alert('Error saving token: ' + (result.data.message || 'Unknown error'));
     150                }
     151            })
     152            .catch(error => {
     153                console.error('Airtable Setup Error:', error);
     154                alert('Network error. Please try again.');
     155            })
     156            .finally(() => {
     157                atSave.textContent = originalText;
     158                atSave.disabled = false;
     159            });
     160        });
     161    }
     162    // -------------------------------------
    82163    // ---------------------------------------------
    83164
  • tablecrafter-wp-data-tables/tags/3.5.0/includes/class-tc-data-fetcher.php

    r3445233 r3445250  
    9494        }
    9595
     96        // Check for Airtable source (airtable:// protocol)
     97        if (strpos($url, 'airtable://') === 0) {
     98            $result = $this->fetch_airtable($url);
     99        }
    96100        // Check for CSV / Google Sheets
    97         $is_google_sheet = preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url);
    98         $is_csv_ext = (substr($url, -4) === '.csv');
    99 
    100         if ($is_google_sheet || $is_csv_ext) {
     101        elseif (preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url) || substr($url, -4) === '.csv') {
    101102            $result = TC_CSV_Source::fetch($url);
    102103        } else {
     
    126127
    127128        // Check if URL is local
    128         if (strpos($url, $site_url) !== 0 &&
     129        if (
     130            strpos($url, $site_url) !== 0 &&
    129131            strpos($url, $home_url) !== 0 &&
    130             strpos($url, $plugin_url) !== 0) {
     132            strpos($url, $plugin_url) !== 0
     133        ) {
    131134            return false;
    132135        }
     
    202205
    203206        return false;
     207    }
     208
     209    /**
     210     * Fetch data from Airtable source
     211     *
     212     * Parses airtable:// URL and delegates to TC_Airtable_Source.
     213     *
     214     * @param string $url Airtable URL (airtable://baseId/tableName?token=xxx)
     215     * @return array|WP_Error Parsed data or error
     216     */
     217    private function fetch_airtable(string $url)
     218    {
     219        // Parse the Airtable URL
     220        $parsed = TC_Airtable_Source::parse_url($url);
     221
     222        if (is_wp_error($parsed)) {
     223            $this->log_error('Airtable URL Parse Error', array('url' => $url));
     224            return $parsed;
     225        }
     226
     227        // Token is required
     228        if (empty($parsed['token'])) {
     229            // Try to get token from saved settings
     230            $saved_token = get_option('tablecrafter_airtable_token', '');
     231            if (!empty($saved_token)) {
     232                $parsed['token'] = $this->security->decrypt_token($saved_token);
     233            }
     234        }
     235
     236        if (empty($parsed['token'])) {
     237            return new WP_Error(
     238                'airtable_no_token',
     239                __('Airtable Personal Access Token is required.', 'tablecrafter-wp-data-tables')
     240            );
     241        }
     242
     243        // Build optional params
     244        $params = [];
     245        if (!empty($parsed['view'])) {
     246            $params['view'] = $parsed['view'];
     247        }
     248
     249        // Fetch from Airtable
     250        $result = TC_Airtable_Source::fetch(
     251            $parsed['base_id'],
     252            $parsed['table_name'],
     253            $parsed['token'],
     254            $params
     255        );
     256
     257        if (is_wp_error($result)) {
     258            $this->log_error('Airtable Fetch Error', array(
     259                'base_id' => $parsed['base_id'],
     260                'table' => $parsed['table_name'],
     261                'error' => $result->get_error_message()
     262            ));
     263        }
     264
     265        return $result;
    204266    }
    205267
  • tablecrafter-wp-data-tables/tags/3.5.0/includes/class-tc-security.php

    r3445233 r3445250  
    214214        $proxy_headers = array(
    215215            'cloudflare' => 'HTTP_CF_CONNECTING_IP',
    216             'forwarded'  => 'HTTP_X_FORWARDED_FOR',
    217             'real_ip'    => 'HTTP_X_REAL_IP',
     216            'forwarded' => 'HTTP_X_FORWARDED_FOR',
     217            'real_ip' => 'HTTP_X_REAL_IP',
    218218        );
    219219
     
    276276        }
    277277    }
     278
     279    /**
     280     * Encrypt a token for secure storage
     281     *
     282     * Uses WordPress salt for encryption key.
     283     *
     284     * @param string $token Plain text token
     285     * @return string Encrypted token (base64 encoded)
     286     */
     287    public function encrypt_token(string $token): string
     288    {
     289        if (empty($token)) {
     290            return '';
     291        }
     292
     293        $key = $this->get_encryption_key();
     294        $iv = openssl_random_pseudo_bytes(16);
     295
     296        $encrypted = openssl_encrypt(
     297            $token,
     298            'AES-256-CBC',
     299            $key,
     300            OPENSSL_RAW_DATA,
     301            $iv
     302        );
     303
     304        if ($encrypted === false) {
     305            return '';
     306        }
     307
     308        // Combine IV and encrypted data, then base64 encode
     309        return base64_encode($iv . $encrypted);
     310    }
     311
     312    /**
     313     * Decrypt a stored token
     314     *
     315     * @param string $encrypted_token Base64 encoded encrypted token
     316     * @return string Decrypted token
     317     */
     318    public function decrypt_token(string $encrypted_token): string
     319    {
     320        if (empty($encrypted_token)) {
     321            return '';
     322        }
     323
     324        $key = $this->get_encryption_key();
     325        $data = base64_decode($encrypted_token);
     326
     327        if ($data === false || strlen($data) < 17) {
     328            return '';
     329        }
     330
     331        // Extract IV (first 16 bytes) and encrypted data
     332        $iv = substr($data, 0, 16);
     333        $encrypted = substr($data, 16);
     334
     335        $decrypted = openssl_decrypt(
     336            $encrypted,
     337            'AES-256-CBC',
     338            $key,
     339            OPENSSL_RAW_DATA,
     340            $iv
     341        );
     342
     343        return $decrypted !== false ? $decrypted : '';
     344    }
     345
     346    /**
     347     * Get encryption key from WordPress salts
     348     *
     349     * @return string 32-byte encryption key
     350     */
     351    private function get_encryption_key(): string
     352    {
     353        // Use WordPress AUTH_KEY salt, hash to 32 bytes for AES-256
     354        $salt = defined('AUTH_KEY') ? AUTH_KEY : 'tablecrafter-default-salt';
     355        return hash('sha256', $salt, true);
     356    }
    278357}
  • tablecrafter-wp-data-tables/tags/3.5.0/readme.txt

    r3445233 r3445250  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 3.4.0
     6Stable tag: 3.5.0
    77Requires PHP: 7.4
    88License: GPLv2 or later
  • tablecrafter-wp-data-tables/tags/3.5.0/tablecrafter.php

    r3445233 r3445250  
    44 * Plugin URI: https://github.com/TableCrafter/wp-data-tables
    55 * Description: Transform any data source into responsive WordPress tables. WCAG 2.1 compliant, advanced export (Excel/PDF), keyboard navigation, screen readers.
    6  * Version: 3.4.0
     6 * Version: 3.5.0
    77 * Author: TableCrafter Team
    88 * Author URI: https://github.com/fahdi
     
    1919 * Global Constants
    2020 */
    21 define('TABLECRAFTER_VERSION', '3.4.0');
     21define('TABLECRAFTER_VERSION', '3.5.0');
    2222define('TABLECRAFTER_URL', plugin_dir_url(__FILE__));
    2323define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__));
     
    139139        add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
    140140        add_action('wp_ajax_nopriv_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
     141
     142        // Airtable Token Handler [v3.5.0]
     143        add_action('wp_ajax_tc_save_airtable_token', array($this, 'ajax_save_airtable_token'));
    141144
    142145        if (!wp_next_scheduled('tc_refresher_cron')) {
     
    197200        $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv';
    198201        $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0';
     202        $airtable_demo_url = 'airtable://appSampleBaseId/Employees';
    199203        ?>
    200204        <div class="wrap">
     
    242246                                    data-url="<?php echo esc_url($employees_url); ?>">📊
    243247                                    <?php esc_html_e('Employee List (CSV)', 'tablecrafter-wp-data-tables'); ?></a></li>
    244                             <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     248                            <li style="margin-bottom: 8px;"><a href="#" class="button button-large"
    245249                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
    246250                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     
    248252                                    data-url="<?php echo esc_url($metrics_url); ?>">📈
    249253                                    <?php esc_html_e('Sales Metrics (JSON)', 'tablecrafter-wp-data-tables'); ?></a></li>
     254                            <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     255                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
     256                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     257                                    onmouseout="this.style.background='white'; this.style.color='#0e7490'"
     258                                    data-url="<?php echo esc_url($airtable_demo_url); ?>">🔮
     259                                    <?php esc_html_e('Airtable Base (API)', 'tablecrafter-wp-data-tables'); ?></a></li>
    250260                        </ul>
    251261                        <div
     
    280290                                        style="margin-right: 4px; vertical-align: middle;"></span>
    281291                                    <?php esc_html_e('Google Sheets', 'tablecrafter-wp-data-tables'); ?>
     292                                </button>
     293                                <button id="tc-airtable-btn" class="button button-secondary" type="button"
     294                                    title="<?php esc_attr_e('Connect to Airtable Base', 'tablecrafter-wp-data-tables'); ?>">
     295                                    <span class="dashicons dashicons-database"
     296                                        style="margin-right: 4px; vertical-align: middle;"></span>
     297                                    <?php esc_html_e('Airtable', 'tablecrafter-wp-data-tables'); ?>
    282298                                </button>
    283299                            </div>
     
    515531                array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'),
    516532                array('label' => __('📋 Google Sheets Example', 'tablecrafter-wp-data-tables'), 'value' => 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'),
     533                array('label' => __('🔮 Airtable Base (API)', 'tablecrafter-wp-data-tables'), 'value' => 'airtable://appSampleBaseId/Employees'),
    517534            )
    518535        ));
     
    678695        <?php
    679696        return ob_get_clean();
     697    }
     698
     699    /**
     700     * AJAX Handler: Save Airtable Token securely
     701     */
     702    public function ajax_save_airtable_token()
     703    {
     704        // Check permissions and nonce
     705        if (!current_user_can('manage_options') || !check_ajax_referer('tc_proxy_nonce', 'nonce', false)) {
     706            wp_send_json_error(array('message' => 'Permission denied.'));
     707        }
     708
     709        $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : '';
     710
     711        if (empty($token)) {
     712            wp_send_json_error(array('message' => 'Token is required.'));
     713        }
     714
     715        // Encrypt and store
     716        $security = TC_Security::get_instance();
     717        $encrypted = $security->encrypt_token($token);
     718
     719        if (empty($encrypted)) {
     720            wp_send_json_error(array('message' => 'Encryption failed.'));
     721        }
     722
     723        update_option('tablecrafter_airtable_token', $encrypted);
     724
     725        wp_send_json_success(array('message' => 'Token saved securely.'));
    680726    }
    681727
  • tablecrafter-wp-data-tables/tags/3.5.1/assets/js/admin.js

    r3441951 r3445250  
    8080        });
    8181    }
     82
     83    // --- Airtable Integration (v3.5.0) ---
     84    const atBtn = document.getElementById('tc-airtable-btn');
     85    const atModal = document.getElementById('tc-airtable-modal');
     86    const atCancel = document.getElementById('tc-at-cancel');
     87    const atSave = document.getElementById('tc-at-save');
     88
     89    if (atBtn && atModal) {
     90        // Open Modal
     91        atBtn.addEventListener('click', function(e) {
     92            e.preventDefault();
     93            atModal.style.display = 'block';
     94        });
     95
     96        // Close Modal
     97        const closeModal = () => { atModal.style.display = 'none'; };
     98        atCancel.addEventListener('click', closeModal);
     99       
     100        // Close on overlay click
     101        atModal.querySelector('.tc-modal-overlay').addEventListener('click', closeModal);
     102
     103        // Save & Connect
     104        atSave.addEventListener('click', function() {
     105            const baseId = document.getElementById('tc-at-base').value.trim();
     106            const tableName = document.getElementById('tc-at-table').value.trim();
     107            const viewName = document.getElementById('tc-at-view').value.trim();
     108            const token = document.getElementById('tc-at-token').value.trim();
     109
     110            if (!baseId || !tableName || !token) {
     111                alert('Please fill in Base ID, Table Name, and Personal Access Token.');
     112                return;
     113            }
     114
     115            // 1. Save Token Securely via AJAX
     116            const originalText = atSave.textContent;
     117            atSave.textContent = 'Saving...';
     118            atSave.disabled = true;
     119
     120            const formData = new FormData();
     121            formData.append('action', 'tc_save_airtable_token');
     122            formData.append('token', token);
     123            formData.append('nonce', tablecrafterAdmin.nonce);
     124
     125            fetch(tablecrafterAdmin.ajaxUrl, {
     126                method: 'POST',
     127                body: formData
     128            })
     129            .then(response => response.json())
     130            .then(result => {
     131                if (result.success) {
     132                    // 2. Generate Secure URL (Token is stored server-side)
     133                    let url = `airtable://${baseId}/${tableName}`;
     134                    if (viewName) {
     135                        url += `?view=${encodeURIComponent(viewName)}`;
     136                    }
     137
     138                    // 3. Update Input & Preview
     139                    urlInput.value = url;
     140                    urlInput.dispatchEvent(new Event('input'));
     141                   
     142                    closeModal();
     143                   
     144                    // Clear sensitive input
     145                    document.getElementById('tc-at-token').value = '';
     146                   
     147                    setTimeout(() => previewBtn.click(), 100);
     148                } else {
     149                    alert('Error saving token: ' + (result.data.message || 'Unknown error'));
     150                }
     151            })
     152            .catch(error => {
     153                console.error('Airtable Setup Error:', error);
     154                alert('Network error. Please try again.');
     155            })
     156            .finally(() => {
     157                atSave.textContent = originalText;
     158                atSave.disabled = false;
     159            });
     160        });
     161    }
     162    // -------------------------------------
    82163    // ---------------------------------------------
    83164
  • tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-cache.php

    r3445233 r3445250  
    138138     * @param string $cache_key Cache key
    139139     * @param mixed $data Data to cache
     140     * @param int $ttl Cache TTL in seconds (optional)
    140141     * @return bool Success
    141142     */
    142     public function set_data_cache(string $cache_key, $data): bool
    143     {
    144         return set_transient($cache_key, $data, self::DATA_CACHE_TTL);
     143    public function set_data_cache(string $cache_key, $data, int $ttl = self::DATA_CACHE_TTL): bool
     144    {
     145        return set_transient($cache_key, $data, $ttl);
    145146    }
    146147
  • tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-data-fetcher.php

    r3445233 r3445250  
    9494        }
    9595
     96        // Check for Airtable source (airtable:// protocol)
     97        if (strpos($url, 'airtable://') === 0) {
     98            $result = $this->fetch_airtable($url);
     99        }
    96100        // Check for CSV / Google Sheets
    97         $is_google_sheet = preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url);
    98         $is_csv_ext = (substr($url, -4) === '.csv');
    99 
    100         if ($is_google_sheet || $is_csv_ext) {
     101        elseif (preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url) || substr($url, -4) === '.csv') {
    101102            $result = TC_CSV_Source::fetch($url);
    102103        } else {
     
    106107        // Cache successful results
    107108        if ($use_cache && !is_wp_error($result) && !empty($result)) {
    108             $this->cache->set_data_cache($this->cache->get_data_cache_key($url), $result);
     109            // Determine TTL based on source type
     110            $ttl = HOUR_IN_SECONDS;
     111
     112            // Airtable requires stricter caching (5 minutes) due to rate limits
     113            if (strpos($url, 'airtable://') === 0) {
     114                $ttl = 5 * MINUTE_IN_SECONDS;
     115            }
     116
     117            $this->cache->set_data_cache($this->cache->get_data_cache_key($url), $result, $ttl);
    109118            $this->cache->track_url($url);
    110119        }
     
    126135
    127136        // Check if URL is local
    128         if (strpos($url, $site_url) !== 0 &&
     137        if (
     138            strpos($url, $site_url) !== 0 &&
    129139            strpos($url, $home_url) !== 0 &&
    130             strpos($url, $plugin_url) !== 0) {
     140            strpos($url, $plugin_url) !== 0
     141        ) {
    131142            return false;
    132143        }
     
    202213
    203214        return false;
     215    }
     216
     217    /**
     218     * Fetch data from Airtable source
     219     *
     220     * Parses airtable:// URL and delegates to TC_Airtable_Source.
     221     *
     222     * @param string $url Airtable URL (airtable://baseId/tableName?token=xxx)
     223     * @return array|WP_Error Parsed data or error
     224     */
     225    private function fetch_airtable(string $url)
     226    {
     227        // Parse the Airtable URL
     228        $parsed = TC_Airtable_Source::parse_url($url);
     229
     230        if (is_wp_error($parsed)) {
     231            $this->log_error('Airtable URL Parse Error', array('url' => $url));
     232            return $parsed;
     233        }
     234
     235        // Token is required
     236        if (empty($parsed['token'])) {
     237            // Try to get token from saved settings
     238            $saved_token = get_option('tablecrafter_airtable_token', '');
     239            if (!empty($saved_token)) {
     240                $parsed['token'] = $this->security->decrypt_token($saved_token);
     241            }
     242        }
     243
     244        if (empty($parsed['token'])) {
     245            return new WP_Error(
     246                'airtable_no_token',
     247                __('Airtable Personal Access Token is required.', 'tablecrafter-wp-data-tables')
     248            );
     249        }
     250
     251        // Build optional params
     252        $params = [];
     253        if (!empty($parsed['view'])) {
     254            $params['view'] = $parsed['view'];
     255        }
     256
     257        // Fetch from Airtable
     258        $result = TC_Airtable_Source::fetch(
     259            $parsed['base_id'],
     260            $parsed['table_name'],
     261            $parsed['token'],
     262            $params
     263        );
     264
     265        if (is_wp_error($result)) {
     266            $this->log_error('Airtable Fetch Error', array(
     267                'base_id' => $parsed['base_id'],
     268                'table' => $parsed['table_name'],
     269                'error' => $result->get_error_message()
     270            ));
     271        }
     272
     273        return $result;
    204274    }
    205275
  • tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-security.php

    r3445233 r3445250  
    214214        $proxy_headers = array(
    215215            'cloudflare' => 'HTTP_CF_CONNECTING_IP',
    216             'forwarded'  => 'HTTP_X_FORWARDED_FOR',
    217             'real_ip'    => 'HTTP_X_REAL_IP',
     216            'forwarded' => 'HTTP_X_FORWARDED_FOR',
     217            'real_ip' => 'HTTP_X_REAL_IP',
    218218        );
    219219
     
    276276        }
    277277    }
     278
     279    /**
     280     * Encrypt a token for secure storage
     281     *
     282     * Uses WordPress salt for encryption key.
     283     *
     284     * @param string $token Plain text token
     285     * @return string Encrypted token (base64 encoded)
     286     */
     287    public function encrypt_token(string $token): string
     288    {
     289        if (empty($token)) {
     290            return '';
     291        }
     292
     293        $key = $this->get_encryption_key();
     294        $iv = openssl_random_pseudo_bytes(16);
     295
     296        $encrypted = openssl_encrypt(
     297            $token,
     298            'AES-256-CBC',
     299            $key,
     300            OPENSSL_RAW_DATA,
     301            $iv
     302        );
     303
     304        if ($encrypted === false) {
     305            return '';
     306        }
     307
     308        // Combine IV and encrypted data, then base64 encode
     309        return base64_encode($iv . $encrypted);
     310    }
     311
     312    /**
     313     * Decrypt a stored token
     314     *
     315     * @param string $encrypted_token Base64 encoded encrypted token
     316     * @return string Decrypted token
     317     */
     318    public function decrypt_token(string $encrypted_token): string
     319    {
     320        if (empty($encrypted_token)) {
     321            return '';
     322        }
     323
     324        $key = $this->get_encryption_key();
     325        $data = base64_decode($encrypted_token);
     326
     327        if ($data === false || strlen($data) < 17) {
     328            return '';
     329        }
     330
     331        // Extract IV (first 16 bytes) and encrypted data
     332        $iv = substr($data, 0, 16);
     333        $encrypted = substr($data, 16);
     334
     335        $decrypted = openssl_decrypt(
     336            $encrypted,
     337            'AES-256-CBC',
     338            $key,
     339            OPENSSL_RAW_DATA,
     340            $iv
     341        );
     342
     343        return $decrypted !== false ? $decrypted : '';
     344    }
     345
     346    /**
     347     * Get encryption key from WordPress salts
     348     *
     349     * @return string 32-byte encryption key
     350     */
     351    private function get_encryption_key(): string
     352    {
     353        // Use WordPress AUTH_KEY salt, hash to 32 bytes for AES-256
     354        $salt = defined('AUTH_KEY') ? AUTH_KEY : 'tablecrafter-default-salt';
     355        return hash('sha256', $salt, true);
     356    }
    278357}
  • tablecrafter-wp-data-tables/tags/3.5.1/readme.txt

    r3445233 r3445250  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 3.4.0
     6Stable tag: 3.5.1
    77Requires PHP: 7.4
    88License: GPLv2 or later
  • tablecrafter-wp-data-tables/tags/3.5.1/tablecrafter.php

    r3445233 r3445250  
    44 * Plugin URI: https://github.com/TableCrafter/wp-data-tables
    55 * Description: Transform any data source into responsive WordPress tables. WCAG 2.1 compliant, advanced export (Excel/PDF), keyboard navigation, screen readers.
    6  * Version: 3.4.0
     6 * Version: 3.5.1
    77 * Author: TableCrafter Team
    88 * Author URI: https://github.com/fahdi
     
    1919 * Global Constants
    2020 */
    21 define('TABLECRAFTER_VERSION', '3.4.0');
     21define('TABLECRAFTER_VERSION', '3.5.1');
    2222define('TABLECRAFTER_URL', plugin_dir_url(__FILE__));
    2323define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__));
     
    139139        add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
    140140        add_action('wp_ajax_nopriv_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
     141
     142        // Airtable Token Handler [v3.5.0]
     143        add_action('wp_ajax_tc_save_airtable_token', array($this, 'ajax_save_airtable_token'));
    141144
    142145        if (!wp_next_scheduled('tc_refresher_cron')) {
     
    197200        $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv';
    198201        $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0';
     202        $airtable_demo_url = 'airtable://appSampleBaseId/Employees';
    199203        ?>
    200204        <div class="wrap">
     
    242246                                    data-url="<?php echo esc_url($employees_url); ?>">📊
    243247                                    <?php esc_html_e('Employee List (CSV)', 'tablecrafter-wp-data-tables'); ?></a></li>
    244                             <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     248                            <li style="margin-bottom: 8px;"><a href="#" class="button button-large"
    245249                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
    246250                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     
    248252                                    data-url="<?php echo esc_url($metrics_url); ?>">📈
    249253                                    <?php esc_html_e('Sales Metrics (JSON)', 'tablecrafter-wp-data-tables'); ?></a></li>
     254                            <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     255                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
     256                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     257                                    onmouseout="this.style.background='white'; this.style.color='#0e7490'"
     258                                    data-url="<?php echo esc_url($airtable_demo_url); ?>">🔮
     259                                    <?php esc_html_e('Airtable Base (API)', 'tablecrafter-wp-data-tables'); ?></a></li>
    250260                        </ul>
    251261                        <div
     
    280290                                        style="margin-right: 4px; vertical-align: middle;"></span>
    281291                                    <?php esc_html_e('Google Sheets', 'tablecrafter-wp-data-tables'); ?>
     292                                </button>
     293                                <button id="tc-airtable-btn" class="button button-secondary" type="button"
     294                                    title="<?php esc_attr_e('Connect to Airtable Base', 'tablecrafter-wp-data-tables'); ?>">
     295                                    <span class="dashicons dashicons-database"
     296                                        style="margin-right: 4px; vertical-align: middle;"></span>
     297                                    <?php esc_html_e('Airtable', 'tablecrafter-wp-data-tables'); ?>
    282298                                </button>
    283299                            </div>
     
    515531                array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'),
    516532                array('label' => __('📋 Google Sheets Example', 'tablecrafter-wp-data-tables'), 'value' => 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'),
     533                array('label' => __('🔮 Airtable Base (API)', 'tablecrafter-wp-data-tables'), 'value' => 'airtable://appSampleBaseId/Employees'),
    517534            )
    518535        ));
     
    678695        <?php
    679696        return ob_get_clean();
     697    }
     698
     699    /**
     700     * AJAX Handler: Save Airtable Token securely
     701     */
     702    public function ajax_save_airtable_token()
     703    {
     704        // Check permissions and nonce
     705        if (!current_user_can('manage_options') || !check_ajax_referer('tc_proxy_nonce', 'nonce', false)) {
     706            wp_send_json_error(array('message' => 'Permission denied.'));
     707        }
     708
     709        $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : '';
     710
     711        if (empty($token)) {
     712            wp_send_json_error(array('message' => 'Token is required.'));
     713        }
     714
     715        // Encrypt and store
     716        $security = TC_Security::get_instance();
     717        $encrypted = $security->encrypt_token($token);
     718
     719        if (empty($encrypted)) {
     720            wp_send_json_error(array('message' => 'Encryption failed.'));
     721        }
     722
     723        update_option('tablecrafter_airtable_token', $encrypted);
     724
     725        wp_send_json_success(array('message' => 'Token saved securely.'));
    680726    }
    681727
  • tablecrafter-wp-data-tables/trunk/assets/js/admin.js

    r3441951 r3445250  
    8080        });
    8181    }
     82
     83    // --- Airtable Integration (v3.5.0) ---
     84    const atBtn = document.getElementById('tc-airtable-btn');
     85    const atModal = document.getElementById('tc-airtable-modal');
     86    const atCancel = document.getElementById('tc-at-cancel');
     87    const atSave = document.getElementById('tc-at-save');
     88
     89    if (atBtn && atModal) {
     90        // Open Modal
     91        atBtn.addEventListener('click', function(e) {
     92            e.preventDefault();
     93            atModal.style.display = 'block';
     94        });
     95
     96        // Close Modal
     97        const closeModal = () => { atModal.style.display = 'none'; };
     98        atCancel.addEventListener('click', closeModal);
     99       
     100        // Close on overlay click
     101        atModal.querySelector('.tc-modal-overlay').addEventListener('click', closeModal);
     102
     103        // Save & Connect
     104        atSave.addEventListener('click', function() {
     105            const baseId = document.getElementById('tc-at-base').value.trim();
     106            const tableName = document.getElementById('tc-at-table').value.trim();
     107            const viewName = document.getElementById('tc-at-view').value.trim();
     108            const token = document.getElementById('tc-at-token').value.trim();
     109
     110            if (!baseId || !tableName || !token) {
     111                alert('Please fill in Base ID, Table Name, and Personal Access Token.');
     112                return;
     113            }
     114
     115            // 1. Save Token Securely via AJAX
     116            const originalText = atSave.textContent;
     117            atSave.textContent = 'Saving...';
     118            atSave.disabled = true;
     119
     120            const formData = new FormData();
     121            formData.append('action', 'tc_save_airtable_token');
     122            formData.append('token', token);
     123            formData.append('nonce', tablecrafterAdmin.nonce);
     124
     125            fetch(tablecrafterAdmin.ajaxUrl, {
     126                method: 'POST',
     127                body: formData
     128            })
     129            .then(response => response.json())
     130            .then(result => {
     131                if (result.success) {
     132                    // 2. Generate Secure URL (Token is stored server-side)
     133                    let url = `airtable://${baseId}/${tableName}`;
     134                    if (viewName) {
     135                        url += `?view=${encodeURIComponent(viewName)}`;
     136                    }
     137
     138                    // 3. Update Input & Preview
     139                    urlInput.value = url;
     140                    urlInput.dispatchEvent(new Event('input'));
     141                   
     142                    closeModal();
     143                   
     144                    // Clear sensitive input
     145                    document.getElementById('tc-at-token').value = '';
     146                   
     147                    setTimeout(() => previewBtn.click(), 100);
     148                } else {
     149                    alert('Error saving token: ' + (result.data.message || 'Unknown error'));
     150                }
     151            })
     152            .catch(error => {
     153                console.error('Airtable Setup Error:', error);
     154                alert('Network error. Please try again.');
     155            })
     156            .finally(() => {
     157                atSave.textContent = originalText;
     158                atSave.disabled = false;
     159            });
     160        });
     161    }
     162    // -------------------------------------
    82163    // ---------------------------------------------
    83164
  • tablecrafter-wp-data-tables/trunk/includes/class-tc-cache.php

    r3445233 r3445250  
    138138     * @param string $cache_key Cache key
    139139     * @param mixed $data Data to cache
     140     * @param int $ttl Cache TTL in seconds (optional)
    140141     * @return bool Success
    141142     */
    142     public function set_data_cache(string $cache_key, $data): bool
    143     {
    144         return set_transient($cache_key, $data, self::DATA_CACHE_TTL);
     143    public function set_data_cache(string $cache_key, $data, int $ttl = self::DATA_CACHE_TTL): bool
     144    {
     145        return set_transient($cache_key, $data, $ttl);
    145146    }
    146147
  • tablecrafter-wp-data-tables/trunk/includes/class-tc-data-fetcher.php

    r3445233 r3445250  
    9494        }
    9595
     96        // Check for Airtable source (airtable:// protocol)
     97        if (strpos($url, 'airtable://') === 0) {
     98            $result = $this->fetch_airtable($url);
     99        }
    96100        // Check for CSV / Google Sheets
    97         $is_google_sheet = preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url);
    98         $is_csv_ext = (substr($url, -4) === '.csv');
    99 
    100         if ($is_google_sheet || $is_csv_ext) {
     101        elseif (preg_match('/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/', $url) || substr($url, -4) === '.csv') {
    101102            $result = TC_CSV_Source::fetch($url);
    102103        } else {
     
    106107        // Cache successful results
    107108        if ($use_cache && !is_wp_error($result) && !empty($result)) {
    108             $this->cache->set_data_cache($this->cache->get_data_cache_key($url), $result);
     109            // Determine TTL based on source type
     110            $ttl = HOUR_IN_SECONDS;
     111
     112            // Airtable requires stricter caching (5 minutes) due to rate limits
     113            if (strpos($url, 'airtable://') === 0) {
     114                $ttl = 5 * MINUTE_IN_SECONDS;
     115            }
     116
     117            $this->cache->set_data_cache($this->cache->get_data_cache_key($url), $result, $ttl);
    109118            $this->cache->track_url($url);
    110119        }
     
    126135
    127136        // Check if URL is local
    128         if (strpos($url, $site_url) !== 0 &&
     137        if (
     138            strpos($url, $site_url) !== 0 &&
    129139            strpos($url, $home_url) !== 0 &&
    130             strpos($url, $plugin_url) !== 0) {
     140            strpos($url, $plugin_url) !== 0
     141        ) {
    131142            return false;
    132143        }
     
    202213
    203214        return false;
     215    }
     216
     217    /**
     218     * Fetch data from Airtable source
     219     *
     220     * Parses airtable:// URL and delegates to TC_Airtable_Source.
     221     *
     222     * @param string $url Airtable URL (airtable://baseId/tableName?token=xxx)
     223     * @return array|WP_Error Parsed data or error
     224     */
     225    private function fetch_airtable(string $url)
     226    {
     227        // Parse the Airtable URL
     228        $parsed = TC_Airtable_Source::parse_url($url);
     229
     230        if (is_wp_error($parsed)) {
     231            $this->log_error('Airtable URL Parse Error', array('url' => $url));
     232            return $parsed;
     233        }
     234
     235        // Token is required
     236        if (empty($parsed['token'])) {
     237            // Try to get token from saved settings
     238            $saved_token = get_option('tablecrafter_airtable_token', '');
     239            if (!empty($saved_token)) {
     240                $parsed['token'] = $this->security->decrypt_token($saved_token);
     241            }
     242        }
     243
     244        if (empty($parsed['token'])) {
     245            return new WP_Error(
     246                'airtable_no_token',
     247                __('Airtable Personal Access Token is required.', 'tablecrafter-wp-data-tables')
     248            );
     249        }
     250
     251        // Build optional params
     252        $params = [];
     253        if (!empty($parsed['view'])) {
     254            $params['view'] = $parsed['view'];
     255        }
     256
     257        // Fetch from Airtable
     258        $result = TC_Airtable_Source::fetch(
     259            $parsed['base_id'],
     260            $parsed['table_name'],
     261            $parsed['token'],
     262            $params
     263        );
     264
     265        if (is_wp_error($result)) {
     266            $this->log_error('Airtable Fetch Error', array(
     267                'base_id' => $parsed['base_id'],
     268                'table' => $parsed['table_name'],
     269                'error' => $result->get_error_message()
     270            ));
     271        }
     272
     273        return $result;
    204274    }
    205275
  • tablecrafter-wp-data-tables/trunk/includes/class-tc-security.php

    r3445233 r3445250  
    214214        $proxy_headers = array(
    215215            'cloudflare' => 'HTTP_CF_CONNECTING_IP',
    216             'forwarded'  => 'HTTP_X_FORWARDED_FOR',
    217             'real_ip'    => 'HTTP_X_REAL_IP',
     216            'forwarded' => 'HTTP_X_FORWARDED_FOR',
     217            'real_ip' => 'HTTP_X_REAL_IP',
    218218        );
    219219
     
    276276        }
    277277    }
     278
     279    /**
     280     * Encrypt a token for secure storage
     281     *
     282     * Uses WordPress salt for encryption key.
     283     *
     284     * @param string $token Plain text token
     285     * @return string Encrypted token (base64 encoded)
     286     */
     287    public function encrypt_token(string $token): string
     288    {
     289        if (empty($token)) {
     290            return '';
     291        }
     292
     293        $key = $this->get_encryption_key();
     294        $iv = openssl_random_pseudo_bytes(16);
     295
     296        $encrypted = openssl_encrypt(
     297            $token,
     298            'AES-256-CBC',
     299            $key,
     300            OPENSSL_RAW_DATA,
     301            $iv
     302        );
     303
     304        if ($encrypted === false) {
     305            return '';
     306        }
     307
     308        // Combine IV and encrypted data, then base64 encode
     309        return base64_encode($iv . $encrypted);
     310    }
     311
     312    /**
     313     * Decrypt a stored token
     314     *
     315     * @param string $encrypted_token Base64 encoded encrypted token
     316     * @return string Decrypted token
     317     */
     318    public function decrypt_token(string $encrypted_token): string
     319    {
     320        if (empty($encrypted_token)) {
     321            return '';
     322        }
     323
     324        $key = $this->get_encryption_key();
     325        $data = base64_decode($encrypted_token);
     326
     327        if ($data === false || strlen($data) < 17) {
     328            return '';
     329        }
     330
     331        // Extract IV (first 16 bytes) and encrypted data
     332        $iv = substr($data, 0, 16);
     333        $encrypted = substr($data, 16);
     334
     335        $decrypted = openssl_decrypt(
     336            $encrypted,
     337            'AES-256-CBC',
     338            $key,
     339            OPENSSL_RAW_DATA,
     340            $iv
     341        );
     342
     343        return $decrypted !== false ? $decrypted : '';
     344    }
     345
     346    /**
     347     * Get encryption key from WordPress salts
     348     *
     349     * @return string 32-byte encryption key
     350     */
     351    private function get_encryption_key(): string
     352    {
     353        // Use WordPress AUTH_KEY salt, hash to 32 bytes for AES-256
     354        $salt = defined('AUTH_KEY') ? AUTH_KEY : 'tablecrafter-default-salt';
     355        return hash('sha256', $salt, true);
     356    }
    278357}
  • tablecrafter-wp-data-tables/trunk/readme.txt

    r3445233 r3445250  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 3.4.0
     6Stable tag: 3.5.1
    77Requires PHP: 7.4
    88License: GPLv2 or later
  • tablecrafter-wp-data-tables/trunk/tablecrafter.php

    r3445233 r3445250  
    44 * Plugin URI: https://github.com/TableCrafter/wp-data-tables
    55 * Description: Transform any data source into responsive WordPress tables. WCAG 2.1 compliant, advanced export (Excel/PDF), keyboard navigation, screen readers.
    6  * Version: 3.4.0
     6 * Version: 3.5.1
    77 * Author: TableCrafter Team
    88 * Author URI: https://github.com/fahdi
     
    1919 * Global Constants
    2020 */
    21 define('TABLECRAFTER_VERSION', '3.4.0');
     21define('TABLECRAFTER_VERSION', '3.5.1');
    2222define('TABLECRAFTER_URL', plugin_dir_url(__FILE__));
    2323define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__));
     
    139139        add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
    140140        add_action('wp_ajax_nopriv_tc_elementor_preview', array($this, 'ajax_elementor_preview'));
     141
     142        // Airtable Token Handler [v3.5.0]
     143        add_action('wp_ajax_tc_save_airtable_token', array($this, 'ajax_save_airtable_token'));
    141144
    142145        if (!wp_next_scheduled('tc_refresher_cron')) {
     
    197200        $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv';
    198201        $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0';
     202        $airtable_demo_url = 'airtable://appSampleBaseId/Employees';
    199203        ?>
    200204        <div class="wrap">
     
    242246                                    data-url="<?php echo esc_url($employees_url); ?>">📊
    243247                                    <?php esc_html_e('Employee List (CSV)', 'tablecrafter-wp-data-tables'); ?></a></li>
    244                             <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     248                            <li style="margin-bottom: 8px;"><a href="#" class="button button-large"
    245249                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
    246250                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     
    248252                                    data-url="<?php echo esc_url($metrics_url); ?>">📈
    249253                                    <?php esc_html_e('Sales Metrics (JSON)', 'tablecrafter-wp-data-tables'); ?></a></li>
     254                            <li style="margin-bottom: 0;"><a href="#" class="button button-large"
     255                                    style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;"
     256                                    onmouseover="this.style.background='#0891b2'; this.style.color='white'"
     257                                    onmouseout="this.style.background='white'; this.style.color='#0e7490'"
     258                                    data-url="<?php echo esc_url($airtable_demo_url); ?>">🔮
     259                                    <?php esc_html_e('Airtable Base (API)', 'tablecrafter-wp-data-tables'); ?></a></li>
    250260                        </ul>
    251261                        <div
     
    280290                                        style="margin-right: 4px; vertical-align: middle;"></span>
    281291                                    <?php esc_html_e('Google Sheets', 'tablecrafter-wp-data-tables'); ?>
     292                                </button>
     293                                <button id="tc-airtable-btn" class="button button-secondary" type="button"
     294                                    title="<?php esc_attr_e('Connect to Airtable Base', 'tablecrafter-wp-data-tables'); ?>">
     295                                    <span class="dashicons dashicons-database"
     296                                        style="margin-right: 4px; vertical-align: middle;"></span>
     297                                    <?php esc_html_e('Airtable', 'tablecrafter-wp-data-tables'); ?>
    282298                                </button>
    283299                            </div>
     
    515531                array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'),
    516532                array('label' => __('📋 Google Sheets Example', 'tablecrafter-wp-data-tables'), 'value' => 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'),
     533                array('label' => __('🔮 Airtable Base (API)', 'tablecrafter-wp-data-tables'), 'value' => 'airtable://appSampleBaseId/Employees'),
    517534            )
    518535        ));
     
    678695        <?php
    679696        return ob_get_clean();
     697    }
     698
     699    /**
     700     * AJAX Handler: Save Airtable Token securely
     701     */
     702    public function ajax_save_airtable_token()
     703    {
     704        // Check permissions and nonce
     705        if (!current_user_can('manage_options') || !check_ajax_referer('tc_proxy_nonce', 'nonce', false)) {
     706            wp_send_json_error(array('message' => 'Permission denied.'));
     707        }
     708
     709        $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : '';
     710
     711        if (empty($token)) {
     712            wp_send_json_error(array('message' => 'Token is required.'));
     713        }
     714
     715        // Encrypt and store
     716        $security = TC_Security::get_instance();
     717        $encrypted = $security->encrypt_token($token);
     718
     719        if (empty($encrypted)) {
     720            wp_send_json_error(array('message' => 'Encryption failed.'));
     721        }
     722
     723        update_option('tablecrafter_airtable_token', $encrypted);
     724
     725        wp_send_json_success(array('message' => 'Token saved securely.'));
    680726    }
    681727
Note: See TracChangeset for help on using the changeset viewer.