Файловый менеджер - Редактировать - /home/bean7936/perfect-community.com/442aa3/classes.tar
Назад
CLI/BulkOptimizeCommand.php 0000644 00000002453 15174671731 0011621 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CLI; use Imagify\Bulk\Bulk; /** * Command class for the bulk optimization */ class BulkOptimizeCommand extends AbstractCommand { /** * Executes the command. * * @param array $arguments Positional argument. * @param array $options Optional arguments. */ public function __invoke( $arguments, $options ) { $level = 2; if ( isset( $options['lossless'] ) ) { $level = 0; } foreach ( $arguments as $context ) { Bulk::get_instance()->run_optimize( $context, $level ); } \WP_CLI::log( 'Imagify bulk optimization triggered.' ); } /** * {@inheritdoc} */ protected function get_command_name(): string { return 'bulk-optimize'; } /** * {@inheritdoc} */ public function get_description(): string { return 'Run the bulk optimization'; } /** * {@inheritdoc} */ public function get_synopsis(): array { return [ [ 'type' => 'positional', 'name' => 'contexts', 'description' => 'The context(s) to run the bulk optimization for. Possible values are wp and custom-folders.', 'optional' => false, 'repeating' => true, ], [ 'type' => 'flag', 'name' => 'lossless', 'description' => 'Use lossless compression.', 'optional' => true, ], ]; } } CLI/GenerateMissingNextgenCommand.php 0000644 00000002200 15174671731 0013606 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CLI; use Imagify\Bulk\Bulk; /** * Command class for the missing Nextgen generation */ class GenerateMissingNextgenCommand extends AbstractCommand { /** * Executes the command. * * @param array $arguments Positional argument. * @param array $options Optional arguments. */ public function __invoke( $arguments, $options ) { Bulk::get_instance()->run_generate_nextgen( $arguments ); \WP_CLI::log( 'Imagify missing next-gen images generation triggered.' ); } /** * {@inheritdoc} */ protected function get_command_name(): string { return 'generate-missing-nextgen'; } /** * {@inheritdoc} */ public function get_description(): string { return 'Run the generation of the missing next-gen images versions'; } /** * {@inheritdoc} */ public function get_synopsis(): array { return [ [ 'type' => 'positional', 'name' => 'contexts', 'description' => 'The context(s) to run the missing next-gen images generation for. Possible values are wp and custom-folders.', 'optional' => false, 'repeating' => true, ], ]; } } CLI/AbstractCommand.php 0000644 00000000775 15174671731 0010753 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CLI; /** * Abstrat class for CLI Command */ abstract class AbstractCommand implements CommandInterface { /** * {@inheritdoc} */ final public function get_name(): string { return sprintf( 'imagify %s', $this->get_command_name() ); } /** * Get the "imagify" command name. * * @return string */ abstract protected function get_command_name(): string; /** * {@inheritdoc} */ public function get_synopsis(): array { return []; } } CLI/CommandInterface.php 0000644 00000001156 15174671731 0011102 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CLI; interface CommandInterface { /** * Get the command name. * * @return string */ public function get_name(): string; /** * Executes the command. * * @param array $arguments Positional argument. * @param array $options Optional arguments. */ public function __invoke( $arguments, $options ); /** * Get the positional and associative arguments a command accepts. * * @return array */ public function get_synopsis(): array; /** * Get the command description. * * @return string */ public function get_description(): string; } Bulk/BulkInterface.php 0000644 00000003241 15174671731 0010704 0 ustar 00 <?php namespace Imagify\Bulk; /** * Interface to use for bulk. * * @since 1.9 */ interface BulkInterface { /** * Get all unoptimized media ids. * * @since 1.9 * * @param int $optimization_level The optimization level. * @return array A list of unoptimized media. Array keys are media IDs prefixed with an underscore character, array values are the main file’s URL. */ public function get_unoptimized_media_ids( $optimization_level ); /** * Get ids of all optimized media without Next gen versions. * * @since 2.2 * * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. * @type array $errors { * @type array $no_file_path A list of media IDs. * @type array $no_backup A list of media IDs. * } * } */ public function get_optimized_media_ids_without_format( $format ); /** * Tell if there are optimized media without next-gen versions. * * @since 2.2 * * @return int The number of media. */ public function has_optimized_media_without_nextgen(); /** * Get the context data. * * @since 1.9 * * @return array { * The formated data. * The array keys corresponds to the table cell classes: "imagify-cell-{key}". * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ public function get_context_data(); } Bulk/Bulk.php 0000644 00000042134 15174671731 0007067 0 ustar 00 <?php namespace Imagify\Bulk; use Exception; use Imagify\Traits\InstanceGetterTrait; use Imagify\Optimization\Process\ProcessInterface; use WP_Error; /** * Bulk optimization */ final class Bulk { use InstanceGetterTrait; /** * Class init: launch hooks. * * @since 2.1 */ public function init() { add_action( 'imagify_optimize_media', [ $this, 'optimize_media' ], 10, 3 ); add_action( 'imagify_convert_next_gen', [ $this, 'generate_nextgen_versions' ], 10, 2 ); // @phpstan-ignore-line add_action( 'wp_ajax_imagify_bulk_optimize', [ $this, 'bulk_optimize_callback' ] ); add_action( 'wp_ajax_imagify_missing_nextgen_generation', [ $this, 'missing_nextgen_callback' ] ); add_action( 'wp_ajax_imagify_get_folder_type_data', [ $this, 'get_folder_type_data_callback' ] ); add_action( 'wp_ajax_imagify_bulk_info_seen', [ $this, 'bulk_info_seen_callback' ] ); add_action( 'wp_ajax_imagify_bulk_get_stats', [ $this, 'bulk_get_stats_callback' ] ); add_action( 'imagify_after_optimize', [ $this, 'check_optimization_status' ], 10, 2 ); add_action( 'imagify_deactivation', [ $this, 'delete_transients_data' ] ); add_action( 'update_option_imagify_settings', [ $this, 'maybe_generate_missing_nextgen' ], 10, 2 ); } /** * Delete transients data on deactivation * * @return void */ public function delete_transients_data() { delete_transient( 'imagify_custom-folders_optimize_running' ); delete_transient( 'imagify_wp_optimize_running' ); delete_transient( 'imagify_bulk_optimization_complete' ); delete_transient( 'imagify_missing_next_gen_total' ); } /** * Checks bulk optimization status after each optimization task * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. * * @return void */ public function check_optimization_status( $process, $item ) { $custom_folders = get_transient( 'imagify_custom-folders_optimize_running' ); $library_wp = get_transient( 'imagify_wp_optimize_running' ); if ( ! $custom_folders && ! $library_wp ) { return; } $data = $process->get_data(); if ( ! $data ) { return; } $progress = get_transient( 'imagify_bulk_optimization_result' ); if ( $data->is_optimized() ) { $size_data = $data->get_size_data(); if ( false === $progress ) { $progress = [ 'total' => 0, 'original_size' => 0, 'optimized_size' => 0, ]; } ++$progress['total']; $progress['original_size'] += $size_data['original_size']; $progress['optimized_size'] += $size_data['optimized_size']; set_transient( 'imagify_bulk_optimization_result', $progress, DAY_IN_SECONDS ); } $remaining = 0; if ( false !== $custom_folders ) { if ( false !== strpos( $item['process_class'], 'CustomFolders' ) ) { --$custom_folders['remaining']; set_transient( 'imagify_custom-folders_optimize_running', $custom_folders, DAY_IN_SECONDS ); $remaining += $custom_folders['remaining']; } } if ( false !== $library_wp ) { if ( false !== strpos( $item['process_class'], 'WP' ) ) { --$library_wp['remaining']; set_transient( 'imagify_wp_optimize_running', $library_wp, DAY_IN_SECONDS ); $remaining += $library_wp['remaining']; } } if ( 0 >= $remaining ) { delete_transient( 'imagify_custom-folders_optimize_running' ); delete_transient( 'imagify_wp_optimize_running' ); set_transient( 'imagify_bulk_optimization_complete', 1, DAY_IN_SECONDS ); } } /** * Decrease optimization running counter for the given context * * @param string $context Context to update. * * @return void */ private function decrease_counter( string $context ) { $counter = get_transient( "imagify_{$context}_optimize_running" ); if ( false === $counter ) { return; } $counter['total'] = $counter['total'] - 1; $counter['remaining'] = $counter['remaining'] - 1; if ( 0 === $counter['total'] && 0 >= $counter['remaining'] ) { delete_transient( "imagify_{$context}_optimize_running" ); } set_transient( "imagify_{$context}_optimize_running", $counter, DAY_IN_SECONDS ); } /** * Process a media with the requested imagify bulk action. * * @since 2.1 * * @param int $media_id Media ID. * @param string $context Current context. * @param int $optimization_level Optimization level. */ public function optimize_media( int $media_id, string $context, int $optimization_level ) { if ( ! $media_id || ! $context ) { $this->decrease_counter( $context ); return; } $this->force_optimize( $media_id, $context, $optimization_level ); } /** * Runs the bulk optimization * * @param string $context Current context (WP/Custom folders). * @param int $optimization_level Optimization level. * * @return array */ public function run_optimize( string $context, int $optimization_level ) { if ( ! $this->can_optimize() ) { return [ 'success' => false, 'message' => 'over-quota', ]; } $media_ids = $this->get_bulk_instance( $context )->get_unoptimized_media_ids( $optimization_level ); if ( empty( $media_ids ) ) { return [ 'success' => false, 'message' => 'no-images', ]; } foreach ( $media_ids as $media_id ) { try { as_enqueue_async_action( 'imagify_optimize_media', [ 'id' => $media_id, 'context' => $context, 'level' => $optimization_level, ], "imagify-{$context}-optimize-media" ); } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // nothing to do. } } $data = [ 'total' => count( $media_ids ), 'remaining' => count( $media_ids ), ]; set_transient( "imagify_{$context}_optimize_running", $data, DAY_IN_SECONDS ); return [ 'success' => true, 'message' => 'success', ]; } /** * Runs the next-gen generation * * @param array $contexts An array of contexts (WP/Custom folders). * @param array $formats An array of format to generate. * * @return array */ public function run_generate_nextgen( array $contexts, array $formats ) { if ( ! $this->can_optimize() ) { return [ 'success' => false, 'message' => 'over-quota', ]; } delete_transient( 'imagify_stat_without_next_gen' ); $medias = []; foreach ( $contexts as $context ) { foreach ( $formats as $format ) { $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format ); if ( ! $media['ids'] && $media['errors']['no_backup'] ) { // No backup, no next-gen. return [ 'success' => false, 'message' => 'no-backup', ]; } elseif ( ! $media['ids'] && $media['errors']['no_file_path'] ) { // Error. return [ 'success' => false, 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ), ]; } $medias[ $context ] = $media['ids']; } } if ( empty( $medias ) ) { return [ 'success' => false, 'message' => 'no-images', ]; } $total = 0; foreach ( $medias as $context => $media_ids ) { $total += count( $media_ids ); foreach ( $media_ids as $media_id ) { try { as_enqueue_async_action( 'imagify_convert_next_gen', [ 'id' => $media_id, 'context' => $context, ], "imagify-{$context}-convert-nextgen" ); } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // nothing to do. } } } set_transient( 'imagify_missing_next_gen_total', $total, HOUR_IN_SECONDS ); return [ 'success' => true, 'message' => $total, ]; } /** * Get the Bulk class name depending on a context. * * @since 2.1 * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * @return string The Bulk class name. */ private function get_bulk_class_name( string $context ): string { switch ( $context ) { case 'wp': $class_name = WP::class; break; case 'custom-folders': $class_name = CustomFolders::class; break; default: $class_name = Noop::class; } /** * Filter the name of the class to use for bulk process. * * @since 1.9 * * @param string $class_name The class name. * @param string $context The context name. */ $class_name = wpm_apply_filters_typed( 'string', 'imagify_bulk_class_name', $class_name, $context ); return '\\' . ltrim( $class_name, '\\' ); } /** * Get the Bulk instance depending on a context. * * @since 2.1 * * @param string $context The context name. Default values are 'wp' and 'custom-folders'. * * @return BulkInterface The optimization process instance. */ public function get_bulk_instance( string $context ): BulkInterface { $class_name = $this->get_bulk_class_name( $context ); return new $class_name(); } /** * Optimize all files from a media, whatever this media’s previous optimization status (will be restored if needed). * This is used by the bulk optimization page. * * @since 1.9 * * @param int $media_id The media ID. * @param string $context The context. * @param int $level The optimization level. * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ private function force_optimize( int $media_id, string $context, int $level ) { if ( ! $this->can_optimize() ) { $this->decrease_counter( $context ); return false; } $process = imagify_get_optimization_process( $media_id, $context ); $data = $process->get_data(); // Restore before re-optimizing. if ( $data->is_optimized() ) { $result = $process->restore(); if ( is_wp_error( $result ) ) { $this->decrease_counter( $context ); // Return an error message. return $result; } } return $process->optimize( $level ); } /** * Generate next-gen images if they are missing. * * @since 2.1 * * @param int $media_id Media ID. * @param string $context Current context. * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function generate_nextgen_versions( int $media_id, string $context ) { if ( ! $this->can_optimize() ) { return false; } return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions(); } /** * Check if the user has a valid account and has quota. Die on failure. * * @since 2.1 */ public function can_optimize() { if ( ! \Imagify_Requirements::is_api_key_valid() ) { return false; } if ( \Imagify_Requirements::is_over_quota() ) { return false; } return true; } /** * Get the submitted context. * * @since 1.9 * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * * @return string */ public function get_context( $method = 'GET', $parameter = 'context' ) { if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) { // No context. return 'noop'; } $context = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended return imagify_sanitize_context( $context ); } /** * Get the submitted optimization level. * * @since 1.7 * @since 1.9 Added $method and $parameter parameters. * @author Grégory Viguier * * @param string $method The method used: 'GET' (default), or 'POST'. * @param string $parameter The name of the parameter to look for. * @return int */ public function get_optimization_level( $method = 'GET', $parameter = 'optimization_level' ) { $method = 'POST' === $method ? INPUT_POST : INPUT_GET; $level = filter_input( $method, $parameter ); if ( ! is_numeric( $level ) || $level < 0 || $level > 2 ) { if ( get_imagify_option( 'lossless' ) ) { return 0; } return get_imagify_option( 'optimization_level' ); } return (int) $level; } /** * Launch the bulk optimization action * * @return void */ public function bulk_optimize_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); $context = $this->get_context(); $level = $this->get_optimization_level(); if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { imagify_die(); } $data = $this->run_optimize( $context, $level ); if ( false === $data['success'] ) { wp_send_json_error( [ 'message' => $data['message'] ] ); } wp_send_json_success( [ 'total' => $data['message'] ] ); } /** * Launch the missing Next-gen versions generation * * @return void */ public function missing_nextgen_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); $contexts = $this->get_contexts(); foreach ( $contexts as $context ) { if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { imagify_die(); } } $formats = imagify_nextgen_images_formats(); $data = $this->run_generate_nextgen( $contexts, $formats ); if ( false === $data['success'] ) { wp_send_json_error( [ 'message' => $data['message'] ] ); } wp_send_json_success( [ 'total' => $data['message'] ] ); } /** * Get stats data for a specific folder type. * * @since 1.7 */ public function get_folder_type_data_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); $context = $this->get_context(); if ( ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { imagify_die(); } $bulk = $this->get_bulk_instance( $context ); wp_send_json_success( $bulk->get_context_data() ); } /** * Set the "bulk info" popup state as "seen". * * @since 1.7 */ public function bulk_info_seen_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); $context = $this->get_context(); if ( ! $context ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { imagify_die(); } set_transient( 'imagify_bulk_optimization_infos', 1, WEEK_IN_SECONDS ); wp_send_json_success(); } /** * Get generic stats to display in the bulk page. * * @since 1.7.1 */ public function bulk_get_stats_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); $folder_types = filter_input( INPUT_GET, 'types', FILTER_REQUIRE_ARRAY ); $folder_types = is_array( $folder_types ) ? $folder_types : []; if ( ! $folder_types ) { imagify_die( __( 'Invalid request', 'imagify' ) ); } foreach ( $folder_types as $folder_type_data ) { $context = ! empty( $folder_type_data['context'] ) ? $folder_type_data['context'] : 'noop'; if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { imagify_die(); } } wp_send_json_success( imagify_get_bulk_stats( array_flip( $folder_types ) ) ); } /** * Update Options callback to start bulk optimization. * * @since 2.2 * * @param array $old_value The old option value. * @param array $value The new option value. * * @return void */ public function maybe_generate_missing_nextgen( $old_value, $value ) { if ( ! isset( $old_value['optimization_format'], $value['optimization_format'] ) ) { return; } if ( $old_value['optimization_format'] === $value['optimization_format'] ) { // Old value = new value so do nothing. return; } if ( 'off' === $value['optimization_format'] ) { // No need to generate next-gen images. return; } $contexts = $this->get_contexts(); $formats = imagify_nextgen_images_formats(); $this->run_generate_nextgen( $contexts, $formats ); } /** * Get the context for the bulk optimization page. * * @since 2.2 * * @return array The array of unique contexts ('wp' or 'custom-folders'). */ public function get_contexts() { $contexts = []; $types = []; // Library: in each site. if ( ! is_network_admin() ) { $types['library|wp'] = 1; } // Custom folders: in network admin only if network activated, in each site otherwise. if ( imagify_can_optimize_custom_folders() && ( ( imagify_is_active_for_network() && is_network_admin() ) || ! imagify_is_active_for_network() ) ) { $types['custom-folders|custom-folders'] = 1; } /** * Filter the types to display in the bulk optimization page. * * @since 1.7.1 * * @param array $types The folder types displayed on the page. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys. */ $types = wpm_apply_filters_typed( 'array', 'imagify_bulk_page_types', $types ); $types = array_filter( (array) $types ); if ( isset( $types['library|wp'] ) ) { $contexts[] = 'wp'; } if ( isset( $types['custom-folders|custom-folders'] ) ) { $folders_instance = \Imagify_Folders_DB::get_instance(); if ( ! $folders_instance->has_items() ) { if ( ! in_array( 'wp', $contexts, true ) ) { $contexts[] = 'wp'; } } elseif ( $folders_instance->has_active_folders() ) { $contexts[] = 'custom-folders'; } } return $contexts; } } Bulk/Noop.php 0000644 00000003422 15174671731 0007102 0 ustar 00 <?php namespace Imagify\Bulk; /** * Falback class for bulk. * * @since 1.9 */ class Noop extends AbstractBulk { /** * Get all unoptimized media ids. * * @since 1.9 * * @param int $optimization_level The optimization level. * @return array A list of unoptimized media. Array keys are media IDs prefixed with an underscore character, array values are the main file’s URL. */ public function get_unoptimized_media_ids( $optimization_level ) { return []; } /** * * Get ids of all optimized media without Next gen versions. * * @since 2.2 * * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. * @type array $errors { * @type array $no_file_path A list of media IDs. * @type array $no_backup A list of media IDs. * } * } */ public function get_optimized_media_ids_without_format( $format ) { return [ 'ids' => [], 'errors' => [ 'no_file_path' => [], 'no_backup' => [], ], ]; } /** * Get the context data. * * @since 1.9 * * @return array { * The formated data. * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ public function get_context_data() { $data = [ 'count-optimized' => 0, 'count-errors' => 0, 'optimized-size' => 0, 'original-size' => 0, 'errors_url' => get_imagify_admin_url( 'folder-errors', 'noop' ), ]; return $this->format_context_data( $data ); } } Bulk/CustomFolders.php 0000644 00000011675 15174671731 0010771 0 ustar 00 <?php namespace Imagify\Bulk; use Imagify_Custom_Folders; use Imagify_Files_Scan; use Imagify_Files_DB; use Imagify_Folders_DB; use Imagify_DB; use Imagify_Files_Stats; /** * Class to use for bulk for custom folders. * * @since 1.9 */ class CustomFolders extends AbstractBulk { /** * Context "short name". * * @var string * @since 1.9 */ protected $context = 'custom-folders'; /** * Get all unoptimized media ids. * * @since 1.9 * * @param int $optimization_level The optimization level. * * @return array A list of unoptimized media IDs. */ public function get_unoptimized_media_ids( $optimization_level ) { $this->set_no_time_limit(); /** * Get the folders from DB. */ $folders = Imagify_Custom_Folders::get_folders( [ 'active' => true, ] ); if ( ! $folders ) { return []; } /** * Fires before getting file IDs. * * @since 1.7 * * @param array $folders An array of folders data. * @param int $optimization_level The optimization level that will be used for the optimization. */ do_action( 'imagify_bulk_optimize_files_before_get_files', $folders, $optimization_level ); /** * Get the files from DB, and from the folders. */ $files = Imagify_Custom_Folders::get_files_from_folders( $folders, [ 'optimization_level' => $optimization_level, ] ); if ( ! $files ) { return []; } foreach ( $files as $k => $file ) { $files[ $k ] = $file['file_id']; } return $files; } /** * Get ids of all optimized media without Next gen versions. * * @since 2.2 * * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. * @type array $errors { * @type array $no_file_path A list of media IDs. * @type array $no_backup A list of media IDs. * } * } */ public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); $files_table = Imagify_Files_DB::get_instance()->get_table_name(); $folders_table = Imagify_Folders_DB::get_instance()->get_table_name(); $mime_types = Imagify_DB::get_mime_types( 'image' ); // Remove single quotes and explode string into array. $mime_types_array = explode( ',', str_replace( "'", '', $mime_types ) ); // Iterate over array and check if string contains input. foreach ( $mime_types_array as $item ) { if ( strpos( $item, $format ) !== false ) { $mime = $item; break; } } if ( ! isset( $mime ) && empty( $mime ) ) { $mime = 'image/webp'; } $mime_types = str_replace( ",'" . $mime . "'", '', $mime_types ); $nextgen_suffix = constant( imagify_get_optimization_process_class_name( 'custom-folders' ) . '::' . strtoupper( $format ) . '_SUFFIX' ); $files = $wpdb->get_results( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT fi.file_id, fi.path FROM $files_table as fi INNER JOIN $folders_table AS fo ON ( fi.folder_id = fo.folder_id ) WHERE fi.mime_type IN ( $mime_types ) AND ( fi.status = 'success' OR fi.status = 'already_optimized' ) AND ( fi.data NOT LIKE %s OR fi.data IS NULL ) ORDER BY fi.file_id DESC", '%' . $wpdb->esc_like( $nextgen_suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); $wpdb->flush(); unset( $mime_types, $files_table, $folders_table, $nextgen_suffix, $mime ); $data = [ 'ids' => [], 'errors' => [ 'no_file_path' => [], 'no_backup' => [], ], ]; if ( ! $files ) { return $data; } foreach ( $files as $file ) { $file_id = absint( $file->file_id ); if ( empty( $file->path ) ) { // Problem. $data['errors']['no_file_path'][] = $file_id; continue; } $file_path = Imagify_Files_Scan::remove_placeholder( $file->path ); $backup_path = Imagify_Custom_Folders::get_file_backup_path( $file_path ); if ( ! $this->filesystem->exists( $backup_path ) ) { // No backup, no WebP. $data['errors']['no_backup'][] = $file_id; continue; } $data['ids'][] = $file_id; } return $data; } /** * Get the context data. * * @since 1.9 * * @return array { * The formated data. * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ public function get_context_data() { $data = [ 'count-optimized' => Imagify_Files_Stats::count_optimized_files(), 'count-errors' => Imagify_Files_Stats::count_error_files(), 'optimized-size' => Imagify_Files_Stats::get_optimized_size(), 'original-size' => Imagify_Files_Stats::get_original_size(), 'errors_url' => get_imagify_admin_url( 'folder-errors', $this->context ), ]; return $this->format_context_data( $data ); } } Bulk/WP.php 0000644 00000022367 15174671731 0006526 0 ustar 00 <?php namespace Imagify\Bulk; use Imagify_DB; /** * Class to use for bulk for WP attachments. * * @since 1.9 */ class WP extends AbstractBulk { /** * Context "short name". * * @var string * @since 1.9 */ protected $context = 'wp'; /** * Get all unoptimized media ids. * * @since 1.9 * * @param int $optimization_level The optimization level. * * @return array A list of unoptimized media IDs. */ public function get_unoptimized_media_ids( $optimization_level ) { global $wpdb; $this->set_no_time_limit(); $mime_types = Imagify_DB::get_mime_types(); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause( [ 'prepared' => true, ] ); $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT DISTINCT p.ID FROM $wpdb->posts AS p $nodata_join LEFT JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) LEFT JOIN $wpdb->postmeta AS mt2 ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_optimization_level' ) WHERE p.post_mime_type IN ( $mime_types ) AND ( mt1.meta_value = 'error' OR mt2.meta_value != %d OR mt2.post_id IS NULL ) AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $nodata_where ORDER BY CASE mt1.meta_value WHEN 'already_optimized' THEN 2 ELSE 1 END ASC, p.ID DESC LIMIT 0, %d", $optimization_level, imagify_get_unoptimized_attachment_limit() ) ); $wpdb->flush(); unset( $mime_types ); $ids = array_filter( array_map( 'absint', $ids ) ); if ( ! $ids ) { return []; } $metas = Imagify_DB::get_metas( [ // Get attachments filename. 'filenames' => '_wp_attached_file', // Get attachments data. 'data' => '_imagify_data', // Get attachments optimization level. 'optimization_levels' => '_imagify_optimization_level', // Get attachments status. 'statuses' => '_imagify_status', ], $ids ); // First run. foreach ( $ids as $i => $id ) { $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false; $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false; $attachment_error = ''; if ( isset( $metas['data'][ $id ]['sizes']['full']['error'] ) ) { $attachment_error = $metas['data'][ $id ]['sizes']['full']['error']; } // Don't try to re-optimize if the optimization level is still the same. if ( $optimization_level === $attachment_optimization_level && is_string( $attachment_error ) ) { unset( $ids[ $i ] ); continue; } // Don't try to re-optimize images already compressed. if ( 'already_optimized' === $attachment_status && $attachment_optimization_level >= $optimization_level ) { unset( $ids[ $i ] ); continue; } $attachment_error = trim( $attachment_error ); // Don't try to re-optimize images with an empty error message. if ( 'error' === $attachment_status && empty( $attachment_error ) ) { unset( $ids[ $i ] ); } } if ( ! $ids ) { return []; } $ids = array_values( $ids ); /** * Fires before testing for file existence. * * @since 1.6.7 * * @param array $ids An array of attachment IDs. * @param array $metas An array of the data fetched from the database. * @param int $optimization_level The optimization level that will be used for the optimization. */ do_action( 'imagify_bulk_optimize_before_file_existence_tests', $ids, $metas, $optimization_level ); $data = []; foreach ( $ids as $i => $id ) { if ( empty( $metas['filenames'][ $id ] ) ) { // Problem. continue; } $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] ); if ( ! $file_path || ! $this->filesystem->exists( $file_path ) ) { continue; } $attachment_backup_path = get_imagify_attachment_backup_path( $file_path ); $attachment_status = isset( $metas['statuses'][ $id ] ) ? $metas['statuses'][ $id ] : false; $attachment_optimization_level = isset( $metas['optimization_levels'][ $id ] ) ? $metas['optimization_levels'][ $id ] : false; // Don't try to re-optimize if there is no backup file. if ( 'success' === $attachment_status && $optimization_level !== $attachment_optimization_level && ! $this->filesystem->exists( $attachment_backup_path ) ) { continue; } $data[] = $id; } return $data; } /** * Get ids of all optimized media without Next gen versions. * * @since 2.2 * * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. * @type array $errors { * @type array $no_file_path A list of media IDs. * @type array $no_backup A list of media IDs. * } * } */ public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); $mime_types = Imagify_DB::get_mime_types( 'image' ); // Remove single quotes and explode string into array. $mime_types_array = explode( ',', str_replace( "'", '', $mime_types ) ); // Iterate over array and check if string contains input. foreach ( $mime_types_array as $item ) { if ( strpos( $item, $format ) !== false ) { $mime = $item; break; } } if ( ! isset( $mime ) && empty( $mime ) ) { $mime = 'image/webp'; } $mime_types = str_replace( ",'" . $mime . "'", '', $mime_types ); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = ''; $nodata_where = ''; if ( ! imagify_has_attachments_without_required_metadata() ) { $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause( [ 'prepared' => true, ] ); } $nextgen_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::' . strtoupper( $format ) . '_SUFFIX' ); $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT p.ID FROM $wpdb->posts AS p $nodata_join LEFT JOIN $wpdb->postmeta AS mt1 ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) LEFT JOIN $wpdb->postmeta AS mt2 ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' ) WHERE p.post_mime_type IN ( $mime_types ) AND (mt1.meta_key IS NULL OR mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' ) AND mt2.meta_value NOT LIKE %s AND p.post_type = 'attachment' AND p.post_status IN ( $statuses ) $nodata_where ORDER BY p.ID DESC LIMIT 0, %d", '%' . $wpdb->esc_like( $nextgen_suffix . '";a:4:{s:7:"success";b:1;' ) . '%', imagify_get_unoptimized_attachment_limit() ) ); $wpdb->flush(); unset( $mime_types, $statuses, $nextgen_suffix, $mime ); $ids = array_filter( array_map( 'absint', $ids ) ); $data = [ 'ids' => [], 'errors' => [ 'no_file_path' => [], 'no_backup' => [], ], ]; if ( ! $ids ) { return $data; } $metas = Imagify_DB::get_metas( [ // Get attachments filename. 'filenames' => '_wp_attached_file', ], $ids ); /** * Fires before testing for file existence. * * @since 1.9 * * @param array $ids An array of attachment IDs. * @param array $metas An array of the data fetched from the database. * @param string $context The context. */ do_action( 'imagify_bulk_generate_nextgen_before_file_existence_tests', $ids, $metas, 'wp' ); foreach ( $ids as $i => $id ) { if ( empty( $metas['filenames'][ $id ] ) ) { // Problem. Should not happen, thanks to the wpdb query. $data['errors']['no_file_path'][] = $id; continue; } $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] ); if ( ! $file_path ) { // Main file not found. $data['errors']['no_file_path'][] = $id; continue; } $backup_path = get_imagify_attachment_backup_path( $file_path ); if ( ! $this->filesystem->exists( $backup_path ) ) { // No backup, no WebP. $data['errors']['no_backup'][] = $id; continue; } $data['ids'][] = $id; } return $data; } /** * Get the context data. * * @since 1.9 * * @return array { * The formated data. * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ public function get_context_data() { $total_saving_data = imagify_count_saving_data(); $data = [ 'count-optimized' => imagify_count_optimized_attachments(), 'count-errors' => imagify_count_error_attachments(), 'optimized-size' => $total_saving_data['optimized_size'], 'original-size' => $total_saving_data['original_size'], 'errors_url' => get_imagify_admin_url( 'folder-errors', $this->context ), ]; return $this->format_context_data( $data ); } } Bulk/AbstractBulk.php 0000644 00000007211 15174671731 0010550 0 ustar 00 <?php namespace Imagify\Bulk; use Imagify_Filesystem; /** * Abstract class to use for bulk. * * @since 1.9 */ abstract class AbstractBulk implements BulkInterface { /** * Filesystem object. * * @var Imagify_Filesystem * @since 1.9 */ protected $filesystem; /** * The constructor. * * @since 1.9 */ public function __construct() { $this->filesystem = Imagify_Filesystem::get_instance(); } /** * Format context data (stats). * * @since 1.9 * * @param array $data { * The data to format. * * @type int $count-optimized Number of media optimized. * @type int $count-errors Number of media having an optimization error. * @type int $optimized-size Optimized filesize. * @type int $original-size Original filesize. * @type string $errors_url URL to the page listing the optimization errors. * } * @return array { * The formated data. * * @type string $count-optimized Number of media optimized. * @type string $count-errors Number of media having an optimization error, with a link to the page listing the optimization errors. * @type string $optimized-size Optimized filesize. * @type string $original-size Original filesize. * } */ protected function format_context_data( $data ) { $defaults = [ 'count-optimized' => '', 'count-errors' => '', 'optimized-size' => '', 'original-size' => '', ]; $data = wp_parse_args( $data, $defaults ); $data = array_map( function ( $item ) { return empty( $item ) ? '' : $item; }, $data ); if ( ! empty( $data['count-optimized'] ) ) { // translators: %s is a formatted number, dont use %d. $data['count-optimized'] = sprintf( _n( '%s Media File Optimized', '%s Media Files Optimized', $data['count-optimized'], 'imagify' ), '<span>' . number_format_i18n( $data['count-optimized'] ) . '</span>' ); } if ( ! empty( $data['count-errors'] ) ) { /* translators: %s is a formatted number, dont use %d. */ $data['count-errors'] = sprintf( _n( '%s Error', '%s Errors', $data['count-errors'], 'imagify' ), '<span>' . number_format_i18n( $data['count-errors'] ) . '</span>' ); $data['count-errors'] .= ' <a href="' . esc_url( $data['errors_url'] ) . '">' . __( 'View Errors', 'imagify' ) . '</a>'; } if ( ! empty( $data['optimized-size'] ) ) { $data['optimized-size'] = '<span class="imagify-cell-label">' . __( 'Optimized Filesize', 'imagify' ) . '</span> ' . imagify_size_format( $data['optimized-size'], 2 ); } if ( ! empty( $data['original-size'] ) ) { $data['original-size'] = '<span class="imagify-cell-label">' . __( 'Original Filesize', 'imagify' ) . '</span> ' . imagify_size_format( $data['original-size'], 2 ); } unset( $data['errors_url'] ); return $data; } /** * Attempts to set no limit to the PHP timeout for time intensive processes. * * @return void */ protected function set_no_time_limit() { if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved ) { @set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } /** * Tell if there are optimized media without next-gen versions. * * @since 1.9 * * @return int The number of media. */ public function has_optimized_media_without_nextgen() { $format = get_imagify_option( 'optimization_format' ); if ( 'off' === $format ) { return 0; } return count( $this->get_optimized_media_ids_without_format( $format )['ids'] ); } } DB/DBInterface.php 0000644 00000002672 15174671731 0007673 0 ustar 00 <?php namespace Imagify\DB; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Interface to interact with the database. * * @since 1.9 * @author Grégory Viguier */ interface DBInterface { /** * Get the main Instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return DBInterface Main instance. */ public static function get_instance(); /** * Retrieve a row by the primary key. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $row_id A primary key. * @return array */ public function get( $row_id ); /** * Update a row. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $row_id A primary key. * @param array $data New data. * @param string $where A column name. * @return bool */ public function update( $row_id, $data = [], $where = '' ); /** * Delete a row identified by the primary key. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $row_id A primary key. * @return bool */ public function delete( $row_id ); /** * Default column values. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_column_defaults(); /** * Get the primary column name. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_primary_key(); } Job/MediaOptimization.php 0000644 00000024735 15174671731 0011444 0 ustar 00 <?php namespace Imagify\Job; use Imagify\Optimization\Process\ProcessInterface; use Imagify\Traits\InstanceGetterTrait; use WP_Error; /** * Job class for media optimization. * * @since 1.9 */ final class MediaOptimization extends \Imagify_Abstract_Background_Process { use InstanceGetterTrait; /** * Background process: the action to perform. * * @var string * @since 1.9 */ protected $action = 'optimize_media'; /** * The optimization process instance. * * @var ?ProcessInterface * @since 1.9 */ protected $optimization_process; /** * Handle job logic. * * @since 1.9 * * @param array $item { * The data to use for this job. * * @type string $task The task to perform. Optional: set it only if you know what you’re doing. * @type int $id The media ID. * @type array $sizes An array of media sizes (strings). Use "full" for the size of the main file. * @type array $sizes_done Used internally to store the media sizes that have been processed. * @type int $optimization_level The optimization level. Null for the level set in the settings. * @type string $process_class The name of the process class. The class must implement ProcessInterface. * @type array $data { * Can be used to pass any data. Keep it short, don’t forget it will be stored in the database. * It should contain the following though: * * @type string $hook_suffix Suffix used to trigger hooks before and after optimization. Should be always provided. * @type bool $delete_backup True to delete the backup file after the optimization process. This is used when a temporary backup of the original file has been created, but backup option is disabled. Default is false. * } * } * @return array|bool The modified item to put back in the queue. False to remove the item from the queue. */ protected function task( $item ) { $item = $this->validate_item( $item ); if ( ! $item ) { // Not valid. return false; } // Launch the task. $method = 'task_' . $item['task']; $item = $this->$method( $item ); if ( $item['task'] ) { // Next task. return $item; } // End of the queue. $this->optimization_process->unlock(); return false; } /** * Trigger hooks before the optimization job. * * @since 1.9 * * @param array $item See $this->task(). * @return array The item. */ private function task_before( $item ) { if ( ! empty( $item['error'] ) && is_wp_error( $item['error'] ) ) { $wp_error = $item['error']; } else { $wp_error = new WP_Error(); } /** * Fires before optimizing a media. * Any number of files can be optimized, not necessarily all of the media files. * If you want to return a WP_Error, use the existing $wp_error object. * * @since 1.9 * * @param array|WP_Error $data New data to pass along the item. A WP_Error object to stop the process. * @param WP_Error $wp_error Add errors to this object and return it to stop the process. * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. See $this->task(). */ $data = apply_filters( 'imagify_before_optimize', [], $wp_error, $this->optimization_process, $item ); // @phpstan-ignore-line if ( is_wp_error( $data ) ) { $wp_error = $data; } elseif ( $data && is_array( $data ) ) { $item['data'] = array_merge( $data, $item['data'] ); } if ( $wp_error->get_error_codes() ) { // Don't optimize if there is an error. $item['task'] = 'after'; $item['error'] = $wp_error; return $item; } if ( empty( $item['data']['hook_suffix'] ) ) { // Next task. $item['task'] = 'optimize'; return $item; } $hook_suffix = $item['data']['hook_suffix']; /** * Fires before optimizing a media. * Any number of files can be optimized, not necessarily all of the media files. * If you want to return a WP_Error, use the existing $wp_error object. * * @since 1.9 * * @param array|WP_Error $data New data to pass along the item. A WP_Error object to stop the process. * @param WP_Error $wp_error Add errors to this object and return it to stop the process. * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. See $this->task(). */ $data = apply_filters( "imagify_before_{$hook_suffix}", [], $wp_error, $this->optimization_process, $item ); // @phpstan-ignore-line if ( is_wp_error( $data ) ) { $wp_error = $data; } elseif ( $data && is_array( $data ) ) { $item['data'] = array_merge( $data, $item['data'] ); } if ( $wp_error->get_error_codes() ) { // Don't optimize if there is an error. $item['task'] = 'after'; $item['error'] = $wp_error; return $item; } // Next task. $item['task'] = 'optimize'; return $item; } /** * Start the optimization job. * * @since 1.9 * * @param array $item See $this->task(). * @return array The item. */ private function task_optimize( $item ) { // Determine which size we're going to optimize. The 'full' size must be optimized before any other. if ( in_array( 'full', $item['sizes'], true ) ) { $current_size = 'full'; $item['sizes'] = array_diff( $item['sizes'], [ 'full' ] ); } else { $current_size = array_shift( $item['sizes'] ); } $item['sizes_done'][] = $current_size; // Optimize the file. $data = $this->optimization_process->optimize_size( $current_size, $item['optimization_level'] ); if ( 'full' === $current_size ) { if ( is_wp_error( $data ) ) { // Don't go further if there is an error. $item['sizes'] = []; $item['error'] = $data; } elseif ( 'already_optimized' === $data['status'] ) { // Status is "already_optimized", try to create next-gen versions only. $item['sizes'] = array_filter( $item['sizes'], [ $this->optimization_process, 'is_size_next_gen' ] ); } elseif ( 'success' !== $data['status'] ) { // Don't go further if the full size has not the "success" status. $item['sizes'] = []; } } if ( ! $item['sizes'] ) { // No more files to optimize. $item['task'] = 'after'; } // Optimize the next file or go to the next task. return $item; } /** * Trigger hooks after the optimization job. * * @since 1.9 * * @param array $item See $this->task(). * @return array The item. */ private function task_after( $item ) { if ( ! empty( $item['data']['delete_backup'] ) ) { $this->optimization_process->delete_backup(); } /** * Fires after optimizing a media. * Any number of files can be optimized, not necessarily all of the media files. * * @since 1.9 * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. See $this->task(). */ do_action( 'imagify_after_optimize', $this->optimization_process, $item ); if ( empty( $item['data']['hook_suffix'] ) ) { $item['task'] = false; return $item; } $hook_suffix = $item['data']['hook_suffix']; /** * Fires after optimizing a media. * Any number of files can be optimized, not necessarily all of the media files. * * @since 1.9 * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. See $this->task(). */ do_action( "imagify_after_{$hook_suffix}", $this->optimization_process, $item ); $item['task'] = false; return $item; } /** * Validate an item. * On success, the property $this->optimization_process is set. * * @since 1.9 * * @param array $item See $this->task(). * @return array|bool The item. False if invalid. */ protected function validate_item( $item ) { $this->optimization_process = null; $default = [ 'task' => '', 'id' => 0, 'sizes' => [], 'sizes_done' => [], 'optimization_level' => null, 'process_class' => '', 'data' => [], ]; $item = imagify_merge_intersect( $item, $default ); // Validate some types first. if ( ! is_array( $item['sizes'] ) ) { return false; } if ( isset( $item['error'] ) && ! is_wp_error( $item['error'] ) ) { unset( $item['error'] ); } if ( isset( $item['data']['hook_suffix'] ) && ! is_string( $item['data']['hook_suffix'] ) ) { unset( $item['data']['hook_suffix'] ); } $item['id'] = (int) $item['id']; $item['optimization_level'] = $this->sanitize_optimization_level( $item['optimization_level'] ); if ( ! $item['id'] || ! $item['process_class'] ) { return false; } // Process. $item['process_class'] = '\\' . ltrim( $item['process_class'], '\\' ); if ( ! class_exists( $item['process_class'] ) ) { return false; } $process = $this->get_process( $item ); if ( ! $process ) { return false; } $this->optimization_process = $process; // Validate the current task. if ( empty( $item['task'] ) ) { $item['task'] = 'before'; } if ( ! $item['task'] || ! method_exists( $this, 'task_' . $item['task'] ) ) { return false; } if ( ! $item['sizes'] && 'after' !== $item['task'] ) { // Allow to have no sizes, but only after the optimize task is complete. return false; } if ( ! isset( $item['sizes_done'] ) || ! is_array( $item['sizes_done'] ) ) { $item['sizes_done'] = []; } return $item; } /** * Get the process instance. * * @since 1.9 * * @param array $item See $this->task(). * @return ProcessInterface|bool The instance object on success. False on failure. */ protected function get_process( $item ) { $process_class = $item['process_class']; $process = new $process_class( $item['id'] ); if ( ! $process instanceof ProcessInterface || ! $process->is_valid() ) { return false; } return $process; } /** * Sanitize and validate an optimization level. * If not provided (false, null), fallback to the level set in the plugin's settings. * * @since 1.9 * * @param mixed $optimization_level The optimization level. * @return int */ protected function sanitize_optimization_level( $optimization_level ) { if ( ! is_numeric( $optimization_level ) ) { if ( get_imagify_option( 'lossless' ) ) { return 0; } return get_imagify_option( 'optimization_level' ); } return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level ); } } Picture/ServiceProvider.php 0000644 00000002041 15174671731 0012014 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Picture; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for Picture display */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ Display::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ Display::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( Display::class ) ->addArgument( 'filesystem' ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } Picture/Display.php 0000644 00000050576 15174671731 0010326 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Picture; use Imagify\EventManagement\SubscriberInterface; use Imagify_Filesystem; /** * Display Next-gen images on the site with <picture> tags. * * @since 1.9 */ class Display implements SubscriberInterface { /** * Option value. * * @var string */ const OPTION_VALUE = 'picture'; /** * Filesystem object. * * @var Imagify_Filesystem */ protected $filesystem; /** * Constructor. * * @param Imagify_Filesystem $filesystem Filesystem instance. */ public function __construct( Imagify_Filesystem $filesystem ) { $this->filesystem = $filesystem; } /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'template_redirect' => 'start_content_process', 'imagify_process_webp_content' => 'process_content', ]; } /** ----------------------------------------------------------------------------------------- */ /** ADD <PICTURE> TAGS TO THE PAGE ========================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Start buffering the page content. * * @since 1.9 * * @return void */ public function start_content_process() { if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } $allow = apply_filters_deprecated( 'imagify_allow_picture_tags_for_webp', [ true ], '2.2', 'imagify_allow_picture_tags_for_nextgen' ); /** * Prevent the replacement of <img> tags into <picture> tags. * * @since 1.9 * * @param bool $allow True to allow the use of <picture> tags (default). False to prevent their use. */ $allow = apply_filters( 'imagify_allow_picture_tags_for_nextgen', true ); if ( ! $allow ) { return; } ob_start( [ $this, 'maybe_process_buffer' ] ); } /** * Maybe process the page content. * * @since 1.9 * * @param string $buffer The buffer content. * * @return string */ public function maybe_process_buffer( $buffer ) { if ( ! $this->is_html( $buffer ) ) { return $buffer; } if ( strlen( $buffer ) <= 255 ) { // Buffer length must be > 255 (IE does not read pages under 255 c). return $buffer; } $buffer = $this->process_content( $buffer ); /** * Filter the page content after Imagify. * * @since 1.9 * * @param string $buffer The page content. */ $buffer = (string) apply_filters( 'imagify_buffer', $buffer ); return $buffer; } /** * Process the content. * * @since 1.9 * * @param string $content The content. * * @return string */ public function process_content( $content ) { $html_no_picture_tags = $this->remove_picture_tags( $content ); $images = $this->get_images( $html_no_picture_tags ); if ( ! $images ) { return $content; } foreach ( $images as $image ) { $tag = $this->build_picture_tag( $image ); $content = str_replace( $image['tag'], $tag, $content ); } return $content; } /** * Remove pre-existing <picture> tags. * * We shouldn't replace images already nested inside picture tags * that are already in the page. * * @since 1.10.0 * * @param string $html Content of the page. * * @return string HTML content without pre-existing <picture> tags. */ private function remove_picture_tags( $html ) { $replace = preg_replace( '#<picture[^>]*>.*?<\/picture\s*>#mis', '', $html ); if ( null === $replace ) { return $html; } return $replace; } /** ----------------------------------------------------------------------------------------- */ /** BUILD HTML TAGS AND ATTRIBUTES ========================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Build the <picture> tag to insert. * * @since 1.9 * @see $this->process_image() * * @param array $image An array of data. * * @return string A <picture> tag. */ protected function build_picture_tag( $image ) { $to_remove = [ 'alt' => '', 'height' => '', 'width' => '', 'data-lazy-src' => '', 'data-src' => '', 'src' => '', 'data-lazy-srcset' => '', 'data-srcset' => '', 'srcset' => '', 'data-lazy-sizes' => '', 'data-sizes' => '', 'sizes' => '', ]; $attributes = array_diff_key( $image['attributes'], $to_remove ); /** * Filter the attributes to be added to the <picture> tag. * * @since 1.9 * * @param array $attributes A list of attributes to be added to the <picture> tag. * @param array $data Data built from the originale <img> tag. See $this->process_image(). */ $attributes = apply_filters( 'imagify_picture_attributes', $attributes, $image ); /** * Remove Gutenberg specific attributes from picture tag, leave them on img tag. * Optional: $attributes['class'] = 'imagify-webp-cover-wrapper'; for website admin styling ease. */ if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) { unset( $attributes['style'] ); unset( $attributes['class'] ); unset( $attributes['data-object-fit'] ); unset( $attributes['data-object-position'] ); } $output = '<picture' . $this->build_attributes( $attributes ) . ">\n"; /** * Allow to add more <source> tags to the <picture> tag. * * @since 1.9 * * @param string $more_source_tags Additional <source> tags. * @param array $data Data built from the originale <img> tag. See $this->process_image(). */ $output .= apply_filters( 'imagify_additional_source_tags', '', $image ); $output .= $this->build_source_tag( $image ); $output .= $this->build_img_tag( $image ); $output .= "</picture>\n"; return $output; } /** * Build the <source> tag to insert in the <picture>. * * @since 1.9 * @see $this->process_image() * * @param array $image An array of data. * * @return string A <source> tag. */ protected function build_source_tag( $image ) { $source = ''; foreach ( [ 'avif', 'webp' ] as $image_type ) { $attributes = $this->build_source_attributes( $image, $image_type ); if ( empty( $attributes ) ) { continue; } $source .= '<source' . $this->build_attributes( $attributes ) . "/>\n"; } return $source; } /** * Build the attribute for the source tag. * * @param array $image An array of data. * @param string $image_type Type of image. * * @return array */ protected function build_source_attributes( array $image, string $image_type ): array { $mime_type = ''; $url = ''; switch ( $image_type ) { case 'webp': $mime_type = 'image/webp'; $url = 'webp_url'; break; case 'avif': $mime_type = 'image/avif'; $url = 'avif_url'; break; } $srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set'; $attributes = [ 'type' => $mime_type, $srcset_source => [], ]; if ( ! empty( $image['srcset'] ) ) { foreach ( $image['srcset'] as $srcset ) { if ( empty( $srcset[ $url ] ) ) { continue; } $attributes[ $srcset_source ][] = $srcset[ $url ] . ' ' . $srcset['descriptor']; } } if ( empty( $attributes[ $srcset_source ] ) && empty( $image['src'][ $url ] ) ) { return []; } if ( empty( $attributes[ $srcset_source ] ) ) { $attributes[ $srcset_source ][] = $image['src'][ $url ]; } $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] ); foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) { if ( ! empty( $image['attributes'][ $srcset_attr ] ) && $srcset_attr !== $srcset_source ) { $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ]; } } if ( 'srcset' !== $srcset_source && empty( $attributes['srcset'] ) && ! empty( $image['attributes']['src'] ) ) { // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ). $attributes['srcset'] = $image['attributes']['src']; } foreach ( [ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr ) { if ( ! empty( $image['attributes'][ $sizes_attr ] ) ) { $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ]; } } /** * Filter the attributes to be added to the <source> tag. * * @since 1.9 * * @param array $attributes A list of attributes to be added to the <source> tag. * @param array $data Data built from the original <img> tag. See $this->process_image(). */ $attributes = apply_filters( 'imagify_picture_source_attributes', $attributes, $image ); return $attributes; } /** * Build the <img> tag to insert in the <picture>. * * @since 1.9 * @see $this->process_image() * * @param array $image An array of data. * * @return string A <img> tag. */ protected function build_img_tag( $image ) { /** * Gutenberg fix. * Check for the 'wp-block-cover__image-background' class on the original image, and leave that class and style attributes if found. */ if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) { $to_remove = [ 'id' => '', 'title' => '', ]; $attributes = array_diff_key( $image['attributes'], $to_remove ); } else { $to_remove = [ 'class' => '', 'id' => '', 'style' => '', 'title' => '', ]; $attributes = array_diff_key( $image['attributes'], $to_remove ); } /** * Filter the attributes to be added to the <img> tag. * * @since 1.9 * * @param array $attributes A list of attributes to be added to the <img> tag. * @param array $data Data built from the originale <img> tag. See $this->process_image(). */ $attributes = apply_filters( 'imagify_picture_img_attributes', $attributes, $image ); return '<img' . $this->build_attributes( $attributes ) . "/>\n"; } /** * Create HTML attributes from an array. * * @since 1.9 * * @param array $attributes A list of attribute pairs. * * @return string HTML attributes. */ protected function build_attributes( $attributes ) { if ( ! $attributes || ! is_array( $attributes ) ) { return ''; } $out = ''; foreach ( $attributes as $attribute => $value ) { $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"'; } return $out; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get a list of images in a content. * * @since 1.9 * * @param string $content The content. * * @return array */ protected function get_images( $content ) { // Remove comments. $content = preg_replace( '/<!--(.*)-->/Uis', '', $content ); if ( ! preg_match_all( '/<img\s.*>/isU', $content, $matches ) ) { return []; } $images = array_map( [ $this, 'process_image' ], $matches[0] ); $images = array_filter( $images ); /** * Filter the images to display with a <picture> tag. * * @since 1.9 * @see $this->process_image() * * @param array $images A list of arrays. * @param string $content The page content. */ $images = apply_filters( 'imagify_webp_picture_images_to_display', $images, $content ); if ( ! $images || ! is_array( $images ) ) { return []; } foreach ( $images as $i => $image ) { if ( ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) && ( empty( $image['src']['avif_exists'] ) || empty( $image['src']['avif_url'] ) ) ) { unset( $images[ $i ] ); continue; } if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) { unset( $images[ $i ]['src']['webp_url'] ); } if ( empty( $image['src']['avif_exists'] ) || empty( $image['src']['avif_url'] ) ) { unset( $images[ $i ]['src']['avif_url'] ); } unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] ); unset( $images[ $i ]['src']['avif_path'], $images[ $i ]['src']['avif_exists'] ); if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) { unset( $images[ $i ]['srcset'] ); continue; } foreach ( $image['srcset'] as $j => $srcset ) { if ( ! is_array( $srcset ) ) { continue; } if ( ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) && ( empty( $srcset['avif_exists'] ) || empty( $srcset['avif_url'] ) ) ) { unset( $images[ $i ]['srcset'][ $j ]['webp_url'] ); unset( $images[ $i ]['srcset'][ $j ]['avif_url'] ); } if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) { unset( $images[ $i ]['srcset'][ $j ]['webp_url'] ); } if ( empty( $srcset['avif_exists'] ) || empty( $srcset['avif_url'] ) ) { unset( $images[ $i ]['srcset'][ $j ]['avif_url'] ); } unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] ); unset( $images[ $i ]['srcset'][ $j ]['avif_path'], $images[ $i ]['srcset'][ $j ]['avif_exists'] ); } } return $images; } /** * Process an image tag and get an array containing some data. * * @since 1.9 * * @param string $image An image html tag. * @return array|false { * An array of data if the image has a WebP version. False otherwise. * * @type string $tag The image tag. * @type array $attributes The image attributes (minus src and srcset). * @type array $src { * @type string $url URL to the original image. * @type string $webp_url URL to the WebP version. * } * @type array $srcset { * An array or arrays. Not set if not applicable. * * @type string $url URL to the original image. * @type string $webp_url URL to the WebP version. Not set if not applicable. * @type string $descriptor A src descriptor. * } * } */ protected function process_image( $image ) { static $extensions; $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s'; if ( ! preg_match_all( $atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER ) ) { // No attributes? return false; } $attributes = []; foreach ( $tmp_attributes as $attribute ) { $attributes[ $attribute['name'] ] = $attribute['value']; } if ( ! empty( $attributes['class'] ) && strpos( $attributes['class'], 'imagify-no-webp' ) !== false ) { // Has the 'imagify-no-webp' class. return false; } // Deal with the src attribute. $src_source = false; foreach ( [ 'data-lazy-src', 'data-src', 'src' ] as $src_attr ) { if ( ! empty( $attributes[ $src_attr ] ) ) { $src_source = $src_attr; break; } } if ( ! $src_source ) { // No src attribute. return false; } if ( ! isset( $extensions ) ) { $extensions = imagify_get_mime_types( 'image' ); $extensions = array_keys( $extensions ); $extensions = implode( '|', $extensions ); } if ( ! preg_match( '@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src ) ) { // Not a supported image format. return false; } $data = [ 'tag' => $image, 'attributes' => $attributes, 'src_attribute' => $src_source, 'src' => [ 'url' => $attributes[ $src_source ], ], 'srcset_attribute' => false, 'srcset' => [], ]; foreach ( $this->get_nextgen_image_data_set( $src ) as $key => $value ) { $data['src'][ $key ] = $value; } // Deal with the srcset attribute. $srcset_source = false; foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) { if ( ! empty( $attributes[ $srcset_attr ] ) ) { $srcset_source = $srcset_attr; break; } } if ( $srcset_source ) { $srcset_data = []; $data['srcset_attribute'] = $srcset_source; $srcset = explode( ',', $attributes[ $srcset_source ] ); foreach ( $srcset as $srcs ) { $srcs = preg_split( '/\s+/', trim( $srcs ) ); if ( count( $srcs ) > 2 ) { // Not a good idea to have space characters in file name. $descriptor = array_pop( $srcs ); $srcs = [ implode( ' ', $srcs ), $descriptor ]; } if ( empty( $srcs[1] ) ) { $srcs[1] = '1x'; } if ( ! preg_match( '@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src ) ) { // Not a supported image format. $data['srcset'][] = [ 'url' => $srcs[0], 'descriptor' => $srcs[1], ]; continue; } $srcset_data = [ 'url' => $srcs[0], 'descriptor' => $srcs[1], ]; foreach ( $this->get_nextgen_image_data_set( $src ) as $key => $value ) { $srcset_data[ $key ] = $value; } $data['srcset'][] = $srcset_data; } } /** * Filter a processed image tag. * * @since 1.9 * * @param array $data An array of data for this image. * @param string $image An image html tag. */ $data = apply_filters( 'imagify_webp_picture_process_image', $data, $image ); if ( ! $data || ! is_array( $data ) ) { return false; } if ( ! isset( $data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'] ) ) { return false; } return $data; } /** * Get the next-gen image(webp & avif) data set. * * @param array $src Array of url/path segments. * * @return array */ protected function get_nextgen_image_data_set( array $src ): array { $webp_url = imagify_path_to_nextgen( $src['src'], 'webp' ); $webp_path = $this->url_to_path( $webp_url ); $avif_url = imagify_path_to_nextgen( $src['src'], 'avif' ); $avif_path = $this->url_to_path( $avif_url ); $query_string = ! empty( $src['query'] ) ? $src['query'] : ''; return [ // WebP data set. 'webp_url' => $webp_url . $query_string, 'webp_path' => $webp_path, 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ), // Avif data set. 'avif_url' => $avif_url . $query_string, 'avif_path' => $avif_path, 'avif_exists' => $avif_path && $this->filesystem->exists( $avif_path ), ]; } /** * Tell if a content is HTML. * * @since 1.9 * * @param string $content The content. * * @return bool */ protected function is_html( $content ) { return preg_match( '/<\/html>/i', $content ); } /** * Convert a file URL to an absolute path. * * @since 1.9 * * @param string $url A file URL. * * @return string|bool The file path. False on failure. */ protected function url_to_path( $url ) { static $uploads_url; static $uploads_dir; static $root_url; static $root_dir; static $cdn_url; static $domain_url; /** * $url, $uploads_url, $root_url, and $cdn_url are passed through `set_url_scheme()` only to make sure `stripos()` doesn't fail over a stupid http/https difference. */ if ( ! isset( $uploads_url ) ) { $uploads_url = set_url_scheme( $this->filesystem->get_upload_baseurl() ); $uploads_dir = $this->filesystem->get_upload_basedir( true ); $root_url = set_url_scheme( $this->filesystem->get_site_root_url() ); $root_dir = $this->filesystem->get_site_root(); $cdn_url = apply_filters( 'imagify_cdn_source_url', '' ); $cdn_url = $cdn_url['url'] ? set_url_scheme( $cdn_url['url'] ) : false; $domain_url = wp_parse_url( $root_url ); if ( ! empty( $domain_url['scheme'] ) && ! empty( $domain_url['host'] ) ) { $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/'; } else { $domain_url = false; } } // Get the right URL format. if ( $domain_url && strpos( $url, '/' ) === 0 ) { // URL like `/path/to/image.jpg.webp`. $url = $domain_url . ltrim( $url, '/' ); } $url = set_url_scheme( $url ); if ( $cdn_url && $domain_url && stripos( $url, $cdn_url ) === 0 ) { // CDN. $url = str_ireplace( $cdn_url, $domain_url, $url ); } // Return the path. if ( stripos( $url, $uploads_url ) === 0 ) { return str_ireplace( $uploads_url, $uploads_dir, $url ); } if ( stripos( $url, $root_url ) === 0 ) { return str_ireplace( $root_url, $root_dir, $url ); } return false; } } Optimization/Process/ProcessInterface.php 0000644 00000016644 15174671731 0014647 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Optimization\Process; use Imagify\Media\MediaInterface; use Imagify\Optimization\Data\DataInterface; use Imagify\Optimization\File; use WP_Error; /** * Interface to use to optimize medias. * * @since 1.9 */ interface ProcessInterface { /** * The suffix used in the thumbnail size name. * * @var string * @since 1.9 */ const WEBP_SUFFIX = '@imagify-webp'; /** * The suffix used in the thumbnail size name. * * @var string * @since 2.2 */ const AVIF_SUFFIX = '@imagify-avif'; /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * * @param mixed $id Whatever. * * @return bool */ public static function constructor_accepts( $id ); /** * Get the data instance. * * @since 1.9 * * @return DataInterface|false */ public function get_data(); /** * Get the media instance. * * @since 1.9 * * @return MediaInterface|false */ public function get_media(); /** * Get the File instance of the original file. * * @since 1.9.8 * * @return File|false */ public function get_original_file(); /** * Get the File instance of the full size file. * * @since 1.9.8 * * @return File|false */ public function get_fullsize_file(); /** * Tell if the current media is valid. * * @since 1.9 * * @return bool */ public function is_valid(); /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * * @return bool */ public function current_user_can( $describer ); /** * Optimize a media files by pushing tasks into the queue. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize( $optimization_level = null ); /** * Re-optimize a media files with a different level. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function reoptimize( $optimization_level = null ); /** * Optimize several file sizes by pushing tasks into the queue. * * @since 1.9 * * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null ); /** * Optimize one file with Imagify directly. * * @since 1.9 * * @param string $size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array|WP_Error The optimization data. A \WP_Error instance on failure. */ public function optimize_size( $size, $optimization_level = null ); /** * Restore the media files from the backup file. * * @since 1.9 * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function restore(); /** * Get the sizes for this media that have not get through optimization. * No sizes are returned if the file is not optimized, has no backup, or is not an image. * The 'full' size os never returned. * * @since 1.9 * * @return array|WP_Error { * A WP_Error object on failure. * An array of data for the thumbnail sizes on success. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * @type string $file The name the thumbnail "should" have. * } */ public function get_missing_sizes(); /** * Optimize missing thumbnail sizes. * * @since 1.9 * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_missing_thumbnails(); /** * Delete the backup file. * * @since 1.9 */ public function delete_backup(); /** * Maybe resize an image. * * @since 1.9 * * @param string $size The size name. * @param File $file A File instance. * * @return array|WP_Error A \WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. * @type int $file_size The file size in bytes. * } */ public function maybe_resize( $size, $file ); /** * Generate next-gen images if they are missing. * * @since 1.9 * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function generate_nextgen_versions(); /** * Delete the next gen format images. * This doesn't delete the related optimization data. * * @since 2.2 * * @param bool $keep_full Set to true to keep the full size. * * @return bool|WP_Error True on success. A \WP_Error object on failure. */ public function delete_nextgen_files( $keep_full = false ); /** * Tell if a thumbnail size is an "Imagify Next-Gen" size. * * @since 2.2 * * @param string $size_name The size name. * * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen. */ public function is_size_next_gen( $size_name ); /** * Tell if the media has all next-gen versions. * * @return bool */ public function is_full_next_gen(); /** * Tell if the media has a next-gen format. * * @since 2.2 * * @return bool */ public function has_next_gen(); /** * Tell if a process is running for this media. * * @since 1.9 * * @return bool */ public function is_locked(); /** * Set the running status to "running" for a period of time. * * @since 1.9 */ public function lock(); /** * Delete the running status. * * @since 1.9 */ public function unlock(); /** * Tell if a size already has optimization data. * * @since 1.9 * * @param string $size The size name. * * @return bool */ public function size_has_optimization_data( $size ); /** * Update the optimization data for a size. * * @since 1.9 * * @param object $response The API response. * @param string $size The size name. * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array { * The optimization data. * * @type string $size The size name. * @type int $level The optimization level. * @type string $status The status: 'success', 'already_optimized', 'error'. * @type bool $success True if successfully optimized. False on error or if already optimized. * @type string $error An error message. * @type int $original_size The weight of the file, before optimization. * @type int $optimized_size The weight of the file, once optimized. * } */ public function update_size_optimization_data( $response, $size, $level ); } Optimization/Process/AbstractProcess.php 0000644 00000161214 15174671731 0014504 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Optimization\Process; use Imagify_Filesystem; use Imagify\Deprecated\Traits\Optimization\Process\AbstractProcessDeprecatedTrait; use Imagify\Job\MediaOptimization; use Imagify\Optimization\Data\DataInterface; use Imagify\Optimization\File; use Imagify\Media\MediaInterface; use WP_Error; /** * Abstract class used to optimize medias. * * @since 1.9 */ abstract class AbstractProcess implements ProcessInterface { use AbstractProcessDeprecatedTrait; /** * The suffix used in file name to create a temporary copy of the full size. * * @var string * @since 1.9 */ const TMP_SUFFIX = '@imagify-tmp'; /** * Used for the name of the transient telling if a media is locked. * %1$s is the context, %2$s is the media ID. * * @var string * @since 1.9 */ const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked'; /** * The data optimization object. * * @var DataInterface * @since 1.9 */ protected $data; /** * The optimization data format. * * @var array * @since 1.9 */ protected $data_format = [ 'level' => null, 'status' => null, 'success' => null, 'error' => null, 'original_size' => null, 'optimized_size' => null, ]; /** * A File instance. * * @var File * @since 1.9 */ protected $file; /** * Filesystem object. * * @var Imagify_Filesystem * @since 1.9 */ protected $filesystem; /** * Used to cache the plugin’s options. * * @var array * @since 1.9 */ protected $options = []; /** * Tells the format we are currently processing * * @var string * @since 2.2 */ protected $format; /** * Array of image extensions processed. * * @var array */ protected $extensions = [ 'webp', 'avif', ]; /** * The constructor. * * @since 1.9 * @see self::constructor_accepts() * * @param mixed $id An ID, or whatever type the constructor accepts. */ public function __construct( $id ) { if ( $id instanceof DataInterface ) { $this->data = $id; } elseif ( static::constructor_accepts( $id ) ) { $data_class = str_replace( '\\Optimization\\Process\\', '\\Optimization\\Data\\', get_called_class() ); $data_class = '\\' . ltrim( $data_class, '\\' ); $this->data = new $data_class( $id ); } else { $this->data = false; } $this->filesystem = \Imagify_Filesystem::get_instance(); $this->format = $this->get_current_format(); } /** * Tell if the given entry can be accepted in the constructor. * * @since 1.9 * * @param mixed $id Whatever. * * @return bool */ public static function constructor_accepts( $id ) { if ( $id instanceof DataInterface ) { return true; } $data_class = str_replace( '\\Optimization\\Process\\', '\\Optimization\\Data\\', get_called_class() ); $data_class = '\\' . ltrim( $data_class, '\\' ); return $data_class::constructor_accepts( $id ); } /** * Get the data instance. * * @since 1.9 * * @return DataInterface|false */ public function get_data() { return $this->data; } /** * Get the media instance. * * @since 1.9 * * @return MediaInterface|false */ public function get_media() { if ( ! $this->get_data() ) { return false; } return $this->get_data()->get_media(); } /** * Get the File instance of the original file. * * @since 1.9.8 * * @return File|false */ public function get_original_file() { if ( isset( $this->file ) ) { return $this->file; } $this->file = false; if ( $this->get_media() ) { $this->file = new File( $this->get_media()->get_raw_original_path() ); } return $this->file; } /** * Get the File instance of the full size file. * * @since 1.9.8 * * @return File|false */ public function get_fullsize_file() { if ( isset( $this->file ) ) { return $this->file; } $this->file = false; if ( $this->get_media() ) { $this->file = new File( $this->get_media()->get_raw_fullsize_path() ); } return $this->file; } /** * Tell if the current media is valid. * * @since 1.9 * * @return bool */ public function is_valid() { return $this->get_media() && $this->get_media()->is_valid(); } /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * @return bool */ public function current_user_can( $describer ) { if ( ! $this->is_valid() ) { return false; } $media = $this->get_media(); return $media->get_context_instance()->current_user_can( $describer, $media->get_id() ); } /** * Optimize a media files. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param array $args An array of optionnal arguments. * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize( $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $data = $this->get_data(); if ( $data->is_optimized() ) { return new WP_Error( 'optimized', __( 'This media has already been optimized by Imagify.', 'imagify' ) ); } if ( $data->is_already_optimized() && $this->has_next_gen() ) { // If already optimized but has next-gen, delete next-gen versions and optimization data. $data->delete_optimization_data(); $deleted = $this->delete_nextgen_files(); if ( is_wp_error( $deleted ) ) { return new WP_Error( 'next_gen_not_deleted', __( 'Previous Next-Gen files could not be deleted.', 'imagify' ) ); } } $sizes = $media->get_media_files(); $args = is_array( $args ) ? $args : []; $args['hook_suffix'] = 'optimize_media'; // Optimize. return $this->optimize_sizes( array_keys( $sizes ), $optimization_level, $args ); } /** * Re-optimize a media files with a different level. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param array $args An array of optionnal arguments. * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function reoptimize( $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $data = $this->get_data(); if ( ! $data->get_optimization_status() ) { return new WP_Error( 'not_processed_yet', __( 'This media has not been processed yet.', 'imagify' ) ); } $optimization_level = $this->sanitize_optimization_level( $optimization_level ); if ( $data->get_optimization_level() === $optimization_level ) { return new WP_Error( 'identical_optimization_level', __( 'This media is already optimized with this level.', 'imagify' ) ); } $this->restore(); $sizes = $media->get_media_files(); $args = is_array( $args ) ? $args : []; $args['hook_suffix'] = 'reoptimize_media'; // Optimize. return $this->optimize_sizes( array_keys( $sizes ), $optimization_level, $args ); } /** * Optimize several file sizes by pushing tasks into the queue. * * @since 1.9 * @see MediaOptimization->task_before() * @see MediaOptimization->task_after() * * @since 2.2 * Addition of the image format * * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param array $args { * An array of optionnal arguments. * * @type string $hook_suffix Suffix used to trigger hooks before and after optimization. * } * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } if ( ! $sizes ) { return new WP_Error( 'no_sizes', __( 'No sizes given to be optimized.', 'imagify' ) ); } if ( empty( $args['locked'] ) ) { if ( $this->is_locked() ) { return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) ); } $this->lock(); } if ( $media->is_image() ) { // Add Next-Gen conversion. $formats = imagify_nextgen_images_formats(); foreach ( $formats as $format ) { if ( 'avif' === $format ) { $format_suffix = static::AVIF_SUFFIX; } elseif ( 'webp' === $format ) { $format_suffix = static::WEBP_SUFFIX; } $files = $media->get_media_files(); foreach ( $sizes as $size_name ) { if ( empty( $files[ $size_name ] ) ) { continue; } if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) { continue; } if ( in_array( $size_name . $format_suffix, $sizes, true ) ) { continue; } array_unshift( $sizes, $size_name . $format_suffix ); } } if ( ! $media->get_context_instance()->can_backup() && ! $media->get_backup_path() && ! $this->get_data()->get_size_data( 'full', 'success' ) ) { /** * Backup is NOT activated, and a backup file does NOT exist yet, and the full size is NOT optimized yet. * Next-Gen conversion needs a backup file, even a temporary one: we’ll create one. */ $next_gen = false; foreach ( $sizes as $size_name ) { if ( $this->is_size_next_gen( $size_name ) ) { $next_gen = true; break; } } if ( $next_gen ) { // We have at least one next-gen conversion to do: create a temporary backup. $backuped = $this->get_original_file()->backup( $media->get_raw_backup_path() ); if ( $backuped ) { $args['delete_backup'] = true; } } } } $sizes = array_unique( $sizes ); $optimization_level = $this->sanitize_optimization_level( $optimization_level ); /** * Filter the data sent to the optimization process. * * @since 1.9 * * @param array $new_args Additional data to send to the optimization process. * @param array $args Current data sent to the process. * @param ProcessInterface $process The current optimization process. * @param array $sizes Sizes being processed. * @param int $optimization_level Optimization level. */ $new_args = apply_filters( 'imagify_optimize_sizes_args', [], $args, $this, $sizes, $optimization_level ); if ( $new_args && is_array( $new_args ) ) { $args = array_merge( $new_args, $args ); } /** * Push the item to the queue, save the queue in the DB, empty the queue. * A "batch" is then created in the DB with this unique item, it is then free to loop through its steps (files) without another item interfering (each media optimization has its own dedicated batch/queue). */ MediaOptimization::get_instance()->push_to_queue( [ 'id' => $media->get_id(), 'sizes' => $sizes, 'optimization_level' => $optimization_level, 'process_class' => get_class( $this ), 'data' => $args, ] )->save(); return true; } /** * Optimize one file with Imagify directly. * * @since 1.9 * * @param string $size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array|WP_Error Optimized image data. A WP_Error object on error. */ public function optimize_size( $size, $optimization_level = null ) { if ( ! $this->is_valid() ) { // Bail out. return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); $sizes = $media->get_media_files(); $thumb_size = $size; $next_gen = $this->is_size_next_gen( $size ); $path_is_temp = false; if ( $next_gen ) { // We'll make sure the file is an image later. $thumb_size = $next_gen; // Contains the name of the non-next-gen size. $next_gen = true; } if ( empty( $sizes[ $thumb_size ]['path'] ) ) { // Bail out. // This size is not in our list. return new WP_Error( 'unknown_size', sprintf( /* translators: %s is a size name. */ __( 'The size %s is unknown.', 'imagify' ), '<code>' . esc_html( $thumb_size ) . '</code>' ) ); } if ( $this->get_data()->get_size_data( $size, 'success' ) ) { // Bail out. // This size is already optimized with Imagify, and must not be optimized again. if ( $next_gen ) { return new WP_Error( 'size_is_successfully_optimized', sprintf( /* translators: %s is a size name. */ __( 'The Next-Gen format for the size %s already exists.', 'imagify' ), '<code>' . esc_html( $thumb_size ) . '</code>' ) ); } else { return new WP_Error( 'size_is_successfully_optimized', sprintf( /* translators: %s is a size name. */ __( 'The size %s is already optimized by Imagify.', 'imagify' ), '<code>' . esc_html( $thumb_size ) . '</code>' ) ); } } /** * Starting from here, errors will be stored in the optimization data of the size. */ $path = $sizes[ $thumb_size ]['path']; $optimization_level = $this->sanitize_optimization_level( $optimization_level ); if ( $next_gen && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) { // We want a next-gen version but the source file is already optimized by Imagify. $result = $this->create_temporary_copy( $thumb_size, $sizes ); if ( ! $result ) { // Bail out. // Could not create a copy of the non-next-gen version. $response = new WP_Error( 'non_next_gen_copy_failed', sprintf( /* translators: %s is a size name. */ __( 'Could not create an unoptimized copy of the size %s.', 'imagify' ), '<code>' . esc_html( $thumb_size ) . '</code>' ) ); $this->update_size_optimization_data( $response, $size, $optimization_level ); return $response; } /** * $path now targets a temporary file. */ $path = $this->get_temporary_copy_path( $thumb_size, $sizes ); $path_is_temp = true; } $file = new File( $path ); // Original file or temporary copy. if ( ! $file->is_supported( $media->get_allowed_mime_types() ) ) { // Bail out. // This file type is not supported. $extension = $file->get_extension(); if ( ! $extension ) { $response = new WP_Error( 'extension_not_mime', __( 'This file has an extension that does not match a mime type.', 'imagify' ) ); } elseif ( '' === $extension ) { $response = new WP_Error( 'no_extension', __( 'With no extension, this file cannot be optimized.', 'imagify' ) ); } elseif ( ! $extension ) { $response = new WP_Error( 'extension_not_mime', __( 'This file has an extension that does not match a mime type.', 'imagify' ) ); } else { $response = new WP_Error( 'extension_not_supported', sprintf( /* translators: %s is a file extension. */ __( '%s cannot be optimized.', 'imagify' ), '<code>' . esc_html( strtolower( $extension ) ) . '</code>' ) ); } if ( $path_is_temp ) { $this->filesystem->delete( $path ); } $this->update_size_optimization_data( $response, $size, $optimization_level ); return $response; } if ( $next_gen && ! $file->is_image() ) { // Bail out. if ( $path_is_temp ) { $this->filesystem->delete( $path ); } $response = new WP_Error( 'no_next_gen', __( 'This file is not an image and cannot be converted to Next-Gen format.', 'imagify' ) ); $this->update_size_optimization_data( $response, $size, $optimization_level ); return $response; } $is_disabled = ! empty( $sizes[ $thumb_size ]['disabled'] ); /** * Fires before optimizing a file. * Return a WP_Error object to prevent the optimization. * * @since 1.9 * * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization. * @param ProcessInterface $process The optimization process instance. * @param File $file The file instance. If $webp is true, $file references the non-WebP file. * @param string $thumb_size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param bool $webp The image will be converted to WebP. * @param bool $is_disabled Tell if this size is disabled from optimization. */ $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled ); if ( ! is_wp_error( $response ) ) { if ( $is_disabled ) { // This size must not be optimized. $response = new WP_Error( 'unauthorized_size', sprintf( /* translators: %s is a size name. */ __( 'The size %s is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ), '<code>' . esc_html( $thumb_size ) . '</code>' ) ); } elseif ( ! $this->filesystem->exists( $file->get_path() ) ) { $response = new WP_Error( 'file_not_exists', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to exist.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>' ) ); } elseif ( $next_gen && ! $this->can_create_next_gen_version( $file->get_path() ) ) { $response = new WP_Error( 'is_animated_gif', __( 'This file is an animated gif: since Imagify does not support animated WebP/AVIF, WebP/AVIF creation for animated gif is disabled.', 'imagify' ) ); } elseif ( ! $this->filesystem->is_writable( $file->get_path() ) ) { $response = new WP_Error( 'file_not_writable', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '</code>' ) ); } else { // Maybe resize the file. $response = $this->maybe_resize( $thumb_size, $file ); $convert = ''; if ( $next_gen ) { if ( strpos( $size, static::AVIF_SUFFIX ) ) { $convert = 'avif'; } elseif ( strpos( $size, static::WEBP_SUFFIX ) ) { $convert = 'webp'; } } if ( ! is_wp_error( $response ) ) { // Resizing succeeded: optimize the file. $response = $file->optimize( [ 'backup' => ! $response['backuped'] && $this->can_backup( $size ), 'backup_path' => $media->get_raw_backup_path(), 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null, 'optimization_level' => $optimization_level, 'convert' => $convert, 'keep_exif' => true, 'context' => $media->get_context(), 'original_size' => $response['file_size'], ] ); $response = $this->compare_next_gen_file_size( [ 'response' => $response, 'file' => $file, 'is_next_gen' => $next_gen, 'next_gen_format' => $convert, 'non_next_gen_thumb_size' => $thumb_size, 'non_next_gen_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file. 'optimization_level' => $optimization_level, ] ); if ( property_exists( $response, 'message' ) ) { $path_is_temp = false; if ( $path !== $sizes[ $thumb_size ]['path'] ) { $this->filesystem->delete( $path ); } $path = $sizes[ $thumb_size ]['path']; } } } } $data = $this->update_size_optimization_data( $response, $size, $optimization_level ); /** * Fires after optimizing a file. * * @since 1.9 * * @param ProcessInterface $process The optimization process instance. * @param File $file The file instance. * @param string $thumb_size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * @param bool $webp The image was supposed to be converted to WebP. * @param bool $is_disabled Tell if this size is disabled from optimization. */ do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled ); if ( ! $path_is_temp ) { return $data; } // Delete the temporary copy. $this->filesystem->delete( $path ); if ( is_wp_error( $response ) ) { return $data; } // Rename the optimized file. $destination_path = str_replace( static::TMP_SUFFIX . '.', '.', $file->get_path() ); $this->filesystem->move( $file->get_path(), $destination_path, true ); return $data; } /** * Compare the file size of a file and its Next-Gen version: if the Next-Gen version is heavier than the non-next-gen file, delete it. * * @since 2.2 * * @param array $args { * A list of mandatory arguments. * * @type \sdtClass|WP_Error $response Optimized image data. A WP_Error object on error. * @type File $file The File instance of the file currently being optimized. * @type bool $is_next_gen Tell if we're requesting a next-gen file. * @type string $non_next_gen_thumb_size Name of the corresponding non-next-gen thumbnail size. If we're not creating a Next-Gen file, this corresponds to the current thumbnail size. * @type string $non_next_gen_file_path Path to the corresponding non-next-gen file. If we're not creating a Next-Gen file, this corresponds to the current file path. * @type string $optimization_level The optimization level. * } * * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error. */ protected function compare_next_gen_file_size( $args ) { static $keep_large_next_gen; if ( ! isset( $keep_large_next_gen ) ) { /** * Allow to not store next-gen images that are larger than their non-next-gen version. * * @since 1.9.4 * * @param bool $keep_large_next-gen Set to false if you prefer your visitors over your Pagespeed score. Default value is true. */ $keep_large_next_gen = apply_filters( 'imagify_keep_large_next_gen', true ); } if ( $keep_large_next_gen || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) { return $args['response']; } // Optimization succeeded. if ( ! property_exists( $args['response'], 'message' ) && $args['is_next_gen'] ) { /** * We just created a next-gen version: * Check if it is lighter than the (maybe optimized) non-next-gen file. */ $data = $this->get_data()->get_size_data( $args['non_next_gen_thumb_size'] ); if ( ! $data ) { // We haven’t tried to optimize the non-next-gen size yet. return $args['response']; } if ( ! empty( $data['optimized_size'] ) ) { // The non-next-gen size is optimized, we know the file size. $non_next_gen_file_size = $data['optimized_size']; } else { // The non-next-gen size is "already optimized" or "error": grab the file size directly from the file. $non_next_gen_file_size = $this->filesystem->size( $args['non_next_gen_file_path'] ); } if ( ! $non_next_gen_file_size || $non_next_gen_file_size > $args['response']->new_size ) { // The new next-gen file is lighter. return $args['response']; } // The new next-gen file is heavier than the non-next-gen file: delete it and return an error. $this->filesystem->delete( $args['file']->get_path() ); return new WP_Error( 'next_gen_heavy', sprintf( /* translators: %s is a size name. */ __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ), '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>' ) ); } /** * We just created a non-next-gen version: * Check if its next-gen version file is lighter than this one. */ $next_gen_size = $args['non_next_gen_thumb_size'] . $args['next_gen_format']; $next_gen_file_size = $this->get_data()->get_size_data( $next_gen_size, 'optimized_size' ); if ( property_exists( $args['response'], 'message' ) || ! $next_gen_file_size || $next_gen_file_size < $args['response']->new_size ) { // The next-gen file is lighter than this one. return $args['response']; } // The new optimized file is lighter than the next-gen file: delete the next-gen file and store an error. $next_gen_path = $args['file']->get_path_to_nextgen( $args['next_gen_format'] ); if ( $next_gen_path && $this->filesystem->is_writable( $next_gen_path ) ) { $this->filesystem->delete( $next_gen_path ); } $next_gen_response = new WP_Error( 'next_gen_heavy', sprintf( /* translators: %s is a size name. */ __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ), '<code>' . esc_html( $args['non_next_gen_thumb_size'] ) . '</code>' ) ); $this->update_size_optimization_data( $next_gen_response, $next_gen_size, $args['optimization_level'] ); return $args['response']; } /** * Restore the media files from the backup file. * * @since 1.9 * * @return bool|WP_Error True on success. A WP_Error instance on failure. */ public function restore() { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } if ( ! $media->has_backup() ) { return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) ); } if ( $this->is_locked() ) { return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) ); } $this->lock( 'restoring' ); $backup_path = $media->get_backup_path(); $original_path = $media->get_raw_original_path(); if ( $backup_path === $original_path ) { $this->unlock(); return new WP_Error( 'same_path', __( 'Image path and backup path are identical.', 'imagify' ) ); } $dest_dir = $this->filesystem->dir_path( $original_path ); if ( ! $this->filesystem->exists( $dest_dir ) ) { $this->filesystem->make_dir( $dest_dir ); } $dest_file_is_writable = ! $this->filesystem->exists( $original_path ) || $this->filesystem->is_writable( $original_path ); if ( ! $dest_file_is_writable || ! $this->filesystem->is_writable( $dest_dir ) ) { $this->unlock(); return new WP_Error( 'destination_not_writable', __( 'The image to replace is not writable.', 'imagify' ) ); } // Get some data before doing anything. $data = $this->get_data()->get_optimization_data(); $files = $media->get_media_files(); /** * Fires before restoring a media. * Return a WP_Error object to prevent the restoration. * * @since 1.9 * * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization. * @param ProcessInterface $process Instance of this process. */ $response = apply_filters( 'imagify_before_restore_media', null, $this ); if ( ! is_wp_error( $response ) ) { // Create the original image from the backup. $response = $this->filesystem->copy( $backup_path, $original_path, true ); if ( ! $response ) { // Failure. $response = new WP_Error( 'copy_failed', __( 'The backup file could not be copied over the optimized one.', 'imagify' ) ); } else { // Backup successfully copied. $this->filesystem->chmod_file( $original_path ); // Remove old optimization data. $this->get_data()->delete_optimization_data(); if ( $media->is_image() ) { // Restore the original dimensions in the database. $media->update_dimensions(); // Delete the WebP version. $this->delete_nextgen_file( $original_path, true ); // Restore the thumbnails. $response = $this->restore_thumbnails(); } } } /** * Fires after restoring a media. * * @since 1.9 * * @param ProcessInterface $process Instance of this process. * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure. * @param array $files The list of files, before restoring them. * @param array $data The optimization data, before deleting it. */ do_action( 'imagify_after_restore_media', $this, $response, $files, $data ); $this->unlock(); return $response; } /** * Restore the thumbnails. * * @since 1.9 * * @return bool|WP_Error True on success. A WP_Error instance on failure. */ protected function restore_thumbnails() { $media = $this->get_media(); /** * Delete the next-gen versions. * If the full size file and the original file are not the same, the full size is considered like a thumbnail. * In that case we must also delete the next-gen file associated to the full size. */ $keep_full_next_gen = $media->get_raw_original_path() === $media->get_raw_fullsize_path(); $this->delete_nextgen_files( $keep_full_next_gen, true ); // Generate new thumbnails. return $media->generate_thumbnails(); } /** * Delete the backup file. * * @since 1.9 */ public function delete_backup() { if ( ! $this->is_valid() ) { return; } $backup_path = $this->get_media()->get_backup_path(); if ( $backup_path ) { $this->filesystem->delete( $backup_path ); // Check for the -scaled version in the backup. $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path ); if ( $this->filesystem->exists( $scaled_backup_path ) ) { // Delete the -scaled version from the backup. $this->filesystem->delete( $scaled_backup_path ); } } } /** * Create a temporary copy of a size file. * * If we need to create a next-gen version, we must create it from an unoptimized image. * The full size is always optimized before the next-gen version creation, and in some cases it’s the same for the thumbnails. * Then we use the backup file to create temporary files. * * @since 1.9 * * @param string $size The image size name. * @param array $sizes A list of thumbnail sizes being optimized. * * @return bool True if the file exists/is created. False on failure. */ protected function create_temporary_copy( $size, $sizes = null ) { $media = $this->get_media(); if ( ! isset( $sizes ) ) { $sizes = $media->get_media_files(); } if ( empty( $sizes[ $size ] ) ) { // What? return false; } $tmp_path = $this->get_temporary_copy_path( $size, $sizes ); if ( $tmp_path && $this->filesystem->exists( $tmp_path ) ) { // The temporary file already exists. return true; } $tmp_file = new File( $tmp_path ); if ( ! $tmp_file->is_image() ) { // The file is not an image. return false; } if ( ! $tmp_file->is_supported( $media->get_allowed_mime_types() ) ) { // The file is not supported. return false; } /** * Use the backup file as source. */ $backup_path = $media->get_backup_path(); if ( ! $backup_path ) { // No backup, no hope for you. return false; } /** * In all cases we must make a copy of the backup file, and not use the backup directly: * sometimes the backup image does not have a valid file extension (yes I’m looking at you NextGEN Gallery). */ $copied = $this->filesystem->copy( $backup_path, $tmp_path, true ); if ( ! $copied ) { return false; } if ( 'full' === $size ) { /** * We create a copy of the backup to be able to create a next-gen version from it. * That means the optimization process will resize the file if needed, so there is nothing more to do here. */ return true; } // We need to create a thumbnail from it. $size_data = $sizes[ $size ]; $context_sizes = $media->get_context_instance()->get_thumbnail_sizes(); if ( ! empty( $context_sizes[ $size ] ) ) { // Not a dynamic size, yay! $size_data = array_merge( $size_data, $context_sizes[ $size ] ); } if ( empty( $size_data['path'] ) ) { // Should not happen. return false; } if ( ! isset( $size_data['crop'] ) ) { /** * In case of a dynamic thumbnail we don’t know if the image must be croped or resized. * * @since 1.9 * * @param bool $crop True to crop the thumbnail, false to resize. Null by default. * @param string $size Name of the thumbnail size. * @param array $size_data Data of the thumbnail being processed. Contains at least 'width', 'height', and 'path'. * @param MediaInterface $media The MediaInterface instance corresponding to the image being processed. */ $crop = apply_filters( 'imagify_crop_thumbnail', null, $size, $size_data, $media ); if ( null !== $crop ) { $size_data['crop'] = (bool) $crop; } } if ( ! isset( $size_data['crop'] ) ) { // We don't have the 'crop' data in that case: let’s try to guess it. if ( ! $size_data['height'] || ! $size_data['width'] ) { // One of the size dimensions is 0, that means crop is probably disabled. $size_data['crop'] = false; } else { if ( ! $this->filesystem->exists( $size_data['path'] ) ) { // Screwed. return false; } $thumb_dimensions = $this->filesystem->get_image_size( $size_data['path'] ); if ( ! $thumb_dimensions || ! $thumb_dimensions['width'] || ! $thumb_dimensions['height'] ) { return false; } // Compare dimensions. $new_height = $thumb_dimensions['width'] * $size_data['height'] / $size_data['width']; // If the difference is > to 1px, let's assume that crop is enabled. $size_data['crop'] = abs( $thumb_dimensions['height'] - $new_height ) > 1; } } $resized = $tmp_file->create_thumbnail( [ 'path' => $tmp_path, 'width' => $size_data['width'], 'height' => $size_data['height'], 'crop' => $size_data['crop'], 'adjust_filename' => false, ] ); if ( is_wp_error( $resized ) ) { return false; } // Make sure the new file has the expected name. $new_tmp_path = $this->filesystem->dir_path( $tmp_path ) . $resized['file']; if ( $new_tmp_path === $tmp_path ) { return true; } return $this->filesystem->move( $new_tmp_path, $tmp_path, true ); } /** * Get the path to a temporary copy of a size file. * * @since 1.9 * * @param string $size The image size name. * @param array $sizes A list of thumbnail sizes being optimized. * * @return string|bool An image path. False on failure. */ protected function get_temporary_copy_path( $size, $sizes = null ) { if ( 'full' === $size ) { $path = $this->get_media()->get_raw_fullsize_path(); } else { if ( ! isset( $sizes ) ) { $sizes = $this->get_media()->get_media_files(); } $path = ! empty( $sizes[ $size ]['path'] ) ? $sizes[ $size ]['path'] : false; } if ( ! $path ) { return false; } $info = $this->filesystem->path_info( $path ); if ( ! $info['file_base'] ) { return false; } return $info['dir_path'] . $info['file_base'] . static::TMP_SUFFIX . '.' . $info['extension']; } /** * Maybe resize an image. * * @since 1.9 * * @param string $size The size name. * @param File $file A File instance. * * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. * @type int $file_size The file size in bytes. * } */ public function maybe_resize( $size, $file ) { if ( ! $this->can_resize( $size, $file ) ) { // This file should not be resized. return [ 'resized' => false, 'backuped' => false, 'file_size' => 0, ]; } $dimensions = $file->get_dimensions(); if ( ! $dimensions['width'] ) { // Could not get the image dimensions. return new WP_Error( 'no_dimensions', sprintf( /* translators: %s is an error message. */ __( 'Resizing failed: %s', 'imagify' ), __( 'Imagify could not get the image dimensions.', 'imagify' ) ) ); } $media = $this->get_media(); $resize_width = $media->get_context_instance()->get_resizing_threshold(); if ( $resize_width >= $dimensions['width'] ) { // No need to resize. return [ 'resized' => false, 'backuped' => false, 'file_size' => 0, ]; } $resized_path = $file->resize( $dimensions, $resize_width ); if ( is_wp_error( $resized_path ) ) { // The resizement failed. return new WP_Error( 'resize_failure', sprintf( /* translators: %s is an error message. */ __( 'Resizing failed: %s', 'imagify' ), $resized_path->get_error_message() ) ); } if ( $this->can_backup( $size ) ) { $source = 'full' === $size ? $media->get_original_path() : null; $backuped = $file->backup( $media->get_raw_backup_path(), $source ); if ( is_wp_error( $backuped ) ) { // The backup failed. return new WP_Error( 'backup_failure', sprintf( /* translators: %s is an error message. */ __( 'Backup failed: %s', 'imagify' ), $backuped->get_error_message() ) ); } } else { $backuped = false; } $file_size = (int) $this->filesystem->size( $file->get_path() ); $resized = $this->filesystem->move( $resized_path, $file->get_path(), true ); if ( ! $resized ) { // The resizement failed. return new WP_Error( 'resize_move_failure', __( 'The image could not be replaced by the resized one.', 'imagify' ) ); } // Store the new dimensions. $media->update_dimensions(); return [ 'resized' => true, 'backuped' => $backuped, 'file_size' => $file_size, ]; } /** * Tell if a size should be resized. * * @since 1.9 * * @param string $size The size name. * @param File $file A File instance. * * @return bool */ protected function can_resize( $size, $file ) { if ( ! $this->is_valid() ) { return false; } if ( 'full' !== $size && ( 'full' . static::WEBP_SUFFIX !== $size || 'full' . static::AVIF_SUFFIX !== $size ) ) { // We resize only the main file and its next-gen version. return false; } if ( ! $file->is_image() ) { return false; } return $this->get_media()->get_context_instance()->can_resize(); } /** * Tell if a size should be backuped. * * @since 1.9 * * @param string $size The size name. * * @return bool */ protected function can_backup( $size ) { if ( ! $this->is_valid() ) { return false; } if ( 'full' !== $size ) { // We backup only the main file. return false; } return $this->get_media()->get_context_instance()->can_backup(); } /** * Get mime type * * @param string $format nextgen image format. */ private function get_mime_type( $format ) { $mime_types = [ 'avif' => 'image/avif', 'webp' => 'image/webp', ]; return isset( $mime_types[ $format ] ) ? $mime_types[ $format ] : false; } /** * Delete the next gen format images. * This doesn't delete the related optimization data. * * @since 2.2 * * @param bool $keep_full Set to true to keep the full size. * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format. * * @return bool|WP_Error True on success. A WP_Error object on failure. */ public function delete_nextgen_files( $keep_full = false, $all_next_gen = false ) { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_image() ) { return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) ); } $files = $media->get_media_files(); if ( $keep_full ) { unset( $files['full'] ); } if ( ! $files ) { return true; } $error_count = 0; foreach ( $files as $file ) { if ( 0 === strpos( $file['mime-type'], 'image/' ) ) { $deleted = $this->delete_nextgen_file( $file['path'], $all_next_gen ); if ( is_wp_error( $deleted ) ) { ++$error_count; } } } if ( $error_count ) { return new WP_Error( 'files_not_deleted', sprintf( /* translators: %s is a formatted number, don’t use %d. */ _n( '%s file could not be deleted.', '%s files could not be deleted.', $error_count, 'imagify' ), number_format_i18n( $error_count ) ) ); } return true; } /** * Delete a next gen format image, given its non-next-gen version's path. * This doesn't delete the related optimization data. * * @since 2.2 * * @param string $file_path Path to the non-next-gen file. * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format. * * @return void|WP_Error A \WP_Error object on failure. */ protected function delete_nextgen_file( $file_path, $all_next_gen = false ) { if ( ! $file_path ) { return new WP_Error( 'no_path', __( 'Path to non-next-gen file not provided.', 'imagify' ) ); } $next_gen_file = new File( $file_path ); $formats = $this->extensions; if ( ! $all_next_gen ) { $formats = imagify_nextgen_images_formats(); } // Delete next-gen images. foreach ( $formats as $extension ) { $path = $next_gen_file->get_path_to_nextgen( $extension ); if ( ! $path ) { continue; } $this->delete_file( $path ); } } /** * Delete a next gen format image, given its non-next-gen version's path. * * @param string $next_gen_path Path to the non-next-gen file. * * @return bool|WP_Error True on success. A WP_Error object on failure. */ protected function delete_file( string $next_gen_path ) { if ( empty( $next_gen_path ) ) { return new WP_Error( 'no_$next_gen_path', __( 'Could not get the path to the Next-Gen format file.', 'imagify' ) ); } if ( ! $this->filesystem->exists( $next_gen_path ) ) { return true; } if ( ! $this->filesystem->is_writable( $next_gen_path ) ) { return new WP_Error( 'file_not_writable', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>' ) ); } if ( ! $this->filesystem->is_file( $next_gen_path ) ) { return new WP_Error( 'not_a_file', sprintf( /* translators: %s is a file path. */ __( 'This does not seem to be a file: %s.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>' ) ); } $deleted = $this->filesystem->delete( $next_gen_path, false, 'f' ); if ( ! $deleted ) { return new WP_Error( 'file_not_deleted', sprintf( /* translators: %s is a file path. */ __( 'The file %s could not be deleted.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '</code>' ) ); } return true; } /** * Gives the next-gen image format we are processing. * * @return string Current format we are targeting. */ public function get_current_format() { $format = get_imagify_option( 'optimization_format' ); return ( 'avif' === $format ) ? static::AVIF_SUFFIX : static::WEBP_SUFFIX; } /** * Tell if a thumbnail size is an "Imagify Next-Gen" size. * * @since 1.9 * @since 2.2 addition of the format parameter. * * @param string $size_name The size name. * * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen. */ public function is_size_next_gen( $size_name ) { $formats = imagify_nextgen_images_formats(); foreach ( $formats as $format ) { $suffix = preg_quote( $this->get_suffix_from_format( $format ), '/' ); if ( preg_match( '/^(?<size>.+)' . $suffix . '$/', (string) $size_name, $matches ) ) { return $matches['size']; } } return false; } /** * Get suffix from format. * * @param string $format Format extension of next-gen image. * @return string */ private function get_suffix_from_format( string $format ): string { $suffixes = [ 'avif' => static::AVIF_SUFFIX, 'webp' => static::WEBP_SUFFIX, ]; return $suffixes[ $format ]; } /** * Tell if the media has a next gen format. * * @since 2.2 * * @return bool */ public function has_next_gen() { if ( ! $this->is_valid() ) { return false; } if ( ! $this->get_media()->is_image() ) { return false; } $data = $this->get_data()->get_optimization_data(); if ( empty( $data['sizes'] ) ) { return false; } $needle = $this->format . '";a:4:{s:7:"success";b:1;'; $data = maybe_serialize( $data['sizes'] ); return is_string( $data ) && strpos( $data, $needle ); } /** * Tell if the media has all Next-Gen versions. * * @return bool */ public function is_full_next_gen() { if ( ! $this->is_valid() ) { return false; } if ( ! $this->get_media()->is_image() ) { return false; } $data = $this->get_data()->get_optimization_data(); $sizes = $data['sizes']; if ( empty( $sizes ) ) { return false; } $keys = array_keys( $sizes ); $non_next_gen_keys = array_values( array_filter( $keys, function ( $key ) { return strpos( (string) $key, $this->format ) === false; } ) ); return array_reduce( $non_next_gen_keys, function ( $is_fully, $key ) use ( $sizes ) { return key_exists( $key . $this->format, $sizes ) && $is_fully; }, true ); } /** * Tell if a Next-Gen version can be created for the given file. * Make sure the file is an image before using this method. * * @since 1.9.5 * * @param string $file_path Path to the file. * * @return bool */ public function can_create_next_gen_version( $file_path ) { if ( ! $file_path ) { return false; } $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', [ null, $file_path ], '2.2', 'imagify_pre_can_create_next_gen_version' ); /** * Tell if a next-gen version can be created for the given file. * The file is an image. * * @since 1.9.5 * * @param bool $can True to create a next-gen version, false otherwise. Null by default. * @param string $file_path Path to the file. */ $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path ); if ( isset( $can ) ) { return (bool) $can; } $is_animated_gif = $this->filesystem->is_animated_gif( $file_path ); if ( is_bool( $is_animated_gif ) ) { // Ok if it’s not an animated gif. return ! $is_animated_gif; } // At this point $is_animated_gif is null, which means the file cannot be read (yet). return true; } /** * Generate next-gen images if they are missing. * * @since 1.9 * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function generate_nextgen_versions() { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media->is_image() ) { return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) ); } if ( ! $media->has_backup() ) { return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) ); } $data = $this->get_data(); if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) { return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) ); } if ( $this->has_next_gen() ) { return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) ); } $files = $media->get_media_files(); $sizes = []; $args = [ 'hook_suffix' => 'generate_nextgen_versions', ]; foreach ( $files as $size_name => $file ) { $formats = imagify_nextgen_images_formats(); foreach ( $formats as $format ) { if ( 'avif' === $format ) { $format_suffix = static::AVIF_SUFFIX; } elseif ( 'webp' === $format ) { $format_suffix = static::WEBP_SUFFIX; } if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) { continue; } array_unshift( $sizes, $size_name . $format_suffix ); } } if ( ! $sizes ) { return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) ); } $optimization_level = $data->get_optimization_level(); // Optimize. return $this->optimize_sizes( $sizes, $optimization_level, $args ); } /** * Tell if a process is running for this media. * * @since 1.9 * * @return string|bool The action if locked ('optimizing' or 'restoring'). False if not locked. */ public function is_locked() { $name = $this->get_lock_name(); if ( ! $name ) { return false; } $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'get_site_transient' : 'get_transient'; $action = call_user_func( $callback, $name ); if ( ! $action ) { return false; } return $this->validate_lock_action( $action ); } /** * Set the running status to "running" for 10 minutes. * * @since 1.9 * * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'. */ public function lock( $action = 'optimizing' ) { $name = $this->get_lock_name(); if ( ! $name ) { return; } $action = $this->validate_lock_action( $action ); $media = $this->get_media(); $callback = $media->get_context_instance()->is_network_wide() ? 'set_site_transient' : 'set_transient'; call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS ); } /** * Unset the running status. * * @since 1.9 */ public function unlock() { $name = $this->get_lock_name(); if ( ! $name ) { return false; } $callback = $this->get_media()->get_context_instance()->is_network_wide() ? 'delete_site_transient' : 'delete_transient'; call_user_func( $callback, $name ); } /** * Get the name of the transient that stores the lock status. * * @since 1.9 * * @return string|bool The name on success. False on failure. */ protected function get_lock_name() { $media = $this->get_media(); if ( ! $media ) { return false; } /** * Note that the site transient used by WP Background is named '*_process_lock'. * That would give something like 'imagify_optimize_media_process_lock' for the optimization process, while here it would be 'imagify_wp_42_process_locked'. */ return sprintf( static::LOCK_NAME, $media->get_context(), $media->get_id() ); } /** * Validate the lock action. * * @since 1.9 * * @param string $action The action performed behind this lock: 'optimizing' or 'restoring'. * @return string The valid action. */ protected function validate_lock_action( $action ) { switch ( $action ) { case 'restore': case 'restoring': $action = 'restoring'; break; default: $action = 'optimizing'; } return $action; } /** * Tell if a size already has optimization data. * * @since 1.9 * * @param string $size The size name. * @return bool */ public function size_has_optimization_data( $size ) { $data = $this->get_data()->get_optimization_data(); return ! empty( $data['sizes'][ $size ] ); } /** * Update the optimization data for a size. * * @since 1.9 * @since 2.2 - Addition of the format in the call. * * @param object $response The API response. * @param string $size The size name. * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array { * The optimization data. * * @type int $level The optimization level. * @type string $status The status: 'success', 'already_optimized', 'error'. * @type bool $success True if successfully optimized. False on error or if already optimized. * @type string $error An error message. * @type int $original_size The weight of the file, before optimization. * @type int $optimized_size The weight of the file, once optimized. * } */ public function update_size_optimization_data( $response, $size, $level ) { $disabled = false; $data = $this->data_format; $data['level'] = is_numeric( $level ) ? (int) $level : $this->get_option( 'optimization_level' ); if ( is_wp_error( $response ) ) { /** * Error. */ $disabled = 'unauthorized_size' === $response->get_error_code(); // Size data. $data['success'] = false; $data['error'] = $response->get_error_message(); // Status. if ( false !== strpos( $data['error'], 'This image is already compressed' ) ) { $data['status'] = 'already_optimized'; } else { $data['status'] = 'error'; } } else { /** * Success. */ $response = (object) array_merge( [ 'original_size' => 0, 'new_size' => 0, 'percent' => 0, ], (array) $response ); // Status. $data['status'] = 'success'; $data['error'] = null; // Size data. $data['success'] = true; if ( property_exists( $response, 'message' ) ) { $data['message'] = imagify_translate_api_message( $response->message ); } $data['original_size'] = $response->original_size; $data['optimized_size'] = $response->new_size; } $_unauthorized = $disabled ? '_unauthorized' : ''; /** * Filter the optimization data. * * @since 1.9 * * @param array $data { * The optimization data. * * @type int $level The optimization level. * @type string $status The status: 'success', 'already_optimized', 'error'. * @type bool $success True if successfully optimized. False on error or if already optimized. * @type string $error An error message. * @type int $original_size The weight of the file, before optimization. * @type int $optimized_size The weight of the file, once optimized. * } * @param object $response The API response. * @param string $size The size name. * @param int $level The optimization level. * @param object $media_data The DataInterface instance of the media. */ $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() ); if ( property_exists( $response, 'message' ) ) { $size = str_replace( $this->format, '', $size ); } // Store. $this->get_data()->update_size_optimization_data( $size, $data ); return $data; } /** * Get a plugin’s option. * * @since 1.9 * * @param string $option_name The option name. * * @return mixed */ protected function get_option( $option_name ) { if ( isset( $this->options[ $option_name ] ) ) { return $this->options[ $option_name ]; } $this->options[ $option_name ] = get_imagify_option( $option_name ); return $this->options[ $option_name ]; } /** * Sanitize and validate an optimization level. * If not provided (false, null), fallback to the level set in the plugin's settings. * * @since 1.9 * * @param mixed $optimization_level The optimization level. * * @return int */ protected function sanitize_optimization_level( $optimization_level ) { if ( ! is_numeric( $optimization_level ) ) { if ( $this->get_option( 'lossless' ) ) { return 0; } return $this->get_option( 'optimization_level' ); } return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level ); } /** * Tell if the media has AVIF versions. * * @since 2.2 * * @return bool */ public function has_avif() { if ( ! $this->is_valid() ) { return false; } if ( ! $this->get_media()->is_image() ) { return false; } $data = $this->get_data()->get_optimization_data(); if ( empty( $data['sizes'] ) ) { return false; } $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;'; $data = maybe_serialize( $data['sizes'] ); return is_string( $data ) && strpos( $data, $needle ); } } Optimization/Process/Noop.php 0000644 00000022172 15174671731 0012314 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Optimization\Process; use Imagify\Optimization\Data\DataInterface; use Imagify\Media\MediaInterface; use Imagify\Optimization\File; use WP_Error; /** * Fallback class to optimize medias. */ class Noop implements ProcessInterface { /** * The suffix used in the thumbnail size name. * * @var string * @since 1.9 */ const WEBP_SUFFIX = '@imagify-webp'; /** * The suffix used in the thumbnail size name. * * @var string * @since 2.2 */ const AVIF_SUFFIX = '@imagify-avif'; /** * The suffix used in file name to create a temporary copy of the full size. * * @var string * @since 1.9 */ const TMP_SUFFIX = '@imagify-tmp'; /** * Used for the name of the transient telling if a media is locked. * %1$s is the context, %2$s is the media ID. * * @var string * @since 1.9 */ const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked'; /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * * @param mixed $id Whatever. * * @return bool */ public static function constructor_accepts( $id ) { return false; } /** * Get the data instance. * * @since 1.9 * * @return DataInterface|false */ public function get_data() { return false; } /** * Get the media instance. * * @since 1.9 * * @return MediaInterface|false */ public function get_media() { return false; } /** * Get the File instance of the original file. * * @since 1.9.8 * * @return File|false */ public function get_original_file() { return false; } /** * Get the File instance of the full size file. * * @since 1.9.8 * * @return File|false */ public function get_fullsize_file() { return false; } /** * Get the File instance. * * @since 1.9 * * @return File|false */ public function get_file() { return false; } /** * Tell if the current media is valid. * * @since 1.9 * * @return bool */ public function is_valid() { return false; } /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * * @return bool */ public function current_user_can( $describer ) { return false; } /** * Optimize a media files by pushing tasks into the queue. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize( $optimization_level = null ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Re-optimize a media files with a different level. * * @since 1.9 * * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function reoptimize( $optimization_level = null ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Optimize several file sizes by pushing tasks into the queue. * * @since 1.9 * * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Optimize one file with Imagify directly. * * @since 1.9 * * @param string $size The media size. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array|WP_Error The optimization data. A WP_Error instance on failure. */ public function optimize_size( $size, $optimization_level = null ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Restore the media files from the backup file. * * @since 1.9 * * @return bool|WP_Error True on success. A WP_Error instance on failure. */ public function restore() { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Get the sizes for this media that have not get through optimization. * No sizes are returned if the file is not optimized, has no backup, or is not an image. * The 'full' size os never returned. * * @since 1.9 * * @return array|WP_Error { * A WP_Error object on failure. * An array of data for the thumbnail sizes on success. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * @type string $file The name the thumbnail "should" have. * } */ public function get_missing_sizes() { return []; } /** * Optimize missing thumbnail sizes. * * @since 1.9 * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize_missing_thumbnails() { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Delete the backup file. * * @since 1.9 */ public function delete_backup() {} /** * Maybe resize an image. * * @since 1.9 * * @param string $size The size name. * @param File $file A File instance. * * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. * @type int $file_size The file size in bytes. * } */ public function maybe_resize( $size, $file ) { return [ 'resized' => false, 'backuped' => false, 'file_size' => 0, ]; } /** * Generate Nextgen images if they are missing. * * @since 1.9 * * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function generate_nextgen_versions() { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Delete the next gen format images. * This doesn't delete the related optimization data. * * @since 2.2 * * @param bool $keep_full Set to true to keep the full size. * @return bool|WP_Error True on success. A WP_Error object on failure. */ public function delete_nextgen_files( $keep_full = false ) { return false; } /** * Tell if a thumbnail size is an "Imagify Next-Gen" size. * * @since 2.2 * * @param string $size_name The size name. * * @return string|bool The unsuffixed name of the size if Next-Gen. False if not a Next-Gen. */ public function is_size_next_gen( $size_name ) { return false; } /** * Tell if the media has all next-gen versions. * * @return bool */ public function is_full_next_gen() { return false; } /** * Tell if the media has a next-gen format. * * @since 2.2 * * @return bool */ public function has_next_gen() { return false; } /** * Tell if a process is running for this media. * * @since 1.9 * * @return bool */ public function is_locked() { return false; } /** * Set the running status to "running" for a period of time. * * @since 1.9 */ public function lock() {} /** * Delete the running status. * * @since 1.9 */ public function unlock() {} /** * Tell if a size already has optimization data. * * @since 1.9 * * @param string $size The size name. * * @return bool */ public function size_has_optimization_data( $size ) { return false; } /** * Update the optimization data for a size. * * @since 1.9 * * @param object $response The API response. * @param string $size The size name. * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * * @return array { * The optimization data. * * @type string $size The size name. * @type int $level The optimization level. * @type string $status The status: 'success', 'already_optimized', 'error'. * @type bool $success True if successfully optimized. False on error or if already optimized. * @type string $error An error message. * @type int $original_size The weight of the file, before optimization. * @type int $optimized_size The weight of the file, once optimized. * } */ public function update_size_optimization_data( $response, $size, $level ) { return [ 'size' => 'noop', 'level' => false, 'status' => '', 'success' => false, 'error' => '', 'original_size' => 0, 'optimized_size' => 0, ]; } } Optimization/Process/CustomFolders.php 0000644 00000006365 15174671731 0014200 0 ustar 00 <?php namespace Imagify\Optimization\Process; use WP_Error; /** * Optimization class for the custom folders. * This class constructor accepts: * - A post ID (int). * - An array of data coming from the files DB table /!\ * - An object of data coming from the files DB table /!\ * - A \Imagify\Media\MediaInterface object. * - A \Imagify\Media\DataInterface object. * * @since 1.9 * @see Imagify\Media\CustomFolders * @author Grégory Viguier */ class CustomFolders extends AbstractProcess { /** * Restore the thumbnails. * This context has no thumbnails. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ protected function restore_thumbnails() { return true; } /** ----------------------------------------------------------------------------------------- */ /** MISSING THUMBNAILS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the sizes for this media that have not get through optimization. * Since this context has no thumbnails, this will always return an empty array, unless an error is triggered. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array|WP_Error A WP_Error object on failure. An empty array on success: this context has no thumbnails. * The tests are kept for consistency. */ public function get_missing_sizes() { // The media must have been optimized once and have a backup. if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media ) { return new WP_Error( 'no_media', __( 'No media found.', 'imagify' ) ); } if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $data = $this->get_data(); if ( ! $data->is_optimized() ) { return new WP_Error( 'media_not_optimized', __( 'This media is not optimized yet.', 'imagify' ) ); } if ( ! $media->has_backup() ) { return new WP_Error( 'no_backup', __( 'This file has no backup file.', 'imagify' ) ); } if ( ! $media->is_image() ) { return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) ); } return []; } /** * Optimize missing thumbnail sizes. * Since this context has no thumbnails, this will always return a \WP_Error object. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_missing_thumbnails() { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media ) { return new WP_Error( 'no_media', __( 'No media found.', 'imagify' ) ); } if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } return new WP_Error( 'no_sizes', __( 'No thumbnails seem to be missing.', 'imagify' ) ); } } Optimization/Process/WP.php 0000644 00000020501 15174671731 0011721 0 ustar 00 <?php namespace Imagify\Optimization\Process; use Imagify\Optimization\File; use WP_Error; /** * Optimization class for the attachments in the WP library. * This class constructor accepts: * - A post ID (int). * - A \WP_Post object. * - A \Imagify\Media\MediaInterface object. * - A \Imagify\Media\DataInterface object. * * @since 1.9 * @author Grégory Viguier */ class WP extends AbstractProcess { /** ----------------------------------------------------------------------------------------- */ /** MISSING THUMBNAILS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the sizes for this media that have not get through optimization. * No sizes are returned if the file is not optimized, has no backup, or is not an image. * The 'full' size os never returned. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array|WP_Error { * A WP_Error object on failure. * An array of data for the thumbnail sizes on success. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * @type string $file The name the thumbnail "should" have. * } */ public function get_missing_sizes() { // The media must have been optimized once and have a backup. if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media ) { return new WP_Error( 'no_media', __( 'No media found.', 'imagify' ) ); } if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $data = $this->get_data(); if ( ! $data->is_optimized() ) { return new WP_Error( 'media_not_optimized', __( 'This media is not optimized yet.', 'imagify' ) ); } if ( ! $media->has_backup() ) { return new WP_Error( 'no_backup', __( 'This file has no backup file.', 'imagify' ) ); } if ( ! $media->is_image() ) { return new WP_Error( 'media_not_an_image', __( 'This media is not an image.', 'imagify' ) ); } // Compare registered sizes and optimized sizes. $context_sizes = $media->get_context_instance()->get_thumbnail_sizes(); $optimized_sizes = $data->get_optimization_data(); $missing_sizes = array_diff_key( $context_sizes, $optimized_sizes['sizes'] ); if ( ! $missing_sizes ) { // We have everything we need. return []; } $media_sizes = $media->get_media_files(); $full_size = $media_sizes['full']; if ( ! $full_size['path'] || ! $full_size['width'] || ! $full_size['height'] ) { return []; } $file_name = $this->filesystem->path_info( $full_size['path'] ); $file_name = $file_name['file_base'] . '-{%suffix%}.' . $file_name['extension']; // Test if the missing sizes are needed. foreach ( $missing_sizes as $size_name => $size_data ) { if ( $full_size['width'] === $size_data['width'] && $full_size['height'] === $size_data['height'] ) { // Same dimensions as the full size. unset( $missing_sizes[ $size_name ] ); continue; } if ( ! empty( $media_sizes[ $size_name ]['disabled'] ) ) { // This size must not be optimized. unset( $missing_sizes[ $size_name ] ); continue; } $resize_result = image_resize_dimensions( $full_size['width'], $full_size['height'], $size_data['width'], $size_data['height'], $size_data['crop'] ); if ( ! $resize_result ) { // This thumbnail is not needed, it is smaller than this size. unset( $missing_sizes[ $size_name ] ); continue; } // Provide what should be the file name. list( , , , , $new_width, $new_height ) = $resize_result; $missing_sizes[ $size_name ]['file'] = str_replace( '{%suffix%}', "{$new_width}x{$new_height}", $file_name ); } return $missing_sizes; } /** * Optimize missing thumbnail sizes. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_missing_thumbnails() { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } $media = $this->get_media(); if ( ! $media ) { return new WP_Error( 'no_media', __( 'No media found.', 'imagify' ) ); } if ( ! $media->is_supported() ) { return new WP_Error( 'media_not_supported', __( 'This media is not supported.', 'imagify' ) ); } $missing_sizes = $this->get_missing_sizes(); if ( ! $missing_sizes ) { return new WP_Error( 'no_sizes', __( 'No thumbnails seem to be missing.', 'imagify' ) ); } if ( is_wp_error( $missing_sizes ) ) { return $missing_sizes; } if ( $this->is_locked() ) { return new WP_Error( 'media_locked', __( 'This media is already being processed.', 'imagify' ) ); } $this->lock(); // Create the missing thumbnails. $sizes = $this->create_missing_thumbnails( $missing_sizes ); if ( ! $sizes ) { $this->unlock(); return new WP_Error( 'thumbnail_creation_failed', __( 'The thumbnails failed to be created.', 'imagify' ) ); } $optimization_level = $this->get_data()->get_optimization_level(); if ( false === $optimization_level ) { $this->unlock(); return new \WP_Error( 'optimization_level_not_set', __( 'The optimization level of this media seems to have disappear from the database. You should restore this media and then launch a new optimization.', 'imagify' ) ); } $args = [ 'hook_suffix' => 'optimize_missing_thumbnails', 'locked' => true, ]; // Optimize. return $this->optimize_sizes( array_keys( $sizes ), $optimization_level, $args ); } /** * Create all missing thumbnails if they don't exist and update the attachment metadata. * * @since 1.9 * @access protected * @see $this->get_missing_sizes() * @author Grégory Viguier * * @param array $missing_sizes array { * An array of data for the thumbnail sizes on success. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * @type string $file The name the thumbnail "should" have. * } * @return array { * An array of thumbnail data (those without errors): * * @type string $file File name. * @type int $width The image width. * @type int $height The image height. * @type string $mime-type The mime type. * } */ protected function create_missing_thumbnails( $missing_sizes ) { if ( ! $missing_sizes ) { return []; } $media = $this->get_media(); if ( ! $media ) { return []; } $media_id = $media->get_id(); $metadata = wp_get_attachment_metadata( $media_id ); $metadata['sizes'] = ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ? $metadata['sizes'] : []; $destination_dir = $this->filesystem->dir_path( $media->get_raw_fullsize_path() ); $backup_file = new File( $media->get_backup_path() ); $without_errors = []; $has_new_data = false; // Create the missing thumbnails. foreach ( $missing_sizes as $size_name => $thumbnail_data ) { // The path to the destination file. $thumbnail_data['path'] = $destination_dir . $thumbnail_data['file']; if ( ! $this->filesystem->exists( $thumbnail_data['path'] ) ) { $result = $backup_file->create_thumbnail( $thumbnail_data ); if ( is_array( $result ) ) { // New file. $metadata['sizes'][ $size_name ] = $result; $has_new_data = true; } } else { $result = true; } if ( ! empty( $metadata['sizes'][ $size_name ] ) && ! is_wp_error( $result ) ) { // phpstan-ignore-line // Not an error. $without_errors[ $size_name ] = $metadata['sizes'][ $size_name ]; } } // Save the new data into the attachment metadata. if ( $has_new_data ) { /** * Here we don't use wp_update_attachment_metadata() to prevent triggering unwanted hooks. */ update_post_meta( $media_id, '_wp_attachment_metadata', $metadata ); } return $without_errors; } } Optimization/File.php 0000644 00000057416 15174671731 0010653 0 ustar 00 <?php namespace Imagify\Optimization; use Imagify_Requirements; use WP_Error; /** * A generic optimization class focused on the file itself. * * @since 1.9 * @author Grégory Viguier */ class File { /** * Absolute path to the file. * * @var string * @since 1.9 * @author Grégory Viguier */ protected $path; /** * Tell if the file is an image. * * @var bool * @since 1.9 * @see $this->is_image() * @author Grégory Viguier */ protected $is_image; /** * Store the file mime type + file extension (if the file is supported). * * @var array * @since 1.9 * @see $this->get_file_type() * @author Grégory Viguier */ protected $file_type; /** * Filesystem object. * * @var \Imagify_Filesystem * @since 1.9 * @author Grégory Viguier */ protected $filesystem; /** * The editor instance used to resize the file. * * @var \WP_Image_Editor_Imagick|\WP_Image_Editor_GD|WP_Error. * @since 1.9 * @author Grégory Viguier */ protected $editor; /** * Used to cache the plugin’s options. * * @var array * @since 1.9 * @author Grégory Viguier */ protected $options = []; /** * The constructor. * * @since 1.9 * @author Grégory Viguier * * @param string $file_path Absolute path to the file. */ public function __construct( $file_path ) { $this->path = $file_path; $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Tell if the file is valid. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_valid() { return (bool) $this->path; } /** * Tell if the file can be processed. * * @since 1.9 * @author Grégory Viguier * * @return bool|WP_Error */ public function can_be_processed() { if ( ! $this->path ) { return new \WP_Error( 'empty_path', __( 'File path is empty.', 'imagify' ) ); } if ( ! empty( $this->filesystem->errors->errors ) ) { return new \WP_Error( 'filesystem_error', __( 'Filesystem error.', 'imagify' ), $this->filesystem->errors ); } if ( ! $this->filesystem->exists( $this->path ) ) { return new \WP_Error( 'not_exists', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to exist.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>' ) ); } if ( ! $this->filesystem->is_file( $this->path ) ) { return new \WP_Error( 'not_a_file', sprintf( /* translators: %s is a file path. */ __( 'This does not seem to be a file: %s.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>' ) ); } if ( ! $this->filesystem->is_writable( $this->path ) ) { return new \WP_Error( 'not_writable', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>' ) ); } $parent_folder = $this->filesystem->dir_path( $this->path ); if ( ! $this->filesystem->is_writable( $parent_folder ) ) { return new \WP_Error( 'folder_not_writable', sprintf( /* translators: %s is a file path. */ __( 'The folder %s does not seem to be writable.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $parent_folder ) ) . '</code>' ) ); } return true; } /** ----------------------------------------------------------------------------------------- */ /** EDITION ================================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Resize (and rotate) an image if it is bigger than the maximum width provided. * * @since 1.9 * @author Grégory Viguier * @author Remy Perona * * @param array $dimensions { * Array of image dimensions. * * @type int $width The image width. * @type int $height The image height. * } * @param int $max_width Maximum width to resize to. * @return string|WP_Error Path the the resized image. A WP_Error object on failure. */ public function resize( $dimensions = [], $max_width = 0 ) { $can_be_processed = $this->can_be_processed(); if ( is_wp_error( $can_be_processed ) ) { return $can_be_processed; } if ( ! $max_width ) { return new \WP_Error( 'no_resizing_threshold', __( 'No threshold provided for resizing.', 'imagify' ) ); } if ( ! $this->is_image() ) { return new \WP_Error( 'not_an_image', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>' ) ); } $editor = $this->get_editor(); if ( is_wp_error( $editor ) ) { return $editor; } // Try to correct the auto-rotation if the info is available. if ( $this->filesystem->can_get_exif() && 'image/jpeg' === $this->get_mime_type() ) { $exif = $this->filesystem->get_image_exif( $this->path ); $orientation = isset( $exif['Orientation'] ) ? (int) $exif['Orientation'] : 1; switch ( $orientation ) { case 2: // Flip horizontally. $editor->flip( true, false ); break; case 3: // Rotate 180 degrees or flip horizontally and vertically. // Flipping seems faster/uses less resources. $editor->flip( true, true ); break; case 4: // Flip vertically. $editor->flip( false, true ); break; case 5: // Rotate 90 degrees counter-clockwise and flip vertically. $result = $editor->rotate( 90 ); if ( ! is_wp_error( $result ) ) { $editor->flip( false, true ); } break; case 6: // Rotate 90 degrees clockwise (270 counter-clockwise). $editor->rotate( 270 ); break; case 7: // Rotate 90 degrees counter-clockwise and flip horizontally. $result = $editor->rotate( 90 ); if ( ! is_wp_error( $result ) ) { $editor->flip( true, false ); } break; case 8: // Rotate 90 degrees counter-clockwise. $editor->rotate( 90 ); break; } } if ( ! $dimensions ) { $dimensions = $this->get_dimensions(); } // Prevent removal of the exif data when resizing (only works with Imagick). add_filter( 'image_strip_meta', '__return_false', 789 ); // Resize. $new_sizes = wp_constrain_dimensions( $dimensions['width'], $dimensions['height'], $max_width ); $resized = $editor->resize( $new_sizes[0], $new_sizes[1], false ); // Remove the filter when we're done to prevent any conflict. remove_filter( 'image_strip_meta', '__return_false', 789 ); if ( is_wp_error( $resized ) ) { return $resized; } $resized_image_path = $editor->generate_filename( 'imagifyresized' ); $resized_image_saved = $editor->save( $resized_image_path ); if ( is_wp_error( $resized_image_saved ) ) { return $resized_image_saved; } return $resized_image_path; } /** * Create a thumbnail. * Warning: If the destination file already exists, it will be overwritten. * * @since 1.9 * @author Grégory Viguier * * @param array $destination { * The thumbnail data. * * @type string $path Path to the destination file. * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type bool $adjust_filename True to adjust the file name like what `$editor->multi_resize()` returns, like WP default behavior (default). False to prevent it, and use the file name from $path instead. * } * @return bool|array|WP_Error { * A WP_Error object on error. True if the file exists. * An array of thumbnail data if the file has just been created: * * @type string $file File name. * @type int $width The image width. * @type int $height The image height. * @type string $mime-type The mime type. * } */ public function create_thumbnail( $destination ) { $can_be_processed = $this->can_be_processed(); if ( is_wp_error( $can_be_processed ) ) { return $can_be_processed; } if ( ! $this->is_image() ) { return new WP_Error( 'not_an_image', sprintf( /* translators: %s is a file path. */ __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ), '<code>' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '</code>' ) ); } $editor = $this->get_editor(); if ( is_wp_error( $editor ) ) { return $editor; } // Create the file. $result = $editor->multi_resize( [ $destination ] ); if ( ! $result ) { return new WP_Error( 'image_resize_error', __( 'The thumbnail could not be created.', 'imagify' ) ); } $result = reset( $result ); $filename = $result['file']; $source_thumb_path = $this->filesystem->dir_path( $this->path ) . $filename; if ( ! isset( $destination['adjust_filename'] ) || $destination['adjust_filename'] ) { // The file name can change from what we expected (1px wider, etc), let's use the resulting data to move the file to the right place. $destination_thumb_path = $this->filesystem->dir_path( $destination['path'] ) . $filename; } else { // Respect what is set in $path. $destination_thumb_path = $destination['path']; $result['file'] = $this->filesystem->file_name( $destination['path'] ); } if ( $source_thumb_path === $destination_thumb_path ) { return $result; } $moved = $this->filesystem->move( $source_thumb_path, $destination_thumb_path, true ); if ( ! $moved ) { return new WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) ); } return $result; } /** * Backup a file. * * @since 1.9 * @since 1.9.8 Added $backup_source argument. * @author Grégory Viguier * * @param string $backup_path The backup path. * @param string $backup_source Path to the file to backup. This is useful in WP 5.3+ when we want to optimize the full size: in that case we need to backup the original file. * @return bool|WP_Error True on success. False if the backup option is disabled. A WP_Error object on failure. */ public function backup( $backup_path = null, $backup_source = null ) { $can_be_processed = $this->can_be_processed(); if ( is_wp_error( $can_be_processed ) ) { return $can_be_processed; } // Make sure the backups directory has no errors. if ( ! $backup_path ) { return new \WP_Error( 'wp_upload_error', __( 'Error while retrieving the backups directory path.', 'imagify' ) ); } // Create sub-directories. $created = $this->filesystem->make_dir( $this->filesystem->dir_path( $backup_path ) ); if ( ! $created ) { return new \WP_Error( 'backup_dir_not_writable', __( 'The backup directory is not writable.', 'imagify' ) ); } $path = $backup_source && $this->filesystem->exists( $backup_source ) ? $backup_source : $this->path; /** * Allow to overwrite the backup file if it already exists. * * @since 1.6.9 * @author Grégory Viguier * * @param bool $overwrite Whether to overwrite the backup file. * @param string $path The file path. * @param string $backup_path The backup path. */ $overwrite = apply_filters( 'imagify_backup_overwrite_backup', false, $path, $backup_path ); // Copy the file. $this->filesystem->copy( $path, $backup_path, $overwrite, FS_CHMOD_FILE ); // Make sure the backup copy exists. if ( ! $this->filesystem->exists( $backup_path ) ) { return new \WP_Error( 'backup_doesnt_exist', __( 'The file could not be saved.', 'imagify' ), [ 'file_path' => $this->filesystem->make_path_relative( $path ), 'backup_path' => $this->filesystem->make_path_relative( $backup_path ), ] ); } // Check if a '-scaled' version of the image exists. $scaled_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_source ); if ( $this->filesystem->exists( $scaled_path ) ) { // Create a backup path for the scaled image. $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path ); // Copy the '-scaled' version to the backup. $this->filesystem->copy( $scaled_path, $scaled_backup_path, $overwrite, FS_CHMOD_FILE ); if ( ! $this->filesystem->exists( $scaled_backup_path ) ) { return new \WP_Error( 'backup_doesnt_exist', __( 'The file could not be saved.', 'imagify' ), [ 'file_path' => $this->filesystem->make_path_relative( $scaled_path ), 'backup_path' => $this->filesystem->make_path_relative( $scaled_backup_path ), ] ); } } return true; } /** * Optimize a file with Imagify. * * @since 1.9 * @author Grégory Viguier * * @param array $args { * Optional. An array of arguments. * * @type bool $backup False to prevent backup. True to follow the user's setting. A backup can't be forced. * @type string $backup_path If a backup must be done, this is the path to use. Default is the backup path used for the WP Media Library. * @type int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). * @type string $convert Set to 'webp' to convert the image to WebP, 'avif' to convert image to AVIF. * @type string $context The context. * @type int $original_size The file size, sent to the API. * } * @return \sdtClass|\WP_Error Optimized image data. A \WP_Error object on error. */ public function optimize( $args = [] ) { $args = array_merge( [ 'backup' => true, 'backup_path' => null, 'backup_source' => null, 'optimization_level' => 0, 'convert' => '', 'context' => 'wp', 'original_size' => 0, ], $args ); $can_be_processed = $this->can_be_processed(); if ( is_wp_error( $can_be_processed ) ) { return $can_be_processed; } // Check if external HTTP requests are blocked. if ( Imagify_Requirements::is_imagify_blocked() ) { return new \WP_Error( 'http_block_external', __( 'External HTTP requests are blocked.', 'imagify' ) ); } /** * Fires before a media file optimization. * * @since 1.9 * @author Grégory Viguier * * @param string $path Absolute path to the media file. * @param array $args Arguments passed to the method. */ do_action( 'imagify_before_optimize_file', $this->path, $args ); /** * Fires before to optimize the Image with Imagify. * * @since 1.0 * @deprecated * * @param string $path Absolute path to the image file. * @param bool $backup True if a backup will be make. */ do_action_deprecated( 'before_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' ); if ( $args['backup'] ) { $backup_result = $this->backup( $args['backup_path'], $args['backup_source'] ); if ( is_wp_error( $backup_result ) ) { // Stop the process if we can't backup the file. return $backup_result; } } // Send file for optimization and fetch the response. $data = [ 'normal' => 0 === $args['optimization_level'], 'aggressive' => 1 === $args['optimization_level'], 'ultra' => 2 === $args['optimization_level'], 'keep_exif' => true, 'original_size' => $args['original_size'], 'context' => $args['context'], ]; if ( $args['convert'] ) { $data['convert'] = $args['convert']; $format = $args['convert']; } $response = upload_imagify_image( [ 'image' => $this->path, 'data' => wp_json_encode( $data ), ] ); if ( is_wp_error( $response ) ) { return new \WP_Error( 'api_error', $response->get_error_message() ); } if ( ! function_exists( 'download_url' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $temp_file = download_url( $response->image ); if ( is_wp_error( $temp_file ) ) { return new \WP_Error( 'temp_file_not_found', $temp_file->get_error_message() ); } if ( property_exists( $response, 'message' ) ) { $args['convert'] = ''; } $formats = [ 'webp', 'avif', ]; if ( in_array( $args['convert'], $formats, true ) ) { $destination_path = $this->get_path_to_nextgen( $args['convert'] ); $this->path = $destination_path; $this->file_type = null; $this->editor = null; } else { $destination_path = $this->path; } $moved = $this->filesystem->move( $temp_file, $destination_path, true ); if ( ! $moved ) { return new \WP_Error( 'move_error', __( 'The file could not be moved to its final destination.', 'imagify' ) ); } /** * Fires after to optimize the Image with Imagify. * * @since 1.0 * @deprecated * * @param string $path Absolute path to the image file. * @param bool $backup True if a backup has been made. */ do_action_deprecated( 'after_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' ); /** * Fires after a media file optimization. * * @since 1.9 * @author Grégory Viguier * * @param string $path Absolute path to the media file. * @param array $args Arguments passed to the method. */ do_action( 'imagify_after_optimize_file', $this->path, $args ); return $response; } /** ----------------------------------------------------------------------------------------- */ /** IMAGE EDITOR (GD/IMAGEMAGICK) =========================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get an image editor instance (WP_Image_Editor_Imagick, WP_Image_Editor_GD). * * @since 1.9 * @author Grégory Viguier * * @return WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error */ protected function get_editor() { if ( isset( $this->editor ) ) { return $this->editor; } $this->editor = wp_get_image_editor( $this->path, [ 'methods' => $this->get_editor_methods(), ] ); if ( ! is_wp_error( $this->editor ) ) { return $this->editor; } $this->editor = new \WP_Error( 'image_editor', sprintf( /* translators: %1$s is an error message, %2$s is a "More info?" link. */ __( 'No php extensions are available to edit images on the server. ImageMagick or GD is required. The internal error is: %1$s. %2$s', 'imagify' ), $this->editor->get_error_message(), '<a href="' . esc_url( imagify_get_external_url( 'documentation-imagick-gd' ) ) . '" target="_blank">' . __( 'More info?', 'imagify' ) . '</a>' ) ); return $this->editor; } /** * Get the image editor methods we will use. * * @since 1.9 * @author Grégory Viguier * * @return array */ protected function get_editor_methods() { static $methods; if ( isset( $methods ) ) { return $methods; } $methods = [ 'resize', 'multi_resize', 'generate_filename', 'save', ]; if ( $this->filesystem->can_get_exif() ) { $methods[] = 'rotate'; } return $methods; } /** ----------------------------------------------------------------------------------------- */ /** VARIOUS TOOLS =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Check if a file exceeds the weight limit (> 5mo). * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_exceeded() { if ( ! $this->is_valid() ) { return false; } $size = $this->filesystem->size( $this->path ); return $size > IMAGIFY_MAX_BYTES; } /** * Tell if the current file is supported for a given context. * * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier * * @param array $allowed_mime_types A list of allowed mime types. * @return bool */ public function is_supported( $allowed_mime_types ) { return in_array( $this->get_mime_type(), $allowed_mime_types, true ); } /** * Tell if the file is an image. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_image() { if ( isset( $this->is_image ) ) { return $this->is_image; } $this->is_image = strpos( $this->get_mime_type(), 'image/' ) === 0; return $this->is_image; } /** * Tell if the file is a pdf. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_pdf() { return 'application/pdf' === $this->get_mime_type(); } /** * Get the file mime type. * * @since 1.9 * @author Grégory Viguier * * @return string */ public function get_mime_type() { return $this->get_file_type()->type; } /** * Get the file extension. * * @since 1.9 * * @return string|false */ public function get_extension() { return $this->get_file_type()->ext; } /** * Get the file path. * * @since 1.9 * @author Grégory Viguier * * @return string */ public function get_path() { return $this->path; } /** * Replace the file extension by WebP. * * @since 1.9 * @author Grégory Viguier * * @return string|bool The file path on success. False if not an image or on failure. */ public function get_path_to_webp() { if ( ! $this->is_image() ) { return false; } if ( $this->is_webp() ) { return false; } return imagify_path_to_webp( $this->path ); } /** * Replace the file extension by its next-gen format extension. * * @since 2.2 * * @param string $format the format we are targeting. * @return string|bool The file path on success. False if not an image or on failure. */ public function get_path_to_nextgen( string $format ) { if ( ! $this->is_image() ) { return false; } if ( $this->is_webp() || $this->is_avif() ) { return false; } return imagify_path_to_nextgen( $this->path, $format ); } /** * Tell if the file is a WebP image. * Rejects "path/to/.webp" files. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_webp() { return preg_match( '@(?!^|/|\\\)\.webp$@i', $this->path ); } /** * Tell if the file is an AVIF image. * Rejects "path/to/.avif" files. * * @since 2.2 * * @return bool */ public function is_avif() { return preg_match( '@(?!^|/|\\\)\.avif$@i', $this->path ); } /** * Get the file mime type + file extension. * * @since 1.9 * @see wp_check_filetype() * @author Grégory Viguier * * @return object { * @type string $ext The file extension. * @type string $type The mime type. * } */ protected function get_file_type() { if ( isset( $this->file_type ) ) { return $this->file_type; } $this->file_type = (object) [ 'ext' => '', 'type' => '', ]; if ( ! $this->is_valid() ) { return $this->file_type; } $this->file_type = (object) wp_check_filetype( $this->path ); return $this->file_type; } /** * If the media is an image, get its width and height. * * @since 1.9 * @author Grégory Viguier * * @return array */ public function get_dimensions() { if ( ! $this->is_image() ) { return [ 'width' => 0, 'height' => 0, ]; } $values = $this->filesystem->get_image_size( $this->path ); if ( empty( $values ) ) { return [ 'width' => 0, 'height' => 0, ]; } return [ 'width' => $values['width'], 'height' => $values['height'], ]; } /** * Get a plugin’s option. * * @since 1.9 * @author Grégory Viguier * * @param string $option_name The option nme. * @return mixed */ protected function get_option( $option_name ) { if ( isset( $this->options[ $option_name ] ) ) { return $this->options[ $option_name ]; } $this->options[ $option_name ] = get_imagify_option( $option_name ); return $this->options[ $option_name ]; } } Optimization/Data/DataInterface.php 0000644 00000016657 15174671731 0013341 0 ustar 00 <?php namespace Imagify\Optimization\Data; use Imagify\Media\MediaInterface; /** * Interface to use to handle the optimization data of "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ interface DataInterface { /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ); /** * Get the media instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return MediaInterface|false */ public function get_media(); /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid(); /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION DATA ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_optimized(); /** * Check if the main file is optimized (NOT by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_already_optimized(); /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_error(); /** * Get the whole media optimization data. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * The data. * * @type string $status The optimization status of the whole media: 'success', 'already_optimized', or 'error'. * It is the same as the main file’s status. * @type int|bool $level The optimization level (0=normal, 1=aggressive, 2=ultra). False if not set. * @type array $sizes { * A list of size data, keyed by size name, and containing: * * @type bool $success Whether the optimization has been successful. * If a success: * @type int $original_size The file size before optimization. * @type int $optimized_size The file size after optimization. * @type int $percent Saving in percent. * If an error or 'already_optimized': * @type string $error An error message. * } * @type array $stats { * @type int $original_size Overall size before optimization. * @type int $optimized_size Overall size after optimization. * @type int $percent Overall saving in percent. * } * } */ public function get_optimization_data(); /** * Update the optimization data, level, and status for a size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The size name. * @param array $data { * The optimization data. * * @type int $level The optimization level. * @type string $status The status: 'success', 'already_optimized', 'error'. * @type bool $success True if successfully optimized. False on error or if already optimized. * @type string $error An error message. * @type int $original_size The weight of the file, before optimization. * @type int $optimized_size The weight of the file, after optimization. * } */ public function update_size_optimization_data( $size, array $data ); /** * Delete the media optimization data, level, and status. * * @since 1.9 * @access public * @author Grégory Viguier */ public function delete_optimization_data(); /** * Delete the optimization data for the given sizes. * If all sizes are removed, all optimization data is deleted. * Status and level are not modified nor removed if the "full" size is removed. This leaves the media in a Schrödinger state. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @param array $sizes A list of sizes to remove. */ public function delete_sizes_optimization_data( array $sizes ); /** * Get the media's optimization level. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int|bool The optimization level. False if not optimized. */ public function get_optimization_level(); /** * Get the media's optimization status (success or error). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string The optimization status. An empty string if there is none. */ public function get_optimization_status(); /** * Count number of optimized sizes. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int Number of optimized sizes. */ public function get_optimized_sizes_count(); /** * Get the original media's size (weight). * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_original_size( $human_format = true, $decimals = 2 ); /** * Get the file size of the full size file. * If the Nextgen size is available, it is used. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @param bool $use_nextgen Use the Nextgen size if available. * @return string|int */ public function get_optimized_size( $human_format = true, $decimals = 2, $use_nextgen = true ); /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION STATS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get one or all statistics of a specific size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The thumbnail slug. * @param string $key The specific data slug. * @return array|string */ public function get_size_data( $size = 'full', $key = '' ); /** * Get the overall statistics data or a specific one. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $key The specific data slug. * @return array|string */ public function get_stats_data( $key = '' ); /** * Get the optimized/original saving of the original image in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_saving_percent(); /** * Get the overall optimized/original saving (original image + all thumbnails) in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_overall_saving_percent(); } Optimization/Data/Noop.php 0000644 00000015021 15174671731 0011542 0 ustar 00 <?php namespace Imagify\Optimization\Data; use Imagify\Media\MediaInterface; /** * Fallback class optimization data of "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ class Noop implements DataInterface { /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { return false; } /** * Get the media instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return MediaInterface|false */ public function get_media() { return false; } /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid() { return false; } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION DATA ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_optimized() { return false; } /** * Check if the main file is optimized (NOT by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_already_optimized() { return false; } /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_error() { return false; } /** * Get the whole media optimization data. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array The data. See parent method for details. */ public function get_optimization_data() { return [ 'status' => '', 'level' => false, 'sizes' => [], 'stats' => [ 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ], ]; } /** * Update the optimization data, level, and status for a size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The size name. * @param array $data The optimization data. See parent method for details. */ public function update_size_optimization_data( $size, array $data ) {} /** * Delete the media optimization data, level, and status. * * @since 1.9 * @access public * @author Grégory Viguier */ public function delete_optimization_data() {} /** * Delete the optimization data for the given sizes. * If all sizes are removed, all optimization data is deleted. * Status and level are not modified nor removed if the "full" size is removed. This leaves the media in a Schrödinger state. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @param array $sizes A list of sizes to remove. */ public function delete_sizes_optimization_data( array $sizes ) {} /** * Get the media's optimization level. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int|bool The optimization level. False if not optimized. */ public function get_optimization_level() { return false; } /** * Get the media's optimization status (success or error). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string The optimization status. An empty string if there is none. */ public function get_optimization_status() { return ''; } /** * Count number of optimized sizes. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int Number of optimized sizes. */ public function get_optimized_sizes_count() { return 0; } /** * Get the original media's size (weight). * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_original_size( $human_format = true, $decimals = 2 ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } /** * Get the file size of the full size file. * If the Nextgen size is available, it is used. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @param bool $use_nextgen Use the Nextgen size if available. * @return string|int */ public function get_optimized_size( $human_format = true, $decimals = 2, $use_nextgen = true ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION STATS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get one or all statistics of a specific size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The thumbnail slug. * @param string $key The specific data slug. * @return array|string */ public function get_size_data( $size = 'full', $key = '' ) { return $key ? '' : []; } /** * Get the overall statistics data or a specific one. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $key The specific data slug. * @return array|string */ public function get_stats_data( $key = '' ) { return $key ? '' : []; } /** * Get the optimized/original saving of the original image in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_saving_percent() { return round( (float) 0, 2 ); } /** * Get the overall optimized/original saving (original image + all thumbnails) in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_overall_saving_percent() { return round( (float) 0, 2 ); } } Optimization/Data/AbstractData.php 0000644 00000027036 15174671731 0013175 0 ustar 00 <?php namespace Imagify\Optimization\Data; use Imagify\Media\MediaInterface; /** * Abstract class used to handle the optimization data of "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractData implements DataInterface { /** * Optimization data structure. * This is the format returned when we "get" optimization data from the DB. * * @var array * @since 1.9 * @access protected * @see $this->get_optimization_data() * @author Grégory Viguier */ protected $default_optimization_data = [ 'status' => '', 'message' => '', 'level' => false, 'sizes' => [], 'stats' => [ 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ], ]; /** * The media object. * * @var MediaInterface * @since 1.9 * @access protected * @author Grégory Viguier */ protected $media; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.9 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * The constructor. * * @since 1.9 * @access public * @see self::constructor_accepts() * @author Grégory Viguier * * @param mixed $id An ID, or whatever type the constructor accepts. */ public function __construct( $id ) { // Set the Media instance. if ( $id instanceof MediaInterface ) { $this->media = $id; } elseif ( static::constructor_accepts( $id ) ) { $media_class = str_replace( '\\Optimization\\Data\\', '\\Media\\', get_called_class() ); $media_class = '\\' . ltrim( $media_class, '\\' ); $this->media = new $media_class( $id ); } else { $this->media = false; } $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Tell if the given entry can be accepted in the constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { if ( $id instanceof MediaInterface ) { return true; } $media_class = str_replace( '\\Optimization\\Data\\', '\\Media\\', get_called_class() ); $media_class = '\\' . ltrim( $media_class, '\\' ); return $media_class::constructor_accepts( $id ); } /** * Get the media instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return MediaInterface|false */ public function get_media() { return $this->media; } /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid() { return $this->get_media() && $this->get_media()->is_valid(); } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION DATA ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_optimized() { return 'success' === $this->get_optimization_status(); } /** * Check if the main file is optimized (NOT by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_already_optimized() { return 'already_optimized' === $this->get_optimization_status(); } /** * Check if the main file is optimized (by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media is optimized. */ public function is_error() { return 'error' === $this->get_optimization_status(); } /** * Get the media's optimization level. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int|false The optimization level. False if not optimized. */ public function get_optimization_level() { if ( ! $this->is_valid() ) { return false; } $data = $this->get_optimization_data(); return $data['level']; } /** * Get the media's optimization status (success or error). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string The optimization status. An empty string if there is none. */ public function get_optimization_status() { if ( ! $this->is_valid() ) { return ''; } $data = $this->get_optimization_data(); return $data['status']; } /** * Count number of optimized sizes. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int Number of optimized sizes. */ public function get_optimized_sizes_count() { $data = $this->get_optimization_data(); $count = 0; if ( ! $data['sizes'] ) { return 0; } $context_sizes = $this->get_media()->get_media_files(); $data['sizes'] = array_intersect_key( $data['sizes'], $context_sizes ); if ( ! $data['sizes'] ) { return 0; } foreach ( $data['sizes'] as $size ) { if ( ! empty( $size['success'] ) ) { ++$count; } } return $count; } /** * Get the original media's size (weight). * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @return string|int */ public function get_original_size( $human_format = true, $decimals = 2 ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $size = $this->get_optimization_data(); $size = ! empty( $size['sizes']['full']['original_size'] ) ? $size['sizes']['full']['original_size'] : 0; // If nothing in the database, try to get the info from the file. if ( ! $size ) { // Check for the backup file first. $filepath = $this->get_media()->get_backup_path(); if ( ! $filepath ) { // Try the original file then. $filepath = $this->get_media()->get_original_path(); } $size = $filepath ? $this->filesystem->size( $filepath ) : 0; } if ( $human_format ) { return imagify_size_format( (int) $size, $decimals ); } return (int) $size; } /** * Get the file size of the full size file. * If the Nextgen size is available, it is used. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $human_format True to display the image human format size (1Mb). * @param int $decimals Precision of number of decimal places. * @param bool $use_nextgen Use the Nextgen size if available. * @return string|int */ public function get_optimized_size( $human_format = true, $decimals = 2, $use_nextgen = true ) { if ( ! $this->is_valid() ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $data = $this->get_optimization_data(); $media = $this->get_media(); $format = 'webp'; $process_class_name = imagify_get_optimization_process_class_name( $media->get_context() ); $nextgen_avif_size_name = 'full' . constant( $process_class_name . '::AVIF_SUFFIX' ); $nextgen_webp_size_name = 'full' . constant( $process_class_name . '::WEBP_SUFFIX' ); $size = 0; if ( $use_nextgen ) { /**Checking for success status before size, some cases the response is false * because the image is already compressed, or we have a connection timed out * */ $size = ! empty( $data['sizes'][ $nextgen_webp_size_name ] ) && $data['sizes'][ $nextgen_webp_size_name ]['success'] ? (int) $data['sizes'][ $nextgen_webp_size_name ]['optimized_size'] : 0; if ( ! empty( $data['sizes'][ $nextgen_avif_size_name ]['optimized_size'] ) && $data['sizes'][ $nextgen_avif_size_name ] ) { $size = (int) $data['sizes'][ $nextgen_avif_size_name ]['optimized_size']; } } elseif ( ! empty( $data['sizes']['full']['optimized_size'] ) ) { $size = (int) $data['sizes']['full']['optimized_size']; } if ( $size ) { return $human_format ? imagify_size_format( $size, $decimals ) : $size; } // If nothing in the database, try to get the info from the file. $filepath = false; if ( $use_nextgen ) { if ( ! empty( $data['sizes'][ $nextgen_avif_size_name ]['success'] ) ) { $format = 'avif'; } // Try with the Nextgen file first. $filepath = $media->get_raw_fullsize_path(); $filepath = $filepath ? imagify_path_to_nextgen( $filepath, $format ) : false; if ( ! $filepath || ! $this->filesystem->exists( $filepath ) ) { $filepath = false; } } if ( ! $filepath ) { // No Nextgen? The full size then. $filepath = $media->get_fullsize_path(); } if ( ! $filepath ) { return $human_format ? imagify_size_format( 0, $decimals ) : 0; } $size = (int) $this->filesystem->size( $filepath ); return $human_format ? imagify_size_format( $size, $decimals ) : $size; } /** ----------------------------------------------------------------------------------------- */ /** OPTIMIZATION STATS ====================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get one or all statistics of a specific size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The thumbnail slug. * @param string $key The specific data slug. * @return array|string */ public function get_size_data( $size = 'full', $key = '' ) { $data = $this->get_optimization_data(); if ( ! isset( $data['sizes'][ $size ] ) ) { return $key ? '' : []; } if ( ! $key ) { return $data['sizes'][ $size ]; } if ( ! isset( $data['sizes'][ $size ][ $key ] ) ) { return ''; } return $data['sizes'][ $size ][ $key ]; } /** * Get the overall statistics data or a specific one. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $key The specific data slug. * @return array|string */ public function get_stats_data( $key = '' ) { $data = $this->get_optimization_data(); $stats = ''; if ( empty( $data['stats'] ) ) { return $key ? '' : []; } if ( ! isset( $data['stats'][ $key ] ) ) { return ''; } return $data['stats'][ $key ]; } /** * Get the optimized/original saving of the original image in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_saving_percent() { if ( ! $this->is_valid() ) { return round( (float) 0, 2 ); } $process_class_name = imagify_get_optimization_process_class_name( $this->get_media()->get_context() ); $nextgen_webp_size_name = 'full' . constant( $process_class_name . '::WEBP_SUFFIX' ); $nextgen_avif_size_name = 'full' . constant( $process_class_name . '::AVIF_SUFFIX' ); $percent = $this->get_size_data( $nextgen_avif_size_name, 'percent' ); // Check for webp version if avif is not found. if ( ! $percent ) { $percent = $this->get_size_data( $nextgen_webp_size_name, 'percent' ); } if ( ! $percent ) { $percent = $this->get_size_data( 'full', 'percent' ); } $percent = $percent ? $percent : 0; return round( (float) $percent, 2 ); } /** * Get the overall optimized/original saving (original image + all thumbnails) in percent. * * @since 1.9 * @access public * @author Grégory Viguier * * @return float A 2-decimals float. */ public function get_overall_saving_percent() { if ( ! $this->is_valid() ) { return round( (float) 0, 2 ); } $percent = $this->get_stats_data( 'percent' ); return round( (float) $percent, 2 ); } } Optimization/Data/CustomFolders.php 0000644 00000021610 15174671731 0013421 0 ustar 00 <?php namespace Imagify\Optimization\Data; use Imagify\Traits\MediaRowTrait; /** * Optimization data class for the custom folders. * This class constructor accepts: * - A media ID (int). * - An array of data coming from the files DB table /!\ * - An object of data coming from the files DB table /!\ * - A \Imagify\Media\MediaInterface object. * * @since 1.9 * @see Imagify\Media\CustomFolders * @author Grégory Viguier */ class CustomFolders extends AbstractData { use MediaRowTrait; /** * The attachment SQL DB class. * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $db_class_name = 'Imagify_Files_DB'; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id An ID, or whatever type the "Media" class constructor accepts. */ public function __construct( $id ) { parent::__construct( $id ); if ( ! $this->is_valid() ) { return; } $media = $this->get_media(); if ( ! $media ) { return; } // This is required by MediaRowTrait. $this->id = $media->get_id(); // In this context, the media data and the optimization data are stored in the same DB table, so, no need to request twice the DB. $this->row = $media->get_row(); // @phpstan-ignore-line } /** * Get the whole media optimization data. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array The data. See parent method for details. */ public function get_optimization_data() { if ( ! $this->is_valid() ) { return $this->default_optimization_data; } $row = array_merge( $this->get_row_db_instance()->get_column_defaults(), $this->get_row() ); $data = $this->default_optimization_data; $data['status'] = $row['status']; $data['level'] = $row['optimization_level']; $data['level'] = is_numeric( $data['level'] ) ? (int) $data['level'] : false; if ( 'success' === $row['status'] ) { /** * Success. */ $data['sizes']['full'] = [ 'success' => true, 'original_size' => $row['original_size'], 'optimized_size' => $row['optimized_size'], 'percent' => $row['percent'], ]; } elseif ( ! empty( $row['status'] ) ) { /** * Error. */ $data['sizes']['full'] = [ 'success' => false, 'error' => $row['error'], ]; } if ( ! empty( $row['data']['sizes'] ) && is_array( $row['data']['sizes'] ) ) { unset( $row['data']['sizes']['full'] ); $data['sizes'] = array_merge( $data['sizes'], $row['data']['sizes'] ); $data['sizes'] = array_filter( $data['sizes'], 'is_array' ); } if ( empty( $data['sizes'] ) ) { return $data; } foreach ( $data['sizes'] as $size_data ) { // Cast. if ( isset( $size_data['original_size'] ) ) { $size_data['original_size'] = (int) $size_data['original_size']; } if ( isset( $size_data['optimized_size'] ) ) { $size_data['optimized_size'] = (int) $size_data['optimized_size']; } if ( isset( $size_data['percent'] ) ) { $size_data['percent'] = round( $size_data['percent'], 2 ); } // Stats. if ( ! empty( $size_data['original_size'] ) && ! empty( $size_data['optimized_size'] ) ) { $data['stats']['original_size'] += $size_data['original_size']; $data['stats']['optimized_size'] += $size_data['optimized_size']; } } if ( $data['stats']['original_size'] && $data['stats']['optimized_size'] ) { $data['stats']['percent'] = $data['stats']['original_size'] - $data['stats']['optimized_size']; $data['stats']['percent'] = round( $data['stats']['percent'] / $data['stats']['original_size'] * 100, 2 ); } return $data; } /** * Update the optimization data, level, and status for a size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The size name. * @param array $data The optimization data. See parent method for details. */ public function update_size_optimization_data( $size, array $data ) { if ( ! $this->is_valid() ) { return; } $old_data = array_merge( $this->get_reset_data(), $this->get_row() ); if ( 'full' === $size ) { /** * Original file. */ $old_data['optimization_level'] = $data['level']; $old_data['status'] = $data['status']; $old_data['modified'] = 0; $file_path = $this->get_media()->get_fullsize_path(); if ( $file_path ) { $old_data['hash'] = md5_file( $file_path ); } if ( key_exists( 'message', $data ) ) { $old_data['message'] = $data['message']; } if ( ! $data['success'] ) { /** * Error. */ $old_data['error'] = $data['error']; } else { /** * Success. */ $old_data['original_size'] = $data['original_size']; $old_data['optimized_size'] = $data['optimized_size']; $old_data['percent'] = $data['original_size'] - $data['optimized_size']; $old_data['percent'] = round( ( $old_data['percent'] / $data['original_size'] ) * 100, 2 ); } } else { /** * WebP version or any other size. */ $old_data['data'] = ! empty( $old_data['data'] ) && is_array( $old_data['data'] ) ? $old_data['data'] : []; $old_data['data']['sizes'] = ! empty( $old_data['data']['sizes'] ) && is_array( $old_data['data']['sizes'] ) ? $old_data['data']['sizes'] : []; if ( ! $data['success'] ) { /** * Error. */ $old_data['data']['sizes'][ $size ] = [ 'success' => false, 'error' => $data['error'], ]; } else { /** * Success. */ $old_data['data']['sizes'][ $size ] = [ 'success' => true, 'original_size' => $data['original_size'], 'optimized_size' => $data['optimized_size'], 'percent' => round( ( ( $data['original_size'] - $data['optimized_size'] ) / $data['original_size'] ) * 100, 2 ), ]; } } if ( isset( $old_data['data']['sizes'] ) && ( ! $old_data['data']['sizes'] || ! is_array( $old_data['data']['sizes'] ) ) ) { unset( $old_data['data']['sizes'] ); } $this->update_row( $old_data ); } /** * Delete the media optimization data, level, and status. * * @since 1.9 * @access public * @author Grégory Viguier */ public function delete_optimization_data() { if ( ! $this->is_valid() ) { return; } $this->update_row( $this->get_reset_data() ); } /** * Delete the optimization data for the given sizes. * If all sizes are removed, all optimization data is deleted. * Status and level are not modified nor removed if the "full" size is removed. This leaves the media in a Schrödinger state. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @param array $sizes A list of sizes to remove. */ public function delete_sizes_optimization_data( array $sizes ) { if ( ! $sizes || ! $this->is_valid() ) { return; } $data = array_merge( $this->get_reset_data(), $this->get_row() ); $data['data']['sizes'] = ! empty( $data['data']['sizes'] ) && is_array( $data['data']['sizes'] ) ? $data['data']['sizes'] : []; if ( ! $data['data']['sizes'] ) { return; } $remaining_sizes_data = array_diff_key( $data['data']['sizes'], array_flip( $sizes ) ); if ( ! $remaining_sizes_data ) { // All sizes have been removed: delete everything. $this->delete_optimization_data(); return; } if ( count( $remaining_sizes_data ) === count( $data['data']['sizes'] ) ) { // Nothing has been removed. return; } $data['data']['sizes'] = $remaining_sizes_data; $this->update_row( $data ); } /** * Get default values used to reset optimization data. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return array { * The default values related to the optimization. * * @type string $hash The file hash. * @type int $modified 0 to tell that the file has not been modified * @type int $optimized_size File size after optimization. * @type int $percent Saving optimized/original in percent. * @type int $optimization_level The optimization level. * @type string $status The status: success, already_optimized, error. * @type string $error An error message. * } */ protected function get_reset_data() { static $column_defaults; if ( ! isset( $column_defaults ) ) { $column_defaults = $this->get_row_db_instance()->get_column_defaults(); // All DB columns that have `null` as default value, are Imagify data. foreach ( $column_defaults as $column_name => $value ) { if ( 'hash' === $column_name || 'modified' === $column_name || 'data' === $column_name ) { continue; } if ( isset( $value ) ) { unset( $column_defaults[ $column_name ] ); } } } $imagify_columns = $column_defaults; // Also set the new file hash. $file_path = $this->get_media()->get_fullsize_path(); if ( $file_path ) { $imagify_columns['hash'] = md5_file( $file_path ); } return $imagify_columns; } } Optimization/Data/WP.php 0000644 00000012732 15174671731 0011163 0 ustar 00 <?php namespace Imagify\Optimization\Data; /** * Optimization data class for the medias in the WP library. * This class constructor accepts: * - A post ID (int). * - A \WP_Post object. * - A \Imagify\Media\MediaInterface object. * * @since 1.9 * @author Grégory Viguier */ class WP extends AbstractData { /** * Get the whole media optimization data. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array The data. See parent method for details. */ public function get_optimization_data() { if ( ! $this->is_valid() ) { return $this->default_optimization_data; } $id = $this->get_media()->get_id(); $data = get_post_meta( $id, '_imagify_data', true ); $data = is_array( $data ) ? $data : []; if ( isset( $data['sizes'] ) && ! is_array( $data['sizes'] ) ) { $data['sizes'] = []; } if ( isset( $data['stats'] ) && ! is_array( $data['stats'] ) ) { $data['stats'] = []; } $data = array_merge( $this->default_optimization_data, $data ); $data['status'] = get_post_meta( $id, '_imagify_status', true ); $data['status'] = is_string( $data['status'] ) ? $data['status'] : ''; $data['level'] = get_post_meta( $id, '_imagify_optimization_level', true ); $data['level'] = is_numeric( $data['level'] ) ? (int) $data['level'] : false; return $data; } /** * Update the optimization data, level, and status for a size. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $size The size name. * @param array $data The optimization data. See parent method for details. */ public function update_size_optimization_data( $size, array $data ) { if ( ! $this->is_valid() ) { return; } $id = $this->get_media()->get_id(); if ( 'full' === $size ) { // Optimization level. update_post_meta( $id, '_imagify_optimization_level', $data['level'] ); // Optimization status. update_post_meta( $id, '_imagify_status', $data['status'] ); } // Size data and stats. $old_data = get_post_meta( $id, '_imagify_data', true ); $old_data = is_array( $old_data ) ? $old_data : []; if ( ! isset( $old_data['sizes'] ) || ! is_array( $old_data['sizes'] ) ) { $old_data['sizes'] = []; } if ( ! isset( $old_data['stats'] ) || ! is_array( $old_data['stats'] ) ) { $old_data['stats'] = []; } $old_data['stats'] = array_merge( [ 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, 'message' => '', ], $old_data['stats'] ); if ( key_exists( 'message', $data ) ) { $old_data['message'] = $data['message']; } if ( ! $data['success'] ) { /** * Error. */ $old_data['sizes'][ $size ] = [ 'success' => false, 'error' => $data['error'], ]; } else { /** * Success. */ $old_data['sizes'][ $size ] = [ 'success' => true, 'original_size' => $data['original_size'], 'optimized_size' => $data['optimized_size'], 'percent' => round( ( ( $data['original_size'] - $data['optimized_size'] ) / $data['original_size'] ) * 100, 2 ), ]; $old_data['stats']['original_size'] += $data['original_size']; $old_data['stats']['optimized_size'] += $data['optimized_size']; $old_data['stats']['percent'] = round( ( ( $old_data['stats']['original_size'] - $old_data['stats']['optimized_size'] ) / $old_data['stats']['original_size'] ) * 100, 2 ); } update_post_meta( $id, '_imagify_data', $old_data ); } /** * Delete the media optimization data, level, and status. * * @since 1.9 * @access public * @author Grégory Viguier */ public function delete_optimization_data() { if ( ! $this->is_valid() ) { return; } $id = $this->get_media()->get_id(); delete_post_meta( $id, '_imagify_data' ); delete_post_meta( $id, '_imagify_status' ); delete_post_meta( $id, '_imagify_optimization_level' ); } /** * Delete the optimization data for the given sizes. * If all sizes are removed, all optimization data is deleted. * Status and level are not modified nor removed if the "full" size is removed. This leaves the media in a Schrödinger state. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @param array $sizes A list of sizes to remove. */ public function delete_sizes_optimization_data( array $sizes ) { if ( ! $sizes || ! $this->is_valid() ) { return; } $media_id = $this->get_media()->get_id(); $data = get_post_meta( $media_id, '_imagify_data', true ); if ( empty( $data['sizes'] ) || ! is_array( $data['sizes'] ) ) { return; } $remaining_sizes_data = array_diff_key( $data['sizes'], array_flip( $sizes ) ); if ( ! $remaining_sizes_data ) { // All sizes have been removed: delete everything. $this->delete_optimization_data(); return; } if ( count( $remaining_sizes_data ) === count( $data['sizes'] ) ) { // Nothing has been removed. return; } $data['sizes'] = $remaining_sizes_data; // Update stats. $data['stats'] = [ 'original_size' => 0, 'optimized_size' => 0, 'percent' => 0, ]; foreach ( $data['sizes'] as $size_data ) { if ( empty( $size_data['success'] ) ) { continue; } $data['stats']['original_size'] += $size_data['original_size']; $data['stats']['optimized_size'] += $size_data['optimized_size']; } $data['stats']['percent'] = round( ( ( $data['stats']['original_size'] - $data['stats']['optimized_size'] ) / $data['stats']['original_size'] ) * 100, 2 ); update_post_meta( $media_id, '_imagify_data', $data ); } } Auth/Basic.php 0000644 00000005631 15174671731 0007220 0 ustar 00 <?php namespace Imagify\Auth; use Imagify\Traits\InstanceGetterTrait; /** * Class that allows the use of Basic Auth for internal requests. * If this doesn’t work automatically, define the constants IMAGIFY_AUTH_USER and IMAGIFY_AUTH_PASSWORD. * * @since 1.9.5 * @author Grégory Viguier */ final class Basic { use InstanceGetterTrait; /** * Class init: launch hooks. * * @since 1.9.5 * @access public * @author Grégory Viguier */ public function init() { add_filter( 'imagify_background_process_url', [ $this, 'get_auth_url' ] ); add_filter( 'imagify_async_job_url', [ $this, 'get_auth_url' ] ); add_filter( 'imagify_internal_request_url', [ $this, 'get_auth_url' ] ); add_filter( 'cron_request', [ $this, 'cron_request_args' ] ); } /** * If the site uses basic authentication, add the required user and password to the given URL. * * @since 1.9.5 * @access public * @author Grégory Viguier * * @param string $url An URL. * @return string */ public function get_auth_url( $url ) { if ( ! $url || ! is_string( $url ) ) { // Invalid. return ''; } if ( preg_match( '%.+?//(.+?):(.+?)@%', $url ) ) { // Credentials already in the URL. return $url; } $user = ''; $pass = ''; if ( defined( 'IMAGIFY_AUTH_USER' ) && defined( 'IMAGIFY_AUTH_PASSWORD' ) && IMAGIFY_AUTH_USER && IMAGIFY_AUTH_PASSWORD ) { $user = IMAGIFY_AUTH_USER; $pass = IMAGIFY_AUTH_PASSWORD; } else { $auth_type = ! empty( $_SERVER['AUTH_TYPE'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['AUTH_TYPE'] ) ) ) : ''; if ( 'basic' === $auth_type && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { $user = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) ); $pass = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_PW'] ) ); } } if ( empty( $user ) ) { // No credentials. return $url; } return preg_replace( '%^(.+?//)(.+?)$%', '$1' . rawurlencode( $user ) . ':' . rawurlencode( $pass ) . '@$2', $url ); } /** * If the site uses basic authentication, add the required user and password to the given URL. * * @since 1.9.5 * @access public * @author Grégory Viguier * * @param array $args { * An array of cron request URL arguments. * * @type string $url The cron request URL. * @type int $key The 22 digit GMT microtime. * @type array $args { * An array of cron request arguments. * * @type int $timeout The request timeout in seconds. Default .01 seconds. * @type bool $blocking Whether to set blocking for the request. Default false. * @type bool $sslverify Whether SSL should be verified for the request. Default false. * } * } * @return array */ public function cron_request_args( $args ) { if ( ! empty( $args['url'] ) ) { $args['url'] = $this->get_auth_url( $args['url'] ); } return $args; } } Dependencies/Psr/Container/ContainerInterface.php 0000644 00000002053 15174671731 0016110 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get(string $id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has(string $id): bool; } Dependencies/Psr/Container/NotFoundExceptionInterface.php 0000644 00000000263 15174671731 0017602 0 ustar 00 <?php namespace Imagify\Dependencies\Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends ContainerExceptionInterface { } Dependencies/Psr/Container/ContainerExceptionInterface.php 0000644 00000000315 15174671731 0017766 0 ustar 00 <?php namespace Imagify\Dependencies\Psr\Container; use Throwable; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface extends Throwable { } Dependencies/WPMedia/PluginFamily/Model/PluginFamily.php 0000644 00000013275 15174671731 0017215 0 ustar 00 <?php namespace Imagify\Dependencies\WPMedia\PluginFamily\Model; /** * Handles the data to be passed to the frontend. */ class PluginFamily { /** * An array of referrers for wp rocket. * * @var array */ protected $wp_rocket_referrer = [ 'imagify' => 'imagify', 'seo-by-rank-math' => '', 'backwpup' => '', 'uk-cookie-consent' => '', ]; /** * Get filtered plugins. * * @param string $main_plugin Main plugin installed. * * @return array */ public function get_filtered_plugins( string $main_plugin ): array { $plugins = require_once 'wp_media_plugins.php'; return $this->filter_plugins_by_activation( $plugins, $main_plugin ); } /** * Filter plugins family data by activation status and returns both categorized and uncategorized format. * * @param array $plugins Array of family plugins. * @param string $main_plugin Main plugin installed. * * @return array */ public function filter_plugins_by_activation( array $plugins, string $main_plugin ): array { if ( empty( $plugins ) ) { return []; } list( $active_plugins, $inactive_plugins ) = [ [], [] ]; foreach ( $plugins as $cat => $cat_data ) { foreach ( $cat_data['plugins'] as $plugin => $data ) { $plugin_path = $plugin . '.php'; $plugin_slug = dirname( $plugin ); $main_plugin_slug = dirname( $main_plugin ); $wpr_referrer = 'wp-rocket' !== $main_plugin_slug ? $this->wp_rocket_referrer[ $main_plugin_slug ] : ''; /** * Check for activated plugins and pop them out of the array * to re-add them back using array_merge to be displayed after * plugins that are not installed or not activated. */ if ( is_plugin_active( $plugin_path ) && $main_plugin . '.php' !== $plugin_path ) { // set cta data of active plugins. $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Activated', 'imagify' ), 'url' => '#', ]; // Send active plugin to new array. $active_plugins[ $plugin ] = $plugins[ $cat ]['plugins'][ $plugin ]; // Remove active plugin from current category. $active_plugin = $plugins[ $cat ]['plugins'][ $plugin ]; unset( $plugins[ $cat ]['plugins'][ $plugin ] ); // Send active plugin to the end of array in current category. $plugins[ $cat ]['plugins'][ $plugin ] = $active_plugin; // Remove category with active plugin from current array. $active_cat = $plugins[ $cat ]; unset( $plugins[ $cat ] ); // Send category with active plugins to the end of array. $plugins[ $cat ] = $active_cat; continue; } $install_activate_url = admin_url( 'admin-post.php' ); $args = [ 'action' => 'plugin_family_install_' . $plugin_slug, '_wpnonce' => wp_create_nonce( 'plugin_family_install_' . $plugin_slug ), 'plugin_to_install' => rawurlencode( $plugin ), ]; if ( 'imagify' === $plugin_slug ) { $args = [ 'action' => 'install_imagify_from_partner_' . $main_plugin_slug, '_wpnonce' => wp_create_nonce( 'install_imagify_from_partner' ), '_wp_http_referer' => rawurlencode( $this->get_current_url() ), ]; } $install_activate_url = add_query_arg( $args, $install_activate_url ); // Set Installation link. $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Install', 'imagify' ), 'url' => $install_activate_url, ]; // Create unique CTA data for WP Rocket. if ( 'wp-rocket/wp-rocket' === $plugin ) { $url = 'https://wp-rocket.me/?utm_source=' . $wpr_referrer . '-coupon&utm_medium=plugin&utm_campaign=' . $wpr_referrer; $plugins[ $cat ]['plugins'][ $plugin ]['cta'] = [ 'text' => __( 'Get it Now', 'imagify' ), 'url' => $url, ]; $plugins[ $cat ]['plugins'][ $plugin ]['link'] = $url; } // Set activation text. if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_path ) ) { $plugins[ $cat ]['plugins'][ $plugin ]['cta']['text'] = __( 'Activate', 'imagify' ); if ( 'wp-rocket/wp-rocket' === $plugin ) { $plugins[ $cat ]['plugins'][ $plugin ]['cta']['url'] = $install_activate_url; } } // Send inactive plugins to new array. $inactive_plugins[ $plugin ] = $plugins[ $cat ]['plugins'][ $plugin ]; } // Remove main plugin from categorized array. if ( isset( $plugins[ $cat ]['plugins'][ $main_plugin ] ) ) { unset( $plugins[ $cat ]['plugins'][ $main_plugin ] ); } } $uncategorized = array_merge( $inactive_plugins, $active_plugins ); // Remove main plugin from uncategorized array. unset( $uncategorized[ $main_plugin ] ); return [ 'categorized' => $plugins, 'uncategorized' => $uncategorized, ]; } /** * Get the current URL. * Gotten from Imagify_Partner Package. * * @return string */ protected function get_current_url(): string { if ( ! isset( $_SERVER['SERVER_PORT'], $_SERVER['HTTP_HOST'] ) ) { return ''; } $port = (int) wp_unslash( $_SERVER['SERVER_PORT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $port = 80 !== $port && 443 !== $port ? ( ':' . $port ) : ''; $url = ! empty( $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] ) ? $GLOBALS['HTTP_SERVER_VARS']['REQUEST_URI'] : ( ! empty( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return 'http' . ( is_ssl() ? 's' : '' ) . '://' . $_SERVER['HTTP_HOST'] . $port . $url; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash } } Dependencies/WPMedia/PluginFamily/Model/wp_media_plugins.php 0000644 00000005145 15174671731 0020140 0 ustar 00 <?php /** * WP Media plugin family data. */ return [ 'optimize_performance' => [ 'title' => __( 'Optimize Performance', 'imagify' ), 'plugins' => [ 'wp-rocket/wp-rocket' => [ 'logo' => [ 'file' => 'logo-wp-rocket.svg', 'width' => '50%', ], 'title' => __( 'Speed Up Your Website, Instantly', 'imagify' ), 'desc' => __( 'WP Rocket is the easiest way to make your WordPress website faster and boost your Google PageSpeed score. Get more traffic, better engagement, and higher conversions effortlessly.', 'imagify' ), 'link' => '', ], 'imagify/imagify' => [ 'logo' => [ 'file' => 'logo-imagify.svg', 'width' => '50%', ], 'title' => __( 'Speed Up Your Website With Lighter Images', 'imagify' ), 'desc' => __( 'Imagify is the easiest WordPress image optimizer. It automatically compresses images, converts them to WebP and AVIF formats, and lets you resize and optimize with just one click!', 'imagify' ), 'link' => 'https://imagify.io/', ], ], ], 'boost_traffic' => [ 'title' => __( 'Boost Traffic', 'imagify' ), 'plugins' => [ 'seo-by-rank-math/rank-math' => [ 'logo' => [ 'file' => 'logo-rank-math.svg', 'width' => '60%', ], 'title' => __( 'The Swiss Army Knife of SEO Tools', 'imagify' ), 'desc' => __( 'Rank Math SEO is the Best WordPress SEO plugin with the features of many SEO and AI SEO tools in a single package to help multiply your SEO traffic.', 'imagify' ), 'link' => 'https://rankmath.com/wordpress/plugin/seo-suite/', ], ], ], 'protect_secure' => [ 'title' => __( 'Protect & Secure', 'imagify' ), 'plugins' => [ 'backwpup/backwpup' => [ 'logo' => [ 'file' => 'logo-backwpup.svg', 'width' => '60%', ], 'title' => __( 'The Easiest Way to Protect Your Website', 'imagify' ), 'desc' => __( 'BackWPup is the most comprehensive and user-friendly backup & restore plugin for WordPress. Easily schedule automatic backups, securely store and restore with just a few clicks!', 'imagify' ), 'link' => 'https://backwpup.com/', ], 'uk-cookie-consent/uk-cookie-consent' => [ 'logo' => [ 'file' => 'logo-termly.svg', 'width' => '50%', ], 'title' => __( 'GDPR/CCPA Cookie Consent Banner', 'imagify' ), 'desc' => __( 'One of the easiest, most comprehensive, and popular cookie consent plugins available. Google Gold Certified Partner to quickly comply with data privacy laws from around the world.', 'imagify' ), 'link' => 'https://termly.io/resources/articles/wordpress-cookies-guide/', ], ], ], ]; Dependencies/WPMedia/PluginFamily/Controller/PluginFamily.php 0000644 00000015412 15174671731 0020273 0 ustar 00 <?php namespace Imagify\Dependencies\WPMedia\PluginFamily\Controller; /** * Handles installation and Activation of plugin family members. */ class PluginFamily implements PluginFamilyInterface { /** * Error transient. * * @var string */ protected $error_transient = 'plugin_family_error'; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events(): array { $events = self::get_post_install_event(); $events['admin_notices'] = 'display_error_notice'; return $events; } /** * Set post install event. * * @return array */ public static function get_post_install_event(): array { $allowed_plugin = [ 'uk-cookie-consent', 'backwpup', 'imagify', 'seo-by-rank-math', 'wp-rocket', ]; if ( ! isset( $_GET['action'], $_GET['_wpnonce'], $_GET['plugin_to_install'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return []; } $plugin = str_replace( 'plugin_family_install_', '', sanitize_text_field( wp_unslash( $_GET['action'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $plugin, $allowed_plugin, true ) ) { return []; } return [ 'admin_post_plugin_family_install_' . $plugin => 'install_activate', ]; } /** * Process to install and activate plugin. * * @return void */ public function install_activate() { if ( ! $this->is_allowed() ) { wp_die( 'Plugin Installation is not allowed.', '', [ 'back_link' => true ] ); } // Install plugin. $this->install(); // Activate plugin. $result = activate_plugin( $this->get_plugin(), '', is_multisite() ); if ( is_wp_error( $result ) ) { $this->set_error( $result ); } wp_safe_redirect( wp_get_referer() ); exit; } /** * Install plugin. * * @return void */ private function install() { if ( $this->is_installed() ) { return; } $upgrader_class = ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; if ( ! defined( 'ABSPATH' ) || ! file_exists( $upgrader_class ) ) { wp_die( 'Plugin Installation failed. class-wp-upgrader.php not found', '', [ 'back_link' => true ] ); } require_once $upgrader_class; // @phpstan-ignore-line $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); $result = $upgrader->install( $this->get_download_url() ); if ( is_wp_error( $result ) ) { $this->set_error( $result ); } clearstatcache(); } /** * Check if plugin is installed. * * @return boolean */ private function is_installed(): bool { return file_exists( WP_PLUGIN_DIR . '/' . $this->get_plugin() ); } /** * Check if installation is allowed. * * @return boolean */ private function is_allowed(): bool { if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'plugin_family_install_' . $this->get_slug() ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated return false; } if ( ! current_user_can( is_multisite() ? 'manage_network_plugins' : 'install_plugins' ) ) { return false; } return true; } /** * Get plugin slug. * * @return string */ private function get_slug(): string { return dirname( rawurldecode( sanitize_text_field( wp_unslash( $_GET['plugin_to_install'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated } /** * Get plugin identifier. * * @return string */ private function get_plugin(): string { return rawurldecode( sanitize_text_field( wp_unslash( $_GET['plugin_to_install'] ) ) ) . '.php'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated } /** * Get plugin download url. * * @return string */ private function get_download_url(): string { $slug = $this->get_slug(); $custom_download_url = $this->maybe_get_custom_download_url( $slug ); if ( false !== $custom_download_url ) { return $custom_download_url; } $plugin_install = ABSPATH . 'wp-admin/includes/plugin-install.php'; if ( ! defined( 'ABSPATH' ) || ! file_exists( $plugin_install ) ) { wp_die( 'Plugin Installation failed. plugin-install.php not found', '', [ 'back_link' => true ] ); } require_once $plugin_install; // @phpstan-ignore-line $data = [ 'slug' => $slug, 'fields' => [ 'download_link' => true, 'short_description' => false, 'sections' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, ], ]; // Get Plugin Infos. $plugin_info = plugins_api( 'plugin_information', $data ); if ( is_wp_error( $plugin_info ) ) { $this->set_error( $plugin_info ); } // Ensure that $plugin_info is an object before accessing the property. if ( ! is_object( $plugin_info ) || ! isset( $plugin_info->download_link ) ) { return ''; } return $plugin_info->download_link; } /** * Maybe display error notice. * * @return void */ public function display_error_notice() { $errors = get_transient( $this->error_transient ); if ( ! $errors ) { return; } if ( ! is_wp_error( $errors ) ) { delete_transient( $this->error_transient ); return; } $errors = $errors->get_error_messages(); if ( ! $errors ) { $errors[] = 'Installation process failed'; } $notice = '<div class="error notice is-dismissible"><p>' . implode( '<br/>', $errors ) . '</p></div>'; echo wp_kses_post( $notice ); // Remove transient after displaying notice. delete_transient( $this->error_transient ); } /** * Store an error message in a transient then redirect. * * @param object $error A WP_Error object. * @return void */ private function set_error( $error ) { set_transient( $this->error_transient, $error, 30 ); wp_safe_redirect( wp_get_referer() ); exit; } /** * Returns a custom download url for plugin if exists. * * @param string $plugin_slug plugin slug. * @return string|bool */ private function maybe_get_custom_download_url( string $plugin_slug ) { $parent_plugin_slug = $this->get_parent_plugin_slug(); $urls = [ 'seo-by-rank-math' => 'https://rankmath.com/downloads/plugin-family/' . $parent_plugin_slug, ]; if ( ! isset( $urls[ $plugin_slug ] ) ) { return false; } return $urls[ $plugin_slug ]; } /** * Get parent plugin slug. * * @return string */ private function get_parent_plugin_slug(): string { $plugin_path = plugin_basename( __FILE__ ); $chunks = explode( '/', $plugin_path ); return $chunks[0]; } } Dependencies/WPMedia/PluginFamily/Controller/PluginFamilyInterface.php 0000644 00000000505 15174671731 0022111 0 ustar 00 <?php namespace Imagify\Dependencies\WPMedia\PluginFamily\Controller; interface PluginFamilyInterface { /** * Process to install and activate plugin. * * @return void */ public function install_activate(); /** * Maybe display error notice. * * @return void */ public function display_error_notice(); } Dependencies/League/Container/Container.php 0000644 00000013506 15174671731 0014732 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container; use Imagify\Dependencies\League\Container\Definition\{DefinitionAggregate, DefinitionInterface, DefinitionAggregateInterface}; use Imagify\Dependencies\League\Container\Exception\{NotFoundException, ContainerException}; use Imagify\Dependencies\League\Container\Inflector\{InflectorAggregate, InflectorInterface, InflectorAggregateInterface}; use Imagify\Dependencies\League\Container\ServiceProvider\{ServiceProviderAggregate, ServiceProviderAggregateInterface, ServiceProviderInterface}; use Imagify\Dependencies\Psr\Container\ContainerInterface; class Container implements DefinitionContainerInterface { /** * @var boolean */ protected $defaultToShared = false; /** * @var DefinitionAggregateInterface */ protected $definitions; /** * @var ServiceProviderAggregateInterface */ protected $providers; /** * @var InflectorAggregateInterface */ protected $inflectors; /** * @var ContainerInterface[] */ protected $delegates = []; public function __construct( ?DefinitionAggregateInterface $definitions = null, ?ServiceProviderAggregateInterface $providers = null, ?InflectorAggregateInterface $inflectors = null ) { $this->definitions = $definitions ?? new DefinitionAggregate(); $this->providers = $providers ?? new ServiceProviderAggregate(); $this->inflectors = $inflectors ?? new InflectorAggregate(); if ($this->definitions instanceof ContainerAwareInterface) { $this->definitions->setContainer($this); } if ($this->providers instanceof ContainerAwareInterface) { $this->providers->setContainer($this); } if ($this->inflectors instanceof ContainerAwareInterface) { $this->inflectors->setContainer($this); } } public function add(string $id, $concrete = null): DefinitionInterface { $concrete = $concrete ?? $id; if (true === $this->defaultToShared) { return $this->addShared($id, $concrete); } return $this->definitions->add($id, $concrete); } public function addShared(string $id, $concrete = null): DefinitionInterface { $concrete = $concrete ?? $id; return $this->definitions->addShared($id, $concrete); } public function defaultToShared(bool $shared = true): ContainerInterface { $this->defaultToShared = $shared; return $this; } public function extend(string $id): DefinitionInterface { if ($this->providers->provides($id)) { $this->providers->register($id); } if ($this->definitions->has($id)) { return $this->definitions->getDefinition($id); } throw new NotFoundException(sprintf( 'Unable to extend alias (%s) as it is not being managed as a definition', $id )); } public function addServiceProvider(ServiceProviderInterface $provider): DefinitionContainerInterface { $this->providers->add($provider); return $this; } /** * @template RequestedType * * @param class-string<RequestedType>|string $id * * @return RequestedType|mixed */ public function get($id) { return $this->resolve($id); } /** * @template RequestedType * * @param class-string<RequestedType>|string $id * * @return RequestedType|mixed */ public function getNew($id) { return $this->resolve($id, true); } public function has($id): bool { if ($this->definitions->has($id)) { return true; } if ($this->definitions->hasTag($id)) { return true; } if ($this->providers->provides($id)) { return true; } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { return true; } } return false; } public function inflector(string $type, ?callable $callback = null): InflectorInterface { return $this->inflectors->add($type, $callback); } public function delegate(ContainerInterface $container): self { $this->delegates[] = $container; if ($container instanceof ContainerAwareInterface) { $container->setContainer($this); } return $this; } protected function resolve($id, bool $new = false) { if ($this->definitions->has($id)) { $resolved = (true === $new) ? $this->definitions->resolveNew($id) : $this->definitions->resolve($id); return $this->inflectors->inflect($resolved); } if ($this->definitions->hasTag($id)) { $arrayOf = (true === $new) ? $this->definitions->resolveTaggedNew($id) : $this->definitions->resolveTagged($id); array_walk($arrayOf, function (&$resolved) { $resolved = $this->inflectors->inflect($resolved); }); return $arrayOf; } if ($this->providers->provides($id)) { $this->providers->register($id); if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); } return $this->resolve($id, $new); } foreach ($this->delegates as $delegate) { if ($delegate->has($id)) { $resolved = $delegate->get($id); return $this->inflectors->inflect($resolved); } } throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); } } Dependencies/League/Container/Exception/NotFoundException.php 0000644 00000000442 15174671731 0020354 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Exception; use Imagify\Dependencies\Psr\Container\NotFoundExceptionInterface; use InvalidArgumentException; class NotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface { } Dependencies/League/Container/Exception/ContainerException.php 0000644 00000000425 15174671731 0020543 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Exception; use Imagify\Dependencies\Psr\Container\ContainerExceptionInterface; use RuntimeException; class ContainerException extends RuntimeException implements ContainerExceptionInterface { } Dependencies/League/Container/Argument/ArgumentResolverInterface.php 0000644 00000000652 15174671731 0021715 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; use Imagify\Dependencies\League\Container\ContainerAwareInterface; use ReflectionFunctionAbstract; interface ArgumentResolverInterface extends ContainerAwareInterface { public function resolveArguments(array $arguments): array; public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array; } Dependencies/League/Container/Argument/ArgumentResolverTrait.php 0000644 00000007113 15174671731 0021077 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; use Imagify\Dependencies\League\Container\DefinitionContainerInterface; use Imagify\Dependencies\League\Container\Exception\{ContainerException, NotFoundException}; use Imagify\Dependencies\League\Container\ReflectionContainer; use Imagify\Dependencies\Psr\Container\ContainerInterface; use ReflectionFunctionAbstract; use ReflectionNamedType; trait ArgumentResolverTrait { public function resolveArguments(array $arguments): array { try { $container = $this->getContainer(); } catch (ContainerException $e) { $container = ($this instanceof ReflectionContainer) ? $this : null; } foreach ($arguments as &$arg) { // if we have a literal, we don't want to do anything more with it if ($arg instanceof LiteralArgumentInterface) { $arg = $arg->getValue(); continue; } if ($arg instanceof ArgumentInterface) { $argValue = $arg->getValue(); } else { $argValue = $arg; } if (!is_string($argValue)) { continue; } // resolve the argument from the container, if it happens to be another // argument wrapper, use that value if ($container instanceof ContainerInterface && $container->has($argValue)) { try { $arg = $container->get($argValue); if ($arg instanceof ArgumentInterface) { $arg = $arg->getValue(); } continue; } catch (NotFoundException $e) { } } // if we have a default value, we use that, no more resolution as // we expect a default/optional argument value to be literal if ($arg instanceof DefaultValueInterface) { $arg = $arg->getDefaultValue(); } } return $arguments; } public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []): array { $params = $method->getParameters(); $arguments = []; foreach ($params as $param) { $name = $param->getName(); // if we've been given a value for the argument, treat as literal if (array_key_exists($name, $args)) { $arguments[] = new LiteralArgument($args[$name]); continue; } $type = $param->getType(); if ($type instanceof ReflectionNamedType) { // in PHP 8, nullable arguments have "?" prefix $typeHint = ltrim($type->getName(), '?'); if ($param->isDefaultValueAvailable()) { $arguments[] = new DefaultValueArgument($typeHint, $param->getDefaultValue()); continue; } $arguments[] = new ResolvableArgument($typeHint); continue; } if ($param->isDefaultValueAvailable()) { $arguments[] = new LiteralArgument($param->getDefaultValue()); continue; } throw new NotFoundException(sprintf( 'Unable to resolve a value for parameter (%s) in the function/method (%s)', $name, $method->getName() )); } return $this->resolveArguments($arguments); } abstract public function getContainer(): DefinitionContainerInterface; } Dependencies/League/Container/Argument/LiteralArgumentInterface.php 0000644 00000000235 15174671731 0021505 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; interface LiteralArgumentInterface extends ArgumentInterface { } Dependencies/League/Container/Argument/LiteralArgument.php 0000644 00000002316 15174671731 0017666 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; use InvalidArgumentException; class LiteralArgument implements LiteralArgumentInterface { public const TYPE_ARRAY = 'array'; public const TYPE_BOOL = 'boolean'; public const TYPE_BOOLEAN = 'boolean'; public const TYPE_CALLABLE = 'callable'; public const TYPE_DOUBLE = 'double'; public const TYPE_FLOAT = 'double'; public const TYPE_INT = 'integer'; public const TYPE_INTEGER = 'integer'; public const TYPE_OBJECT = 'object'; public const TYPE_STRING = 'string'; /** * @var mixed */ protected $value; public function __construct($value, ?string $type = null) { if ( null === $type || ($type === self::TYPE_CALLABLE && is_callable($value)) || ($type === self::TYPE_OBJECT && is_object($value)) || gettype($value) === $type ) { $this->value = $value; } else { throw new InvalidArgumentException('Incorrect type for value.'); } } /** * {@inheritdoc} */ public function getValue() { return $this->value; } } Dependencies/League/Container/Argument/DefaultValueInterface.php 0000644 00000000346 15174671731 0020772 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; interface DefaultValueInterface extends ArgumentInterface { /** * @return mixed */ public function getDefaultValue(); } Dependencies/League/Container/Argument/Literal/BooleanArgument.php 0000644 00000000525 15174671731 0021245 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class BooleanArgument extends LiteralArgument { public function __construct(bool $value) { parent::__construct($value, LiteralArgument::TYPE_BOOL); } } Dependencies/League/Container/Argument/Literal/CallableArgument.php 0000644 00000000536 15174671731 0021367 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class CallableArgument extends LiteralArgument { public function __construct(callable $value) { parent::__construct($value, LiteralArgument::TYPE_CALLABLE); } } Dependencies/League/Container/Argument/Literal/IntegerArgument.php 0000644 00000000523 15174671731 0021261 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class IntegerArgument extends LiteralArgument { public function __construct(int $value) { parent::__construct($value, LiteralArgument::TYPE_INT); } } Dependencies/League/Container/Argument/Literal/StringArgument.php 0000644 00000000530 15174671731 0021130 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class StringArgument extends LiteralArgument { public function __construct(string $value) { parent::__construct($value, LiteralArgument::TYPE_STRING); } } Dependencies/League/Container/Argument/Literal/FloatArgument.php 0000644 00000000525 15174671731 0020733 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class FloatArgument extends LiteralArgument { public function __construct(float $value) { parent::__construct($value, LiteralArgument::TYPE_FLOAT); } } Dependencies/League/Container/Argument/Literal/ArrayArgument.php 0000644 00000000525 15174671731 0020744 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class ArrayArgument extends LiteralArgument { public function __construct(array $value) { parent::__construct($value, LiteralArgument::TYPE_ARRAY); } } Dependencies/League/Container/Argument/Literal/ObjectArgument.php 0000644 00000000530 15174671731 0021070 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument\Literal; use Imagify\Dependencies\League\Container\Argument\LiteralArgument; class ObjectArgument extends LiteralArgument { public function __construct(object $value) { parent::__construct($value, LiteralArgument::TYPE_OBJECT); } } Dependencies/League/Container/Argument/ResolvableArgument.php 0000644 00000000542 15174671731 0020367 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; class ResolvableArgument implements ResolvableArgumentInterface { protected $value; public function __construct(string $value) { $this->value = $value; } public function getValue(): string { return $this->value; } } Dependencies/League/Container/Argument/ResolvableArgumentInterface.php 0000644 00000000310 15174671731 0022201 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; interface ResolvableArgumentInterface extends ArgumentInterface { public function getValue(): string; } Dependencies/League/Container/Argument/DefaultValueArgument.php 0000644 00000000771 15174671731 0020656 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; class DefaultValueArgument extends ResolvableArgument implements DefaultValueInterface { protected $defaultValue; public function __construct(string $value, $defaultValue = null) { $this->defaultValue = $defaultValue; parent::__construct($value); } /** * @return mixed|null */ public function getDefaultValue() { return $this->defaultValue; } } Dependencies/League/Container/Argument/ArgumentInterface.php 0000644 00000000301 15174671731 0020162 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Argument; interface ArgumentInterface { /** * @return mixed */ public function getValue(); } Dependencies/League/Container/Inflector/InflectorAggregate.php 0000644 00000001754 15174671731 0020473 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Inflector; use Generator; use Imagify\Dependencies\League\Container\ContainerAwareTrait; class InflectorAggregate implements InflectorAggregateInterface { use ContainerAwareTrait; /** * @var Inflector[] */ protected $inflectors = []; public function add(string $type, ?callable $callback = null): Inflector { $inflector = new Inflector($type, $callback); $this->inflectors[] = $inflector; return $inflector; } public function inflect($object) { foreach ($this->getIterator() as $inflector) { $type = $inflector->getType(); if ($object instanceof $type) { $inflector->setContainer($this->getContainer()); $inflector->inflect($object); } } return $object; } public function getIterator(): Generator { yield from $this->inflectors; } } Dependencies/League/Container/Inflector/InflectorAggregateInterface.php 0000644 00000000620 15174671731 0022303 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Inflector; use IteratorAggregate; use Imagify\Dependencies\League\Container\ContainerAwareInterface; interface InflectorAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(string $type, ?callable $callback = null): Inflector; public function inflect(object $object); } Dependencies/League/Container/Inflector/Inflector.php 0000644 00000004573 15174671731 0016666 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Inflector; use Imagify\Dependencies\League\Container\Argument\ArgumentResolverInterface; use Imagify\Dependencies\League\Container\Argument\ArgumentResolverTrait; use Imagify\Dependencies\League\Container\ContainerAwareTrait; class Inflector implements ArgumentResolverInterface, InflectorInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $type; /** * @var callable|null */ protected $callback; /** * @var array */ protected $methods = []; /** * @var array */ protected $properties = []; public function __construct(string $type, ?callable $callback = null) { $this->type = $type; $this->callback = $callback; } public function getType(): string { return $this->type; } public function invokeMethod(string $name, array $args): InflectorInterface { $this->methods[$name] = $args; return $this; } public function invokeMethods(array $methods): InflectorInterface { foreach ($methods as $name => $args) { $this->invokeMethod($name, $args); } return $this; } public function setProperty(string $property, $value): InflectorInterface { $this->properties[$property] = $this->resolveArguments([$value])[0]; return $this; } public function setProperties(array $properties): InflectorInterface { foreach ($properties as $property => $value) { $this->setProperty($property, $value); } return $this; } public function inflect(object $object): void { $properties = $this->resolveArguments(array_values($this->properties)); $properties = array_combine(array_keys($this->properties), $properties); // array_combine() can technically return false foreach ($properties ?: [] as $property => $value) { $object->{$property} = $value; } foreach ($this->methods as $method => $args) { $args = $this->resolveArguments($args); $callable = [$object, $method]; call_user_func_array($callable, $args); } if ($this->callback !== null) { call_user_func($this->callback, $object); } } } Dependencies/League/Container/Inflector/InflectorInterface.php 0000644 00000001011 15174671731 0020467 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Inflector; interface InflectorInterface { public function getType(): string; public function inflect(object $object): void; public function invokeMethod(string $name, array $args): InflectorInterface; public function invokeMethods(array $methods): InflectorInterface; public function setProperties(array $properties): InflectorInterface; public function setProperty(string $property, $value): InflectorInterface; } Dependencies/League/Container/Definition/DefinitionAggregate.php 0000644 00000006037 15174671731 0021000 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Definition; use Generator; use Imagify\Dependencies\League\Container\ContainerAwareTrait; use Imagify\Dependencies\League\Container\Exception\NotFoundException; class DefinitionAggregate implements DefinitionAggregateInterface { use ContainerAwareTrait; /** * @var DefinitionInterface[] */ protected $definitions = []; public function __construct(array $definitions = []) { $this->definitions = array_filter($definitions, static function ($definition) { return ($definition instanceof DefinitionInterface); }); } public function add(string $id, $definition): DefinitionInterface { if (false === ($definition instanceof DefinitionInterface)) { $definition = new Definition($id, $definition); } $this->definitions[] = $definition->setAlias($id); return $definition; } public function addShared(string $id, $definition): DefinitionInterface { $definition = $this->add($id, $definition); return $definition->setShared(true); } public function has(string $id): bool { $id = Definition::normaliseAlias($id); foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return true; } } return false; } public function hasTag(string $tag): bool { foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { return true; } } return false; } public function getDefinition(string $id): DefinitionInterface { $id = Definition::normaliseAlias($id); foreach ($this->getIterator() as $definition) { if ($id === $definition->getAlias()) { return $definition->setContainer($this->getContainer()); } } throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); } public function resolve(string $id) { return $this->getDefinition($id)->resolve(); } public function resolveNew(string $id) { return $this->getDefinition($id)->resolveNew(); } public function resolveTagged(string $tag): array { $arrayOf = []; foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { $arrayOf[] = $definition->setContainer($this->getContainer())->resolve(); } } return $arrayOf; } public function resolveTaggedNew(string $tag): array { $arrayOf = []; foreach ($this->getIterator() as $definition) { if ($definition->hasTag($tag)) { $arrayOf[] = $definition->setContainer($this->getContainer())->resolveNew(); } } return $arrayOf; } public function getIterator(): Generator { yield from $this->definitions; } } Dependencies/League/Container/Definition/DefinitionAggregateInterface.php 0000644 00000001417 15174671731 0022616 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Definition; use IteratorAggregate; use Imagify\Dependencies\League\Container\ContainerAwareInterface; interface DefinitionAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(string $id, $definition): DefinitionInterface; public function addShared(string $id, $definition): DefinitionInterface; public function getDefinition(string $id): DefinitionInterface; public function has(string $id): bool; public function hasTag(string $tag): bool; public function resolve(string $id); public function resolveNew(string $id); public function resolveTagged(string $tag): array; public function resolveTaggedNew(string $tag): array; } Dependencies/League/Container/Definition/DefinitionInterface.php 0000644 00000001755 15174671731 0021014 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Definition; use Imagify\Dependencies\League\Container\ContainerAwareInterface; interface DefinitionInterface extends ContainerAwareInterface { public function addArgument($arg): DefinitionInterface; public function addArguments(array $args): DefinitionInterface; public function addMethodCall(string $method, array $args = []): DefinitionInterface; public function addMethodCalls(array $methods = []): DefinitionInterface; public function addTag(string $tag): DefinitionInterface; public function getAlias(): string; public function getConcrete(); public function hasTag(string $tag): bool; public function isShared(): bool; public function resolve(); public function resolveNew(); public function setAlias(string $id): DefinitionInterface; public function setConcrete($concrete): DefinitionInterface; public function setShared(bool $shared): DefinitionInterface; } Dependencies/League/Container/Definition/Definition.php 0000644 00000013431 15174671731 0017165 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\Definition; use Imagify\Dependencies\League\Container\Argument\{ ArgumentResolverInterface, ArgumentResolverTrait, ArgumentInterface, LiteralArgumentInterface }; use Imagify\Dependencies\League\Container\ContainerAwareTrait; use Imagify\Dependencies\League\Container\Exception\ContainerException; use Imagify\Dependencies\League\Container\Exception\NotFoundException; use Imagify\Dependencies\Psr\Container\ContainerInterface; use ReflectionClass; class Definition implements ArgumentResolverInterface, DefinitionInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var string */ protected $alias; /** * @var mixed */ protected $concrete; /** * @var boolean */ protected $shared = false; /** * @var array */ protected $tags = []; /** * @var array */ protected $arguments = []; /** * @var array */ protected $methods = []; /** * @var mixed */ protected $resolved; /** * @var array */ protected $recursiveCheck = []; /** * @param string $id * @param mixed|null $concrete */ public function __construct(string $id, $concrete = null) { $id = static::normaliseAlias($id); $concrete = $concrete ?? $id; $this->alias = $id; $this->concrete = $concrete; } public function addTag(string $tag): DefinitionInterface { $this->tags[$tag] = true; return $this; } public function hasTag(string $tag): bool { return isset($this->tags[$tag]); } public function setAlias(string $id): DefinitionInterface { $id = static::normaliseAlias($id); $this->alias = $id; return $this; } public function getAlias(): string { return $this->alias; } public function setShared(bool $shared = true): DefinitionInterface { $this->shared = $shared; return $this; } public function isShared(): bool { return $this->shared; } public function getConcrete() { return $this->concrete; } public function setConcrete($concrete): DefinitionInterface { $this->concrete = $concrete; $this->resolved = null; return $this; } public function addArgument($arg): DefinitionInterface { $this->arguments[] = $arg; return $this; } public function addArguments(array $args): DefinitionInterface { foreach ($args as $arg) { $this->addArgument($arg); } return $this; } public function addMethodCall(string $method, array $args = []): DefinitionInterface { $this->methods[] = [ 'method' => $method, 'arguments' => $args ]; return $this; } public function addMethodCalls(array $methods = []): DefinitionInterface { foreach ($methods as $method => $args) { $this->addMethodCall($method, $args); } return $this; } public function resolve() { if (null !== $this->resolved && $this->isShared()) { return $this->resolved; } return $this->resolveNew(); } public function resolveNew() { $concrete = $this->concrete; if (is_callable($concrete)) { $concrete = $this->resolveCallable($concrete); } if ($concrete instanceof LiteralArgumentInterface) { $this->resolved = $concrete->getValue(); return $concrete->getValue(); } if ($concrete instanceof ArgumentInterface) { $concrete = $concrete->getValue(); } if (is_string($concrete) && class_exists($concrete)) { $concrete = $this->resolveClass($concrete); } if (is_object($concrete)) { $concrete = $this->invokeMethods($concrete); } try { $container = $this->getContainer(); } catch (ContainerException $e) { $container = null; } // stop recursive resolving if (is_string($concrete) && in_array($concrete, $this->recursiveCheck)) { $this->resolved = $concrete; return $concrete; } // if we still have a string, try to pull it from the container // this allows for `alias -> alias -> ... -> concrete if (is_string($concrete) && $container instanceof ContainerInterface && $container->has($concrete)) { $this->recursiveCheck[] = $concrete; $concrete = $container->get($concrete); } $this->resolved = $concrete; return $concrete; } /** * @param callable $concrete * @return mixed */ protected function resolveCallable(callable $concrete) { $resolved = $this->resolveArguments($this->arguments); return call_user_func_array($concrete, $resolved); } protected function resolveClass(string $concrete): object { $resolved = $this->resolveArguments($this->arguments); $reflection = new ReflectionClass($concrete); return $reflection->newInstanceArgs($resolved); } protected function invokeMethods(object $instance): object { foreach ($this->methods as $method) { $args = $this->resolveArguments($method['arguments']); $callable = [$instance, $method['method']]; call_user_func_array($callable, $args); } return $instance; } public static function normaliseAlias(string $alias): string { if (strpos($alias, '\\') === 0) { return substr($alias, 1); } return $alias; } } Dependencies/League/Container/ServiceProvider/ServiceProviderInterface.php 0000644 00000000672 15174671731 0023057 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\ServiceProvider; use Imagify\Dependencies\League\Container\ContainerAwareInterface; interface ServiceProviderInterface extends ContainerAwareInterface { public function getIdentifier(): string; public function provides(string $id): bool; public function register(): void; public function setIdentifier(string $id): ServiceProviderInterface; } Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php 0000644 00000000641 15174671731 0024523 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\ServiceProvider; interface BootableServiceProviderInterface extends ServiceProviderInterface { /** * Method will be invoked on registration of a service provider implementing * this interface. Provides ability for eager loading of Service Providers. * * @return void */ public function boot(): void; } Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php 0000644 00000000746 15174671731 0024670 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\ServiceProvider; use IteratorAggregate; use Imagify\Dependencies\League\Container\ContainerAwareInterface; interface ServiceProviderAggregateInterface extends ContainerAwareInterface, IteratorAggregate { public function add(ServiceProviderInterface $provider): ServiceProviderAggregateInterface; public function provides(string $id): bool; public function register(string $service): void; } Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php 0000644 00000001111 15174671731 0022707 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\ServiceProvider; use Imagify\Dependencies\League\Container\ContainerAwareTrait; abstract class AbstractServiceProvider implements ServiceProviderInterface { use ContainerAwareTrait; /** * @var string */ protected $identifier; public function getIdentifier(): string { return $this->identifier ?? get_class($this); } public function setIdentifier(string $id): ServiceProviderInterface { $this->identifier = $id; return $this; } } Dependencies/League/Container/ServiceProvider/ServiceProviderAggregate.php 0000644 00000003613 15174671731 0023043 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container\ServiceProvider; use Generator; use Imagify\Dependencies\League\Container\Exception\ContainerException; use Imagify\Dependencies\League\Container\{ContainerAwareInterface, ContainerAwareTrait}; class ServiceProviderAggregate implements ServiceProviderAggregateInterface { use ContainerAwareTrait; /** * @var ServiceProviderInterface[] */ protected $providers = []; /** * @var array */ protected $registered = []; public function add(ServiceProviderInterface $provider): ServiceProviderAggregateInterface { if (in_array($provider, $this->providers, true)) { return $this; } $provider->setContainer($this->getContainer()); if ($provider instanceof BootableServiceProviderInterface) { $provider->boot(); } $this->providers[] = $provider; return $this; } public function provides(string $service): bool { foreach ($this->getIterator() as $provider) { if ($provider->provides($service)) { return true; } } return false; } public function getIterator(): Generator { yield from $this->providers; } public function register(string $service): void { if (false === $this->provides($service)) { throw new ContainerException( sprintf('(%s) is not provided by a service provider', $service) ); } foreach ($this->getIterator() as $provider) { if (in_array($provider->getIdentifier(), $this->registered, true)) { continue; } if ($provider->provides($service)) { $provider->register(); $this->registered[] = $provider->getIdentifier(); } } } } Dependencies/League/Container/DefinitionContainerInterface.php 0000644 00000001546 15174671731 0020565 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container; use Imagify\Dependencies\League\Container\Definition\DefinitionInterface; use Imagify\Dependencies\League\Container\Inflector\InflectorInterface; use Imagify\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface; use Imagify\Dependencies\Psr\Container\ContainerInterface; interface DefinitionContainerInterface extends ContainerInterface { public function add(string $id, $concrete = null): DefinitionInterface; public function addServiceProvider(ServiceProviderInterface $provider): self; public function addShared(string $id, $concrete = null): DefinitionInterface; public function extend(string $id): DefinitionInterface; public function getNew($id); public function inflector(string $type, ?callable $callback = null): InflectorInterface; } Dependencies/League/Container/ContainerAwareInterface.php 0000644 00000000437 15174671731 0017532 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container; interface ContainerAwareInterface { public function getContainer(): DefinitionContainerInterface; public function setContainer(DefinitionContainerInterface $container): ContainerAwareInterface; } Dependencies/League/Container/ReflectionContainer.php 0000644 00000006271 15174671731 0016746 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container; use Imagify\Dependencies\League\Container\Argument\{ArgumentResolverInterface, ArgumentResolverTrait}; use Imagify\Dependencies\League\Container\Exception\ContainerException; use Imagify\Dependencies\League\Container\Exception\NotFoundException; use Imagify\Dependencies\Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; class ReflectionContainer implements ArgumentResolverInterface, ContainerInterface { use ArgumentResolverTrait; use ContainerAwareTrait; /** * @var boolean */ protected $cacheResolutions; /** * @var array */ protected $cache = []; public function __construct(bool $cacheResolutions = false) { $this->cacheResolutions = $cacheResolutions; } public function get($id, array $args = []) { if ($this->cacheResolutions === true && array_key_exists($id, $this->cache)) { return $this->cache[$id]; } if (!$this->has($id)) { throw new NotFoundException( sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) ); } $reflector = new ReflectionClass($id); $construct = $reflector->getConstructor(); if ($construct && !$construct->isPublic()) { throw new NotFoundException( sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id) ); } $resolution = $construct === null ? new $id() : $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) ; if ($this->cacheResolutions === true) { $this->cache[$id] = $resolution; } return $resolution; } public function has($id): bool { return class_exists($id); } public function call(callable $callable, array $args = []) { if (is_string($callable) && strpos($callable, '::') !== false) { $callable = explode('::', $callable); } if (is_array($callable)) { if (is_string($callable[0])) { // if we have a definition container, try that first, otherwise, reflect try { $callable[0] = $this->getContainer()->get($callable[0]); } catch (ContainerException $e) { $callable[0] = $this->get($callable[0]); } } $reflection = new ReflectionMethod($callable[0], $callable[1]); if ($reflection->isStatic()) { $callable[0] = null; } return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); } if (is_object($callable)) { $reflection = new ReflectionMethod($callable, '__invoke'); return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); } $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); } } Dependencies/League/Container/ContainerAwareTrait.php 0000644 00000002010 15174671731 0016702 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Dependencies\League\Container; use BadMethodCallException; use Imagify\Dependencies\League\Container\Exception\ContainerException; trait ContainerAwareTrait { /** * @var ?DefinitionContainerInterface */ protected $container; public function setContainer(DefinitionContainerInterface $container): ContainerAwareInterface { $this->container = $container; if ($this instanceof ContainerAwareInterface) { return $this; } throw new BadMethodCallException(sprintf( 'Attempt to use (%s) while not implementing (%s)', ContainerAwareTrait::class, ContainerAwareInterface::class )); } public function getContainer(): DefinitionContainerInterface { if ($this->container instanceof DefinitionContainerInterface) { return $this->container; } throw new ContainerException('No container implementation has been set.'); } } Avif/Apache.php 0000644 00000001124 15174671731 0007335 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif; use Imagify\WriteFile\AbstractApacheDirConfFile; /** * Add and remove contents to the .htaccess file to display AVIF images on the site. */ class Apache extends AbstractApacheDirConfFile { /** * Name of the tag used as block delemiter. * * @var string */ const TAG_NAME = 'Imagify: avif file type'; /** * Get unfiltered new contents to write into the file. * * @return string */ protected function get_raw_new_contents() { return trim( ' <IfModule mod_mime.c> AddType image/avif .avif </IfModule>' ); } } Avif/RewriteRules/Apache.php 0000644 00000002661 15174671731 0012000 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif\RewriteRules; use Imagify\WriteFile\AbstractApacheDirConfFile; /** * Add and remove rewrite rules to the .htaccess file to display AVIF images on the site. */ class Apache extends AbstractApacheDirConfFile { /** * Name of the tag used as block delimiter. * * @var string */ const TAG_NAME = 'Imagify: rewrite rules for avif'; /** * Get unfiltered new contents to write into the file. * * @access protected * * @return string */ protected function get_raw_new_contents() { $extensions = $this->get_extensions_pattern(); $extensions = str_replace( '|avif', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' <IfModule mod_setenvif.c> # Vary: Accept for all the requests to jpeg, png, and gif. SetEnvIf Request_URI "\.(' . $extensions . ')$" REQUEST_image </IfModule> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase ' . $home_root . ' # Check if browser supports AVIF images. # Update the MIME type accordingly. RewriteCond %{HTTP_ACCEPT} image/avif # Check if AVIF replacement image exists. RewriteCond %{REQUEST_FILENAME}.avif -f # Serve AVIF image instead. RewriteRule (.+)\.(' . $extensions . ')$ $1.$2.avif [T=image/avif,NC] </IfModule> <IfModule mod_headers.c> # Update the MIME type accordingly. Header append Vary Accept env=REQUEST_image </IfModule>' ); } } Avif/RewriteRules/Display.php 0000644 00000011702 15174671731 0012220 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif\RewriteRules; use Imagify\EventManagement\SubscriberInterface; use Imagify\Notices\Notices; use Imagify\WriteFile\WriteFileInterface; /** * Display Avif images on the site with rewrite rules. */ class Display implements SubscriberInterface { /** * Configuration file writer. * * @var WriteFileInterface|null */ protected $server_conf = null; /** * Option value. * * @var string */ const OPTION_VALUE = 'rewrite'; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_settings_on_save' => [ 'maybe_add_rewrite_rules', 11 ], 'imagify_settings_webp_info' => 'maybe_add_avif_info', 'imagify_activation' => 'activate', 'imagify_deactivation' => 'deactivate', ]; } /** * If display AVIF images via rewrite rules, add the rules to the .htaccess/etc file. * * @since 1.9 * * @param array $values The option values. * * @return array */ public function maybe_add_rewrite_rules( $values ) { $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); $is_enabled = ! empty( $values['display_nextgen'] ); // Which method? $old_value = get_imagify_option( 'display_nextgen_method' ); $new_value = ! empty( $values['display_nextgen_method'] ) ? $values['display_nextgen_method'] : ''; // Decide when to add or remove rules. $is_rewrite = self::OPTION_VALUE === $new_value; $was_rewrite = self::OPTION_VALUE === $old_value; if ( ! $this->get_server_conf() ) { return $values; } $result = false; if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) { // Add the rewrite rules. $result = $this->get_server_conf()->add(); } elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) { // Remove the rewrite rules. $result = $this->get_server_conf()->remove(); } if ( ! is_wp_error( $result ) ) { return $values; } // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); return $values; } Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); return $values; } /** * If the conf file is not writable, add a warning. */ public function maybe_add_avif_info() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } $writable = $conf->is_file_writable(); if ( is_wp_error( $writable ) ) { $rules = $conf->get_new_contents(); if ( ! $rules ) { // Uh? return; } printf( /* translators: %s is a file name. */ esc_html__( 'If you choose to use rewrite rules, you will have to add the following lines manually to the %s file:', 'imagify' ), '<code>' . $this->get_file_path( true ) . '</code>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<pre class="code">' . esc_html( $rules ) . '</pre>'; } } /** * Add rules on plugin activation. */ public function activate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } if ( is_wp_error( $conf->is_file_writable() ) ) { return; } $conf->add(); } /** * Remove rules on plugin deactivation. * * @since 1.9 */ public function deactivate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } $file_path = $conf->get_file_path(); $filesystem = \Imagify_Filesystem::get_instance(); if ( ! $filesystem->exists( $file_path ) ) { return; } if ( ! $filesystem->is_writable( $file_path ) ) { return; } $conf->remove(); } /** * Get the path to the directory conf file. * * @param bool $relative True to get a path relative to the site’s root. * * @return string|bool The file path. False on failure. */ public function get_file_path( $relative = false ) { if ( ! $this->get_server_conf() ) { return false; } $file_path = $this->get_server_conf()->get_file_path(); if ( $relative ) { return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); } return $file_path; } /** * Get the server conf instance. * * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7, $is_nginx; if ( isset( $this->server_conf ) ) { return $this->server_conf; } if ( $is_apache ) { $this->server_conf = new Apache(); } elseif ( $is_iis7 ) { $this->server_conf = new IIS(); } elseif ( $is_nginx ) { $this->server_conf = new Nginx(); } return $this->server_conf; } } Avif/RewriteRules/IIS.php 0000644 00000003444 15174671731 0011243 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif\RewriteRules; use Imagify\WriteFile\AbstractIISDirConfFile; /** * Add and remove rewrite rules to the web.config file to display AVIF images on the site. */ class IIS extends AbstractIISDirConfFile { /** * Name of the tag used as block delemiter. * * @var string */ const TAG_NAME = 'Imagify: rewrite rules for avif'; /** * Get unfiltered new contents to write into the file. * * @source https://github.com/igrigorik/webp-detect/blob/master/iis.config * * @return string */ protected function get_raw_new_contents() { $extensions = $this->get_extensions_pattern(); $extensions = str_replace( '|avif', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' <!-- @parent /configuration/system.webServer/rewrite/rules --> <rule name="' . esc_attr( static::TAG_NAME ) . ' 2"> <match url="^(' . $home_root . '.+)\.(' . $extensions . ')$" ignoreCase="true" /> <conditions logicalGrouping="MatchAll"> <add input="{HTTP_ACCEPT}" pattern="image/avif" ignoreCase="false" /> <add input="{DOCUMENT_ROOT}/{R:1}{R:2}.avif" matchType="IsFile" /> </conditions> <action type="Rewrite" url="{R:1}{R:2}.avif" logRewrittenUrl="true" /> <serverVariables> <set name="ACCEPTS_AVIF" value="true" /> </serverVariables> </rule> <!-- @parent /configuration/system.webServer/rewrite/outboundRules --> <rule preCondition="IsAvif" name="' . esc_attr( static::TAG_NAME ) . ' 3"> <match serverVariable="RESPONSE_Vary" pattern=".*" /> <action type="Rewrite" value="Accept"/> </rule> <preConditions name="' . esc_attr( static::TAG_NAME ) . ' 4"> <preCondition name="IsAvif"> <add input="{ACCEPTS_AVIF}" pattern="true" ignoreCase="false" /> </preCondition> </preConditions>' ); } } Avif/RewriteRules/Nginx.php 0000644 00000001102 15174671731 0011667 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif\RewriteRules; use Imagify\WriteFile\AbstractNginxDirConfFile; /** * Add and remove rewrite rules to the imagify.conf file to display AVIF images on the site. */ class Nginx extends AbstractNginxDirConfFile { /** * Name of the tag used as block delimiter. * * @var string */ const TAG_NAME = 'Imagify: rewrite rules for avif'; /** * Get unfiltered new contents to write into the file. * * @access protected * * @return string */ protected function get_raw_new_contents() { return ''; } } Avif/ServiceProvider.php 0000644 00000002240 15174671731 0011267 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use Imagify\Avif\RewriteRules\Display as RewriteRules; /** * Service provider for AVIF rewrite rules */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ Display::class, RewriteRules::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ Display::class, RewriteRules::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( Display::class ); $this->getContainer()->addShared( RewriteRules::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } Avif/Display.php 0000644 00000006706 15174671731 0007574 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif; use Imagify\EventManagement\SubscriberInterface; use Imagify\Notices\Notices; use Imagify\WriteFile\WriteFileInterface; /** * Display AVIF images on the site using picture tag. */ class Display implements SubscriberInterface { /** * Server conf object. * * @var WriteFileInterface|null * @since 1.9 */ protected $server_conf = null; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_settings_on_save' => [ 'maybe_add_rewrite_rules', 12 ], 'imagify_activation' => 'activate', 'imagify_deactivation' => 'deactivate', ]; } /** * If display Next-Gen images, add the AVIF type to the .htaccess/etc file. * * @since 1.9 * * @param array $values The option values. * * @return array */ public function maybe_add_rewrite_rules( $values ) { if ( ! $this->get_server_conf() ) { return $values; } $enabled = isset( $values['display_nextgen'] ) ? true : false; $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); $result = false; if ( $enabled && ! $was_enabled ) { // Add the WebP file type. $result = $this->get_server_conf()->add(); } elseif ( ! $enabled && $was_enabled ) { // Remove the WebP file type. $result = $this->get_server_conf()->remove(); } if ( ! is_wp_error( $result ) ) { return $values; } // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); return $values; } Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); return $values; } /** * Add rules on plugin activation. * * @since 1.9 */ public function activate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( is_wp_error( $conf->is_file_writable() ) ) { return; } $conf->add(); } /** * Remove rules on plugin deactivation. * * @since 1.9 */ public function deactivate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } $file_path = $conf->get_file_path(); $filesystem = \Imagify_Filesystem::get_instance(); if ( ! $filesystem->exists( $file_path ) ) { return; } if ( ! $filesystem->is_writable( $file_path ) ) { return; } $conf->remove(); } /** * Get the path to the directory conf file. * * @since 1.9 * * @param bool $relative True to get a path relative to the site’s root. * @return string|bool The file path. False on failure. */ public function get_file_path( $relative = false ) { if ( ! $this->get_server_conf() ) { return false; } $file_path = $this->get_server_conf()->get_file_path(); if ( $relative ) { return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); } return $file_path; } /** * Get the server conf instance. * Note: nothing needed for nginx. * * @since 1.9 * * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7; if ( isset( $this->server_conf ) ) { return $this->server_conf; } if ( $is_apache ) { $this->server_conf = new Apache(); } elseif ( $is_iis7 ) { $this->server_conf = new IIS(); } return $this->server_conf; } } Avif/IIS.php 0000644 00000001311 15174671731 0006576 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Avif; use Imagify\WriteFile\AbstractIISDirConfFile; /** * Add and remove contents to the web.config file to display AVIF images on the site. */ class IIS extends AbstractIISDirConfFile { /** * Name of the tag used as block delemiter. * * @var string */ const TAG_NAME = 'Imagify: avif file type'; /** * Get unfiltered new contents to write into the file. * * @return string */ protected function get_raw_new_contents() { return trim( ' <!-- @parent /configuration/system.webServer --> <staticContent name="' . esc_attr( static::TAG_NAME ) . ' 1"> <mimeMap fileExtension=".avif" mimeType="image/avif" /> </staticContent>' ); } } WriteFile/AbstractApacheDirConfFile.php 0000644 00000003466 15174671731 0014106 0 ustar 00 <?php namespace Imagify\WriteFile; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Abstract class used to add and remove contents to the .htaccess file. * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractApacheDirConfFile extends AbstractWriteDirConfFile { /** * Insert new contents into the directory conf file. * Replaces existing marked info. Creates file if none exists. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param string $new_contents Contents to insert. * @return bool|\WP_Error True on write success, a \WP_Error object on failure. */ protected function insert_contents( $new_contents ) { $contents = $this->get_file_contents(); if ( is_wp_error( $contents ) ) { return $contents; } $start_marker = '# BEGIN ' . static::TAG_NAME; $end_marker = '# END ' . static::TAG_NAME; // Remove previous rules. $contents = preg_replace( '/\s*?' . preg_quote( $start_marker, '/' ) . '.*' . preg_quote( $end_marker, '/' ) . '\s*?/isU', "\n\n", $contents ); $contents = trim( $contents ); if ( $new_contents ) { $contents = $new_contents . "\n\n" . $contents; } return $this->put_file_contents( $contents ); } /** * Get new contents to write into the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_new_contents() { $contents = parent::get_new_contents(); if ( ! $contents ) { return ''; } return '# BEGIN ' . static::TAG_NAME . "\n" . $contents . "\n# END " . static::TAG_NAME; } /** * Get the unfiltered path to the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ protected function get_raw_file_path() { return $this->filesystem->get_site_root() . '.htaccess'; } } WriteFile/AbstractWriteDirConfFile.php 0000644 00000022523 15174671731 0014012 0 ustar 00 <?php namespace Imagify\WriteFile; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Abstract class used to add and remove contents to a directory conf file (.htaccess, etc). * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractWriteDirConfFile implements WriteFileInterface { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 * @author Grégory Viguier */ const TAG_NAME = 'Imagify ###'; /** * Filesystem object. * * @var \Imagify_Filesystem * @since 1.9 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * Constructor. * * @since 1.9 * @access public * @author Grégory Viguier */ public function __construct() { $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Add new contents to the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True on success. A \WP_Error object on error. */ public function add() { $result = $this->insert_contents( $this->get_new_contents() ); if ( ! is_wp_error( $result ) ) { return true; } $file_path = $this->get_file_path(); $file_name = $this->filesystem->make_path_relative( $file_path ); if ( 'edition_disabled' === $result->get_error_code() ) { return new \WP_Error( 'edition_disabled', sprintf( /* translators: %s is a file name. */ __( 'Imagify did not add contents to the %s file, as its edition is disabled.', 'imagify' ), $file_name ) ); } return new \WP_Error( 'add_contents_failure', sprintf( /* translators: 1 is a file name, 2 is an error message. */ __( 'Imagify could not insert contents into the %1$s file: %2$s', 'imagify' ), $file_name, $result->get_error_message() ), [ 'code' => $result->get_error_code() ] ); } /** * Remove the related contents from the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True on success. A \WP_Error object on error. */ public function remove() { $result = $this->insert_contents( '' ); if ( ! is_wp_error( $result ) ) { return true; } $file_name = $this->filesystem->make_path_relative( $file_path ); if ( 'edition_disabled' === $result->get_error_code() ) { return new \WP_Error( 'edition_disabled', sprintf( /* translators: %s is a file name. */ __( 'Imagify did not remove the contents from the %s file, as its edition is disabled.', 'imagify' ), $file_name ) ); } return new \WP_Error( 'add_contents_failure', sprintf( /* translators: 1 is a file name, 2 is an error message. */ __( 'Imagify could not remove contents from the %1$s file: %2$s', 'imagify' ), $file_name, $result->get_error_message() ), [ 'code' => $result->get_error_code() ] ); } /** * Get the path to the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_file_path() { $file_path = $this->get_raw_file_path(); /** * Filter the path to the directory conf file. * * @since 1.9 * @author Grégory Viguier * * @param string $file_path Path to the file. */ $new_file_path = apply_filters( 'imagify_dir_conf_path', $file_path ); if ( $new_file_path && is_string( $new_file_path ) ) { return $new_file_path; } return $file_path; } /** * Tell if the file is writable. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True if writable. A \WP_Error object if not. */ public function is_file_writable() { $file_path = $this->get_file_path(); $file_name = $this->filesystem->make_path_relative( $file_path ); if ( $this->is_conf_edition_disabled() ) { return new \WP_Error( 'edition_disabled', sprintf( /* translators: %s is a file name. */ __( 'Edition of the %s file is disabled.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } if ( ! $this->filesystem->exists( $file_path ) ) { $dir_path = $this->filesystem->dir_path( $file_path ); $this->filesystem->make_dir( $dir_path ); if ( ! $this->filesystem->is_writable( $dir_path ) ) { return new \WP_Error( 'parent_not_writable', sprintf( /* translators: %s is a file name. */ __( '%s’s parent folder is not writable.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } if ( ! $this->filesystem->touch( $file_path ) ) { return new \WP_Error( 'not_created', sprintf( /* translators: %s is a file name. */ __( 'The %s file could not be created.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } } elseif ( ! $this->filesystem->is_writable( $file_path ) ) { return new \WP_Error( 'not_writable', sprintf( /* translators: %s is a file name. */ __( 'The %s file is not writable.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } return true; } /** * Get new contents to write into the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_new_contents() { $contents = $this->get_raw_new_contents(); /** * Filter the contents to add to the directory conf file. * * @since 1.9 * @author Grégory Viguier * * @param string $contents The contents. */ $new_contents = apply_filters( 'imagify_dir_conf_contents', $contents ); if ( $new_contents && is_string( $new_contents ) ) { return $new_contents; } return $contents; } /** ----------------------------------------------------------------------------------------- */ /** ABSTRACT METHODS ======================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Insert new contents into the directory conf file. * Replaces existing marked info. Creates file if none exists. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param string $new_contents Contents to insert. * @return bool|\WP_Error True on write success, a \WP_Error object on failure. */ abstract protected function insert_contents( $new_contents ); /** * Get the unfiltered path to the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ abstract protected function get_raw_file_path(); /** * Get unfiltered new contents to write into the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ abstract protected function get_raw_new_contents(); /** ----------------------------------------------------------------------------------------- */ /** OTHER TOOLS ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the file contents. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return mixed|\WP_Error The file contents on success, a \WP_Error object on failure. */ protected function get_file_contents() { $writable = $this->is_file_writable(); if ( is_wp_error( $writable ) ) { return $writable; } $file_path = $this->get_file_path(); if ( ! $this->filesystem->exists( $file_path ) ) { // This should not happen. return ''; } $contents = $this->filesystem->get_contents( $file_path ); if ( false === $contents ) { return new \WP_Error( 'not_read', sprintf( /* translators: %s is a file name. */ __( 'The %s file could not be read.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } return $contents; } /** * Put new contents into the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param string $contents New contents to add to the file. * @return bool|\WP_Error True on success, a \WP_Error object on failure. */ protected function put_file_contents( $contents ) { $file_path = $this->get_file_path(); $result = $this->filesystem->put_contents( $file_path, $contents ); if ( $result ) { return true; } $file_name = $this->filesystem->make_path_relative( $file_path ); return new \WP_Error( 'edition_failed', sprintf( /* translators: %s is a file name. */ __( 'Could not write into the %s file.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } /** * Tell if edition of the directory conf file is disabled. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True to disable, false otherwise. */ protected function is_conf_edition_disabled() { /** * Disable directory conf edition. * * @since 1.9 * @author Grégory Viguier * * @param bool $disable True to disable, false otherwise. */ return (bool) apply_filters( 'imagify_disable_dir_conf_edition', false ); } /** * Get a regex pattern to be used to match the supported file extensions. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ protected function get_extensions_pattern() { $extensions = imagify_get_mime_types( 'image' ); $extensions = array_keys( $extensions ); return implode( '|', $extensions ); } } WriteFile/AbstractIISDirConfFile.php 0000644 00000016356 15174671731 0013353 0 ustar 00 <?php namespace Imagify\WriteFile; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Abstract class used to add and remove contents to the web.config file. * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractIISDirConfFile extends AbstractWriteDirConfFile { /** * Insert new contents into the directory conf file. * Replaces existing marked info. Creates file if none exists. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param string $new_contents Contents to insert. * @return bool|\WP_Error True on write success, a \WP_Error object on failure. */ protected function insert_contents( $new_contents ) { $doc = $this->get_file_contents(); if ( is_wp_error( $doc ) ) { return $doc; } $marker = static::TAG_NAME; $xpath = new \DOMXPath( $doc ); // Remove previous rules. $old_nodes = $xpath->query( ".//*[starts-with(@name,'$marker')]" ); if ( $old_nodes->length > 0 ) { foreach ( $old_nodes as $old_node ) { $old_node->parentNode->removeChild( $old_node ); } } // No new contents? Stop here. if ( ! $new_contents ) { return $this->put_file_contents( $doc ); } $new_contents = preg_split( '/<!--\s+@parent\s+(.+?)\s+-->/', $new_contents, -1, PREG_SPLIT_DELIM_CAPTURE ); unset( $new_contents[0] ); $new_contents = array_chunk( $new_contents, 2 ); foreach ( $new_contents as $i => $new_content ) { $path = rtrim( $new_content[0], '/' ); $new_content = trim( $new_content[1] ); if ( '' === $new_content ) { continue; } $fragment = $doc->createDocumentFragment(); $fragment->appendXML( $new_content ); $this->get_node( $doc, $xpath, $path, $fragment ); } return $this->put_file_contents( $doc ); } /** * Get the unfiltered path to the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ protected function get_raw_file_path() { return $this->filesystem->get_site_root() . 'web.config'; } /** ----------------------------------------------------------------------------------------- */ /** OTHER TOOLS ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the file is writable. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True if writable. A \WP_Error object if not. */ public function is_file_writable() { $file_path = $this->get_file_path(); $file_name = $this->filesystem->make_path_relative( $file_path ); if ( $this->is_conf_edition_disabled() ) { return new \WP_Error( 'edition_disabled', sprintf( /* translators: %s is a file name. */ __( 'Edition of the %s file is disabled.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } if ( ! class_exists( '\DOMDocument' ) ) { return new \WP_Error( 'not_domdocument', sprintf( /* translators: 1 is a php class name, 2 is a file name. */ __( 'The class %1$s is not present on your server, a %2$s file cannot be created nor edited.', 'imagify' ), '<code>DOMDocument</code>', '<code>' . esc_html( $file_name ) . '</code>' ) ); } if ( ! $this->filesystem->exists( $file_path ) ) { $dir_path = $this->filesystem->dir_path( $file_path ); $this->filesystem->make_dir( $dir_path ); if ( ! $this->filesystem->is_writable( $dir_path ) ) { return new \WP_Error( 'parent_not_writable', sprintf( /* translators: %s is a file name. */ __( '%s’s parent folder is not writable.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } if ( ! $this->filesystem->exists( $file_path ) ) { $result = $this->filesystem->put_contents( $file_path, '<configuration/>' ); if ( ! $result ) { return new \WP_Error( 'not_created', sprintf( /* translators: %s is a file name. */ __( 'The %s file could not be created.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } } } elseif ( ! $this->filesystem->is_writable( $file_path ) ) { return new \WP_Error( 'not_writable', sprintf( /* translators: %s is a file name. */ __( 'The %s file is not writable.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } return true; } /** * Get the file contents. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return \DOMDocument|\WP_Error A \DOMDocument object on success, a \WP_Error object on failure. */ protected function get_file_contents() { $writable = $this->is_file_writable(); if ( is_wp_error( $writable ) ) { return $writable; } $file_path = $this->get_file_path(); $doc = new \DOMDocument(); $doc->preserveWhiteSpace = false; if ( false === $doc->load( $file_path ) ) { $file_path = $this->get_file_path(); $file_name = $this->filesystem->make_path_relative( $file_path ); return new \WP_Error( 'not_read', sprintf( /* translators: %s is a file name. */ __( 'The %s file could not be read.', 'imagify' ), '<code>' . esc_html( $file_name ) . '</code>' ) ); } return $doc; } /** * Put new contents into the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param \DOMDocument $contents A \DOMDocument object. * @return bool|\WP_Error True on success, a \WP_Error object on failure. */ protected function put_file_contents( $contents ) { $contents->encoding = 'UTF-8'; $contents->formatOutput = true; saveDomDocument( $contents, $this->get_file_path() ); return true; } /** * Get a DOMNode node. * If it does not exist it is created recursively. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param \DOMDocument $doc A \DOMDocument element. * @param \DOMXPath $xpath A \DOMXPath element. * @param string $path Path to the desired node. * @param \DOMNode $child A \DOMNode to be prepended. * @return \DOMNode The \DOMNode node. */ protected function get_node( $doc, $xpath, $path, $child ) { $nodelist = $xpath->query( $path ); if ( $nodelist->length > 0 ) { return $this->prepend_node( $nodelist->item( 0 ), $child ); } $path = explode( '/', $path ); $node = array_pop( $path ); $path = implode( '/', $path ); $final_node = $doc->createElement( $node ); if ( $child ) { $final_node->appendChild( $child ); } return $this->get_node( $doc, $xpath, $path, $final_node ); } /** * Prepend a DOMNode node. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param \DOMNode $container_node The \DOMNode that will contain the new node. * @param \DOMNode $new_node The \DOMNode to be prepended. * @return \DOMNode The \DOMNode containing the new node. */ protected function prepend_node( $container_node, $new_node ) { if ( ! $new_node ) { return $container_node; } if ( $container_node->hasChildNodes() ) { $container_node->insertBefore( $new_node, $container_node->firstChild ); } else { $container_node->appendChild( $new_node ); } return $container_node; } } WriteFile/AbstractNginxDirConfFile.php 0000644 00000003474 15174671731 0014007 0 ustar 00 <?php namespace Imagify\WriteFile; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Abstract class used to add and remove contents to imagify.conf file. * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractNginxDirConfFile extends AbstractWriteDirConfFile { /** * Insert new contents into the directory conf file. * Replaces existing marked info. Creates file if none exists. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param string $new_contents Contents to insert. * @return bool|\WP_Error True on write success, a \WP_Error object on failure. */ protected function insert_contents( $new_contents ) { $contents = $this->get_file_contents(); if ( is_wp_error( $contents ) ) { return $contents; } $start_marker = '# BEGIN ' . static::TAG_NAME; $end_marker = '# END ' . static::TAG_NAME; // Remove previous rules. $contents = preg_replace( '/\s*?' . preg_quote( $start_marker, '/' ) . '.*' . preg_quote( $end_marker, '/' ) . '\s*?/isU', "\n\n", $contents ); $contents = trim( $contents ); if ( $new_contents ) { $contents = $new_contents . "\n\n" . $contents; } return $this->put_file_contents( $contents ); } /** * Get new contents to write into the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_new_contents() { $contents = parent::get_new_contents(); if ( ! $contents ) { return ''; } return '# BEGIN ' . static::TAG_NAME . "\n" . $contents . "\n# END " . static::TAG_NAME; } /** * Get the unfiltered path to the file. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return string */ protected function get_raw_file_path() { return $this->filesystem->get_site_root() . 'conf/imagify.conf'; } } WriteFile/WriteFileInterface.php 0000644 00000002334 15174671731 0012700 0 ustar 00 <?php namespace Imagify\WriteFile; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Interface to add and remove contents to a file. * * @since 1.9 * @author Grégory Viguier */ interface WriteFileInterface { /** * Add new contents to the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True on success. A \WP_Error object on error. */ public function add(); /** * Remove the related contents from the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True on success. A \WP_Error object on error. */ public function remove(); /** * Get the path to the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_file_path(); /** * Tell if the file is writable. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|\WP_Error True if writable. A \WP_Error object if not. */ public function is_file_writable(); /** * Get new contents to write into the file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_new_contents(); } Imagifybeat/Core.php 0000644 00000010100 15174671731 0010372 0 ustar 00 <?php namespace Imagify\Imagifybeat; use Imagify\Traits\InstanceGetterTrait; /** * Imagifybeat core. * * @since 1.9.3 * @author Grégory Viguier */ final class Core { use InstanceGetterTrait; /** * Class init: launch hooks. * * @since 1.9.3 * @access public * @author Grégory Viguier */ public function init() { add_action( 'wp_ajax_imagifybeat', [ $this, 'core_handler' ], 1 ); add_filter( 'imagifybeat_refresh_nonces', [ $this, 'refresh_imagifybeat_nonces' ] ); } /** * Ajax handler for the Imagifybeat API. * * Runs when the user is logged in. * * @since 1.9.3 * @access public * @author Grégory Viguier */ public function core_handler() { if ( empty( $_POST['_nonce'] ) ) { wp_send_json_error(); } $data = []; $response = []; $nonce_state = wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_nonce'] ) ), 'imagifybeat-nonce' ); // Screen_id is the same as $current_screen->id and the JS global 'pagenow'. if ( ! empty( $_POST['screen_id'] ) ) { $screen_id = sanitize_key( $_POST['screen_id'] ); } else { $screen_id = 'front'; } if ( ! empty( $_POST['data'] ) ) { $data = wp_unslash( (array) $_POST['data'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } if ( 1 !== $nonce_state ) { /** * Filters the nonces to send. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @param string $screen_id The screen id. */ $response = wpm_apply_filters_typed( 'array', 'imagifybeat_refresh_nonces', $response, $data, $screen_id ); if ( false === $nonce_state ) { // User is logged in but nonces have expired. $response['nonces_expired'] = true; wp_send_json( $response ); } } if ( ! empty( $data ) ) { /** * Filters the Imagifybeat response received. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @param string $screen_id The screen id. */ $response = wpm_apply_filters_typed( 'array', 'imagifybeat_received', $response, $data, $screen_id ); } /** * Filters the Imagifybeat response sent. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param string $screen_id The screen id. */ $response = wpm_apply_filters_typed( 'array', 'imagifybeat_send', $response, $screen_id ); /** * Fires when Imagifybeat ticks in logged-in environments. * * Allows the transport to be easily replaced with long-polling. * * @since 1.9.3 * @author Grégory Viguier * * @param array $response The Imagifybeat response. * @param string $screen_id The screen id. */ do_action( 'imagifybeat_tick', $response, $screen_id ); // Send the current time according to the server. $response['server_time'] = time(); wp_send_json( $response ); } /** * Add the latest Imagifybeat nonce to the Imagifybeat response. * * @since 1.9.3 * @access public * @author Grégory Viguier * * @param array $response The Imagifybeat response. * @return array The Imagifybeat response. */ public function refresh_imagifybeat_nonces( $response ) { // Refresh the Imagifybeat nonce. $response['imagifybeat_nonce'] = wp_create_nonce( 'imagifybeat-nonce' ); return $response; } /** * Get Imagifybeat settings. * * @since 1.9.3 * @access public * @author Grégory Viguier * * @return array */ public function get_settings() { global $pagenow; $settings = []; if ( ! is_admin() ) { $settings['ajaxurl'] = admin_url( 'admin-ajax.php', 'relative' ); } if ( is_user_logged_in() ) { $settings['nonce'] = wp_create_nonce( 'imagifybeat-nonce' ); } if ( 'customize.php' === $pagenow ) { $settings['screenId'] = 'customize'; } /** * Filters the Imagifybeat settings. * * @since 1.9.3 * * @param array $settings Imagifybeat settings array. */ return (array) wpm_apply_filters_typed( 'array', 'imagifybeat_settings', $settings ); } } Imagifybeat/Actions.php 0000644 00000025675 15174671731 0011131 0 ustar 00 <?php namespace Imagify\Imagifybeat; use Imagify_Requirements; use Imagify\Traits\InstanceGetterTrait; use Imagify\Bulk\Bulk; /** * Imagifybeat actions. * * @since 1.9.3 */ final class Actions { use InstanceGetterTrait; /** * The list of action IDs. * Keys are related to method names, values are Imagifybeat IDs. * * @var array * * @since 1.9.3 */ private $imagifybeat_ids = [ 'requirements' => 'imagify_requirements', 'bulk_optimization_stats' => 'imagify_bulk_optimization_stats', 'bulk_optimization_status' => 'imagify_bulk_optimization_status', 'options_optimization_status' => 'imagify_options_optimization_status', 'library_optimization_status' => 'imagify_library_optimization_status', 'custom_folders_optimization_status' => 'imagify_custom_folders_optimization_status', ]; /** * Class init: launch hooks. * * @since 1.9.3 */ public function init() { foreach ( $this->imagifybeat_ids as $action => $imagifybeat_id ) { add_filter( 'imagifybeat_received', [ $this, 'add_' . $action . '_to_response' ], 10, 2 ); } } /** ----------------------------------------------------------------------------------------- */ /** IMAGIFYBEAT CALLBACKS =================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Add requirements to Imagifybeat data. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_requirements_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) ) { return $response; } $response[ $imagifybeat_id ] = [ 'curl_missing' => ! Imagify_Requirements::supports_curl(), 'editor_missing' => ! Imagify_Requirements::supports_image_editor(), 'external_http_blocked' => Imagify_Requirements::is_imagify_blocked(), 'api_down' => ! Imagify_Requirements::is_api_up(), 'key_is_valid' => Imagify_Requirements::is_api_key_valid(), 'is_over_quota' => Imagify_Requirements::is_over_quota(), ]; return $response; } /** * Add bulk stats to Imagifybeat data. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_bulk_optimization_stats_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) ) { return $response; } $folder_types = array_flip( array_filter( $data[ $imagifybeat_id ] ) ); $response[ $imagifybeat_id ] = imagify_get_bulk_stats( $folder_types, [ 'fullset' => true, ] ); return $response; } /** * Look for media where status has changed, compared to what Imagifybeat sends. * This is used in the bulk optimization page. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_bulk_optimization_status_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) || ! is_array( $data[ $imagifybeat_id ] ) ) { return $response; } if ( ! isset( $data[ $imagifybeat_id ] ) ) { return $response; } $bulk = Bulk::get_instance(); $groups_data = []; $types = []; $total = 0; $remaining = 0; $percentage = 0; foreach ( $data[ $imagifybeat_id ] as $group ) { $types[ $group['groupID'] . '|' . $group['context'] ] = true; $transient = get_transient( "imagify_{$group['context']}_optimize_running" ); if ( false !== $transient ) { $total += $transient['total']; $remaining += $transient['remaining']; } $groups_data[ $group['context'] ] = $bulk->get_bulk_instance( $group['context'] )->get_context_data(); } if ( 0 !== $total ) { $percentage = ( $total - $remaining ) / $total * 100; } $response[ $imagifybeat_id ] = [ 'groups_data' => $groups_data, 'remaining' => $remaining, 'percentage' => round( $percentage ), 'result' => get_transient( 'imagify_bulk_optimization_result' ), ]; return $response; } /** * Look for media where status has changed, compared to what Imagifybeat sends. * This is used in the settings page. * * @since 1.9 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_options_optimization_status_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) || ! is_array( $data[ $imagifybeat_id ] ) ) { return $response; } $remaining = 0; $total = get_transient( 'imagify_missing_next_gen_total' ); if ( false === $total ) { return $response; } $bulk = Bulk::get_instance(); $format = get_imagify_option( 'optimization_format' ); if ( 'off' === $format ) { return $response; } foreach ( $data[ $imagifybeat_id ] as $context ) { $media = $bulk->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format ); $remaining += count( $media['ids'] ); } $response[ $imagifybeat_id ] = [ 'remaining' => $remaining, 'total' => (int) $total, ]; return $response; } /** * Look for media where status has changed, compared to what Imagifybeat sends. * This is used in the WP Media Library. * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_library_optimization_status_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) || ! is_array( $data[ $imagifybeat_id ] ) ) { return $response; } $response[ $imagifybeat_id ] = $this->get_modified_optimization_statuses( $data[ $imagifybeat_id ] ); if ( ! $response[ $imagifybeat_id ] ) { return $response; } // Sanitize received data and grab some other info. foreach ( $response[ $imagifybeat_id ] as $context_id => $media_atts ) { $process = imagify_get_optimization_process( $media_atts['media_id'], $media_atts['context'] ); $response[ $imagifybeat_id ][ $context_id ] = get_imagify_media_column_content( $process, false ); } return $response; } /** * Look for media where status has changed, compared to what Imagifybeat sends. * This is used in the custom folders list (the "Other Media" page). * * @since 1.9.3 * * @param array $response The Imagifybeat response. * @param array $data The $_POST data sent. * @return array */ public function add_custom_folders_optimization_status_to_response( $response, $data ) { $imagifybeat_id = $this->get_imagifybeat_id_for_callback( __FUNCTION__ ); if ( ! $imagifybeat_id || empty( $data[ $imagifybeat_id ] ) || ! is_array( $data[ $imagifybeat_id ] ) ) { return $response; } $response[ $imagifybeat_id ] = $this->get_modified_optimization_statuses( $data[ $imagifybeat_id ] ); if ( ! $response[ $imagifybeat_id ] ) { return $response; } $admin_ajax_post = \Imagify_Admin_Ajax_Post::get_instance(); $list_table = new \Imagify_Files_List_Table( [ 'screen' => 'imagify-files', ] ); // Sanitize received data and grab some other info. foreach ( $response[ $imagifybeat_id ] as $context_id => $media_atts ) { $process = imagify_get_optimization_process( $media_atts['media_id'], $media_atts['context'] ); $response[ $imagifybeat_id ][ $context_id ] = $admin_ajax_post->get_media_columns( $process, $list_table ); } return $response; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Look for media where status has changed, compared to what Imagifybeat sends. * * @since 1.9.3 * * @param array $data The data received. * @return array */ private function get_modified_optimization_statuses( $data ) { if ( ! $data ) { return []; } $output = []; // Sanitize received data and grab some other info. foreach ( $data as $context => $media_statuses ) { if ( ! $context || ! $media_statuses || ! is_array( $media_statuses ) ) { continue; } // Sanitize the IDs: IDs come as strings, prefixed with an undescore character (to prevent JavaScript from screwing everything). $media_ids = array_keys( $media_statuses ); $media_ids = array_map( function ( $media_id ) { return substr( $media_id, 1 ); }, $media_ids ); $media_ids = array_filter( $media_ids ); if ( ! $media_ids ) { continue; } // Sanitize the context. $context_instance = imagify_get_context( $context ); $context = $context_instance->get_name(); $process_class_name = imagify_get_optimization_process_class_name( $context ); $transient_name = sprintf( $process_class_name::LOCK_NAME, $context, '%' ); $is_network_wide = $context_instance->is_network_wide(); \Imagify_DB::cache_process_locks( $context, $media_ids ); // Now that everything is cached for this context, we can get the transients without hitting the DB. foreach ( $media_ids as $id ) { $is_locked = (bool) $media_statuses[ '_' . $id ]; $option_name = str_replace( '%', $id, $transient_name ); if ( $is_network_wide ) { $in_db = (bool) get_site_transient( $option_name ); } else { $in_db = (bool) get_transient( $option_name ); } if ( $is_locked === $in_db ) { continue; } $output[ $context . '_' . $id ] = [ 'media_id' => $id, 'context' => $context, ]; } } return $output; } /** * Get an Imagifybeat ID, given an action. * * @since 1.9.3 * * @param string $action An action corresponding to the ID we want. * @return string|bool The ID. False on failure. */ public function get_imagifybeat_id( $action ) { if ( ! empty( $this->imagifybeat_ids[ $action ] ) ) { return $this->imagifybeat_ids[ $action ]; } return false; } /** * Get an Imagifybeat ID, given a callback name. * * @since 1.9.3 * * @param string $callback A method’s name. * @return string|bool The ID. False on failure. */ private function get_imagifybeat_id_for_callback( $callback ) { if ( preg_match( '@^add_(?<id>.+)_to_response$@', $callback, $matches ) ) { return $this->get_imagifybeat_id( $matches['id'] ); } return false; } } Stats/ServiceProvider.php 0000644 00000002063 15174671731 0011503 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Stats; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for Stats */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ OptimizedMediaWithoutNextGen::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ OptimizedMediaWithoutNextGen::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( OptimizedMediaWithoutNextGen::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } Stats/StatInterface.php 0000644 00000001237 15174671731 0011126 0 ustar 00 <?php namespace Imagify\Stats; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Interface to use to get and cache a stat. * * @since 1.9 * @author Grégory Viguier */ interface StatInterface { /** * Get the stat value. * * @since 1.9 * @access public * @author Grégory Viguier * * @return mixed */ public function get_stat(); /** * Get and cache the stat value. * * @since 1.9 * @access public * @author Grégory Viguier * * @return mixed */ public function get_cached_stat(); /** * Clear the stat cache. * * @since 1.9 * @access public * @author Grégory Viguier */ public function clear_cache(); } Stats/OptimizedMediaWithoutNextGen.php 0000644 00000013412 15174671731 0014151 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Stats; use Imagify\Bulk\Bulk; use Imagify\Optimization\Process\ProcessInterface; use Imagify\EventManagement\SubscriberInterface; use Imagify\Traits\InstanceGetterTrait; use WP_Error; /** * Class to get and cache the number of optimized media without next-gen versions. */ final class OptimizedMediaWithoutNextGen implements StatInterface, SubscriberInterface { use InstanceGetterTrait; /** * Name of the transient storing the cached result. * * @var string */ const NAME = 'imagify_stat_without_next_gen'; /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_after_optimize' => [ 'maybe_clear_cache_after_optimization', 10, 2 ], 'imagify_after_restore_media' => [ 'maybe_clear_cache_after_restoration', 10, 4 ], 'imagify_delete_media' => 'maybe_clear_cache_on_deletion', 'update_option_imagify_settings' => [ 'maybe_clear_stat_cache', 9, 2 ], ]; } /** * Get the number of optimized media without next-gen versions. * * @since 2.2 * * @return int */ public function get_stat() { $bulk = Bulk::get_instance(); $stat = 0; // Sum the counts of each context. foreach ( imagify_get_context_names() as $context ) { $stat += $bulk->get_bulk_instance( $context )->has_optimized_media_without_nextgen(); } return $stat; } /** * Get and cache the number of optimized media without next-gen versions. * * @since 2.2 * * @return int */ public function get_cached_stat() { $contexts = implode( '|', imagify_get_context_names() ); $stat = get_transient( self::NAME ); if ( isset( $stat['stat'], $stat['contexts'] ) && $stat['contexts'] === $contexts ) { // The number is stored and the contexts are the same. return (int) $stat['stat']; } $stat = [ 'contexts' => $contexts, 'stat' => $this->get_stat(), ]; set_transient( self::NAME, $stat, 2 * DAY_IN_SECONDS ); return $stat['stat']; } /** * Clear the stat cache. * * @since 2.2 */ public function clear_cache() { delete_transient( self::NAME ); } /** * Clear cache after optimizing a media. * * @since 2.2 * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. */ public function maybe_clear_cache_after_optimization( $process, $item ) { if ( ! $process->get_media()->is_image() || false === get_transient( self::NAME ) ) { return; } $sizes = $process->get_data()->get_optimization_data(); $sizes = isset( $sizes['sizes'] ) ? (array) $sizes['sizes'] : []; $new_sizes = array_flip( $item['sizes_done'] ); $new_sizes = array_intersect_key( $sizes, $new_sizes ); $size_name = 'full' . $process::WEBP_SUFFIX; if ( 'avif' === get_imagify_option( 'optimization_format' ) ) { $size_name = 'full' . $process::AVIF_SUFFIX; } if ( ! isset( $new_sizes['full'] ) && ! empty( $new_sizes[ $size_name ]['success'] ) ) { /** * We just successfully generated the next-gen version of the full size. * The full size was not optimized at the same time, that means it was optimized previously. * Meaning: we just added a next-gen version to a media that was previously optimized, so there is one less optimized media without next-gen. */ $this->clear_cache(); return; } if ( ! empty( $new_sizes['full']['success'] ) && empty( $new_sizes[ $size_name ]['success'] ) ) { /** * We now have a new optimized media without next-gen. */ $this->clear_cache(); } } /** * Clear cache after restoring a media. * * @since 2.2 * * @param ProcessInterface $process The optimization process. * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure. * @param array $files The list of files, before restoring them. * @param array $data The optimization data, before deleting it. */ public function maybe_clear_cache_after_restoration( $process, $response, $files, $data ) { if ( ! $process->get_media()->is_image() || false === get_transient( self::NAME ) ) { return; } $sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : []; $size_name = 'full' . $process::WEBP_SUFFIX; if ( 'avif' === get_imagify_option( 'optimization_format' ) ) { $size_name = 'full' . $process::AVIF_SUFFIX; } if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) { /** * This media had no next-gen versions. */ $this->clear_cache(); } } /** * Clear cache on media deletion. * * @since 2.2 * * @param ProcessInterface $process An optimization process. */ public function maybe_clear_cache_on_deletion( $process ) { if ( false === get_transient( self::NAME ) ) { return; } $data = $process->get_data()->get_optimization_data(); $sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : []; $size_name = 'full' . $process::WEBP_SUFFIX; if ( 'avif' === get_imagify_option( 'optimization_format' ) ) { $size_name = 'full' . $process::AVIF_SUFFIX; } if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) { /** * This media had no next-gen versions. */ $this->clear_cache(); } } /** * Maybe clear the stat cache on option change * * @since 2.2 * * @param array $old_value The old option value. * @param array $value The new option value. * * @return void */ public function maybe_clear_stat_cache( $old_value, $value ) { if ( ! isset( $old_value['optimization_format'], $value['optimization_format'] ) ) { return; } if ( $old_value['optimization_format'] === $value['optimization_format'] ) { return; } $this->clear_cache(); } } Notices/Notices.php 0000644 00000055115 15174671731 0010310 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Notices; use Imagify\Traits\InstanceGetterTrait; /** * Class that handles the admin notices. * * @since 1.6.10 */ final class Notices { use InstanceGetterTrait; /** * Class version. * * @var string */ const VERSION = '1.0.1'; /** * Name of the transient storing temporary notices. * * @var string */ const TEMPORARY_NOTICES_TRANSIENT_NAME = 'imagify_temporary_notices'; /** * Name of the user meta that stores the dismissed notice IDs. * * @var string */ const DISMISS_META_NAME = '_imagify_ignore_notices'; /** * Action used in the nonce to dismiss a notice. * * @var string */ const DISMISS_NONCE_ACTION = 'imagify-dismiss-notice'; /** * Action used in the nonce to deactivate a plugin. * * @var string */ const DEACTIVATE_PLUGIN_NONCE_ACTION = 'imagify-deactivate-plugin'; /** * List of notice IDs. * They correspond to method names and IDs stored in the "dismissed" transient. * Only use "-" character, not "_". * * @var array */ protected static $notice_ids = [ // This warning is displayed when the API key is empty. Dismissible. 'welcome-steps', // This warning is displayed when the API key is wrong. Dismissible. 'wrong-api-key', // This warning is displayed if some plugins are active. NOT dismissible. 'plugins-to-deactivate', // This notice is displayed when external HTTP requests are blocked via the WP_HTTP_BLOCK_EXTERNAL constant. Dismissible. 'http-block-external', // This warning is displayed when the grid view is active on the library. Dismissible. 'grid-view', // This warning is displayed if the backup folder is not writable. NOT dismissible. 'backup-folder-not-writable', // This notice is displayed to rate the plugin after 100 optimizations & 7 days after the first installation. Dismissible. 'rating', // Add a message about WP Rocket on the "Bulk Optimization" screen. Dismissible. 'wp-rocket', 'bulk-optimization-complete', 'bulk-optimization-running', 'upsell-banner', 'upsell-admin-bar', ]; /** * List of user capabilities to use for each notice. * Default value 'manage' is not listed. * * @var array */ protected static $capabilities = [ 'grid-view' => 'optimize', 'backup-folder-not-writable' => 'bulk-optimize', 'rating' => 'bulk-optimize', 'wp-rocket' => 'bulk-optimize', 'bulk-optimization-complete' => 'bulk-optimize', 'bulk-optimization-running' => 'bulk-optimize', ]; /** * List of plugins that conflict with Imagify. * * @var array */ protected static $conflicting_plugins = [ 'wp-smush' => 'wp-smushit/wp-smush.php', // WP Smush. 'wp-smush-pro' => 'wp-smush-pro/wp-smush.php', // WP Smush Pro. 'kraken' => 'kraken-image-optimizer/kraken.php', // Kraken.io. 'tinypng' => 'tiny-compress-images/tiny-compress-images.php', // TinyPNG. 'shortpixel' => 'shortpixel-image-optimiser/wp-shortpixel.php', // Shortpixel. 'ewww' => 'ewww-image-optimizer/ewww-image-optimizer.php', // EWWW Image Optimizer. 'ewww-cloud' => 'ewww-image-optimizer-cloud/ewww-image-optimizer-cloud.php', // EWWW Image Optimizer Cloud. 'imagerecycle' => 'imagerecycle-pdf-image-compression/wp-image-recycle.php', // ImageRecycle. ]; /** * The constructor. * * @return void */ protected function __construct() {} /** ----------------------------------------------------------------------------------------- */ /** INIT ==================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Launch the hooks. * * @since 1.6.10 */ public function init() { // For generic purpose. add_action( 'all_admin_notices', [ $this, 'render_notices' ] ); add_action( 'wp_ajax_imagify_dismiss_notice', [ $this, 'admin_post_dismiss_notice' ] ); add_action( 'admin_post_imagify_dismiss_notice', [ $this, 'admin_post_dismiss_notice' ] ); // For specific notices. add_action( 'imagify_dismiss_notice', [ $this, 'clear_scheduled_rating' ] ); add_action( 'admin_post_imagify_deactivate_plugin', [ $this, 'deactivate_plugin' ] ); add_action( 'imagify_not_almost_over_quota_anymore', [ $this, 'renew_almost_over_quota_notice' ] ); } /** ----------------------------------------------------------------------------------------- */ /** HOOKS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Maybe display some notices. * * @since 1.6.10 */ public function render_notices() { foreach ( $this->get_notice_ids() as $notice_id ) { // Get the name of the method that will tell if this notice should be displayed. $callback = 'display_' . str_replace( '-', '_', $notice_id ); if ( ! method_exists( $this, $callback ) ) { continue; } $data = call_user_func( [ $this, $callback ] ); if ( $data ) { // The notice must be displayed: render the view. \Imagify_Views::get_instance()->print_template( 'notice-' . $notice_id, $data ); } } // Temporary notices. $this->render_temporary_notices(); } /** * Process a dismissed notice. * * @since 1.6.10 * @see _do_admin_post_imagify_dismiss_notice() */ public function admin_post_dismiss_notice() { imagify_check_nonce( self::DISMISS_NONCE_ACTION ); $notice = ! empty( $_GET['notice'] ) ? sanitize_text_field( wp_unslash( $_GET['notice'] ) ) : ''; $notices = $this->get_notice_ids(); $notices = array_flip( $notices ); if ( ! $notice || ! isset( $notices[ $notice ] ) || ! $this->user_can( $notice ) ) { imagify_die(); } self::dismiss_notice( $notice ); /** * Fires when a notice is dismissed. * * @since 1.4.2 * * @param string $notice The notice slug */ do_action( 'imagify_dismiss_notice', $notice ); imagify_maybe_redirect(); wp_send_json_success(); } /** * Stop the rating cron when the notice is dismissed. * * @since 1.6.10 * @see _imagify_clear_scheduled_rating() * * @param string $notice The notice name. */ public function clear_scheduled_rating( $notice ) { if ( 'rating' === $notice ) { set_site_transient( 'do_imagify_rating_cron', 'no' ); \Imagify_Cron_Rating::get_instance()->unschedule_event(); } } /** * Disable a plugin which can be in conflict with Imagify. * * @since 1.6.10 * @see _imagify_deactivate_plugin() */ public function deactivate_plugin() { imagify_check_nonce( self::DEACTIVATE_PLUGIN_NONCE_ACTION ); if ( empty( $_GET['plugin'] ) || ! $this->user_can( 'plugins-to-deactivate' ) ) { imagify_die(); } $plugin = sanitize_text_field( wp_unslash( $_GET['plugin'] ) ); $plugins = $this->get_conflicting_plugins(); $plugins = array_flip( $plugins ); if ( empty( $plugins[ $plugin ] ) ) { imagify_die(); } deactivate_plugins( $plugin ); imagify_maybe_redirect(); wp_send_json_success(); } /** * Renew the "almost-over-quota" notice when the consumed quota percent decreases back below 80%. * * @since 1.7 */ public function renew_almost_over_quota_notice() { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT umeta_id, user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value LIKE %s", self::DISMISS_META_NAME, '%upsell%' ) ); if ( ! $results ) { return; } // Prevent multiple queries to the DB by caching user metas. $not_cached = []; foreach ( $results as $result ) { if ( ! wp_cache_get( $result->umeta_id, 'user_meta' ) ) { $not_cached[] = $result->umeta_id; } } if ( $not_cached ) { update_meta_cache( 'user', $not_cached ); } // Renew the notice for all users. foreach ( $results as $result ) { self::renew_notice( 'upsell-banner', $result->user_id ); self::renew_notice( 'upsell-admin-bar', $result->user_id ); } } /** ----------------------------------------------------------------------------------------- */ /** NOTICES ================================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the 'welcome-steps' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_welcome_steps() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'welcome-steps' ) ) { return $display; } if ( imagify_is_screen( 'imagify-settings' ) ) { return $display; } if ( self::notice_is_dismissed( 'welcome-steps' ) || get_imagify_option( 'api_key' ) ) { return $display; } $display = true; return $display; } /** * Tell if the 'wrong-api-key' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_wrong_api_key() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'wrong-api-key' ) ) { return $display; } if ( ! imagify_is_screen( 'bulk' ) ) { return $display; } if ( self::notice_is_dismissed( 'wrong-api-key' ) || ! get_imagify_option( 'api_key' ) || \Imagify_Requirements::is_api_key_valid() ) { return $display; } $display = true; return $display; } /** * Tell if the 'plugins-to-deactivate' notice should be displayed. * * @since 1.6.10 * * @return array|false An array of plugins to deactivate. false if the notice should not be displayed. */ public function display_plugins_to_deactivate() { static $display; if ( isset( $display ) ) { return $display; } if ( ! $this->user_can( 'plugins-to-deactivate' ) ) { $display = false; return $display; } $display = $this->get_conflicting_plugins(); return $display; } /** * Tell if the 'plugins-to-deactivate' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_http_block_external() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'http-block-external' ) ) { return $display; } if ( imagify_is_screen( 'imagify-settings' ) ) { return $display; } if ( self::notice_is_dismissed( 'http-block-external' ) || ! \Imagify_Requirements::is_imagify_blocked() ) { return $display; } $display = true; return $display; } /** * Tell if the 'grid-view' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_grid_view() { global $wp_version; static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'grid-view' ) ) { return $display; } if ( ! imagify_is_screen( 'library' ) ) { return $display; } $media_library_mode = get_user_option( 'media_library_mode', get_current_user_id() ); if ( 'list' === $media_library_mode || self::notice_is_dismissed( 'grid-view' ) || version_compare( $wp_version, '4.0' ) < 0 ) { return $display; } // Don't display the notice if the API key isn't valid. if ( ! \Imagify_Requirements::is_api_key_valid() ) { return $display; } $display = true; return $display; } /** * Tell if the 'backup-folder-not-writable' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_backup_folder_not_writable() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'backup-folder-not-writable' ) ) { return $display; } // Every places where images can be optimized, automatically or not (+ the settings page). if ( ! imagify_is_screen( 'imagify-settings' ) && ! imagify_is_screen( 'library' ) && ! imagify_is_screen( 'upload' ) && ! imagify_is_screen( 'bulk' ) && ! imagify_is_screen( 'media-modal' ) ) { return $display; } if ( ! get_imagify_option( 'backup' ) ) { return $display; } if ( \Imagify_Requirements::attachments_backup_dir_is_writable() ) { return $display; } $display = true; return $display; } /** * Tell if the 'rating' notice should be displayed. * * @since 1.6.10 * * @return bool|int */ public function display_rating() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'rating' ) ) { return $display; } if ( ! imagify_is_screen( 'bulk' ) && ! imagify_is_screen( 'library' ) && ! imagify_is_screen( 'upload' ) ) { return $display; } if ( self::notice_is_dismissed( 'rating' ) ) { return $display; } $user_images_count = (int) get_site_transient( 'imagify_user_images_count' ); if ( ! $user_images_count || get_site_transient( 'imagify_seen_rating_notice' ) ) { return $display; } $display = $user_images_count; return $display; } /** * Tell if the 'wp-rocket' notice should be displayed. * * @since 1.6.10 * * @return bool */ public function display_wp_rocket() { static $display; if ( isset( $display ) ) { return $display; } $display = false; if ( ! $this->user_can( 'wp-rocket' ) ) { return $display; } if ( ! imagify_is_screen( 'bulk' ) ) { return $display; } $plugins = get_plugins(); if ( isset( $plugins['wp-rocket/wp-rocket.php'] ) || self::notice_is_dismissed( 'wp-rocket' ) ) { return $display; } $display = true; return $display; } /** * Tell if the bulk optimization complete notice should be displayed * * @since 2.1 * * @return array */ public function display_bulk_optimization_complete(): array { if ( ! $this->user_can( 'bulk-optimization-complete' ) ) { return []; } if ( imagify_is_screen( 'bulk' ) ) { return []; } if ( self::notice_is_dismissed( 'bulk-optimization-complete' ) ) { return []; } if ( false === get_transient( 'imagify_bulk_optimization_complete' ) ) { return []; } $data = get_transient( 'imagify_bulk_optimization_result' ); if ( empty( $data ) ) { return []; } $global_gain = $data['original_size'] - $data['optimized_size']; $data['original_size'] = imagify_size_format( $data['original_size'], 2 ); $data['optimized_size'] = imagify_size_format( $global_gain, 2 ); $data['bulk_page_url'] = admin_url( 'upload.php?page=imagify-bulk-optimization' ); return $data; } /** * Tell if the bulk optimization running notice should be displayed * * @since 2.1 * * @return array */ public function display_bulk_optimization_running(): array { if ( ! $this->user_can( 'bulk-optimization-running' ) ) { return []; } if ( imagify_is_screen( 'bulk' ) ) { return []; } if ( self::notice_is_dismissed( 'bulk-optimization-running' ) ) { return []; } $custom_folders = get_transient( 'imagify_custom-folders_optimize_running' ); $library_wp = get_transient( 'imagify_wp_optimize_running' ); if ( ! $custom_folders && ! $library_wp ) { return []; } $data = []; $data['bulk_page_url'] = admin_url( 'upload.php?page=imagify-bulk-optimization' ); return $data; } /** ----------------------------------------------------------------------------------------- */ /** TEMPORARY NOTICES ======================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Maybe display some notices. * * @since 1.7 */ protected function render_temporary_notices() { if ( is_network_admin() ) { $notices = $this->get_network_temporary_notices(); } else { $notices = $this->get_site_temporary_notices(); } if ( ! $notices ) { return; } $views = \Imagify_Views::get_instance(); foreach ( $notices as $i => $notice_data ) { $notices[ $i ]['type'] = ! empty( $notice_data['type'] ) ? $notice_data['type'] : 'error'; } $views->print_template( 'notice-temporary', $notices ); } /** * Get temporary notices for the network. * * @since 1.7 * * @return array */ protected function get_network_temporary_notices() { $notices = get_site_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); if ( false === $notices ) { return []; } delete_site_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); return $notices && is_array( $notices ) ? $notices : []; } /** * Create a temporary notice for the network. * * @since 1.7 * * @param array|object|string $notice_data Some data, with the message to display. */ public function add_network_temporary_notice( $notice_data ) { $notices = get_site_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); $notices = is_array( $notices ) ? $notices : []; if ( is_wp_error( $notice_data ) ) { $notice_data = $notice_data->get_error_messages(); $notice_data = implode( '<br/>', $notice_data ); } if ( is_string( $notice_data ) ) { $notice_data = [ 'message' => $notice_data, ]; } elseif ( is_object( $notice_data ) ) { $notice_data = (array) $notice_data; } if ( ! is_array( $notice_data ) || empty( $notice_data['message'] ) ) { return; } $notices[] = $notice_data; set_site_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME, $notices, 30 ); } /** * Get temporary notices for the current site. * * @since 1.7 * * @return array */ protected function get_site_temporary_notices() { $notices = get_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); if ( false === $notices ) { return []; } delete_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); return $notices && is_array( $notices ) ? $notices : []; } /** * Create a temporary notice for the current site. * * @since 1.7 * * @param array|string $notice_data Some data, with the message to display. */ public function add_site_temporary_notice( $notice_data ) { $notices = get_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME ); $notices = is_array( $notices ) ? $notices : []; if ( is_string( $notice_data ) ) { $notice_data = [ 'message' => $notice_data, ]; } elseif ( is_object( $notice_data ) ) { $notice_data = (array) $notice_data; } if ( ! is_array( $notice_data ) || empty( $notice_data['message'] ) ) { return; } $notices[] = $notice_data; set_transient( self::TEMPORARY_NOTICES_TRANSIENT_NAME, $notices, 30 ); } /** ----------------------------------------------------------------------------------------- */ /** PUBLIC TOOLS ============================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Renew a dismissed Imagify notice. * * @since 1.6.10 * * @param string $notice A notice ID. * @param int $user_id A user ID. */ public static function renew_notice( $notice, $user_id = 0 ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); $notices = get_user_meta( $user_id, self::DISMISS_META_NAME, true ); $notices = $notices && is_array( $notices ) ? array_flip( $notices ) : []; if ( ! isset( $notices[ $notice ] ) ) { return; } unset( $notices[ $notice ] ); $notices = array_flip( $notices ); $notices = array_filter( $notices ); $notices = array_values( $notices ); update_user_meta( $user_id, self::DISMISS_META_NAME, $notices ); } /** * Dismiss an Imagify notice. * * @since 1.6.10 * @see imagify_dismiss_notice() * * @param string $notice A notice ID. * @param int $user_id A user ID. */ public static function dismiss_notice( $notice, $user_id = 0 ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); $notices = get_user_meta( $user_id, self::DISMISS_META_NAME, true ); $notices = $notices && is_array( $notices ) ? array_flip( $notices ) : []; if ( isset( $notices[ $notice ] ) ) { return; } $notices = array_flip( $notices ); $notices[] = $notice; $notices = array_filter( $notices ); $notices = array_values( $notices ); update_user_meta( $user_id, self::DISMISS_META_NAME, $notices ); } /** * Tell if an Imagify notice is dismissed. * * @since 1.6.10 * @see imagify_notice_is_dismissed() * * @param string $notice A notice ID. * @param int $user_id A user ID. * @return bool */ public static function notice_is_dismissed( $notice, $user_id = 0 ) { $user_id = $user_id ? (int) $user_id : get_current_user_id(); $notices = get_user_meta( $user_id, self::DISMISS_META_NAME, true ); $notices = $notices && is_array( $notices ) ? array_flip( $notices ) : []; return isset( $notices[ $notice ] ); } /** * Tell if one or more notices will be displayed later in the page. * * @since 1.6.10 * * @return bool */ public function has_notices() { foreach ( self::$notice_ids as $notice_id ) { $callback = 'display_' . str_replace( '-', '_', $notice_id ); if ( method_exists( $this, $callback ) && call_user_func( [ $this, $callback ] ) ) { return true; } } return false; } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL TOOLS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get all notice IDs. * * @since 1.6.10 * @since 1.10 Cast return value to array. * * @return array The filtered notice ids. */ protected function get_notice_ids() { /** * Filter the notices Imagify can display. * * @since 1.6.10 * * @param array $notice_ids An array of notice "IDs". */ return (array) wpm_apply_filters_typed( 'array', 'imagify_notices', self::$notice_ids ); } /** * Tell if the current user can see the notices. * Notice IDs that are not listed in self::$capabilities are assumed as 'manage'. * * @since 1.6.10 * * @param string $notice_id A notice ID. * @return bool */ protected function user_can( $notice_id ) { $capability = isset( self::$capabilities[ $notice_id ] ) ? self::$capabilities[ $notice_id ] : 'manage'; return imagify_get_context( 'wp' )->current_user_can( $capability ); } /** * Get a list of plugins that can conflict with Imagify. * * @since 1.6.10 * * @return array */ protected function get_conflicting_plugins() { /** * Filter the recommended plugins to deactivate to prevent conflicts. * * @since 1.0 * * @param array $plugins List of recommended plugins to deactivate. */ $plugins = wpm_apply_filters_typed( 'array', 'imagify_plugins_to_deactivate', self::$conflicting_plugins ); return array_filter( $plugins, 'is_plugin_active' ); } } Plugin.php 0000644 00000015112 15174671731 0006527 0 ustar 00 <?php declare(strict_types=1); namespace Imagify; use Imagify\Admin\AdminBar; use Imagify\Bulk\Bulk; use Imagify\CLI\{BulkOptimizeCommand, GenerateMissingNextgenCommand}; use Imagify\Dependencies\League\Container\Container; use Imagify\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface; use Imagify\EventManagement\{EventManager, SubscriberInterface}; use Imagify\Notices\Notices; use Imagify_Filesystem; /** * Main plugin class. */ class Plugin { /** * Container instance. * * @var Container */ private $container; /** * Is the plugin loaded * * @var boolean */ private $loaded = false; /** * Absolute path to the plugin (with trailing slash). * * @var string */ private $plugin_path; /** * Instantiate the class. * * @since 1.9 * * @param Container $container Instance of the container. * @param array $plugin_args { * An array of arguments. * * @type string $plugin_path Absolute path to the plugin (with trailing slash). * } */ public function __construct( Container $container, $plugin_args ) { $this->container = $container; $this->plugin_path = $plugin_args['plugin_path']; add_filter( 'imagify_container', [ $this, 'get_container' ] ); } /** * Returns the container instance. * * @return Container */ public function get_container() { return $this->container; } /** * Checks if the plugin is loaded * * @return boolean */ private function is_loaded(): bool { return $this->loaded; } /** * Plugin init. * * @param array $providers Array of service providers. * * @since 1.9 */ public function init( $providers ) { if ( $this->is_loaded() ) { return; } $this->container->addShared( 'event_manager', function () { return new EventManager(); } ); $this->container->addShared( 'filesystem', function () { return new Imagify_Filesystem(); } ); $this->include_files(); class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeSingletonTrait' ); \Imagify_Auto_Optimization::get_instance()->init(); \Imagify_Options::get_instance()->init(); \Imagify_Data::get_instance()->init(); \Imagify_Folders_DB::get_instance()->init(); \Imagify_Files_DB::get_instance()->init(); \Imagify_Cron_Library_Size::get_instance()->init(); \Imagify_Cron_Rating::get_instance()->init(); \Imagify_Cron_Sync_Files::get_instance()->init(); \Imagify\Auth\Basic::get_instance()->init(); \Imagify\Job\MediaOptimization::get_instance()->init(); Bulk::get_instance()->init(); if ( is_admin() ) { Notices::get_instance()->init(); \Imagify_Admin_Ajax_Post::get_instance()->init(); \Imagify_Settings::get_instance()->init(); \Imagify_Views::get_instance()->init(); \Imagify\Imagifybeat\Core::get_instance()->init(); \Imagify\Imagifybeat\Actions::get_instance()->init(); } if ( ! wp_doing_ajax() ) { \Imagify_Assets::get_instance()->init(); } add_action( 'init', [ $this, 'maybe_activate' ] ); imagify_add_command( new BulkOptimizeCommand() ); imagify_add_command( new GenerateMissingNextgenCommand() ); foreach ( $providers as $service_provider ) { $provider_instance = new $service_provider(); $this->container->addServiceProvider( $provider_instance ); // Load each service provider's subscribers if found. $this->load_subscribers( $provider_instance ); } /** * Fires when Imagify is fully loaded. * * @since 1.0 * @since 1.9 Added the class instance as parameter. * * @param \Imagify_Plugin $plugin Instance of this class. */ do_action( 'imagify_loaded', $this ); $this->loaded = true; } /** * Include plugin files. * * @since 1.9 */ public function include_files() { $instance_getter_path = $this->plugin_path . 'classes/Traits/InstanceGetterTrait.php'; if ( file_exists( $instance_getter_path . '.suspected' ) && ! file_exists( $instance_getter_path ) ) { // Trolling greedy antiviruses. require_once $instance_getter_path . '.suspected'; } $inc_path = $this->plugin_path . 'inc/'; require_once $inc_path . '/Dependencies/ActionScheduler/action-scheduler.php'; require_once $inc_path . 'deprecated/deprecated.php'; require_once $inc_path . 'deprecated/3rd-party.php'; require_once $inc_path . 'functions/common.php'; require_once $inc_path . 'functions/options.php'; require_once $inc_path . 'functions/formatting.php'; require_once $inc_path . 'functions/admin.php'; require_once $inc_path . 'functions/api.php'; require_once $inc_path . 'functions/media.php'; require_once $inc_path . 'functions/attachments.php'; require_once $inc_path . 'functions/process.php'; require_once $inc_path . 'functions/admin-ui.php'; require_once $inc_path . 'functions/admin-stats.php'; require_once $inc_path . 'functions/i18n.php'; require_once $inc_path . 'functions/partners.php'; require_once $inc_path . 'common/attachments.php'; require_once $inc_path . 'common/partners.php'; require_once $inc_path . '3rd-party/3rd-party.php'; if ( ! is_admin() ) { return; } require_once $inc_path . 'admin/upgrader.php'; require_once $inc_path . 'admin/upload.php'; require_once $inc_path . 'admin/media.php'; require_once $inc_path . 'admin/meta-boxes.php'; require_once $inc_path . 'admin/custom-folders.php'; } /** * Trigger a hook on plugin activation after the plugin is loaded. * * @since 1.9 * @see imagify_set_activation() */ public function maybe_activate() { if ( imagify_is_active_for_network() ) { $user_id = get_site_transient( 'imagify_activation' ); } else { $user_id = get_transient( 'imagify_activation' ); } if ( ! is_numeric( $user_id ) ) { return; } if ( imagify_is_active_for_network() ) { delete_site_transient( 'imagify_activation' ); } else { delete_transient( 'imagify_activation' ); } /** * Imagify activation. * * @since 1.9 * * @param int $user_id ID of the user activating the plugin. */ do_action( 'imagify_activation', (int) $user_id ); } /** * Load list of event subscribers from service provider. * * @param ServiceProviderInterface $service_provider Instance of service provider. * * @return void */ private function load_subscribers( ServiceProviderInterface $service_provider ) { if ( empty( $service_provider->get_subscribers() ) ) { return; } foreach ( $service_provider->get_subscribers() as $subscriber ) { $subscriber_object = $this->container->get( $subscriber ); if ( $subscriber_object instanceof SubscriberInterface ) { $this->container->get( 'event_manager' )->add_subscriber( $subscriber_object ); } } } } CDN/ServiceProvider.php 0000644 00000001762 15174671731 0011016 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CDN; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for CDN compatibility */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ CDN::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ CDN::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( CDN::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } CDN/PushCDNInterface.php 0000644 00000004274 15174671731 0010771 0 ustar 00 <?php namespace Imagify\CDN; /** * Interface to use for Push CDNs. * * @since 1.9 * @author Grégory Viguier */ interface PushCDNInterface { /** * Tell if the CDN is ready (not necessarily reachable). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_ready(); /** * Tell if the media is on the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function media_is_on_cdn(); /** * Get files from the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $file_paths A list of file paths. * @return bool|\WP_Error True on success. A \WP_error object on failure. */ public function get_files_from_cdn( $file_paths ); /** * Remove files from the CDN. * Don't use this to empty a folder. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $file_paths A list of file paths. Those paths are not necessary absolute, and can be also file names. * @return bool|\WP_Error True on success. A \WP_error object on failure. */ public function remove_files_from_cdn( $file_paths ); /** * Send all files from a media to the CDN. * * @since 1.9 * @access public * @author Grégory Viguier * * @param bool $is_new_upload Tell if the current media is a new upload. If not, it means it's a media being regenerated, restored, etc. * @return bool|\WP_Error True/False if sent or not. A \WP_error object on failure. */ public function send_to_cdn( $is_new_upload ); /** * Get a file URL. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $file_name Name of the file. Leave empty for the full size file. * @return string URL to the file. */ public function get_file_url( $file_name = '' ); /** * Get a file path. * * @since 1.9 * @access public * @author Grégory Viguier * * @param string $file_name Name of the file. Leave empty for the full size file. Use 'original' to get the path to the original file. * @return string Path to the file. */ public function get_file_path( $file_name = '' ); } CDN/CDN.php 0000644 00000005431 15174671731 0006304 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\CDN; use Imagify\EventManagement\SubscriberInterface; /** * CDN subscriber */ class CDN implements SubscriberInterface { /** * Array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_cdn_source_url' => 'get_cdn_source', ]; } /** * Get the CDN "source". * * @since 1.9.3 * * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter. * * @return array { * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'. * @type string $name Who? Can be a constant name, a plugin name, or an empty string. * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set. * } */ public function get_cdn_source( $option_url = '' ) { if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) { // Use a constant. $source = [ 'source' => 'constant', 'name' => 'IMAGIFY_CDN_URL', 'url' => IMAGIFY_CDN_URL, ]; } else { // Maybe use a filter. $filter_source = [ 'name' => null, 'url' => null, ]; /** * Provide a custom CDN source. * * @since 1.9.3 * * @param array $filter_source { * @type $name string The name of which provides the URL (plugin name, etc). * @type $url string The CDN URL. * } */ $filter_source = wpm_apply_filters_typed( 'array', 'imagify_cdn_source', $filter_source ); if ( ! empty( $filter_source['url'] ) ) { $source = [ 'source' => 'filter', 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '', 'url' => $filter_source['url'], ]; } } if ( empty( $source['url'] ) ) { // No constant, no filter: use the option. $source = [ 'source' => 'option', 'name' => '', 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ), ]; } if ( empty( $source['url'] ) ) { // Nothing set. return [ 'source' => 'option', 'name' => '', 'url' => '', ]; } $source['url'] = $this->sanitize_cdn_url( $source['url'] ); if ( empty( $source['url'] ) ) { // Not an URL. return [ 'source' => 'option', 'name' => '', 'url' => '', ]; } return $source; } /** * Sanitize the CDN URL value. * * @since 1.9.3 * * @param string $url The URL to sanitize. * * @return string */ public function sanitize_cdn_url( $url ) { $url = sanitize_text_field( $url ); if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) { // Not an URL. return ''; } return trailingslashit( $url ); } } ThirdParty/ServiceProvider.php 0000644 00000002410 15174671731 0012473 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\ThirdParty; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use Imagify\ThirdParty\Hostings\Extendify; use Imagify\ThirdParty\Plugins\GravityForms; /** * Service provider for Third Party(Plugins, Themes, Hosting). */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ 'gravity_from_subscriber', Extendify::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ 'gravity_from_subscriber', Extendify::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( 'gravity_from_subscriber', GravityForms::class ); $this->getContainer()->addShared( Extendify::class ); } } ThirdParty/Plugins/GravityForms.php 0000644 00000003451 15174671731 0013443 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\ThirdParty\Plugins; use GFForms; use Imagify\EventManagement\SubscriberInterface; /** * Subscriber for compatibility with GravityForms */ class GravityForms implements SubscriberInterface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { if ( ! class_exists( 'GFCommon' ) ) { return []; } return [ 'gform_noconflict_styles' => 'imagify_gf_noconflict_styles', 'gform_noconflict_scripts' => 'imagify_gf_noconflict_scripts', ]; } /** * Register imagify styles to gravity forms conflict styles * * @param array $styles Array fo registered styles. * * @return array */ public function imagify_gf_noconflict_styles( $styles ): array { if ( ! $this->is_gravity_forms_no_conflict_mode_enabled() ) { return $styles; } $styles[] = 'imagify-admin-bar'; $styles[] = 'imagify-admin'; $styles[] = 'imagify-notices'; $styles[] = 'imagify-pricing-modal'; return $styles; } /** * Register Imagify scripts to gravity forms conflict scripts * * @param array $scripts Array fo registered scripts. * * @return array */ public function imagify_gf_noconflict_scripts( $scripts ): array { if ( ! $this->is_gravity_forms_no_conflict_mode_enabled() ) { return $scripts; } $scripts[] = 'imagify-admin-bar'; $scripts[] = 'imagify-sweetalert'; $scripts[] = 'imagify-admin'; $scripts[] = 'imagify-notices'; $scripts[] = 'imagify-pricing-modal'; return $scripts; } /** * Check if gravity form is active and no_conflict mode is enabled. * * @return bool */ private function is_gravity_forms_no_conflict_mode_enabled(): bool { return (bool) get_option( 'gform_enable_noconflict', false ); } } ThirdParty/Hostings/Extendify.php 0000644 00000001400 15174671731 0013113 0 ustar 00 <?php declare( strict_types=1 ); namespace Imagify\ThirdParty\Hostings; use Imagify\EventManagement\SubscriberInterface; /** * Extendify compatibility class */ class Extendify implements SubscriberInterface { /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'imagify_hide_plugin_family' => 'hide_plugin_family', ]; } /** * Hide the plugin family if the Extendify site ID option is present. * * @param bool $hide Current value. * * @return bool */ public function hide_plugin_family( $hide ) { $option = get_option( 'extendify_site_id', false ); if ( false === $option ) { return $hide; } return true; } } Media/MediaInterface.php 0000644 00000017454 15174671731 0011163 0 ustar 00 <?php namespace Imagify\Media; use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; use WP_Error; /** * Interface to use for "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ interface MediaInterface { /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ); /** * Get the media ID. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int */ public function get_id(); /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid(); /** * Get the media context name. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_context(); /** * Get the media context instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return ContextInterface */ public function get_context_instance(); /** * Get the CDN instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|PushCDNInterface A PushCDNInterface instance. False if no CDN is used. */ public function get_cdn(); /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_original_path(); /** * Get the original media's path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_original_path(); /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the URL of the media’s full size file. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_fullsize_url(); /** * Get the path to the media’s full size file, even if the file doesn't exist. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_fullsize_path(); /** * Get the path to the media’s full size file if the file exists. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_fullsize_path(); /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup URL, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url(); /** * Get the backup file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path(); /** * Get the backup file path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_backup_path(); /** * Check if the media has a backup of the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media has a backup. */ public function has_backup(); /** ----------------------------------------------------------------------------------------- */ /** THUMBNAILS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create the media thumbnails. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function generate_thumbnails(); /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media type is supported. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_supported(); /** * Tell if the current media refers to an image, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool Returns false in case it's an image but not in a supported format (bmp for example). */ public function is_image(); /** * Tell if the current media refers to a pdf, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_pdf(); /** * Get the original file extension (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|null */ public function get_extension(); /** * Get the original file mime type (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_mime_type(); /** * Get the file mime type + file extension (if the file is supported by Imagify). * This test is ran against the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_allowed_mime_types(); /** * Tell if the current media has the required data (the data containing the file paths and thumbnails). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_media_data(); /** * Get the list of the files of this media, including the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * An array with the size names as keys ('full' is used for the original file), and arrays of data as values: * * @type string $path Absolute path to the file. * @type int $width The file width. * @type int $height The file height. * @type string $mime-type The file mime type. * } */ public function get_media_files(); /** * If the media is an image, get its width and height. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions(); /** * If the media is an image, update the dimensions in the database with the current file dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function update_dimensions(); } Media/Subscriber.php 0000644 00000001756 15174671731 0010424 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Media; use Imagify\EventManagement\SubscriberInterface; use Imagify\Media\Upload\Upload; /** * Media Subscriber */ class Subscriber implements SubscriberInterface { /** * Upload instance. * * @var Upload */ private $upload; /** * Constructor * * @param Upload $upload Upload Instance. */ public function __construct( Upload $upload ) { $this->upload = $upload; } /** * Returns an array of events that this subscriber wants to listen to. * * @return array */ public static function get_subscribed_events(): array { return [ 'restrict_manage_posts' => 'imagify_attachments_filter_dropdown', ]; } /** * Adds a dropdown that allows filtering on the attachments Imagify status. * * @return void */ public function imagify_attachments_filter_dropdown() { if ( ! \Imagify_Views::get_instance()->is_wp_library_page() ) { return; } $this->upload->add_imagify_filter_to_attachments_dropdown(); } } Media/ServiceProvider.php 0000644 00000002210 15174671731 0011416 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Media; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use Imagify\Media\Upload\Upload; /** * Service provider for Media */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ Upload::class, Subscriber::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ Subscriber::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers(): array { return $this->subscribers; } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->add( Upload::class ); $this->getContainer()->addShared( Subscriber::class ) ->addArgument( Upload::class ); } } Media/Upload/Upload.php 0000644 00000003244 15174671731 0010763 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Media\Upload; /** * Upload Media Class. */ class Upload { /** * Adds a dropdown that allows filtering on the attachments Imagify status. * * @return void */ public function add_imagify_filter_to_attachments_dropdown() { $data = []; /** * Tell if imagify stats query should run. * * @param bool $boolean True if the query should be run. False otherwise. */ if ( wpm_apply_filters_typed( 'boolean', 'imagify_display_library_stats', false ) ) { $data['optimized'] = imagify_count_optimized_attachments(); $data['unoptimized'] = imagify_count_unoptimized_attachments(); $data['errors'] = imagify_count_error_attachments(); } $status = isset( $_GET['imagify-status'] ) ? sanitize_text_field( wp_unslash( $_GET['imagify-status'] ) ) : 0; $options = [ 'optimized' => _x( 'Optimized', 'Media Files', 'imagify' ), 'unoptimized' => _x( 'Unoptimized', 'Media Files', 'imagify' ), 'errors' => _x( 'Errors', 'Media Files', 'imagify' ), ]; echo '<label class="screen-reader-text" for="filter-by-optimization-status">' . esc_html__( 'Filter by status', 'imagify' ) . '</label>'; echo '<select id="filter-by-optimization-status" name="imagify-status">'; echo '<option value="0" selected="selected">' . esc_html__( 'All Media Files', 'imagify' ) . '</option>'; foreach ( $options as $value => $label ) { $filter_value = isset( $data[ $value ] ) ? ' (' . $data[ $value ] . ')' : ''; echo '<option value="' . esc_attr( $value ) . '" ' . selected( $status, $value, false ) . '>' . esc_html( $label ) . esc_html( $filter_value ) . '</option>'; } echo '</select> '; } } Media/AbstractMedia.php 0000644 00000030247 15174671731 0011021 0 ustar 00 <?php namespace Imagify\Media; use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); /** * Abstract used for "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractMedia implements MediaInterface { /** * The media ID. * * @var int * @since 1.9 * @access protected * @author Grégory Viguier */ protected $id; /** * Context (where the media "comes from"). * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $context; /** * CDN to use. * * @var PushCDNInterface * @since 1.9 * @access protected * @author Grégory Viguier */ protected $cdn; /** * Tell if the media/context is network-wide. * * @var bool * @since 1.9 * @access protected * @author Grégory Viguier */ protected $is_network_wide = false; /** * Tell if the file is an image. * * @var bool * @since 1.9 * @access protected * @see $this->is_image() * @author Grégory Viguier */ protected $is_image; /** * Tell if the file is a pdf. * * @var bool * @since 1.9 * @access protected * @see $this->is_pdf() * @author Grégory Viguier */ protected $is_pdf; /** * Store the file mime type + file extension (if the file is supported). * * @var array * @since 1.9 * @access protected * @see $this->get_file_type() * @author Grégory Viguier */ protected $file_type; /** * Filesystem object. * * @var object Imagify_Filesystem * @since 1.9 * @access protected * @author Grégory Viguier */ protected $filesystem; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int $id The media ID. */ public function __construct( $id ) { $this->id = (int) $id; $this->filesystem = \Imagify_Filesystem::get_instance(); } /** * Get the media ID. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int */ public function get_id() { return $this->id; } /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid() { return $this->get_id() > 0; } /** * Get the media context name. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_context() { return $this->get_context_instance()->get_name(); } /** * Get the media context instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return ContextInterface */ public function get_context_instance() { if ( $this->context ) { if ( is_string( $this->context ) ) { $this->context = imagify_get_context( $this->context ); } return $this->context; } $class_name = get_class( $this ); $class_name = '\\' . trim( $class_name, '\\' ); $class_name = str_replace( '\\Media\\', '\\Context\\', $class_name ); $this->context = new $class_name(); return $this->context; } /** * Get the CDN instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|PushCDNInterface A PushCDNInterface instance. False if no CDN is used. */ public function get_cdn() { if ( isset( $this->cdn ) ) { return $this->cdn; } if ( ! $this->is_valid() ) { $this->cdn = false; return $this->cdn; } $media_id = $this->get_id(); $context = $this->get_context_instance(); /** * The CDN to use for this media. * * @since 1.9 * @author Grégory Viguier * * @param bool|PushCDNInterface $cdn A PushCDNInterface instance. False if no CDN is used. * @param int $media_id The media ID. * @param ContextInterface $context The context object. */ $this->cdn = apply_filters( 'imagify_cdn', false, $media_id, $context ); if ( ! $this->cdn || ! $this->cdn instanceof PushCDNInterface ) { $this->cdn = false; return $this->cdn; } return $this->cdn; } /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original media's path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_original_path() { if ( ! $this->is_valid() ) { return false; } $original_path = $this->get_raw_original_path(); if ( ! $original_path || ! $this->filesystem->exists( $original_path ) ) { return false; } return $original_path; } /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the path to the media’s full size file if the file exists. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_fullsize_path() { if ( ! $this->is_valid() ) { return false; } $original_path = $this->get_raw_fullsize_path(); if ( ! $original_path || ! $this->filesystem->exists( $original_path ) ) { return false; } return $original_path; } /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup file path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_backup_path() { if ( ! $this->is_valid() ) { return false; } $backup_path = $this->get_raw_backup_path(); if ( ! $backup_path || ! $this->filesystem->exists( $backup_path ) ) { return false; } return $backup_path; } /** * Check if the media has a backup of the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media has a backup. */ public function has_backup() { return (bool) $this->get_backup_path(); } /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media type is supported. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_supported() { return (bool) $this->get_mime_type(); } /** * Tell if the current media refers to an image, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool Returns false in case it's an image but not in a supported format (bmp for example). */ public function is_image() { if ( isset( $this->is_image ) ) { return $this->is_image; } $this->is_image = strpos( (string) $this->get_mime_type(), 'image/' ) === 0; return $this->is_image; } /** * Tell if the current media refers to a pdf, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_pdf() { if ( isset( $this->is_pdf ) ) { return $this->is_pdf; } $this->is_pdf = 'application/pdf' === $this->get_mime_type(); return $this->is_pdf; } /** * Get the original file extension (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|null */ public function get_extension() { return $this->get_file_type()->ext; } /** * Get the original file mime type (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_mime_type() { return $this->get_file_type()->type; } /** * Get the file mime type + file extension (if the file is supported by Imagify). * This test is ran against the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_allowed_mime_types() { return imagify_get_mime_types( $this->get_context_instance()->get_allowed_mime_types() ); } /** * If the media is an image, update the dimensions in the database with the current file dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function update_dimensions() { if ( ! $this->is_image() ) { // The media is not a supported image. return false; } $dimensions = $this->filesystem->get_image_size( $this->get_raw_fullsize_path() ); if ( ! $dimensions ) { // Could not get the new dimensions. return false; } $context = $this->get_context(); /** * Triggered before updating an image width and height into its metadata. * * @since 1.9 * @see Imagify_Filesystem->get_image_size() * @author Grégory Viguier * * @param int $media_id The media ID. * @param array $dimensions { * An array with, among other data: * * @type int $width The image width. * @type int $height The image height. * } */ do_action( "imagify_before_update_{$context}_media_data_dimensions", $this->get_id(), $dimensions ); $this->update_media_data_dimensions( $dimensions ); /** * Triggered after updating an image width and height into its metadata. * * @since 1.9 * @see Imagify_Filesystem->get_image_size() * @author Grégory Viguier * * @param int $media_id The media ID. * @param array $dimensions { * An array with, among other data: * * @type int $width The image width. * @type int $height The image height. * } */ do_action( "imagify_after_update_{$context}_media_data_dimensions", $this->get_id(), $dimensions ); return true; } /** * Update the media data dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $dimensions { * An array containing width and height. * * @type int $width The image width. * @type int $height The image height. * } */ abstract protected function update_media_data_dimensions( $dimensions ); /** * Get the file mime type + file extension (if the file is supported by Imagify). * This test is ran against the original file. * * @since 1.9 * @access protected * @see wp_check_filetype() * @author Grégory Viguier * * @return object */ protected function get_file_type() { if ( isset( $this->file_type ) ) { return $this->file_type; } $this->file_type = (object) [ 'ext' => '', 'type' => '', ]; if ( ! $this->is_valid() ) { return $this->file_type; } $path = $this->get_raw_fullsize_path(); if ( ! $path ) { return $this->file_type; } $this->file_type = (object) wp_check_filetype( $path, $this->get_allowed_mime_types() ); return $this->file_type; } /** * Filter the result of $this->get_media_files(). * * @since 1.9 * @access protected * @see $this->get_media_files() * @author Grégory Viguier * * @param array $files An array with the size names as keys ('full' is used for the full size file), and arrays of data as values. * @return array */ protected function filter_media_files( $files ) { /** * Filter the media files. * * @since 1.9 * @author Grégory Viguier * * @param array $files An array with the size names as keys ('full' is used for the full size file), and arrays of data as values. * @param MediaInterface $media This instance. */ return (array) apply_filters( 'imagify_media_files', $files, $this ); } } Media/Noop.php 0000644 00000021053 15174671731 0007224 0 ustar 00 <?php namespace Imagify\Media; use Imagify\CDN\PushCDNInterface; use Imagify\Context\ContextInterface; use Imagify\Deprecated\Traits\Media\NoopDeprecatedTrait; use WP_Error; /** * Fallback class for "media groups" (aka attachments). * * @since 1.9 * @author Grégory Viguier */ class Noop implements MediaInterface { use NoopDeprecatedTrait; /** * Tell if the given entry can be accepted in the constructor. * For example it can include `is_numeric( $id )` if the constructor accepts integers. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { return false; } /** * Get the media ID. * * @since 1.9 * @access public * @author Grégory Viguier * * @return int */ public function get_id() { return 0; } /** * Tell if the current media is valid. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_valid() { return false; } /** * Get the media context name. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_context() { return 'noop'; } /** * Get the media context instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return ContextInterface */ public function get_context_instance() { return \Imagify\Context\Noop::get_instance(); } /** * Get the CDN instance. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|PushCDNInterface A PushCDNInterface instance. False if no CDN is used. */ public function get_cdn() { return false; } /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_original_path() { return false; } /** * Get the original media's path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_original_path() { return false; } /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the URL of the media’s full size file. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_fullsize_url() { return false; } /** * Get the path to the media’s full size file, even if the file doesn't exist. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_fullsize_path() { return false; } /** * Get the path to the media’s full size file if the file exists. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_fullsize_path() { return false; } /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup URL, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url() { return false; } /** * Get the backup file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { return false; } /** * Get the backup file path if the file exists. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False if it doesn't exist. */ public function get_backup_path() { return false; } /** * Check if the media has a backup of the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True if the media has a backup. */ public function has_backup() { return false; } /** ----------------------------------------------------------------------------------------- */ /** THUMBNAILS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create the media thumbnails. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function generate_thumbnails() { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media type is supported. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_supported() { return false; } /** * Tell if the current media refers to an image, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool Returns false in case it's an image but not in a supported format (bmp for example). */ public function is_image() { return false; } /** * Tell if the current media refers to a pdf, based on file extension. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function is_pdf() { return false; } /** * Get the original file extension (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|null */ public function get_extension() { return ''; } /** * Get the original file mime type (if supported by Imagify). * * @since 1.9 * @access public * @author Grégory Viguier * * @return string */ public function get_mime_type() { return ''; } /** * Get the file mime type + file extension (if the file is supported by Imagify). * This test is ran against the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_allowed_mime_types() { return imagify_get_mime_types( 'all' ); } /** * Tell if the current media has the required data (the data containing the file paths and thumbnails). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_media_data() { return false; } /** * Get the list of the files of this media, including the original file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * An array with the size names as keys ('full' is used for the original file), and arrays of data as values: * * @type string $path Absolute path to the file. * @type int $width The file width. * @type int $height The file height. * @type string $mime-type The file mime type. * } */ public function get_media_files() { return []; } /** * If the media is an image, get its width and height. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { return [ 'width' => 0, 'height' => 0, ]; } /** * If the media is an image, update the dimensions in the database with the current file dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool True on success. False on failure. */ public function update_dimensions() { return false; } } Media/CustomFolders.php 0000644 00000021104 15174671731 0011077 0 ustar 00 <?php namespace Imagify\Media; use Imagify\Traits\MediaRowTrait; use Imagify\Deprecated\Traits\Media\CustomFoldersDeprecatedTrait; use WP_Error; /** * Media class for the custom folders. * * @since 1.9 * @author Grégory Viguier */ class CustomFolders extends AbstractMedia { use MediaRowTrait; use CustomFoldersDeprecatedTrait; /** * Context (where the media "comes from"). * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $context = 'custom-folders'; /** * The attachment SQL DB class. * * @var string * @since 1.9 * @access protected * @author Grégory Viguier */ protected $db_class_name = 'Imagify_Files_DB'; /** * Tell if the media/context is network-wide. * * @var bool * @since 1.9 * @access protected * @author Grégory Viguier */ protected $is_network_wide = true; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int|array|object $id The file ID. It can also be an array or object representing the file data. */ public function __construct( $id ) { if ( ! static::constructor_accepts( $id ) ) { $this->invalidate_row(); parent::__construct( 0 ); return; } if ( is_numeric( $id ) ) { $this->id = (int) $id; $this->get_row(); } else { $prim_key = $this->get_row_db_instance()->get_primary_key(); $this->row = (array) $id; $this->id = $this->row[ $prim_key ]; } parent::__construct( $this->id ); } /** * Tell if the given entry can be accepted in the constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { return $id && ( is_numeric( $id ) || is_array( $id ) || is_object( $id ) ); } /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original media's path. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_original_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path( 'original' ); } $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return false; } return \Imagify_Files_Scan::remove_placeholder( $row['path'] ); } /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the URL of the media’s full size file. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_fullsize_url() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return false; } return \Imagify_Files_Scan::remove_placeholder( $row['path'], 'url' ); } /** * Get the path to the media’s full size file, even if the file doesn't exist. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_fullsize_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path(); } $row = $this->get_row(); if ( ! $row || empty( $row['path'] ) ) { return false; } return \Imagify_Files_Scan::remove_placeholder( $row['path'] ); } /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup URL, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return site_url( $this->filesystem->make_path_relative( $this->get_raw_backup_path() ) ); } /** * Get the backup file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { if ( ! $this->is_valid() ) { return false; } return \Imagify_Custom_Folders::get_file_backup_path( $this->get_raw_original_path() ); } /** ----------------------------------------------------------------------------------------- */ /** THUMBNAILS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create the media thumbnails. * And since this context does not support thumbnails... * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A WP_Error instance on failure. */ public function generate_thumbnails() { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } return true; } /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media has the required data (the data containing the file paths and thumbnails). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_media_data() { return $this->is_valid(); } /** * Get the list of the files of this media, including the full size file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * An array with the size names as keys ('full' is used for the full size file), and arrays of data as values: * * @type string $size The size name. * @type string $path Absolute path to the file. * @type int $width The file width. * @type int $height The file height. * @type string $mime-type The file mime type. * @type bool $disabled True if the size is disabled in the plugin’s settings. * } */ public function get_media_files() { if ( ! $this->is_valid() ) { return []; } $fullsize_path = $this->get_raw_fullsize_path(); if ( ! $fullsize_path ) { return []; } $dimensions = $this->get_dimensions(); $sizes = [ 'full' => [ 'size' => 'full', 'path' => $fullsize_path, 'width' => $dimensions['width'], 'height' => $dimensions['height'], 'mime-type' => $this->get_mime_type(), 'disabled' => false, ], ]; return $this->filter_media_files( $sizes ); } /** * If the media is an image, get its width and height. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { if ( ! $this->is_image() ) { return [ 'width' => 0, 'height' => 0, ]; } $row = $this->get_row(); return [ 'width' => ! empty( $row['width'] ) ? $row['width'] : 0, 'height' => ! empty( $row['height'] ) ? $row['height'] : 0, ]; } /** * Update the media data dimensions. * * @since 1.9 * @access public * @author Grégory Viguier * * @param array $dimensions { * An array containing width and height. * * @type int $width The image width. * @type int $height The image height. * } */ protected function update_media_data_dimensions( $dimensions ) { $row = $this->get_row(); if ( ! is_array( $row ) ) { $row = []; } if ( isset( $row['width'], $row['height'] ) && $row['width'] === $dimensions['width'] && $row['height'] === $dimensions['height'] ) { return; } $row['width'] = $dimensions['width']; $row['height'] = $dimensions['height']; $this->update_row( $row ); } } Media/WP.php 0000644 00000026400 15174671731 0006640 0 ustar 00 <?php namespace Imagify\Media; use WP_Error; /** * Media class for the medias in the WP library. * * @since 1.9 * @author Grégory Viguier */ class WP extends AbstractMedia { use \Imagify\Deprecated\Traits\Media\WPDeprecatedTrait; /** * Tell if we’re playing in WP 5.3’s garden. * * @var bool * @since 1.9.8 * @access protected * @author Grégory Viguier */ protected $is_wp53 = false; /** * The constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param int|\WP_Post $id The attachment ID, or \WP_Post object. */ public function __construct( $id ) { if ( ! static::constructor_accepts( $id ) ) { parent::__construct( 0 ); return; } if ( is_numeric( $id ) ) { $id = get_post( (int) $id ); } if ( ! $id || 'attachment' !== $id->post_type ) { parent::__construct( 0 ); return; } parent::__construct( $id->ID ); } /** * Tell if the given entry can be accepted in the constructor. * * @since 1.9 * @access public * @author Grégory Viguier * * @param mixed $id Whatever. * @return bool */ public static function constructor_accepts( $id ) { return $id && ( is_numeric( $id ) || $id instanceof \WP_Post ); } /** ----------------------------------------------------------------------------------------- */ /** ORIGINAL FILE =========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the original file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_original_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path( 'original' ); } if ( $this->is_wp_53() ) { // `wp_get_original_image_path()` may return false. $path = wp_get_original_image_path( $this->id ); } else { $path = false; } if ( ! $path ) { $path = get_attached_file( $this->id ); } return $path ? $path : false; } /** ----------------------------------------------------------------------------------------- */ /** FULL SIZE FILE ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the URL of the media’s full size file. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_fullsize_url() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_url(); } $url = wp_get_attachment_url( $this->id ); return $url ? $url : false; } /** * Get the path to the media’s full size file, even if the file doesn't exist. * * @since 1.9.8 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_fullsize_path() { if ( ! $this->is_valid() ) { return false; } if ( $this->get_cdn() ) { return $this->get_cdn()->get_file_path(); } $path = get_attached_file( $this->id ); return $path ? $path : false; } /** ----------------------------------------------------------------------------------------- */ /** BACKUP FILE ============================================================================= */ /** ----------------------------------------------------------------------------------------- */ /** * Get the backup URL, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file URL. False on failure. */ public function get_backup_url() { if ( ! $this->is_valid() ) { return false; } return get_imagify_attachment_url( $this->get_raw_backup_path() ); } /** * Get the backup file path, even if the file doesn't exist. * * @since 1.9 * @access public * @author Grégory Viguier * * @return string|bool The file path. False on failure. */ public function get_raw_backup_path() { if ( ! $this->is_valid() ) { return false; } return get_imagify_attachment_backup_path( $this->get_raw_original_path() ); } /** ----------------------------------------------------------------------------------------- */ /** THUMBNAILS ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Create the media thumbnails. * With WP 5.3+, this will also generate a new full size file if the original file is wider or taller than a defined threshold. * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function generate_thumbnails() { if ( ! $this->is_valid() ) { return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; // @phpstan-ignore-line } // Store the path to the current full size file before generating the thumbnails. $old_full_size_path = $this->get_raw_fullsize_path(); $metadata = wp_generate_attachment_metadata( $this->get_id(), $this->get_raw_original_path() ); if ( empty( $metadata['file'] ) ) { update_post_meta( $this->get_id(), '_wp_attachment_metadata', $metadata ); return true; } /** * Don't change the full size file name. * WP 5.3+ will rename the full size file if the resizing threshold has changed (not the same as the one used to generate it previously). * This will force WP to keep the previous file name. */ $old_full_size_file_name = $this->filesystem->file_name( $old_full_size_path ); $new_full_size_file_name = $this->filesystem->file_name( $metadata['file'] ); if ( $new_full_size_file_name !== $old_full_size_file_name ) { $new_full_size_path = $this->filesystem->dir_path( $old_full_size_path ) . $new_full_size_file_name; $moved = $this->filesystem->move( $new_full_size_path, $old_full_size_path, true ); if ( $moved ) { $metadata['file'] = $this->filesystem->dir_path( $metadata['file'] ) . $old_full_size_file_name; update_post_meta( $this->get_id(), '_wp_attached_file', $metadata['file'] ); } } update_post_meta( $this->get_id(), '_wp_attachment_metadata', $metadata ); return true; } /** ----------------------------------------------------------------------------------------- */ /** MEDIA DATA ============================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if the current media has the required data (the data containing the file paths and thumbnails). * * @since 1.9 * @access public * @author Grégory Viguier * * @return bool */ public function has_required_media_data() { if ( ! $this->is_valid() ) { return false; } $file = get_post_meta( $this->id, '_wp_attached_file', true ); if ( ! $file || preg_match( '@://@', $file ) || preg_match( '@^.:\\\@', $file ) ) { return false; } return (bool) wp_get_attachment_metadata( $this->id, true ); } /** * Get the list of the files of this media, including the full size file. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array { * An array with the size names as keys ('full' is used for the full size file), and arrays of data as values: * * @type string $size The size name. * @type string $path Absolute path to the file. * @type int $width The file width. * @type int $height The file height. * @type string $mime-type The file mime type. * @type bool $disabled True if the size is disabled in the plugin’s settings. * } */ public function get_media_files() { if ( ! $this->is_valid() ) { return []; } $fullsize_path = $this->get_raw_fullsize_path(); if ( ! $fullsize_path ) { return []; } $dimensions = $this->get_dimensions(); $all_sizes = [ 'full' => [ 'size' => 'full', 'path' => $fullsize_path, 'width' => $dimensions['width'], 'height' => $dimensions['height'], 'mime-type' => $this->get_mime_type(), 'disabled' => false, ], ]; if ( $this->is_image() ) { $sizes = wp_get_attachment_metadata( $this->id, true ); $sizes = ! empty( $sizes['sizes'] ) && is_array( $sizes['sizes'] ) ? $sizes['sizes'] : []; $sizes = array_intersect_key( $sizes, $this->get_context_instance()->get_thumbnail_sizes() ); } else { $sizes = []; } if ( ! $sizes ) { return $all_sizes; } $dir_path = $this->filesystem->dir_path( $fullsize_path ); $disallowed_sizes = get_imagify_option( 'disallowed-sizes' ); $is_active_for_network = imagify_is_active_for_network(); foreach ( $sizes as $size => $size_data ) { $all_sizes[ $size ] = [ 'size' => $size, 'path' => $dir_path . $size_data['file'], 'width' => $size_data['width'], 'height' => $size_data['height'], 'mime-type' => $size_data['mime-type'], 'disabled' => ! $is_active_for_network && isset( $disallowed_sizes[ $size ] ), ]; } return $this->filter_media_files( $all_sizes ); } /** * If the media is an image, get its width and height. * * @since 1.9 * @access public * @author Grégory Viguier * * @return array */ public function get_dimensions() { if ( ! $this->is_image() ) { return [ 'width' => 0, 'height' => 0, ]; } $values = wp_get_attachment_image_src( $this->id, 'full' ); return [ 'width' => $values[1], 'height' => $values[2], ]; } /** * Update the media data dimensions. * * @since 1.9 * @access protected * @author Grégory Viguier * * @param array $dimensions { * An array containing width and height. * * @type int $width The image width. * @type int $height The image height. * } */ protected function update_media_data_dimensions( $dimensions ) { $metadata = wp_get_attachment_metadata( $this->id ); if ( ! is_array( $metadata ) ) { $row = []; } if ( isset( $metadata['width'], $metadata['height'] ) && $metadata['width'] === $dimensions['width'] && $metadata['height'] === $dimensions['height'] ) { return; } $metadata['width'] = $dimensions['width']; $metadata['height'] = $dimensions['height']; update_post_meta( $this->get_id(), '_wp_attachment_metadata', $metadata ); } /** ----------------------------------------------------------------------------------------- */ /** INTERNAL TOOLS ========================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Tell if we’re playing in WP 5.3’s garden. * * @since 1.9.8 * @access protected * @author Grégory Viguier * * @return bool */ protected function is_wp_53() { $this->is_wp53 = function_exists( 'wp_get_original_image_path' ); return $this->is_wp53; } } Webp/Apache.php 0000644 00000001207 15174671731 0007347 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp; use Imagify\WriteFile\AbstractApacheDirConfFile; /** * Add and remove contents to the .htaccess file to display WebP images on the site. * * @since 1.9 */ class Apache extends AbstractApacheDirConfFile { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 */ const TAG_NAME = 'Imagify: webp file type'; /** * Get unfiltered new contents to write into the file. * * @since 1.9 * * @return string */ protected function get_raw_new_contents() { return trim( ' <IfModule mod_mime.c> AddType image/webp .webp </IfModule>' ); } } Webp/RewriteRules/Apache.php 0000644 00000002705 15174671731 0012007 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp\RewriteRules; use Imagify\WriteFile\AbstractApacheDirConfFile; /** * Add and remove rewrite rules to the .htaccess file to display WebP images on the site. * * @since 1.9 */ class Apache extends AbstractApacheDirConfFile { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 */ const TAG_NAME = 'Imagify: rewrite rules for webp'; /** * Get unfiltered new contents to write into the file. * * @since 1.9 * @source https://github.com/vincentorback/WebP-images-with-htaccess * * @return string */ protected function get_raw_new_contents() { $extensions = $this->get_extensions_pattern(); $extensions = str_replace( '|webp', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' <IfModule mod_setenvif.c> # Vary: Accept for all the requests to jpeg, png, and gif. SetEnvIf Request_URI "\.(' . $extensions . ')$" REQUEST_image </IfModule> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase ' . $home_root . ' # Check if browser supports WebP images. RewriteCond %{HTTP_ACCEPT} image/webp # Check if WebP replacement image exists. RewriteCond %{REQUEST_FILENAME}.webp -f # Serve WebP image instead. RewriteRule (.+)\.(' . $extensions . ')$ $1.$2.webp [T=image/webp,NC] </IfModule> <IfModule mod_headers.c> Header append Vary Accept env=REQUEST_image </IfModule>' ); } } Webp/RewriteRules/Display.php 0000644 00000012706 15174671731 0012235 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp\RewriteRules; use Imagify\EventManagement\SubscriberInterface; use Imagify\Notices\Notices; use Imagify\WriteFile\WriteFileInterface; /** * Display WebP images on the site with rewrite rules. * * @since 1.9 */ class Display implements SubscriberInterface { /** * Configuration file writer. * * @var WriteFileInterface|null */ protected $server_conf = null; /** * Option value. * * @var string * @since 1.9 */ const OPTION_VALUE = 'rewrite'; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_settings_on_save' => [ 'maybe_add_rewrite_rules', 10 ], 'imagify_settings_webp_info' => 'maybe_add_webp_info', 'imagify_activation' => 'activate', 'imagify_deactivation' => 'deactivate', ]; } /** * If display WebP images via rewrite rules, add the rules to the .htaccess/etc file. * * @since 1.9 * * @param array $values The option values. * * @return array */ public function maybe_add_rewrite_rules( $values ) { $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); $is_enabled = ! empty( $values['display_nextgen'] ); // Which method? $old_value = get_imagify_option( 'display_nextgen_method' ); $new_value = ! empty( $values['display_nextgen_method'] ) ? $values['display_nextgen_method'] : ''; // Decide when to add or remove rules. $is_rewrite = self::OPTION_VALUE === $new_value; $was_rewrite = self::OPTION_VALUE === $old_value; if ( ! $this->get_server_conf() ) { return $values; } $result = false; if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) { // Add the rewrite rules. $result = $this->get_server_conf()->add(); } elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) { // Remove the rewrite rules. $result = $this->get_server_conf()->remove(); } if ( ! is_wp_error( $result ) ) { return $values; } // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); return $values; } Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); return $values; } /** * If the conf file is not writable, add a warning. * * @since 1.9 */ public function maybe_add_webp_info() { global $is_nginx; $conf = $this->get_server_conf(); if ( ! $conf ) { return; } $writable = $conf->is_file_writable(); if ( is_wp_error( $writable ) ) { $rules = $conf->get_new_contents(); if ( ! $rules ) { // Uh? return; } printf( /* translators: %s is a file name. */ esc_html__( 'If you choose to use rewrite rules, you will have to add the following lines manually to the %s file:', 'imagify' ), '<code>' . $this->get_file_path( true ) . '</code>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<pre class="code">' . esc_html( $rules ) . '</pre>'; } elseif ( $is_nginx ) { printf( /* translators: %s is a file name. */ esc_html__( 'If you choose to use rewrite rules, the file %s will be created and must be included into the server’s configuration file (then restart the server).', 'imagify' ), '<code>' . $this->get_file_path( true ) . '</code>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } } /** * Add rules on plugin activation. * * @since 1.9 */ public function activate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } if ( is_wp_error( $conf->is_file_writable() ) ) { return; } $conf->add(); } /** * Remove rules on plugin deactivation. * * @since 1.9 */ public function deactivate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } $file_path = $conf->get_file_path(); $filesystem = \Imagify_Filesystem::get_instance(); if ( ! $filesystem->exists( $file_path ) ) { return; } if ( ! $filesystem->is_writable( $file_path ) ) { return; } $conf->remove(); } /** * Get the path to the directory conf file. * * @since 1.9 * * @param bool $relative True to get a path relative to the site’s root. * * @return string|bool The file path. False on failure. */ public function get_file_path( $relative = false ) { if ( ! $this->get_server_conf() ) { return false; } $file_path = $this->get_server_conf()->get_file_path(); if ( $relative ) { return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); } return $file_path; } /** * Get the server conf instance. * * @since 1.9 * * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7, $is_nginx; if ( isset( $this->server_conf ) ) { return $this->server_conf; } if ( $is_apache ) { $this->server_conf = new Apache(); } elseif ( $is_iis7 ) { $this->server_conf = new IIS(); } elseif ( $is_nginx ) { $this->server_conf = new Nginx(); } return $this->server_conf; } } Webp/RewriteRules/IIS.php 0000644 00000003523 15174671731 0011251 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp\RewriteRules; use Imagify\WriteFile\AbstractIISDirConfFile; /** * Add and remove rewrite rules to the web.config file to display WebP images on the site. * * @since 1.9 */ class IIS extends AbstractIISDirConfFile { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 */ const TAG_NAME = 'Imagify: rewrite rules for webp'; /** * Get unfiltered new contents to write into the file. * * @since 1.9 * @source https://github.com/igrigorik/webp-detect/blob/master/iis.config * * @return string */ protected function get_raw_new_contents() { $extensions = $this->get_extensions_pattern(); $extensions = str_replace( '|webp', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' <!-- @parent /configuration/system.webServer/rewrite/rules --> <rule name="' . esc_attr( static::TAG_NAME ) . ' 2"> <match url="^(' . $home_root . '.+)\.(' . $extensions . ')$" ignoreCase="true" /> <conditions logicalGrouping="MatchAll"> <add input="{HTTP_ACCEPT}" pattern="image/webp" ignoreCase="false" /> <add input="{DOCUMENT_ROOT}/{R:1}{R:2}.webp" matchType="IsFile" /> </conditions> <action type="Rewrite" url="{R:1}{R:2}.webp" logRewrittenUrl="true" /> <serverVariables> <set name="ACCEPTS_WEBP" value="true" /> </serverVariables> </rule> <!-- @parent /configuration/system.webServer/rewrite/outboundRules --> <rule preCondition="IsWebp" name="' . esc_attr( static::TAG_NAME ) . ' 3"> <match serverVariable="RESPONSE_Vary" pattern=".*" /> <action type="Rewrite" value="Accept"/> </rule> <preConditions name="' . esc_attr( static::TAG_NAME ) . ' 4"> <preCondition name="IsWebp"> <add input="{ACCEPTS_WEBP}" pattern="true" ignoreCase="false" /> </preCondition> </preConditions>' ); } } Webp/RewriteRules/Nginx.php 0000644 00000002440 15174671731 0011705 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp\RewriteRules; use Imagify\WriteFile\AbstractNginxDirConfFile; /** * Add and remove rewrite rules to the imagify.conf file to display WebP images on the site. * * @since 1.9 */ class Nginx extends AbstractNginxDirConfFile { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 */ const TAG_NAME = 'Imagify: rewrite rules for webp'; /** * Get unfiltered new contents to write into the file. * * @since 1.9 * * @return string */ protected function get_raw_new_contents() { $extensions = $this->get_extensions_pattern() . '|avif'; $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' location ~* ^(' . $home_root . '.+)\.(' . $extensions . ')$ { add_header Vary Accept; set $canavif 1; if ($http_accept !~* "avif"){ set $canavif 0; } if (!-f $request_filename.avif) { set $canavif 0; } if ($canavif = 1){ rewrite ^(.*) $1.avif; break; } set $canwebp 1; if ($http_accept !~* "webp"){ set $canwebp 0; } if (!-f $request_filename.webp) { set $canwebp 0; } if ($canwebp = 1){ rewrite ^(.*) $1.webp; break; } }' ); } } Webp/ServiceProvider.php 0000644 00000002240 15174671731 0011277 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use Imagify\Webp\RewriteRules\Display as RewriteRules; /** * Service provider for WebP rewrite rules */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ Display::class, RewriteRules::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ Display::class, RewriteRules::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( Display::class ); $this->getContainer()->addShared( RewriteRules::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } Webp/Display.php 0000644 00000010547 15174671731 0007602 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp; use Imagify\EventManagement\SubscriberInterface; use Imagify\Notices\Notices; use Imagify\Traits\InstanceGetterTrait; use Imagify\WriteFile\WriteFileInterface; /** * Display WebP images on the site using picture tag. * * @since 1.9 */ class Display implements SubscriberInterface { use InstanceGetterTrait; /** * Server conf object. * * @var WriteFileInterface|null * @since 1.9 */ protected $server_conf = null; /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { return [ 'imagify_settings_on_save' => [ 'maybe_add_rewrite_rules', 13 ], 'imagify_settings_webp_info' => 'maybe_add_webp_info', 'imagify_activation' => 'activate', 'imagify_deactivation' => 'deactivate', ]; } /** * If display WebP images, add the WebP type to the .htaccess/etc file. * * @since 1.9 * * @param array $values The option values. * * @return array */ public function maybe_add_rewrite_rules( $values ) { if ( ! $this->get_server_conf() ) { return $values; } $enabled = isset( $values['display_nextgen'] ) ? true : false; $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); $result = false; if ( $enabled && ! $was_enabled ) { // Add the WebP file type. $result = $this->get_server_conf()->add(); } elseif ( ! $enabled && $was_enabled ) { // Remove the WebP file type. $result = $this->get_server_conf()->remove(); } if ( ! is_wp_error( $result ) ) { return $values; } // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); return $values; } Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); return $values; } /** * If the conf file is not writable, add a warning. * * @since 1.9 */ public function maybe_add_webp_info() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } $writable = $conf->is_file_writable(); if ( ! is_wp_error( $writable ) ) { return; } $rules = $conf->get_new_contents(); if ( ! $rules ) { // Uh? return; } echo '<br/>'; printf( /* translators: %s is a file name. */ esc_html__( 'Imagify does not seem to be able to edit or create a %s file, you will have to add the following lines manually to it:', 'imagify' ), '<code>' . $this->get_file_path( true ) . '</code>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); echo '<pre class="code">' . esc_html( $rules ) . '</pre>'; } /** * Add rules on plugin activation. * * @since 1.9 */ public function activate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } if ( is_wp_error( $conf->is_file_writable() ) ) { return; } $conf->add(); } /** * Remove rules on plugin deactivation. * * @since 1.9 */ public function deactivate() { $conf = $this->get_server_conf(); if ( ! $conf ) { return; } $file_path = $conf->get_file_path(); $filesystem = \Imagify_Filesystem::get_instance(); if ( ! $filesystem->exists( $file_path ) ) { return; } if ( ! $filesystem->is_writable( $file_path ) ) { return; } $conf->remove(); } /** * Get the path to the directory conf file. * * @since 1.9 * * @param bool $relative True to get a path relative to the site’s root. * @return string|bool The file path. False on failure. */ public function get_file_path( $relative = false ) { if ( ! $this->get_server_conf() ) { return false; } $file_path = $this->get_server_conf()->get_file_path(); if ( $relative ) { return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); } return $file_path; } /** * Get the server conf instance. * Note: nothing needed for nginx. * * @since 1.9 * * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7; if ( isset( $this->server_conf ) ) { return $this->server_conf; } if ( $is_apache ) { $this->server_conf = new Apache(); } elseif ( $is_iis7 ) { $this->server_conf = new IIS(); } return $this->server_conf; } } Webp/IIS.php 0000644 00000001374 15174671731 0006617 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Webp; use Imagify\WriteFile\AbstractIISDirConfFile; /** * Add and remove contents to the web.config file to display WebP images on the site. * * @since 1.9 */ class IIS extends AbstractIISDirConfFile { /** * Name of the tag used as block delemiter. * * @var string * @since 1.9 */ const TAG_NAME = 'Imagify: webp file type'; /** * Get unfiltered new contents to write into the file. * * @since 1.9 * * @return string */ protected function get_raw_new_contents() { return trim( ' <!-- @parent /configuration/system.webServer --> <staticContent name="' . esc_attr( static::TAG_NAME ) . ' 1"> <mimeMap fileExtension=".webp" mimeType="image/webp" /> </staticContent>' ); } } Traits/InstanceGetterTrait.php 0000644 00000001505 15174671731 0012463 0 ustar 00 <?php namespace Imagify\Traits; /** * Trait that simulates a singleton pattern. * The idea is more to ease the instance retrieval than to prevent multiple instances. * This is temporary, until we get a DI container. * * @since 1.9 * @since 1.9.4 Renamed into InstanceGetterTrait. * @author Grégory Viguier */ trait InstanceGetterTrait { /** * The "not-so-single" instance of the class. * * @var ?object * @since 1.9 * @access protected * @author Grégory Viguier */ protected static $instance = null; /** * Get the main Instance. * * @since 1.9 * @access protected * @author Grégory Viguier * * @return object Main instance. */ public static function get_instance() { if ( ! isset( static::$instance ) ) { static::$instance = new static(); } return static::$instance; } } Traits/MediaRowTrait.php 0000644 00000003716 15174671731 0011261 0 ustar 00 <?php namespace Imagify\Traits; /** * Trait to use to connect medias and database. * It also cache the results. * Classes using that trait must define a protected property $db_class_name (string) containing the media SQL DB class name. * * @since 1.9 */ trait MediaRowTrait { /** * The media SQL data row. * * @var ?array * @since 1.9 */ protected $row; /** * The media ID. * * @var int * @since 1.9 */ protected $id; /** * Get the row. * * @since 1.9 * * @return array */ public function get_row() { if ( isset( $this->row ) ) { return $this->row; } if ( ! $this->db_class_name || $this->id <= 0 ) { return $this->invalidate_row(); } $this->row = $this->get_row_db_instance()->get( $this->id ); if ( ! $this->row ) { return $this->invalidate_row(); } return $this->row; } /** * Update the row. * * @since 1.9 * * @param array $data The data to update. */ public function update_row( $data ) { if ( ! $this->db_class_name || $this->id <= 0 ) { return; } $this->get_row_db_instance()->update( $this->id, $data ); $this->reset_row_cache(); } /** * Delete the row. * * @since 1.9 */ public function delete_row() { if ( ! $this->db_class_name || $this->id <= 0 ) { return; } $this->get_row_db_instance()->delete( $this->id ); $this->invalidate_row(); } /** * Shorthand to get the DB table instance. * * @since 1.9 * * @return \Imagify\DB\DBInterface The DB table instance. */ public function get_row_db_instance() { return call_user_func( [ $this->db_class_name, 'get_instance' ] ); } /** * Invalidate the row, by setting it to an empty array. * * @since 1.9 * * @return array The row. */ public function invalidate_row() { $this->row = []; return $this->row; } /** * Reset the row cache. * * @since 1.9 * * @return null The row. */ public function reset_row_cache() { $this->row = null; return $this->row; } } Admin/PluginFamilySubscriber.php 0000644 00000002224 15174671731 0012745 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Admin; use Imagify\EventManagement\SubscriberInterface; use Imagify\Dependencies\WPMedia\PluginFamily\Controller\{ PluginFamily, PluginFamilyInterface }; /** * Process plugin family actions. */ class PluginFamilySubscriber implements SubscriberInterface, PluginFamilyInterface { /** * PluginFamily instance. * * @var PluginFamily */ protected $plugin_family; /** * Instantiate the class * * @param PluginFamily $plugin_family PluginFamily instance. */ public function __construct( PluginFamily $plugin_family ) { $this->plugin_family = $plugin_family; } /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { $events = PluginFamily::get_subscribed_events(); return $events; } /** * Process to install & activate plugin. * * @return void */ public function install_activate() { $this->plugin_family->install_activate(); } /** * Display error notice if available. * * @return void */ public function display_error_notice() { $this->plugin_family->display_error_notice(); } } Admin/ServiceProvider.php 0000644 00000003004 15174671731 0011431 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Admin; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; use Imagify\Dependencies\WPMedia\PluginFamily\Controller\PluginFamily; use Imagify\User\User; /** * Service provider for Admin. */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ AdminBar::class, AdminSubscriber::class, PluginFamily::class, PluginFamilySubscriber::class, ]; /** * Subscribers provided by this provider * * @var array */ public $subscribers = [ AdminBar::class, AdminSubscriber::class, PluginFamilySubscriber::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->addShared( AdminBar::class ) ->addArgument( User::class ); $this->getContainer()->addShared( AdminSubscriber::class ) ->addArgument( User::class ); $this->getContainer()->add( PluginFamily::class ); $this->getContainer()->addShared( PluginFamilySubscriber::class ) ->addArgument( PluginFamily::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return $this->subscribers; } } Admin/AdminBar.php 0000644 00000013140 15174671731 0007775 0 ustar 00 <?php declare( strict_types=1 ); namespace Imagify\Admin; use Imagify\EventManagement\SubscriberInterface; use Imagify\User\User; use Imagify_Views; use WP_Admin_Bar; /** * Admin bar handler */ class AdminBar implements SubscriberInterface { /** * User instance. * * @var User */ private $user; /** * AdminBar constructor. * * @param User $user User instance. */ public function __construct( User $user ) { $this->user = $user; } /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events(): array { return [ 'wp_ajax_imagify_get_admin_bar_profile' => 'get_admin_bar_profile_callback', 'admin_bar_menu' => [ 'add_imagify_admin_bar_menu', IMAGIFY_INT_MAX ], ]; } /** * Add Imagify menu in the admin bar. * * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference. */ public function add_imagify_admin_bar_menu( $wp_admin_bar ) { if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { return; } if ( ! get_imagify_option( 'admin_bar_menu' ) ) { return; } // Parent. $wp_admin_bar->add_menu( [ 'id' => 'imagify', 'title' => 'Imagify', 'href' => get_imagify_admin_url(), ] ); // Settings. $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-settings', 'title' => __( 'Settings' ), 'href' => get_imagify_admin_url(), ] ); // Bulk Optimization. if ( ! is_network_admin() ) { $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-bulk-optimization', 'title' => __( 'Bulk Optimization', 'imagify' ), 'href' => get_imagify_admin_url( 'bulk-optimization' ), ] ); } // Documentation. $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-documentation', 'title' => __( 'Documentation', 'imagify' ), 'href' => imagify_get_external_url( 'documentation' ), 'meta' => [ 'target' => '_blank', ], ] ); // Rate it. $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-rate-it', /* translators: %s is WordPress.org. */ 'title' => sprintf( __( 'Rate Imagify on %s', 'imagify' ), 'WordPress.org' ), 'href' => imagify_get_external_url( 'rate' ), 'meta' => [ 'target' => '_blank', ], ] ); // Quota & Profile informations. if ( ( defined( 'IMAGIFY_HIDDEN_ACCOUNT' ) && IMAGIFY_HIDDEN_ACCOUNT ) || ! get_imagify_option( 'api_key' ) ) { return; } $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-upgrade-plan', 'title' => '<div id="wp-admin-bar-imagify-pricing-content" class="hide-if-no-js"></div>', ] ); $wp_admin_bar->add_menu( [ 'parent' => 'imagify', 'id' => 'imagify-profile', 'title' => wp_nonce_field( 'imagify-get-admin-bar-profile', 'imagifygetadminbarprofilenonce', false, false ) . '<div id="wp-admin-bar-imagify-profile-loading" class="hide-if-no-js">' . __( 'Loading...', 'imagify' ) . '</div><div id="wp-admin-bar-imagify-profile-content" class="hide-if-no-js"></div>', ] ); } /** * Get admin bar profile output. * * @return void */ public function get_admin_bar_profile_callback() { imagify_check_nonce( 'imagify-get-admin-bar-profile', 'imagifygetadminbarprofilenonce' ); if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) { imagify_die(); } $views = Imagify_Views::get_instance(); $unconsumed_quota = $views->get_quota_percent(); $text = ''; $button_text = ''; $upgrade_link = ''; if ( $this->user->is_free() ) { $text = esc_html__( 'Upgrade your plan now for more!', 'imagify' ) . '<br>' . esc_html__( 'From $5.99/month only, keep going with image optimization!', 'imagify' ); $button_text = esc_html__( 'Upgrade My Plan', 'imagify' ); $upgrade_link = IMAGIFY_APP_DOMAIN . '/subscription/?utm_source=plugin&utm_medium=notification'; } elseif ( $this->user->is_growth() ) { $text = esc_html__( 'Switch to Infinite plan for unlimited optimization:', 'imagify' ) . '<br>'; if ( $this->user->is_monthly ) { $text .= esc_html__( 'For $11.99/month, optimize as many images as you like!', 'imagify' ); $upgrade_link = IMAGIFY_APP_DOMAIN . '/subscription/plan_switch/?label=infinite&payment_plan=1&utm_source=plugin&utm_medium=notification '; } else { $text .= esc_html__( 'For $9.99/month, optimize as many images as you like!', 'imagify' ); $upgrade_link = IMAGIFY_APP_DOMAIN . '/subscription/plan_switch/?label=infinite&payment_plan=2&utm_source=plugin&utm_medium=notification '; } $button_text = esc_html__( 'Switch To Infinite Plan', 'imagify' ); } $data = [ 'quota_icon' => $views->get_quota_icon(), 'quota_class' => $views->get_quota_class(), 'plan_label' => $this->user->plan_label, 'plan_with_quota' => $this->user->is_free() || $this->user->is_growth(), 'unconsumed_quota' => $unconsumed_quota, 'user_quota' => $this->user->get_quota(), 'next_update' => $this->user->next_date_update, 'text' => $text, 'button_text' => $button_text, 'upgrade_link' => $upgrade_link, ]; $template = [ 'admin_bar_status' => $views->get_template( 'admin/admin-bar-status', $data ), 'admin_bar_pricing' => $views->get_template( 'admin/admin-bar-pricing', [ 'upgrade_pricing' => $this->user->is_free() && ( $this->user->get_percent_unconsumed_quota() > 20 ), ] ), ]; wp_send_json_success( $template ); } } Admin/AdminSubscriber.php 0000644 00000003377 15174671731 0011407 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\Admin; use Imagify\EventManagement\SubscriberInterface; use Imagify\User\User; /** * Admin Subscriber */ class AdminSubscriber implements SubscriberInterface { /** * User instance. * * @var User */ protected $user; /** * Instantiate the class * * @param User $user User instance. */ public function __construct( User $user ) { $this->user = $user; } /** * Returns an array of events this subscriber listens to * * @return array */ public static function get_subscribed_events() { $basename = plugin_basename( IMAGIFY_FILE ); return [ 'plugin_action_links_' . $basename => 'plugin_action_links', 'network_admin_plugin_action_links_' . $basename => 'plugin_action_links', ]; } /** * Add links to the plugin row in the plugins list. * * @since 1.7 * * @param array $actions An array of action links. * @return array */ public function plugin_action_links( $actions ) { $text = 1 !== $this->user->get_plan_id() ? __( 'Documentation', 'imagify' ) : __( 'Upgrade', 'imagify' ); $url = 1 !== $this->user->get_plan_id() ? 'documentation' : 'subscription'; $class = 1 !== $this->user->get_plan_id() ? '' : ' class="imagify-plugin-upgrade"'; array_unshift( $actions, sprintf( '<a href="%s" target="_blank"%s>%s</a>', esc_url( imagify_get_external_url( $url ) ), $class, $text ) ); array_unshift( $actions, sprintf( '<a href="%s">%s</a>', esc_url( get_imagify_admin_url( 'bulk-optimization' ) ), __( 'Bulk Optimization', 'imagify' ) ) ); array_unshift( $actions, sprintf( '<a href="%s">%s</a>', esc_url( get_imagify_admin_url() ), __( 'Settings', 'imagify' ) ) ); return $actions; } } User/ServiceProvider.php 0000644 00000001552 15174671731 0011325 0 ustar 00 <?php declare(strict_types=1); namespace Imagify\User; use Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider; /** * Service provider for Picture display */ class ServiceProvider extends AbstractServiceProvider { /** * Services provided by this provider * * @var array */ protected $provides = [ User::class, ]; /** * Check if the service provider provides a specific service. * * @param string $id The id of the service. * * @return bool */ public function provides( string $id ): bool { return in_array( $id, $this->provides, true ); } /** * Registers the provided classes * * @return void */ public function register(): void { $this->getContainer()->add( User::class ); } /** * Returns the subscribers array * * @return array */ public function get_subscribers() { return []; } } User/User.php 0000644 00000014645 15174671731 0007137 0 ustar 00 <?php namespace Imagify\User; use Imagify_Data; use WP_Error; /** * Imagify User class. * * @since 1.0 */ class User { /** * The Imagify user ID. * * @since 1.0 * * @var string */ public $id; /** * The user email. * * @since 1.0 * * @var string */ public $email; /** * The plan ID. * * @since 1.0 * * @var int */ public $plan_id; /** * The plan label. * * @since 1.2 * * @var string */ public $plan_label; /** * The total quota. * * @since 1.0 * * @var int */ public $quota; /** * The total extra quota (Imagify Pack). * * @since 1.0 * * @var int */ public $extra_quota; /** * The extra quota consumed. * * @since 1.0 * * @var int */ public $extra_quota_consumed; /** * The current month consumed quota. * * @since 1.0 * * @var int */ public $consumed_current_month_quota; /** * The next month date to credit the account. * * @since 1.1.1 * * @var string */ public $next_date_update; /** * If the account is activate or not. * * @since 1.0.1 * * @var bool */ public $is_active; /** * If the account is monthly or yearly. * * @var bool */ public $is_monthly; /** * Store a \WP_Error object if the request to fetch the user data failed. * False overwise. * * @var bool|WP_Error * @since 1.9.9 */ private $error = false; /** * Initialisation. * * @var bool */ protected $initialized = false; /** * Initialise the user data by fetching the api data * * @return void */ public function init_user() { if ( $this->initialized ) { return; } $user = get_imagify_user(); if ( is_wp_error( $user ) ) { $this->error = $user; return; } $this->set_user_properties( $user ); $this->initialized = true; } /** * Set user properties * * @param object $user User object data. * * @return void */ private function set_user_properties( $user ) { $this->id = $user->id; $this->email = $user->email; $this->plan_id = (int) $user->plan_id; $this->plan_label = ucfirst( $user->plan_label ); $this->quota = $user->quota; $this->extra_quota = $user->extra_quota; $this->extra_quota_consumed = $user->extra_quota_consumed; $this->consumed_current_month_quota = $user->consumed_current_month_quota; $this->next_date_update = $user->next_date_update; $this->is_active = $user->is_active; $this->is_monthly = $user->is_monthly; } /** * Get the possible error returned when fetching user data. * * @return bool|WP_Error A \WP_Error object if the request to fetch the user data failed. False overwise. * @since 1.9.9 */ public function get_error() { $this->init_user(); return $this->error; } /** * Percentage of consumed quota, including extra quota. * * @since 1.0 * * @return float|int */ public function get_percent_consumed_quota() { static $done = false; if ( $this->get_error() ) { return 0; } $quota = $this->quota; $consumed_quota = $this->consumed_current_month_quota; if ( imagify_round_half_five( $this->extra_quota_consumed ) < $this->extra_quota ) { $quota += $this->extra_quota; $consumed_quota += $this->extra_quota_consumed; } if ( ! $quota || ! $consumed_quota ) { $percent = 0; } else { $percent = 100 * $consumed_quota / $quota; $percent = round( $percent, 1 ); $percent = min( max( 0, $percent ), 100 ); } $percent = (float) $percent; if ( $done ) { return $percent; } $previous_percent = Imagify_Data::get_instance()->get( 'previous_quota_percent' ); // Percent is not 100% anymore. if ( 100.0 === (float) $previous_percent && $percent < 100 ) { /** * Triggered when the consumed quota percent decreases below 100%. * * @since 1.7 * @author Grégory Viguier * * @param float|int $percent The current percentage of consumed quota. */ do_action( 'imagify_not_over_quota_anymore', $percent ); } // Percent is not >= 80% anymore. if ( ( (float) $previous_percent >= 80.0 && $percent < 80 ) ) { /** * Triggered when the consumed quota percent decreases below 80%. * * @since 1.7 * @author Grégory Viguier * * @param float|int $percent The current percentage of consumed quota. * @param float|int $previous_percent The previous percentage of consumed quota. */ do_action( 'imagify_not_almost_over_quota_anymore', $percent, $previous_percent ); } if ( (float) $previous_percent !== (float) $percent ) { Imagify_Data::get_instance()->set( 'previous_quota_percent', $percent ); } $done = true; return $percent; } /** * Count percent of unconsumed quota. * * @since 1.0 * * @return float|int */ public function get_percent_unconsumed_quota() { $this->init_user(); return 100 - $this->get_percent_consumed_quota(); } /** * Check if the user has a free account. * * @since 1.1.1 * * @return bool */ public function is_free() { $this->init_user(); return 1 === $this->plan_id; } /** * Check if the user is a growth account * * @return bool */ public function is_growth() { $this->init_user(); return ( 16 === $this->plan_id || 18 === $this->plan_id ); } /** * Check if the user is an infinite account * * @return bool */ public function is_infinite() { $this->init_user(); return ( 15 === $this->plan_id || 17 === $this->plan_id ); } /** * Check if the user has consumed all his/her quota. * * @since 1.1.1 * @since 1.9.9 Return false if the request to fetch the user data failed. * * @return bool */ public function is_over_quota() { if ( $this->get_error() ) { return false; } return ( $this->is_free() && floatval( 100 ) === round( $this->get_percent_consumed_quota() ) ); } /** * Get user Id * * @return string */ public function get_id() { $this->init_user(); return $this->id; } /** * Get user email. * * @return string */ public function get_email() { $this->init_user(); return $this->email; } /** * Get plan id. * * @return int */ public function get_plan_id() { $this->init_user(); return $this->plan_id; } /** * Get user quota. * * @return int */ public function get_quota() { $this->init_user(); return $this->quota; } } Context/AbstractContext.php 0000644 00000016222 15174671731 0012030 0 ustar 00 <?php namespace Imagify\Context; /** * Abstract used for contexts. * * @since 1.9 * @author Grégory Viguier */ abstract class AbstractContext implements ContextInterface { /** * Context "short name". * * @var string * @since 1.9 * @author Grégory Viguier */ protected $context = ''; /** * Tell if the media/context is network-wide. * * @var bool * @since 1.9 * @author Grégory Viguier */ protected $is_network_wide = false; /** * Type of files this context allows. * * @var string Possible values are: * - 'all' to allow all types. * - 'image' to allow only images. * - 'not-image' to allow only pdf files. * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier */ protected $allowed_mime_types = 'all'; /** * The thumbnail sizes for this context, except the full size. * * @var array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } * @since 1.9 * @author Grégory Viguier */ protected $thumbnail_sizes = []; /** * Tell if the optimization process is allowed to backup in this context. * * @var bool * @since 1.9 * @author Grégory Viguier */ protected $can_backup = false; /** * Get the context "short name". * * @since 1.9 * @author Grégory Viguier * * @return string */ public function get_name() { return $this->context; } /** * Tell if the context is network-wide. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_network_wide() { return $this->is_network_wide; } /** * Get the type of files this context allows. * * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier * * @return string Possible values are: * - 'all' to allow all types. * - 'image' to allow only images. * - 'not-image' to allow only pdf files. */ public function get_allowed_mime_types() { return $this->allowed_mime_types; } /** * Get the thumbnail sizes for this context, except the full size. * * @since 1.9 * @author Grégory Viguier * * @return array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } */ public function get_thumbnail_sizes() { return $this->thumbnail_sizes; } /** * Tell if the optimization process is allowed resize in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_resize() { return $this->get_resizing_threshold() > 0; } /** * Tell if the optimization process is allowed to backup in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_backup() { return $this->can_backup; } /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function current_user_can( $describer, $media_id = null ) { return $this->user_can( 0, $describer, $media_id ); } /** * Tell if a user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param int|\WP_User $user_id A user ID or \WP_User object. Fallback to the current user ID. * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function user_can( $user_id, $describer, $media_id = null ) { $user = 0; $current_user_id = get_current_user_id(); if ( ! $user_id ) { $user = $current_user_id; $user_id = $current_user_id; } elseif ( $user_id instanceof \WP_User ) { $user = $user_id; $user_id = (int) $user->ID; } elseif ( is_numeric( $user_id ) ) { $user = (int) $user_id; $user_id = $user; } else { $user_id = 0; } if ( ! $user_id ) { return false; } $media_id = $media_id ? (int) $media_id : null; $capacity = $this->get_capacity( $describer ); if ( $user_id === $current_user_id ) { $user_can = current_user_can( $capacity, $media_id ); /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.6.11 * @since 1.9 Added the context name as parameter. * * @param bool $user_can Tell if the current user is allowed to operate Imagify in this context. * @param string $capacity The user capacity. * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @param string $context The context name. */ $user_can = wpm_apply_filters_typed( 'boolean', 'imagify_current_user_can', $user_can, $capacity, $describer, $media_id, $this->get_name() ); } else { $user_can = user_can( $user, $capacity, $media_id ); } /** * Tell if the given user is allowed to operate Imagify in this context. * * @since 1.9 * * @param bool $user_can Tell if the given user is allowed to operate Imagify in this context. * @param int $user_id The user ID. * @param string $capacity The user capacity. * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @param string $context The context name. */ return wpm_apply_filters_typed( 'boolean', 'imagify_user_can', $user_can, $user_id, $capacity, $describer, $media_id, $this->get_name() ); } /** * Filter a user capacity used to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $capacity The user capacity. * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ protected function filter_capacity( $capacity, $describer ) { /** * Filter a user capacity used to operate Imagify in this context. * * @since 1.0 * @since 1.6.5 Added $force_mono parameter. * @since 1.6.11 Replaced $force_mono by $describer. * @since 1.9 Added the context name as parameter. * * @param string $capacity The user capacity. * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @param string $context The context name. */ return wpm_apply_filters_typed( 'string', 'imagify_capacity', $capacity, $describer, $this->get_name() ); } } Context/ContextInterface.php 0000644 00000006376 15174671731 0012176 0 ustar 00 <?php namespace Imagify\Context; /** * Interface to use for contexts. * * @since 1.9 * @author Grégory Viguier */ interface ContextInterface { /** * Get the main Instance. * * @since 1.9 * @author Grégory Viguier * * @return object Main instance. */ public static function get_instance(); /** * Get the context "short name". * * @since 1.9 * @author Grégory Viguier * * @return string */ public function get_name(); /** * Tell if the context is network-wide. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_network_wide(); /** * Get the type of files this context allows. * * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier * * @return string Possible values are: * - 'all' to allow all types. * - 'image' to allow only images. * - 'not-image' to allow only pdf files. */ public function get_allowed_mime_types(); /** * Get the thumbnail sizes for this context, except the full size. * * @since 1.9 * @author Grégory Viguier * * @return array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } */ public function get_thumbnail_sizes(); /** * Get images max width for this context. This is used when resizing. * 0 means to not resize. * * @since 1.9.8 * @author Grégory Viguier * * @return int */ public function get_resizing_threshold(); /** * Tell if the optimization process is allowed resize in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_resize(); /** * Tell if the optimization process is allowed to backup in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_backup(); /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function current_user_can( $describer, $media_id = null ); /** * Tell if a user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param int $user_id A user ID. * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function user_can( $user_id, $describer, $media_id = null ); /** * Get user capacity to operate Imagify in this context. * * @since 1.9 * @since 1.9 The describer 'auto-optimize' is not used anymore. * @author Grégory Viguier * * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ public function get_capacity( $describer ); } Context/Noop.php 0000644 00000006454 15174671731 0007641 0 ustar 00 <?php namespace Imagify\Context; use Imagify\Traits\InstanceGetterTrait; /** * Fallback class for contexts. * * @since 1.9 * @author Grégory Viguier */ final class Noop implements ContextInterface { use InstanceGetterTrait; /** * Get the context "short name". * * @since 1.9 * @author Grégory Viguier * * @return string */ public function get_name() { return 'noop'; } /** * Tell if the context is network-wide. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function is_network_wide() { return false; } /** * Get the type of files this context allows. * * @since 1.9 * @see imagify_get_mime_types() * @author Grégory Viguier * * @return string Possible values are: * - 'all' to allow all types. * - 'image' to allow only images. * - 'not-image' to allow only pdf files. */ public function get_allowed_mime_types() { return 'all'; } /** * Get the thumbnail sizes for this context, except the full size. * * @since 1.9 * @author Grégory Viguier * * @return array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } */ public function get_thumbnail_sizes() { return []; } /** * Get images max width for this context. This is used when resizing. * 0 means to not resize. * * @since 1.9.8 * @author Grégory Viguier * * @return int */ public function get_resizing_threshold() { return 0; } /** * Tell if the optimization process is allowed resize in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_resize() { return false; } /** * Tell if the optimization process is allowed to backup in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_backup() { return false; } /** * Tell if the current user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function current_user_can( $describer, $media_id = null ) { return false; } /** * Tell if a user is allowed to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param int $user_id A user ID. * @param string $describer Capacity describer. See $this->get_capacity() for possible values. Can also be a "real" user capacity. * @param int $media_id A media ID. * @return bool */ public function user_can( $user_id, $describer, $media_id = null ) { return false; } /** * Get user capacity to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ public function get_capacity( $describer ) { return 'noop'; } } Context/CustomFolders.php 0000644 00000004246 15174671731 0011514 0 ustar 00 <?php namespace Imagify\Context; use Imagify\Traits\InstanceGetterTrait; /** * Context class used for the custom folders. * * @since 1.9 * @author Grégory Viguier */ final class CustomFolders extends AbstractContext { use InstanceGetterTrait; /** * Context "short name". * * @var string * @since 1.9 * @author Grégory Viguier */ protected $context = 'custom-folders'; /** * The thumbnail sizes for this context, except the full size. * * @var array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } * @since 1.9 * @author Grégory Viguier */ protected $thumbnail_sizes = []; /** * Get images max width for this context. This is used when resizing. * 0 means to not resize. * * @since 1.9.8 * @author Grégory Viguier * * @return int */ public function get_resizing_threshold() { return 0; } /** * Tell if the optimization process is allowed to backup in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_backup() { $this->can_backup = get_imagify_option( 'backup' ); return $this->can_backup; } /** * Get user capacity to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ public function get_capacity( $describer ) { switch ( $describer ) { case 'manage': $capacity = imagify_is_active_for_network() ? 'manage_network_options' : 'manage_options'; break; case 'bulk-optimize': case 'optimize': case 'restore': case 'manual-optimize': case 'manual-restore': case 'auto-optimize': $capacity = is_multisite() ? 'manage_network_options' : 'manage_options'; break; default: $capacity = $describer; } return $this->filter_capacity( $capacity, $describer ); } } Context/WP.php 0000644 00000006053 15174671731 0007247 0 ustar 00 <?php namespace Imagify\Context; use Imagify\Traits\InstanceGetterTrait; /** * Context class used for the WP media library. * * @since 1.9 * @author Grégory Viguier */ final class WP extends AbstractContext { use InstanceGetterTrait; /** * Context "short name". * * @var string * @since 1.9 * @author Grégory Viguier */ protected $context = 'wp'; /** * Images max width for this context. This is used when resizing. * * @var int * @since 1.9.8 * @author Grégory Viguier */ protected $resizing_threshold = 0; /** * Get the thumbnail sizes for this context, except the full size. * * @since 1.9 * @author Grégory Viguier * * @return array { * Data for the currently registered thumbnail sizes. * Size names are used as array keys. * * @type int $width The image width. * @type int $height The image height. * @type bool $crop True to crop, false to resize. * @type string $name The size name. * } */ public function get_thumbnail_sizes() { $this->thumbnail_sizes = get_imagify_thumbnail_sizes(); return $this->thumbnail_sizes; } /** * Get images max width for this context. This is used when resizing. * 0 means to not resize. * * @since 1.9.8 * @author Grégory Viguier * * @return int */ public function get_resizing_threshold() { if ( get_imagify_option( 'resize_larger' ) ) { $this->resizing_threshold = max( 0, get_imagify_option( 'resize_larger_w' ) ); } return $this->resizing_threshold; } /** * Tell if the optimization process is allowed to backup in this context. * * @since 1.9 * @author Grégory Viguier * * @return bool */ public function can_backup() { $this->can_backup = get_imagify_option( 'backup' ); return $this->can_backup; } /** * Get user capacity to operate Imagify in this context. * * @since 1.9 * @author Grégory Viguier * * @param string $describer Capacity describer. Possible values are like 'manage', 'bulk-optimize', 'manual-optimize', 'auto-optimize'. * @return string */ public function get_capacity( $describer ) { static $edit_attachment_cap; switch ( $describer ) { case 'manage': $capacity = imagify_is_active_for_network() ? 'manage_network_options' : 'manage_options'; break; case 'bulk-optimize': $capacity = 'manage_options'; break; case 'optimize': case 'restore': // This is a generic capacity: don't use it unless you have no other choices! if ( ! isset( $edit_attachment_cap ) ) { $edit_attachment_cap = get_post_type_object( 'attachment' ); $edit_attachment_cap = $edit_attachment_cap ? $edit_attachment_cap->cap->edit_posts : 'edit_posts'; } $capacity = $edit_attachment_cap; break; case 'manual-optimize': case 'manual-restore': // Must be used with an Attachment ID. $capacity = 'edit_post'; break; case 'auto-optimize': $capacity = 'upload_files'; break; default: $capacity = $describer; } return $this->filter_capacity( $capacity, $describer ); } }
| ver. 1.6 |
Github
|
.
| PHP 8.3.30 | Генерация страницы: 0.01 |
proxy
|
phpinfo
|
Настройка