*/ public static function default_settings() { return array( 'max_dimension' => 2000, 'webp_quality' => 80, 'delete_original' => 1, 'max_file_size_mb' => 25, 'max_megapixels' => 50, 'limit_file_size' => 0, 'limit_megapixels' => 0, ); } /** * Set defaults without overwriting an existing site-specific policy. */ public static function activate() { if ( self::legacy_is_active() ) { wp_die( esc_html__( 'Deactivate Image Upload Optimizer before activating UploadAlly. UploadAlly will copy your saved settings.', 'uploadally' ) ); } add_option( self::OPTION_NAME, self::get_settings(), '', false ); } /** WordPress treats the two ZIP paths as separate plugins. */ public static function legacy_is_active() { return class_exists( 'DBD_Image_Upload_Optimizer', false ) || in_array( self::LEGACY_PLUGIN, (array) get_option( 'active_plugins', array() ), true ) || ( is_multisite() && isset( get_site_option( 'active_sitewide_plugins', array() )[ self::LEGACY_PLUGIN ] ) ); } /** Retain the site-specific callbacks already installed for reference discovery. */ public static function filter_references( $suffix, $value, ...$args ) { $value = apply_filters( 'dbd_image_upload_optimizer_' . $suffix, $value, ...$args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy site callbacks retain their original hook names. return apply_filters( 'uploadally_' . $suffix, $value, ...$args ); } /** Either generation of the filter can protect an attachment. */ public static function protects_attachment( $id ) { return apply_filters( 'dbd_image_upload_optimizer_protect_attachment', false, $id ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Preserve existing attachment protection callbacks. || apply_filters( 'uploadally_protect_attachment', false, $id ); } /** * Return validated settings merged with defaults. * * @return array */ public static function get_settings() { $settings = get_option( self::OPTION_NAME, false ); if ( false === $settings ) { $legacy = get_option( self::LEGACY_OPTION, false ); if ( is_array( $legacy ) ) { // Also copy settings the first time each subsite uses a network install. add_option( self::OPTION_NAME, $legacy, '', false ); $settings = get_option( self::OPTION_NAME, $legacy ); } } return wp_parse_args( is_array( $settings ) ? $settings : array(), self::default_settings() ); } /** * Check whether the active WordPress image editor can write WebP files. * * @return bool */ private function supports_webp() { return function_exists( 'wp_image_editor_supports' ) && wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ); } /** * Apply the configured quality only to WebP output. * * @param int $quality Current quality. * @param string $mime_type Output MIME type. * @param array $size Output dimensions. * @return int */ public function filter_webp_quality( $quality, $mime_type, $size = array() ) { unset( $size ); if ( 'image/webp' === $mime_type ) { $settings = self::get_settings(); return (int) $settings['webp_quality']; } return $quality; } /** * Reject image uploads that are unsafe to process on typical hosting. * * @param array $file WordPress upload data. * @return array */ public function enforce_upload_limits( $file ) { if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) ) { return $file; } $settings = self::get_settings(); if ( empty( $settings['limit_file_size'] ) && empty( $settings['limit_megapixels'] ) ) { return $file; } $detected_mime = wp_get_image_mime( $file['tmp_name'] ); $declared_mime = isset( $file['type'] ) ? (string) $file['type'] : ''; $is_image = is_string( $detected_mime ) && str_starts_with( $detected_mime, 'image/' ); if ( ! $is_image && str_starts_with( $declared_mime, 'image/' ) ) { $is_image = true; } if ( ! $is_image ) { return $file; } $max_file_size = (int) $settings['max_file_size_mb'] * MB_IN_BYTES; $file_size = isset( $file['size'] ) ? (int) $file['size'] : 0; if ( ! $file_size && is_readable( $file['tmp_name'] ) ) { $file_size = (int) filesize( $file['tmp_name'] ); } if ( ! empty( $settings['limit_file_size'] ) && $file_size > $max_file_size ) { $file['error'] = sprintf( /* translators: 1: Actual file size. 2: Maximum file size. */ __( 'This image is %1$s. The maximum image upload size is %2$s. Please resize it before uploading.', 'uploadally' ), size_format( $file_size ), size_format( $max_file_size ) ); return $file; } if ( empty( $settings['limit_megapixels'] ) ) { return $file; } $image_size = wp_getimagesize( $file['tmp_name'] ); if ( is_array( $image_size ) && isset( $image_size[0], $image_size[1] ) ) { $pixels = (int) $image_size[0] * (int) $image_size[1]; $max_pixels = (int) $settings['max_megapixels'] * 1000000; if ( $pixels > $max_pixels ) { $file['error'] = sprintf( /* translators: 1: Actual megapixels. 2: Maximum megapixels. */ __( 'This image is %1$s megapixels. The maximum image resolution is %2$s megapixels. Please resize it before uploading.', 'uploadally' ), number_format_i18n( $pixels / 1000000, 1 ), number_format_i18n( (int) $settings['max_megapixels'] ) ); } } return $file; } /** * Register the policy for the plugin settings screen. */ public function register_media_settings() { register_setting( 'uploadally', self::OPTION_NAME, array( 'type' => 'array', 'default' => self::default_settings(), 'sanitize_callback' => array( $this, 'sanitize_settings' ), ) ); add_settings_section( 'uploadally', __( 'Image upload optimisation', 'uploadally' ), array( $this, 'render_settings_section' ), 'uploadally_settings' ); $fields = array( 'max_dimension' => __( 'Recommended maximum upload width', 'uploadally' ), 'webp_quality' => __( 'WebP quality', 'uploadally' ), 'delete_original' => __( 'Converted source files', 'uploadally' ), 'max_file_size_mb' => __( 'Maximum image upload size', 'uploadally' ), 'max_megapixels' => __( 'Maximum image resolution', 'uploadally' ), ); foreach ( $fields as $field => $label ) { add_settings_field( 'uploadally_' . $field, $label, array( $this, 'render_settings_field' ), 'uploadally_settings', 'uploadally', array( 'field' => $field ) ); } } /** * Add the plugin screen beneath Media. */ public function register_admin_page() { add_media_page( __( 'Optimization Settings', 'uploadally' ), __( 'Optimization Settings', 'uploadally' ), 'manage_options', 'uploadally', array( $this, 'render_admin_page' ) ); } /** * Render the settings tab. The unused-media controller owns its own tab. */ public function render_admin_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'uploadally' ) ); } $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation; mutations use their own nonce checks. if ( 'unused-media' === $tab ) { UploadAlly_Unused_Media::render_page(); return; } if ( 'optimize-library' === $tab ) { UploadAlly_Library::render_page(); return; } echo '

' . esc_html__( 'UploadAlly', 'uploadally' ) . '

'; $this->render_admin_tabs( 'settings' ); echo '
'; settings_fields( 'uploadally' ); do_settings_sections( 'uploadally_settings' ); submit_button(); echo '
'; } /** * Render navigation shared by both plugin screens. * * @param string $active Active tab slug. */ public static function render_admin_tabs( $active ) { $base = admin_url( 'upload.php?page=uploadally' ); $tabs = array( 'settings' => __( 'Upload Optimization', 'uploadally' ), 'optimize-library' => __( 'Optimize Existing Images', 'uploadally' ), 'unused-media' => __( 'Remove Unused Media', 'uploadally' ), ); echo ''; } /** * Validate settings submitted through the WordPress Settings API. * * @param mixed $input Raw option input. * @return array */ public function sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); $defaults = self::default_settings(); return array( 'max_dimension' => $this->bounded_integer( $input, 'max_dimension', 320, 8192, $defaults['max_dimension'] ), 'webp_quality' => $this->bounded_integer( $input, 'webp_quality', 50, 100, $defaults['webp_quality'] ), 'delete_original' => empty( $input['delete_original'] ) ? 0 : 1, 'max_file_size_mb' => $this->bounded_integer( $input, 'max_file_size_mb', 1, 100, $defaults['max_file_size_mb'] ), 'max_megapixels' => $this->bounded_integer( $input, 'max_megapixels', 1, 200, $defaults['max_megapixels'] ), 'limit_file_size' => empty( $input['limit_file_size'] ) ? 0 : 1, 'limit_megapixels' => empty( $input['limit_megapixels'] ) ? 0 : 1, ); } /** * Clamp an integer setting to its supported range. * * @param array $input Submitted settings. * @param string $key Setting key. * @param int $minimum Minimum value. * @param int $maximum Maximum value. * @param int $default Default value. * @return int */ private function bounded_integer( $input, $key, $minimum, $maximum, $default ) { if ( ! isset( $input[ $key ] ) || ! is_numeric( $input[ $key ] ) ) { return $default; } return min( $maximum, max( $minimum, absint( $input[ $key ] ) ) ); } /** * Explain the active policy and capability status. */ public function render_settings_section() { $status = $this->supports_webp() ? __( 'WebP output is supported by this server.', 'uploadally' ) : __( 'WebP output is not supported by this server. JPEG and PNG sources will be retained unchanged.', 'uploadally' ); echo '

' . esc_html__( 'New JPEG and PNG uploads stay in their original format until you confirm compression in the upload dialog. SVGs, GIFs, icons and existing WebP files are not converted. Standard WordPress image processing still applies.', 'uploadally' ) . '

'; echo '

' . esc_html( $status ) . '

'; } /** * Render a Media setting field. * * @param array $args Field arguments. */ public function render_settings_field( $args ) { $settings = self::get_settings(); $field = isset( $args['field'] ) ? $args['field'] : ''; $name = self::OPTION_NAME . '[' . $field . ']'; switch ( $field ) { case 'max_dimension': printf( ' px', esc_attr( $name ), (int) $settings[ $field ] ); echo '

' . esc_html__( 'Default: 2000 px. Suggested width in the upload dialog; smaller images are never enlarged.', 'uploadally' ) . '

'; break; case 'webp_quality': printf( '', esc_attr( $name ), (int) $settings[ $field ] ); echo '

' . esc_html__( 'Quality for the existing-image converter. New-upload dialogs always start at 80 and allow 50–100.', 'uploadally' ) . '

'; break; case 'delete_original': printf( '', esc_attr( $name ), checked( 1, (int) $settings[ $field ], false ), esc_html__( 'Delete the uploaded JPG or PNG after verified WebP conversion', 'uploadally' ) ); echo '

' . esc_html__( 'Applies to server-side reviews of already-uploaded images. Browser pre-upload compression sends only WebP; its source stays on your device. Disable to retain sources for server-side reviews.', 'uploadally' ) . '

'; break; case 'max_file_size_mb': case 'max_megapixels': $is_size = 'max_file_size_mb' === $field; $toggle = $is_size ? 'limit_file_size' : 'limit_megapixels'; $label = $is_size ? __( 'Maximum image upload size', 'uploadally' ) : __( 'Maximum image resolution', 'uploadally' ); printf( ' %9$s', esc_attr( self::OPTION_NAME . '[' . $toggle . ']' ), checked( 1, (int) $settings[ $toggle ], false ), esc_html__( 'Enable', 'uploadally' ), esc_html( sprintf( /* translators: %s: Image limit setting label. */ __( 'Enable %s', 'uploadally' ), $label ) ), $is_size ? 100 : 200, esc_attr( $name ), (int) $settings[ $field ], esc_attr( $label ), $is_size ? 'MB' : 'MP' ); break; } } /** * Add a direct link to the Media settings. * * @param string[] $links Plugin action links. * @return string[] */ public function add_settings_link( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'uploadally' ) . '' ); return $links; } /** * Register a direct Site Health test. * * @param array $tests Site Health tests. * @return array */ public function register_site_health_test( $tests ) { $tests['direct']['uploadally'] = array( 'label' => __( 'UploadAlly image optimisation', 'uploadally' ), 'test' => array( $this, 'run_site_health_test' ), ); return $tests; } /** * Report server support and effective filter conflicts. * * @return array */ public function run_site_health_test() { $settings = self::get_settings(); $critical = array(); $warnings = array(); if ( ! $this->supports_webp() ) { $critical[] = __( 'The active WordPress image editor cannot write WebP files.', 'uploadally' ); } $effective_quality = apply_filters( 'wp_editor_set_quality', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Invoke the WordPress image-editor filter to check the effective quality. 86, 'image/webp', array( 'width' => (int) $settings['max_dimension'], 'height' => 1000, ) ); if ( (int) $effective_quality !== (int) $settings['webp_quality'] ) { $warnings[] = __( 'Another filter is overriding the configured WebP quality.', 'uploadally' ); } if ( '1' === (string) get_option( 'siteground_optimizer_webp_support', '0' ) ) { $warnings[] = __( 'SiteGround Optimizer WebP generation is also enabled and may create redundant sidecar files.', 'uploadally' ); } $status = 'good'; $label = __( 'Image upload review is configured correctly', 'uploadally' ); $issues = array(); if ( $critical ) { $status = 'critical'; $label = __( 'WebP compression is not available', 'uploadally' ); $issues = $critical; } elseif ( $warnings ) { $status = 'recommended'; $label = __( 'Image optimisation has a configuration conflict', 'uploadally' ); $issues = $warnings; } $description = sprintf( '

%s

', esc_html( sprintf( /* translators: 1: Maximum dimension. 2: WebP quality. 3: Maximum file size or Off. 4: Maximum megapixels or Off. */ __( 'Upload confirmation enabled. Recommended width %1$d px, existing-image WebP quality %2$d, image upload size limit %3$s, image resolution limit %4$s. Each upload dialog starts at quality 80.', 'uploadally' ), (int) $settings['max_dimension'], (int) $settings['webp_quality'], empty( $settings['limit_file_size'] ) ? __( 'Off', 'uploadally' ) : (int) $settings['max_file_size_mb'] . ' MB', empty( $settings['limit_megapixels'] ) ? __( 'Off', 'uploadally' ) : (int) $settings['max_megapixels'] . ' MP' ) ) ); if ( $issues ) { $description .= ''; } return array( 'label' => $label, 'status' => $status, 'badge' => array( 'label' => __( 'Performance', 'uploadally' ), 'color' => 'blue', ), 'description' => $description, 'actions' => sprintf( '

%2$s

', esc_url( admin_url( 'upload.php?page=uploadally' ) ), esc_html__( 'Review image optimisation settings', 'uploadally' ) ), 'test' => 'uploadally', ); } } require_once __DIR__ . '/includes/class-uploadally-unused-media.php'; require_once __DIR__ . '/includes/class-uploadally-library.php'; require_once __DIR__ . '/includes/class-uploadally-upload-review.php'; register_activation_hook( __FILE__, array( 'UploadAlly', 'activate' ) ); if ( ! UploadAlly::legacy_is_active() ) { new UploadAlly(); new UploadAlly_Unused_Media(); new UploadAlly_Library(); new UploadAlly_Upload_Review(); }