From 2a3283c0ad563a2de979b99480c03cdf0986dad4 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 11:19:28 +0100 Subject: [PATCH 01/23] chore: add settings view - use include pattern instead of rendering in class --- src/views/settings-diagnostics.php | 30 ++++++++++++++++++++++++++++++ src/views/settings.php | 3 +++ 2 files changed, 33 insertions(+) create mode 100644 src/views/settings-diagnostics.php diff --git a/src/views/settings-diagnostics.php b/src/views/settings-diagnostics.php new file mode 100644 index 0000000..464d638 --- /dev/null +++ b/src/views/settings-diagnostics.php @@ -0,0 +1,30 @@ + + + + + + + + +
+

+ +

+

+ > + +

+ +
\ No newline at end of file diff --git a/src/views/settings.php b/src/views/settings.php index 07946ac..0deb0e4 100644 --- a/src/views/settings.php +++ b/src/views/settings.php @@ -59,6 +59,9 @@ + + +

From 1625d03bd4ef7ffb69e4ac06d9a8eca553e02db2 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 11:20:53 +0100 Subject: [PATCH 02/23] chore: add logger singleton --- src/class-tiny-logger.php | 270 ++++++++++++++++++++++++++++++++++++++ tiny-compress-images.php | 1 + 2 files changed, 271 insertions(+) create mode 100644 src/class-tiny-logger.php diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php new file mode 100644 index 0000000..d128a7a --- /dev/null +++ b/src/class-tiny-logger.php @@ -0,0 +1,270 @@ +log_enabled = 'on' === get_option('tinypng_logging_enabled', false); + if ($this->log_enabled) { + $this->log_file_path = $this->get_log_file_path(); + } + + add_action('pre_update_option_tinypng_logging_enabled', array($this, 'on_save_log_enabled'), 10, 3); + } + + /** + * Triggered when log_enabled is saved + * - set the setting on the instance + * - if turn off, clear the logs + * - if turned on, check if we can create the log file + * + * @since 3.7.0 + */ + public function on_save_log_enabled($new, $old, $option) + { + $this->log_enabled = 'on' === $new; + if ($this->log_enabled) { + $this->log_file_path = $this->get_log_file_path(); + } + } + + /** + * Gets the log file path. + * + * @since 3.7.0 + * + * @return string The log file path. + */ + private function get_log_file_path() + { + $upload_dir = wp_upload_dir(); + $log_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-logs'; + + if (! file_exists($log_dir)) { + wp_mkdir_p($log_dir); + // Add .htaccess to protect log files. + file_put_contents(trailingslashit($log_dir) . '.htaccess', 'Deny from all'); + } + + return trailingslashit($log_dir) . 'tiny-compress.log'; + } + + /** + * Gets the log directory path. + * + * @since 3.7.0 + * + * @return string The log directory path. + */ + public function get_log_dir() + { + return dirname($this->log_file_path); + } + + /** + * Checks if logging is enabled. + * + * @since 3.7.0 + * + * @return bool True if logging is enabled. + */ + public function is_enabled() + { + return $this->log_enabled; + } + + /** + * Logs an error message. + * + * @since 3.7.0 + * + * @param string $message The message to log. + * @param array $context Optional. Additional context data. Default empty array. + */ + public static function error($message, $context = array()) + { + $instance = self::get_instance(); + $instance->log(self::LOG_LEVEL_ERROR, $message, $context); + } + + /** + * Logs a debug message. + * + * @since 3.7.0 + * + * @param string $message The message to log. + * @param array $context Optional. Additional context data. Default empty array. + */ + public static function debug($message, $context = array()) + { + $instance = self::get_instance(); + $instance->log(self::LOG_LEVEL_DEBUG, $message, $context); + } + + /** + * Logs a message. + * + * @since 3.7.0 + * + * @param string $level The log level. + * @param string $message The message to log. + * @param array $context Optional. Additional context data. Default empty array. + */ + private function log($level, $message, $context = array()) + { + if (! $this->log_enabled) { + return; + } + + $this->rotate_logs(); + + $timestamp = current_time('Y-m-d H:i:s'); + $level_str = strtoupper($level); + $context_str = ! empty($context) ? ' ' . wp_json_encode($context) : ''; + $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}\n"; + + $file = fopen($this->log_file_path, 'a'); + if ($file) { + fwrite($file, $log_entry); + fclose($file); + } + } + + /** + * Rotates log files when they exceed the max size. + * + * @since 3.7.0 + */ + private function rotate_logs() + { + if (! file_exists($this->log_file_path)) { + return; + } + + $file_size = filesize($this->log_file_path); + if ($file_size < self::MAX_LOG_SIZE) { + return; + } + + for ($i = self::MAX_LOG_FILES - 1; $i > 0; $i--) { + $old_file = $this->log_file_path . '.' . $i; + $new_file = $this->log_file_path . '.' . ($i + 1); + + if (file_exists($old_file)) { + if ($i === self::MAX_LOG_FILES - 1) { + unlink($old_file); + } else { + rename($old_file, $new_file); + } + } + } + + rename($this->log_file_path, $this->log_file_path . '.1'); + } + + /** + * Clears all log files. + * + * @since 3.7.0 + * + * @return bool True if logs were cleared successfully. + */ + public function clear_logs() + { + $cleared = true; + + // Remove main log file. + if (file_exists($this->log_file_path)) { + $cleared = unlink($this->log_file_path) && $cleared; + } + + // Remove rotated log files. + for ($i = 1; $i <= self::MAX_LOG_FILES; $i++) { + $log_file = $this->log_file_path . '.' . $i; + if (file_exists($log_file)) { + $cleared = unlink($log_file) && $cleared; + } + } + + return $cleared; + } + + /** + * Gets all log file paths. + * + * @since 3.7.0 + * + * @return array Array of log file paths. + */ + public function get_log_files() + { + $files = array(); + + if (file_exists($this->log_file_path)) { + $files[] = $this->log_file_path; + } + + for ($i = 1; $i <= self::MAX_LOG_FILES; $i++) { + $log_file = $this->log_file_path . '.' . $i; + if (file_exists($log_file)) { + $files[] = $log_file; + } + } + + return $files; + } +} diff --git a/tiny-compress-images.php b/tiny-compress-images.php index 2efa12f..d5b8738 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -14,6 +14,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-php.php'; require dirname( __FILE__ ) . '/src/class-tiny-wp-base.php'; require dirname( __FILE__ ) . '/src/class-tiny-exception.php'; +require dirname( __FILE__ ) . '/src/class-tiny-logger.php'; require dirname( __FILE__ ) . '/src/class-tiny-compress.php'; require dirname( __FILE__ ) . '/src/class-tiny-bulk-optimization.php'; require dirname( __FILE__ ) . '/src/class-tiny-image-size.php'; From d3e3be1ef90d9d921bcad07042f03e0b1fe5e0a8 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 11:21:39 +0100 Subject: [PATCH 03/23] chore: add diagnostics class - download zip file including logs and information about setup --- src/class-tiny-diagnostics.php | 279 +++++++++++++++++++++++++++++++++ src/class-tiny-settings.php | 4 + tiny-compress-images.php | 1 + 3 files changed, 284 insertions(+) create mode 100644 src/class-tiny-diagnostics.php diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php new file mode 100644 index 0000000..34c56d0 --- /dev/null +++ b/src/class-tiny-diagnostics.php @@ -0,0 +1,279 @@ +settings = $settings; + + add_action( + 'wp_ajax_tiny_download_diagnostics', + array($this, 'download_diagnostics') + ); + } + + /** + * Collects all diagnostic information. + * + * File contains: + * - timestamp of export + * - server information + * - site information + * - plugin list + * - tinify settings + * - image settings + * - logs + * + * @since 3.7.0 + * + * @return array Array of diagnostic information. + */ + public function collect_info() + { + $info = array( + 'timestamp' => current_time('Y-m-d H:i:s'), + 'server_info' => self::get_server_info(), + 'site_info' => self::get_site_info(), + 'active_plugins' => self::get_active_plugins(), + 'tiny_info' => $this->get_tiny_info(), + 'image_sizes' => $this->settings->get_active_tinify_sizes(), + ); + + return $info; + } + + /** + * Gets site information. + * + * @since 3.7.0 + * + * @return array Site information. + */ + private static function get_site_info() + { + global $wp_version; + $theme = wp_get_theme(); + + return array( + 'wp_version' => $wp_version, + 'site_url' => get_site_url(), + 'home_url' => get_home_url(), + 'is_multisite' => is_multisite(), + 'site_language' => get_locale(), + 'timezone' => wp_timezone_string(), + 'theme_name' => $theme->get('Name'), + 'theme_version' => $theme->get('Version'), + 'theme_uri' => $theme->get('ThemeURI'), + ); + } + + /** + * Gets server information. + * + * @since 3.7.0 + * + * @return array Server information. + */ + private static function get_server_info() + { + global $wpdb; + + return array( + 'php_version' => phpversion(), + 'server_software' => isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : 'Unknown', + 'mysql_version' => $wpdb->db_version(), + 'max_execution_time' => ini_get('max_execution_time'), + 'memory_limit' => ini_get('memory_limit'), + 'post_max_size' => ini_get('post_max_size'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'max_input_vars' => ini_get('max_input_vars'), + 'curl_version' => function_exists('curl_version') ? curl_version()['version'] : 'Not available', + 'disabled_functions' => ini_get('disable_functions'), + ); + } + + /** + * Gets list of active plugins. + * + * @since 3.7.0 + * + * @return array List of active plugins. + */ + private static function get_active_plugins() + { + $active_plugins = get_option('active_plugins', array()); + $plugins = array(); + + foreach ($active_plugins as $plugin) { + $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin); + $plugins[] = array( + 'name' => $plugin_data['Name'], + 'version' => $plugin_data['Version'], + 'author' => $plugin_data['Author'], + 'file' => $plugin, + ); + } + + return $plugins; + } + + /** + * Gets TinyPNG plugin info & settings. + * + * @since 3.7.0 + * + * @return array Plugin settings + */ + private function get_tiny_info() + { + return array( + 'version' => Tiny_Plugin::version(), + 'status' => $this->settings->get_status(), + 'php_client_supported' => Tiny_PHP::client_supported(), + + 'compression_count' => $this->settings->get_compression_count(), + 'compression_timing' => $this->settings->get_compression_timing(), + 'conversion' => $this->settings->get_conversion_options(), + 'paying_state' => $this->settings->get_paying_state(), + ); + } + + public function download_diagnostics() + { + $zippath = $this->create_diagnostic_zip(); + return $this->download_zip($zippath); + } + + /** + * Creates a diagnostic zip file. + * + * @since 3.7.0 + * + * @return string|WP_Error Path to the created zip file or WP_Error on failure. + */ + public function create_diagnostic_zip() + { + if (! class_exists('ZipArchive')) { + return new WP_Error('zip_not_available', __('ZipArchive class is not available on this server.', 'tiny-compress-images')); + } + + $upload_dir = wp_upload_dir(); + $temp_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-temp'; + + if (! file_exists($temp_dir)) { + wp_mkdir_p($temp_dir); + } + + $zip_filename = 'tiny-compress-diagnostics-' . gmdate('Y-m-d-His') . '.zip'; + $zip_path = trailingslashit($temp_dir) . $zip_filename; + + $zip = new ZipArchive(); + if (true !== $zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { + return new WP_Error('zip_create_failed', __('Failed to create zip file.', 'tiny-compress-images')); + } + + // Add diagnostic info. + $info = self::collect_info(); + $zip->addFromString('diagnostics.json', wp_json_encode($info, JSON_PRETTY_PRINT)); + + // Add log files. + $logger = Tiny_Logger::get_instance(); + $log_files = $logger->get_log_files(); + + foreach ($log_files as $log_file) { + if (file_exists($log_file)) { + $zip->addFile($log_file, 'logs/' . basename($log_file)); + } + } + + $zip->close(); + return $zip_path; + } + + /** + * Downloads and removes the zip + * + * @since 3.7.0 + * + * @param string $zip_path Path to the zip file. + */ + public static function download_zip($zip_path) + { + if (! file_exists($zip_path)) { + wp_die(esc_html__('Diagnostic file not found.', 'tiny-compress-images')); + } + + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="' . basename($zip_path) . '"'); + header('Content-Length: ' . filesize($zip_path)); + header('Pragma: no-cache'); + header('Expires: 0'); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile + readfile($zip_path); + + // Clean up. + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + unlink($zip_path); + + exit; + } + + /** + * Cleans up old diagnostic zip files. + * + * @since 3.7.0 + */ + public static function cleanup_old_diagnostics() + { + $upload_dir = wp_upload_dir(); + $temp_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-temp'; + + if (! file_exists($temp_dir)) { + return; + } + + $files = glob(trailingslashit($temp_dir) . 'tiny-compress-diagnostics-*.zip'); + $max_age = DAY_IN_SECONDS; // 1 day. + + foreach ($files as $file) { + if (file_exists($file) && (time() - filemtime($file)) > $max_age) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + unlink($file); + } + } + } +} diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 19c3308..a0678cb 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -30,6 +30,7 @@ class Tiny_Settings extends Tiny_WP_Base { public function __construct() { parent::__construct(); $this->notices = new Tiny_Notices(); + new Tiny_Diagnostics($this); } private function init_compressor() { @@ -127,6 +128,9 @@ public function admin_init() { $field = self::get_prefixed_name( 'convert_format' ); register_setting( 'tinify', $field ); + + $field = self::get_prefixed_name( 'logging_enabled' ); + register_setting( 'tinify', $field ); } public function admin_menu() { diff --git a/tiny-compress-images.php b/tiny-compress-images.php index d5b8738..95f8514 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -15,6 +15,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-wp-base.php'; require dirname( __FILE__ ) . '/src/class-tiny-exception.php'; require dirname( __FILE__ ) . '/src/class-tiny-logger.php'; +require dirname( __FILE__ ) . '/src/class-tiny-diagnostics.php'; require dirname( __FILE__ ) . '/src/class-tiny-compress.php'; require dirname( __FILE__ ) . '/src/class-tiny-bulk-optimization.php'; require dirname( __FILE__ ) . '/src/class-tiny-image-size.php'; From f38b3c7e0aa76a419e5bca68a4343dd873ceecc4 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 11:21:54 +0100 Subject: [PATCH 04/23] chore: download zip file from settings page --- src/js/admin.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/js/admin.js b/src/js/admin.js index b8a972f..ad4e5e9 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -1,4 +1,10 @@ (function() { + function downloadDiagnostics() { + const downloadURL = `${ajaxurl}?action=tiny_download_diagnostics&security=${tinyCompress.nonce}`; + window.location.href = downloadURL; + } + jQuery('#tiny-download-diagnostics').click(downloadDiagnostics); + function compressImage(event) { var element = jQuery(event.target); var container = element.closest('div.tiny-ajax-container'); From 25bbc4640d64075d05c9361e9a4f271f664b1523 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 11:31:45 +0100 Subject: [PATCH 05/23] chore: add loader and improve markup - added utility classes --- src/css/admin.css | 8 ++++++++ src/js/admin.js | 13 ++++++++++--- src/views/settings-diagnostics.php | 7 +++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/css/admin.css b/src/css/admin.css index f87d6dc..977dadf 100644 --- a/src/css/admin.css +++ b/src/css/admin.css @@ -473,4 +473,12 @@ fieldset.tinypng_convert_fields label span { fieldset.tinypng_convert_fields[disabled] { opacity: 0.6; +} + +.tiny-d-flex { + display: flex; +} + +.tiny-mt-2 { + margin-top: 10px; } \ No newline at end of file diff --git a/src/js/admin.js b/src/js/admin.js index ad4e5e9..0ead0f4 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -1,10 +1,17 @@ (function() { function downloadDiagnostics() { - const downloadURL = `${ajaxurl}?action=tiny_download_diagnostics&security=${tinyCompress.nonce}`; - window.location.href = downloadURL; + try { + jQuery('#download-diagnostics-spinner').show(); + jQuery('#tiny-download-diagnostics').attr('disabled', true); + const downloadURL = `${ajaxurl}?action=tiny_download_diagnostics&security=${tinyCompress.nonce}`; + window.location.href = downloadURL; + } finally { + jQuery('#tiny-download-diagnostics').attr('disabled', false); + jQuery('#download-diagnostics-spinner').hide(); + } } jQuery('#tiny-download-diagnostics').click(downloadDiagnostics); - + function compressImage(event) { var element = jQuery(event.target); var container = element.closest('div.tiny-ajax-container'); diff --git a/src/views/settings-diagnostics.php b/src/views/settings-diagnostics.php index 464d638..2638e7e 100644 --- a/src/views/settings-diagnostics.php +++ b/src/views/settings-diagnostics.php @@ -21,9 +21,12 @@

- + + + From 47cd48f43a9ef97f5477130d7c382d46d88f6dfe Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 2 Jan 2026 22:11:20 +0100 Subject: [PATCH 06/23] Add various log points --- src/class-tiny-compress-client.php | 6 +++++- src/class-tiny-compress-fopen.php | 6 ++++++ src/class-tiny-image.php | 27 +++++++++++++++++++++++++++ src/class-tiny-logger.php | 18 ++++++++++-------- src/class-tiny-plugin.php | 26 +++++++++++++++++++++++++- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index 5bbf8c9..f868a23 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -92,7 +92,6 @@ protected function compress( $input, $resize_opts, $preserve_opts, $convert_to ) try { $this->last_error_code = 0; $this->set_request_options( \Tinify\Tinify::getClient() ); - $source = \Tinify\fromBuffer( $input ); if ( $resize_opts ) { @@ -137,6 +136,11 @@ protected function compress( $input, $resize_opts, $preserve_opts, $convert_to ) return $result; } catch ( \Tinify\Exception $err ) { $this->last_error_code = $err->status; + + Tiny_Logger::error('client compress error', array( + "error" => $err->getMessage(), + "status" => $err->status, + )); throw new Tiny_Exception( $err->getMessage(), diff --git a/src/class-tiny-compress-fopen.php b/src/class-tiny-compress-fopen.php index 4e7a9cd..83a1928 100644 --- a/src/class-tiny-compress-fopen.php +++ b/src/class-tiny-compress-fopen.php @@ -86,6 +86,12 @@ protected function compress( $input, $resize_opts, $preserve_opts, $convert_to ) $params = $this->request_options( 'POST', $input ); list($details, $headers, $status_code) = $this->request( $params ); + Tiny_Logger::debug('client fopen compress out', array( + 'details' => $details, + 'headers' => $headers, + 'status' => $status_code, + )); + $output_url = isset( $headers['location'] ) ? $headers['location'] : null; if ( $status_code >= 400 && is_array( $details ) && isset( $details['error'] ) ) { throw new Tiny_Exception( diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index 3a5a267..d8f803c 100644 --- a/src/class-tiny-image.php +++ b/src/class-tiny-image.php @@ -185,6 +185,10 @@ public function get_mime_type() { } public function compress() { + Tiny_Logger::debug('compress', array( + 'image_id' => $this->id, + 'name' => $this->name, + )); if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) { return; } @@ -207,6 +211,20 @@ public function compress() { $convert_to = $this->convert_to(); foreach ( $unprocessed_sizes as $size_name => $size ) { + Tiny_Logger::debug('compress size', array( + 'image_id' => $this->id, + 'size' => $size_name, + 'resize' => $resize, + 'preserve' => $preserve, + 'convert' => $convert_to, + 'modified' => $size->modified(), + 'filename' => $size->filename, + 'is_duplicate' => $size->is_duplicate(), + 'exists' => $size->exists(), + 'has_been_compressed' => $size->has_been_compressed(), + 'filesize' => $size->filesize(), + 'mimetype' => $size->mimetype(), + )); if ( ! $size->is_duplicate() ) { $size->add_tiny_meta_start(); $this->update_tiny_post_meta(); @@ -227,9 +245,18 @@ public function compress() { $size->add_tiny_meta( $response ); $success++; + Tiny_Logger::debug('compress success', array( + 'size' => $size_name, + 'image_id' => $this->id, + )); } catch ( Tiny_Exception $e ) { $size->add_tiny_meta_error( $e ); $failed++; + Tiny_Logger::error('compress failed', array( + 'error' => $e, + 'size' => $size_name, + 'image_id' => $this->id, + )); } $this->add_wp_metadata( $size_name, $size ); $this->update_tiny_post_meta(); diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index d128a7a..2253ff1 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -59,11 +59,11 @@ public static function get_instance() private function __construct() { $this->log_enabled = 'on' === get_option('tinypng_logging_enabled', false); - if ($this->log_enabled) { - $this->log_file_path = $this->get_log_file_path(); - } + $this->log_file_path = $this->get_log_file_path(); + } - add_action('pre_update_option_tinypng_logging_enabled', array($this, 'on_save_log_enabled'), 10, 3); + public static function init() { + add_filter('pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled', 10, 3); } /** @@ -74,12 +74,14 @@ private function __construct() * * @since 3.7.0 */ - public function on_save_log_enabled($new, $old, $option) + public static function on_save_log_enabled($log_enabled, $old, $option) { - $this->log_enabled = 'on' === $new; - if ($this->log_enabled) { - $this->log_file_path = $this->get_log_file_path(); + if ($log_enabled !== "on") { + $instance = self::get_instance(); + $instance->clear_logs(); } + + return $log_enabled; } /** diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index f6ff1d1..078be44 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -178,6 +178,7 @@ public function admin_init() { $this->tiny_compatibility(); add_thickbox(); + Tiny_Logger::init(); } public function admin_menu() { @@ -314,6 +315,11 @@ public function process_attachment( $metadata, $attachment_id ) { public function blocking_compress_on_upload( $metadata, $attachment_id ) { if ( ! empty( $metadata ) ) { $tiny_image = new Tiny_Image( $this->settings, $attachment_id, $metadata ); + + Tiny_Logger::debug('blocking compress on upload', array( + 'image_id' => $attachment_id, + )); + $result = $tiny_image->compress(); return $tiny_image->get_wp_metadata(); } else { @@ -354,6 +360,10 @@ public function async_compress_on_upload( $metadata, $attachment_id ) { set_transient( 'tiny_rpc_' . $rpc_hash, get_current_user_id(), 10 ); } + Tiny_Logger::debug('remote post', array( + 'image_id' => $attachment_id, + )); + if ( getenv( 'WORDPRESS_HOST' ) !== false ) { wp_remote_post( getenv( 'WORDPRESS_HOST' ) . '/wp-admin/admin-ajax.php', $args ); } else { @@ -406,6 +416,11 @@ public function compress_on_upload() { $metadata = $_POST['metadata']; if ( is_array( $metadata ) ) { $tiny_image = new Tiny_Image( $this->settings, $attachment_id, $metadata ); + + Tiny_Logger::debug('compress on upload', array( + 'image_id' => $attachment_id, + )); + $result = $tiny_image->compress(); // The wp_update_attachment_metadata call is thrown because the // dimensions of the original image can change. This will then @@ -469,8 +484,12 @@ public function compress_image_from_library() { echo $response['error']; exit(); } - list($id, $metadata) = $response['data']; + + Tiny_Logger::debug('compress from library', array( + 'image_id' => $id, + )); + $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); $result = $tiny_image->compress(); @@ -503,6 +522,11 @@ public function compress_image_for_bulk() { $size_before = $image_statistics_before['compressed_total_size']; $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); + + Tiny_Logger::debug('compress from bulk', array( + 'image_id' => $id, + )); + $result = $tiny_image->compress(); $image_statistics = $tiny_image->get_statistics( $this->settings->get_sizes(), From df29291890187dffc23e6601f680ef3f33a277b1 Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 13:36:23 +0100 Subject: [PATCH 07/23] Set timeout and proxy options on curl client --- src/class-tiny-compress-client.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index f868a23..e91922f 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -30,6 +30,14 @@ class Tiny_Compress_Client extends Tiny_Compress { + /** + * API request timeout in seconds. + * + * @since 3.6.8 + * @var int + */ + const API_TIMEOUT = 10; + private $last_error_code = 0; private $last_message = ''; private $proxy; @@ -176,6 +184,10 @@ private function set_request_options( $client ) { $property->setAccessible( true ); $options = $property->getValue( $client ); + // Set API request timeout to prevent indefinite hanging + $options[ CURLOPT_TIMEOUT ] = self::API_TIMEOUT; + $options[ CURLOPT_CONNECTTIMEOUT ] = self::API_TIMEOUT; + if ( TINY_DEBUG ) { $file = fopen( dirname( __FILE__ ) . '/curl.log', 'w' ); if ( is_resource( $file ) ) { @@ -194,5 +206,7 @@ private function set_request_options( $client ) { $options[ CURLOPT_PROXYUSERPWD ] = $this->proxy->authentication(); } } + + $property->setValue( $client, $options ); } } From 98eade789c2c9113324a1ef839dc0852974c18a8 Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 13:50:35 +0100 Subject: [PATCH 08/23] Scaffold tests --- src/class-tiny-logger.php | 6 ------ test/unit/TinyDiagnosticsTest.php | 11 +++++++++++ test/unit/TinyLoggerTest.php | 11 +++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 test/unit/TinyDiagnosticsTest.php create mode 100644 test/unit/TinyLoggerTest.php diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 2253ff1..fe3a3c0 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -96,12 +96,6 @@ private function get_log_file_path() $upload_dir = wp_upload_dir(); $log_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-logs'; - if (! file_exists($log_dir)) { - wp_mkdir_p($log_dir); - // Add .htaccess to protect log files. - file_put_contents(trailingslashit($log_dir) . '.htaccess', 'Deny from all'); - } - return trailingslashit($log_dir) . 'tiny-compress.log'; } diff --git a/test/unit/TinyDiagnosticsTest.php b/test/unit/TinyDiagnosticsTest.php new file mode 100644 index 0000000..c9db2ed --- /dev/null +++ b/test/unit/TinyDiagnosticsTest.php @@ -0,0 +1,11 @@ + Date: Mon, 5 Jan 2026 19:09:12 +0100 Subject: [PATCH 09/23] formatting --- src/class-tiny-compress-client.php | 6 +- src/class-tiny-diagnostics.php | 128 +++++----- src/class-tiny-image.php | 4 +- src/class-tiny-logger.php | 108 ++++---- src/class-tiny-plugin.php | 6 +- src/class-tiny-settings.php | 2 +- src/views/settings-diagnostics.php | 58 ++--- src/views/settings.php | 2 +- test/helpers/wordpress.php | 389 +++++++++++++++++------------ 9 files changed, 380 insertions(+), 323 deletions(-) diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index e91922f..d4a8918 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -144,10 +144,10 @@ protected function compress( $input, $resize_opts, $preserve_opts, $convert_to ) return $result; } catch ( \Tinify\Exception $err ) { $this->last_error_code = $err->status; - + Tiny_Logger::error('client compress error', array( - "error" => $err->getMessage(), - "status" => $err->status, + 'error' => $err->getMessage(), + 'status' => $err->status, )); throw new Tiny_Exception( diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php index 34c56d0..989e93b 100644 --- a/src/class-tiny-diagnostics.php +++ b/src/class-tiny-diagnostics.php @@ -23,8 +23,8 @@ * * @since 3.7.0 */ -class Tiny_Diagnostics -{ +class Tiny_Diagnostics { + /** * Tiny settings * @@ -35,19 +35,18 @@ class Tiny_Diagnostics /** * @param Tiny_Settings $settings */ - public function __construct($settings) - { + public function __construct( $settings ) { $this->settings = $settings; add_action( 'wp_ajax_tiny_download_diagnostics', - array($this, 'download_diagnostics') + array( $this, 'download_diagnostics' ) ); } /** * Collects all diagnostic information. - * + * * File contains: * - timestamp of export * - server information @@ -56,15 +55,14 @@ public function __construct($settings) * - tinify settings * - image settings * - logs - * + * * @since 3.7.0 * * @return array Array of diagnostic information. */ - public function collect_info() - { + public function collect_info() { $info = array( - 'timestamp' => current_time('Y-m-d H:i:s'), + 'timestamp' => current_time( 'Y-m-d H:i:s' ), 'server_info' => self::get_server_info(), 'site_info' => self::get_site_info(), 'active_plugins' => self::get_active_plugins(), @@ -82,8 +80,7 @@ public function collect_info() * * @return array Site information. */ - private static function get_site_info() - { + private static function get_site_info() { global $wp_version; $theme = wp_get_theme(); @@ -94,9 +91,9 @@ private static function get_site_info() 'is_multisite' => is_multisite(), 'site_language' => get_locale(), 'timezone' => wp_timezone_string(), - 'theme_name' => $theme->get('Name'), - 'theme_version' => $theme->get('Version'), - 'theme_uri' => $theme->get('ThemeURI'), + 'theme_name' => $theme->get( 'Name' ), + 'theme_version' => $theme->get( 'Version' ), + 'theme_uri' => $theme->get( 'ThemeURI' ), ); } @@ -107,21 +104,20 @@ private static function get_site_info() * * @return array Server information. */ - private static function get_server_info() - { + private static function get_server_info() { global $wpdb; return array( 'php_version' => phpversion(), - 'server_software' => isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])) : 'Unknown', + 'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown', 'mysql_version' => $wpdb->db_version(), - 'max_execution_time' => ini_get('max_execution_time'), - 'memory_limit' => ini_get('memory_limit'), - 'post_max_size' => ini_get('post_max_size'), - 'upload_max_filesize' => ini_get('upload_max_filesize'), - 'max_input_vars' => ini_get('max_input_vars'), - 'curl_version' => function_exists('curl_version') ? curl_version()['version'] : 'Not available', - 'disabled_functions' => ini_get('disable_functions'), + 'max_execution_time' => ini_get( 'max_execution_time' ), + 'memory_limit' => ini_get( 'memory_limit' ), + 'post_max_size' => ini_get( 'post_max_size' ), + 'upload_max_filesize' => ini_get( 'upload_max_filesize' ), + 'max_input_vars' => ini_get( 'max_input_vars' ), + 'curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : 'Not available', + 'disabled_functions' => ini_get( 'disable_functions' ), ); } @@ -132,13 +128,12 @@ private static function get_server_info() * * @return array List of active plugins. */ - private static function get_active_plugins() - { - $active_plugins = get_option('active_plugins', array()); + private static function get_active_plugins() { + $active_plugins = get_option( 'active_plugins', array() ); $plugins = array(); - foreach ($active_plugins as $plugin) { - $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin); + foreach ( $active_plugins as $plugin ) { + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); $plugins[] = array( 'name' => $plugin_data['Name'], 'version' => $plugin_data['Version'], @@ -157,8 +152,7 @@ private static function get_active_plugins() * * @return array Plugin settings */ - private function get_tiny_info() - { + private function get_tiny_info() { return array( 'version' => Tiny_Plugin::version(), 'status' => $this->settings->get_status(), @@ -171,10 +165,9 @@ private function get_tiny_info() ); } - public function download_diagnostics() - { + public function download_diagnostics() { $zippath = $this->create_diagnostic_zip(); - return $this->download_zip($zippath); + return $this->download_zip( $zippath ); } /** @@ -184,38 +177,37 @@ public function download_diagnostics() * * @return string|WP_Error Path to the created zip file or WP_Error on failure. */ - public function create_diagnostic_zip() - { - if (! class_exists('ZipArchive')) { - return new WP_Error('zip_not_available', __('ZipArchive class is not available on this server.', 'tiny-compress-images')); + public function create_diagnostic_zip() { + if ( ! class_exists( 'ZipArchive' ) ) { + return new WP_Error( 'zip_not_available', __( 'ZipArchive class is not available on this server.', 'tiny-compress-images' ) ); } $upload_dir = wp_upload_dir(); - $temp_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-temp'; + $temp_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-temp'; - if (! file_exists($temp_dir)) { - wp_mkdir_p($temp_dir); + if ( ! file_exists( $temp_dir ) ) { + wp_mkdir_p( $temp_dir ); } - $zip_filename = 'tiny-compress-diagnostics-' . gmdate('Y-m-d-His') . '.zip'; - $zip_path = trailingslashit($temp_dir) . $zip_filename; + $zip_filename = 'tiny-compress-diagnostics-' . gmdate( 'Y-m-d-His' ) . '.zip'; + $zip_path = trailingslashit( $temp_dir ) . $zip_filename; $zip = new ZipArchive(); - if (true !== $zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { - return new WP_Error('zip_create_failed', __('Failed to create zip file.', 'tiny-compress-images')); + if ( true !== $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { + return new WP_Error( 'zip_create_failed', __( 'Failed to create zip file.', 'tiny-compress-images' ) ); } // Add diagnostic info. $info = self::collect_info(); - $zip->addFromString('diagnostics.json', wp_json_encode($info, JSON_PRETTY_PRINT)); + $zip->addFromString( 'diagnostics.json', wp_json_encode( $info, JSON_PRETTY_PRINT ) ); // Add log files. $logger = Tiny_Logger::get_instance(); $log_files = $logger->get_log_files(); - foreach ($log_files as $log_file) { - if (file_exists($log_file)) { - $zip->addFile($log_file, 'logs/' . basename($log_file)); + foreach ( $log_files as $log_file ) { + if ( file_exists( $log_file ) ) { + $zip->addFile( $log_file, 'logs/' . basename( $log_file ) ); } } @@ -230,24 +222,23 @@ public function create_diagnostic_zip() * * @param string $zip_path Path to the zip file. */ - public static function download_zip($zip_path) - { - if (! file_exists($zip_path)) { - wp_die(esc_html__('Diagnostic file not found.', 'tiny-compress-images')); + public static function download_zip( $zip_path ) { + if ( ! file_exists( $zip_path ) ) { + wp_die( esc_html__( 'Diagnostic file not found.', 'tiny-compress-images' ) ); } - header('Content-Type: application/zip'); - header('Content-Disposition: attachment; filename="' . basename($zip_path) . '"'); - header('Content-Length: ' . filesize($zip_path)); - header('Pragma: no-cache'); - header('Expires: 0'); + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename="' . basename( $zip_path ) . '"' ); + header( 'Content-Length: ' . filesize( $zip_path ) ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile - readfile($zip_path); + readfile( $zip_path ); // Clean up. // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink - unlink($zip_path); + unlink( $zip_path ); exit; } @@ -257,22 +248,21 @@ public static function download_zip($zip_path) * * @since 3.7.0 */ - public static function cleanup_old_diagnostics() - { + public static function cleanup_old_diagnostics() { $upload_dir = wp_upload_dir(); - $temp_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-temp'; + $temp_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-temp'; - if (! file_exists($temp_dir)) { + if ( ! file_exists( $temp_dir ) ) { return; } - $files = glob(trailingslashit($temp_dir) . 'tiny-compress-diagnostics-*.zip'); + $files = glob( trailingslashit( $temp_dir ) . 'tiny-compress-diagnostics-*.zip' ); $max_age = DAY_IN_SECONDS; // 1 day. - foreach ($files as $file) { - if (file_exists($file) && (time() - filemtime($file)) > $max_age) { + foreach ( $files as $file ) { + if ( file_exists( $file ) && (time() - filemtime( $file )) > $max_age ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink - unlink($file); + unlink( $file ); } } } diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index d8f803c..a77ab38 100644 --- a/src/class-tiny-image.php +++ b/src/class-tiny-image.php @@ -260,8 +260,8 @@ public function compress() { } $this->add_wp_metadata( $size_name, $size ); $this->update_tiny_post_meta(); - } - } + }// End if(). + }// End foreach(). /* Other plugins can hook into this action to execute custom logic diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index fe3a3c0..0f833de 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -23,8 +23,8 @@ * * @since 3.7.0 */ -class Tiny_Logger -{ +class Tiny_Logger { + const LOG_LEVEL_ERROR = 'error'; const LOG_LEVEL_DEBUG = 'debug'; @@ -43,9 +43,8 @@ class Tiny_Logger * * @return Tiny_Logger The logger instance. */ - public static function get_instance() - { - if (null === self::$instance) { + public static function get_instance() { + if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; @@ -56,14 +55,13 @@ public static function get_instance() * * @since 3.7.0 */ - private function __construct() - { - $this->log_enabled = 'on' === get_option('tinypng_logging_enabled', false); + private function __construct() { + $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); $this->log_file_path = $this->get_log_file_path(); } public static function init() { - add_filter('pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled', 10, 3); + add_filter( 'pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled', 10, 3 ); } /** @@ -74,9 +72,8 @@ public static function init() { * * @since 3.7.0 */ - public static function on_save_log_enabled($log_enabled, $old, $option) - { - if ($log_enabled !== "on") { + public static function on_save_log_enabled( $log_enabled, $old, $option ) { + if ( $log_enabled !== 'on' ) { $instance = self::get_instance(); $instance->clear_logs(); } @@ -91,12 +88,11 @@ public static function on_save_log_enabled($log_enabled, $old, $option) * * @return string The log file path. */ - private function get_log_file_path() - { + private function get_log_file_path() { $upload_dir = wp_upload_dir(); - $log_dir = trailingslashit($upload_dir['basedir']) . 'tiny-compress-logs'; + $log_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-logs'; - return trailingslashit($log_dir) . 'tiny-compress.log'; + return trailingslashit( $log_dir ) . 'tiny-compress.log'; } /** @@ -106,9 +102,8 @@ private function get_log_file_path() * * @return string The log directory path. */ - public function get_log_dir() - { - return dirname($this->log_file_path); + public function get_log_dir() { + return dirname( $this->log_file_path ); } /** @@ -118,8 +113,7 @@ public function get_log_dir() * * @return bool True if logging is enabled. */ - public function is_enabled() - { + public function is_enabled() { return $this->log_enabled; } @@ -131,10 +125,9 @@ public function is_enabled() * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. */ - public static function error($message, $context = array()) - { + public static function error( $message, $context = array() ) { $instance = self::get_instance(); - $instance->log(self::LOG_LEVEL_ERROR, $message, $context); + $instance->log( self::LOG_LEVEL_ERROR, $message, $context ); } /** @@ -145,10 +138,9 @@ public static function error($message, $context = array()) * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. */ - public static function debug($message, $context = array()) - { + public static function debug( $message, $context = array() ) { $instance = self::get_instance(); - $instance->log(self::LOG_LEVEL_DEBUG, $message, $context); + $instance->log( self::LOG_LEVEL_DEBUG, $message, $context ); } /** @@ -160,23 +152,22 @@ public static function debug($message, $context = array()) * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. */ - private function log($level, $message, $context = array()) - { - if (! $this->log_enabled) { + private function log( $level, $message, $context = array() ) { + if ( ! $this->log_enabled ) { return; } $this->rotate_logs(); - $timestamp = current_time('Y-m-d H:i:s'); - $level_str = strtoupper($level); - $context_str = ! empty($context) ? ' ' . wp_json_encode($context) : ''; + $timestamp = current_time( 'Y-m-d H:i:s' ); + $level_str = strtoupper( $level ); + $context_str = ! empty( $context ) ? ' ' . wp_json_encode( $context ) : ''; $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}\n"; - $file = fopen($this->log_file_path, 'a'); - if ($file) { - fwrite($file, $log_entry); - fclose($file); + $file = fopen( $this->log_file_path, 'a' ); + if ( $file ) { + fwrite( $file, $log_entry ); + fclose( $file ); } } @@ -185,31 +176,30 @@ private function log($level, $message, $context = array()) * * @since 3.7.0 */ - private function rotate_logs() - { - if (! file_exists($this->log_file_path)) { + private function rotate_logs() { + if ( ! file_exists( $this->log_file_path ) ) { return; } - $file_size = filesize($this->log_file_path); - if ($file_size < self::MAX_LOG_SIZE) { + $file_size = filesize( $this->log_file_path ); + if ( $file_size < self::MAX_LOG_SIZE ) { return; } - for ($i = self::MAX_LOG_FILES - 1; $i > 0; $i--) { + for ( $i = self::MAX_LOG_FILES - 1; $i > 0; $i-- ) { $old_file = $this->log_file_path . '.' . $i; $new_file = $this->log_file_path . '.' . ($i + 1); - if (file_exists($old_file)) { - if ($i === self::MAX_LOG_FILES - 1) { - unlink($old_file); + if ( file_exists( $old_file ) ) { + if ( $i === self::MAX_LOG_FILES - 1 ) { + unlink( $old_file ); } else { - rename($old_file, $new_file); + rename( $old_file, $new_file ); } } } - rename($this->log_file_path, $this->log_file_path . '.1'); + rename( $this->log_file_path, $this->log_file_path . '.1' ); } /** @@ -219,20 +209,19 @@ private function rotate_logs() * * @return bool True if logs were cleared successfully. */ - public function clear_logs() - { + public function clear_logs() { $cleared = true; // Remove main log file. - if (file_exists($this->log_file_path)) { - $cleared = unlink($this->log_file_path) && $cleared; + if ( file_exists( $this->log_file_path ) ) { + $cleared = unlink( $this->log_file_path ) && $cleared; } // Remove rotated log files. - for ($i = 1; $i <= self::MAX_LOG_FILES; $i++) { + for ( $i = 1; $i <= self::MAX_LOG_FILES; $i++ ) { $log_file = $this->log_file_path . '.' . $i; - if (file_exists($log_file)) { - $cleared = unlink($log_file) && $cleared; + if ( file_exists( $log_file ) ) { + $cleared = unlink( $log_file ) && $cleared; } } @@ -246,17 +235,16 @@ public function clear_logs() * * @return array Array of log file paths. */ - public function get_log_files() - { + public function get_log_files() { $files = array(); - if (file_exists($this->log_file_path)) { + if ( file_exists( $this->log_file_path ) ) { $files[] = $this->log_file_path; } - for ($i = 1; $i <= self::MAX_LOG_FILES; $i++) { + for ( $i = 1; $i <= self::MAX_LOG_FILES; $i++ ) { $log_file = $this->log_file_path . '.' . $i; - if (file_exists($log_file)) { + if ( file_exists( $log_file ) ) { $files[] = $log_file; } } diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 078be44..8c9c05f 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -319,7 +319,7 @@ public function blocking_compress_on_upload( $metadata, $attachment_id ) { Tiny_Logger::debug('blocking compress on upload', array( 'image_id' => $attachment_id, )); - + $result = $tiny_image->compress(); return $tiny_image->get_wp_metadata(); } else { @@ -489,7 +489,7 @@ public function compress_image_from_library() { Tiny_Logger::debug('compress from library', array( 'image_id' => $id, )); - + $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); $result = $tiny_image->compress(); @@ -522,7 +522,7 @@ public function compress_image_for_bulk() { $size_before = $image_statistics_before['compressed_total_size']; $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); - + Tiny_Logger::debug('compress from bulk', array( 'image_id' => $id, )); diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index a0678cb..4793a6a 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -30,7 +30,7 @@ class Tiny_Settings extends Tiny_WP_Base { public function __construct() { parent::__construct(); $this->notices = new Tiny_Notices(); - new Tiny_Diagnostics($this); + new Tiny_Diagnostics( $this ); } private function init_compressor() { diff --git a/src/views/settings-diagnostics.php b/src/views/settings-diagnostics.php index 2638e7e..4a678a3 100644 --- a/src/views/settings-diagnostics.php +++ b/src/views/settings-diagnostics.php @@ -1,33 +1,33 @@ - - - - + + - - -
-

- +

+

+ -

-

- > - -

-
- - -
-
\ No newline at end of file + 'tiny-compress-images' + ) ?> +

+

+ > + +

+
+ + +
+ + + + diff --git a/src/views/settings.php b/src/views/settings.php index 0deb0e4..df06cb9 100644 --- a/src/views/settings.php +++ b/src/views/settings.php @@ -60,7 +60,7 @@ - +

diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index 184b57a..debe8aa 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -1,16 +1,19 @@ values = array( + public function __construct() + { + $this->values = array( 'thumbnail_size_w' => 150, 'thumbnail_size_h' => 150, 'medium_size_w' => 300, @@ -19,26 +22,29 @@ public function __construct() { 'medium_large_size_h' => 0, 'large_size_w' => 1024, 'large_size_h' => 1024, - ); + ); } - public function set( $key, $value ) { - if ( preg_match( '#^(.+)\[(.+)\]$#', $key, $match ) ) { - if ( ! isset( $this->values[ $match[1] ] ) ) { - $this->values[ $match[1] ] = array(); + public function set($key, $value) + { + if (preg_match('#^(.+)\[(.+)\]$#', $key, $match)) { + if (! isset($this->values[$match[1]])) { + $this->values[$match[1]] = array(); } - $this->values[ $match[1] ][ $match[2] ] = $value; + $this->values[$match[1]][$match[2]] = $value; } else { - $this->values[ $key ] = $value; + $this->values[$key] = $value; } } - public function get( $key, $default = null ) { - return isset( $this->values[ $key ] ) ? $this->values[ $key ] : $default; + public function get($key, $default = null) + { + return isset($this->values[$key]) ? $this->values[$key] : $default; } } -class WordPressStubs { +class WordPressStubs +{ const UPLOAD_DIR = 'wp-content/uploads'; private $vfs; @@ -50,46 +56,60 @@ class WordPressStubs { private $stubs; private $filters; - public function __construct( $vfs ) { + public function __construct($vfs) + { $GLOBALS['wp'] = $this; $this->vfs = $vfs; - $this->addMethod( 'add_action' ); - $this->addMethod( 'do_action' ); - $this->addMethod( 'add_filter' ); - $this->addMethod( 'apply_filters' ); - $this->addMethod( 'register_setting' ); - $this->addMethod( 'add_settings_section' ); - $this->addMethod( 'add_settings_field' ); - $this->addMethod( 'get_option' ); - $this->addMethod( 'get_site_option' ); - $this->addMethod( 'update_site_option' ); - $this->addMethod( 'get_post_meta' ); - $this->addMethod( 'update_post_meta' ); - $this->addMethod( 'get_intermediate_image_sizes' ); - $this->addMethod( 'add_image_size' ); - $this->addMethod( 'translate' ); - $this->addMethod( 'load_plugin_textdomain' ); - $this->addMethod( 'get_post_mime_type' ); - $this->addMethod( 'get_plugin_data' ); - $this->addMethod( 'wp_upload_dir' ); - $this->addMethod( 'get_site_url' ); - $this->addMethod( 'plugin_basename' ); - $this->addMethod( 'is_multisite' ); - $this->addMethod( 'current_user_can' ); - $this->addMethod( 'wp_get_attachment_metadata' ); - $this->addMethod( 'is_admin' ); - $this->addMethod( 'is_customize_preview' ); - $this->addMethod( 'is_plugin_active' ); + $this->addMethod('add_action'); + $this->addMethod('do_action'); + $this->addMethod('add_filter'); + $this->addMethod('apply_filters'); + $this->addMethod('register_setting'); + $this->addMethod('add_settings_section'); + $this->addMethod('add_settings_field'); + $this->addMethod('get_option'); + $this->addMethod('get_site_option'); + $this->addMethod('update_site_option'); + $this->addMethod('get_post_meta'); + $this->addMethod('update_post_meta'); + $this->addMethod('get_intermediate_image_sizes'); + $this->addMethod('add_image_size'); + $this->addMethod('translate'); + $this->addMethod('load_plugin_textdomain'); + $this->addMethod('get_post_mime_type'); + $this->addMethod('get_plugin_data'); + $this->addMethod('wp_upload_dir'); + $this->addMethod('get_site_url'); + $this->addMethod('plugin_basename'); + $this->addMethod('is_multisite'); + $this->addMethod('current_user_can'); + $this->addMethod('wp_get_attachment_metadata'); + $this->addMethod('is_admin'); + $this->addMethod('is_customize_preview'); + $this->addMethod('is_plugin_active'); + $this->addMethod('trailingslashit'); $this->defaults(); $this->create_filesystem(); } - public function create_filesystem() { - vfsStream::newDirectory( self::UPLOAD_DIR ) - ->at( $this->vfs ); + /** + * Mocked function for https://developer.wordpress.org/reference/functions/trailingslashit/ + * + * @return void + */ + public function trailingslashit($value) + { + return $value; + } + + public function create_filesystem() + { + vfsStream::newDirectory(self::UPLOAD_DIR) + ->at($this->vfs); } - public function defaults() { + public function defaults() + { $this->initFunctions = array(); $this->admin_initFunctions = array(); $this->options = new WordPressOptions(); @@ -98,49 +118,50 @@ public function defaults() { $GLOBALS['_wp_additional_image_sizes'] = array(); } - public function call( $method, $args ) { - $this->calls[ $method ][] = $args; - if ( 'add_action' === $method ) { - if ( 'init' === $args[0] ) { + public function call($method, $args) + { + $this->calls[$method][] = $args; + if ('add_action' === $method) { + if ('init' === $args[0]) { $this->initFunctions[] = $args[1]; - } elseif ( 'admin_init' === $args[0] ) { + } elseif ('admin_init' === $args[0]) { $this->admin_initFunctions[] = $args[1]; } } // Allow explicit stubs to override defaults/behaviors - if ( isset( $this->stubs[ $method ] ) && $this->stubs[ $method ] ) { - return call_user_func_array( $this->stubs[ $method ], $args ); + if (isset($this->stubs[$method]) && $this->stubs[$method]) { + return call_user_func_array($this->stubs[$method], $args); } - if ( 'add_filter' === $method ) { - $tag = isset( $args[0] ) ? $args[0] : ''; - $function_to_add = isset( $args[1] ) ? $args[1] : ''; - $priority = isset( $args[2] ) ? intval( $args[2] ) : 10; - $accepted_args = isset( $args[3] ) ? intval( $args[3] ) : 1; - if ( ! isset( $this->filters[ $tag ] ) ) { - $this->filters[ $tag ] = array(); + if ('add_filter' === $method) { + $tag = isset($args[0]) ? $args[0] : ''; + $function_to_add = isset($args[1]) ? $args[1] : ''; + $priority = isset($args[2]) ? intval($args[2]) : 10; + $accepted_args = isset($args[3]) ? intval($args[3]) : 1; + if (! isset($this->filters[$tag])) { + $this->filters[$tag] = array(); } - if ( ! isset( $this->filters[ $tag ][ $priority ] ) ) { - $this->filters[ $tag ][ $priority ] = array(); + if (! isset($this->filters[$tag][$priority])) { + $this->filters[$tag][$priority] = array(); } - $this->filters[ $tag ][ $priority ][] = array( + $this->filters[$tag][$priority][] = array( 'function' => $function_to_add, 'accepted_args' => $accepted_args, ); return true; } - if ( 'apply_filters' === $method ) { - $tag = isset( $args[0] ) ? $args[0] : ''; + if ('apply_filters' === $method) { + $tag = isset($args[0]) ? $args[0] : ''; // $value is the first value passed to filters - $value = isset( $args[1] ) ? $args[1] : null; - $call_args = array_slice( $args, 1 ); - if ( isset( $this->filters[ $tag ] ) ) { - $priorities = array_keys( $this->filters[ $tag ] ); - sort( $priorities, SORT_NUMERIC ); - foreach ( $priorities as $priority ) { - foreach ( $this->filters[ $tag ][ $priority ] as $callback ) { - $accepted = max( 1, intval( $callback['accepted_args'] ) ); - $args_to_pass = array_slice( $call_args, 0, $accepted ); - $returned = call_user_func_array( $callback['function'], $args_to_pass ); + $value = isset($args[1]) ? $args[1] : null; + $call_args = array_slice($args, 1); + if (isset($this->filters[$tag])) { + $priorities = array_keys($this->filters[$tag]); + sort($priorities, SORT_NUMERIC); + foreach ($priorities as $priority) { + foreach ($this->filters[$tag][$priority] as $callback) { + $accepted = max(1, intval($callback['accepted_args'])); + $args_to_pass = array_slice($call_args, 0, $accepted); + $returned = call_user_func_array($callback['function'], $args_to_pass); // Filters should return the (possibly modified) value as first argument. $call_args[0] = $returned; } @@ -148,116 +169,130 @@ public function call( $method, $args ) { } return $call_args[0]; } - if ( 'translate' === $method ) { + if ('translate' === $method) { return $args[0]; - } elseif ( 'get_option' === $method ) { - return call_user_func_array( array( $this->options, 'get' ), $args ); - } elseif ( 'get_post_meta' === $method ) { - return call_user_func_array( array( $this, 'getMetadata' ), $args ); - } elseif ( 'add_image_size' === $method ) { - return call_user_func_array( array( $this, 'addImageSize' ), $args ); - } elseif ( 'update_post_meta' === $method ) { - return call_user_func_array( array( $this, 'updateMetadata' ), $args ); - } elseif ( 'get_intermediate_image_sizes' === $method ) { - return array_merge( array( 'thumbnail', 'medium', 'medium_large', 'large' ), array_keys( $GLOBALS['_wp_additional_image_sizes'] ) ); - } elseif ( 'get_plugin_data' === $method ) { - return array( 'Version' => '1.7.2' ); - } elseif ( 'plugin_basename' === $method ) { + } elseif ('get_option' === $method) { + return call_user_func_array(array($this->options, 'get'), $args); + } elseif ('get_post_meta' === $method) { + return call_user_func_array(array($this, 'getMetadata'), $args); + } elseif ('add_image_size' === $method) { + return call_user_func_array(array($this, 'addImageSize'), $args); + } elseif ('update_post_meta' === $method) { + return call_user_func_array(array($this, 'updateMetadata'), $args); + } elseif ('get_intermediate_image_sizes' === $method) { + return array_merge(array('thumbnail', 'medium', 'medium_large', 'large'), array_keys($GLOBALS['_wp_additional_image_sizes'])); + } elseif ('get_plugin_data' === $method) { + return array('Version' => '1.7.2'); + } elseif ('plugin_basename' === $method) { return 'tiny-compress-images'; - } elseif ( 'wp_upload_dir' === $method ) { - return array( 'basedir' => $this->vfs->url() . '/' . self::UPLOAD_DIR, 'baseurl' => '/' . self::UPLOAD_DIR ); - } elseif ( 'is_admin' === $method ) { + } elseif ('wp_upload_dir' === $method) { + return array('basedir' => $this->vfs->url() . '/' . self::UPLOAD_DIR, 'baseurl' => '/' . self::UPLOAD_DIR); + } elseif ('is_admin' === $method) { return true; } } - public function addMethod( $method ) { - $this->calls[ $method ] = array(); - $this->stubs[ $method ] = array(); - if ( ! function_exists( $method ) ) { - eval( "function $method() { return \$GLOBALS['wp']->call('$method', func_get_args()); }" ); + public function addMethod($method) + { + $this->calls[$method] = array(); + $this->stubs[$method] = array(); + if (! function_exists($method)) { + eval("function $method() { return \$GLOBALS['wp']->call('$method', func_get_args()); }"); } } - public function addOption( $key, $value ) { - $this->options->set( $key, $value ); + public function addOption($key, $value) + { + $this->options->set($key, $value); } - public function addImageSize( $size, $values ) { - $GLOBALS['_wp_additional_image_sizes'][ $size ] = $values; + public function addImageSize($size, $values) + { + $GLOBALS['_wp_additional_image_sizes'][$size] = $values; } - public function getMetadata( $id, $key, $single = false ) { - $values = isset( $this->metadata[ $id ] ) ? $this->metadata[ $id ] : array(); - $value = isset( $values[ $key ] ) ? $values[ $key ] : ''; - return $single ? $value : array( $value ); + public function getMetadata($id, $key, $single = false) + { + $values = isset($this->metadata[$id]) ? $this->metadata[$id] : array(); + $value = isset($values[$key]) ? $values[$key] : ''; + return $single ? $value : array($value); } - public function updateMetadata( $id, $key, $values ) { - $this->metadata[ $id ][ $key ] = $values; + public function updateMetadata($id, $key, $values) + { + $this->metadata[$id][$key] = $values; } - public function setTinyMetadata( $id, $values ) { - $this->metadata[ $id ] = array( Tiny_Config::META_KEY => $values ); + public function setTinyMetadata($id, $values) + { + $this->metadata[$id] = array(Tiny_Config::META_KEY => $values); } - public function getCalls( $method ) { - return $this->calls[ $method ]; + public function getCalls($method) + { + return $this->calls[$method]; } - public function init() { - foreach ( $this->initFunctions as $func ) { - call_user_func( $func ); + public function init() + { + foreach ($this->initFunctions as $func) { + call_user_func($func); } } - public function admin_init() { - foreach ( $this->admin_initFunctions as $func ) { - call_user_func( $func ); + public function admin_init() + { + foreach ($this->admin_initFunctions as $func) { + call_user_func($func); } } - public function stub( $method, $func ) { - $this->stubs[ $method ] = $func; + public function stub($method, $func) + { + $this->stubs[$method] = $func; } - public function createImage( $file_size, $path, $name ) { - if ( ! $this->vfs->hasChild( self::UPLOAD_DIR . "/$path" ) ) { - vfsStream::newDirectory( self::UPLOAD_DIR . "/$path" )->at( $this->vfs ); + public function createImage($file_size, $path, $name) + { + if (! $this->vfs->hasChild(self::UPLOAD_DIR . "/$path")) { + vfsStream::newDirectory(self::UPLOAD_DIR . "/$path")->at($this->vfs); } - $dir = $this->vfs->getChild( self::UPLOAD_DIR . "/$path" ); + $dir = $this->vfs->getChild(self::UPLOAD_DIR . "/$path"); - vfsStream::newFile( $name ) - ->withContent( new LargeFileContent( $file_size ) ) - ->at( $dir ); + vfsStream::newFile($name) + ->withContent(new LargeFileContent($file_size)) + ->at($dir); } - public function createImages( $sizes = null, $original_size = 12345, $path = '14/01', $name = 'test' ) { - vfsStream::newDirectory( self::UPLOAD_DIR . "/$path" )->at( $this->vfs ); - $dir = $this->vfs->getChild( self::UPLOAD_DIR . '/' . $path ); + public function createImages($sizes = null, $original_size = 12345, $path = '14/01', $name = 'test') + { + vfsStream::newDirectory(self::UPLOAD_DIR . "/$path")->at($this->vfs); + $dir = $this->vfs->getChild(self::UPLOAD_DIR . '/' . $path); - vfsStream::newFile( "$name.png" ) - ->withContent( new LargeFileContent( $original_size ) ) - ->at( $dir ); + vfsStream::newFile("$name.png") + ->withContent(new LargeFileContent($original_size)) + ->at($dir); - if ( is_null( $sizes ) ) { - $sizes = array( 'thumbnail' => 100, 'medium' => 1000 , 'large' => 10000, 'post-thumbnail' => 1234 ); + if (is_null($sizes)) { + $sizes = array('thumbnail' => 100, 'medium' => 1000, 'large' => 10000, 'post-thumbnail' => 1234); } - foreach ( $sizes as $key => $size ) { - vfsStream::newFile( "$name-$key.png" ) - ->withContent( new LargeFileContent( $size ) ) - ->at( $dir ); + foreach ($sizes as $key => $size) { + vfsStream::newFile("$name-$key.png") + ->withContent(new LargeFileContent($size)) + ->at($dir); } } - public function createImagesFromJSON( $virtual_images ) { - foreach ( $virtual_images['images'] as $image ) { - self::createImage( $image['size'], $virtual_images['path'], $image['file'] ); + public function createImagesFromJSON($virtual_images) + { + foreach ($virtual_images['images'] as $image) { + self::createImage($image['size'], $virtual_images['path'], $image['file']); } } - public function getTestMetadata( $path = '14/01', $name = 'test' ) { + public function getTestMetadata($path = '14/01', $name = 'test') + { $metadata = array( 'file' => "$path/$name.png", 'width' => 4000, @@ -265,29 +300,73 @@ public function getTestMetadata( $path = '14/01', $name = 'test' ) { 'sizes' => array(), ); - $regex = '#^' . preg_quote( $name ) . '-([^.]+)[.](png|jpe?g)$#'; - $dir = $this->vfs->getChild( self::UPLOAD_DIR . "/$path" ); - foreach ( $dir->getChildren() as $child ) { + $regex = '#^' . preg_quote($name) . '-([^.]+)[.](png|jpe?g)$#'; + $dir = $this->vfs->getChild(self::UPLOAD_DIR . "/$path"); + foreach ($dir->getChildren() as $child) { $file = $child->getName(); - if ( preg_match( $regex, $file, $match ) ) { - $metadata['sizes'][ $match[1] ] = array( 'file' => $file ); + if (preg_match($regex, $file, $match)) { + $metadata['sizes'][$match[1]] = array('file' => $file); } } return $metadata; } + + /** + * Testhelper to easily assert if a hook has been invoked + * + * @param string $hookname name of the filter or action + * @param mixed $expected_args arguments to the hook + */ + public static function assertHook($hookname, $expected_args = null) + { + $hooks = array('add_action', 'add_filter'); + $found = false; + + foreach ($hooks as $method) { + if (! isset($GLOBALS['wp'])) { + break; + } + + foreach ($GLOBALS['wp']->getCalls($method) as $call) { + if (! isset($call[0]) || $call[0] !== $hookname) { + continue; + } + + if (is_null($expected_args)) { + $found = true; + break 2; + } + + if ($expected_args === array_slice($call, 1)[0]) { + $found = true; + break 2; + } + } + } + + $message = is_null($expected_args) + ? sprintf('Expected hook "%s" to be called.', $hookname) + : sprintf('Expected hook "%s" to be called with the given arguments.', $hookname); + + Assert::assertTrue($found, $message); + } } -class WP_HTTP_Proxy { - public function is_enabled() { +class WP_HTTP_Proxy +{ + public function is_enabled() + { return false; } } -function __( $text, $domain = 'default' ) { - return translate( $text, $domain ); +function __($text, $domain = 'default') +{ + return translate($text, $domain); } -function esc_html__( $text, $domain = 'default' ) { - return translate( $text, $domain ); -} \ No newline at end of file +function esc_html__($text, $domain = 'default') +{ + return translate($text, $domain); +} From 178d63f8b360ad85e7d3009188f240fe7ed19d8e Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 19:09:29 +0100 Subject: [PATCH 10/23] Add initial tests --- test/unit/TinyDiagnosticsTest.php | 7 +++++++ test/unit/TinyLoggerTest.php | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/test/unit/TinyDiagnosticsTest.php b/test/unit/TinyDiagnosticsTest.php index c9db2ed..ec4bb1a 100644 --- a/test/unit/TinyDiagnosticsTest.php +++ b/test/unit/TinyDiagnosticsTest.php @@ -8,4 +8,11 @@ public function set_up() { parent::set_up(); } + + public function test_adds_ajax_action_to_download_diagnostics() { + $tiny_settings = new Tiny_Settings(); + $tiny_diagnostics = new Tiny_Diagnostics($tiny_settings); + + WordPressStubs::assertHook('wp_ajax_tiny_download_diagnostics', array($tiny_diagnostics, 'download_diagnostics')); + } } diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php index fda9c86..b55a59e 100644 --- a/test/unit/TinyLoggerTest.php +++ b/test/unit/TinyLoggerTest.php @@ -1,5 +1,7 @@ Date: Mon, 5 Jan 2026 19:23:45 +0100 Subject: [PATCH 11/23] On constructor log enabled test --- src/class-tiny-logger.php | 30 ++++++++++++++++++++++++++++++ test/unit/TinyLoggerTest.php | 14 ++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 0f833de..6a34f28 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -25,6 +25,7 @@ */ class Tiny_Logger { + const LOG_LEVEL_ERROR = 'error'; const LOG_LEVEL_DEBUG = 'debug'; @@ -60,10 +61,39 @@ private function __construct() { $this->log_file_path = $this->get_log_file_path(); } + /** + * Initializes the logger by registering WordPress hooks. + * + * This method hooks into 'pre_update_option_tinypng_logging_enabled' to + * intercept and process logging settings before they are saved to the database. + * + * @since 3.7.0 + * + * @return void + */ public static function init() { add_filter( 'pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled', 10, 3 ); } + /** + * Resets the singleton instance. + * Used primarily for unit testing. + */ + public static function reset() { + self::$instance = null; + } + + /** + * Retrieves whether logging is currently enabled. + * + * @since 3.7.0 + * + * @return bool True if logging is enabled, false otherwise. + */ + public function get_log_enabled() { + return $this->log_enabled; + } + /** * Triggered when log_enabled is saved * - set the setting on the instance diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php index b55a59e..4482a6d 100644 --- a/test/unit/TinyLoggerTest.php +++ b/test/unit/TinyLoggerTest.php @@ -1,6 +1,7 @@ reset(); + } + public function test_logger_always_has_one_instance() { $instance1 = Tiny_Logger::get_instance(); $instance2 = Tiny_Logger::get_instance(); assertEquals($instance1, $instance2, 'logger should be a singleton'); } + + public function test_log_enabled_when_option_is_on() { + $this->wp->addOption('tinypng_logging_enabled', 'on'); + $logger = Tiny_Logger::get_instance(); + assertTrue($logger->get_log_enabled(), 'log should be enabled when tinypng_logging_enabled is on'); + } } From 2cfabf1a444a8cd9b5037f474d69c97472919004 Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 19:24:54 +0100 Subject: [PATCH 12/23] Manual formatting logger --- src/class-tiny-logger.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 6a34f28..33119fa 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -72,7 +72,9 @@ private function __construct() { * @return void */ public static function init() { - add_filter( 'pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled', 10, 3 ); + add_filter( + 'pre_update_option_tinypng_logging_enabled', + 'Tiny_Logger::on_save_log_enabled', 10, 3 ); } /** @@ -103,7 +105,7 @@ public function get_log_enabled() { * @since 3.7.0 */ public static function on_save_log_enabled( $log_enabled, $old, $option ) { - if ( $log_enabled !== 'on' ) { + if ( 'on' !== $log_enabled ) { $instance = self::get_instance(); $instance->clear_logs(); } @@ -221,7 +223,7 @@ private function rotate_logs() { $new_file = $this->log_file_path . '.' . ($i + 1); if ( file_exists( $old_file ) ) { - if ( $i === self::MAX_LOG_FILES - 1 ) { + if ( self::MAX_LOG_FILES - 1 === $i ) { unlink( $old_file ); } else { rename( $old_file, $new_file ); From f54b12dee70d216a43260d4fe6d55263144603a7 Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 19:27:27 +0100 Subject: [PATCH 13/23] Manual formatting fixes in diagnostics --- src/class-tiny-diagnostics.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php index 989e93b..29093c7 100644 --- a/src/class-tiny-diagnostics.php +++ b/src/class-tiny-diagnostics.php @@ -109,14 +109,18 @@ private static function get_server_info() { return array( 'php_version' => phpversion(), - 'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown', + 'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? + sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : + 'Unknown', 'mysql_version' => $wpdb->db_version(), 'max_execution_time' => ini_get( 'max_execution_time' ), 'memory_limit' => ini_get( 'memory_limit' ), 'post_max_size' => ini_get( 'post_max_size' ), 'upload_max_filesize' => ini_get( 'upload_max_filesize' ), 'max_input_vars' => ini_get( 'max_input_vars' ), - 'curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : 'Not available', + 'curl_version' => function_exists( 'curl_version' ) ? + curl_version()['version'] : + 'Not available', 'disabled_functions' => ini_get( 'disable_functions' ), ); } @@ -179,7 +183,12 @@ public function download_diagnostics() { */ public function create_diagnostic_zip() { if ( ! class_exists( 'ZipArchive' ) ) { - return new WP_Error( 'zip_not_available', __( 'ZipArchive class is not available on this server.', 'tiny-compress-images' ) ); + return new WP_Error( + 'zip_not_available', + __( 'ZipArchive class is not available on this server.', + 'tiny-compress-images' + ) + ); } $upload_dir = wp_upload_dir(); @@ -194,7 +203,10 @@ public function create_diagnostic_zip() { $zip = new ZipArchive(); if ( true !== $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { - return new WP_Error( 'zip_create_failed', __( 'Failed to create zip file.', 'tiny-compress-images' ) ); + return new WP_Error( 'zip_create_failed', + __( 'Failed to create zip file.', + 'tiny-compress-images' ) + ); } // Add diagnostic info. From 9adff724e434ac180e2ec6ac1582bab5d5c75742 Mon Sep 17 00:00:00 2001 From: tijmen Date: Mon, 5 Jan 2026 20:42:39 +0100 Subject: [PATCH 14/23] chore: add tests for log path --- src/class-tiny-diagnostics.php | 1 - src/class-tiny-image.php | 28 ++++++++--------- src/class-tiny-logger.php | 49 +++++++++++++++-------------- test/helpers/wordpress.php | 25 +++++++++------ test/unit/TinyLoggerTest.php | 7 ++++- test/unit/TinySettingsAdminTest.php | 1 + test/unit/TinySettingsAjaxTest.php | 40 +++++++++-------------- 7 files changed, 76 insertions(+), 75 deletions(-) diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php index 29093c7..b760bf5 100644 --- a/src/class-tiny-diagnostics.php +++ b/src/class-tiny-diagnostics.php @@ -193,7 +193,6 @@ public function create_diagnostic_zip() { $upload_dir = wp_upload_dir(); $temp_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-temp'; - if ( ! file_exists( $temp_dir ) ) { wp_mkdir_p( $temp_dir ); } diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index a77ab38..22b4740 100644 --- a/src/class-tiny-image.php +++ b/src/class-tiny-image.php @@ -211,25 +211,25 @@ public function compress() { $convert_to = $this->convert_to(); foreach ( $unprocessed_sizes as $size_name => $size ) { - Tiny_Logger::debug('compress size', array( - 'image_id' => $this->id, - 'size' => $size_name, - 'resize' => $resize, - 'preserve' => $preserve, - 'convert' => $convert_to, - 'modified' => $size->modified(), - 'filename' => $size->filename, - 'is_duplicate' => $size->is_duplicate(), - 'exists' => $size->exists(), - 'has_been_compressed' => $size->has_been_compressed(), - 'filesize' => $size->filesize(), - 'mimetype' => $size->mimetype(), - )); if ( ! $size->is_duplicate() ) { $size->add_tiny_meta_start(); $this->update_tiny_post_meta(); $resize = $this->settings->get_resize_options( $size_name ); $preserve = $this->settings->get_preserve_options( $size_name ); + Tiny_Logger::debug('compress size', array( + 'image_id' => $this->id, + 'size' => $size_name, + 'resize' => $resize, + 'preserve' => $preserve, + 'convert' => $convert_to, + 'modified' => $size->modified(), + 'filename' => $size->filename, + 'is_duplicate' => $size->is_duplicate(), + 'exists' => $size->exists(), + 'has_been_compressed' => $size->has_been_compressed(), + 'filesize' => $size->filesize(), + 'mimetype' => $size->mimetype(), + )); try { $response = $compressor->compress_file( $size->filename, diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 33119fa..8bb2bd5 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -18,8 +18,10 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ + /** * Handles logging of plugin events to file. + * * * @since 3.7.0 */ @@ -33,7 +35,8 @@ class Tiny_Logger { const MAX_LOG_FILES = 3; private static $instance = null; - private $log_enabled = false; + + private $log_enabled = null; private $log_file_path = null; /** @@ -53,12 +56,10 @@ public static function get_instance() { /** * Constructor. - * - * @since 3.7.0 + */ private function __construct() { - $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); - $this->log_file_path = $this->get_log_file_path(); + $this->log_file_path = $this->resolve_log_file_path(); } /** @@ -67,8 +68,6 @@ private function __construct() { * This method hooks into 'pre_update_option_tinypng_logging_enabled' to * intercept and process logging settings before they are saved to the database. * - * @since 3.7.0 - * * @return void */ public static function init() { @@ -88,14 +87,26 @@ public static function reset() { /** * Retrieves whether logging is currently enabled. * - * @since 3.7.0 - * * @return bool True if logging is enabled, false otherwise. */ public function get_log_enabled() { + if ( null === $this->log_enabled) { + $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); + } + return $this->log_enabled; } + /** + * Retrieves the absolute filesystem path to the log file. + * + * @return string The full filesystem path to the tiny-compress.log file. + */ + public function get_log_file_path() + { + return $this->log_file_path; + } + /** * Triggered when log_enabled is saved * - set the setting on the instance @@ -114,28 +125,18 @@ public static function on_save_log_enabled( $log_enabled, $old, $option ) { } /** - * Gets the log file path. + * Retrieves the log path using wp_upload_dir. This operation + * should only be used internally. Use the getter to get the + * memoized function. * * @since 3.7.0 * * @return string The log file path. */ - private function get_log_file_path() { + private function resolve_log_file_path() { $upload_dir = wp_upload_dir(); $log_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-logs'; - - return trailingslashit( $log_dir ) . 'tiny-compress.log'; - } - - /** - * Gets the log directory path. - * - * @since 3.7.0 - * - * @return string The log directory path. - */ - public function get_log_dir() { - return dirname( $this->log_file_path ); + return trailingslashit($log_dir) . 'tiny-compress.log'; } /** diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index debe8aa..f186163 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -92,16 +92,6 @@ public function __construct($vfs) $this->create_filesystem(); } - /** - * Mocked function for https://developer.wordpress.org/reference/functions/trailingslashit/ - * - * @return void - */ - public function trailingslashit($value) - { - return $value; - } - public function create_filesystem() { vfsStream::newDirectory(self::UPLOAD_DIR) @@ -120,6 +110,7 @@ public function defaults() public function call($method, $args) { + $mocks = new WordPressMocks(); $this->calls[$method][] = $args; if ('add_action' === $method) { if ('init' === $args[0]) { @@ -189,6 +180,8 @@ public function call($method, $args) return array('basedir' => $this->vfs->url() . '/' . self::UPLOAD_DIR, 'baseurl' => '/' . self::UPLOAD_DIR); } elseif ('is_admin' === $method) { return true; + } elseif (method_exists($mocks, $method)) { + return $mocks->$method($args[0]); } } @@ -353,6 +346,18 @@ public static function assertHook($hookname, $expected_args = null) } } +class WordPressMocks { + /** + * Mocked function for https://developer.wordpress.org/reference/functions/trailingslashit/ + * + * @return void + */ + public function trailingslashit($value) + { + return $value . '/'; + } +} + class WP_HTTP_Proxy { public function is_enabled() diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php index 4482a6d..5932bf0 100644 --- a/test/unit/TinyLoggerTest.php +++ b/test/unit/TinyLoggerTest.php @@ -26,9 +26,14 @@ public function test_logger_always_has_one_instance() assertEquals($instance1, $instance2, 'logger should be a singleton'); } - public function test_log_enabled_when_option_is_on() { + public function test_get_log_enabled_memoizes_log_enabled() { $this->wp->addOption('tinypng_logging_enabled', 'on'); $logger = Tiny_Logger::get_instance(); assertTrue($logger->get_log_enabled(), 'log should be enabled when tinypng_logging_enabled is on'); } + + public function test_sets_log_path_on_construct() { + $logger = Tiny_Logger::get_instance(); + assertEquals($logger->get_log_file_path(), 'vfs://root/wp-content/uploads/tiny-compress-logs/tiny-compress.log'); + } } diff --git a/test/unit/TinySettingsAdminTest.php b/test/unit/TinySettingsAdminTest.php index 0fdbf7f..d5577aa 100644 --- a/test/unit/TinySettingsAdminTest.php +++ b/test/unit/TinySettingsAdminTest.php @@ -20,6 +20,7 @@ public function test_admin_init_should_register_keys() { array( 'tinify', 'tinypng_resize_original' ), array( 'tinify', 'tinypng_preserve_data' ), array( 'tinify', 'tinypng_convert_format' ), + array( 'tinify', 'tinypng_logging_enabled' ), ), $this->wp->getCalls( 'register_setting' )); } diff --git a/test/unit/TinySettingsAjaxTest.php b/test/unit/TinySettingsAjaxTest.php index 4501fd0..40fb44b 100644 --- a/test/unit/TinySettingsAjaxTest.php +++ b/test/unit/TinySettingsAjaxTest.php @@ -3,37 +3,27 @@ require_once dirname( __FILE__ ) . '/TinyTestCase.php'; class Tiny_Settings_Ajax_Test extends Tiny_TestCase { - protected $subject; protected $notices; public function set_up() { parent::set_up(); - $this->subject = new Tiny_Settings(); - $this->notices = new Tiny_Notices(); - $this->subject->ajax_init(); } + + public function test_settings_ajax_init() { + $tiny_settings = new Tiny_Settings(); + $tiny_settings->ajax_init(); + + WordPressStubs::assertHook('wp_ajax_tiny_image_sizes_notice', array( $tiny_settings, 'image_sizes_notice' )); + WordPressStubs::assertHook('wp_ajax_tiny_account_status', array( $tiny_settings, 'account_status' )); + WordPressStubs::assertHook('wp_ajax_tiny_settings_create_api_key', array( $tiny_settings, 'create_api_key' )); + WordPressStubs::assertHook('wp_ajax_tiny_settings_update_api_key', array( $tiny_settings, 'update_api_key' )); + } + + public function test_notices_ajax_init() { + $tiny_notices = new Tiny_Notices(); + $tiny_notices->ajax_init(); - public function test_ajax_init_should_add_actions() { - $this->assertEquals(array( - array( 'init', array( $this->subject, 'init' ) ), - array( 'rest_api_init', array( $this->subject, 'rest_init' ) ), - array( 'admin_init', array( $this->subject, 'admin_init' ) ), - array( 'admin_menu', array( $this->subject, 'admin_menu' ) ), - array( 'init', array( $this->notices, 'init' ) ), - array( 'rest_api_init', array( $this->notices, 'rest_init' ) ), - array( 'admin_init', array( $this->notices, 'admin_init' ) ), - array( 'admin_menu', array( $this->notices, 'admin_menu' ) ), - array( 'init', array( $this->notices, 'init' ) ), - array( 'rest_api_init', array( $this->notices, 'rest_init' ) ), - array( 'admin_init', array( $this->notices, 'admin_init' ) ), - array( 'admin_menu', array( $this->notices, 'admin_menu' ) ), - array( 'wp_ajax_tiny_image_sizes_notice', array( $this->subject, 'image_sizes_notice' ) ), - array( 'wp_ajax_tiny_account_status', array( $this->subject, 'account_status' ) ), - array( 'wp_ajax_tiny_settings_create_api_key', array( $this->subject, 'create_api_key' ) ), - array( 'wp_ajax_tiny_settings_update_api_key', array( $this->subject, 'update_api_key' ) ), - ), - $this->wp->getCalls( 'add_action' ) - ); + WordPressStubs::assertHook('wp_ajax_tiny_dismiss_notice', array( $tiny_notices, 'dismiss' )); } } From 76ce4cbc4a65aaa90f47a608904378b33e5b4499 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Jan 2026 16:08:34 +0100 Subject: [PATCH 15/23] chore: simplify rotation --- src/class-tiny-logger.php | 90 ++++++++---------------------------- test/helpers/wordpress.php | 21 +++++++++ test/unit/TinyLoggerTest.php | 81 +++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 73 deletions(-) diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 8bb2bd5..7769205 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -32,7 +32,6 @@ class Tiny_Logger { const LOG_LEVEL_DEBUG = 'debug'; const MAX_LOG_SIZE = 5242880; // 5MB - const MAX_LOG_FILES = 3; private static $instance = null; @@ -43,8 +42,6 @@ class Tiny_Logger { * To log on various places easily, we create a singleton * to prevent passing around the instance. * - * @since 3.7.0 - * * @return Tiny_Logger The logger instance. */ public static function get_instance() { @@ -60,6 +57,7 @@ public static function get_instance() { */ private function __construct() { $this->log_file_path = $this->resolve_log_file_path(); + $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); } /** @@ -90,10 +88,6 @@ public static function reset() { * @return bool True if logging is enabled, false otherwise. */ public function get_log_enabled() { - if ( null === $this->log_enabled) { - $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); - } - return $this->log_enabled; } @@ -112,12 +106,12 @@ public function get_log_file_path() * - set the setting on the instance * - if turn off, clear the logs * - if turned on, check if we can create the log file - * - * @since 3.7.0 */ public static function on_save_log_enabled( $log_enabled, $old, $option ) { + $instance = self::get_instance(); + $instance->log_enabled = 'on' === $log_enabled; + if ( 'on' !== $log_enabled ) { - $instance = self::get_instance(); $instance->clear_logs(); } @@ -129,8 +123,6 @@ public static function on_save_log_enabled( $log_enabled, $old, $option ) { * should only be used internally. Use the getter to get the * memoized function. * - * @since 3.7.0 - * * @return string The log file path. */ private function resolve_log_file_path() { @@ -139,22 +131,9 @@ private function resolve_log_file_path() { return trailingslashit($log_dir) . 'tiny-compress.log'; } - /** - * Checks if logging is enabled. - * - * @since 3.7.0 - * - * @return bool True if logging is enabled. - */ - public function is_enabled() { - return $this->log_enabled; - } - /** * Logs an error message. * - * @since 3.7.0 - * * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. */ @@ -166,8 +145,6 @@ public static function error( $message, $context = array() ) { /** * Logs a debug message. * - * @since 3.7.0 - * * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. */ @@ -179,11 +156,10 @@ public static function debug( $message, $context = array() ) { /** * Logs a message. * - * @since 3.7.0 - * * @param string $level The log level. * @param string $message The message to log. * @param array $context Optional. Additional context data. Default empty array. + * @return void */ private function log( $level, $message, $context = array() ) { if ( ! $this->log_enabled ) { @@ -192,10 +168,16 @@ private function log( $level, $message, $context = array() ) { $this->rotate_logs(); + // Ensure log directory exists. + $log_dir = dirname( $this->log_file_path ); + if ( ! file_exists( $log_dir ) ) { + wp_mkdir_p( $log_dir ); + } + $timestamp = current_time( 'Y-m-d H:i:s' ); $level_str = strtoupper( $level ); $context_str = ! empty( $context ) ? ' ' . wp_json_encode( $context ) : ''; - $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}\n"; + $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}\\n"; $file = fopen( $this->log_file_path, 'a' ); if ( $file ) { @@ -205,9 +187,10 @@ private function log( $level, $message, $context = array() ) { } /** - * Rotates log files when they exceed the max size. + * Deletes log file and creates a new one when the + * MAX_LOG_SIZE is met. * - * @since 3.7.0 + * @return void */ private function rotate_logs() { if ( ! file_exists( $this->log_file_path ) ) { @@ -219,53 +202,25 @@ private function rotate_logs() { return; } - for ( $i = self::MAX_LOG_FILES - 1; $i > 0; $i-- ) { - $old_file = $this->log_file_path . '.' . $i; - $new_file = $this->log_file_path . '.' . ($i + 1); - - if ( file_exists( $old_file ) ) { - if ( self::MAX_LOG_FILES - 1 === $i ) { - unlink( $old_file ); - } else { - rename( $old_file, $new_file ); - } - } - } - - rename( $this->log_file_path, $this->log_file_path . '.1' ); + unlink( $this->log_file_path ); } /** - * Clears all log files. - * - * @since 3.7.0 + * Clears log file * * @return bool True if logs were cleared successfully. */ public function clear_logs() { - $cleared = true; - - // Remove main log file. if ( file_exists( $this->log_file_path ) ) { - $cleared = unlink( $this->log_file_path ) && $cleared; - } - - // Remove rotated log files. - for ( $i = 1; $i <= self::MAX_LOG_FILES; $i++ ) { - $log_file = $this->log_file_path . '.' . $i; - if ( file_exists( $log_file ) ) { - $cleared = unlink( $log_file ) && $cleared; - } + return unlink( $this->log_file_path ); } - return $cleared; + return true; } /** * Gets all log file paths. * - * @since 3.7.0 - * * @return array Array of log file paths. */ public function get_log_files() { @@ -275,13 +230,6 @@ public function get_log_files() { $files[] = $this->log_file_path; } - for ( $i = 1; $i <= self::MAX_LOG_FILES; $i++ ) { - $log_file = $this->log_file_path . '.' . $i; - if ( file_exists( $log_file ) ) { - $files[] = $log_file; - } - } - return $files; } } diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index f186163..3803568 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -88,6 +88,8 @@ public function __construct($vfs) $this->addMethod('is_customize_preview'); $this->addMethod('is_plugin_active'); $this->addMethod('trailingslashit'); + $this->addMethod('current_time'); + $this->addMethod('wp_mkdir_p'); $this->defaults(); $this->create_filesystem(); } @@ -356,6 +358,25 @@ public function trailingslashit($value) { return $value . '/'; } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/current_time/ + * + * @return int|string + */ + public function current_time() { + $dt = new DateTime( 'now' ); + return $dt->format('Y-m-d H:i:s'); + } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/wp_mkdir_p/ + * + * @return bool + */ + public function wp_mkdir_p( $dir ) { + mkdir( $dir, 0755, true ); + } } class WP_HTTP_Proxy diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php index 5932bf0..621b4c3 100644 --- a/test/unit/TinyLoggerTest.php +++ b/test/unit/TinyLoggerTest.php @@ -1,7 +1,10 @@ clear_logs(); $logger->reset(); } @@ -26,14 +30,87 @@ public function test_logger_always_has_one_instance() assertEquals($instance1, $instance2, 'logger should be a singleton'); } - public function test_get_log_enabled_memoizes_log_enabled() { + public function test_get_log_enabled_memoizes_log_enabled() + { $this->wp->addOption('tinypng_logging_enabled', 'on'); $logger = Tiny_Logger::get_instance(); assertTrue($logger->get_log_enabled(), 'log should be enabled when tinypng_logging_enabled is on'); } - public function test_sets_log_path_on_construct() { + public function test_sets_log_path_on_construct() + { $logger = Tiny_Logger::get_instance(); assertEquals($logger->get_log_file_path(), 'vfs://root/wp-content/uploads/tiny-compress-logs/tiny-compress.log'); } + + public function test_registers_save_update_when_log_enabled() + { + $logger = Tiny_Logger::get_instance(); + $logger->init(); + WordPressStubs::assertHook('pre_update_option_tinypng_logging_enabled', 'Tiny_Logger::on_save_log_enabled'); + } + + public function test_option_hook_updates_log_enabled() + { + $this->wp->addOption('tinypng_logging_enabled', false); + Tiny_Logger::init(); + $logger = Tiny_Logger::get_instance(); + + assertFalse($logger->get_log_enabled(), 'option is not set so should be false'); + + apply_filters('pre_update_option_tinypng_logging_enabled', 'on', null, ''); + + assertTrue($logger->get_log_enabled(), 'when option is updated, should be true'); + } + + public function test_will_not_log_if_disabled() + { + $this->wp->addOption('tinypng_logging_enabled', false); + $logger = Tiny_Logger::get_instance(); + + Tiny_Logger::error('This should not be logged'); + Tiny_Logger::debug('This should also not be logged'); + + $log_path = $logger->get_log_file_path(); + $log_exists = file_exists($log_path); + assertFalse($log_exists, 'log file should not exist when logging is disabled'); + } + + public function test_creates_log_when_log_is_enabled() + { + $this->wp->addOption('tinypng_logging_enabled', 'on'); + + $logger = Tiny_Logger::get_instance(); + $log_path = $logger->get_log_file_path(); + $log_exists = file_exists($log_path); + assertFalse($log_exists, 'log file should not exist initially'); + + Tiny_Logger::error('This should be logged'); + Tiny_Logger::debug('This should also be logged'); + + $log_path = $logger->get_log_file_path(); + $log_exists = file_exists($log_path); + assertTrue($log_exists, 'log file is created when logging is enabled'); + } + + public function test_removes_full_log_and_creates_new() + { + $this->wp->addOption('tinypng_logging_enabled', 'on'); + + $log_dir_path = 'wp-content/uploads/tiny-compress-logs'; + vfsStream::newDirectory($log_dir_path)->at($this->vfs); + $log_dir = $this->vfs->getChild($log_dir_path); + + vfsStream::newFile('tiny-compress.log') + ->withContent(LargeFileContent::withMegabytes(5.1)) + ->at($log_dir); + + $logger = Tiny_Logger::get_instance(); + + assertTrue(filesize($logger->get_log_file_path()) > 5242880, 'log file should be larger than 5MB'); + + Tiny_Logger::error('This should be logged'); + + assertTrue(filesize($logger->get_log_file_path()) < 1048576, 'log file rotated and less than 1MB'); + } } From 396ccbb9efa03b379ef83874c8a6147a9b546f97 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Jan 2026 16:08:58 +0100 Subject: [PATCH 16/23] Format --- src/class-tiny-logger.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 7769205..f12dfd5 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -21,7 +21,7 @@ /** * Handles logging of plugin events to file. - * + * * * @since 3.7.0 */ @@ -96,8 +96,7 @@ public function get_log_enabled() { * * @return string The full filesystem path to the tiny-compress.log file. */ - public function get_log_file_path() - { + public function get_log_file_path() { return $this->log_file_path; } @@ -110,7 +109,7 @@ public function get_log_file_path() public static function on_save_log_enabled( $log_enabled, $old, $option ) { $instance = self::get_instance(); $instance->log_enabled = 'on' === $log_enabled; - + if ( 'on' !== $log_enabled ) { $instance->clear_logs(); } @@ -128,7 +127,7 @@ public static function on_save_log_enabled( $log_enabled, $old, $option ) { private function resolve_log_file_path() { $upload_dir = wp_upload_dir(); $log_dir = trailingslashit( $upload_dir['basedir'] ) . 'tiny-compress-logs'; - return trailingslashit($log_dir) . 'tiny-compress.log'; + return trailingslashit( $log_dir ) . 'tiny-compress.log'; } /** @@ -187,7 +186,7 @@ private function log( $level, $message, $context = array() ) { } /** - * Deletes log file and creates a new one when the + * Deletes log file and creates a new one when the * MAX_LOG_SIZE is met. * * @return void From 6cfae116d059cb43f2b88b6b011ee124b2ae5894 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Jan 2026 16:42:06 +0100 Subject: [PATCH 17/23] change file name, format and size --- src/class-tiny-diagnostics.php | 2 +- src/class-tiny-image.php | 2 +- src/class-tiny-logger.php | 11 +++++++---- test/unit/TinyLoggerTest.php | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php index b760bf5..256989d 100644 --- a/src/class-tiny-diagnostics.php +++ b/src/class-tiny-diagnostics.php @@ -210,7 +210,7 @@ public function create_diagnostic_zip() { // Add diagnostic info. $info = self::collect_info(); - $zip->addFromString( 'diagnostics.json', wp_json_encode( $info, JSON_PRETTY_PRINT ) ); + $zip->addFromString( 'tiny-diagnostics.json', wp_json_encode( $info, JSON_PRETTY_PRINT ) ); // Add log files. $logger = Tiny_Logger::get_instance(); diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index 22b4740..adb0056 100644 --- a/src/class-tiny-image.php +++ b/src/class-tiny-image.php @@ -253,7 +253,7 @@ public function compress() { $size->add_tiny_meta_error( $e ); $failed++; Tiny_Logger::error('compress failed', array( - 'error' => $e, + 'error' => $e->get_message(), 'size' => $size_name, 'image_id' => $this->id, )); diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index f12dfd5..0930034 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -31,7 +31,7 @@ class Tiny_Logger { const LOG_LEVEL_ERROR = 'error'; const LOG_LEVEL_DEBUG = 'debug'; - const MAX_LOG_SIZE = 5242880; // 5MB + const MAX_LOG_SIZE = 2 * 1024 * 1024; // 2MB private static $instance = null; @@ -176,11 +176,14 @@ private function log( $level, $message, $context = array() ) { $timestamp = current_time( 'Y-m-d H:i:s' ); $level_str = strtoupper( $level ); $context_str = ! empty( $context ) ? ' ' . wp_json_encode( $context ) : ''; - $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}\\n"; - + $log_entry = "[{$timestamp}] [{$level_str}] {$message}{$context_str}" . PHP_EOL; $file = fopen( $this->log_file_path, 'a' ); if ( $file ) { - fwrite( $file, $log_entry ); + if ( flock( $file, LOCK_EX ) ) { + fwrite( $file, $log_entry ); + fflush( $file ); + flock( $file, LOCK_UN ); + } fclose( $file ); } } diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php index 621b4c3..7140c0e 100644 --- a/test/unit/TinyLoggerTest.php +++ b/test/unit/TinyLoggerTest.php @@ -102,12 +102,12 @@ public function test_removes_full_log_and_creates_new() $log_dir = $this->vfs->getChild($log_dir_path); vfsStream::newFile('tiny-compress.log') - ->withContent(LargeFileContent::withMegabytes(5.1)) + ->withContent(LargeFileContent::withMegabytes(2.1)) ->at($log_dir); $logger = Tiny_Logger::get_instance(); - assertTrue(filesize($logger->get_log_file_path()) > 5242880, 'log file should be larger than 5MB'); + assertTrue(filesize($logger->get_log_file_path()) > 2097152, 'log file should be larger than 2MB'); Tiny_Logger::error('This should be logged'); From d430ff64d081bfac35f047d8603d779d6ce14da0 Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 7 Jan 2026 14:33:15 +0100 Subject: [PATCH 18/23] Clear logs when turned on --- src/class-tiny-logger.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/class-tiny-logger.php b/src/class-tiny-logger.php index 0930034..11f9465 100644 --- a/src/class-tiny-logger.php +++ b/src/class-tiny-logger.php @@ -103,14 +103,13 @@ public function get_log_file_path() { /** * Triggered when log_enabled is saved * - set the setting on the instance - * - if turn off, clear the logs - * - if turned on, check if we can create the log file + * - if turn on, clear the old logs */ public static function on_save_log_enabled( $log_enabled, $old, $option ) { $instance = self::get_instance(); $instance->log_enabled = 'on' === $log_enabled; - if ( 'on' !== $log_enabled ) { + if ( $instance->get_log_enabled() ) { $instance->clear_logs(); } From e4c3ef3b9a143aed1323b01b48b52892c4e1769f Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 7 Jan 2026 15:08:23 +0100 Subject: [PATCH 19/23] Add more tests --- test/unit/TinyDiagnosticsTest.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/unit/TinyDiagnosticsTest.php b/test/unit/TinyDiagnosticsTest.php index ec4bb1a..088cf2b 100644 --- a/test/unit/TinyDiagnosticsTest.php +++ b/test/unit/TinyDiagnosticsTest.php @@ -1,5 +1,11 @@ collect_info(); + + // were just verifying the main structure + assertArrayHasKey('timestamp', $info); + assertArrayHasKey('server_info', $info); + assertArrayHasKey('site_info', $info); + assertArrayHasKey('active_plugins', $info); + assertArrayHasKey('tiny_info', $info); + assertArrayHasKey('image_sizes', $info); + } } From 1d7cd63791f2a9bbfa3ba1f2818c79646644e195 Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 7 Jan 2026 15:10:47 +0100 Subject: [PATCH 20/23] Add mocks for test --- test/helpers/wordpress.php | 91 +++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index 3803568..6f684d9 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -59,6 +59,7 @@ class WordPressStubs public function __construct($vfs) { $GLOBALS['wp'] = $this; + $GLOBALS['wpdb'] = $this; $this->vfs = $vfs; $this->addMethod('add_action'); $this->addMethod('do_action'); @@ -90,6 +91,12 @@ public function __construct($vfs) $this->addMethod('trailingslashit'); $this->addMethod('current_time'); $this->addMethod('wp_mkdir_p'); + $this->addMethod('db_version'); + $this->addMethod('wp_get_theme'); + $this->addMethod('get_home_url'); + $this->addMethod('get_locale'); + $this->addMethod('wp_timezone_string'); + $this->addMethod('update_option'); $this->defaults(); $this->create_filesystem(); } @@ -110,6 +117,11 @@ public function defaults() $GLOBALS['_wp_additional_image_sizes'] = array(); } + public function __call($method, $args) + { + return $this->call($method, $args); + } + public function call($method, $args) { $mocks = new WordPressMocks(); @@ -183,7 +195,7 @@ public function call($method, $args) } elseif ('is_admin' === $method) { return true; } elseif (method_exists($mocks, $method)) { - return $mocks->$method($args[0]); + return call_user_func_array(array($mocks, $method), $args); } } @@ -348,7 +360,8 @@ public static function assertHook($hookname, $expected_args = null) } } -class WordPressMocks { +class WordPressMocks +{ /** * Mocked function for https://developer.wordpress.org/reference/functions/trailingslashit/ * @@ -358,14 +371,15 @@ public function trailingslashit($value) { return $value . '/'; } - + /** * Mocked function for https://developer.wordpress.org/reference/functions/current_time/ * * @return int|string */ - public function current_time() { - $dt = new DateTime( 'now' ); + public function current_time() + { + $dt = new DateTime('now'); return $dt->format('Y-m-d H:i:s'); } @@ -374,8 +388,71 @@ public function current_time() { * * @return bool */ - public function wp_mkdir_p( $dir ) { - mkdir( $dir, 0755, true ); + public function wp_mkdir_p($dir) + { + mkdir($dir, 0755, true); + } + + /** + * https://developer.wordpress.org/reference/classes/wpdb/db_version/ + * + * @return string|null database version + */ + public function db_version() + { + return 'mysqlv'; + } + + /** + * https://developer.wordpress.org/reference/classes/wpdb/db_version/ + * + * @return string|null database version + */ + public function wp_get_theme() + { + return new class { + function get($val) + { + return $val; + } + }; + } + + /** + * https://developer.wordpress.org/reference/classes/wpdb/db_version/ + * + * @return string|null database version + */ + public function get_home_url() + { + return 'http://localhost'; + } + + /** + * https://developer.wordpress.org/reference/classes/wpdb/db_version/ + * + * @return string|null database version + */ + public function get_locale() + { + return 'gb_GB'; + } + /** + * https://developer.wordpress.org/reference/functions/wp_timezone_string/ + * + * @return string|null database version + */ + public function wp_timezone_string() + { + return 'timezone'; + } + /** + * https://developer.wordpress.org/reference/functions/wp_timezone_string/ + * + * @return void + */ + public function update_option() + { } } From 85f8158ef4716117c0cd6994b6a7a361712bb76d Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 7 Jan 2026 22:13:31 +0100 Subject: [PATCH 21/23] set timeout to 300 --- src/class-tiny-compress-client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index d4a8918..3b4636c 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -36,7 +36,7 @@ class Tiny_Compress_Client extends Tiny_Compress { * @since 3.6.8 * @var int */ - const API_TIMEOUT = 10; + const API_TIMEOUT = 300; private $last_error_code = 0; private $last_message = ''; From f995de476de7b9a64034eb3164f8a684f3595cc2 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 9 Jan 2026 13:39:01 +0100 Subject: [PATCH 22/23] Seperate API timeout and connect timeout --- src/class-tiny-compress-client.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index 3b4636c..b41aa6c 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -36,7 +36,8 @@ class Tiny_Compress_Client extends Tiny_Compress { * @since 3.6.8 * @var int */ - const API_TIMEOUT = 300; + const API_TIMEOUT = 120; + const CONNECT_TIMEOUT = 8; private $last_error_code = 0; private $last_message = ''; @@ -186,7 +187,7 @@ private function set_request_options( $client ) { // Set API request timeout to prevent indefinite hanging $options[ CURLOPT_TIMEOUT ] = self::API_TIMEOUT; - $options[ CURLOPT_CONNECTTIMEOUT ] = self::API_TIMEOUT; + $options[ CURLOPT_CONNECTTIMEOUT ] = self::CONNECT_TIMEOUT; if ( TINY_DEBUG ) { $file = fopen( dirname( __FILE__ ) . '/curl.log', 'w' ); From 42623d1e863aa655ce48d56d0729f2938bc5adde Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 9 Jan 2026 14:23:23 +0100 Subject: [PATCH 23/23] remove server info and add a phpinfo file --- src/class-tiny-diagnostics.php | 37 +++++++------------------------ test/unit/TinyDiagnosticsTest.php | 6 ----- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/src/class-tiny-diagnostics.php b/src/class-tiny-diagnostics.php index 256989d..b335042 100644 --- a/src/class-tiny-diagnostics.php +++ b/src/class-tiny-diagnostics.php @@ -63,7 +63,6 @@ public function __construct( $settings ) { public function collect_info() { $info = array( 'timestamp' => current_time( 'Y-m-d H:i:s' ), - 'server_info' => self::get_server_info(), 'site_info' => self::get_site_info(), 'active_plugins' => self::get_active_plugins(), 'tiny_info' => $this->get_tiny_info(), @@ -97,34 +96,6 @@ private static function get_site_info() { ); } - /** - * Gets server information. - * - * @since 3.7.0 - * - * @return array Server information. - */ - private static function get_server_info() { - global $wpdb; - - return array( - 'php_version' => phpversion(), - 'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? - sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : - 'Unknown', - 'mysql_version' => $wpdb->db_version(), - 'max_execution_time' => ini_get( 'max_execution_time' ), - 'memory_limit' => ini_get( 'memory_limit' ), - 'post_max_size' => ini_get( 'post_max_size' ), - 'upload_max_filesize' => ini_get( 'upload_max_filesize' ), - 'max_input_vars' => ini_get( 'max_input_vars' ), - 'curl_version' => function_exists( 'curl_version' ) ? - curl_version()['version'] : - 'Not available', - 'disabled_functions' => ini_get( 'disable_functions' ), - ); - } - /** * Gets list of active plugins. * @@ -212,6 +183,14 @@ public function create_diagnostic_zip() { $info = self::collect_info(); $zip->addFromString( 'tiny-diagnostics.json', wp_json_encode( $info, JSON_PRETTY_PRINT ) ); + // Add phpinfo HTML. + ob_start(); + phpinfo( INFO_GENERAL ); + phpinfo( INFO_CONFIGURATION ); + phpinfo( INFO_MODULES ); + $phpinfo_html = ob_get_clean(); + $zip->addFromString( 'phpinfo.html', $phpinfo_html ); + // Add log files. $logger = Tiny_Logger::get_instance(); $log_files = $logger->get_log_files(); diff --git a/test/unit/TinyDiagnosticsTest.php b/test/unit/TinyDiagnosticsTest.php index 088cf2b..548934c 100644 --- a/test/unit/TinyDiagnosticsTest.php +++ b/test/unit/TinyDiagnosticsTest.php @@ -1,9 +1,4 @@