Changeset 3445250
- Timestamp:
- 01/23/2026 01:56:44 AM (2 months ago)
- Location:
- tablecrafter-wp-data-tables
- Files:
-
- 3 added
- 8 edited
- 24 copied
-
tags/3.5.0 (copied) (copied from tablecrafter-wp-data-tables/trunk)
-
tags/3.5.0/assets/css/tablecrafter.css (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/css/tablecrafter.css)
-
tags/3.5.0/assets/js/admin.js (modified) (1 diff)
-
tags/3.5.0/assets/js/block.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/block.js)
-
tags/3.5.0/assets/js/performance-optimizer.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/performance-optimizer.js)
-
tags/3.5.0/assets/js/tablecrafter.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/tablecrafter.js)
-
tags/3.5.0/includes/class-tc-cache.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-cache.php)
-
tags/3.5.0/includes/class-tc-data-fetcher.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-data-fetcher.php) (3 diffs)
-
tags/3.5.0/includes/class-tc-security.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-security.php) (2 diffs)
-
tags/3.5.0/includes/sources/class-tc-airtable-source.php (added)
-
tags/3.5.0/includes/sources/class-tc-csv-source.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/sources/class-tc-csv-source.php)
-
tags/3.5.0/readme.txt (copied) (copied from tablecrafter-wp-data-tables/trunk/readme.txt) (1 diff)
-
tags/3.5.0/tablecrafter.php (copied) (copied from tablecrafter-wp-data-tables/trunk/tablecrafter.php) (9 diffs)
-
tags/3.5.0/uninstall.php (copied) (copied from tablecrafter-wp-data-tables/trunk/uninstall.php)
-
tags/3.5.1 (copied) (copied from tablecrafter-wp-data-tables/trunk)
-
tags/3.5.1/assets/css/tablecrafter.css (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/css/tablecrafter.css)
-
tags/3.5.1/assets/js/admin.js (modified) (1 diff)
-
tags/3.5.1/assets/js/block.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/block.js)
-
tags/3.5.1/assets/js/performance-optimizer.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/performance-optimizer.js)
-
tags/3.5.1/assets/js/tablecrafter.js (copied) (copied from tablecrafter-wp-data-tables/trunk/assets/js/tablecrafter.js)
-
tags/3.5.1/includes/class-tc-cache.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-cache.php) (1 diff)
-
tags/3.5.1/includes/class-tc-data-fetcher.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-data-fetcher.php) (4 diffs)
-
tags/3.5.1/includes/class-tc-security.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/class-tc-security.php) (2 diffs)
-
tags/3.5.1/includes/sources/class-tc-airtable-source.php (added)
-
tags/3.5.1/includes/sources/class-tc-csv-source.php (copied) (copied from tablecrafter-wp-data-tables/trunk/includes/sources/class-tc-csv-source.php)
-
tags/3.5.1/readme.txt (copied) (copied from tablecrafter-wp-data-tables/trunk/readme.txt) (1 diff)
-
tags/3.5.1/tablecrafter.php (copied) (copied from tablecrafter-wp-data-tables/trunk/tablecrafter.php) (9 diffs)
-
tags/3.5.1/uninstall.php (copied) (copied from tablecrafter-wp-data-tables/trunk/uninstall.php)
-
trunk/assets/js/admin.js (modified) (1 diff)
-
trunk/includes/class-tc-cache.php (modified) (1 diff)
-
trunk/includes/class-tc-data-fetcher.php (modified) (4 diffs)
-
trunk/includes/class-tc-security.php (modified) (2 diffs)
-
trunk/includes/sources/class-tc-airtable-source.php (added)
-
trunk/readme.txt (modified) (1 diff)
-
trunk/tablecrafter.php (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
tablecrafter-wp-data-tables/tags/3.5.0/assets/js/admin.js
r3441951 r3445250 80 80 }); 81 81 } 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 // ------------------------------------- 82 163 // --------------------------------------------- 83 164 -
tablecrafter-wp-data-tables/tags/3.5.0/includes/class-tc-data-fetcher.php
r3445233 r3445250 94 94 } 95 95 96 // Check for Airtable source (airtable:// protocol) 97 if (strpos($url, 'airtable://') === 0) { 98 $result = $this->fetch_airtable($url); 99 } 96 100 // 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') { 101 102 $result = TC_CSV_Source::fetch($url); 102 103 } else { … … 126 127 127 128 // Check if URL is local 128 if (strpos($url, $site_url) !== 0 && 129 if ( 130 strpos($url, $site_url) !== 0 && 129 131 strpos($url, $home_url) !== 0 && 130 strpos($url, $plugin_url) !== 0) { 132 strpos($url, $plugin_url) !== 0 133 ) { 131 134 return false; 132 135 } … … 202 205 203 206 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; 204 266 } 205 267 -
tablecrafter-wp-data-tables/tags/3.5.0/includes/class-tc-security.php
r3445233 r3445250 214 214 $proxy_headers = array( 215 215 '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', 218 218 ); 219 219 … … 276 276 } 277 277 } 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 } 278 357 } -
tablecrafter-wp-data-tables/tags/3.5.0/readme.txt
r3445233 r3445250 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 3. 4.06 Stable tag: 3.5.0 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later -
tablecrafter-wp-data-tables/tags/3.5.0/tablecrafter.php
r3445233 r3445250 4 4 * Plugin URI: https://github.com/TableCrafter/wp-data-tables 5 5 * 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.06 * Version: 3.5.0 7 7 * Author: TableCrafter Team 8 8 * Author URI: https://github.com/fahdi … … 19 19 * Global Constants 20 20 */ 21 define('TABLECRAFTER_VERSION', '3. 4.0');21 define('TABLECRAFTER_VERSION', '3.5.0'); 22 22 define('TABLECRAFTER_URL', plugin_dir_url(__FILE__)); 23 23 define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__)); … … 139 139 add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview')); 140 140 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')); 141 144 142 145 if (!wp_next_scheduled('tc_refresher_cron')) { … … 197 200 $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv'; 198 201 $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'; 202 $airtable_demo_url = 'airtable://appSampleBaseId/Employees'; 199 203 ?> 200 204 <div class="wrap"> … … 242 246 data-url="<?php echo esc_url($employees_url); ?>">📊 243 247 <?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" 245 249 style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;" 246 250 onmouseover="this.style.background='#0891b2'; this.style.color='white'" … … 248 252 data-url="<?php echo esc_url($metrics_url); ?>">📈 249 253 <?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> 250 260 </ul> 251 261 <div … … 280 290 style="margin-right: 4px; vertical-align: middle;"></span> 281 291 <?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'); ?> 282 298 </button> 283 299 </div> … … 515 531 array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'), 516 532 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'), 517 534 ) 518 535 )); … … 678 695 <?php 679 696 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.')); 680 726 } 681 727 -
tablecrafter-wp-data-tables/tags/3.5.1/assets/js/admin.js
r3441951 r3445250 80 80 }); 81 81 } 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 // ------------------------------------- 82 163 // --------------------------------------------- 83 164 -
tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-cache.php
r3445233 r3445250 138 138 * @param string $cache_key Cache key 139 139 * @param mixed $data Data to cache 140 * @param int $ttl Cache TTL in seconds (optional) 140 141 * @return bool Success 141 142 */ 142 public function set_data_cache(string $cache_key, $data ): bool143 { 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); 145 146 } 146 147 -
tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-data-fetcher.php
r3445233 r3445250 94 94 } 95 95 96 // Check for Airtable source (airtable:// protocol) 97 if (strpos($url, 'airtable://') === 0) { 98 $result = $this->fetch_airtable($url); 99 } 96 100 // 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') { 101 102 $result = TC_CSV_Source::fetch($url); 102 103 } else { … … 106 107 // Cache successful results 107 108 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); 109 118 $this->cache->track_url($url); 110 119 } … … 126 135 127 136 // Check if URL is local 128 if (strpos($url, $site_url) !== 0 && 137 if ( 138 strpos($url, $site_url) !== 0 && 129 139 strpos($url, $home_url) !== 0 && 130 strpos($url, $plugin_url) !== 0) { 140 strpos($url, $plugin_url) !== 0 141 ) { 131 142 return false; 132 143 } … … 202 213 203 214 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; 204 274 } 205 275 -
tablecrafter-wp-data-tables/tags/3.5.1/includes/class-tc-security.php
r3445233 r3445250 214 214 $proxy_headers = array( 215 215 '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', 218 218 ); 219 219 … … 276 276 } 277 277 } 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 } 278 357 } -
tablecrafter-wp-data-tables/tags/3.5.1/readme.txt
r3445233 r3445250 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 3. 4.06 Stable tag: 3.5.1 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later -
tablecrafter-wp-data-tables/tags/3.5.1/tablecrafter.php
r3445233 r3445250 4 4 * Plugin URI: https://github.com/TableCrafter/wp-data-tables 5 5 * 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.06 * Version: 3.5.1 7 7 * Author: TableCrafter Team 8 8 * Author URI: https://github.com/fahdi … … 19 19 * Global Constants 20 20 */ 21 define('TABLECRAFTER_VERSION', '3. 4.0');21 define('TABLECRAFTER_VERSION', '3.5.1'); 22 22 define('TABLECRAFTER_URL', plugin_dir_url(__FILE__)); 23 23 define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__)); … … 139 139 add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview')); 140 140 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')); 141 144 142 145 if (!wp_next_scheduled('tc_refresher_cron')) { … … 197 200 $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv'; 198 201 $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'; 202 $airtable_demo_url = 'airtable://appSampleBaseId/Employees'; 199 203 ?> 200 204 <div class="wrap"> … … 242 246 data-url="<?php echo esc_url($employees_url); ?>">📊 243 247 <?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" 245 249 style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;" 246 250 onmouseover="this.style.background='#0891b2'; this.style.color='white'" … … 248 252 data-url="<?php echo esc_url($metrics_url); ?>">📈 249 253 <?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> 250 260 </ul> 251 261 <div … … 280 290 style="margin-right: 4px; vertical-align: middle;"></span> 281 291 <?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'); ?> 282 298 </button> 283 299 </div> … … 515 531 array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'), 516 532 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'), 517 534 ) 518 535 )); … … 678 695 <?php 679 696 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.')); 680 726 } 681 727 -
tablecrafter-wp-data-tables/trunk/assets/js/admin.js
r3441951 r3445250 80 80 }); 81 81 } 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 // ------------------------------------- 82 163 // --------------------------------------------- 83 164 -
tablecrafter-wp-data-tables/trunk/includes/class-tc-cache.php
r3445233 r3445250 138 138 * @param string $cache_key Cache key 139 139 * @param mixed $data Data to cache 140 * @param int $ttl Cache TTL in seconds (optional) 140 141 * @return bool Success 141 142 */ 142 public function set_data_cache(string $cache_key, $data ): bool143 { 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); 145 146 } 146 147 -
tablecrafter-wp-data-tables/trunk/includes/class-tc-data-fetcher.php
r3445233 r3445250 94 94 } 95 95 96 // Check for Airtable source (airtable:// protocol) 97 if (strpos($url, 'airtable://') === 0) { 98 $result = $this->fetch_airtable($url); 99 } 96 100 // 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') { 101 102 $result = TC_CSV_Source::fetch($url); 102 103 } else { … … 106 107 // Cache successful results 107 108 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); 109 118 $this->cache->track_url($url); 110 119 } … … 126 135 127 136 // Check if URL is local 128 if (strpos($url, $site_url) !== 0 && 137 if ( 138 strpos($url, $site_url) !== 0 && 129 139 strpos($url, $home_url) !== 0 && 130 strpos($url, $plugin_url) !== 0) { 140 strpos($url, $plugin_url) !== 0 141 ) { 131 142 return false; 132 143 } … … 202 213 203 214 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; 204 274 } 205 275 -
tablecrafter-wp-data-tables/trunk/includes/class-tc-security.php
r3445233 r3445250 214 214 $proxy_headers = array( 215 215 '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', 218 218 ); 219 219 … … 276 276 } 277 277 } 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 } 278 357 } -
tablecrafter-wp-data-tables/trunk/readme.txt
r3445233 r3445250 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 3. 4.06 Stable tag: 3.5.1 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later -
tablecrafter-wp-data-tables/trunk/tablecrafter.php
r3445233 r3445250 4 4 * Plugin URI: https://github.com/TableCrafter/wp-data-tables 5 5 * 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.06 * Version: 3.5.1 7 7 * Author: TableCrafter Team 8 8 * Author URI: https://github.com/fahdi … … 19 19 * Global Constants 20 20 */ 21 define('TABLECRAFTER_VERSION', '3. 4.0');21 define('TABLECRAFTER_VERSION', '3.5.1'); 22 22 define('TABLECRAFTER_URL', plugin_dir_url(__FILE__)); 23 23 define('TABLECRAFTER_PATH', plugin_dir_path(__FILE__)); … … 139 139 add_action('wp_ajax_tc_elementor_preview', array($this, 'ajax_elementor_preview')); 140 140 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')); 141 144 142 145 if (!wp_next_scheduled('tc_refresher_cron')) { … … 197 200 $employees_url = TABLECRAFTER_URL . 'demo-data/employees.csv'; 198 201 $sheets_url = 'https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=0'; 202 $airtable_demo_url = 'airtable://appSampleBaseId/Employees'; 199 203 ?> 200 204 <div class="wrap"> … … 242 246 data-url="<?php echo esc_url($employees_url); ?>">📊 243 247 <?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" 245 249 style="width: 100%; text-align: left; background: white; border: 2px solid #0891b2; color: #0e7490; font-weight: 600; transition: all 0.2s ease;" 246 250 onmouseover="this.style.background='#0891b2'; this.style.color='white'" … … 248 252 data-url="<?php echo esc_url($metrics_url); ?>">📈 249 253 <?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> 250 260 </ul> 251 261 <div … … 280 290 style="margin-right: 4px; vertical-align: middle;"></span> 281 291 <?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'); ?> 282 298 </button> 283 299 </div> … … 515 531 array('label' => __('👥 Employee List (CSV)', 'tablecrafter-wp-data-tables'), 'value' => TABLECRAFTER_URL . 'demo-data/employees.csv'), 516 532 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'), 517 534 ) 518 535 )); … … 678 695 <?php 679 696 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.')); 680 726 } 681 727
Note: See TracChangeset
for help on using the changeset viewer.