| 1 |
<?php |
| 2 |
|
| 3 |
namespace ISC; |
| 4 |
|
| 5 |
use ISC\Image_Sources\Post_Meta\Image_Posts_Meta; |
| 6 |
use ISC\Image_Sources\Post_Meta\Post_Images_Meta; |
| 7 |
use ISC_Log, ISC_Model; |
| 8 |
|
| 9 |
/** |
| 10 |
* Index content for images and write isc_post_images and isc_image_posts post meta |
| 11 |
*/ |
| 12 |
class Indexer { |
| 13 |
|
| 14 |
const BEFORE_UPDATE_META_KEY = 'isc_post_images_before_update'; |
| 15 |
|
| 16 |
/** |
| 17 |
* Handle index updates on frontend visit after a post save. |
| 18 |
* Compares pre-save state with current render and updates indexes. |
| 19 |
* |
| 20 |
* @param string $content Rendered content of the target post. |
| 21 |
* |
| 22 |
* @return void |
| 23 |
*/ |
| 24 |
public static function update_indexes( string $content ) { |
| 25 |
|
| 26 |
if ( ! self::can_index_the_page() ) { |
| 27 |
return; |
| 28 |
} |
| 29 |
|
| 30 |
global $post; |
| 31 |
|
| 32 |
// Skip indexing if this is a page with a Global list. |
| 33 |
if ( self::is_global_list_page( $content ) ) { |
| 34 |
// Ensure no temporary meta is left behind if user adds shortcode later. |
| 35 |
self::cleanup_after_reindex( $post->ID ); // Use the cleanup method |
| 36 |
// An empty isc_post_images meta value indicates the post was indexed (or intentionally skipped). |
| 37 |
if ( Post_Images_Meta::get( $post->ID ) === '' ) { |
| 38 |
Post_Images_Meta::update_images_in_posts( $post->ID, [] ); |
| 39 |
} |
| 40 |
ISC_Log::log( sprintf( 'Exiting update_indexes for post %d: Global list page.', $post->ID ) ); |
| 41 |
return; |
| 42 |
} |
| 43 |
|
| 44 |
/** |
| 45 |
* Triggered before updating the indexes. |
| 46 |
* Useful to run code with the index even though the Image Source already have an index |
| 47 |
* |
| 48 |
* @param int $post->ID Post ID. |
| 49 |
* @param string $content Post content. |
| 50 |
*/ |
| 51 |
do_action( 'isc_before_update_indexes', $post->ID, $content ); |
| 52 |
|
| 53 |
// ignore existing indexes if the bot is running |
| 54 |
if ( ! self::is_index_bot() ) { |
| 55 |
$attachments = self::get_attachments_for_index( $post->ID ); |
| 56 |
|
| 57 |
/** |
| 58 |
* $attachments is an empty string if it was never set and an array if it was set |
| 59 |
* the array is empty if no images were found in the past. This prevents re-indexing as well |
| 60 |
*/ |
| 61 |
if ( $attachments !== '' ) { |
| 62 |
// Remove the temporary data since we are not updating the index |
| 63 |
self::cleanup_after_reindex( $post->ID ); |
| 64 |
return; |
| 65 |
} |
| 66 |
} |
| 67 |
|
| 68 |
ISC_Log::log( 'Start updating index for post ID ' . $post->ID ); |
| 69 |
|
| 70 |
// Check if we can even save the image information |
| 71 |
// Abort on archive pages, home, or unsupported post types |
| 72 |
if ( is_archive() || is_home() || ! self::can_save_image_information( $post->ID ) ) { |
| 73 |
ISC_Log::log( sprintf( 'Exiting update_indexes for post %d: Cannot save image information (archive/home/post type).', $post->ID ) ); |
| 74 |
// Clean up temporary meta if we abort here |
| 75 |
self::cleanup_after_reindex( $post->ID ); |
| 76 |
return; |
| 77 |
} |
| 78 |
|
| 79 |
// 1. Get the state before the last update(s). |
| 80 |
$old_indexed_data = self::get_pre_update_state( $post->ID ); |
| 81 |
|
| 82 |
// 2. Get the image IDs from the currently rendered content. |
| 83 |
// Call filter_image_ids only ONCE here. |
| 84 |
$new_rendered_ids = ISC_Model::filter_image_ids( $content ); |
| 85 |
|
| 86 |
$thumb_id = get_post_thumbnail_id( $post->ID ); |
| 87 |
if ( ! empty( $thumb_id ) && ! isset( $new_rendered_ids[ $thumb_id ] ) ) { |
| 88 |
// Add thumbnail to the list if it's not already there from content parsing. |
| 89 |
// The value structure should match what filter_image_ids returns, |
| 90 |
// though sync_image_post_associations only cares about the keys. |
| 91 |
// The 'thumbnail' flag itself is added later in Post_Meta\Post_Images_Meta::update_images_in_posts. |
| 92 |
$thumb_url = wp_get_attachment_url( $thumb_id ); |
| 93 |
if ( $thumb_url ) { |
| 94 |
$new_rendered_ids[ $thumb_id ] = [ $thumb_url ]; |
| 95 |
ISC_Log::log( sprintf( 'Added thumbnail ID %d to new_rendered_ids for post %d.', $thumb_id, $post->ID ) ); |
| 96 |
} |
| 97 |
} |
| 98 |
|
| 99 |
/** |
| 100 |
* Allows developers to modify the list before synchronization. |
| 101 |
* |
| 102 |
* @return array $new_rendered_ids Image IDs found in the content ([id => data]). |
| 103 |
* id is expected to be numeric and the attachment post type |
| 104 |
*/ |
| 105 |
$new_rendered_ids = apply_filters( 'isc_images_in_posts_simple', $new_rendered_ids, $post->ID ); |
| 106 |
if ( has_filter( 'isc_images_in_posts_simple' ) ) { |
| 107 |
ISC_Log::log( sprintf( 'Post %d - new_rendered_ids after isc_images_in_posts_simple filter ran: %s', $post->ID, ! empty( $new_rendered_ids ) ? implode( ', ', array_keys( $new_rendered_ids ) ) : 'Empty' ) ); |
| 108 |
} |
| 109 |
|
| 110 |
// Check if image IDs refer to a valid post type (default: 'attachment'). |
| 111 |
$valid_image_post_types = apply_filters( 'isc_valid_post_types', [ 'attachment' ] ); |
| 112 |
if ( ! empty( $new_rendered_ids ) ) { // Avoid errors if array is empty |
| 113 |
foreach ( $new_rendered_ids as $_id => $_data ) { |
| 114 |
// Ensure ID is numeric before checking post type |
| 115 |
if ( ! is_numeric( $_id ) || $_id <= 0 ) { |
| 116 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r |
| 117 |
ISC_Log::log( sprintf( 'Removing invalid image ID %s from index for post %d.', print_r( $_id, true ), $post->ID ) ); |
| 118 |
unset( $new_rendered_ids[ $_id ] ); |
| 119 |
continue; |
| 120 |
} |
| 121 |
$post_type = get_post_type( (int) $_id ); |
| 122 |
if ( ! $post_type || ! in_array( $post_type, $valid_image_post_types, true ) ) { |
| 123 |
ISC_Log::log( sprintf( 'Removing image ID %d (type: %s) due to invalid post type for post %d.', $_id, $post_type ? $post_type : 'unknown', $post->ID ) ); |
| 124 |
unset( $new_rendered_ids[ $_id ] ); |
| 125 |
} |
| 126 |
} |
| 127 |
} |
| 128 |
|
| 129 |
// 3. Sync the image->post associations based on comparison. |
| 130 |
// This handles adding/removing $post->ID from individual image's isc_image_posts meta. |
| 131 |
self::sync_image_post_associations( $post->ID, $old_indexed_data, $new_rendered_ids ); |
| 132 |
|
| 133 |
// 4. Update the main post->image index ('isc_post_images') with the current state. |
| 134 |
// This saves the $new_rendered_ids to the post's meta. |
| 135 |
Post_Images_Meta::update_images_in_posts( $post->ID, $new_rendered_ids ); |
| 136 |
|
| 137 |
// 5. Clean up the temporary meta key. |
| 138 |
self::cleanup_after_reindex( $post->ID ); |
| 139 |
|
| 140 |
/** |
| 141 |
* Triggered after updating the indexes. |
| 142 |
* |
| 143 |
* @param int $post_id Post ID. |
| 144 |
* @param string $content Post content. |
| 145 |
* @param array $new_rendered_ids Image IDs found in the content ([id => data]). |
| 146 |
*/ |
| 147 |
do_action( 'isc_after_update_indexes', $post->ID, $content, $new_rendered_ids ); |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* Return the attachments array used for indexing image-post relations |
| 152 |
* |
| 153 |
* @param int $post_id Post ID. |
| 154 |
* |
| 155 |
* @return string|array |
| 156 |
*/ |
| 157 |
public static function get_attachments_for_index( int $post_id ) { |
| 158 |
$attachments = ''; |
| 159 |
$ignore_caches = apply_filters( 'isc_add_sources_to_content_ignore_post_images_index', ISC_Log::ignore_caches() ); |
| 160 |
|
| 161 |
if ( $ignore_caches ) { |
| 162 |
ISC_Log::log( 'ignoring post-image index' ); |
| 163 |
} else { |
| 164 |
// check if a post-images index exists |
| 165 |
$attachments = Post_Images_Meta::get( $post_id ); |
| 166 |
if ( $attachments === '' ) { |
| 167 |
ISC_Log::log( 'no post-images index found' ); |
| 168 |
} elseif ( is_array( $attachments ) ) { |
| 169 |
ISC_Log::log( sprintf( 'found existing list of %d images for post ID %d', count( $attachments ), $post_id ) ); |
| 170 |
} |
| 171 |
} |
| 172 |
|
| 173 |
return $attachments; |
| 174 |
} |
| 175 |
|
| 176 |
|
| 177 |
/** |
| 178 |
* Return true if the current page can be indexed and sources should be added |
| 179 |
* |
| 180 |
* @return bool |
| 181 |
*/ |
| 182 |
public static function can_index_the_page(): bool { |
| 183 |
// bail early if the content is used to create the excerpt |
| 184 |
if ( doing_filter( 'get_the_excerpt' ) ) { |
| 185 |
ISC_Log::log( 'skipped adding sources to the excerpt' ); |
| 186 |
return false; |
| 187 |
} |
| 188 |
|
| 189 |
// disabling the content filters while working in page builders or block editor |
| 190 |
if ( wp_is_json_request() || defined( 'REST_REQUEST' ) ) { |
| 191 |
ISC_Log::log( 'skipped adding sources while working in page builders' ); |
| 192 |
return false; |
| 193 |
} |
| 194 |
|
| 195 |
global $post; |
| 196 |
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; |
| 197 |
|
| 198 |
if ( empty( $post->ID ) ) { |
| 199 |
if ( $request_uri ) { |
| 200 |
ISC_Log::log( 'exit content for ' . $request_uri . ' due to missing post_id' ); |
| 201 |
} |
| 202 |
return false; |
| 203 |
} |
| 204 |
|
| 205 |
if ( $request_uri ) { |
| 206 |
ISC_Log::log( 'can index content for ' . $request_uri . ' and post ID ' . $post->ID ); |
| 207 |
} |
| 208 |
|
| 209 |
return true; |
| 210 |
} |
| 211 |
|
| 212 |
/** |
| 213 |
* Don’t save meta data for non-public post types, since those shouldn’t be visible in the frontend |
| 214 |
* ignore also attachment posts |
| 215 |
* ignore revisions |
| 216 |
* |
| 217 |
* @param integer $post_id WP_Post ID. Useful if post object is not given. |
| 218 |
*/ |
| 219 |
public static function can_save_image_information( int $post_id = 0 ): bool { |
| 220 |
$post = get_post( $post_id ); |
| 221 |
|
| 222 |
if ( ! isset( $post->post_type ) |
| 223 |
|| ! in_array( $post->post_type, get_post_types( [ 'public' => true ] ), true ) // is the post type public |
| 224 |
|| $post->post_type === 'attachment' |
| 225 |
|| $post->post_type === 'revision' ) { |
| 226 |
return false; |
| 227 |
} |
| 228 |
|
| 229 |
return true; |
| 230 |
} |
| 231 |
|
| 232 |
/** |
| 233 |
* Return true if the current user agent is the index bot |
| 234 |
*/ |
| 235 |
public static function is_index_bot(): bool { |
| 236 |
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; |
| 237 |
// Check the user agent first |
| 238 |
$is_bot = strpos( $user_agent, 'ISC Index Bot' ) !== false; |
| 239 |
|
| 240 |
/** |
| 241 |
* Apply a filter to allow overriding the result, passing the original check result |
| 242 |
* |
| 243 |
* @param bool $is_bot The result of the initial check. |
| 244 |
* @return bool The final result after applying the filter. |
| 245 |
*/ |
| 246 |
return apply_filters( 'isc_is_index_bot', $is_bot ); |
| 247 |
} |
| 248 |
|
| 249 |
/** |
| 250 |
* Prepares a post for re-indexing on the next frontend visit. |
| 251 |
* Moves the current post-image index to a temporary key if it exists. |
| 252 |
* |
| 253 |
* Hooked to 'wp_insert_post'. |
| 254 |
* |
| 255 |
* @param int $post_id Post ID. |
| 256 |
* |
| 257 |
* @return bool True if preparation was done (index moved), false otherwise. |
| 258 |
*/ |
| 259 |
public static function prepare_for_reindex( int $post_id ): bool { |
| 260 |
if ( ! self::can_save_image_information( $post_id ) ) { |
| 261 |
ISC_Log::log( sprintf( 'Skipping prepare_for_reindex for post %d: Cannot save image information.', $post_id ) ); |
| 262 |
return false; |
| 263 |
} |
| 264 |
|
| 265 |
$old_value = Post_Images_Meta::get( $post_id ); |
| 266 |
|
| 267 |
if ( is_array( $old_value ) ) { |
| 268 |
ISC_Log::log( sprintf( 'Preparing post %d for frontend re-index. Moving existing index.', $post_id ) ); |
| 269 |
update_post_meta( $post_id, self::BEFORE_UPDATE_META_KEY, $old_value ); |
| 270 |
Post_Images_Meta::delete( $post_id ); |
| 271 |
return true; |
| 272 |
} else { |
| 273 |
ISC_Log::log( sprintf( 'Skipping prepare_for_reindex for post %d: No existing index found or already prepared.', $post_id ) ); |
| 274 |
return false; |
| 275 |
} |
| 276 |
} |
| 277 |
|
| 278 |
/** |
| 279 |
* Retrieves the pre-update state of the post-image index. |
| 280 |
* |
| 281 |
* @param int $post_id Post ID. |
| 282 |
* |
| 283 |
* @return array The old index map, or an empty array if none found. |
| 284 |
*/ |
| 285 |
public static function get_pre_update_state( int $post_id ): array { |
| 286 |
$state = get_post_meta( $post_id, self::BEFORE_UPDATE_META_KEY, true ); |
| 287 |
return is_array( $state ) ? $state : []; |
| 288 |
} |
| 289 |
|
| 290 |
/** |
| 291 |
* Compares old and new image associations for a post and updates |
| 292 |
* the 'isc_image_posts' meta field on individual images accordingly. |
| 293 |
* |
| 294 |
* @param int $post_id The ID of the post being updated. |
| 295 |
* @param array $old_image_map Map of images previously associated [id => data]. |
| 296 |
* @param array $new_image_map Map of images currently associated [id => data]. |
| 297 |
*/ |
| 298 |
public static function sync_image_post_associations( int $post_id, array $old_image_map, array $new_image_map ) { |
| 299 |
ISC_Log::log( sprintf( 'Entering for post %d.', $post_id ) ); |
| 300 |
|
| 301 |
// 1. Calculate differences based on image IDs (keys). |
| 302 |
$old_ids = array_keys( $old_image_map ); |
| 303 |
$new_ids = array_keys( $new_image_map ); |
| 304 |
|
| 305 |
// Find IDs present in the new map but not in the old map. |
| 306 |
$added_ids = array_diff( $new_ids, $old_ids ); |
| 307 |
// Find IDs present in the old map but not in the new map. |
| 308 |
$removed_ids = array_diff( $old_ids, $new_ids ); |
| 309 |
|
| 310 |
ISC_Log::log( sprintf( 'Post %d - Sync Calculated Added IDs: %s', $post_id, ! empty( $added_ids ) ? implode( ', ', $added_ids ) : 'None' ) ); |
| 311 |
ISC_Log::log( sprintf( 'Post %d - Sync Calculated Removed IDs: %s', $post_id, ! empty( $removed_ids ) ? implode( ', ', $removed_ids ) : 'None' ) ); |
| 312 |
|
| 313 |
// 2. Update added associations |
| 314 |
foreach ( $added_ids as $id ) { |
| 315 |
// Basic validation for ID |
| 316 |
if ( ! empty( $id ) && is_numeric( $id ) ) { |
| 317 |
Image_Posts_Meta::add_image_post_association( (int) $id, $post_id ); |
| 318 |
} else { |
| 319 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r |
| 320 |
ISC_Log::log( sprintf( 'Skipping add association for invalid ID: %s', print_r( $id, true ) ) ); |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
// 3. Update removed associations |
| 325 |
foreach ( $removed_ids as $id ) { |
| 326 |
// Basic validation for ID |
| 327 |
if ( ! empty( $id ) && is_numeric( $id ) ) { |
| 328 |
Image_Posts_Meta::remove_image_post_association( (int) $id, $post_id ); |
| 329 |
} else { |
| 330 |
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r |
| 331 |
ISC_Log::log( sprintf( 'Skipping remove association for invalid ID: %s', print_r( $id, true ) ) ); |
| 332 |
} |
| 333 |
} |
| 334 |
ISC_Log::log( sprintf( 'Exiting for post %d.', $post_id ) ); |
| 335 |
} |
| 336 |
|
| 337 |
/** |
| 338 |
* Cleans up the temporary meta key after re-indexing. |
| 339 |
* |
| 340 |
* @param int $post_id Post ID. |
| 341 |
*/ |
| 342 |
public static function cleanup_after_reindex( int $post_id ) { |
| 343 |
ISC_Log::log( sprintf( 'Deleting temporary index key %s for post %d.', self::BEFORE_UPDATE_META_KEY, $post_id ) ); |
| 344 |
delete_post_meta( $post_id, self::BEFORE_UPDATE_META_KEY ); |
| 345 |
} |
| 346 |
|
| 347 |
/** |
| 348 |
* Remove all image-post relations |
| 349 |
* this concerns the post meta fields `isc_image_posts` and `isc_post_images` |
| 350 |
* |
| 351 |
* @return bool True on success, false on failure. |
| 352 |
*/ |
| 353 |
public static function clear_index(): bool { |
| 354 |
return Post_Images_Meta::delete_all() && Image_Posts_Meta::delete_all(); |
| 355 |
} |
| 356 |
|
| 357 |
/** |
| 358 |
* Handle post deletion and clean up image-post associations. |
| 359 |
* |
| 360 |
* @param int $post_id Post ID. |
| 361 |
*/ |
| 362 |
public static function handle_post_deletion( int $post_id ): void { |
| 363 |
$images = Post_Images_Meta::get( $post_id ); |
| 364 |
if ( is_array( $images ) ) { |
| 365 |
foreach ( array_keys( $images ) as $image_id ) { |
| 366 |
Image_Posts_Meta::remove_image_post_association( (int) $image_id, $post_id ); |
| 367 |
} |
| 368 |
} |
| 369 |
|
| 370 |
// Clean up the isc_post_images meta too |
| 371 |
Post_Images_Meta::delete( $post_id ); |
| 372 |
} |
| 373 |
|
| 374 |
/** |
| 375 |
* Return true if the content indicates that this is a page with the Global List on it |
| 376 |
* |
| 377 |
* @param string $content The content to check. |
| 378 |
* @return bool True if the content contains the Global List shortcode or class, false otherwise. |
| 379 |
*/ |
| 380 |
public static function is_global_list_page( $content ): bool { |
| 381 |
return has_shortcode( $content, '[isc_list_all]' ) || false !== strpos( $content, 'isc_all_image_list_box' ); |
| 382 |
} |
| 383 |
} |
| 384 |
|