PluginProbe
Image Source Control Lite – Show Image Credits and Captions / trunk
Image Source Control Lite – Show Image Credits and Captions vtrunk
3.12.0 3.11.0 trunk 1.1 1.1.1 1.1.2 1.1.2.1 1.1.3 1.10 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.2 1.2.0.1 1.2.0.2 1.2.0.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.4.1 1.3.5 All 110 releases
image-source-control-isc / includes / indexer.php

indexer.php in Image Source Control Lite – Show Image Credits and Captions trunk, at includes/indexer.php

384 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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