Plugin Directory

Changeset 3403597


Ignore:
Timestamp:
11/26/2025 05:39:34 PM (4 months ago)
Author:
instarank
Message:

Version 1.4.9: Added DELETE endpoint for custom post types, fixed Kadence block Unicode escaping

Location:
instarank/trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • instarank/trunk/admin/templates-field-mapping.php

    r3402479 r3403597  
    2525}
    2626
    27 // Detect custom fields in template.
     27// Detect custom fields in template (including standard and SEO fields).
    2828$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);
    3030
    3131// Get existing mappings.
     
    3333$instarank_saved_mappings = get_option($instarank_option_name, []);
    3434$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';
     38if (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}
    3545
    3646?>
     
    169179    </div>
    170180
     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
    171330    <!-- Dataset Connection -->
    172331    <div class="instarank-dataset-connection" style="background: white; padding: 20px; margin: 20px 0; border: 1px solid #ccd0d4; border-radius: 4px;">
     
    392551                <?php endforeach; ?>
    393552            </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;">
    396648                <button type="submit" name="instarank_save_field_mappings" class="button button-primary button-large">
    397649                    <span class="dashicons dashicons-saved" style="vertical-align: middle;"></span>
  • instarank/trunk/api/endpoints.php

    r3402479 r3403597  
    144144            'methods' => 'PUT',
    145145            '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'],
    146153            'permission_callback' => [$this, 'verify_api_key']
    147154        ]);
     
    13391346
    13401347    /**
     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    /**
    13411417     * Sync a programmatic page to WordPress
    13421418     */
     
    13611437    // Normalize content if it's a string (fix smart quotes and en-dashes in block comments)
    13621438    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);
    13661447        // Robust replacement for corrupted block comments using regex
    13671448        // Matches <! followed by any dash-like char (Unicode property Pd), and / followed by any dash-like char >
     
    17821863
    17831864            // WordPress core fields that need special handling
     1865            // Keys match the field names defined in class-field-detector.php get_standard_fields()
    17841866            $wp_core_fields = [
    17851867                'parent' => 'post_parent',
     
    17891871                'ping_status' => 'ping_status',
    17901872                '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',
    17911880            ];
    17921881
     
    18241913                    $wp_field = $wp_core_fields[$field_key];
    18251914
     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
    18261967                    // 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'])) {
    18281969                        wp_update_post([
    18291970                            'ID' => $post_id,
     
    21492290            'applied' => true
    21502291        ]);
     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;
    21512362    }
    21522363
  • instarank/trunk/includes/class-field-detector.php

    r3402479 r3403597  
    211211        }
    212212
     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
    213263        // Also check for {{placeholder}} syntax (legacy/fallback)
    214264        preg_match_all('/\{\{([a-zA-Z0-9_-]+)\}\}/', $content, $placeholder_matches);
     
    443493
    444494    /**
     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    /**
    445808     * Guess field type based on field name and context
    446809     *
  • instarank/trunk/instarank.php

    r3402479 r3403597  
    44 * Plugin URI: https://instarank.com/wordpress-plugin
    55 * 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.8
     6 * Version: 1.4.9
    77 * Author: InstaRank
    88 * Author URI: https://instarank.com
     
    1818
    1919// Define plugin constants
    20 define('INSTARANK_VERSION', '1.4.8');
     20define('INSTARANK_VERSION', '1.4.9');
    2121define('INSTARANK_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2222define('INSTARANK_PLUGIN_URL', plugin_dir_url(__FILE__));
  • instarank/trunk/readme.txt

    r3402479 r3403597  
    44Requires at least: 5.6
    55Tested up to: 6.8
    6 Stable tag: 1.4.8
     6Stable tag: 1.4.9
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    159159== Changelog ==
    160160
     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
    161166= 1.4.8 =
    162167* Security: Fixed all WordPress Plugin Check warnings and errors
Note: See TracChangeset for help on using the changeset viewer.