Changeset 3403597
- Timestamp:
- 11/26/2025 05:39:34 PM (4 months ago)
- Location:
- instarank/trunk
- Files:
-
- 5 edited
-
admin/templates-field-mapping.php (modified) (4 diffs)
-
api/endpoints.php (modified) (7 diffs)
-
includes/class-field-detector.php (modified) (2 diffs)
-
instarank.php (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
instarank/trunk/admin/templates-field-mapping.php
r3402479 r3403597 25 25 } 26 26 27 // Detect custom fields in template .27 // Detect custom fields in template (including standard and SEO fields). 28 28 $instarank_detector = InstaRank_Field_Detector::instance(); 29 $instarank_detection_result = $instarank_detector->detect_ fields($instarank_template_id);29 $instarank_detection_result = $instarank_detector->detect_all_fields($instarank_template_id, true, true); 30 30 31 31 // Get existing mappings. … … 33 33 $instarank_saved_mappings = get_option($instarank_option_name, []); 34 34 $instarank_mappings = isset($instarank_saved_mappings['mappings']) ? $instarank_saved_mappings['mappings'] : []; 35 36 // Detect SEO plugin (needed for display in multiple places). 37 $instarank_seo_plugin = 'None'; 38 if (defined('WPSEO_VERSION')) { 39 $instarank_seo_plugin = 'Yoast SEO'; 40 } elseif (class_exists('RankMath')) { 41 $instarank_seo_plugin = 'Rank Math'; 42 } elseif (defined('AIOSEO_VERSION') || class_exists('AIOSEO')) { 43 $instarank_seo_plugin = 'All in One SEO'; 44 } 35 45 36 46 ?> … … 169 179 </div> 170 180 181 <!-- Standard WordPress Fields --> 182 <?php if ( ! empty($instarank_detection_result['standard_fields'])) : ?> 183 <div class="instarank-standard-fields" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; border-radius: 4px;"> 184 <h2> 185 <span class="dashicons dashicons-wordpress" style="color: #2271b1; vertical-align: middle;"></span> 186 <?php esc_html_e('Standard WordPress Fields', 'instarank'); ?> 187 </h2> 188 189 <p class="description"> 190 <?php esc_html_e('These are standard WordPress page/post fields. Map them to populate page properties like title, slug, status, etc.', 'instarank'); ?> 191 </p> 192 193 <table class="wp-list-table widefat fixed striped" style="margin-top: 15px;"> 194 <thead> 195 <tr> 196 <th style="width: 20%;"><?php esc_html_e('Field Name', 'instarank'); ?></th> 197 <th style="width: 15%;"><?php esc_html_e('Type', 'instarank'); ?></th> 198 <th style="width: 35%;"><?php esc_html_e('Description', 'instarank'); ?></th> 199 <th style="width: 15%;"><?php esc_html_e('Required', 'instarank'); ?></th> 200 <th style="width: 15%;"><?php esc_html_e('Status', 'instarank'); ?></th> 201 </tr> 202 </thead> 203 <tbody> 204 <?php foreach ($instarank_detection_result['standard_fields'] as $instarank_field) : ?> 205 <tr> 206 <td> 207 <strong><code><?php echo esc_html($instarank_field['field_name']); ?></code></strong> 208 </td> 209 <td> 210 <?php 211 $instarank_type_icons = [ 212 'image' => 'format-image', 213 'url' => 'admin-links', 214 'number' => 'chart-line', 215 'date' => 'calendar-alt', 216 'email' => 'email', 217 'text' => 'text', 218 ]; 219 $instarank_icon = isset($instarank_type_icons[$instarank_field['field_type']]) ? $instarank_type_icons[$instarank_field['field_type']] : 'text'; 220 ?> 221 <span class="dashicons dashicons-<?php echo esc_attr($instarank_icon); ?>"></span> 222 <?php echo esc_html(ucfirst($instarank_field['field_type'])); ?> 223 </td> 224 <td> 225 <span class="description"> 226 <?php echo esc_html($instarank_field['description'] ?? ''); ?> 227 </span> 228 </td> 229 <td> 230 <?php if ($instarank_field['required']) : ?> 231 <span class="dashicons dashicons-yes" style="color: #d63638;"></span> 232 <?php esc_html_e('Yes', 'instarank'); ?> 233 <?php else : ?> 234 <span class="dashicons dashicons-minus"></span> 235 <?php esc_html_e('No', 'instarank'); ?> 236 <?php endif; ?> 237 </td> 238 <td> 239 <?php if (isset($instarank_mappings[$instarank_field['field_name']])) : ?> 240 <span class="dashicons dashicons-yes" style="color: #46b450;"></span> 241 <?php esc_html_e('Mapped', 'instarank'); ?> 242 <?php else : ?> 243 <span class="dashicons dashicons-minus" style="color: #999;"></span> 244 <?php esc_html_e('Optional', 'instarank'); ?> 245 <?php endif; ?> 246 </td> 247 </tr> 248 <?php endforeach; ?> 249 </tbody> 250 </table> 251 </div> 252 <?php endif; ?> 253 254 <!-- SEO Plugin Fields --> 255 <?php if ( ! empty($instarank_detection_result['seo_fields'])) : ?> 256 <div class="instarank-seo-fields" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; border-radius: 4px;"> 257 <h2> 258 <span class="dashicons dashicons-google" style="color: #4285f4; vertical-align: middle;"></span> 259 <?php esc_html_e('SEO Fields', 'instarank'); ?> 260 <span style="font-size: 12px; font-weight: normal; color: #666; margin-left: 10px;"> 261 (<?php echo esc_html($instarank_seo_plugin); ?>) 262 </span> 263 </h2> 264 265 <p class="description"> 266 <?php esc_html_e('Map these fields to populate SEO meta data for your pages. These will be saved to your SEO plugin.', 'instarank'); ?> 267 </p> 268 269 <table class="wp-list-table widefat fixed striped" style="margin-top: 15px;"> 270 <thead> 271 <tr> 272 <th style="width: 20%;"><?php esc_html_e('Field Name', 'instarank'); ?></th> 273 <th style="width: 15%;"><?php esc_html_e('Type', 'instarank'); ?></th> 274 <th style="width: 35%;"><?php esc_html_e('Description', 'instarank'); ?></th> 275 <th style="width: 15%;"><?php esc_html_e('Required', 'instarank'); ?></th> 276 <th style="width: 15%;"><?php esc_html_e('Status', 'instarank'); ?></th> 277 </tr> 278 </thead> 279 <tbody> 280 <?php foreach ($instarank_detection_result['seo_fields'] as $instarank_field) : ?> 281 <tr> 282 <td> 283 <strong><code><?php echo esc_html($instarank_field['field_name']); ?></code></strong> 284 </td> 285 <td> 286 <?php 287 $instarank_type_icons = [ 288 'image' => 'format-image', 289 'url' => 'admin-links', 290 'number' => 'chart-line', 291 'date' => 'calendar-alt', 292 'email' => 'email', 293 'text' => 'text', 294 ]; 295 $instarank_icon = isset($instarank_type_icons[$instarank_field['field_type']]) ? $instarank_type_icons[$instarank_field['field_type']] : 'text'; 296 ?> 297 <span class="dashicons dashicons-<?php echo esc_attr($instarank_icon); ?>"></span> 298 <?php echo esc_html(ucfirst($instarank_field['field_type'])); ?> 299 </td> 300 <td> 301 <span class="description"> 302 <?php echo esc_html($instarank_field['description'] ?? ''); ?> 303 </span> 304 </td> 305 <td> 306 <?php if ($instarank_field['required']) : ?> 307 <span class="dashicons dashicons-yes" style="color: #d63638;"></span> 308 <?php esc_html_e('Yes', 'instarank'); ?> 309 <?php else : ?> 310 <span class="dashicons dashicons-minus"></span> 311 <?php esc_html_e('No', 'instarank'); ?> 312 <?php endif; ?> 313 </td> 314 <td> 315 <?php if (isset($instarank_mappings[$instarank_field['field_name']])) : ?> 316 <span class="dashicons dashicons-yes" style="color: #46b450;"></span> 317 <?php esc_html_e('Mapped', 'instarank'); ?> 318 <?php else : ?> 319 <span class="dashicons dashicons-minus" style="color: #999;"></span> 320 <?php esc_html_e('Optional', 'instarank'); ?> 321 <?php endif; ?> 322 </td> 323 </tr> 324 <?php endforeach; ?> 325 </tbody> 326 </table> 327 </div> 328 <?php endif; ?> 329 171 330 <!-- Dataset Connection --> 172 331 <div class="instarank-dataset-connection" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; border-radius: 4px;"> … … 392 551 <?php endforeach; ?> 393 552 </table> 394 395 <p class="submit"> 553 </div> 554 555 <!-- Standard WordPress Fields Mapping --> 556 <?php if ( ! empty($instarank_detection_result['standard_fields'])) : ?> 557 <div class="instarank-wordpress-mappings" style="background: #f9f9f9; padding: 20px; margin: 20px 0; border: 1px solid #e5e5e5; border-radius: 4px;"> 558 <h3> 559 <span class="dashicons dashicons-wordpress" style="color: #2271b1; vertical-align: middle;"></span> 560 <?php esc_html_e('WordPress Page Fields', 'instarank'); ?> 561 </h3> 562 <p class="description" style="margin-bottom: 15px;"> 563 <?php esc_html_e('Optional: Map dataset columns to standard WordPress page properties.', 'instarank'); ?> 564 </p> 565 566 <table class="form-table"> 567 <?php foreach ($instarank_detection_result['standard_fields'] as $instarank_field) : 568 $instarank_field_name = $instarank_field['field_name']; 569 $instarank_existing_mapping = isset($instarank_mappings[$instarank_field_name]) ? $instarank_mappings[$instarank_field_name] : []; 570 ?> 571 <tr> 572 <th style="width: 200px;"> 573 <label for="mapping_wp_<?php echo esc_attr($instarank_field_name); ?>"> 574 <code><?php echo esc_html($instarank_field_name); ?></code> 575 </label> 576 <br> 577 <span class="description" style="font-size: 11px;"><?php echo esc_html($instarank_field['description'] ?? ''); ?></span> 578 </th> 579 <td> 580 <select 581 name="field_mappings[<?php echo esc_attr($instarank_field_name); ?>][dataset_column]" 582 id="mapping_wp_<?php echo esc_attr($instarank_field_name); ?>" 583 class="regular-text dataset-column-select" 584 > 585 <option value=""><?php esc_html_e('-- Select Column --', 'instarank'); ?></option> 586 <?php if ( ! empty($instarank_existing_mapping['dataset_column'])) : ?> 587 <option value="<?php echo esc_attr($instarank_existing_mapping['dataset_column']); ?>" selected> 588 <?php echo esc_html($instarank_existing_mapping['dataset_column']); ?> 589 </option> 590 <?php endif; ?> 591 </select> 592 </td> 593 </tr> 594 <?php endforeach; ?> 595 </table> 596 </div> 597 <?php endif; ?> 598 599 <!-- SEO Fields Mapping --> 600 <?php if ( ! empty($instarank_detection_result['seo_fields'])) : ?> 601 <div class="instarank-seo-mappings" style="background: #f0f9ff; padding: 20px; margin: 20px 0; border: 1px solid #b3d9f9; border-radius: 4px;"> 602 <h3> 603 <span class="dashicons dashicons-google" style="color: #4285f4; vertical-align: middle;"></span> 604 <?php esc_html_e('SEO Meta Fields', 'instarank'); ?> 605 <span style="font-size: 11px; font-weight: normal; color: #666; margin-left: 10px;"> 606 (<?php echo esc_html($instarank_seo_plugin ?? 'No SEO Plugin'); ?>) 607 </span> 608 </h3> 609 <p class="description" style="margin-bottom: 15px;"> 610 <?php esc_html_e('Optional: Map dataset columns to SEO meta fields. These will be saved to your SEO plugin.', 'instarank'); ?> 611 </p> 612 613 <table class="form-table"> 614 <?php foreach ($instarank_detection_result['seo_fields'] as $instarank_field) : 615 $instarank_field_name = $instarank_field['field_name']; 616 $instarank_existing_mapping = isset($instarank_mappings[$instarank_field_name]) ? $instarank_mappings[$instarank_field_name] : []; 617 ?> 618 <tr> 619 <th style="width: 200px;"> 620 <label for="mapping_seo_<?php echo esc_attr($instarank_field_name); ?>"> 621 <code><?php echo esc_html($instarank_field_name); ?></code> 622 </label> 623 <br> 624 <span class="description" style="font-size: 11px;"><?php echo esc_html($instarank_field['description'] ?? ''); ?></span> 625 </th> 626 <td> 627 <select 628 name="field_mappings[<?php echo esc_attr($instarank_field_name); ?>][dataset_column]" 629 id="mapping_seo_<?php echo esc_attr($instarank_field_name); ?>" 630 class="regular-text dataset-column-select" 631 > 632 <option value=""><?php esc_html_e('-- Select Column --', 'instarank'); ?></option> 633 <?php if ( ! empty($instarank_existing_mapping['dataset_column'])) : ?> 634 <option value="<?php echo esc_attr($instarank_existing_mapping['dataset_column']); ?>" selected> 635 <?php echo esc_html($instarank_existing_mapping['dataset_column']); ?> 636 </option> 637 <?php endif; ?> 638 </select> 639 </td> 640 </tr> 641 <?php endforeach; ?> 642 </table> 643 </div> 644 <?php endif; ?> 645 646 <div style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; border-radius: 4px;"> 647 <p class="submit" style="margin: 0; padding: 0;"> 396 648 <button type="submit" name="instarank_save_field_mappings" class="button button-primary button-large"> 397 649 <span class="dashicons dashicons-saved" style="vertical-align: middle;"></span> -
instarank/trunk/api/endpoints.php
r3402479 r3403597 144 144 'methods' => 'PUT', 145 145 'callback' => [$this, 'update_custom_post_type'], 146 'permission_callback' => [$this, 'verify_api_key'] 147 ]); 148 149 // Delete a custom post type from InstaRank 150 register_rest_route('instarank/v1', '/programmatic/post-types/(?P<slug>[a-z0-9_-]+)', [ 151 'methods' => 'DELETE', 152 'callback' => [$this, 'delete_custom_post_type'], 146 153 'permission_callback' => [$this, 'verify_api_key'] 147 154 ]); … … 1339 1346 1340 1347 /** 1348 * Delete a custom post type from InstaRank 1349 */ 1350 public function delete_custom_post_type($request) { 1351 $slug = sanitize_key($request->get_param('slug')); 1352 1353 if (empty($slug)) { 1354 return new WP_Error( 1355 'missing_slug', 1356 'Post type slug is required', 1357 ['status' => 400] 1358 ); 1359 } 1360 1361 // Get stored post types 1362 $stored_post_types = get_option('instarank_custom_post_types', []); 1363 1364 // Check if this post type was created by InstaRank 1365 if (!isset($stored_post_types[$slug])) { 1366 // Post type not found in InstaRank storage - might not exist or wasn't created by InstaRank 1367 // Return success anyway to allow cleanup on the SaaS side 1368 return rest_ensure_response([ 1369 'success' => true, 1370 'message' => 'Post type not found in InstaRank registry (may have been already deleted or was not created by InstaRank)', 1371 'slug' => $slug 1372 ]); 1373 } 1374 1375 // Check if there are any posts of this type 1376 $post_count = wp_count_posts($slug); 1377 $total_posts = 0; 1378 if ($post_count) { 1379 foreach ($post_count as $status => $count) { 1380 $total_posts += $count; 1381 } 1382 } 1383 1384 // Option: Force delete even with existing posts, or warn 1385 $force = $request->get_param('force') === 'true' || $request->get_param('force') === true; 1386 1387 if ($total_posts > 0 && !$force) { 1388 return new WP_Error( 1389 'posts_exist', 1390 "Cannot delete post type '{$slug}' because it has {$total_posts} existing posts. Use force=true to delete anyway (posts will become orphaned).", 1391 ['status' => 409, 'post_count' => $total_posts] 1392 ); 1393 } 1394 1395 // Remove from stored post types 1396 unset($stored_post_types[$slug]); 1397 update_option('instarank_custom_post_types', $stored_post_types); 1398 1399 // Unregister the post type (only works for custom post types registered during this request) 1400 // For persistent removal, the post type won't be re-registered on next load 1401 if (post_type_exists($slug)) { 1402 unregister_post_type($slug); 1403 } 1404 1405 // Flush rewrite rules to remove URL patterns 1406 flush_rewrite_rules(); 1407 1408 return rest_ensure_response([ 1409 'success' => true, 1410 'message' => 'Custom post type deleted successfully', 1411 'slug' => $slug, 1412 'posts_affected' => $total_posts 1413 ]); 1414 } 1415 1416 /** 1341 1417 * Sync a programmatic page to WordPress 1342 1418 */ … … 1361 1437 // Normalize content if it's a string (fix smart quotes and en-dashes in block comments) 1362 1438 if (is_string($content)) { 1363 // Fix escaped newlines that come from JSON (e.g., "\\n" becomes actual newline) 1364 // Use stripcslashes to convert escape sequences like \n, \r, \t to actual characters 1365 $content = stripcslashes($content); 1439 // IMPORTANT: Do NOT use stripcslashes() here! 1440 // stripcslashes() removes backslashes from \u003c (Unicode escapes used in Kadence block JSON) 1441 // which corrupts the block structure. Kadence buttons use "text":"\u003cstrong\u003e..." format. 1442 // Instead, only fix specific escape sequences if needed: 1443 // - Convert literal \\n to newline (double-escaped from JSON) 1444 $content = str_replace('\\n', "\n", $content); 1445 $content = str_replace('\\r', "\r", $content); 1446 $content = str_replace('\\t', "\t", $content); 1366 1447 // Robust replacement for corrupted block comments using regex 1367 1448 // Matches <! followed by any dash-like char (Unicode property Pd), and / followed by any dash-like char > … … 1782 1863 1783 1864 // WordPress core fields that need special handling 1865 // Keys match the field names defined in class-field-detector.php get_standard_fields() 1784 1866 $wp_core_fields = [ 1785 1867 'parent' => 'post_parent', … … 1789 1871 'ping_status' => 'ping_status', 1790 1872 'post_password' => 'post_password', 1873 'author' => 'post_author', 1874 'publish_date' => 'post_date', 1875 'featured_image' => '_thumbnail_id', 1876 'Page_Title' => 'post_title', 1877 'Slug' => 'post_name', 1878 'status' => 'post_status', 1879 'excerpt' => 'post_excerpt', 1791 1880 ]; 1792 1881 … … 1824 1913 $wp_field = $wp_core_fields[$field_key]; 1825 1914 1915 // Special handling for author - can be username, email, or ID 1916 if ($wp_field === 'post_author') { 1917 $author_id = null; 1918 if (is_numeric($field_value)) { 1919 $author_id = intval($field_value); 1920 } else { 1921 // Try to find user by username or email 1922 $user = get_user_by('login', $field_value); 1923 if (!$user) { 1924 $user = get_user_by('email', $field_value); 1925 } 1926 if ($user) { 1927 $author_id = $user->ID; 1928 } 1929 } 1930 if ($author_id && get_user_by('id', $author_id)) { 1931 wp_update_post([ 1932 'ID' => $post_id, 1933 'post_author' => $author_id 1934 ]); 1935 } 1936 continue; 1937 } 1938 1939 // Special handling for featured image - can be URL or attachment ID 1940 if ($wp_field === '_thumbnail_id') { 1941 if (is_numeric($field_value)) { 1942 // It's an attachment ID 1943 set_post_thumbnail($post_id, intval($field_value)); 1944 } elseif (filter_var($field_value, FILTER_VALIDATE_URL)) { 1945 // It's a URL - try to find existing attachment or upload 1946 $attachment_id = $this->get_or_create_attachment_from_url($field_value, $post_id); 1947 if ($attachment_id) { 1948 set_post_thumbnail($post_id, $attachment_id); 1949 } 1950 } 1951 continue; 1952 } 1953 1954 // Special handling for publish_date - validate date format 1955 if ($wp_field === 'post_date') { 1956 $date = strtotime($field_value); 1957 if ($date !== false) { 1958 wp_update_post([ 1959 'ID' => $post_id, 1960 'post_date' => gmdate('Y-m-d H:i:s', $date), 1961 'post_date_gmt' => get_gmt_from_date(gmdate('Y-m-d H:i:s', $date)) 1962 ]); 1963 } 1964 continue; 1965 } 1966 1826 1967 // Update post field directly if it's a post property 1827 if (in_array($wp_field, ['post_parent', 'menu_order', 'comment_status', 'ping_status', 'post_password' ])) {1968 if (in_array($wp_field, ['post_parent', 'menu_order', 'comment_status', 'ping_status', 'post_password', 'post_title', 'post_name', 'post_status', 'post_excerpt'])) { 1828 1969 wp_update_post([ 1829 1970 'ID' => $post_id, … … 2149 2290 'applied' => true 2150 2291 ]); 2292 } 2293 2294 /** 2295 * Get or create attachment from URL 2296 * Used for setting featured images from remote URLs 2297 * 2298 * @param string $url The image URL 2299 * @param int $post_id The post ID to attach the image to 2300 * @return int|false Attachment ID or false on failure 2301 */ 2302 private function get_or_create_attachment_from_url($url, $post_id) { 2303 global $wpdb; 2304 2305 // First check if we already have this URL as an attachment 2306 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 2307 $existing_id = $wpdb->get_var($wpdb->prepare( 2308 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_instarank_source_url' AND meta_value = %s LIMIT 1", 2309 $url 2310 )); 2311 2312 if ($existing_id) { 2313 return intval($existing_id); 2314 } 2315 2316 // Also check by filename in media library 2317 $filename = basename(wp_parse_url($url, PHP_URL_PATH)); 2318 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 2319 $existing_by_name = $wpdb->get_var($wpdb->prepare( 2320 "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_title = %s LIMIT 1", 2321 pathinfo($filename, PATHINFO_FILENAME) 2322 )); 2323 2324 if ($existing_by_name) { 2325 return intval($existing_by_name); 2326 } 2327 2328 // Download and sideload the image 2329 require_once ABSPATH . 'wp-admin/includes/media.php'; 2330 require_once ABSPATH . 'wp-admin/includes/file.php'; 2331 require_once ABSPATH . 'wp-admin/includes/image.php'; 2332 2333 // Download image to temp file 2334 $tmp = download_url($url); 2335 if (is_wp_error($tmp)) { 2336 return false; 2337 } 2338 2339 // Prepare file array for media_handle_sideload 2340 $file_array = [ 2341 'name' => $filename, 2342 'tmp_name' => $tmp 2343 ]; 2344 2345 // Handle the sideload 2346 $attachment_id = media_handle_sideload($file_array, $post_id); 2347 2348 // Clean up temp file 2349 if (file_exists($tmp)) { 2350 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink 2351 unlink($tmp); 2352 } 2353 2354 if (is_wp_error($attachment_id)) { 2355 return false; 2356 } 2357 2358 // Store the source URL for future lookups 2359 update_post_meta($attachment_id, '_instarank_source_url', $url); 2360 2361 return $attachment_id; 2151 2362 } 2152 2363 -
instarank/trunk/includes/class-field-detector.php
r3402479 r3403597 211 211 } 212 212 213 // IMPORTANT: Check for data-custom="field_name" attributes in HTML content 214 // This catches Kadence dynamic fields used in core blocks (headings, paragraphs, etc.) 215 // Pattern: <span ... data-custom="field_name" ... class="kb-inline-dynamic"> 216 preg_match_all('/data-custom="([^"]+)"/', $content, $data_custom_matches); 217 218 if (!empty($data_custom_matches[1])) { 219 foreach ($data_custom_matches[1] as $field_name) { 220 // Skip empty or placeholder values 221 if (empty($field_name) || $field_name === 'No Content') { 222 continue; 223 } 224 225 $fields[] = [ 226 'field_name' => $field_name, 227 'field_type' => $this->guess_field_type($field_name, 'content'), 228 'locations' => ['kadence/inline-dynamic/content'], 229 'required' => true, 230 'page_builder' => 'kadence', 231 'block_type' => 'inline_dynamic', 232 'property' => 'content' 233 ]; 234 } 235 } 236 237 // Also check for fallback values in kadenceDynamic image blocks 238 // Pattern: "fallback":"field_name" or "fallback":"{{field_name}}" 239 preg_match_all('/"fallback":"([^"]+)"/', $content, $fallback_matches); 240 241 if (!empty($fallback_matches[1])) { 242 foreach ($fallback_matches[1] as $field_name) { 243 // Strip {{ }} if present 244 $field_name = preg_replace('/^\{\{|\}\}$/', '', $field_name); 245 246 // Skip empty values or URLs 247 if (empty($field_name) || strpos($field_name, 'http') === 0) { 248 continue; 249 } 250 251 $fields[] = [ 252 'field_name' => $field_name, 253 'field_type' => 'image', 254 'locations' => ['kadence/image/fallback'], 255 'required' => false, 256 'page_builder' => 'kadence', 257 'block_type' => 'image_fallback', 258 'property' => 'url' 259 ]; 260 } 261 } 262 213 263 // Also check for {{placeholder}} syntax (legacy/fallback) 214 264 preg_match_all('/\{\{([a-zA-Z0-9_-]+)\}\}/', $content, $placeholder_matches); … … 443 493 444 494 /** 495 * Get standard WordPress page/post fields that can be mapped 496 * 497 * @param string $post_type Post type (page, post, etc.) 498 * @return array Standard fields 499 */ 500 public function get_standard_fields($post_type = 'page') { 501 $fields = []; 502 503 // Core WordPress fields 504 $core_fields = [ 505 [ 506 'field_name' => 'Page_Title', 507 'field_type' => 'text', 508 'locations' => ['wordpress/post_title'], 509 'required' => true, 510 'page_builder' => 'wordpress', 511 'block_type' => 'core', 512 'property' => 'post_title', 513 'description' => 'WordPress page/post title' 514 ], 515 [ 516 'field_name' => 'Slug', 517 'field_type' => 'text', 518 'locations' => ['wordpress/post_name'], 519 'required' => false, 520 'page_builder' => 'wordpress', 521 'block_type' => 'core', 522 'property' => 'post_name', 523 'description' => 'URL slug' 524 ], 525 [ 526 'field_name' => 'status', 527 'field_type' => 'text', 528 'locations' => ['wordpress/post_status'], 529 'required' => false, 530 'page_builder' => 'wordpress', 531 'block_type' => 'core', 532 'property' => 'post_status', 533 'description' => 'publish, draft, pending, private' 534 ], 535 [ 536 'field_name' => 'excerpt', 537 'field_type' => 'text', 538 'locations' => ['wordpress/post_excerpt'], 539 'required' => false, 540 'page_builder' => 'wordpress', 541 'block_type' => 'core', 542 'property' => 'post_excerpt', 543 'description' => 'Post excerpt/summary' 544 ], 545 [ 546 'field_name' => 'featured_image', 547 'field_type' => 'image', 548 'locations' => ['wordpress/_thumbnail_id'], 549 'required' => false, 550 'page_builder' => 'wordpress', 551 'block_type' => 'core', 552 'property' => '_thumbnail_id', 553 'description' => 'Featured image URL' 554 ], 555 [ 556 'field_name' => 'publish_date', 557 'field_type' => 'date', 558 'locations' => ['wordpress/post_date'], 559 'required' => false, 560 'page_builder' => 'wordpress', 561 'block_type' => 'core', 562 'property' => 'post_date', 563 'description' => 'Publication date (YYYY-MM-DD HH:MM:SS)' 564 ], 565 [ 566 'field_name' => 'author', 567 'field_type' => 'text', 568 'locations' => ['wordpress/post_author'], 569 'required' => false, 570 'page_builder' => 'wordpress', 571 'block_type' => 'core', 572 'property' => 'post_author', 573 'description' => 'Author username or ID' 574 ], 575 [ 576 'field_name' => 'parent', 577 'field_type' => 'text', 578 'locations' => ['wordpress/post_parent'], 579 'required' => false, 580 'page_builder' => 'wordpress', 581 'block_type' => 'core', 582 'property' => 'post_parent', 583 'description' => 'Parent page ID or slug' 584 ], 585 [ 586 'field_name' => 'menu_order', 587 'field_type' => 'number', 588 'locations' => ['wordpress/menu_order'], 589 'required' => false, 590 'page_builder' => 'wordpress', 591 'block_type' => 'core', 592 'property' => 'menu_order', 593 'description' => 'Menu order (for sorting)' 594 ], 595 [ 596 'field_name' => 'comment_status', 597 'field_type' => 'text', 598 'locations' => ['wordpress/comment_status'], 599 'required' => false, 600 'page_builder' => 'wordpress', 601 'block_type' => 'core', 602 'property' => 'comment_status', 603 'description' => 'open or closed' 604 ], 605 [ 606 'field_name' => 'ping_status', 607 'field_type' => 'text', 608 'locations' => ['wordpress/ping_status'], 609 'required' => false, 610 'page_builder' => 'wordpress', 611 'block_type' => 'core', 612 'property' => 'ping_status', 613 'description' => 'open or closed' 614 ], 615 ]; 616 617 // Add category/tag fields for posts 618 if ($post_type === 'post') { 619 $core_fields[] = [ 620 'field_name' => 'category', 621 'field_type' => 'text', 622 'locations' => ['wordpress/category'], 623 'required' => false, 624 'page_builder' => 'wordpress', 625 'block_type' => 'taxonomy', 626 'property' => 'category', 627 'description' => 'Category name(s), comma-separated' 628 ]; 629 $core_fields[] = [ 630 'field_name' => 'tags', 631 'field_type' => 'text', 632 'locations' => ['wordpress/post_tag'], 633 'required' => false, 634 'page_builder' => 'wordpress', 635 'block_type' => 'taxonomy', 636 'property' => 'post_tag', 637 'description' => 'Tag name(s), comma-separated' 638 ]; 639 } 640 641 // Page template field for pages 642 if ($post_type === 'page') { 643 $core_fields[] = [ 644 'field_name' => 'page_template', 645 'field_type' => 'text', 646 'locations' => ['wordpress/_wp_page_template'], 647 'required' => false, 648 'page_builder' => 'wordpress', 649 'block_type' => 'core', 650 'property' => '_wp_page_template', 651 'description' => 'Page template file name' 652 ]; 653 } 654 655 return $core_fields; 656 } 657 658 /** 659 * Get SEO plugin fields based on active SEO plugin 660 * 661 * @return array SEO fields 662 */ 663 public function get_seo_fields() { 664 $fields = []; 665 666 // Detect active SEO plugin 667 $seo_plugin = 'none'; 668 if (defined('WPSEO_VERSION')) { 669 $seo_plugin = 'yoast'; 670 } elseif (class_exists('RankMath')) { 671 $seo_plugin = 'rankmath'; 672 } elseif (defined('AIOSEO_VERSION') || class_exists('AIOSEO')) { 673 $seo_plugin = 'aioseo'; 674 } 675 676 // Common SEO fields (available for all SEO plugins) 677 $seo_fields = [ 678 [ 679 'field_name' => 'SEO_Title', 680 'field_type' => 'text', 681 'locations' => ["seo/{$seo_plugin}/title"], 682 'required' => false, 683 'page_builder' => 'seo', 684 'block_type' => $seo_plugin, 685 'property' => 'title', 686 'description' => 'SEO meta title' 687 ], 688 [ 689 'field_name' => 'SEO_Meta_Description', 690 'field_type' => 'text', 691 'locations' => ["seo/{$seo_plugin}/description"], 692 'required' => false, 693 'page_builder' => 'seo', 694 'block_type' => $seo_plugin, 695 'property' => 'description', 696 'description' => 'SEO meta description' 697 ], 698 [ 699 'field_name' => 'SEO_Focus_Keyword', 700 'field_type' => 'text', 701 'locations' => ["seo/{$seo_plugin}/focus_keyword"], 702 'required' => false, 703 'page_builder' => 'seo', 704 'block_type' => $seo_plugin, 705 'property' => 'focus_keyword', 706 'description' => 'Focus keyphrase for SEO analysis' 707 ], 708 [ 709 'field_name' => 'canonical_url', 710 'field_type' => 'url', 711 'locations' => ["seo/{$seo_plugin}/canonical"], 712 'required' => false, 713 'page_builder' => 'seo', 714 'block_type' => $seo_plugin, 715 'property' => 'canonical', 716 'description' => 'Canonical URL' 717 ], 718 [ 719 'field_name' => 'robots_index', 720 'field_type' => 'text', 721 'locations' => ["seo/{$seo_plugin}/robots_index"], 722 'required' => false, 723 'page_builder' => 'seo', 724 'block_type' => $seo_plugin, 725 'property' => 'robots_index', 726 'description' => 'index or noindex' 727 ], 728 [ 729 'field_name' => 'robots_follow', 730 'field_type' => 'text', 731 'locations' => ["seo/{$seo_plugin}/robots_follow"], 732 'required' => false, 733 'page_builder' => 'seo', 734 'block_type' => $seo_plugin, 735 'property' => 'robots_follow', 736 'description' => 'follow or nofollow' 737 ], 738 [ 739 'field_name' => 'og_title', 740 'field_type' => 'text', 741 'locations' => ["seo/{$seo_plugin}/og_title"], 742 'required' => false, 743 'page_builder' => 'seo', 744 'block_type' => $seo_plugin, 745 'property' => 'og_title', 746 'description' => 'Open Graph title for social sharing' 747 ], 748 [ 749 'field_name' => 'og_description', 750 'field_type' => 'text', 751 'locations' => ["seo/{$seo_plugin}/og_description"], 752 'required' => false, 753 'page_builder' => 'seo', 754 'block_type' => $seo_plugin, 755 'property' => 'og_description', 756 'description' => 'Open Graph description for social sharing' 757 ], 758 [ 759 'field_name' => 'og_image', 760 'field_type' => 'image', 761 'locations' => ["seo/{$seo_plugin}/og_image"], 762 'required' => false, 763 'page_builder' => 'seo', 764 'block_type' => $seo_plugin, 765 'property' => 'og_image', 766 'description' => 'Open Graph image URL for social sharing' 767 ], 768 ]; 769 770 return $seo_fields; 771 } 772 773 /** 774 * Detect all fields including standard and SEO fields 775 * 776 * @param int $post_id Post ID 777 * @param bool $include_standard Include standard WordPress fields 778 * @param bool $include_seo Include SEO plugin fields 779 * @return array All detected fields 780 */ 781 public function detect_all_fields($post_id, $include_standard = true, $include_seo = true) { 782 // Get custom fields from template 783 $result = $this->detect_fields($post_id); 784 785 if (!$result['success']) { 786 return $result; 787 } 788 789 $post = get_post($post_id); 790 $all_fields = $result['fields']; 791 792 // Add standard WordPress fields 793 if ($include_standard) { 794 $standard_fields = $this->get_standard_fields($post->post_type); 795 $result['standard_fields'] = $standard_fields; 796 } 797 798 // Add SEO fields 799 if ($include_seo) { 800 $seo_fields = $this->get_seo_fields(); 801 $result['seo_fields'] = $seo_fields; 802 } 803 804 return $result; 805 } 806 807 /** 445 808 * Guess field type based on field name and context 446 809 * -
instarank/trunk/instarank.php
r3402479 r3403597 4 4 * Plugin URI: https://instarank.com/wordpress-plugin 5 5 * Description: Connect your WordPress site to InstaRank for AI-powered SEO optimization, schema markup generation, and programmatic SEO. Create and sync custom post types, automatically apply SEO improvements, and generate structured data with InstaRank's AI engine. 6 * Version: 1.4. 86 * Version: 1.4.9 7 7 * Author: InstaRank 8 8 * Author URI: https://instarank.com … … 18 18 19 19 // Define plugin constants 20 define('INSTARANK_VERSION', '1.4. 8');20 define('INSTARANK_VERSION', '1.4.9'); 21 21 define('INSTARANK_PLUGIN_DIR', plugin_dir_path(__FILE__)); 22 22 define('INSTARANK_PLUGIN_URL', plugin_dir_url(__FILE__)); -
instarank/trunk/readme.txt
r3402479 r3403597 4 4 Requires at least: 5.6 5 5 Tested up to: 6.8 6 Stable tag: 1.4. 86 Stable tag: 1.4.9 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 159 159 == Changelog == 160 160 161 = 1.4.9 = 162 * Feature: Added DELETE endpoint for custom post types via REST API 163 * Fix: Preserved Unicode escape sequences in Kadence block JSON (fixes corrupted button text) 164 * Fix: Replaced stripcslashes() with targeted escape handling to prevent block corruption 165 161 166 = 1.4.8 = 162 167 * Security: Fixed all WordPress Plugin Check warnings and errors
Note: See TracChangeset
for help on using the changeset viewer.