rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -0,0 +1,81 @@
<?php
namespace Imagify\Admin;
use Imagify\Traits\InstanceGetterTrait;
use Imagify\User\User;
use Imagify_Views;
/**
* Admin bar handler
*/
class AdminBar {
use InstanceGetterTrait;
/**
* Launch the hooks.
*
* @return void
*/
public function init() {
if ( wp_doing_ajax() ) {
add_action( 'wp_ajax_imagify_get_admin_bar_profile', array( $this, 'get_admin_bar_profile_callback' ) );
}
}
/**
* 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();
}
$user = new User();
$views = Imagify_Views::get_instance();
$unconsumed_quota = $views->get_quota_percent();
$text = '';
$button_text = '';
$upgrade_link = '';
if ( $user->is_free() ) {
$text = esc_html__( 'Upgrade your plan now for more!', 'rocket' ) . '<br>' .
esc_html__( 'From $4.99/month only, keep going with image optimization!', 'rocket' );
$button_text = esc_html__( 'Upgrade My Plan', 'rocket' );
$upgrade_link = IMAGIFY_APP_DOMAIN . '/subscription/?utm_source=plugin&utm_medium=notification';
} elseif ( $user->is_growth() ) {
$text = esc_html__( 'Switch to Infinite plan for unlimited optimization:', 'rocket' ) . '<br>';
if ( $user->is_monthly ) {
$text .= esc_html__( 'For $9.99/month, optimize as many images as you like!', 'rocket' );
$upgrade_link = IMAGIFY_APP_DOMAIN . '/subscription/plan_switch/?label=infinite&payment_plan=1&utm_source=plugin&utm_medium=notification ';
} else {
$text .= esc_html__( 'For $99.9/year, optimize as many images as you like!', 'rocket' );
$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', 'rocket' );
}
$data = [
'quota_icon' => $views->get_quota_icon(),
'quota_class' => $views->get_quota_class(),
'plan_label' => $user->plan_label,
'plan_with_quota' => $user->is_free() || $user->is_growth(),
'unconsumed_quota' => $unconsumed_quota,
'user_quota' => $user->quota,
'next_update' => $user->next_date_update,
'text' => $text,
'button_text' => $button_text,
'upgrade_link' => $upgrade_link,
];
$template = $views->get_template( 'admin/admin-bar-status', $data );
wp_send_json_success( $template );
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Imagify\Auth;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Class that allows the use of Basic Auth for internal requests.
* If this doesnt work automatically, define the constants IMAGIFY_AUTH_USER and IMAGIFY_AUTH_PASSWORD.
*
* @since 1.9.5
* @author Grégory Viguier
*/
class Basic {
use \Imagify\Traits\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;
}
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( 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 ) || empty( $pass ) ) {
// 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;
}
}

View File

@@ -0,0 +1,117 @@
<?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 WebP versions.
*
* @since 1.9
*
* @return int The number of media.
*/
public function has_optimized_media_without_webp() {
return count( $this->get_optimized_media_ids_without_webp()['ids'] );
}
}

View File

@@ -0,0 +1,578 @@
<?php
namespace Imagify\Bulk;
use Exception;
use Imagify\Traits\InstanceGetterTrait;
/**
* Bulk optimization
*/
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_webp', [ $this, 'generate_webp_versions' ], 10, 2 );
add_action( 'imagify_convert_webp_finished', [ $this, 'clear_webp_transients' ], 10, 2 );
add_action( 'wp_ajax_imagify_bulk_optimize', [ $this, 'bulk_optimize_callback' ] );
add_action( 'wp_ajax_imagify_missing_webp_generation', [ $this, 'missing_webp_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' ] );
}
/**
* 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_webp_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();
$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 || ! is_numeric( $optimization_level ) ) {
$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 WebP generation
*
* @param array $contexts An array of contexts (WP/Custom folders).
*
* @return array
*/
public function run_generate_webp( array $contexts ) {
if ( ! $this->can_optimize() ) {
return [
'success' => false,
'message' => 'over-quota',
];
}
delete_transient( 'imagify_stat_without_webp' );
$medias = [];
foreach ( $contexts as $context ) {
$media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_webp();
if ( ! $media['ids'] && $media['errors']['no_backup'] ) {
// No backup, no WebP.
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_webp',
[
'id' => $media_id,
'context' => $context,
],
"imagify-{$context}-convert-webp"
);
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// nothing to do.
}
}
}
set_transient( 'imagify_missing_webp_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 int $class_name The class name.
* @param string $context The context name.
*/
$class_name = apply_filters( '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 medias 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 WebP 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_webp_versions( int $media_id, string $context ) {
if ( ! $this->can_optimize() ) {
return false;
}
return imagify_get_optimization_process( $media_id, $context )->generate_webp_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' ) {
$context = 'POST' === $method ? wp_unslash( $_POST[ $parameter ] ) : wp_unslash( $_GET[ $parameter ] ); //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
$context = htmlspecialchars( $context );
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;
}
/** ----------------------------------------------------------------------------------------- */
/** BULK OPTIMIZATION CALLBACKS ============================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* 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 WebP versions generation
*
* @return void
*/
public function missing_webp_callback() {
imagify_check_nonce( 'imagify-bulk-optimize' );
$contexts = explode( '_', sanitize_key( wp_unslash( $_GET['context'] ) ) );
foreach ( $contexts as $context ) {
if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) {
imagify_die();
}
}
$data = $this->run_generate_webp( $contexts );
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 ) ? array_filter( $folder_types, 'is_string' ) : [];
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 ) ) );
}
}

View File

@@ -0,0 +1,61 @@
<?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 files URL.
*/
public function get_unoptimized_media_ids( $optimization_level );
/**
* Get ids of all optimized media without WebP versions.
*
* @since 1.9
* @since 1.9.5 The method doesn't return the IDs directly anymore.
*
* @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_webp();
/**
* Tell if there are optimized media without WebP versions.
*
* @since 1.9
*
* @return int The number of media.
*/
public function has_optimized_media_without_webp();
/**
* 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();
}

View File

@@ -0,0 +1,178 @@
<?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 WebP versions.
*
* @since 1.9
* @since 1.9.5 The method doesn't return the IDs directly anymore.
*
* @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_webp() {
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' );
$mime_types = str_replace( ",'image/webp'", '', $mime_types );
$webp_suffix = constant( imagify_get_optimization_process_class_name( 'custom-folders' ) . '::WEBP_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( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%'
) );
$wpdb->flush();
unset( $mime_types, $files_table, $folders_table, $webp_suffix );
$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;
} // End foreach().
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 );
}
}

View File

@@ -0,0 +1,71 @@
<?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 files URL.
*/
public function get_unoptimized_media_ids( $optimization_level ) {
return [];
}
/**
* Get ids of all optimized media without WebP versions.
*
* @since 1.9
* @since 1.9.5 The method doesn't return the IDs directly anymore.
*
* @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_webp() {
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 );
}
}

View File

@@ -0,0 +1,303 @@
<?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;
} // End foreach().
return $data;
}
/**
* Get ids of all optimized media without WebP versions.
*
* @since 1.9
* @since 1.9.5 The method doesn't return the IDs directly anymore.
*
* @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_webp() {
global $wpdb;
$this->set_no_time_limit();
$mime_types = Imagify_DB::get_mime_types( 'image' );
$mime_types = str_replace( ",'image/webp'", '', $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,
] );
$webp_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_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_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( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%',
imagify_get_unoptimized_attachment_limit()
) );
$wpdb->flush();
unset( $mime_types, $statuses, $webp_suffix );
$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_webp_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;
} // End foreach().
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 );
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Imagify\CDN;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 = false );
/**
* 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 = false );
}

View File

@@ -0,0 +1,30 @@
<?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 [];
}
}

View File

@@ -0,0 +1,66 @@
<?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,
],
];
}
}

View File

@@ -0,0 +1,35 @@
<?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;
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Imagify\CLI;
use Imagify\Bulk\Bulk;
/**
* Command class for the missing WebP generation
*/
class GenerateMissingWebpCommand 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_webp( $arguments );
\WP_CLI::log( 'Imagify missing WebP generation triggered.' );
}
/**
* {@inheritdoc}
*/
protected function get_command_name(): string {
return 'generate-missing-webp';
}
/**
* {@inheritdoc}
*/
public function get_description(): string {
return 'Run the generation of the missing WebP versions';
}
/**
* {@inheritdoc}
*/
public function get_synopsis(): array {
return [
[
'type' => 'positional',
'name' => 'contexts',
'description' => 'The context(s) to run the missing WebP generation for. Possible values are wp and custom-folders.',
'optional' => false,
'repeating' => true,
],
];
}
}

View File

@@ -0,0 +1,262 @@
<?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;
/**
* 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( null, $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 ) {
$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 = (bool) apply_filters( '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
* @author Grégory Viguier
*
* @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 (bool) apply_filters( '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 (string) apply_filters( 'imagify_capacity', $capacity, $describer, $this->get_name() );
}
}

View File

@@ -0,0 +1,141 @@
<?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 );
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Imagify\Context;
use \Imagify\Traits\InstanceGetterTrait;
/**
* Context class used for the custom folders.
*
* @since 1.9
* @author Grégory Viguier
*/
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() {
if ( isset( $this->can_backup ) ) {
return $this->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 );
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Imagify\Context;
use \Imagify\Traits\InstanceGetterTrait;
/**
* Fallback class for contexts.
*
* @since 1.9
* @author Grégory Viguier
*/
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';
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Imagify\Context;
/**
* Context class used for the WP media library.
*
* @since 1.9
* @author Grégory Viguier
*/
class WP extends AbstractContext {
use \Imagify\Traits\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;
/**
* 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() {
if ( isset( $this->thumbnail_sizes ) ) {
return $this->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 ( isset( $this->resizing_threshold ) ) {
return $this->resizing_threshold;
}
if ( ! get_imagify_option( 'resize_larger' ) ) {
$this->resizing_threshold = 0;
} else {
$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() {
if ( isset( $this->can_backup ) ) {
return $this->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 );
}
}

View File

@@ -0,0 +1,84 @@
<?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();
}

View File

@@ -0,0 +1,371 @@
<?php
namespace Imagify\Imagifybeat;
use Imagify_Requirements;
use Imagify\Traits\InstanceGetterTrait;
use Imagify\Bulk\Bulk;
/**
* Imagifybeat actions.
*
* @since 1.9.3
*/
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_webp_total' );
if ( false === $total ) {
return $response;
}
$bulk = Bulk::get_instance();
foreach ( $data[ $imagifybeat_id ] as $context ) {
$media = $bulk->get_bulk_instance( $context )->get_optimized_media_ids_without_webp();
$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 (int) 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 methods 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;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Imagify\Imagifybeat;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Imagifybeat core.
*
* @since 1.9.3
* @author Grégory Viguier
*/
class Core {
use \Imagify\Traits\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( 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'] );
}
if ( 1 !== $nonce_state ) {
/**
* Filters the nonces to send.
*
* @since 1.9.3
* @author Grégory Viguier
*
* @param array $response The Imagifybeat response.
* @param array $data The $_POST data sent.
* @param string $screen_id The screen id.
*/
$response = apply_filters( '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
* @author Grégory Viguier
*
* @param array $response The Imagifybeat response.
* @param array $data The $_POST data sent.
* @param string $screen_id The screen id.
*/
$response = apply_filters( 'imagifybeat_received', $response, $data, $screen_id );
}
/**
* Filters the Imagifybeat response sent.
*
* @since 1.9.3
* @author Grégory Viguier
*
* @param array $response The Imagifybeat response.
* @param string $screen_id The screen id.
*/
$response = apply_filters( '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
* @author Grégory Viguier
*
* @param array $settings Imagifybeat settings array.
*/
return (array) apply_filters( 'imagifybeat_settings', $settings );
}
}

View File

@@ -0,0 +1,373 @@
<?php
namespace Imagify\Job;
use Imagify\Optimization\Process\ProcessInterface;
use Imagify\Traits\InstanceGetterTrait;
use WP_Error;
/**
* Job class for media optimization.
*
* @since 1.9
*/
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 youre 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, dont 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 );
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 );
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 WebP versions only.
$item['sizes'] = array_filter( $item['sizes'], [ $this->optimization_process, 'is_size_webp' ] );
} 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 );
}
}

View File

@@ -0,0 +1,552 @@
<?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 ContextInterface
* @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 medias 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 );
}
}

View File

@@ -0,0 +1,358 @@
<?php
namespace Imagify\Media;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Media class for the custom folders.
*
* @since 1.9
* @author Grégory Viguier
*/
class CustomFolders extends AbstractMedia {
use \Imagify\Traits\MediaRowTrait;
use \Imagify\Deprecated\Traits\Media\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 medias 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 medias 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 plugins 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 );
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace Imagify\Media;
use Imagify\CDN\PushCDNInterface;
use Imagify\Context\ContextInterface;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 medias 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 medias 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 medias 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();
}

View File

@@ -0,0 +1,390 @@
<?php
namespace Imagify\Media;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Fallback class for "media groups" (aka attachments).
*
* @since 1.9
* @author Grégory Viguier
*/
class Noop implements MediaInterface {
use \Imagify\Deprecated\Traits\Media\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 medias 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 medias 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 medias 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;
}
}

View File

@@ -0,0 +1,430 @@
<?php
namespace Imagify\Media;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 were playing in WP 5.3s garden.
*
* @var bool
* @since 1.9.8
* @access protected
* @author Grégory Viguier
*/
protected $is_wp53;
/**
* 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 medias 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 medias 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';
}
// 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 plugins 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 were playing in WP 5.3s garden.
*
* @since 1.9.8
* @access protected
* @author Grégory Viguier
*
* @return bool
*/
protected function is_wp_53() {
if ( isset( $this->is_wp53 ) ) {
return $this->is_wp53;
}
$this->is_wp53 = function_exists( 'wp_get_original_image_path' );
return $this->is_wp53;
}
}

View File

@@ -0,0 +1,909 @@
<?php
declare(strict_types=1);
namespace Imagify\Notices;
use Imagify\Traits\InstanceGetterTrait;
use Imagify\User\User;
/**
* Class that handles the admin notices.
*
* @since 1.6.10
*/
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'] ) ? esc_html( wp_unslash( $_GET['notice'] ) ) : false;
$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 int $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 = esc_html( 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 An array of plugins to deactivate.
*/
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) apply_filters( '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 string $plugins List of recommended plugins to deactivate.
*/
$plugins = apply_filters( 'imagify_plugins_to_deactivate', self::$conflicting_plugins );
return array_filter( $plugins, 'is_plugin_active' );
}
}

View File

@@ -0,0 +1,452 @@
<?php
namespace Imagify\Optimization\Data;
use Imagify\Media\MediaInterface;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 WebP 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_webp Use the WebP size if available.
* @return string|int
*/
public function get_optimized_size( $human_format = true, $decimals = 2, $use_webp = true ) {
if ( ! $this->is_valid() ) {
return $human_format ? imagify_size_format( 0, $decimals ) : 0;
}
$data = $this->get_optimization_data();
$media = $this->get_media();
if ( $use_webp ) {
$process_class_name = imagify_get_optimization_process_class_name( $media->get_context() );
$webp_size_name = 'full' . constant( $process_class_name . '::WEBP_SUFFIX' );
}
if ( $use_webp && ! empty( $data['sizes'][ $webp_size_name ]['optimized_size'] ) ) {
$size = (int) $data['sizes'][ $webp_size_name ]['optimized_size'];
} elseif ( ! empty( $data['sizes']['full']['optimized_size'] ) ) {
$size = (int) $data['sizes']['full']['optimized_size'];
} else {
$size = 0;
}
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_webp && ! empty( $data['sizes'][ $webp_size_name ]['success'] ) ) {
// Try with the WebP file first.
$filepath = $media->get_raw_fullsize_path();
$filepath = $filepath ? imagify_path_to_webp( $filepath ) : false;
if ( ! $filepath || ! $this->filesystem->exists( $filepath ) ) {
$filepath = false;
}
}
if ( ! $filepath ) {
// No WebP? 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() );
$webp_size_name = 'full' . constant( $process_class_name . '::WEBP_SUFFIX' );
$percent = $this->get_size_data( $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 );
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Imagify\Optimization\Data;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 \Imagify\Traits\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;
}
// This is required by MediaRowTrait.
$this->id = $this->get_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 = $this->get_media()->get_row();
}
/**
* 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;
}
}

View File

@@ -0,0 +1,275 @@
<?php
namespace Imagify\Optimization\Data;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 files 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 WebP 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_webp Use the WebP size if available.
* @return string|int
*/
public function get_optimized_size( $human_format = true, $decimals = 2, $use_webp = 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();
}

View File

@@ -0,0 +1,285 @@
<?php
namespace Imagify\Optimization\Data;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 WebP 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_webp Use the WebP size if available.
* @return string|int
*/
public function get_optimized_size( $human_format = true, $decimals = 2, $use_webp = 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 );
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Imagify\Optimization\Data;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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 );
}
}

View File

@@ -0,0 +1,861 @@
<?php
namespace Imagify\Optimization;
use Imagify_Requirements;
/**
* A generic optimization class focussed 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 plugins 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' ), array(
'file_path' => $this->filesystem->make_path_relative( $path ),
'backup_path' => $this->filesystem->make_path_relative( $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.
* @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'];
}
$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'] = '';
}
if ( 'webp' === $args['convert'] ) {
$destination_path = $this->get_path_to_webp();
$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
* @author Grégory Viguier
*
* @return string|null
*/
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 );
}
/**
* 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 );
}
/**
* 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 plugins 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 ];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
<?php
namespace Imagify\Optimization\Process;
use Imagify\Optimization\File;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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->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' ) );
}
if ( ! $this->get_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' ) );
}
}

View File

@@ -0,0 +1,413 @@
<?php
namespace Imagify\Optimization\Process;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Fallback class to optimize medias.
*
* @since 1.9
* @author Grégory Viguier
*/
class Noop implements ProcessInterface {
/**
* The suffix used in the thumbnail size name.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const WEBP_SUFFIX = '@imagify-webp';
/**
* The suffix used in file name to create a temporary copy of the full size.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
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
* @author Grégory Viguier
*/
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
* @access public
* @author Grégory Viguier
*
* @param mixed $id Whatever.
* @return bool
*/
public static function constructor_accepts( $id ) {
return false;
}
/**
* Get the data instance.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return DataInterface|false
*/
public function get_data() {
return false;
}
/**
* Get the media instance.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return MediaInterface|false
*/
public function get_media() {
return false;
}
/**
* Get the File instance.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return File|false
*/
public function get_file() {
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;
}
/**
* Tell if the current user is allowed to operate Imagify in this context.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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;
}
/** ----------------------------------------------------------------------------------------- */
/** OPTIMIZATION ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Optimize a media files by pushing tasks into the queue.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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' ) );
}
/** ----------------------------------------------------------------------------------------- */
/** 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() {
return [];
}
/**
* 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() {
return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
}
/** ----------------------------------------------------------------------------------------- */
/** BACKUP FILE ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Delete the backup file.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function delete_backup() {}
/** ----------------------------------------------------------------------------------------- */
/** RESIZE FILE ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Maybe resize an image.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @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,
];
}
/** ----------------------------------------------------------------------------------------- */
/** WEBP ==================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Generate WebP images if they are missing.
*
* @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 generate_webp_versions() {
return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
}
/**
* Delete the WebP images.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function delete_webp_files() {}
/**
* Tell if a thumbnail size is an "Imagify WebP" size.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @param string $size_name The size name.
* @return string|bool The unsuffixed name of the size if WebP. False if not WebP.
*/
public function is_size_webp( $size_name ) {
return false;
}
/** ----------------------------------------------------------------------------------------- */
/** PROCESS STATUS ========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if a process is running for this media.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_locked() {
return false;
}
/**
* Set the running status to "running" for a period of time.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function lock() {}
/**
* Delete the running status.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function unlock() {}
/** ----------------------------------------------------------------------------------------- */
/** DATA ==================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if a size already has optimization data.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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,
];
}
}

View File

@@ -0,0 +1,369 @@
<?php
namespace Imagify\Optimization\Process;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Interface to use to optimize medias.
*
* @since 1.9
* @author Grégory Viguier
*/
interface ProcessInterface {
/**
* 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 data instance.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return DataInterface|false
*/
public function get_data();
/**
* Get the media instance.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return MediaInterface|false
*/
public function get_media();
/**
* Get the File instance of the original file.
*
* @since 1.9.8
* @access public
* @author Grégory Viguier
*
* @return File|false
*/
public function get_original_file();
/**
* Get the File instance of the full size file.
*
* @since 1.9.8
* @access public
* @author Grégory Viguier
*
* @return File|false
*/
public function get_fullsize_file();
/**
* Tell if the current media is valid.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_valid();
/**
* Tell if the current user is allowed to operate Imagify in this context.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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 );
/** ----------------------------------------------------------------------------------------- */
/** OPTIMIZATION ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Optimize a media files by pushing tasks into the queue.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @return bool|WP_Error True on success. A \WP_Error instance on failure.
*/
public function restore();
/** ----------------------------------------------------------------------------------------- */
/** 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();
/**
* 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();
/** ----------------------------------------------------------------------------------------- */
/** BACKUP FILE ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Delete the backup file.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function delete_backup();
/** ----------------------------------------------------------------------------------------- */
/** RESIZE FILE ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Maybe resize an image.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @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 );
/** ----------------------------------------------------------------------------------------- */
/** WEBP ==================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Generate WebP images if they are missing.
*
* @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 generate_webp_versions();
/**
* Delete the WebP images.
* This doesn't delete the related optimization data.
*
* @since 1.9
* @since 1.9.6 Return WP_Error or true.
* @access public
* @author Grégory Viguier
*
* @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_webp_files( $keep_full = false );
/**
* Tell if a thumbnail size is an "Imagify WebP" size.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @param string $size_name The size name.
* @return string|bool The unsuffixed name of the size if WebP. False if not WebP.
*/
public function is_size_webp( $size_name );
/**
* Tell if the media has all WebP versions.
*
* @return bool
*/
public function is_full_webp();
/**
* Tell if the media has WebP versions.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function has_webp();
/** ----------------------------------------------------------------------------------------- */
/** PROCESS STATUS ========================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if a process is running for this media.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_locked();
/**
* Set the running status to "running" for a period of time.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function lock();
/**
* Delete the running status.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function unlock();
/** ----------------------------------------------------------------------------------------- */
/** DATA ==================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Tell if a size already has optimization data.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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 );
}

View File

@@ -0,0 +1,255 @@
<?php
namespace Imagify\Optimization\Process;
use Imagify\Optimization\File;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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->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' ) );
}
if ( ! $this->get_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();
$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 ) ) {
// 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;
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Imagify;
use Imagify\Bulk\Bulk;
use Imagify\CLI\BulkOptimizeCommand;
use Imagify\CLI\GenerateMissingWebpCommand;
use Imagify\Notices\Notices;
use Imagify\Admin\AdminBar;
/**
* Main plugin class.
*/
class Plugin {
/**
* Absolute path to the plugin (with trailing slash).
*
* @var string
*/
private $plugin_path;
/**
* Instantiate the class.
*
* @since 1.9
*
* @param array $plugin_args {
* An array of arguments.
*
* @type string $plugin_path Absolute path to the plugin (with trailing slash).
* }
*/
public function __construct( $plugin_args ) {
$this->plugin_path = $plugin_args['plugin_path'];
}
/**
* Plugin init.
*
* @since 1.9
*/
public function init() {
$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();
\Imagify\Stats\OptimizedMediaWithoutWebp::get_instance()->init();
Bulk::get_instance()->init();
AdminBar::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();
}
\Imagify\Webp\Display::get_instance()->init();
add_action( 'init', [ $this, 'maybe_activate' ] );
// Load plugin translations.
imagify_load_translations();
imagify_add_command( new BulkOptimizeCommand() );
imagify_add_command( new GenerateMissingWebpCommand() );
/**
* 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 );
}
/**
* 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/admin-bar.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 );
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Imagify\Stats;
use Imagify\Bulk\Bulk;
use Imagify\Traits\InstanceGetterTrait;
/**
* Class to get and cache the number of optimized media without WebP versions.
*
* @since 1.9
*/
class OptimizedMediaWithoutWebp implements StatInterface {
use InstanceGetterTrait;
/**
* Name of the transient storing the cached result.
*
* @var string
*/
const NAME = 'imagify_stat_without_webp';
/**
* Launch hooks.
*
* @since 1.9
*/
public function init() {
add_action( 'imagify_after_optimize', [ $this, 'maybe_clear_cache_after_optimization' ], 10, 2 );
add_action( 'imagify_after_restore_media', [ $this, 'maybe_clear_cache_after_restoration' ], 10, 4 );
add_action( 'imagify_delete_media', [ $this, 'maybe_clear_cache_on_deletion' ] );
}
/** ----------------------------------------------------------------------------------------- */
/** GET/CACHE THE STAT ====================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the number of optimized media without WebP versions.
*
* @since 1.9
*
* @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_webp();
}
return $stat;
}
/**
* Get and cache the number of optimized media without WebP versions.
*
* @since 1.9
*
* @return int
*/
public function get_cached_stat() {
$contexts = implode( '|', imagify_get_context_names() );
$stat = get_transient( static::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( static::NAME, $stat, 2 * DAY_IN_SECONDS );
return $stat['stat'];
}
/**
* Clear the stat cache.
*
* @since 1.9
*/
public function clear_cache() {
delete_transient( static::NAME );
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Clear cache after optimizing a media.
*
* @since 1.9
*
* @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( static::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 ( ! isset( $new_sizes['full'] ) && ! empty( $new_sizes[ $size_name ]['success'] ) ) {
/**
* We just successfully generated the WebP 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 WebP version to a media that was previously optimized, so there is one less optimized media without WebP.
*/
$this->clear_cache();
return;
}
if ( ! empty( $new_sizes['full']['success'] ) && empty( $new_sizes[ $size_name ]['success'] ) ) {
/**
* We now have a new optimized media without WebP.
*/
$this->clear_cache();
}
}
/**
* Clear cache after restoring a media.
*
* @since 1.9
*
* @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( static::NAME ) ) {
return;
}
$sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : [];
$size_name = 'full' . $process::WEBP_SUFFIX;
if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) {
/**
* This media had no WebP versions.
*/
$this->clear_cache();
}
}
/**
* Clear cache on media deletion.
*
* @since 1.9
*
* @param ProcessInterface $process An optimization process.
*/
public function maybe_clear_cache_on_deletion( $process ) {
if ( false === get_transient( static::NAME ) ) {
return;
}
$data = $process->get_data()->get_optimization_data();
$sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : [];
$size_name = 'full' . $process::WEBP_SUFFIX;
if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) {
/**
* This media had no WebP versions.
*/
$this->clear_cache();
}
}
}

View File

@@ -0,0 +1,44 @@
<?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();
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Imagify\Traits;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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;
/**
* 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;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Imagify\Traits;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* 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
* @author Grégory Viguier
*/
trait MediaRowTrait {
/**
* The media SQL data row.
*
* @var array
* @since 1.9
* @access protected
* @author Grégory Viguier
*/
protected $row;
/**
* The media ID.
*
* @var int
* @since 1.9
* @access protected
* @author Grégory Viguier
*/
protected $id;
/**
* Get the row.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*/
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
* @access public
* @author Grégory Viguier
*
* @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
* @access public
* @author Grégory Viguier
*
* @return array The row.
*/
public function invalidate_row() {
$this->row = [];
return $this->row;
}
/**
* Reset the row cache.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @return null The row.
*/
public function reset_row_cache() {
$this->row = null;
return $this->row;
}
}

View File

@@ -0,0 +1,292 @@
<?php
namespace Imagify\User;
use Date;
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 Date
*/
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;
/**
* The constructor.
*
* @since 1.0
*
* @return void
*/
public function __construct() {
$user = get_imagify_user();
if ( is_wp_error( $user ) ) {
$this->error = $user;
return;
}
$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;
$this->error = false;
}
/**
* 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() {
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() {
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() {
return 1 === $this->plan_id;
}
/**
* Check if the user is a growth account
*
* @return bool
*/
public function is_growth() {
return ( 16 === $this->plan_id || 18 === $this->plan_id );
}
/**
* Check if the user is an infinite account
*
* @return bool
*/
public function is_infinite() {
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() )
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Imagify\Webp;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Add and remove contents to the .htaccess file to display WebP images on the site.
*
* @since 1.9
* @author Grégory Viguier
*/
class Apache extends \Imagify\WriteFile\AbstractApacheDirConfFile {
/**
* Name of the tag used as block delemiter.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const TAG_NAME = 'Imagify: webp file type';
/**
* Get unfiltered new contents to write into the file.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @return string
*/
protected function get_raw_new_contents() {
return trim( '
<IfModule mod_mime.c>
AddType image/webp .webp
</IfModule>' );
}
}

View File

@@ -0,0 +1,239 @@
<?php
namespace Imagify\Webp;
use Imagify\Notices\Notices;
use Imagify\Traits\InstanceGetterTrait;
/**
* Display WebP images on the site.
*
* @since 1.9
*/
class Display {
use InstanceGetterTrait;
/**
* Server conf object.
*
* @var \Imagify\WriteFile\WriteFileInterface
* @since 1.9
*/
protected $server_conf;
/**
* Init.
*
* @since 1.9
*/
public function init() {
add_filter( 'imagify_settings_on_save', [ $this, 'maybe_add_rewrite_rules' ] );
add_action( 'imagify_settings_webp_info', [ $this, 'maybe_add_webp_info' ] );
add_action( 'imagify_activation', [ $this, 'activate' ] );
add_action( 'imagify_deactivation', [ $this, 'deactivate' ] );
Picture\Display::get_instance()->init();
RewriteRules\Display::get_instance()->init();
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* 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 ) {
$old_value = (bool) get_imagify_option( 'display_webp' );
// See \Imagify_Options->validate_values_on_update() for why we use 'convert_to_webp' here.
$new_value = ! empty( $values['display_webp'] ) && ! empty( $values['convert_to_webp'] );
if ( $old_value === $new_value ) {
// No changes.
return $values;
}
if ( ! $this->get_server_conf() ) {
return $values;
}
if ( $new_value ) {
// Add the WebP file type.
$result = $this->get_server_conf()->add();
} else {
// 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() );
} else {
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>'
);
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_webp' ) ) {
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_webp' ) ) {
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();
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the path to the directory conf file.
*
* @since 1.9
*
* @param bool $relative True to get a path relative to the sites 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 WebP display method by validating the given value.
*
* @since 1.9
*
* @param array $values The option values.
* @return string 'picture' or 'rewrite'.
*/
public function get_display_webp_method( $values ) {
$options = \Imagify_Options::get_instance();
$default = $options->get_default_values();
$default = $default['display_webp_method'];
$method = ! empty( $values['display_webp_method'] ) ? $values['display_webp_method'] : '';
return $options->sanitize_and_validate( 'display_webp_method', $method, $default );
}
/**
* Get the server conf instance.
* Note: nothing needed for nginx.
*
* @since 1.9
*
* @return \Imagify\WriteFile\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();
} else {
$this->server_conf = false;
}
return $this->server_conf;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Imagify\Webp;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Add and remove contents to the web.config file to display WebP images on the site.
*
* @since 1.9
* @author Grégory Viguier
*/
class IIS extends \Imagify\WriteFile\AbstractIISDirConfFile {
/**
* Name of the tag used as block delemiter.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const TAG_NAME = 'Imagify: webp file type';
/**
* Get unfiltered new contents to write into the file.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @return array
*/
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>' );
}
}

View File

@@ -0,0 +1,800 @@
<?php
namespace Imagify\Webp\Picture;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Display WebP images on the site with <picture> tags.
*
* @since 1.9
* @author Grégory Viguier
*/
class Display {
use \Imagify\Traits\InstanceGetterTrait;
/**
* Option value.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const OPTION_VALUE = 'picture';
/**
* 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();
}
/**
* Init.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function init() {
add_action( 'template_redirect', [ $this, 'start_content_process' ], -1000 );
add_filter( 'imagify_process_webp_content', [ $this, 'process_content' ] );
}
/** ----------------------------------------------------------------------------------------- */
/** ADD <PICTURE> TAGS TO THE PAGE ========================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Start buffering the page content.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*/
public function start_content_process() {
if ( ! get_imagify_option( 'display_webp' ) ) {
return;
}
if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) {
return;
}
/**
* Prevent the replacement of <img> tags into <picture> tags.
*
* @since 1.9
* @author Grégory Viguier
*
* @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_webp', true );
if ( ! $allow ) {
return;
}
ob_start( [ $this, 'maybe_process_buffer' ] );
}
/**
* Maybe process the page content.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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
* @author Grégory Viguier
*
* @param string $buffer The page content.
*/
$buffer = (string) apply_filters( 'imagify_buffer', $buffer );
return $buffer;
}
/**
* Process the content.
*
* @since 1.9
* @access public
* @author Grégory Viguier
*
* @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()
* @access protected
* @author Grégory Viguier
*
* @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
* @author Grégory Viguier
*
* @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
* @author Grégory Viguier
*
* @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()
* @access protected
* @author Grégory Viguier
*
* @param array $image An array of data.
* @return string A <source> tag.
*/
protected function build_source_tag( $image ) {
$srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set';
$attributes = [
'type' => 'image/webp',
$srcset_source => [],
];
if ( ! empty( $image['srcset'] ) ) {
foreach ( $image['srcset'] as $srcset ) {
if ( empty( $srcset['webp_url'] ) ) {
continue;
}
$attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor'];
}
}
if ( empty( $attributes[ $srcset_source ] ) ) {
$attributes[ $srcset_source ][] = $image['src']['webp_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
* @author Grégory Viguier
*
* @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 '<source' . $this->build_attributes( $attributes ) . "/>\n";
}
/**
* Build the <img> tag to insert in the <picture>.
*
* @since 1.9
* @see $this->process_image()
* @access protected
* @author Grégory Viguier
*
* @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
* @author Grégory Viguier
*
* @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
* @access protected
* @author Grégory Viguier
*
* @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
* @access protected
* @author Grégory Viguier
*
* @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()
* @author Grégory Viguier
*
* @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'] ) ) {
unset( $images[ $i ] );
continue;
}
unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_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'] ) ) {
unset( $images[ $i ]['srcset'][ $j ]['webp_url'] );
}
unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] );
}
}
return $images;
}
/**
* Process an image tag and get an array containing some data.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @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;
}
$webp_url = imagify_path_to_webp( $src['src'] );
$webp_path = $this->url_to_path( $webp_url );
$webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
$data = [
'tag' => $image,
'attributes' => $attributes,
'src_attribute' => $src_source,
'src' => [
'url' => $attributes[ $src_source ],
'webp_url' => $webp_url,
'webp_path' => $webp_path,
'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
],
'srcset_attribute' => false,
'srcset' => [],
];
// 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 ) {
$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;
}
$webp_url = imagify_path_to_webp( $src['src'] );
$webp_path = $this->url_to_path( $webp_url );
$webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
$data['srcset'][] = [
'url' => $srcs[0],
'descriptor' => $srcs[1],
'webp_url' => $webp_url,
'webp_path' => $webp_path,
'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
];
}
}
/**
* Filter a processed image tag.
*
* @since 1.9
* @author Grégory Viguier
*
* @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;
}
/**
* Tell if a content is HTML.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @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
* @access protected
* @author Grégory Viguier
*
* @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 = $this->get_cdn_source();
$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;
}
/**
* Get the CDN "source".
*
* @since 1.9.3
* @access public
* @author Grégory Viguier
*
* @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
* @author Grégory Viguier
*
* @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 = apply_filters( '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
* @access public
* @author Grégory Viguier
*
* @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 );
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Imagify\Webp\RewriteRules;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Add and remove rewrite rules to the .htaccess file to display WebP images on the site.
*
* @since 1.9
* @author Grégory Viguier
*/
class Apache extends \Imagify\WriteFile\AbstractApacheDirConfFile {
/**
* Name of the tag used as block delemiter.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const TAG_NAME = 'Imagify: rewrite rules for webp';
/**
* Get unfiltered new contents to write into the file.
*
* @since 1.9
* @access protected
* @source https://github.com/vincentorback/WebP-images-with-htaccess
* @author Grégory Viguier
*
* @return string
*/
protected function get_raw_new_contents() {
$extensions = $this->get_extensions_pattern();
$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>' );
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace Imagify\Webp\RewriteRules;
use Imagify\Notices\Notices;
use Imagify\Traits\InstanceGetterTrait;
use Imagify\WriteFile\AbstractWriteDirConfFile;
/**
* Display WebP images on the site with rewrite rules.
*
* @since 1.9
*/
class Display {
use InstanceGetterTrait;
/**
* Configuration file writer.
*
* @var AbstractWriteDirConfFile
*/
protected $server_conf;
/**
* Option value.
*
* @var string
* @since 1.9
*/
const OPTION_VALUE = 'rewrite';
/**
* Init.
*
* @since 1.9
*/
public function init() {
add_filter( 'imagify_settings_on_save', [ $this, 'maybe_add_rewrite_rules' ] );
add_action( 'imagify_settings_webp_info', [ $this, 'maybe_add_webp_info' ] );
add_action( 'imagify_activation', [ $this, 'activate' ] );
add_action( 'imagify_deactivation', [ $this, 'deactivate' ] );
}
/** ----------------------------------------------------------------------------------------- */
/** HOOKS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* 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 ) {
global $is_apache, $is_iis7, $is_nginx;
// Display WebP?
$was_enabled = (bool) get_imagify_option( 'display_webp' );
// See \Imagify_Options->validate_values_on_update() for why we use 'convert_to_webp' here.
$is_enabled = ! empty( $values['display_webp'] ) && ! empty( $values['convert_to_webp'] );
// Which method?
$old_value = get_imagify_option( 'display_webp_method' );
$new_value = ! empty( $values['display_webp_method'] ) ? $values['display_webp_method'] : '';
// Decide when to add or remove rules.
$is_rewrite = self::OPTION_VALUE === $new_value;
$was_rewrite = self::OPTION_VALUE === $old_value;
$add_or_remove = false;
if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) {
// Display WebP & use rewrite method, but only if one of the values changed: add rules.
$add_or_remove = 'add';
} elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) {
// Was displaying WebP & was using rewrite method, but only if one of the values changed: remove rules.
$add_or_remove = 'remove';
} else {
return $values;
}
if ( $is_apache ) {
$rules = new Apache();
} elseif ( $is_iis7 ) {
$rules = new IIS();
} elseif ( $is_nginx ) {
$rules = new Nginx();
} else {
return $values;
}
if ( 'add' === $add_or_remove ) {
// Add the rewrite rules.
$result = $rules->add();
} else {
// Remove the rewrite rules.
$result = $rules->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() );
} else {
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>'
);
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 servers configuration file (then restart the server).', 'imagify' ),
'<code>' . $this->get_file_path( true ) . '</code>'
);
}
}
/**
* Add rules on plugin activation.
*
* @since 1.9
*/
public function activate() {
$conf = $this->get_server_conf();
if ( ! $conf ) {
return;
}
if ( ! get_imagify_option( 'display_webp' ) ) {
return;
}
if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_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_webp' ) ) {
return;
}
if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_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();
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the path to the directory conf file.
*
* @since 1.9
*
* @param bool $relative True to get a path relative to the sites 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 \Imagify\WriteFile\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();
} else {
$this->server_conf = false;
}
return $this->server_conf;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Imagify\Webp\RewriteRules;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Add and remove rewrite rules to the web.config file to display WebP images on the site.
*
* @since 1.9
* @author Grégory Viguier
*/
class IIS extends \Imagify\WriteFile\AbstractIISDirConfFile {
/**
* Name of the tag used as block delemiter.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const TAG_NAME = 'Imagify: rewrite rules for webp';
/**
* Get unfiltered new contents to write into the file.
*
* @since 1.9
* @access protected
* @source https://github.com/igrigorik/webp-detect/blob/master/iis.config
* @author Grégory Viguier
*
* @return string
*/
protected function get_raw_new_contents() {
$extensions = $this->get_extensions_pattern();
$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>' );
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Imagify\Webp\RewriteRules;
defined( 'ABSPATH' ) || die( 'Cheatin uh?' );
/**
* Add and remove rewrite rules to the imagify.conf file to display WebP images on the site.
*
* @since 1.9
* @author Grégory Viguier
*/
class Nginx extends \Imagify\WriteFile\AbstractNginxDirConfFile {
/**
* Name of the tag used as block delemiter.
*
* @var string
* @since 1.9
* @author Grégory Viguier
*/
const TAG_NAME = 'Imagify: rewrite rules for webp';
/**
* Get unfiltered new contents to write into the file.
*
* @since 1.9
* @access protected
* @author Grégory Viguier
*
* @return string
*/
protected function get_raw_new_contents() {
$extensions = $this->get_extensions_pattern();
$home_root = wp_parse_url( home_url( '/' ) );
$home_root = $home_root['path'];
return trim( '
location ~* ^(' . $home_root . '.+)\.(' . $extensions . ')$ {
add_header Vary Accept;
if ($http_accept ~* "webp"){
set $imwebp A;
}
if (-f $request_filename.webp) {
set $imwebp "${imwebp}B";
}
if ($imwebp = AB) {
rewrite ^(.*) $1.webp;
}
}' );
}
}

View File

@@ -0,0 +1,77 @@
<?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';
}
}

View File

@@ -0,0 +1,283 @@
<?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. */
__( '%ss 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;
}
}

View File

@@ -0,0 +1,77 @@
<?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';
}
}

View File

@@ -0,0 +1,395 @@
<?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. */
__( '%ss 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 );
}
}

View File

@@ -0,0 +1,68 @@
<?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();
}