Plugin Directory

Changeset 3419086


Ignore:
Timestamp:
12/13/2025 09:35:13 PM (3 months ago)
Author:
instarank
Message:

Version 2.0.3: AI-powered image metadata generation, WP_Filesystem compliance, new multilang and virtual pages classes

Location:
instarank/trunk
Files:
2 added
3 edited

Legend:

Unmodified
Added
Removed
  • instarank/trunk/api/endpoints.php

    r3418364 r3419086  
    9696        ]);
    9797
     98        // Dynamic Elements endpoint - render a dynamic element
     99        register_rest_route('instarank/v1', '/dynamic-elements/render', [
     100            'methods' => 'POST',
     101            'callback' => [$this, 'render_dynamic_element'],
     102            'permission_callback' => [$this, 'verify_api_key']
     103        ]);
     104
    98105        // NEW: Page Types Support Endpoints
    99106
     
    291298        ]);
    292299
     300        // PAGE BUILDER CSS REGENERATION ENDPOINTS
     301
     302        // Regenerate CSS for a single post
     303        register_rest_route('instarank/v1', '/regenerate-css', [
     304            'methods' => 'POST',
     305            'callback' => [$this, 'regenerate_css'],
     306            'permission_callback' => [$this, 'verify_api_key']
     307        ]);
     308
     309        // Clear all builder caches
     310        register_rest_route('instarank/v1', '/clear-builder-caches', [
     311            'methods' => 'POST',
     312            'callback' => [$this, 'clear_builder_caches'],
     313            'permission_callback' => [$this, 'verify_api_key']
     314        ]);
     315
     316        // Get CSS regeneration status for a post
     317        register_rest_route('instarank/v1', '/css-status/(?P<id>\d+)', [
     318            'methods' => 'GET',
     319            'callback' => [$this, 'get_css_status'],
     320            'permission_callback' => [$this, 'verify_api_key']
     321        ]);
     322
     323        // Schedule CSS regeneration for later
     324        register_rest_route('instarank/v1', '/schedule-css-regeneration', [
     325            'methods' => 'POST',
     326            'callback' => [$this, 'schedule_css_regeneration'],
     327            'permission_callback' => [$this, 'verify_api_key']
     328        ]);
     329
    293330        // SPINTAX ENDPOINTS
    294331
     
    448485            'methods' => 'GET',
    449486            'callback' => [$this, 'get_page_crawl_data'],
     487            'permission_callback' => [$this, 'verify_api_key']
     488        ]);
     489
     490        // === MULTI-LANGUAGE ENDPOINTS ===
     491
     492        // Get multi-language plugin info
     493        register_rest_route('instarank/v1', '/multilang/info', [
     494            'methods' => 'GET',
     495            'callback' => [$this, 'get_multilang_info'],
     496            'permission_callback' => [$this, 'verify_api_key']
     497        ]);
     498
     499        // Set post language
     500        register_rest_route('instarank/v1', '/multilang/set-language', [
     501            'methods' => 'POST',
     502            'callback' => [$this, 'set_post_language'],
     503            'permission_callback' => [$this, 'verify_api_key']
     504        ]);
     505
     506        // Link translations
     507        register_rest_route('instarank/v1', '/multilang/link-translations', [
     508            'methods' => 'POST',
     509            'callback' => [$this, 'link_translations'],
     510            'permission_callback' => [$this, 'verify_api_key']
     511        ]);
     512
     513        // === UNUSED IMAGES DETECTION ENDPOINTS ===
     514
     515        // Analyze media library for unused images with usage tracking
     516        register_rest_route('instarank/v1', '/media/analyze-usage', [
     517            'methods' => 'GET',
     518            'callback' => [$this, 'analyze_media_usage'],
     519            'permission_callback' => [$this, 'verify_api_key']
     520        ]);
     521
     522        // Get detailed usage info for a specific attachment
     523        register_rest_route('instarank/v1', '/media/(?P<id>\d+)/usage', [
     524            'methods' => 'GET',
     525            'callback' => [$this, 'get_attachment_usage'],
     526            'permission_callback' => [$this, 'verify_api_key']
     527        ]);
     528
     529        // Delete attachment and return backup data
     530        register_rest_route('instarank/v1', '/media/(?P<id>\d+)/delete', [
     531            'methods' => 'POST',
     532            'callback' => [$this, 'delete_attachment_with_backup'],
     533            'permission_callback' => [$this, 'verify_api_key']
     534        ]);
     535
     536        // Bulk delete attachments with backup data
     537        register_rest_route('instarank/v1', '/media/bulk-delete', [
     538            'methods' => 'POST',
     539            'callback' => [$this, 'bulk_delete_attachments'],
     540            'permission_callback' => [$this, 'verify_api_key']
     541        ]);
     542
     543        // Restore a deleted attachment from backup data
     544        register_rest_route('instarank/v1', '/media/restore', [
     545            'methods' => 'POST',
     546            'callback' => [$this, 'restore_attachment_from_backup'],
     547            'permission_callback' => [$this, 'verify_api_key']
     548        ]);
     549
     550        // Rename attachment file (SEO-friendly filename)
     551        register_rest_route('instarank/v1', '/media/(?P<id>\d+)/rename', [
     552            'methods' => 'POST',
     553            'callback' => [$this, 'rename_attachment_file'],
     554            'permission_callback' => [$this, 'verify_api_key']
     555        ]);
     556
     557        // Bulk rename attachments
     558        register_rest_route('instarank/v1', '/media/bulk-rename', [
     559            'methods' => 'POST',
     560            'callback' => [$this, 'bulk_rename_attachments'],
     561            'permission_callback' => [$this, 'verify_api_key']
     562        ]);
     563
     564        // Update all image metadata (title, caption, description, alt text)
     565        register_rest_route('instarank/v1', '/media/(?P<id>\d+)/metadata', [
     566            'methods' => 'POST',
     567            'callback' => [$this, 'update_image_metadata'],
     568            'permission_callback' => [$this, 'verify_api_key']
     569        ]);
     570
     571        // Bulk update image metadata
     572        register_rest_route('instarank/v1', '/media/bulk-update-metadata', [
     573            'methods' => 'POST',
     574            'callback' => [$this, 'bulk_update_image_metadata'],
     575            'permission_callback' => [$this, 'verify_api_key']
     576        ]);
     577
     578        // Update image attributes in post content (lazy loading, dimensions)
     579        register_rest_route('instarank/v1', '/content/update-image-attributes', [
     580            'methods' => 'POST',
     581            'callback' => [$this, 'update_content_image_attributes'],
     582            'permission_callback' => [$this, 'verify_api_key']
     583        ]);
     584
     585        // Bulk update image attributes in multiple posts
     586        register_rest_route('instarank/v1', '/content/bulk-update-image-attributes', [
     587            'methods' => 'POST',
     588            'callback' => [$this, 'bulk_update_content_image_attributes'],
    450589            'permission_callback' => [$this, 'verify_api_key']
    451590        ]);
     
    42064345
    42074346    /**
     4347     * Update all image metadata (title, caption, description, alt text)
     4348     *
     4349     * POST /wp-json/instarank/v1/media/123/metadata
     4350     * Body: { "title": "Image Title", "caption": "Short caption", "description": "Full description", "alt_text": "Alt text" }
     4351     */
     4352    public function update_image_metadata($request) {
     4353        $attachment_id = $request->get_param('id');
     4354        $title = $request->get_param('title');
     4355        $caption = $request->get_param('caption');
     4356        $description = $request->get_param('description');
     4357        $alt_text = $request->get_param('alt_text');
     4358
     4359        if (empty($attachment_id)) {
     4360            return new WP_Error('invalid_id', 'Attachment ID is required', ['status' => 400]);
     4361        }
     4362
     4363        // Verify attachment exists
     4364        $attachment = get_post($attachment_id);
     4365        if (!$attachment || $attachment->post_type !== 'attachment') {
     4366            return new WP_Error('invalid_attachment', 'Invalid attachment', ['status' => 404]);
     4367        }
     4368
     4369        $updated_fields = [];
     4370
     4371        // Update post fields (title, caption/excerpt, description/content)
     4372        $post_update = ['ID' => $attachment_id];
     4373
     4374        if ($title !== null) {
     4375            $post_update['post_title'] = sanitize_text_field($title);
     4376            $updated_fields['title'] = $title;
     4377        }
     4378
     4379        if ($caption !== null) {
     4380            $post_update['post_excerpt'] = sanitize_textarea_field($caption);
     4381            $updated_fields['caption'] = $caption;
     4382        }
     4383
     4384        if ($description !== null) {
     4385            $post_update['post_content'] = wp_kses_post($description);
     4386            $updated_fields['description'] = $description;
     4387        }
     4388
     4389        // Update post if any post fields were provided
     4390        if (count($post_update) > 1) {
     4391            $result = wp_update_post($post_update, true);
     4392            if (is_wp_error($result)) {
     4393                return new WP_Error('update_failed', 'Failed to update attachment: ' . $result->get_error_message(), ['status' => 500]);
     4394            }
     4395        }
     4396
     4397        // Update alt text (stored in post meta)
     4398        if ($alt_text !== null) {
     4399            update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($alt_text));
     4400            $updated_fields['alt_text'] = $alt_text;
     4401        }
     4402
     4403        // Get the updated attachment data
     4404        $updated_attachment = get_post($attachment_id);
     4405        $attachment_url = wp_get_attachment_url($attachment_id);
     4406
     4407        return [
     4408            'success' => true,
     4409            'attachment_id' => intval($attachment_id),
     4410            'updated_fields' => $updated_fields,
     4411            'current_values' => [
     4412                'title' => $updated_attachment->post_title,
     4413                'caption' => $updated_attachment->post_excerpt,
     4414                'description' => $updated_attachment->post_content,
     4415                'alt_text' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
     4416                'url' => $attachment_url,
     4417                'admin_url' => admin_url('post.php?post=' . $attachment_id . '&action=edit')
     4418            ]
     4419        ];
     4420    }
     4421
     4422    /**
     4423     * Bulk update image metadata for multiple attachments
     4424     *
     4425     * POST /wp-json/instarank/v1/media/bulk-update-metadata
     4426     * Body: {
     4427     *   "items": [
     4428     *     { "id": 123, "title": "...", "caption": "...", "description": "...", "alt_text": "..." },
     4429     *     { "id": 456, "title": "...", "caption": "...", "description": "...", "alt_text": "..." }
     4430     *   ]
     4431     * }
     4432     */
     4433    public function bulk_update_image_metadata($request) {
     4434        $items = $request->get_param('items');
     4435
     4436        if (empty($items) || !is_array($items)) {
     4437            return new WP_Error('invalid_items', 'Items array is required', ['status' => 400]);
     4438        }
     4439
     4440        $results = [];
     4441        $success_count = 0;
     4442        $failed_count = 0;
     4443
     4444        foreach ($items as $item) {
     4445            $attachment_id = isset($item['id']) ? intval($item['id']) : 0;
     4446
     4447            if (empty($attachment_id)) {
     4448                $results[] = [
     4449                    'id' => $attachment_id,
     4450                    'success' => false,
     4451                    'error' => 'Invalid attachment ID'
     4452                ];
     4453                $failed_count++;
     4454                continue;
     4455            }
     4456
     4457            // Verify attachment exists
     4458            $attachment = get_post($attachment_id);
     4459            if (!$attachment || $attachment->post_type !== 'attachment') {
     4460                $results[] = [
     4461                    'id' => $attachment_id,
     4462                    'success' => false,
     4463                    'error' => 'Attachment not found'
     4464                ];
     4465                $failed_count++;
     4466                continue;
     4467            }
     4468
     4469            $updated_fields = [];
     4470
     4471            // Update post fields
     4472            $post_update = ['ID' => $attachment_id];
     4473
     4474            if (isset($item['title'])) {
     4475                $post_update['post_title'] = sanitize_text_field($item['title']);
     4476                $updated_fields['title'] = $item['title'];
     4477            }
     4478
     4479            if (isset($item['caption'])) {
     4480                $post_update['post_excerpt'] = sanitize_textarea_field($item['caption']);
     4481                $updated_fields['caption'] = $item['caption'];
     4482            }
     4483
     4484            if (isset($item['description'])) {
     4485                $post_update['post_content'] = wp_kses_post($item['description']);
     4486                $updated_fields['description'] = $item['description'];
     4487            }
     4488
     4489            // Update post if any fields were provided
     4490            if (count($post_update) > 1) {
     4491                $result = wp_update_post($post_update, true);
     4492                if (is_wp_error($result)) {
     4493                    $results[] = [
     4494                        'id' => $attachment_id,
     4495                        'success' => false,
     4496                        'error' => $result->get_error_message()
     4497                    ];
     4498                    $failed_count++;
     4499                    continue;
     4500                }
     4501            }
     4502
     4503            // Update alt text
     4504            if (isset($item['alt_text'])) {
     4505                update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($item['alt_text']));
     4506                $updated_fields['alt_text'] = $item['alt_text'];
     4507            }
     4508
     4509            $results[] = [
     4510                'id' => $attachment_id,
     4511                'success' => true,
     4512                'updated_fields' => $updated_fields,
     4513                'admin_url' => admin_url('post.php?post=' . $attachment_id . '&action=edit')
     4514            ];
     4515            $success_count++;
     4516        }
     4517
     4518        return [
     4519            'success' => $success_count > 0,
     4520            'total' => count($items),
     4521            'succeeded' => $success_count,
     4522            'failed' => $failed_count,
     4523            'results' => $results
     4524        ];
     4525    }
     4526
     4527    /**
    42084528     * Find attachment by URL
    42094529     *
     
    56976017        return rest_ensure_response($response);
    56986018    }
     6019
     6020    // ========================================
     6021    // MULTI-LANGUAGE METHODS
     6022    // ========================================
     6023
     6024    /**
     6025     * Get multi-language plugin info
     6026     *
     6027     * @param WP_REST_Request $request The request object.
     6028     * @return WP_REST_Response|WP_Error Response object.
     6029     */
     6030    public function get_multilang_info($request) {
     6031        $multilang = InstaRank_MultiLang::instance();
     6032
     6033        return rest_ensure_response([
     6034            'success' => true,
     6035            'data' => $multilang->get_info(),
     6036        ]);
     6037    }
     6038
     6039    /**
     6040     * Set language for a post
     6041     *
     6042     * @param WP_REST_Request $request The request object.
     6043     * @return WP_REST_Response|WP_Error Response object.
     6044     */
     6045    public function set_post_language($request) {
     6046        $params = $request->get_json_params();
     6047
     6048        if (empty($params['post_id']) || empty($params['lang'])) {
     6049            return new WP_Error(
     6050                'missing_params',
     6051                __('post_id and lang are required', 'instarank'),
     6052                ['status' => 400]
     6053            );
     6054        }
     6055
     6056        $post_id = intval($params['post_id']);
     6057        $lang = sanitize_text_field($params['lang']);
     6058
     6059        // Verify post exists
     6060        if (!get_post($post_id)) {
     6061            return new WP_Error(
     6062                'post_not_found',
     6063                __('Post not found', 'instarank'),
     6064                ['status' => 404]
     6065            );
     6066        }
     6067
     6068        $multilang = InstaRank_MultiLang::instance();
     6069
     6070        if (!$multilang->is_active()) {
     6071            return new WP_Error(
     6072                'no_multilang_plugin',
     6073                __('No multi-language plugin is active (WPML or Polylang required)', 'instarank'),
     6074                ['status' => 400]
     6075            );
     6076        }
     6077
     6078        $success = $multilang->set_post_language($post_id, $lang);
     6079
     6080        if (!$success) {
     6081            return new WP_Error(
     6082                'set_language_failed',
     6083                __('Failed to set post language', 'instarank'),
     6084                ['status' => 500]
     6085            );
     6086        }
     6087
     6088        return rest_ensure_response([
     6089            'success' => true,
     6090            /* translators: 1: language code, 2: post ID */
     6091            'message' => sprintf(__('Language set to %1$s for post %2$d', 'instarank'), $lang, $post_id),
     6092            'data' => [
     6093                'post_id' => $post_id,
     6094                'lang' => $lang,
     6095            ],
     6096        ]);
     6097    }
     6098
     6099    /**
     6100     * Link posts as translations
     6101     *
     6102     * @param WP_REST_Request $request The request object.
     6103     * @return WP_REST_Response|WP_Error Response object.
     6104     */
     6105    public function link_translations($request) {
     6106        $params = $request->get_json_params();
     6107
     6108        if (empty($params['translations']) || !is_array($params['translations'])) {
     6109            return new WP_Error(
     6110                'missing_params',
     6111                __('translations array is required (format: {lang: post_id})', 'instarank'),
     6112                ['status' => 400]
     6113            );
     6114        }
     6115
     6116        $translations = [];
     6117        foreach ($params['translations'] as $lang => $post_id) {
     6118            $lang = sanitize_text_field($lang);
     6119            $post_id = intval($post_id);
     6120
     6121            if (!get_post($post_id)) {
     6122                return new WP_Error(
     6123                    'post_not_found',
     6124                    /* translators: %d: post ID */
     6125                    sprintf(__('Post not found: %d', 'instarank'), $post_id),
     6126                    ['status' => 404]
     6127                );
     6128            }
     6129
     6130            $translations[$lang] = $post_id;
     6131        }
     6132
     6133        if (count($translations) < 2) {
     6134            return new WP_Error(
     6135                'insufficient_translations',
     6136                __('At least 2 translations are required to link', 'instarank'),
     6137                ['status' => 400]
     6138            );
     6139        }
     6140
     6141        $multilang = InstaRank_MultiLang::instance();
     6142
     6143        if (!$multilang->is_active()) {
     6144            return new WP_Error(
     6145                'no_multilang_plugin',
     6146                __('No multi-language plugin is active (WPML or Polylang required)', 'instarank'),
     6147                ['status' => 400]
     6148            );
     6149        }
     6150
     6151        $success = $multilang->link_translations($translations);
     6152
     6153        if (!$success) {
     6154            return new WP_Error(
     6155                'link_translations_failed',
     6156                __('Failed to link translations', 'instarank'),
     6157                ['status' => 500]
     6158            );
     6159        }
     6160
     6161        return rest_ensure_response([
     6162            'success' => true,
     6163            /* translators: %d: number of translations */
     6164            'message' => sprintf(__('Linked %d translations', 'instarank'), count($translations)),
     6165            'data' => [
     6166                'translations' => $translations,
     6167            ],
     6168        ]);
     6169    }
     6170
     6171    /**
     6172     * Render a dynamic element
     6173     *
     6174     * This endpoint is called from InstaRank SaaS to fetch dynamic content
     6175     * that may require WordPress-specific data (e.g., Media Library images,
     6176     * custom field values, etc.)
     6177     *
     6178     * @param WP_REST_Request $request
     6179     * @return WP_REST_Response|WP_Error
     6180     */
     6181    public function render_dynamic_element($request) {
     6182        $params = $request->get_json_params();
     6183
     6184        $shortcode = $params['shortcode'] ?? null;
     6185        $field_data = $params['fieldData'] ?? [];
     6186
     6187        if (empty($shortcode)) {
     6188            return new WP_Error(
     6189                'missing_shortcode',
     6190                'Shortcode is required',
     6191                ['status' => 400]
     6192            );
     6193        }
     6194
     6195        // Log the request for debugging
     6196        file_put_contents(
     6197            __DIR__ . '/../instarank_debug.log',
     6198            gmdate('Y-m-d H:i:s') . " - Dynamic Element Render Request: shortcode={$shortcode}\n",
     6199            FILE_APPEND
     6200        );
     6201
     6202        // For now, return a simple response
     6203        // The actual rendering happens in the SaaS application
     6204        // This endpoint is here for future WordPress-specific rendering needs
     6205        // (e.g., fetching Media Library images, custom field values, etc.)
     6206
     6207        return rest_ensure_response([
     6208            'success' => true,
     6209            'rendered_html' => '<!-- Dynamic element placeholder: ' . esc_html($shortcode) . ' -->',
     6210            'message' => 'Dynamic element rendering is handled by InstaRank SaaS. This endpoint is reserved for WordPress-specific data fetching in future updates.'
     6211        ]);
     6212    }
     6213
     6214    /**
     6215     * PAGE BUILDER CSS REGENERATION METHODS
     6216     */
     6217
     6218    /**
     6219     * Regenerate CSS for a single post
     6220     *
     6221     * @param WP_REST_Request $request
     6222     * @return WP_REST_Response|WP_Error
     6223     */
     6224    public function regenerate_css($request) {
     6225        $params = $request->get_json_params();
     6226        $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0;
     6227        $builder = isset($params['builder']) ? sanitize_text_field($params['builder']) : '';
     6228        $force = isset($params['force']) ? (bool) $params['force'] : true;
     6229
     6230        if (!$post_id) {
     6231            return new WP_Error('missing_post_id', 'post_id is required', ['status' => 400]);
     6232        }
     6233
     6234        $post = get_post($post_id);
     6235        if (!$post) {
     6236            return new WP_Error('post_not_found', 'Post not found', ['status' => 404]);
     6237        }
     6238
     6239        // Auto-detect builder if not provided
     6240        if (empty($builder)) {
     6241            $builder = $this->detect_page_builder($post_id);
     6242        }
     6243
     6244        $result = $this->regenerate_builder_css($post_id, $builder, $force);
     6245
     6246        return rest_ensure_response([
     6247            'success' => $result['success'],
     6248            'message' => $result['message'],
     6249            'builder' => $builder,
     6250            'post_id' => $post_id,
     6251            'cache_cleared' => $result['cache_cleared'] ?? false,
     6252            'css_regenerated' => $result['css_regenerated'] ?? false,
     6253        ]);
     6254    }
     6255
     6256    /**
     6257     * Detect page builder for a post
     6258     *
     6259     * @param int $post_id
     6260     * @return string
     6261     */
     6262    private function detect_page_builder($post_id) {
     6263        // Check Elementor
     6264        if (get_post_meta($post_id, '_elementor_edit_mode', true) === 'builder') {
     6265            return 'elementor';
     6266        }
     6267
     6268        // Check Bricks
     6269        if (get_post_meta($post_id, '_bricks_page_content_2', true)) {
     6270            return 'bricks';
     6271        }
     6272
     6273        // Check Kadence
     6274        if (get_post_meta($post_id, '_kad_blocks_custom_css', true) || get_post_meta($post_id, '_kad_blocks_head_css', true)) {
     6275            return 'kadence';
     6276        }
     6277
     6278        // Check Oxygen
     6279        if (get_post_meta($post_id, 'ct_builder_json', true)) {
     6280            return 'oxygen';
     6281        }
     6282
     6283        // Check Beaver Builder
     6284        if (get_post_meta($post_id, '_fl_builder_enabled', true)) {
     6285            return 'beaver-builder';
     6286        }
     6287
     6288        // Check Divi
     6289        if (get_post_meta($post_id, '_et_pb_use_builder', true) === 'on') {
     6290            return 'divi';
     6291        }
     6292
     6293        // Check for block editor (Gutenberg)
     6294        $post = get_post($post_id);
     6295        if ($post && has_blocks($post->post_content)) {
     6296            return 'gutenberg';
     6297        }
     6298
     6299        return 'unknown';
     6300    }
     6301
     6302    /**
     6303     * Regenerate CSS for a specific builder
     6304     *
     6305     * @param int $post_id
     6306     * @param string $builder
     6307     * @param bool $force
     6308     * @return array
     6309     */
     6310    private function regenerate_builder_css($post_id, $builder, $force = true) {
     6311        $result = ['success' => false, 'message' => '', 'cache_cleared' => false, 'css_regenerated' => false];
     6312
     6313        switch ($builder) {
     6314            case 'elementor':
     6315                if (class_exists('Elementor\Plugin')) {
     6316                    try {
     6317                        // Clear Elementor cache for this post
     6318                        \Elementor\Plugin::$instance->files_manager->clear_cache();
     6319
     6320                        // Regenerate CSS files
     6321                        $css_file = new \Elementor\Core\Files\CSS\Post($post_id);
     6322                        $css_file->update();
     6323
     6324                        $result['success'] = true;
     6325                        $result['message'] = 'Elementor CSS regenerated successfully';
     6326                        $result['cache_cleared'] = true;
     6327                        $result['css_regenerated'] = true;
     6328                    } catch (Exception $e) {
     6329                        $result['message'] = 'Elementor CSS regeneration failed: ' . $e->getMessage();
     6330                    }
     6331                } else {
     6332                    $result['message'] = 'Elementor plugin not active';
     6333                }
     6334                break;
     6335
     6336            case 'bricks':
     6337                if (class_exists('Bricks\Assets')) {
     6338                    try {
     6339                        // Clear Bricks cache
     6340                        \Bricks\Assets::generate_css_file($post_id);
     6341
     6342                        $result['success'] = true;
     6343                        $result['message'] = 'Bricks CSS regenerated successfully';
     6344                        $result['css_regenerated'] = true;
     6345                    } catch (Exception $e) {
     6346                        $result['message'] = 'Bricks CSS regeneration failed: ' . $e->getMessage();
     6347                    }
     6348                } else {
     6349                    $result['message'] = 'Bricks plugin not active';
     6350                }
     6351                break;
     6352
     6353            case 'kadence':
     6354                if (class_exists('Kadence_Blocks_Frontend')) {
     6355                    try {
     6356                        // Clear Kadence cache
     6357                        delete_post_meta($post_id, '_kad_blocks_custom_css');
     6358                        delete_post_meta($post_id, '_kad_blocks_head_css');
     6359
     6360                        // Trigger regeneration on next page load - using Kadence Blocks hook
     6361                        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
     6362                        do_action('kadence_blocks_force_render_inline_css_in_content', true);
     6363
     6364                        $result['success'] = true;
     6365                        $result['message'] = 'Kadence CSS cache cleared - will regenerate on next page load';
     6366                        $result['cache_cleared'] = true;
     6367                    } catch (Exception $e) {
     6368                        $result['message'] = 'Kadence CSS cache clear failed: ' . $e->getMessage();
     6369                    }
     6370                } else {
     6371                    $result['message'] = 'Kadence Blocks plugin not active';
     6372                }
     6373                break;
     6374
     6375            case 'oxygen':
     6376                if (defined('CT_VERSION')) {
     6377                    try {
     6378                        // Clear Oxygen cache
     6379                        delete_option('oxygen_vsb_universal_css_cache');
     6380                        delete_post_meta($post_id, 'ct_builder_shortcodes');
     6381
     6382                        $result['success'] = true;
     6383                        $result['message'] = 'Oxygen CSS cache cleared';
     6384                        $result['cache_cleared'] = true;
     6385                    } catch (Exception $e) {
     6386                        $result['message'] = 'Oxygen CSS cache clear failed: ' . $e->getMessage();
     6387                    }
     6388                } else {
     6389                    $result['message'] = 'Oxygen plugin not active';
     6390                }
     6391                break;
     6392
     6393            case 'beaver-builder':
     6394                if (class_exists('FLBuilder')) {
     6395                    try {
     6396                        // Clear Beaver Builder cache
     6397                        \FLBuilderModel::delete_asset_cache_for_post($post_id);
     6398
     6399                        $result['success'] = true;
     6400                        $result['message'] = 'Beaver Builder CSS cache cleared';
     6401                        $result['cache_cleared'] = true;
     6402                    } catch (Exception $e) {
     6403                        $result['message'] = 'Beaver Builder CSS cache clear failed: ' . $e->getMessage();
     6404                    }
     6405                } else {
     6406                    $result['message'] = 'Beaver Builder plugin not active';
     6407                }
     6408                break;
     6409
     6410            case 'divi':
     6411                if (function_exists('et_core_clear_wp_cache')) {
     6412                    try {
     6413                        // Clear Divi cache
     6414                        et_core_clear_wp_cache($post_id);
     6415                        delete_post_meta($post_id, '_et_pb_static_css_file');
     6416
     6417                        $result['success'] = true;
     6418                        $result['message'] = 'Divi CSS cache cleared';
     6419                        $result['cache_cleared'] = true;
     6420                    } catch (Exception $e) {
     6421                        $result['message'] = 'Divi CSS cache clear failed: ' . $e->getMessage();
     6422                    }
     6423                } else {
     6424                    $result['message'] = 'Divi theme/plugin not active';
     6425                }
     6426                break;
     6427
     6428            case 'gutenberg':
     6429                // Gutenberg doesn't need CSS regeneration
     6430                $result['success'] = true;
     6431                $result['message'] = 'Gutenberg (Block Editor) - no CSS regeneration needed';
     6432                break;
     6433
     6434            default:
     6435                $result['message'] = 'Unknown or unsupported page builder: ' . $builder;
     6436        }
     6437
     6438        return $result;
     6439    }
     6440
     6441    /**
     6442     * Clear all builder caches
     6443     *
     6444     * @param WP_REST_Request $request
     6445     * @return WP_REST_Response|WP_Error
     6446     */
     6447    public function clear_builder_caches($request) {
     6448        $params = $request->get_json_params();
     6449        $builders = isset($params['builders']) ? $params['builders'] : ['elementor', 'bricks', 'kadence', 'oxygen', 'beaver-builder', 'divi'];
     6450
     6451        $cleared = [];
     6452
     6453        foreach ($builders as $builder) {
     6454            $builder = sanitize_text_field($builder);
     6455
     6456            switch ($builder) {
     6457                case 'elementor':
     6458                    if (class_exists('Elementor\Plugin')) {
     6459                        \Elementor\Plugin::$instance->files_manager->clear_cache();
     6460                        $cleared[] = 'elementor';
     6461                    }
     6462                    break;
     6463
     6464                case 'bricks':
     6465                    if (class_exists('Bricks\Assets')) {
     6466                        \Bricks\Assets::clear_cache();
     6467                        $cleared[] = 'bricks';
     6468                    }
     6469                    break;
     6470
     6471                case 'kadence':
     6472                    if (class_exists('Kadence_Blocks_Frontend')) {
     6473                        global $wpdb;
     6474                        // Delete Kadence Blocks CSS cache - direct query needed as no API exists
     6475                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6476                        $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->postmeta} WHERE meta_key IN (%s, %s)", '_kad_blocks_custom_css', '_kad_blocks_head_css'));
     6477                        $cleared[] = 'kadence';
     6478                    }
     6479                    break;
     6480
     6481                case 'oxygen':
     6482                    if (defined('CT_VERSION')) {
     6483                        delete_option('oxygen_vsb_universal_css_cache');
     6484                        $cleared[] = 'oxygen';
     6485                    }
     6486                    break;
     6487
     6488                case 'beaver-builder':
     6489                    if (class_exists('FLBuilder')) {
     6490                        \FLBuilderModel::delete_all_asset_cache();
     6491                        $cleared[] = 'beaver-builder';
     6492                    }
     6493                    break;
     6494
     6495                case 'divi':
     6496                    if (function_exists('et_core_clear_wp_cache')) {
     6497                        et_core_clear_wp_cache();
     6498                        $cleared[] = 'divi';
     6499                    }
     6500                    break;
     6501            }
     6502        }
     6503
     6504        return rest_ensure_response([
     6505            'success' => true,
     6506            'message' => 'Builder caches cleared',
     6507            'builders_cleared' => $cleared,
     6508        ]);
     6509    }
     6510
     6511    /**
     6512     * Get CSS regeneration status for a post
     6513     *
     6514     * @param WP_REST_Request $request
     6515     * @return WP_REST_Response|WP_Error
     6516     */
     6517    public function get_css_status($request) {
     6518        $post_id = intval($request['id']);
     6519
     6520        if (!$post_id) {
     6521            return new WP_Error('missing_post_id', 'post_id is required', ['status' => 400]);
     6522        }
     6523
     6524        $post = get_post($post_id);
     6525        if (!$post) {
     6526            return new WP_Error('post_not_found', 'Post not found', ['status' => 404]);
     6527        }
     6528
     6529        $builder = $this->detect_page_builder($post_id);
     6530        $css_exists = false;
     6531        $css_file_path = '';
     6532        $last_regenerated = '';
     6533
     6534        // Check for CSS file existence based on builder
     6535        switch ($builder) {
     6536            case 'elementor':
     6537                $upload_dir = wp_upload_dir();
     6538                $css_file_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
     6539                $css_exists = file_exists($css_file_path);
     6540                if ($css_exists) {
     6541                    $last_regenerated = gmdate('Y-m-d H:i:s', filemtime($css_file_path));
     6542                }
     6543                break;
     6544
     6545            case 'bricks':
     6546                $upload_dir = wp_upload_dir();
     6547                $css_file_path = $upload_dir['basedir'] . '/bricks/css/post-' . $post_id . '.css';
     6548                $css_exists = file_exists($css_file_path);
     6549                if ($css_exists) {
     6550                    $last_regenerated = gmdate('Y-m-d H:i:s', filemtime($css_file_path));
     6551                }
     6552                break;
     6553
     6554            default:
     6555                $css_exists = true; // Assume CSS exists for other builders
     6556        }
     6557
     6558        return rest_ensure_response([
     6559            'success' => true,
     6560            'builder' => $builder,
     6561            'css_exists' => $css_exists,
     6562            'css_file_path' => $css_file_path,
     6563            'last_regenerated' => $last_regenerated,
     6564        ]);
     6565    }
     6566
     6567    /**
     6568     * Schedule CSS regeneration for later
     6569     *
     6570     * @param WP_REST_Request $request
     6571     * @return WP_REST_Response|WP_Error
     6572     */
     6573    public function schedule_css_regeneration($request) {
     6574        $params = $request->get_json_params();
     6575        $post_ids = isset($params['post_ids']) ? $params['post_ids'] : [];
     6576        $schedule_time = isset($params['schedule_time']) ? $params['schedule_time'] : null;
     6577
     6578        if (empty($post_ids) || !is_array($post_ids)) {
     6579            return new WP_Error('missing_post_ids', 'post_ids array is required', ['status' => 400]);
     6580        }
     6581
     6582        // For now, we'll use WordPress cron to schedule the regeneration
     6583        // In a production environment, you'd want a more robust job queue
     6584
     6585        $timestamp = $schedule_time ? strtotime($schedule_time) : time() + 60; // Default: 1 minute from now
     6586        $job_id = 'css_regen_' . md5(json_encode($post_ids) . $timestamp);
     6587
     6588        // Schedule the event
     6589        wp_schedule_single_event($timestamp, 'instarank_regenerate_css_batch', [$post_ids]);
     6590
     6591        return rest_ensure_response([
     6592            'success' => true,
     6593            'message' => 'CSS regeneration scheduled',
     6594            'job_id' => $job_id,
     6595            'scheduled_count' => count($post_ids),
     6596            'scheduled_time' => gmdate('Y-m-d H:i:s', $timestamp),
     6597        ]);
     6598    }
     6599
     6600    // ========================================
     6601    // UNUSED IMAGES DETECTION METHODS
     6602    // ========================================
     6603
     6604    /**
     6605     * Analyze media library for unused images
     6606     * Returns all images with their usage count and locations
     6607     *
     6608     * GET /wp-json/instarank/v1/media/analyze-usage
     6609     * Parameters: per_page, page, unused_only (true/false)
     6610     */
     6611    public function analyze_media_usage($request) {
     6612        $per_page = intval($request->get_param('per_page')) ?: 100;
     6613        $page = intval($request->get_param('page')) ?: 1;
     6614        $unused_only = $request->get_param('unused_only') === 'true';
     6615
     6616        // Limit per_page to prevent timeouts
     6617        $per_page = min($per_page, 100);
     6618
     6619        // Get all image attachments
     6620        $args = [
     6621            'post_type' => 'attachment',
     6622            'post_status' => 'inherit',
     6623            'posts_per_page' => $per_page,
     6624            'paged' => $page,
     6625            'post_mime_type' => 'image',
     6626            'orderby' => 'date',
     6627            'order' => 'DESC'
     6628        ];
     6629
     6630        $query = new WP_Query($args);
     6631        $results = [];
     6632
     6633        foreach ($query->posts as $attachment) {
     6634            $usage = $this->analyze_single_attachment_usage($attachment->ID);
     6635
     6636            // Skip if filtering for unused only and this image is used
     6637            if ($unused_only && $usage['usage_count'] > 0) {
     6638                continue;
     6639            }
     6640
     6641            $file_path = get_attached_file($attachment->ID);
     6642            $file_size = ($file_path && file_exists($file_path)) ? filesize($file_path) : 0;
     6643            $metadata = wp_get_attachment_metadata($attachment->ID);
     6644
     6645            $results[] = [
     6646                'id' => $attachment->ID,
     6647                'url' => wp_get_attachment_url($attachment->ID),
     6648                'filename' => basename($file_path ?: $attachment->guid),
     6649                'mime_type' => $attachment->post_mime_type,
     6650                'file_size' => $file_size,
     6651                'width' => isset($metadata['width']) ? $metadata['width'] : null,
     6652                'height' => isset($metadata['height']) ? $metadata['height'] : null,
     6653                'alt' => get_post_meta($attachment->ID, '_wp_attachment_image_alt', true),
     6654                'title' => $attachment->post_title,
     6655                'caption' => $attachment->post_excerpt,
     6656                'description' => $attachment->post_content,
     6657                'uploaded_date' => $attachment->post_date,
     6658                'usage_count' => $usage['usage_count'],
     6659                'usage_locations' => $usage['locations'],
     6660                'is_featured_image' => $usage['is_featured_image'],
     6661                'detection_type' => $this->determine_detection_type($usage)
     6662            ];
     6663        }
     6664
     6665        return rest_ensure_response([
     6666            'success' => true,
     6667            'images' => $results,
     6668            'total' => $query->found_posts,
     6669            'page' => $page,
     6670            'per_page' => $per_page,
     6671            'total_pages' => $query->max_num_pages
     6672        ]);
     6673    }
     6674
     6675    /**
     6676     * Analyze usage for a single attachment
     6677     * Checks all possible locations where an image might be used
     6678     *
     6679     * @param int $attachment_id The attachment ID
     6680     * @return array Usage data with count and locations
     6681     */
     6682    private function analyze_single_attachment_usage($attachment_id) {
     6683        global $wpdb;
     6684
     6685        $locations = [];
     6686        $usage_count = 0;
     6687        $is_featured_image = false;
     6688
     6689        $attachment_url = wp_get_attachment_url($attachment_id);
     6690
     6691        // 1. Check if used as featured image (thumbnail)
     6692        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6693        $featured_posts = $wpdb->get_results($wpdb->prepare(
     6694            "SELECT pm.post_id FROM {$wpdb->postmeta} pm
     6695             INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
     6696             WHERE pm.meta_key = '_thumbnail_id' AND pm.meta_value = %d
     6697             AND p.post_status IN ('publish', 'draft', 'pending', 'private')",
     6698            $attachment_id
     6699        ));
     6700
     6701        foreach ($featured_posts as $row) {
     6702            $post = get_post($row->post_id);
     6703            if ($post) {
     6704                $is_featured_image = true;
     6705                $usage_count++;
     6706                $locations[] = [
     6707                    'type' => 'featured_image',
     6708                    'post_id' => $post->ID,
     6709                    'post_title' => $post->post_title,
     6710                    'post_url' => get_permalink($post->ID),
     6711                    'post_type' => $post->post_type
     6712                ];
     6713            }
     6714        }
     6715
     6716        // 2. Check post content (posts, pages, custom post types)
     6717        // Look for both full URL and wp-image-{id} class
     6718        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6719        $content_posts = $wpdb->get_results($wpdb->prepare(
     6720            "SELECT ID, post_title, post_type, post_status FROM {$wpdb->posts}
     6721             WHERE post_status IN ('publish', 'draft', 'pending', 'private')
     6722             AND post_type NOT IN ('attachment', 'revision', 'nav_menu_item')
     6723             AND (post_content LIKE %s OR post_content LIKE %s)",
     6724            '%' . $wpdb->esc_like($attachment_url) . '%',
     6725            '%wp-image-' . $attachment_id . '%'
     6726        ));
     6727
     6728        foreach ($content_posts as $post) {
     6729            // Check if we already counted this post (as featured image)
     6730            $already_counted = false;
     6731            foreach ($locations as $loc) {
     6732                if (isset($loc['post_id']) && $loc['post_id'] == $post->ID && $loc['type'] === 'featured_image') {
     6733                    $already_counted = true;
     6734                    break;
     6735                }
     6736            }
     6737
     6738            if (!$already_counted) {
     6739                $usage_count++;
     6740                $locations[] = [
     6741                    'type' => 'post_content',
     6742                    'post_id' => $post->ID,
     6743                    'post_title' => $post->post_title,
     6744                    'post_url' => get_permalink($post->ID),
     6745                    'post_type' => $post->post_type
     6746                ];
     6747            }
     6748        }
     6749
     6750        // 3. Check page builder meta (Elementor, Kadence, Divi, Beaver Builder, Bricks)
     6751        $builder_meta_keys = [
     6752            '_elementor_data' => 'Elementor',
     6753            '_kadence_blocks_meta' => 'Kadence',
     6754            '_et_builder_version' => 'Divi',
     6755            '_fl_builder_data' => 'Beaver Builder',
     6756            '_bricks_page_content_2' => 'Bricks'
     6757        ];
     6758
     6759        foreach ($builder_meta_keys as $meta_key => $builder_name) {
     6760            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6761            $builder_posts = $wpdb->get_results($wpdb->prepare(
     6762                "SELECT pm.post_id FROM {$wpdb->postmeta} pm
     6763                 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
     6764                 WHERE pm.meta_key = %s
     6765                 AND (pm.meta_value LIKE %s OR pm.meta_value LIKE %s)
     6766                 AND p.post_status IN ('publish', 'draft', 'pending', 'private')",
     6767                $meta_key,
     6768                '%' . $wpdb->esc_like($attachment_url) . '%',
     6769                '%' . $wpdb->esc_like('"id":' . $attachment_id) . '%'
     6770            ));
     6771
     6772            foreach ($builder_posts as $row) {
     6773                // Check if already counted
     6774                $already_counted = false;
     6775                foreach ($locations as $loc) {
     6776                    if (isset($loc['post_id']) && $loc['post_id'] == $row->post_id) {
     6777                        $already_counted = true;
     6778                        break;
     6779                    }
     6780                }
     6781
     6782                if (!$already_counted) {
     6783                    $post = get_post($row->post_id);
     6784                    if ($post) {
     6785                        $usage_count++;
     6786                        $locations[] = [
     6787                            'type' => 'page_builder',
     6788                            'builder' => $builder_name,
     6789                            'post_id' => $post->ID,
     6790                            'post_title' => $post->post_title,
     6791                            'post_url' => get_permalink($post->ID),
     6792                            'post_type' => $post->post_type
     6793                        ];
     6794                    }
     6795                }
     6796            }
     6797        }
     6798
     6799        // 4. Check theme customizer/options
     6800        $theme_mods = get_theme_mods();
     6801        if ($theme_mods && is_array($theme_mods)) {
     6802            $options_str = wp_json_encode($theme_mods);
     6803            if (strpos($options_str, $attachment_url) !== false ||
     6804                strpos($options_str, '"' . $attachment_id . '"') !== false) {
     6805                $usage_count++;
     6806                $locations[] = [
     6807                    'type' => 'theme_customizer',
     6808                    'location' => 'Theme Settings'
     6809                ];
     6810            }
     6811        }
     6812
     6813        // 5. Check widgets
     6814        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6815        $widget_options = $wpdb->get_results(
     6816            "SELECT option_name, option_value FROM {$wpdb->options}
     6817             WHERE option_name LIKE 'widget_%'"
     6818        );
     6819
     6820        foreach ($widget_options as $widget) {
     6821            if (strpos($widget->option_value, $attachment_url) !== false ||
     6822                strpos($widget->option_value, '"' . $attachment_id . '"') !== false) {
     6823                $usage_count++;
     6824                $locations[] = [
     6825                    'type' => 'widget',
     6826                    'widget_name' => str_replace('widget_', '', $widget->option_name)
     6827                ];
     6828                break; // Count widgets as one usage
     6829            }
     6830        }
     6831
     6832        // 6. Check WooCommerce product galleries
     6833        if (class_exists('WooCommerce')) {
     6834            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     6835            $gallery_posts = $wpdb->get_results($wpdb->prepare(
     6836                "SELECT pm.post_id FROM {$wpdb->postmeta} pm
     6837                 INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
     6838                 WHERE pm.meta_key = '_product_image_gallery'
     6839                 AND pm.meta_value LIKE %s
     6840                 AND p.post_status IN ('publish', 'draft', 'pending', 'private')",
     6841                '%' . $attachment_id . '%'
     6842            ));
     6843
     6844            foreach ($gallery_posts as $row) {
     6845                // Verify it's actually in the gallery (not just substring match)
     6846                $gallery = get_post_meta($row->post_id, '_product_image_gallery', true);
     6847                $gallery_ids = explode(',', $gallery);
     6848                if (in_array($attachment_id, array_map('intval', $gallery_ids))) {
     6849                    $post = get_post($row->post_id);
     6850                    if ($post) {
     6851                        $usage_count++;
     6852                        $locations[] = [
     6853                            'type' => 'woocommerce_gallery',
     6854                            'post_id' => $post->ID,
     6855                            'post_title' => $post->post_title,
     6856                            'post_url' => get_permalink($post->ID)
     6857                        ];
     6858                    }
     6859                }
     6860            }
     6861        }
     6862
     6863        // 7. Check site icon and logo
     6864        $site_icon_id = get_option('site_icon');
     6865        if ($site_icon_id && intval($site_icon_id) === intval($attachment_id)) {
     6866            $usage_count++;
     6867            $locations[] = [
     6868                'type' => 'site_icon',
     6869                'location' => 'Site Icon (Favicon)'
     6870            ];
     6871        }
     6872
     6873        $custom_logo_id = get_theme_mod('custom_logo');
     6874        if ($custom_logo_id && intval($custom_logo_id) === intval($attachment_id)) {
     6875            $usage_count++;
     6876            $locations[] = [
     6877                'type' => 'site_logo',
     6878                'location' => 'Site Logo'
     6879            ];
     6880        }
     6881
     6882        return [
     6883            'usage_count' => $usage_count,
     6884            'locations' => $locations,
     6885            'is_featured_image' => $is_featured_image
     6886        ];
     6887    }
     6888
     6889    /**
     6890     * Determine detection type based on usage analysis
     6891     *
     6892     * @param array $usage Usage analysis result
     6893     * @return string|null Detection type or null if not unused
     6894     */
     6895    private function determine_detection_type($usage) {
     6896        if ($usage['usage_count'] === 0) {
     6897            return 'media_library_orphan';
     6898        } elseif ($usage['usage_count'] === 1) {
     6899            return 'low_usage';
     6900        }
     6901        return null;
     6902    }
     6903
     6904    /**
     6905     * Get detailed usage info for a specific attachment
     6906     *
     6907     * GET /wp-json/instarank/v1/media/{id}/usage
     6908     */
     6909    public function get_attachment_usage($request) {
     6910        $attachment_id = intval($request->get_param('id'));
     6911
     6912        if (!$attachment_id) {
     6913            return new WP_Error('invalid_id', 'Attachment ID is required', ['status' => 400]);
     6914        }
     6915
     6916        $attachment = get_post($attachment_id);
     6917        if (!$attachment || $attachment->post_type !== 'attachment') {
     6918            return new WP_Error('not_found', 'Attachment not found', ['status' => 404]);
     6919        }
     6920
     6921        $usage = $this->analyze_single_attachment_usage($attachment_id);
     6922
     6923        return rest_ensure_response([
     6924            'success' => true,
     6925            'attachment_id' => $attachment_id,
     6926            'filename' => basename(get_attached_file($attachment_id) ?: $attachment->guid),
     6927            'url' => wp_get_attachment_url($attachment_id),
     6928            'usage' => $usage
     6929        ]);
     6930    }
     6931
     6932    /**
     6933     * Delete attachment and return backup data for restore
     6934     *
     6935     * POST /wp-json/instarank/v1/media/{id}/delete
     6936     */
     6937    public function delete_attachment_with_backup($request) {
     6938        $attachment_id = intval($request->get_param('id'));
     6939
     6940        if (!$attachment_id) {
     6941            return new WP_Error('invalid_id', 'Attachment ID is required', ['status' => 400]);
     6942        }
     6943
     6944        $attachment = get_post($attachment_id);
     6945        if (!$attachment || $attachment->post_type !== 'attachment') {
     6946            return new WP_Error('not_found', 'Attachment not found', ['status' => 404]);
     6947        }
     6948
     6949        // Note: Permission is already verified via API key authentication in permission_callback
     6950        // The API key grants full access to the site's media operations
     6951
     6952        // Get all metadata before deletion
     6953        $file_path = get_attached_file($attachment_id);
     6954        $url = wp_get_attachment_url($attachment_id);
     6955        $metadata = wp_get_attachment_metadata($attachment_id);
     6956
     6957        // Read file content for backup (base64 encoded)
     6958        $file_content = null;
     6959        $file_size = 0;
     6960        if ($file_path && file_exists($file_path)) {
     6961            $file_size = filesize($file_path);
     6962            // Only encode files under 10MB to prevent memory issues
     6963            if ($file_size < 10 * 1024 * 1024) {
     6964                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
     6965                $file_content = base64_encode(file_get_contents($file_path));
     6966            }
     6967        }
     6968
     6969        $backup_data = [
     6970            'original_id' => $attachment_id,
     6971            'url' => $url,
     6972            'filename' => basename($file_path ?: $attachment->guid),
     6973            'mime_type' => $attachment->post_mime_type,
     6974            'file_size' => $file_size,
     6975            'alt_text' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
     6976            'title' => $attachment->post_title,
     6977            'caption' => $attachment->post_excerpt,
     6978            'description' => $attachment->post_content,
     6979            'metadata' => $metadata,
     6980            'file_content' => $file_content
     6981        ];
     6982
     6983        // Delete the attachment (this also deletes the files)
     6984        $deleted = wp_delete_attachment($attachment_id, true);
     6985
     6986        if (!$deleted) {
     6987            return new WP_Error('delete_failed', 'Failed to delete attachment', ['status' => 500]);
     6988        }
     6989
     6990        return rest_ensure_response([
     6991            'success' => true,
     6992            'message' => 'Attachment deleted successfully',
     6993            'backup_data' => $backup_data
     6994        ]);
     6995    }
     6996
     6997    /**
     6998     * Bulk delete attachments with backup data
     6999     *
     7000     * POST /wp-json/instarank/v1/media/bulk-delete
     7001     * Body: { "attachment_ids": [1, 2, 3] }
     7002     */
     7003    public function bulk_delete_attachments($request) {
     7004        $params = $request->get_json_params();
     7005        $attachment_ids = isset($params['attachment_ids']) ? $params['attachment_ids'] : [];
     7006
     7007        if (empty($attachment_ids) || !is_array($attachment_ids)) {
     7008            return new WP_Error('invalid_request', 'attachment_ids array is required', ['status' => 400]);
     7009        }
     7010
     7011        // Limit batch size
     7012        if (count($attachment_ids) > 50) {
     7013            return new WP_Error('too_many', 'Maximum 50 attachments per request', ['status' => 400]);
     7014        }
     7015
     7016        $results = [];
     7017        $success_count = 0;
     7018        $failed_count = 0;
     7019
     7020        foreach ($attachment_ids as $attachment_id) {
     7021            $attachment_id = intval($attachment_id);
     7022            $attachment = get_post($attachment_id);
     7023
     7024            if (!$attachment || $attachment->post_type !== 'attachment') {
     7025                $failed_count++;
     7026                $results[] = [
     7027                    'id' => $attachment_id,
     7028                    'success' => false,
     7029                    'error' => 'Not found'
     7030                ];
     7031                continue;
     7032            }
     7033
     7034            // Note: Permission is already verified via API key authentication in permission_callback
     7035            // The API key grants full access to the site's media operations
     7036
     7037            // Get backup data
     7038            $file_path = get_attached_file($attachment_id);
     7039            $file_content = null;
     7040            $file_size = 0;
     7041
     7042            if ($file_path && file_exists($file_path)) {
     7043                $file_size = filesize($file_path);
     7044                if ($file_size < 10 * 1024 * 1024) {
     7045                    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
     7046                    $file_content = base64_encode(file_get_contents($file_path));
     7047                }
     7048            }
     7049
     7050            $backup_data = [
     7051                'original_id' => $attachment_id,
     7052                'url' => wp_get_attachment_url($attachment_id),
     7053                'filename' => basename($file_path ?: $attachment->guid),
     7054                'mime_type' => $attachment->post_mime_type,
     7055                'file_size' => $file_size,
     7056                'alt_text' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
     7057                'title' => $attachment->post_title,
     7058                'caption' => $attachment->post_excerpt,
     7059                'description' => $attachment->post_content,
     7060                'file_content' => $file_content
     7061            ];
     7062
     7063            $deleted = wp_delete_attachment($attachment_id, true);
     7064
     7065            if ($deleted) {
     7066                $success_count++;
     7067                $results[] = [
     7068                    'id' => $attachment_id,
     7069                    'success' => true,
     7070                    'backup_data' => $backup_data
     7071                ];
     7072            } else {
     7073                $failed_count++;
     7074                $results[] = [
     7075                    'id' => $attachment_id,
     7076                    'success' => false,
     7077                    'error' => 'Delete failed'
     7078                ];
     7079            }
     7080        }
     7081
     7082        return rest_ensure_response([
     7083            'success' => $failed_count === 0,
     7084            'total' => count($attachment_ids),
     7085            'deleted' => $success_count,
     7086            'failed' => $failed_count,
     7087            'results' => $results
     7088        ]);
     7089    }
     7090
     7091    /**
     7092     * Restore a deleted attachment from backup data
     7093     *
     7094     * POST /wp-json/instarank/v1/media/restore
     7095     * Body: { "filename": "image.jpg", "file_content": "base64...", "mime_type": "image/jpeg", ... }
     7096     */
     7097    public function restore_attachment_from_backup($request) {
     7098        $params = $request->get_json_params();
     7099
     7100        $required_fields = ['filename', 'file_content', 'mime_type'];
     7101        foreach ($required_fields as $field) {
     7102            if (empty($params[$field])) {
     7103                return new WP_Error('invalid_request', "$field is required", ['status' => 400]);
     7104            }
     7105        }
     7106
     7107        // Note: Permission is already verified via API key authentication in permission_callback
     7108        // The API key grants full access to the site's media operations
     7109
     7110        // Decode file content
     7111        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
     7112        $file_content = base64_decode($params['file_content']);
     7113        if ($file_content === false) {
     7114            return new WP_Error('invalid_content', 'Invalid file content (base64 decode failed)', ['status' => 400]);
     7115        }
     7116
     7117        // Create temp file
     7118        $upload_dir = wp_upload_dir();
     7119        $unique_filename = wp_unique_filename($upload_dir['path'], sanitize_file_name($params['filename']));
     7120        $temp_file = $upload_dir['path'] . '/' . $unique_filename;
     7121
     7122        // Write file
     7123        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
     7124        $written = file_put_contents($temp_file, $file_content);
     7125        if ($written === false) {
     7126            return new WP_Error('write_failed', 'Failed to write file', ['status' => 500]);
     7127        }
     7128
     7129        // Prepare attachment data
     7130        $attachment = [
     7131            'post_mime_type' => sanitize_mime_type($params['mime_type']),
     7132            'post_title' => isset($params['title']) ? sanitize_text_field($params['title']) : sanitize_file_name(pathinfo($params['filename'], PATHINFO_FILENAME)),
     7133            'post_content' => isset($params['description']) ? wp_kses_post($params['description']) : '',
     7134            'post_excerpt' => isset($params['caption']) ? sanitize_text_field($params['caption']) : '',
     7135            'post_status' => 'inherit'
     7136        ];
     7137
     7138        // Insert attachment
     7139        $attachment_id = wp_insert_attachment($attachment, $temp_file);
     7140
     7141        if (is_wp_error($attachment_id)) {
     7142            // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
     7143            @unlink($temp_file);
     7144            return new WP_Error('insert_failed', 'Failed to create attachment: ' . $attachment_id->get_error_message(), ['status' => 500]);
     7145        }
     7146
     7147        // Generate metadata
     7148        require_once ABSPATH . 'wp-admin/includes/image.php';
     7149        $attach_data = wp_generate_attachment_metadata($attachment_id, $temp_file);
     7150        wp_update_attachment_metadata($attachment_id, $attach_data);
     7151
     7152        // Set alt text if provided
     7153        if (!empty($params['alt_text'])) {
     7154            update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($params['alt_text']));
     7155        }
     7156
     7157        return rest_ensure_response([
     7158            'success' => true,
     7159            'attachment_id' => $attachment_id,
     7160            'url' => wp_get_attachment_url($attachment_id),
     7161            'message' => 'Attachment restored successfully'
     7162        ]);
     7163    }
     7164
     7165    /**
     7166     * Rename an attachment file to an SEO-friendly filename
     7167     *
     7168     * POST /wp-json/instarank/v1/media/{id}/rename
     7169     * Body: { "new_filename": "seo-friendly-name" } (without extension)
     7170     */
     7171    public function rename_attachment_file($request) {
     7172        $attachment_id = intval($request->get_param('id'));
     7173        $params = $request->get_json_params();
     7174
     7175        if (!$attachment_id) {
     7176            return new WP_Error('invalid_id', 'Attachment ID is required', ['status' => 400]);
     7177        }
     7178
     7179        if (empty($params['new_filename'])) {
     7180            return new WP_Error('invalid_request', 'new_filename is required', ['status' => 400]);
     7181        }
     7182
     7183        $attachment = get_post($attachment_id);
     7184        if (!$attachment || $attachment->post_type !== 'attachment') {
     7185            return new WP_Error('not_found', 'Attachment not found', ['status' => 404]);
     7186        }
     7187
     7188        // Get the current file path
     7189        $old_file_path = get_attached_file($attachment_id);
     7190        if (!$old_file_path || !file_exists($old_file_path)) {
     7191            return new WP_Error('file_not_found', 'Attachment file not found on server', ['status' => 404]);
     7192        }
     7193
     7194        // Get file info
     7195        $path_info = pathinfo($old_file_path);
     7196        $extension = strtolower($path_info['extension']);
     7197        $upload_dir = wp_upload_dir();
     7198
     7199        // Sanitize the new filename
     7200        $new_basename = sanitize_file_name($params['new_filename']);
     7201        // Remove any extension that might have been included
     7202        $new_basename = preg_replace('/\.[^.]+$/', '', $new_basename);
     7203        // Ensure lowercase and no special chars
     7204        $new_basename = strtolower($new_basename);
     7205        $new_basename = preg_replace('/[^a-z0-9-]/', '-', $new_basename);
     7206        $new_basename = preg_replace('/-+/', '-', $new_basename);
     7207        $new_basename = trim($new_basename, '-');
     7208
     7209        if (empty($new_basename)) {
     7210            return new WP_Error('invalid_filename', 'Invalid filename after sanitization', ['status' => 400]);
     7211        }
     7212
     7213        // Initialize WP_Filesystem
     7214        global $wp_filesystem;
     7215        if (empty($wp_filesystem)) {
     7216            require_once ABSPATH . '/wp-admin/includes/file.php';
     7217            WP_Filesystem();
     7218        }
     7219
     7220        // Generate unique filename if it already exists
     7221        $new_filename = $new_basename . '.' . $extension;
     7222        $new_file_path = $path_info['dirname'] . '/' . wp_unique_filename($path_info['dirname'], $new_filename);
     7223        $actual_new_filename = basename($new_file_path);
     7224
     7225        // Rename the main file using WP_Filesystem
     7226        if (!$wp_filesystem->move($old_file_path, $new_file_path)) {
     7227            return new WP_Error('rename_failed', 'Failed to rename file', ['status' => 500]);
     7228        }
     7229
     7230        // Update attachment metadata
     7231        update_attached_file($attachment_id, $new_file_path);
     7232
     7233        // Get and update attachment metadata for image sizes
     7234        $metadata = wp_get_attachment_metadata($attachment_id);
     7235        $old_url = wp_get_attachment_url($attachment_id);
     7236
     7237        if ($metadata && isset($metadata['file'])) {
     7238            // Update main file in metadata
     7239            $metadata['file'] = str_replace(basename($old_file_path), $actual_new_filename, $metadata['file']);
     7240
     7241            // Rename and update all image sizes
     7242            if (isset($metadata['sizes']) && is_array($metadata['sizes'])) {
     7243                foreach ($metadata['sizes'] as $size_name => $size_data) {
     7244                    if (isset($size_data['file'])) {
     7245                        $old_size_file = $path_info['dirname'] . '/' . $size_data['file'];
     7246                        if (file_exists($old_size_file)) {
     7247                            // Generate new size filename
     7248                            $size_extension = pathinfo($size_data['file'], PATHINFO_EXTENSION);
     7249                            $size_suffix = preg_replace('/^' . preg_quote($path_info['filename'], '/') . '/', '', pathinfo($size_data['file'], PATHINFO_FILENAME));
     7250                            $new_size_filename = $new_basename . $size_suffix . '.' . $size_extension;
     7251                            $new_size_path = $path_info['dirname'] . '/' . $new_size_filename;
     7252
     7253                            if ($wp_filesystem->move($old_size_file, $new_size_path)) {
     7254                                $metadata['sizes'][$size_name]['file'] = $new_size_filename;
     7255                            }
     7256                        }
     7257                    }
     7258                }
     7259            }
     7260
     7261            wp_update_attachment_metadata($attachment_id, $metadata);
     7262        }
     7263
     7264        // Update post title and slug if not set
     7265        $update_data = [
     7266            'ID' => $attachment_id,
     7267            'post_name' => $new_basename,
     7268        ];
     7269        // Only update title if it matches old filename
     7270        if ($attachment->post_title === $path_info['filename']) {
     7271            $update_data['post_title'] = ucwords(str_replace('-', ' ', $new_basename));
     7272        }
     7273        wp_update_post($update_data);
     7274
     7275        // Update references in post content (optional - can be slow on large sites)
     7276        $new_url = wp_get_attachment_url($attachment_id);
     7277        $posts_updated = 0;
     7278
     7279        if (!empty($params['update_references']) && $old_url !== $new_url) {
     7280            global $wpdb;
     7281
     7282            // Find posts containing the old URL
     7283            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     7284            $posts_with_image = $wpdb->get_results($wpdb->prepare(
     7285                "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_status != 'trash'",
     7286                '%' . $wpdb->esc_like(basename($old_file_path)) . '%'
     7287            ));
     7288
     7289            foreach ($posts_with_image as $post) {
     7290                $new_content = str_replace(
     7291                    [basename($old_file_path), $old_url],
     7292                    [$actual_new_filename, $new_url],
     7293                    $post->post_content
     7294                );
     7295
     7296                if ($new_content !== $post->post_content) {
     7297                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     7298                    $wpdb->update(
     7299                        $wpdb->posts,
     7300                        ['post_content' => $new_content],
     7301                        ['ID' => $post->ID]
     7302                    );
     7303                    $posts_updated++;
     7304                }
     7305            }
     7306
     7307            // Clear caches
     7308            if ($posts_updated > 0) {
     7309                wp_cache_flush();
     7310            }
     7311        }
     7312
     7313        return rest_ensure_response([
     7314            'success' => true,
     7315            'attachment_id' => $attachment_id,
     7316            'old_filename' => basename($old_file_path),
     7317            'new_filename' => $actual_new_filename,
     7318            'old_url' => $old_url,
     7319            'new_url' => $new_url,
     7320            'posts_updated' => $posts_updated,
     7321            'message' => 'Attachment renamed successfully'
     7322        ]);
     7323    }
     7324
     7325    /**
     7326     * Bulk rename attachments
     7327     *
     7328     * POST /wp-json/instarank/v1/media/bulk-rename
     7329     * Body: { "attachments": [{ "id": 123, "new_filename": "seo-name" }, ...], "update_references": true }
     7330     */
     7331    public function bulk_rename_attachments($request) {
     7332        $params = $request->get_json_params();
     7333        $attachments = isset($params['attachments']) ? $params['attachments'] : [];
     7334        $update_references = !empty($params['update_references']);
     7335
     7336        if (empty($attachments) || !is_array($attachments)) {
     7337            return new WP_Error('invalid_request', 'attachments array is required', ['status' => 400]);
     7338        }
     7339
     7340        if (count($attachments) > 50) {
     7341            return new WP_Error('too_many', 'Maximum 50 attachments per request', ['status' => 400]);
     7342        }
     7343
     7344        $results = [];
     7345        $success_count = 0;
     7346        $failed_count = 0;
     7347
     7348        foreach ($attachments as $attachment_data) {
     7349            if (empty($attachment_data['id']) || empty($attachment_data['new_filename'])) {
     7350                $failed_count++;
     7351                $results[] = [
     7352                    'id' => $attachment_data['id'] ?? null,
     7353                    'success' => false,
     7354                    'error' => 'Missing id or new_filename'
     7355                ];
     7356                continue;
     7357            }
     7358
     7359            // Create a mock request for the single rename function
     7360            $mock_request = new WP_REST_Request('POST');
     7361            $mock_request->set_param('id', $attachment_data['id']);
     7362            $mock_request->set_body_params([
     7363                'new_filename' => $attachment_data['new_filename'],
     7364                'update_references' => $update_references
     7365            ]);
     7366
     7367            $result = $this->rename_attachment_file($mock_request);
     7368
     7369            if (is_wp_error($result)) {
     7370                $failed_count++;
     7371                $results[] = [
     7372                    'id' => $attachment_data['id'],
     7373                    'success' => false,
     7374                    'error' => $result->get_error_message()
     7375                ];
     7376            } else {
     7377                $response_data = $result->get_data();
     7378                if ($response_data['success']) {
     7379                    $success_count++;
     7380                    $results[] = [
     7381                        'id' => $attachment_data['id'],
     7382                        'success' => true,
     7383                        'old_filename' => $response_data['old_filename'],
     7384                        'new_filename' => $response_data['new_filename'],
     7385                        'new_url' => $response_data['new_url']
     7386                    ];
     7387                } else {
     7388                    $failed_count++;
     7389                    $results[] = [
     7390                        'id' => $attachment_data['id'],
     7391                        'success' => false,
     7392                        'error' => $response_data['error'] ?? 'Unknown error'
     7393                    ];
     7394                }
     7395            }
     7396        }
     7397
     7398        return rest_ensure_response([
     7399            'success' => $failed_count === 0,
     7400            'renamed' => $success_count,
     7401            'failed' => $failed_count,
     7402            'results' => $results
     7403        ]);
     7404    }
     7405
     7406    /**
     7407     * Update image attributes in post content (lazy loading, dimensions)
     7408     *
     7409     * POST /wp-json/instarank/v1/content/update-image-attributes
     7410     * Body: {
     7411     *   "post_id": 123,
     7412     *   "image_url": "https://example.com/image.jpg",
     7413     *   "add_lazy_loading": true,
     7414     *   "add_dimensions": true,
     7415     *   "width": 800,
     7416     *   "height": 600
     7417     * }
     7418     */
     7419    public function update_content_image_attributes($request) {
     7420        $params = $request->get_json_params();
     7421        $post_id = isset($params['post_id']) ? intval($params['post_id']) : 0;
     7422        $image_url = isset($params['image_url']) ? esc_url($params['image_url']) : '';
     7423        $add_lazy_loading = !empty($params['add_lazy_loading']);
     7424        $add_dimensions = !empty($params['add_dimensions']);
     7425        $width = isset($params['width']) ? intval($params['width']) : 0;
     7426        $height = isset($params['height']) ? intval($params['height']) : 0;
     7427
     7428        if (!$post_id) {
     7429            return new WP_Error('invalid_request', 'post_id is required', ['status' => 400]);
     7430        }
     7431
     7432        if (!$image_url) {
     7433            return new WP_Error('invalid_request', 'image_url is required', ['status' => 400]);
     7434        }
     7435
     7436        // Get the post
     7437        $post = get_post($post_id);
     7438        if (!$post) {
     7439            return new WP_Error('not_found', 'Post not found', ['status' => 404]);
     7440        }
     7441
     7442        $content = $post->post_content;
     7443        $original_content = $content;
     7444        $changes_made = [];
     7445
     7446        // Find and update img tags with this URL
     7447        // Escape the URL for use in regex
     7448        $escaped_url = preg_quote($image_url, '/');
     7449        // Also match URL variants (with/without domain, scaled versions)
     7450        $url_without_domain = preg_replace('/^https?:\/\/[^\/]+/', '', $image_url);
     7451        $escaped_url_partial = preg_quote($url_without_domain, '/');
     7452
     7453        // Pattern to match img tags containing this URL
     7454        $pattern = '/<img[^>]*src=["\']([^"\']*' . $escaped_url_partial . '[^"\']*)["\'][^>]*>/i';
     7455
     7456        $content = preg_replace_callback($pattern, function($matches) use ($add_lazy_loading, $add_dimensions, $width, $height, &$changes_made) {
     7457            $img_tag = $matches[0];
     7458            $original_tag = $img_tag;
     7459
     7460            // Add lazy loading if needed and not present
     7461            if ($add_lazy_loading) {
     7462                if (stripos($img_tag, 'loading=') === false) {
     7463                    $img_tag = preg_replace('/<img\s/i', '<img loading="lazy" ', $img_tag);
     7464                    $changes_made[] = 'added_lazy_loading';
     7465                }
     7466            }
     7467
     7468            // Add dimensions if needed
     7469            if ($add_dimensions && $width > 0 && $height > 0) {
     7470                // Check if width is missing
     7471                if (stripos($img_tag, 'width=') === false) {
     7472                    $img_tag = preg_replace('/<img\s/i', '<img width="' . $width . '" ', $img_tag);
     7473                    $changes_made[] = 'added_width';
     7474                }
     7475                // Check if height is missing
     7476                if (stripos($img_tag, 'height=') === false) {
     7477                    $img_tag = preg_replace('/<img\s/i', '<img height="' . $height . '" ', $img_tag);
     7478                    $changes_made[] = 'added_height';
     7479                }
     7480            }
     7481
     7482            return $img_tag;
     7483        }, $content);
     7484
     7485        // Also check page builder meta for this image
     7486        $page_builder_updated = false;
     7487        $page_builder_meta_keys = [
     7488            '_elementor_data',
     7489            '_kadence_blocks_meta',
     7490            '_et_builder_data',
     7491            '_fl_builder_data',
     7492            '_bricks_page_content_2'
     7493        ];
     7494
     7495        foreach ($page_builder_meta_keys as $meta_key) {
     7496            $meta_value = get_post_meta($post_id, $meta_key, true);
     7497            if (!empty($meta_value)) {
     7498                $original_meta = $meta_value;
     7499                $is_json = is_string($meta_value) && (substr($meta_value, 0, 1) === '[' || substr($meta_value, 0, 1) === '{');
     7500
     7501                if ($is_json) {
     7502                    $decoded = json_decode($meta_value, true);
     7503                    if ($decoded) {
     7504                        $decoded = $this->update_image_attributes_in_array($decoded, $image_url, $add_lazy_loading, $add_dimensions, $width, $height);
     7505                        $new_meta = wp_json_encode($decoded);
     7506                        if ($new_meta !== $meta_value) {
     7507                            update_post_meta($post_id, $meta_key, wp_slash($new_meta));
     7508                            $page_builder_updated = true;
     7509                        }
     7510                    }
     7511                }
     7512            }
     7513        }
     7514
     7515        // Update the post content if changed
     7516        $content_updated = false;
     7517        if ($content !== $original_content) {
     7518            $result = wp_update_post([
     7519                'ID' => $post_id,
     7520                'post_content' => $content
     7521            ], true);
     7522
     7523            if (is_wp_error($result)) {
     7524                return $result;
     7525            }
     7526            $content_updated = true;
     7527        }
     7528
     7529        if (!$content_updated && !$page_builder_updated && empty($changes_made)) {
     7530            return rest_ensure_response([
     7531                'success' => true,
     7532                'message' => 'No changes needed - attributes already present or image not found in content',
     7533                'post_id' => $post_id,
     7534                'changes_made' => []
     7535            ]);
     7536        }
     7537
     7538        // Clear caches
     7539        clean_post_cache($post_id);
     7540
     7541        return rest_ensure_response([
     7542            'success' => true,
     7543            'post_id' => $post_id,
     7544            'image_url' => $image_url,
     7545            'content_updated' => $content_updated,
     7546            'page_builder_updated' => $page_builder_updated,
     7547            'changes_made' => array_unique($changes_made),
     7548            'message' => 'Image attributes updated successfully'
     7549        ]);
     7550    }
     7551
     7552    /**
     7553     * Recursively update image attributes in array structures (for page builder data)
     7554     */
     7555    private function update_image_attributes_in_array($data, $image_url, $add_lazy_loading, $add_dimensions, $width, $height) {
     7556        if (!is_array($data)) {
     7557            if (is_string($data) && strpos($data, '<img') !== false && strpos($data, $image_url) !== false) {
     7558                // This is HTML content containing our image
     7559                $pattern = '/<img[^>]*src=["\'][^"\']*' . preg_quote($image_url, '/') . '[^"\']*["\'][^>]*>/i';
     7560                $data = preg_replace_callback($pattern, function($matches) use ($add_lazy_loading, $add_dimensions, $width, $height) {
     7561                    $img_tag = $matches[0];
     7562
     7563                    if ($add_lazy_loading && stripos($img_tag, 'loading=') === false) {
     7564                        $img_tag = preg_replace('/<img\s/i', '<img loading="lazy" ', $img_tag);
     7565                    }
     7566
     7567                    if ($add_dimensions && $width > 0 && $height > 0) {
     7568                        if (stripos($img_tag, 'width=') === false) {
     7569                            $img_tag = preg_replace('/<img\s/i', '<img width="' . $width . '" ', $img_tag);
     7570                        }
     7571                        if (stripos($img_tag, 'height=') === false) {
     7572                            $img_tag = preg_replace('/<img\s/i', '<img height="' . $height . '" ', $img_tag);
     7573                        }
     7574                    }
     7575
     7576                    return $img_tag;
     7577                }, $data);
     7578            }
     7579            return $data;
     7580        }
     7581
     7582        foreach ($data as $key => $value) {
     7583            $data[$key] = $this->update_image_attributes_in_array($value, $image_url, $add_lazy_loading, $add_dimensions, $width, $height);
     7584        }
     7585
     7586        return $data;
     7587    }
     7588
     7589    /**
     7590     * Bulk update image attributes in multiple posts
     7591     *
     7592     * POST /wp-json/instarank/v1/content/bulk-update-image-attributes
     7593     * Body: {
     7594     *   "images": [
     7595     *     { "post_id": 123, "image_url": "...", "add_lazy_loading": true, "add_dimensions": true, "width": 800, "height": 600 },
     7596     *     ...
     7597     *   ]
     7598     * }
     7599     */
     7600    public function bulk_update_content_image_attributes($request) {
     7601        $params = $request->get_json_params();
     7602        $images = isset($params['images']) ? $params['images'] : [];
     7603
     7604        if (empty($images) || !is_array($images)) {
     7605            return new WP_Error('invalid_request', 'images array is required', ['status' => 400]);
     7606        }
     7607
     7608        if (count($images) > 100) {
     7609            return new WP_Error('too_many', 'Maximum 100 images per request', ['status' => 400]);
     7610        }
     7611
     7612        $results = [];
     7613        $success_count = 0;
     7614        $failed_count = 0;
     7615
     7616        foreach ($images as $image_data) {
     7617            if (empty($image_data['post_id']) || empty($image_data['image_url'])) {
     7618                $failed_count++;
     7619                $results[] = [
     7620                    'post_id' => $image_data['post_id'] ?? null,
     7621                    'image_url' => $image_data['image_url'] ?? null,
     7622                    'success' => false,
     7623                    'error' => 'Missing post_id or image_url'
     7624                ];
     7625                continue;
     7626            }
     7627
     7628            // Create a mock request for the single update function
     7629            $mock_request = new WP_REST_Request('POST');
     7630            $mock_request->set_body_params([
     7631                'post_id' => $image_data['post_id'],
     7632                'image_url' => $image_data['image_url'],
     7633                'add_lazy_loading' => !empty($image_data['add_lazy_loading']),
     7634                'add_dimensions' => !empty($image_data['add_dimensions']),
     7635                'width' => isset($image_data['width']) ? intval($image_data['width']) : 0,
     7636                'height' => isset($image_data['height']) ? intval($image_data['height']) : 0
     7637            ]);
     7638
     7639            $result = $this->update_content_image_attributes($mock_request);
     7640
     7641            if (is_wp_error($result)) {
     7642                $failed_count++;
     7643                $results[] = [
     7644                    'post_id' => $image_data['post_id'],
     7645                    'image_url' => $image_data['image_url'],
     7646                    'success' => false,
     7647                    'error' => $result->get_error_message()
     7648                ];
     7649            } else {
     7650                $response_data = $result->get_data();
     7651                if ($response_data['success']) {
     7652                    $success_count++;
     7653                    $results[] = [
     7654                        'post_id' => $image_data['post_id'],
     7655                        'image_url' => $image_data['image_url'],
     7656                        'success' => true,
     7657                        'changes_made' => $response_data['changes_made'] ?? []
     7658                    ];
     7659                } else {
     7660                    $failed_count++;
     7661                    $results[] = [
     7662                        'post_id' => $image_data['post_id'],
     7663                        'image_url' => $image_data['image_url'],
     7664                        'success' => false,
     7665                        'error' => $response_data['error'] ?? 'Unknown error'
     7666                    ];
     7667                }
     7668            }
     7669        }
     7670
     7671        return rest_ensure_response([
     7672            'success' => $failed_count === 0,
     7673            'updated' => $success_count,
     7674            'failed' => $failed_count,
     7675            'results' => $results
     7676        ]);
     7677    }
    56997678}
    57007679
  • instarank/trunk/instarank.php

    r3418364 r3419086  
    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: 2.0.2
     6 * Version: 2.0.3
    77 * Author: InstaRank
    88 * Author URI: https://instarank.com
     
    1818
    1919// Define plugin constants
    20 define('INSTARANK_VERSION', '2.0.2');
     20define('INSTARANK_VERSION', '2.0.3');
    2121define('INSTARANK_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2222define('INSTARANK_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    4343require_once INSTARANK_PLUGIN_DIR . 'includes/class-indexnow.php';
    4444require_once INSTARANK_PLUGIN_DIR . 'includes/class-related-links.php';
     45require_once INSTARANK_PLUGIN_DIR . 'includes/class-virtual-pages.php';
     46require_once INSTARANK_PLUGIN_DIR . 'includes/class-multilang.php';
    4547require_once INSTARANK_PLUGIN_DIR . 'api/endpoints.php';
    4648
  • instarank/trunk/readme.txt

    r3418364 r3419086  
    44Requires at least: 5.6
    55Tested up to: 6.9
    6 Stable tag: 2.0.2
     6Stable tag: 2.0.3
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    169169
    170170== Changelog ==
     171
     172= 2.0.3 =
     173* Feature: AI-powered image metadata generation (alt text, title, caption, description)
     174* Feature: Push image metadata to WordPress Media Library
     175* Feature: Image issue fixing with batch processing support
     176* Fix: Replace PHP rename() with WP_Filesystem::move() for WordPress coding standards compliance
     177* Fix: Improved file operations security using WordPress filesystem API
     178* Enhancement: Enhanced image extractor with better metadata detection
     179* Enhancement: AI content history tracking for all generated metadata
    171180
    172181= 2.0.2 =
Note: See TracChangeset for help on using the changeset viewer.