Changeset 3419086
- Timestamp:
- 12/13/2025 09:35:13 PM (3 months ago)
- Location:
- instarank/trunk
- Files:
-
- 2 added
- 3 edited
-
api/endpoints.php (modified) (5 diffs)
-
includes/class-multilang.php (added)
-
includes/class-virtual-pages.php (added)
-
instarank.php (modified) (3 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
instarank/trunk/api/endpoints.php
r3418364 r3419086 96 96 ]); 97 97 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 98 105 // NEW: Page Types Support Endpoints 99 106 … … 291 298 ]); 292 299 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 293 330 // SPINTAX ENDPOINTS 294 331 … … 448 485 'methods' => 'GET', 449 486 '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'], 450 589 'permission_callback' => [$this, 'verify_api_key'] 451 590 ]); … … 4206 4345 4207 4346 /** 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 /** 4208 4528 * Find attachment by URL 4209 4529 * … … 5697 6017 return rest_ensure_response($response); 5698 6018 } 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 } 5699 7678 } 5700 7679 -
instarank/trunk/instarank.php
r3418364 r3419086 4 4 * Plugin URI: https://instarank.com/wordpress-plugin 5 5 * Description: Connect your WordPress site to InstaRank for AI-powered SEO optimization, schema markup generation, and programmatic SEO. Create and sync custom post types, automatically apply SEO improvements, and generate structured data with InstaRank's AI engine. 6 * Version: 2.0. 26 * Version: 2.0.3 7 7 * Author: InstaRank 8 8 * Author URI: https://instarank.com … … 18 18 19 19 // Define plugin constants 20 define('INSTARANK_VERSION', '2.0. 2');20 define('INSTARANK_VERSION', '2.0.3'); 21 21 define('INSTARANK_PLUGIN_DIR', plugin_dir_path(__FILE__)); 22 22 define('INSTARANK_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 43 43 require_once INSTARANK_PLUGIN_DIR . 'includes/class-indexnow.php'; 44 44 require_once INSTARANK_PLUGIN_DIR . 'includes/class-related-links.php'; 45 require_once INSTARANK_PLUGIN_DIR . 'includes/class-virtual-pages.php'; 46 require_once INSTARANK_PLUGIN_DIR . 'includes/class-multilang.php'; 45 47 require_once INSTARANK_PLUGIN_DIR . 'api/endpoints.php'; 46 48 -
instarank/trunk/readme.txt
r3418364 r3419086 4 4 Requires at least: 5.6 5 5 Tested up to: 6.9 6 Stable tag: 2.0. 26 Stable tag: 2.0.3 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 169 169 170 170 == 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 171 180 172 181 = 2.0.2 =
Note: See TracChangeset
for help on using the changeset viewer.