PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / inc / classes / class-imagify-custom-folders.php

class-imagify-custom-folders.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at inc/classes/class-imagify-custom-folders.php

1,388 lines 44.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class that regroups things about "custom folders".
5 *
6 * @since 1.7
7 * @author Grégory Viguier
8 */
9 class Imagify_Custom_Folders {
10
11 /**
12 * Class version.
13 *
14 * @var string
15 */
16 const VERSION = '1.1';
17
18
19 /** ----------------------------------------------------------------------------------------- */
20 /** BACKUP FOLDER =========================================================================== */
21 /** ----------------------------------------------------------------------------------------- */
22
23 /**
24 * Get the path to the backups directory (custom folders).
25 *
26 * @since 1.7
27 * @access public
28 * @author Grégory Viguier
29 *
30 * @return string Path to the backups directory.
31 */
32 public static function get_backup_dir_path() {
33 static $backup_dir;
34
35 if ( isset( $backup_dir ) ) {
36 return $backup_dir;
37 }
38
39 $filesystem = imagify_get_filesystem();
40 $backup_dir = $filesystem->get_site_root() . 'imagify-backup/';
41
42 /**
43 * Filter the backup directory path (custom folders).
44 *
45 * @since 1.7
46 * @author Grégory Viguier
47 *
48 * @param string $backup_dir The backup directory path.
49 */
50 $backup_dir = apply_filters( 'imagify_files_backup_directory', $backup_dir );
51 $backup_dir = $filesystem->normalize_dir_path( $backup_dir );
52
53 return $backup_dir;
54 }
55
56 /**
57 * Tell if the folder containing the backups is writable (custom folders).
58 *
59 * @since 1.7
60 * @access public
61 * @author Grégory Viguier
62 *
63 * @return bool
64 */
65 public static function backup_dir_is_writable() {
66 return imagify_get_filesystem()->make_dir( self::get_backup_dir_path() );
67 }
68
69 /**
70 * Get the backup path of a specific file (custom folders).
71 *
72 * @since 1.7
73 * @access public
74 * @author Grégory Viguier
75 *
76 * @param string $file_path The file path.
77 * @return string|bool The backup path. False on failure.
78 */
79 public static function get_file_backup_path( $file_path ) {
80 $file_path = wp_normalize_path( (string) $file_path );
81 $site_root = imagify_get_filesystem()->get_site_root();
82 $backup_dir = self::get_backup_dir_path();
83
84 if ( ! $file_path ) {
85 return false;
86 }
87
88 return preg_replace( '@^' . preg_quote( $site_root, '@' ) . '@', $backup_dir, $file_path );
89 }
90
91 /**
92 * Add index.php files recursively to a given directory and all its subdirectories.
93 *
94 * @since 1.9.11
95 *
96 * @param string $backup_dir (optional) Path to the directory where we will start adding indexes.
97 * Defaults to custom-folders backup dir.
98 *
99 * @return void
100 */
101 public static function add_indexes( $backup_dir = '' ) {
102 $filesystem = Imagify_Filesystem::get_instance();
103
104 if ( empty( $backup_dir ) ) {
105 $backup_dir = self::get_backup_dir_path();
106 }
107
108 if ( ! $filesystem->is_writable( $backup_dir ) ) {
109 return;
110 }
111
112 try {
113 $directory = new RecursiveDirectoryIterator( $backup_dir );
114 $iterator = new RecursiveIteratorIterator( $directory );
115
116 foreach ( $iterator as $fileinfo ) {
117
118 if ( '.' !== $fileinfo->getFilename() ) {
119 continue;
120 }
121
122 $path = trailingslashit( $fileinfo->getRealPath() );
123
124 if ( ! $filesystem->is_file( $path . 'index.html' )
125 && ! $filesystem->is_file( $path . 'index.php' )
126 ) {
127 $filesystem->touch( $path . 'index.php' );
128 }
129 }
130 } catch ( Exception $e ) {
131 return;
132 }
133 }
134
135 /** ----------------------------------------------------------------------------------------- */
136 /** SINGLE FILE ============================================================================= */
137 /** ----------------------------------------------------------------------------------------- */
138
139 /**
140 * Insert a file into the DB.
141 *
142 * @since 1.7
143 * @access public
144 * @author Grégory Viguier
145 *
146 * @param array $args An array of arguments to pass to Imagify_Files_DB::insert(). Required values are 'folder_id' and ( 'path' or 'file_path').
147 * @return int The file ID on success. 0 on failure.
148 */
149 public static function insert_file( $args = [] ) {
150 if ( empty( $args['folder_id'] ) ) {
151 return 0;
152 }
153
154 if ( empty( $args['path'] ) ) {
155 if ( empty( $args['file_path'] ) ) {
156 return 0;
157 }
158
159 $args['path'] = Imagify_Files_Scan::add_placeholder( $args['file_path'] );
160 }
161
162 if ( empty( $args['file_path'] ) ) {
163 $args['file_path'] = Imagify_Files_Scan::remove_placeholder( $args['path'] );
164 }
165
166 $filesystem = imagify_get_filesystem();
167
168 if ( ! $filesystem->is_readable( $args['file_path'] ) ) {
169 return 0;
170 }
171
172 if ( empty( $args['file_date'] ) || '0000-00-00 00:00:00' === $args['file_date'] ) {
173 $args['file_date'] = $filesystem->get_date( $args['file_path'] );
174 }
175
176 if ( empty( $args['mime_type'] ) ) {
177 $args['mime_type'] = $filesystem->get_mime_type( $args['file_path'] );
178 }
179
180 if ( ( empty( $args['width'] ) || empty( $args['height'] ) ) && strpos( $args['mime_type'], 'image/' ) === 0 ) {
181 $file_size = $filesystem->get_image_size( $args['file_path'] );
182 $args['width'] = $file_size ? $file_size['width'] : 0;
183 $args['height'] = $file_size ? $file_size['height'] : 0;
184 }
185
186 if ( empty( $args['hash'] ) ) {
187 $args['hash'] = md5_file( $args['file_path'] );
188 }
189
190 if ( empty( $args['original_size'] ) ) {
191 $args['original_size'] = (int) $filesystem->size( $args['file_path'] );
192 }
193
194 $files_db = Imagify_Files_DB::get_instance();
195 $primary_key = $files_db->get_primary_key();
196 unset( $args[ $primary_key ] );
197
198 return $files_db->insert( $args );
199 }
200
201 /**
202 * Delete a custom file.
203 *
204 * @since 1.7
205 * @access public
206 * @author Grégory Viguier
207 *
208 * @param array $args An array of arguments.
209 * At least: 'file_id'. At best: 'file_id', 'file_path' (or 'path' for the placeholder), and 'backup_path'.
210 */
211 public static function delete_file( $args = [] ) {
212 $args = array_merge(
213 [
214 'file_id' => 0,
215 'file_path' => '',
216 'path' => '',
217 'backup_path' => '',
218 'process' => false,
219 ],
220 $args
221 );
222
223 $filesystem = imagify_get_filesystem();
224
225 // Fill the blanks.
226 if ( $args['process'] && $args['process'] instanceof \Imagify\Optimization\Process\ProcessInterface ) {
227 $process = $args['process'];
228 } else {
229 $process = imagify_get_optimization_process( $args['file_id'], 'custom-folders' );
230 }
231
232 if ( ! $process->is_valid() ) {
233 // You fucked up!
234 return;
235 }
236
237 if ( ! $args['file_path'] && $args['path'] ) {
238 $args['file_path'] = Imagify_Files_Scan::remove_placeholder( $args['path'] );
239 }
240
241 if ( ! $args['file_path'] && $args['file_id'] ) {
242 $args['file_path'] = $process->get_media()->get_fullsize_path();
243 }
244
245 if ( ! $args['backup_path'] && $args['file_path'] ) {
246 $args['backup_path'] = self::get_file_backup_path( $args['file_path'] );
247 }
248
249 if ( ! $args['backup_path'] && $args['file_id'] ) {
250 $args['backup_path'] = $process->get_media()->get_raw_backup_path();
251 }
252
253 // Trigger a common hook.
254 imagify_trigger_delete_media_hook( $process );
255
256 // The file.
257 if ( $args['file_path'] && $filesystem->exists( $args['file_path'] ) ) {
258 $filesystem->delete( $args['file_path'] );
259 }
260
261 // The backup file.
262 if ( $args['backup_path'] && $filesystem->exists( $args['backup_path'] ) ) {
263 $filesystem->delete( $args['backup_path'] );
264 }
265
266 // WebP.
267 $mime_type = $filesystem->get_mime_type( $args['file_path'] );
268 $is_image = $mime_type && strpos( $mime_type, 'image/' ) === 0;
269 $webp_path = $is_image ? imagify_path_to_webp( $args['file_path'] ) : false;
270
271 if ( $webp_path && $filesystem->is_writable( $webp_path ) ) {
272 $filesystem->delete( $webp_path );
273 }
274
275 // In the database.
276 $process->get_media()->delete_row();
277 }
278
279 /**
280 * Check if a file has been modified, and update the database accordingly.
281 *
282 * @since 1.7
283 * @access public
284 * @author Grégory Viguier
285 *
286 * @param ProcessInterface $process A \Imagify\Optimization\Process\ProcessInterface object.
287 * @param bool $is_folder_active Tell if the folder is active.
288 * @return int|bool|object The file ID if modified. False if not modified. A WP_Error object if the entry has been removed from the database.
289 * The entry is removed from the database if:
290 * - The file doesn't exist anymore.
291 * - Or if its folder is not active and: the file has been modified, or the file is not optimized by Imagify, or the file is orphan (its folder is not in the database anymore).
292 */
293 public static function refresh_file( $process, $is_folder_active = null ) {
294 global $wpdb;
295
296 if ( ! $process->is_valid() ) {
297 return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
298 }
299
300 $filesystem = imagify_get_filesystem();
301 $media = $process->get_media();
302 $file_path = $media->get_fullsize_path();
303 $mime_type = $filesystem->get_mime_type( $file_path );
304 $is_image = $mime_type && strpos( $mime_type, 'image/' ) === 0;
305 $webp_path = $is_image ? imagify_path_to_webp( $file_path ) : false;
306 $has_webp = $webp_path && $filesystem->is_writable( $webp_path );
307 $modified = false;
308
309 if ( ! $file_path || ! $filesystem->exists( $file_path ) ) {
310 /**
311 * The file doesn't exist anymore.
312 */
313 // Delete the backup file.
314 $process->delete_backup();
315
316 // Get the folder ID before removing the row.
317 $folder_id = $media->get_row();
318 $folder_id = $folder_id['folder_id'];
319
320 // Remove the entry from the database.
321 $media->delete_row();
322
323 // Remove the corresponding folder if inactive and have no files left.
324 self::remove_empty_inactive_folders( $folder_id );
325
326 // Delete the WebP version.
327 if ( $has_webp ) {
328 $filesystem->delete( $webp_path );
329 }
330
331 return new WP_Error( 'no-file', __( 'The file was missing or its path could not be retrieved from the database. The entry has been deleted from the database.', 'imagify' ) );
332 }
333
334 /**
335 * The file still exists.
336 */
337 $old_data = $media->get_row();
338 $new_data = [];
339
340 // Folder ID.
341 if ( $old_data['folder_id'] ) {
342 $folder = wp_cache_get( 'custom_folder_' . $old_data['folder_id'], 'imagify' );
343
344 if ( false === $folder ) {
345 // The folder is not in the cache.
346 $folder = Imagify_Folders_DB::get_instance()->get( $old_data['folder_id'] );
347 $folder = $folder ? $folder : 0;
348 }
349
350 if ( ! $folder ) {
351 // The folder is not in the database anymore.
352 $old_data['folder_id'] = 0;
353 $new_data['folder_id'] = 0;
354 }
355 } else {
356 $folder = 0;
357 }
358
359 // Hash + modified.
360 $current_hash = md5_file( $file_path );
361
362 if ( ! $old_data['hash'] ) {
363 $new_data['modified'] = 0;
364 } else {
365 $new_data['modified'] = (int) ! hash_equals( $old_data['hash'], $current_hash );
366 }
367
368 // The file is modified or is not optimized.
369 if ( $new_data['modified'] || ! $process->get_data()->is_optimized() ) {
370 if ( ! isset( $is_folder_active ) ) {
371 $is_folder_active = $folder && $folder['active'];
372 }
373
374 // Its folder is not active: remove the entry from the database and delete the backup.
375 if ( ! $is_folder_active ) {
376 // Delete the backup file.
377 $process->delete_backup();
378
379 // Remove the entry from the database.
380 $media->delete_row();
381
382 // Remove the corresponding folder if inactive and have no files left.
383 if ( $old_data['folder_id'] ) {
384 self::remove_empty_inactive_folders( $old_data['folder_id'] );
385 }
386
387 // Delete the WebP version.
388 if ( $has_webp ) {
389 $filesystem->delete( $webp_path );
390 }
391
392 return new WP_Error( 'folder-not-active', __( 'The file has been modified or was not optimized: its folder not being selected in the settings, the entry has been deleted from the database.', 'imagify' ) );
393 }
394 }
395
396 $new_data['hash'] = $current_hash;
397
398 // The file is modified.
399 if ( $new_data['modified'] ) {
400 // Delete all optimization data and update file data.
401 $modified = true;
402 $mime_type = ! empty( $old_data['mime_type'] ) ? $old_data['mime_type'] : $filesystem->get_mime_type( $file_path );
403
404 if ( $is_image ) {
405 $size = $filesystem->get_image_size( $file_path );
406
407 // Delete the WebP version.
408 if ( $has_webp ) {
409 $filesystem->delete( $webp_path );
410 }
411 } else {
412 $size = false;
413 }
414
415 $new_data = array_merge(
416 $new_data,
417 [
418 'file_date' => $filesystem->get_date( $file_path ),
419 'width' => $size ? $size['width'] : 0,
420 'height' => $size ? $size['height'] : 0,
421 'original_size' => $filesystem->size( $file_path ),
422 'optimized_size' => null,
423 'percent' => null,
424 'optimization_level' => null,
425 'status' => null,
426 'error' => null,
427 'data' => [],
428 ]
429 );
430
431 // Delete the backup of the previous file.
432 $process->delete_backup();
433 } else {
434 // Update file data to make sure nothing is missing.
435 $backup_path = $media->get_backup_path();
436 $path = $backup_path ? $backup_path : $file_path;
437 $mime_type = ! empty( $old_data['mime_type'] ) ? $old_data['mime_type'] : $filesystem->get_mime_type( $path );
438 $file_date = ! empty( $old_data['file_date'] ) && '0000-00-00 00:00:00' !== $old_data['file_date'] ? $old_data['file_date'] : $filesystem->get_date( $path );
439
440 if ( $is_image ) {
441 $size = $filesystem->get_image_size( $path );
442 } else {
443 $size = false;
444 }
445
446 $new_data = array_merge(
447 $new_data,
448 [
449 'file_date' => $file_date,
450 'width' => $size ? $size['width'] : 0,
451 'height' => $size ? $size['height'] : 0,
452 'original_size' => $filesystem->size( $path ),
453 ]
454 );
455
456 // WebP.
457 $webp_size = 'full' . $process::WEBP_SUFFIX;
458
459 if ( $has_webp && empty( $old_data['data'][ $webp_size ]['success'] ) ) {
460 $webp_file_size = $filesystem->size( $webp_path );
461
462 $old_data['data'][ $webp_size ] = [
463 'success' => true,
464 'original_size' => $new_data['original_size'],
465 'optimized_size' => $webp_file_size,
466 'percent' => round( ( ( $new_data['original_size'] - $webp_file_size ) / $new_data['original_size'] ) * 100, 2 ),
467 ];
468 }
469 }
470
471 // Save the new data.
472 $old_data = array_intersect_key( $old_data, $new_data );
473 ksort( $old_data );
474 ksort( $new_data );
475
476 if ( $old_data !== $new_data ) {
477 $media->update_row( $new_data );
478 }
479
480 return $modified ? $media->get_id() : false;
481 }
482
483
484 /** ----------------------------------------------------------------------------------------- */
485 /** FOLDERS AND FILES ======================================================================= */
486 /** ----------------------------------------------------------------------------------------- */
487
488 /**
489 * Get folders from the DB.
490 *
491 * @since 1.7
492 * @access public
493 * @author Grégory Viguier
494 *
495 * @param array $args A list of arguments to tell more precisely what to fetch:
496 * - bool $active True to fetch only "active" folders (checked in the settings). False to fetch only folders that are not "active".
497 * @return array An array of arrays containing the following values:
498 * - int $folder_id The folder ID.
499 * - string $path The folder path, with placeholder.
500 * - int $active 1 if the folder should be optimized. 0 otherwize.
501 * - string $folder_path The real absolute folder path.
502 * Example:
503 * Array(
504 * [7] => Array(
505 * [folder_id] => 7
506 * [path] => {{ROOT}}/custom-path/
507 * [active] => 1
508 * [folder_path] => /absolute/path/to/custom-path/
509 * )
510 * [13] => Array(
511 * [folder_id] => 13
512 * [path] => {{CONTENT}}/another-custom-path/
513 * [active] => 1
514 * [folder_path] => /absolute/path/to/wp-content/another-custom-path/
515 * )
516 * )
517 */
518 public static function get_folders( $args = [] ) {
519 global $wpdb;
520
521 $folders_db = Imagify_Folders_DB::get_instance();
522 $folders_table = $folders_db->get_table_name();
523 $primary_key = $folders_db->get_primary_key();
524 $where_active = '';
525
526 if ( isset( $args['active'] ) ) {
527 if ( $args['active'] ) {
528 $args['active'] = true;
529 $where_active = 'WHERE active = 1';
530 } else {
531 $args['active'] = false;
532 $where_active = 'WHERE active = 0';
533 }
534 }
535
536 // Get the folders from the DB.
537 $results = $wpdb->get_results( "SELECT * FROM $folders_table $where_active;", ARRAY_A ); // WPCS: unprepared SQL ok.
538
539 if ( ! $results || ! is_array( $results ) ) {
540 return [];
541 }
542
543 // Cast results, add absolute paths.
544 $folders = [];
545
546 foreach ( $results as $row_fields ) {
547 // Cast the row.
548 $row_fields = $folders_db->cast_row( $row_fields );
549
550 // Add the absolute path.
551 $row_fields['folder_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] );
552
553 // Add the row to the list.
554 $folders[ $row_fields[ $primary_key ] ] = $row_fields;
555 }
556
557 return $folders;
558 }
559
560 /**
561 * Get files belonging to the given folders.
562 * Files are scanned from the folders, then:
563 * - If a file doesn't exist in the DB, it is added (maybe, depending on arguments provided).
564 * - If a file is in the DB, but with a wrong folder_id, it is fixed.
565 * - If a file doesn't exist, it is removed from the database and its backup is deleted.
566 *
567 * @since 1.7
568 * @access public
569 * @see Imagify_Custom_Folders::get_folders()
570 * @author Grégory Viguier
571 *
572 * @param array $folders An array of arrays containing at least the keys 'folder_path' and 'active'. See Imagify_Custom_Folders::get_folders() for the format.
573 * @param array $args A list of arguments to tell more precisely what to fetch:
574 * - int $optimization_level If set with an integer, only files that needs to be optimized to this level will be returned (the status is also checked).
575 * - bool $return_only_old_files True to return only files that have not been newly inserted.
576 * - bool $add_inactive_folder_files When true: if a file is not in the database and its folder is not "active", it is added to the DB. Default false: new files are not added to the database if the folder is not active.
577 * @return array A list of files in the following format:
578 * Array(
579 * [_2] => Array(
580 * [file_id] => 2
581 * [folder_id] => 7
582 * [path] => {{ROOT}}/custom-path/image-1.jpg
583 * [optimization_level] => null
584 * [status] => null
585 * [file_path] => /absolute/path/to/custom-path/image-1.jpg
586 * ),
587 * [_3] => Array(
588 * [file_id] => 3
589 * [folder_id] => 7
590 * [path] => {{ROOT}}/custom-path/image-2.jpg
591 * [optimization_level] => 2
592 * [status] => success
593 * [file_path] => /absolute/path/to/custom-path/image-2.jpg
594 * ),
595 * [_6] => Array(
596 * [file_id] => 6
597 * [folder_id] => 13
598 * [path] => {{CONTENT}}/another-custom-path/image-1.jpg
599 * [optimization_level] => 0
600 * [status] => error
601 * [file_path] => /absolute/path/to/wp-content/another-custom-path/image-1.jpg
602 * ),
603 * )
604 * The fields 'optimization_level' and 'status' are set only if the argument 'optimization_level' was set.
605 */
606 public static function get_files_from_folders( $folders, $args = [] ) {
607 global $wpdb;
608
609 if ( ! $folders ) {
610 return [];
611 }
612
613 $filesystem = imagify_get_filesystem();
614 $files_db = Imagify_Files_DB::get_instance();
615 $files_table = $files_db->get_table_name();
616 $files_key = $files_db->get_primary_key();
617 $files_key_esc = esc_sql( $files_key );
618
619 $optimization = isset( $args['optimization_level'] ) && is_numeric( $args['optimization_level'] );
620 $no_new_files = ! empty( $args['return_only_old_files'] );
621 $add_inactive_folder_files = ! empty( $args['add_inactive_folder_files'] );
622
623 /**
624 * Scan folders for files. $files_from_scan will be in the following format:
625 * Array(
626 * [7] => Array(
627 * [/absolute/path/to/custom-path/image-1.jpg] => 0
628 * [/absolute/path/to/custom-path/image-2.jpg] => 1
629 * )
630 * [13] => Array(
631 * [/absolute/path/to/wp-content/another-custom-path/image-1.jpg] => 0
632 * [/absolute/path/to/wp-content/another-custom-path/image-2.jpg] => 1
633 * [/absolute/path/to/wp-content/another-custom-path/image-3.jpg] => 2
634 * )
635 * )
636 */
637 $files_from_scan = [];
638
639 foreach ( $folders as $folder_id => $folder ) {
640 $files_from_scan[ $folder_id ] = Imagify_Files_Scan::get_files_from_folder( $folder['folder_path'] );
641
642 if ( is_wp_error( $files_from_scan[ $folder_id ] ) ) {
643 unset( $files_from_scan[ $folder_id ] );
644 }
645 }
646
647 $files_from_scan = array_map( 'array_flip', $files_from_scan );
648
649 /**
650 * Get the files from DB. $files_from_db will be in the same format as the function output.
651 */
652 $already_optimized = [];
653 $folder_ids = array_keys( $folders );
654 $files_from_db = array_fill_keys( $folder_ids, [] );
655 $folder_ids = Imagify_DB::prepare_values_list( $folder_ids );
656 $select_fields = "$files_key_esc, folder_id, path" . ( $optimization ? ', optimization_level, status' : '' );
657
658 if ( $optimization ) {
659 $orderby = "
660 CASE status
661 WHEN 'already_optimized' THEN 3
662 WHEN 'error' THEN 2
663 ELSE 1
664 END ASC,
665 $files_key_esc DESC";
666 } else {
667 $orderby = "folder_id, $files_key_esc";
668 }
669
670 $results = $wpdb->get_results( "SELECT $select_fields FROM $files_table WHERE folder_id IN ( $folder_ids ) ORDER BY $orderby;", ARRAY_A ); // WPCS: unprepared SQL ok.
671
672 if ( $results ) {
673 $wpdb->flush();
674
675 foreach ( $results as $i => $row_fields ) {
676 // Cast the row.
677 $row_fields = $files_db->cast_row( $row_fields );
678
679 // Add the absolute path.
680 $row_fields['file_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] );
681
682 // Remove the file from the scan.
683 unset( $files_from_scan[ $row_fields['folder_id'] ][ $row_fields['file_path'] ] );
684
685 if ( $optimization ) {
686 if ( 'error' !== $row_fields['status'] && $row_fields['optimization_level'] === $args['optimization_level'] ) {
687 // Try the same level only if the status is an error.
688 continue;
689 }
690
691 if ( 'already_optimized' === $row_fields['status'] && $row_fields['optimization_level'] >= $args['optimization_level'] ) {
692 // If the image is already compressed, optimize only if the requested level is higher.
693 continue;
694 }
695
696 if ( 'success' === $row_fields['status'] && $args['optimization_level'] !== $row_fields['optimization_level'] ) {
697 $file_backup_path = self::get_file_backup_path( $row_fields['file_path'] );
698
699 if ( ! $file_backup_path || ! $filesystem->exists( $file_backup_path ) ) {
700 // Don't try to re-optimize if there is no backup file.
701 continue;
702 }
703 }
704 }
705
706 if ( ! $filesystem->exists( $row_fields['file_path'] ) ) {
707 // If the file doesn't exist: remove all traces of it and bail out.
708 self::delete_file(
709 [
710 'file_id' => $row_fields[ $files_key ],
711 'file_path' => $row_fields['file_path'],
712 ]
713 );
714 continue;
715 }
716
717 if ( $optimization && 'already_optimized' === $row_fields['status'] ) {
718 $already_optimized[ '_' . $row_fields[ $files_key ] ] = 1;
719 }
720
721 // Add the row to the list.
722 $files_from_db[ $row_fields['folder_id'] ][ '_' . $row_fields[ $files_key ] ] = $row_fields;
723 }
724 }
725
726 unset( $results );
727 $files_from_scan = array_filter( $files_from_scan );
728
729 // Make sure files from the scan are not already in the DB with another folder (shouldn't be possible, but, you know...).
730 if ( $files_from_scan ) {
731 $folders_by_placeholder = [];
732
733 foreach ( $files_from_scan as $folder_id => $folder_files ) {
734 foreach ( $folder_files as $file_path => $i ) {
735 $placeholder = Imagify_Files_Scan::add_placeholder( $file_path );
736
737 $folders_by_placeholder[ $placeholder ] = $folder_id;
738 $files_from_scan[ $folder_id ][ $file_path ] = $placeholder;
739 }
740 }
741
742 $select_fields = "$files_key_esc, folder_id, path" . ( $optimization ? ', optimization_level, status' : '' );
743
744 $results = [];
745
746 foreach ( Imagify_DB::chunk_in_values( array_keys( $folders_by_placeholder ) ) as $placeholders_chunk ) {
747 $placeholders = Imagify_DB::prepare_values_list( $placeholders_chunk );
748 $chunk_results = $wpdb->get_results( "SELECT $select_fields FROM $files_table WHERE path IN ( $placeholders );", ARRAY_A ); // WPCS: unprepared SQL ok.
749
750 if ( $chunk_results ) {
751 $results = array_merge( $results, $chunk_results );
752 }
753 }
754
755 if ( $results ) {
756 // Re-apply the ordering that was previously done in SQL (`ORDER BY folder_id, $files_key_esc`).
757 usort(
758 $results,
759 function ( $a, $b ) use ( $files_key_esc ) {
760 if ( $a['folder_id'] !== $b['folder_id'] ) {
761 return $a['folder_id'] <=> $b['folder_id'];
762 }
763 return $a[ $files_key_esc ] <=> $b[ $files_key_esc ];
764 }
765 );
766 }
767
768 if ( $results ) {
769 // Damn...
770 $wpdb->flush();
771
772 foreach ( $results as $i => $row_fields ) {
773 // Cast the row.
774 $row_fields = $files_db->cast_row( $row_fields );
775 $old_folder_id = $row_fields['folder_id'];
776
777 // Add the absolute path.
778 $row_fields['file_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] );
779
780 // Set the new folder ID.
781 $row_fields['folder_id'] = $folders_by_placeholder[ $row_fields['path'] ];
782
783 // Remove the file from everywhere.
784 unset(
785 $files_from_db[ $old_folder_id ][ '_' . $row_fields[ $files_key ] ],
786 $files_from_scan[ $old_folder_id ][ $row_fields['file_path'] ],
787 $files_from_scan[ $row_fields['folder_id'] ][ $row_fields['file_path'] ]
788 );
789
790 if ( $optimization ) {
791 if ( 'error' !== $row_fields['status'] && $row_fields['optimization_level'] === $args['optimization_level'] ) {
792 // Try the same level only if the status is an error.
793 continue;
794 }
795
796 if ( 'already_optimized' === $row_fields['status'] && $row_fields['optimization_level'] >= $args['optimization_level'] ) {
797 // If the image is already compressed, optimize only if the requested level is higher.
798 continue;
799 }
800
801 if ( 'success' === $row_fields['status'] && $args['optimization_level'] !== $row_fields['optimization_level'] ) {
802 $file_backup_path = self::get_file_backup_path( $row_fields['file_path'] );
803
804 if ( ! $file_backup_path || ! $filesystem->exists( $file_backup_path ) ) {
805 // Don't try to re-optimize if there is no backup file.
806 continue;
807 }
808 }
809 }
810
811 if ( ! $filesystem->exists( $row_fields['file_path'] ) ) {
812 // If the file doesn't exist: remove all traces of it and bail out.
813 self::delete_file(
814 [
815 'file_id' => $row_fields[ $files_key ],
816 'file_path' => $row_fields['file_path'],
817 ]
818 );
819 continue;
820 }
821
822 // Set the correct folder ID in the DB.
823 $success = $files_db->update(
824 $row_fields[ $files_key ],
825 [
826 'folder_id' => $row_fields['folder_id'],
827 ]
828 );
829
830 if ( $success ) {
831 if ( $optimization && 'already_optimized' === $row_fields['status'] ) {
832 $already_optimized[ '_' . $row_fields[ $files_key ] ] = 1;
833 }
834
835 $files_from_db[ $row_fields['folder_id'] ][ '_' . $row_fields[ $files_key ] ] = $row_fields;
836 }
837 }
838 }
839
840 unset( $results, $folders_by_placeholder );
841 }
842
843 $files_from_scan = array_filter( $files_from_scan );
844
845 // Insert the remaining files into the DB.
846 if ( $files_from_scan ) {
847 foreach ( $files_from_scan as $folder_id => $placeholders ) {
848 // Don't add the file to the DB if its folder is not "active".
849 if ( ! $add_inactive_folder_files && empty( $folders[ $folder_id ]['active'] ) ) {
850 unset( $files_from_scan[ $folder_id ] );
851 continue;
852 }
853
854 foreach ( $placeholders as $file_path => $placeholder ) {
855 $file_id = self::insert_file(
856 [
857 'folder_id' => $folder_id,
858 'path' => $placeholder,
859 'file_path' => $file_path,
860 ]
861 );
862
863 if ( $file_id && ! $no_new_files ) {
864 $files_from_db[ $folder_id ][ '_' . $file_id ] = [
865 'file_id' => $file_id,
866 'folder_id' => $folder_id,
867 'path' => $placeholder,
868 'optimization_level' => null,
869 'status' => null,
870 'file_path' => $file_path,
871 ];
872 }
873 }
874
875 unset( $files_from_scan[ $folder_id ] );
876 }
877 }
878
879 $files_from_db = array_filter( $files_from_db );
880
881 if ( ! $files_from_db ) {
882 return [];
883 }
884
885 $files_from_db = call_user_func_array( 'array_merge', array_values( $files_from_db ) );
886
887 if ( $already_optimized ) {
888 // Put the files already optimized at the end of the list.
889 $already_optimized = array_intersect_key( $files_from_db, $already_optimized );
890 $files_from_db = array_diff_key( $files_from_db, $already_optimized );
891 $files_from_db = array_merge( $files_from_db, $already_optimized );
892 }
893
894 return $files_from_db;
895 }
896
897 /**
898 * Check if files inside the given folders have been modified, and update the database accordingly.
899 *
900 * @since 1.7
901 * @access public
902 * @author Grégory Viguier
903 *
904 * @param array $folders A list of folders. See Imagify_Custom_Folders::get_folders() for the format.
905 */
906 public static function synchronize_files_from_folders( $folders ) {
907 global $wpdb;
908 /**
909 * Get the files from DB, and from the folder.
910 */
911 $files = self::get_files_from_folders(
912 $folders,
913 [
914 'return_only_old_files' => true,
915 ]
916 );
917
918 if ( ! $files ) {
919 // This folder doesn't have (new) images.
920 return;
921 }
922
923 $files_db = Imagify_Files_DB::get_instance();
924 $files_table = $files_db->get_table_name();
925 $files_key = $files_db->get_primary_key();
926 $files_key_esc = esc_sql( $files_key );
927 $file_ids = wp_list_pluck( $files, $files_key );
928 $results = [];
929
930 foreach ( Imagify_DB::chunk_in_values( $file_ids ) as $file_ids_chunk ) {
931 $file_ids_in = Imagify_DB::prepare_values_list( $file_ids_chunk );
932 $chunk_results = $wpdb->get_results( "SELECT * FROM $files_table WHERE $files_key IN ( $file_ids_in );", ARRAY_A ); // WPCS: unprepared SQL ok.
933
934 if ( $chunk_results ) {
935 $results = array_merge( $results, $chunk_results );
936 }
937 }
938
939 if ( ! $results ) {
940 return;
941 }
942
943 // Re-apply the ordering that was previously done in SQL (`ORDER BY $files_key_esc`).
944 usort(
945 $results,
946 function ( $a, $b ) use ( $files_key_esc ) {
947 return $a[ $files_key_esc ] <=> $b[ $files_key_esc ];
948 }
949 );
950
951 // Caching the folders will prevent unecessary SQL queries in Imagify_Custom_Folders::refresh_file().
952 foreach ( $folders as $folder_id => $folder ) {
953 wp_cache_set( 'custom_folder_' . $folder_id, $folder, 'imagify' );
954 }
955
956 // Finally, refresh the files data.
957 foreach ( $results as $file ) {
958 $file = $files_db->cast_row( $file );
959 $folder_id = $file['folder_id'];
960 $process = imagify_get_optimization_process( $file, 'custom-folders' );
961
962 self::refresh_file( $process, $folders[ $folder_id ]['active'] );
963 }
964
965 foreach ( $folders as $folder_id => $folder ) {
966 wp_cache_delete( 'custom_folder_' . $folder_id, 'imagify' );
967 }
968 }
969
970
971 /** ----------------------------------------------------------------------------------------- */
972 /** WHEN SAVING SELECTED FOLDERS ============================================================ */
973 /** ----------------------------------------------------------------------------------------- */
974
975 /**
976 * Dectivate all active folders.
977 *
978 * @since 1.7
979 * @access public
980 * @author Grégory Viguier
981 */
982 public static function deactivate_all_folders() {
983 self::deactivate_not_selected_folders();
984 }
985
986 /**
987 * Dectivate folders that are not selected.
988 *
989 * @since 1.7
990 * @access public
991 * @author Grégory Viguier
992 *
993 * @param array|object|string $selected_paths A list of "placeholdered" paths corresponding to the selected folders.
994 */
995 public static function deactivate_not_selected_folders( $selected_paths = [] ) {
996 global $wpdb;
997
998 $folders_table = Imagify_Folders_DB::get_instance()->get_table_name();
999
1000 if ( $selected_paths ) {
1001 if ( is_array( $selected_paths ) || is_object( $selected_paths ) ) {
1002 $selected_paths = Imagify_DB::prepare_values_list( $selected_paths );
1003 }
1004
1005 $selected_paths_clause = "AND path NOT IN ( $selected_paths )";
1006 } else {
1007 $selected_paths_clause = '';
1008 }
1009
1010 // Remove the active status from the folders that are not selected.
1011 $wpdb->query( "UPDATE $folders_table SET active = 0 WHERE active != 0 $selected_paths_clause" ); // WPCS: unprepared SQL ok.
1012 }
1013
1014 /**
1015 * Activate folders that are selected.
1016 *
1017 * @since 1.7
1018 * @access public
1019 * @author Grégory Viguier
1020 *
1021 * @param array|object $selected_paths A list of "placeholdered" paths corresponding to the selected folders.
1022 * @return array An array of paths of folders that are not in the DB.
1023 */
1024 public static function activate_selected_folders( $selected_paths ) {
1025 global $wpdb;
1026
1027 if ( ! $selected_paths ) {
1028 return $selected_paths;
1029 }
1030
1031 $folders_db = Imagify_Folders_DB::get_instance();
1032 $folders_table = $folders_db->get_table_name();
1033 $folders_key = $folders_db->get_primary_key();
1034
1035 $selected_paths = (array) $selected_paths;
1036 $folders = [];
1037
1038 // Get folders that already are in the DB.
1039 foreach ( Imagify_DB::chunk_in_values( $selected_paths ) as $selected_paths_chunk ) {
1040 $selected_in = Imagify_DB::prepare_values_list( $selected_paths_chunk );
1041 $chunk_folders = $wpdb->get_results( "SELECT * FROM $folders_table WHERE path IN ( $selected_in );", ARRAY_A ); // WPCS: unprepared SQL ok.
1042
1043 if ( $chunk_folders ) {
1044 $folders = array_merge( $folders, $chunk_folders );
1045 }
1046 }
1047
1048 if ( ! $folders ) {
1049 return $selected_paths;
1050 }
1051
1052 $selected_paths = array_flip( $selected_paths );
1053
1054 foreach ( $folders as $folder ) {
1055 $folder = $folders_db->cast_row( $folder );
1056
1057 if ( Imagify_Files_Scan::placeholder_path_exists( $folder['path'] ) ) {
1058 if ( ! $folder['active'] ) {
1059 // Add the active status only if not already set and if the folder exists.
1060 $folders_db->update(
1061 $folder[ $folders_key ],
1062 [
1063 'active' => 1,
1064 ]
1065 );
1066 }
1067 } else {
1068 // Remove the active status if the folder does not exist.
1069 $folders_db->update(
1070 $folder[ $folders_key ],
1071 [
1072 'active' => 0,
1073 ]
1074 );
1075 }
1076
1077 // Remove the path from the selected list, so the remaining will be created.
1078 unset( $selected_paths[ $folder['path'] ] );
1079 }
1080
1081 // Paths of folders that are not in the DB.
1082 return array_flip( $selected_paths );
1083 }
1084
1085 /**
1086 * Insert folders into the database.
1087 *
1088 * @since 1.7
1089 * @access public
1090 * @author Grégory Viguier
1091 *
1092 * @param array $folders An array of "placeholdered" paths.
1093 * @return array An array of folder IDs.
1094 */
1095 public static function insert_folders( $folders ) {
1096 if ( ! $folders ) {
1097 return [];
1098 }
1099
1100 $folder_ids = [];
1101 $filesystem = imagify_get_filesystem();
1102 $folders_db = Imagify_Folders_DB::get_instance();
1103
1104 foreach ( $folders as $placeholder ) {
1105 $full_path = Imagify_Files_Scan::remove_placeholder( $placeholder );
1106 $full_path = realpath( $full_path );
1107
1108 if ( ! $full_path || ! $filesystem->is_readable( $full_path ) || ! $filesystem->is_dir( $full_path ) ) {
1109 continue;
1110 }
1111
1112 if ( Imagify_Files_Scan::is_path_forbidden( trailingslashit( $full_path ) ) ) {
1113 continue;
1114 }
1115
1116 $folder_ids[] = $folders_db->insert(
1117 [
1118 'path' => $placeholder,
1119 'active' => 1,
1120 ]
1121 );
1122 }
1123
1124 return array_filter( $folder_ids );
1125 }
1126
1127 /**
1128 * Remove files that are in inactive folders and are not optimized.
1129 *
1130 * @since 1.7
1131 * @access public
1132 * @author Grégory Viguier
1133 */
1134 public static function remove_unoptimized_files_from_inactive_folders() {
1135 global $wpdb;
1136
1137 $folders_db = Imagify_Folders_DB::get_instance();
1138 $folders_key = $folders_db->get_primary_key();
1139 $files_table = Imagify_Files_DB::get_instance()->get_table_name();
1140 $folder_ids = $folders_db->get_active_folders_column( $folders_key );
1141
1142 if ( $folder_ids ) {
1143 $folder_ids = Imagify_DB::prepare_values_list( $folder_ids );
1144
1145 $wpdb->query( "DELETE FROM $files_table WHERE folder_id NOT IN ( $folder_ids ) AND ( status != 'success' OR status IS NULL )" ); // WPCS: unprepared SQL ok.
1146 } else {
1147 $wpdb->query( "DELETE FROM $files_table WHERE status != 'success' OR status IS NULL" ); // WPCS: unprepared SQL ok.
1148 }
1149 }
1150
1151 /**
1152 * Reassign inactive files to active folders.
1153 * Example:
1154 * - Consider the file "/a/b/c/d/file.png".
1155 * - The folder "/a/b/c/", previously active, becomes inactive.
1156 * - The folder "/a/b/", previously inactive, becomes active.
1157 * - The file is reassigned to the folder "/a/b/".
1158 *
1159 * @since 1.7
1160 * @access public
1161 * @author Grégory Viguier
1162 */
1163 public static function reassign_inactive_files() {
1164 global $wpdb;
1165
1166 $folders_db = Imagify_Folders_DB::get_instance();
1167 $folders_table = $folders_db->get_table_name();
1168 $folders_key = $folders_db->get_primary_key();
1169 $folders_key_esc = esc_sql( $folders_key );
1170
1171 $files_db = Imagify_Files_DB::get_instance();
1172 $files_table = $files_db->get_table_name();
1173 $files_key = $files_db->get_primary_key();
1174 $files_key_esc = esc_sql( $files_key );
1175
1176 // All active folders.
1177 $active_folders = $wpdb->get_results( "SELECT $folders_key_esc, path FROM $folders_table WHERE active = 1;", ARRAY_A ); // WPCS: unprepared SQL ok.
1178
1179 if ( ! $active_folders ) {
1180 return;
1181 }
1182
1183 $active_folder_ids = [];
1184 $has_site_root = false;
1185
1186 foreach ( $active_folders as $i => $active_folder ) {
1187 $active_folders[ $i ] = $folders_db->cast_row( $active_folder );
1188 $active_folder_ids[] = $active_folders[ $i ][ $folders_key ];
1189
1190 if ( '{{ROOT}}/' === $active_folders[ $i ]['path'] ) {
1191 $has_site_root = true;
1192 break;
1193 }
1194 }
1195
1196 // Files not in active folders.
1197 $active_folder_ids = Imagify_DB::prepare_values_list( $active_folder_ids );
1198 $inactive_files = $wpdb->get_results( "SELECT $files_key_esc, path FROM $files_table WHERE folder_id NOT IN ( $active_folder_ids )", ARRAY_A ); // WPCS: unprepared SQL ok.
1199
1200 if ( ! $inactive_files ) {
1201 return;
1202 }
1203
1204 $filesystem = imagify_get_filesystem();
1205 $file_ids_by_folder = [];
1206 $active_folders = self::sort_folders( $active_folders, true );
1207
1208 foreach ( $inactive_files as $inactive_file ) {
1209 $inactive_file = $files_db->cast_row( $inactive_file );
1210 $inactive_file['full_path'] = Imagify_Files_Scan::remove_placeholder( $inactive_file['path'] );
1211
1212 if ( $has_site_root ) {
1213 $inactive_file['dirname'] = $filesystem->dir_path( $inactive_file['full_path'] );
1214 }
1215
1216 foreach ( $active_folders as $active_folder ) {
1217 $folder_id = $active_folder[ $folders_key ];
1218
1219 if ( strpos( $inactive_file['full_path'], $active_folder['full_path'] ) !== 0 ) {
1220 // The file is not in this folder.
1221 continue;
1222 }
1223
1224 if ( ! isset( $file_ids_by_folder[ $folder_id ] ) ) {
1225 $file_ids_by_folder[ $folder_id ] = [];
1226 }
1227
1228 if ( '{{ROOT}}/' === $active_folder['path'] ) {
1229 // For the site's root: only direct childs.
1230 if ( $inactive_file['dirname'] === $active_folder['full_path'] ) {
1231 // This file is in the site's root folder.
1232 $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ];
1233 }
1234 break;
1235 }
1236
1237 // This file is not in the site's root, but still a grand-child of this folder.
1238 $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ];
1239 break;
1240 }
1241 }
1242
1243 $file_ids_by_folder = array_filter( $file_ids_by_folder );
1244
1245 if ( ! $file_ids_by_folder ) {
1246 return;
1247 }
1248
1249 // Set the new folder ID.
1250 foreach ( $file_ids_by_folder as $folder_id => $file_ids ) {
1251 $file_ids = Imagify_DB::prepare_values_list( $file_ids );
1252
1253 $wpdb->query( "UPDATE $files_table SET folder_id = $folder_id WHERE $files_key_esc IN ( $file_ids )" ); // WPCS: unprepared SQL ok.
1254 }
1255 }
1256
1257 /**
1258 * Remove the given folders from the DB if they are inactive and have no files.
1259 *
1260 * @since 1.7
1261 * @access public
1262 * @author Grégory Viguier
1263 *
1264 * @param array $folder_ids An array of folder IDs.
1265 * @return int Number of removed folders.
1266 */
1267 public static function remove_empty_inactive_folders( $folder_ids = null ) {
1268 global $wpdb;
1269
1270 $folders_db = Imagify_Folders_DB::get_instance();
1271 $folders_table = $folders_db->get_table_name();
1272 $folders_key = $folders_db->get_primary_key();
1273 $folders_key_esc = esc_sql( $folders_key );
1274 $files_table = Imagify_Files_DB::get_instance()->get_table_name();
1275
1276 $folder_ids = array_filter( (array) $folder_ids );
1277
1278 if ( $folder_ids ) {
1279 $folder_ids = $folders_db->cast_col( $folder_ids, $folders_key );
1280 $folder_ids = Imagify_DB::prepare_values_list( $folder_ids );
1281 $in_clause = "folders.$folders_key_esc IN ( $folder_ids )";
1282 } else {
1283 $in_clause = '1=1';
1284 }
1285
1286 // Within the range of given folder IDs, filter the ones that are inactive and have no files.
1287 $results = $wpdb->get_col( // WPCS: unprepared SQL ok.
1288 "
1289 SELECT folders.$folders_key_esc FROM $folders_table AS folders
1290 LEFT JOIN $files_table AS files ON folders.$folders_key_esc = files.folder_id
1291 WHERE $in_clause
1292 AND folders.active != 1
1293 AND files.folder_id IS NULL"
1294 );
1295
1296 if ( ! $results ) {
1297 return 0;
1298 }
1299
1300 $results = $folders_db->cast_col( $results, $folders_key );
1301 $results = Imagify_DB::prepare_values_list( $results );
1302
1303 // Remove inactive folders with no files.
1304 $wpdb->query( "DELETE FROM $folders_table WHERE $folders_key_esc IN ( $results )" ); // WPCS: unprepared SQL ok.
1305
1306 return (int) $wpdb->rows_affected;
1307 }
1308
1309
1310 /** ----------------------------------------------------------------------------------------- */
1311 /** TOOLS =================================================================================== */
1312 /** ----------------------------------------------------------------------------------------- */
1313
1314 /**
1315 * Sort folders by full path.
1316 * The row "full_path" is added to each folder.
1317 *
1318 * @since 1.7
1319 * @access public
1320 * @author Grégory Viguier
1321 *
1322 * @param array $folders An array of folders with at least a "path" row.
1323 * @param bool $reverse Reverse the order.
1324 * @return array
1325 */
1326 public static function sort_folders( $folders, $reverse = false ) {
1327 if ( ! $folders ) {
1328 return [];
1329 }
1330
1331 $keyed_folders = [];
1332 $keyed_paths = [];
1333
1334 foreach ( $folders as $folder ) {
1335 $folder = (array) $folder;
1336 $folder['full_path'] = Imagify_Files_Scan::remove_placeholder( $folder['path'] );
1337
1338 $keyed_folders[ $folder['path'] ] = $folder;
1339 $keyed_paths[ $folder['path'] ] = $folder['full_path'];
1340 }
1341
1342 natcasesort( $keyed_paths );
1343
1344 if ( $reverse ) {
1345 $keyed_paths = array_reverse( $keyed_paths, true );
1346 }
1347
1348 $keyed_folders = array_merge( $keyed_paths, $keyed_folders );
1349
1350 return array_values( $keyed_folders );
1351 }
1352
1353 /**
1354 * Remove sub-paths: if 'a/b/' and 'a/b/c/' are in the array, we keep only the "parent" 'a/b/'.
1355 *
1356 * @since 1.7
1357 * @access public
1358 * @author Grégory Viguier
1359 *
1360 * @param array $placeholders A list of "placeholdered" paths.
1361 * @return array
1362 */
1363 public static function remove_sub_paths( $placeholders ) {
1364 sort( $placeholders );
1365
1366 foreach ( $placeholders as $i => $placeholder_path ) {
1367 if ( '{{ROOT}}/' === $placeholder_path ) {
1368 continue;
1369 }
1370
1371 if ( ! isset( $prev_path ) ) {
1372 $prev_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) );
1373 continue;
1374 }
1375
1376 $placeholder_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) );
1377
1378 if ( strpos( $placeholder_path, $prev_path ) === 0 ) {
1379 unset( $placeholders[ $i ] );
1380 } else {
1381 $prev_path = $placeholder_path;
1382 }
1383 }
1384
1385 return $placeholders;
1386 }
1387 }
1388