PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.2.9
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.2.9
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 2.2.9, at inc/classes/class-imagify-custom-folders.php

1,344 lines 43.1 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 $placeholders = Imagify_DB::prepare_values_list( array_keys( $folders_by_placeholder ) );
743 $select_fields = "$files_key_esc, folder_id, path" . ( $optimization ? ', optimization_level, status' : '' );
744
745 $results = $wpdb->get_results( "SELECT $select_fields FROM $files_table WHERE path IN ( $placeholders ) ORDER BY folder_id, $files_key_esc;", ARRAY_A ); // WPCS: unprepared SQL ok.
746
747 if ( $results ) {
748 // Damn...
749 $wpdb->flush();
750
751 foreach ( $results as $i => $row_fields ) {
752 // Cast the row.
753 $row_fields = $files_db->cast_row( $row_fields );
754 $old_folder_id = $row_fields['folder_id'];
755
756 // Add the absolute path.
757 $row_fields['file_path'] = Imagify_Files_Scan::remove_placeholder( $row_fields['path'] );
758
759 // Set the new folder ID.
760 $row_fields['folder_id'] = $folders_by_placeholder[ $row_fields['path'] ];
761
762 // Remove the file from everywhere.
763 unset(
764 $files_from_db[ $old_folder_id ][ '_' . $row_fields[ $files_key ] ],
765 $files_from_scan[ $old_folder_id ][ $row_fields['file_path'] ],
766 $files_from_scan[ $row_fields['folder_id'] ][ $row_fields['file_path'] ]
767 );
768
769 if ( $optimization ) {
770 if ( 'error' !== $row_fields['status'] && $row_fields['optimization_level'] === $args['optimization_level'] ) {
771 // Try the same level only if the status is an error.
772 continue;
773 }
774
775 if ( 'already_optimized' === $row_fields['status'] && $row_fields['optimization_level'] >= $args['optimization_level'] ) {
776 // If the image is already compressed, optimize only if the requested level is higher.
777 continue;
778 }
779
780 if ( 'success' === $row_fields['status'] && $args['optimization_level'] !== $row_fields['optimization_level'] ) {
781 $file_backup_path = self::get_file_backup_path( $row_fields['file_path'] );
782
783 if ( ! $file_backup_path || ! $filesystem->exists( $file_backup_path ) ) {
784 // Don't try to re-optimize if there is no backup file.
785 continue;
786 }
787 }
788 }
789
790 if ( ! $filesystem->exists( $row_fields['file_path'] ) ) {
791 // If the file doesn't exist: remove all traces of it and bail out.
792 self::delete_file(
793 [
794 'file_id' => $row_fields[ $files_key ],
795 'file_path' => $row_fields['file_path'],
796 ]
797 );
798 continue;
799 }
800
801 // Set the correct folder ID in the DB.
802 $success = $files_db->update(
803 $row_fields[ $files_key ],
804 [
805 'folder_id' => $row_fields['folder_id'],
806 ]
807 );
808
809 if ( $success ) {
810 if ( $optimization && 'already_optimized' === $row_fields['status'] ) {
811 $already_optimized[ '_' . $row_fields[ $files_key ] ] = 1;
812 }
813
814 $files_from_db[ $row_fields['folder_id'] ][ '_' . $row_fields[ $files_key ] ] = $row_fields;
815 }
816 }
817 }
818
819 unset( $results, $folders_by_placeholder );
820 }
821
822 $files_from_scan = array_filter( $files_from_scan );
823
824 // Insert the remaining files into the DB.
825 if ( $files_from_scan ) {
826 foreach ( $files_from_scan as $folder_id => $placeholders ) {
827 // Don't add the file to the DB if its folder is not "active".
828 if ( ! $add_inactive_folder_files && empty( $folders[ $folder_id ]['active'] ) ) {
829 unset( $files_from_scan[ $folder_id ] );
830 continue;
831 }
832
833 foreach ( $placeholders as $file_path => $placeholder ) {
834 $file_id = self::insert_file(
835 [
836 'folder_id' => $folder_id,
837 'path' => $placeholder,
838 'file_path' => $file_path,
839 ]
840 );
841
842 if ( $file_id && ! $no_new_files ) {
843 $files_from_db[ $folder_id ][ '_' . $file_id ] = [
844 'file_id' => $file_id,
845 'folder_id' => $folder_id,
846 'path' => $placeholder,
847 'optimization_level' => null,
848 'status' => null,
849 'file_path' => $file_path,
850 ];
851 }
852 }
853
854 unset( $files_from_scan[ $folder_id ] );
855 }
856 }
857
858 $files_from_db = array_filter( $files_from_db );
859
860 if ( ! $files_from_db ) {
861 return [];
862 }
863
864 $files_from_db = call_user_func_array( 'array_merge', array_values( $files_from_db ) );
865
866 if ( $already_optimized ) {
867 // Put the files already optimized at the end of the list.
868 $already_optimized = array_intersect_key( $files_from_db, $already_optimized );
869 $files_from_db = array_diff_key( $files_from_db, $already_optimized );
870 $files_from_db = array_merge( $files_from_db, $already_optimized );
871 }
872
873 return $files_from_db;
874 }
875
876 /**
877 * Check if files inside the given folders have been modified, and update the database accordingly.
878 *
879 * @since 1.7
880 * @access public
881 * @author Grégory Viguier
882 *
883 * @param array $folders A list of folders. See Imagify_Custom_Folders::get_folders() for the format.
884 */
885 public static function synchronize_files_from_folders( $folders ) {
886 global $wpdb;
887 /**
888 * Get the files from DB, and from the folder.
889 */
890 $files = self::get_files_from_folders(
891 $folders,
892 [
893 'return_only_old_files' => true,
894 ]
895 );
896
897 if ( ! $files ) {
898 // This folder doesn't have (new) images.
899 return;
900 }
901
902 $files_db = Imagify_Files_DB::get_instance();
903 $files_table = $files_db->get_table_name();
904 $files_key = $files_db->get_primary_key();
905 $files_key_esc = esc_sql( $files_key );
906 $file_ids = wp_list_pluck( $files, $files_key );
907 $file_ids = Imagify_DB::prepare_values_list( $file_ids );
908 $results = $wpdb->get_results( "SELECT * FROM $files_table WHERE $files_key IN ( $file_ids ) ORDER BY $files_key_esc;", ARRAY_A ); // WPCS: unprepared SQL ok.
909
910 if ( ! $results ) {
911 return;
912 }
913
914 // Caching the folders will prevent unecessary SQL queries in Imagify_Custom_Folders::refresh_file().
915 foreach ( $folders as $folder_id => $folder ) {
916 wp_cache_set( 'custom_folder_' . $folder_id, $folder, 'imagify' );
917 }
918
919 // Finally, refresh the files data.
920 foreach ( $results as $file ) {
921 $file = $files_db->cast_row( $file );
922 $folder_id = $file['folder_id'];
923 $process = imagify_get_optimization_process( $file, 'custom-folders' );
924
925 self::refresh_file( $process, $folders[ $folder_id ]['active'] );
926 }
927
928 foreach ( $folders as $folder_id => $folder ) {
929 wp_cache_delete( 'custom_folder_' . $folder_id, 'imagify' );
930 }
931 }
932
933
934 /** ----------------------------------------------------------------------------------------- */
935 /** WHEN SAVING SELECTED FOLDERS ============================================================ */
936 /** ----------------------------------------------------------------------------------------- */
937
938 /**
939 * Dectivate all active folders.
940 *
941 * @since 1.7
942 * @access public
943 * @author Grégory Viguier
944 */
945 public static function deactivate_all_folders() {
946 self::deactivate_not_selected_folders();
947 }
948
949 /**
950 * Dectivate folders that are not selected.
951 *
952 * @since 1.7
953 * @access public
954 * @author Grégory Viguier
955 *
956 * @param array|object|string $selected_paths A list of "placeholdered" paths corresponding to the selected folders.
957 */
958 public static function deactivate_not_selected_folders( $selected_paths = [] ) {
959 global $wpdb;
960
961 $folders_table = Imagify_Folders_DB::get_instance()->get_table_name();
962
963 if ( $selected_paths ) {
964 if ( is_array( $selected_paths ) || is_object( $selected_paths ) ) {
965 $selected_paths = Imagify_DB::prepare_values_list( $selected_paths );
966 }
967
968 $selected_paths_clause = "AND path NOT IN ( $selected_paths )";
969 } else {
970 $selected_paths_clause = '';
971 }
972
973 // Remove the active status from the folders that are not selected.
974 $wpdb->query( "UPDATE $folders_table SET active = 0 WHERE active != 0 $selected_paths_clause" ); // WPCS: unprepared SQL ok.
975 }
976
977 /**
978 * Activate folders that are selected.
979 *
980 * @since 1.7
981 * @access public
982 * @author Grégory Viguier
983 *
984 * @param array|object $selected_paths A list of "placeholdered" paths corresponding to the selected folders.
985 * @return array An array of paths of folders that are not in the DB.
986 */
987 public static function activate_selected_folders( $selected_paths ) {
988 global $wpdb;
989
990 if ( ! $selected_paths ) {
991 return $selected_paths;
992 }
993
994 $folders_db = Imagify_Folders_DB::get_instance();
995 $folders_table = $folders_db->get_table_name();
996 $folders_key = $folders_db->get_primary_key();
997
998 $selected_paths = (array) $selected_paths;
999 $selected_in = Imagify_DB::prepare_values_list( $selected_paths );
1000
1001 // Get folders that already are in the DB.
1002 $folders = $wpdb->get_results( "SELECT * FROM $folders_table WHERE path IN ( $selected_in );", ARRAY_A ); // WPCS: unprepared SQL ok.
1003
1004 if ( ! $folders ) {
1005 return $selected_paths;
1006 }
1007
1008 $selected_paths = array_flip( $selected_paths );
1009
1010 foreach ( $folders as $folder ) {
1011 $folder = $folders_db->cast_row( $folder );
1012
1013 if ( Imagify_Files_Scan::placeholder_path_exists( $folder['path'] ) ) {
1014 if ( ! $folder['active'] ) {
1015 // Add the active status only if not already set and if the folder exists.
1016 $folders_db->update(
1017 $folder[ $folders_key ],
1018 [
1019 'active' => 1,
1020 ]
1021 );
1022 }
1023 } else {
1024 // Remove the active status if the folder does not exist.
1025 $folders_db->update(
1026 $folder[ $folders_key ],
1027 [
1028 'active' => 0,
1029 ]
1030 );
1031 }
1032
1033 // Remove the path from the selected list, so the remaining will be created.
1034 unset( $selected_paths[ $folder['path'] ] );
1035 }
1036
1037 // Paths of folders that are not in the DB.
1038 return array_flip( $selected_paths );
1039 }
1040
1041 /**
1042 * Insert folders into the database.
1043 *
1044 * @since 1.7
1045 * @access public
1046 * @author Grégory Viguier
1047 *
1048 * @param array $folders An array of "placeholdered" paths.
1049 * @return array An array of folder IDs.
1050 */
1051 public static function insert_folders( $folders ) {
1052 if ( ! $folders ) {
1053 return [];
1054 }
1055
1056 $folder_ids = [];
1057 $filesystem = imagify_get_filesystem();
1058 $folders_db = Imagify_Folders_DB::get_instance();
1059
1060 foreach ( $folders as $placeholder ) {
1061 $full_path = Imagify_Files_Scan::remove_placeholder( $placeholder );
1062 $full_path = realpath( $full_path );
1063
1064 if ( ! $full_path || ! $filesystem->is_readable( $full_path ) || ! $filesystem->is_dir( $full_path ) ) {
1065 continue;
1066 }
1067
1068 if ( Imagify_Files_Scan::is_path_forbidden( trailingslashit( $full_path ) ) ) {
1069 continue;
1070 }
1071
1072 $folder_ids[] = $folders_db->insert(
1073 [
1074 'path' => $placeholder,
1075 'active' => 1,
1076 ]
1077 );
1078 }
1079
1080 return array_filter( $folder_ids );
1081 }
1082
1083 /**
1084 * Remove files that are in inactive folders and are not optimized.
1085 *
1086 * @since 1.7
1087 * @access public
1088 * @author Grégory Viguier
1089 */
1090 public static function remove_unoptimized_files_from_inactive_folders() {
1091 global $wpdb;
1092
1093 $folders_db = Imagify_Folders_DB::get_instance();
1094 $folders_key = $folders_db->get_primary_key();
1095 $files_table = Imagify_Files_DB::get_instance()->get_table_name();
1096 $folder_ids = $folders_db->get_active_folders_column( $folders_key );
1097
1098 if ( $folder_ids ) {
1099 $folder_ids = Imagify_DB::prepare_values_list( $folder_ids );
1100
1101 $wpdb->query( "DELETE FROM $files_table WHERE folder_id NOT IN ( $folder_ids ) AND ( status != 'success' OR status IS NULL )" ); // WPCS: unprepared SQL ok.
1102 } else {
1103 $wpdb->query( "DELETE FROM $files_table WHERE status != 'success' OR status IS NULL" ); // WPCS: unprepared SQL ok.
1104 }
1105 }
1106
1107 /**
1108 * Reassign inactive files to active folders.
1109 * Example:
1110 * - Consider the file "/a/b/c/d/file.png".
1111 * - The folder "/a/b/c/", previously active, becomes inactive.
1112 * - The folder "/a/b/", previously inactive, becomes active.
1113 * - The file is reassigned to the folder "/a/b/".
1114 *
1115 * @since 1.7
1116 * @access public
1117 * @author Grégory Viguier
1118 */
1119 public static function reassign_inactive_files() {
1120 global $wpdb;
1121
1122 $folders_db = Imagify_Folders_DB::get_instance();
1123 $folders_table = $folders_db->get_table_name();
1124 $folders_key = $folders_db->get_primary_key();
1125 $folders_key_esc = esc_sql( $folders_key );
1126
1127 $files_db = Imagify_Files_DB::get_instance();
1128 $files_table = $files_db->get_table_name();
1129 $files_key = $files_db->get_primary_key();
1130 $files_key_esc = esc_sql( $files_key );
1131
1132 // All active folders.
1133 $active_folders = $wpdb->get_results( "SELECT $folders_key_esc, path FROM $folders_table WHERE active = 1;", ARRAY_A ); // WPCS: unprepared SQL ok.
1134
1135 if ( ! $active_folders ) {
1136 return;
1137 }
1138
1139 $active_folder_ids = [];
1140 $has_site_root = false;
1141
1142 foreach ( $active_folders as $i => $active_folder ) {
1143 $active_folders[ $i ] = $folders_db->cast_row( $active_folder );
1144 $active_folder_ids[] = $active_folders[ $i ][ $folders_key ];
1145
1146 if ( '{{ROOT}}/' === $active_folders[ $i ]['path'] ) {
1147 $has_site_root = true;
1148 break;
1149 }
1150 }
1151
1152 // Files not in active folders.
1153 $active_folder_ids = Imagify_DB::prepare_values_list( $active_folder_ids );
1154 $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.
1155
1156 if ( ! $inactive_files ) {
1157 return;
1158 }
1159
1160 $filesystem = imagify_get_filesystem();
1161 $file_ids_by_folder = [];
1162 $active_folders = self::sort_folders( $active_folders, true );
1163
1164 foreach ( $inactive_files as $inactive_file ) {
1165 $inactive_file = $files_db->cast_row( $inactive_file );
1166 $inactive_file['full_path'] = Imagify_Files_Scan::remove_placeholder( $inactive_file['path'] );
1167
1168 if ( $has_site_root ) {
1169 $inactive_file['dirname'] = $filesystem->dir_path( $inactive_file['full_path'] );
1170 }
1171
1172 foreach ( $active_folders as $active_folder ) {
1173 $folder_id = $active_folder[ $folders_key ];
1174
1175 if ( strpos( $inactive_file['full_path'], $active_folder['full_path'] ) !== 0 ) {
1176 // The file is not in this folder.
1177 continue;
1178 }
1179
1180 if ( ! isset( $file_ids_by_folder[ $folder_id ] ) ) {
1181 $file_ids_by_folder[ $folder_id ] = [];
1182 }
1183
1184 if ( '{{ROOT}}/' === $active_folder['path'] ) {
1185 // For the site's root: only direct childs.
1186 if ( $inactive_file['dirname'] === $active_folder['full_path'] ) {
1187 // This file is in the site's root folder.
1188 $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ];
1189 }
1190 break;
1191 }
1192
1193 // This file is not in the site's root, but still a grand-child of this folder.
1194 $file_ids_by_folder[ $folder_id ][] = $inactive_file[ $files_key ];
1195 break;
1196 }
1197 }
1198
1199 $file_ids_by_folder = array_filter( $file_ids_by_folder );
1200
1201 if ( ! $file_ids_by_folder ) {
1202 return;
1203 }
1204
1205 // Set the new folder ID.
1206 foreach ( $file_ids_by_folder as $folder_id => $file_ids ) {
1207 $file_ids = Imagify_DB::prepare_values_list( $file_ids );
1208
1209 $wpdb->query( "UPDATE $files_table SET folder_id = $folder_id WHERE $files_key_esc IN ( $file_ids )" ); // WPCS: unprepared SQL ok.
1210 }
1211 }
1212
1213 /**
1214 * Remove the given folders from the DB if they are inactive and have no files.
1215 *
1216 * @since 1.7
1217 * @access public
1218 * @author Grégory Viguier
1219 *
1220 * @param array $folder_ids An array of folder IDs.
1221 * @return int Number of removed folders.
1222 */
1223 public static function remove_empty_inactive_folders( $folder_ids = null ) {
1224 global $wpdb;
1225
1226 $folders_db = Imagify_Folders_DB::get_instance();
1227 $folders_table = $folders_db->get_table_name();
1228 $folders_key = $folders_db->get_primary_key();
1229 $folders_key_esc = esc_sql( $folders_key );
1230 $files_table = Imagify_Files_DB::get_instance()->get_table_name();
1231
1232 $folder_ids = array_filter( (array) $folder_ids );
1233
1234 if ( $folder_ids ) {
1235 $folder_ids = $folders_db->cast_col( $folder_ids, $folders_key );
1236 $folder_ids = Imagify_DB::prepare_values_list( $folder_ids );
1237 $in_clause = "folders.$folders_key_esc IN ( $folder_ids )";
1238 } else {
1239 $in_clause = '1=1';
1240 }
1241
1242 // Within the range of given folder IDs, filter the ones that are inactive and have no files.
1243 $results = $wpdb->get_col( // WPCS: unprepared SQL ok.
1244 "
1245 SELECT folders.$folders_key_esc FROM $folders_table AS folders
1246 LEFT JOIN $files_table AS files ON folders.$folders_key_esc = files.folder_id
1247 WHERE $in_clause
1248 AND folders.active != 1
1249 AND files.folder_id IS NULL"
1250 );
1251
1252 if ( ! $results ) {
1253 return 0;
1254 }
1255
1256 $results = $folders_db->cast_col( $results, $folders_key );
1257 $results = Imagify_DB::prepare_values_list( $results );
1258
1259 // Remove inactive folders with no files.
1260 $wpdb->query( "DELETE FROM $folders_table WHERE $folders_key_esc IN ( $results )" ); // WPCS: unprepared SQL ok.
1261
1262 return (int) $wpdb->rows_affected;
1263 }
1264
1265
1266 /** ----------------------------------------------------------------------------------------- */
1267 /** TOOLS =================================================================================== */
1268 /** ----------------------------------------------------------------------------------------- */
1269
1270 /**
1271 * Sort folders by full path.
1272 * The row "full_path" is added to each folder.
1273 *
1274 * @since 1.7
1275 * @access public
1276 * @author Grégory Viguier
1277 *
1278 * @param array $folders An array of folders with at least a "path" row.
1279 * @param bool $reverse Reverse the order.
1280 * @return array
1281 */
1282 public static function sort_folders( $folders, $reverse = false ) {
1283 if ( ! $folders ) {
1284 return [];
1285 }
1286
1287 $keyed_folders = [];
1288 $keyed_paths = [];
1289
1290 foreach ( $folders as $folder ) {
1291 $folder = (array) $folder;
1292 $folder['full_path'] = Imagify_Files_Scan::remove_placeholder( $folder['path'] );
1293
1294 $keyed_folders[ $folder['path'] ] = $folder;
1295 $keyed_paths[ $folder['path'] ] = $folder['full_path'];
1296 }
1297
1298 natcasesort( $keyed_paths );
1299
1300 if ( $reverse ) {
1301 $keyed_paths = array_reverse( $keyed_paths, true );
1302 }
1303
1304 $keyed_folders = array_merge( $keyed_paths, $keyed_folders );
1305
1306 return array_values( $keyed_folders );
1307 }
1308
1309 /**
1310 * Remove sub-paths: if 'a/b/' and 'a/b/c/' are in the array, we keep only the "parent" 'a/b/'.
1311 *
1312 * @since 1.7
1313 * @access public
1314 * @author Grégory Viguier
1315 *
1316 * @param array $placeholders A list of "placeholdered" paths.
1317 * @return array
1318 */
1319 public static function remove_sub_paths( $placeholders ) {
1320 sort( $placeholders );
1321
1322 foreach ( $placeholders as $i => $placeholder_path ) {
1323 if ( '{{ROOT}}/' === $placeholder_path ) {
1324 continue;
1325 }
1326
1327 if ( ! isset( $prev_path ) ) {
1328 $prev_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) );
1329 continue;
1330 }
1331
1332 $placeholder_path = strtolower( Imagify_Files_Scan::remove_placeholder( $placeholder_path ) );
1333
1334 if ( strpos( $placeholder_path, $prev_path ) === 0 ) {
1335 unset( $placeholders[ $i ] );
1336 } else {
1337 $prev_path = $placeholder_path;
1338 }
1339 }
1340
1341 return $placeholders;
1342 }
1343 }
1344