diff --git a/src/class-tiny-compress-client.php b/src/class-tiny-compress-client.php index 5bbf8c9a..690bbf26 100644 --- a/src/class-tiny-compress-client.php +++ b/src/class-tiny-compress-client.php @@ -30,6 +30,23 @@ class Tiny_Compress_Client extends Tiny_Compress { + /** + * API request timeout in seconds. + * 2026-01-14 showed 99% was < 120s + * + * @since 3.6.8 + * @var int + */ + const API_TIMEOUT = 120; + + /** + * Connection setup timeout + * + * @since 3.6.8 + * @var int + */ + const CONNECT_TIMEOUT = 8; + private $last_error_code = 0; private $last_message = ''; private $proxy; @@ -92,7 +109,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 ) { @@ -138,6 +154,11 @@ protected function compress( $input, $resize_opts, $preserve_opts, $convert_to ) } 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(), get_class( $err ), @@ -172,6 +193,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::CONNECT_TIMEOUT; + if ( TINY_DEBUG ) { $file = fopen( dirname( __FILE__ ) . '/curl.log', 'w' ); if ( is_resource( $file ) ) { @@ -190,5 +215,7 @@ private function set_request_options( $client ) { $options[ CURLOPT_PROXYUSERPWD ] = $this->proxy->authentication(); } } + + $property->setValue( $client, $options ); } } diff --git a/src/class-tiny-compress-fopen.php b/src/class-tiny-compress-fopen.php index 4e7a9cd4..f72b697d 100644 --- a/src/class-tiny-compress-fopen.php +++ b/src/class-tiny-compress-fopen.php @@ -86,6 +86,11 @@ 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, + '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-diagnostics.php b/src/class-tiny-diagnostics.php new file mode 100644 index 00000000..80611857 --- /dev/null +++ b/src/class-tiny-diagnostics.php @@ -0,0 +1,266 @@ +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' ), + 'site_info' => self::get_site_info(), + 'server_info' => self::get_server_info(), + 'active_plugins' => self::get_active_plugins(), + 'tiny_info' => $this->get_tiny_info(), + 'image_sizes' => $this->settings->get_active_tinify_sizes(), + ); + + return $info; + } + + /** + * Gets server information. + * We have considered phpinfo but this would be a security concern + * as it contains a lot of information we probably do not need. + * Whenever support needs more server information, we can manually + * add it here. + * + * @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 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 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() { + check_ajax_referer( 'tiny-compress', 'security' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + esc_html__( 'Not allowed to download diagnostics.', 'tiny-compress-images' ), + 403 + ); + } + + $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' + ) + ); + } + + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + $temp_dir = trailingslashit( get_temp_dir() ) . 'tiny-compress-temp'; + if ( ! $wp_filesystem->exists( $temp_dir ) ) { + wp_mkdir_p( $temp_dir ); + } + + $temp_path = tempnam( $temp_dir, 'tiny-compress-diagnostics-' . gmdate( 'Y-m-d-His' ) ); + + $zip = new ZipArchive(); + if ( true !== $zip->open( $temp_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { + return new WP_Error( 'zip_create_failed', + __( 'Failed to create zip file.', + 'tiny-compress-images' ) + ); + } + + $info = self::collect_info(); + $zip->addFromString( 'tiny-diagnostics.json', wp_json_encode( $info, JSON_PRETTY_PRINT ) ); + + $logger = Tiny_Logger::get_instance(); + $log_files = $logger->get_log_files(); + + foreach ( $log_files as $log_file ) { + if ( $wp_filesystem->exists( $log_file ) ) { + $zip->addFile( $log_file, 'logs/' . basename( $log_file ) ); + } + } + + $zip->close(); + return $temp_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 ) { + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + if ( ! $wp_filesystem->exists( $zip_path ) ) { + wp_die( esc_html__( 'Diagnostic file not found.', 'tiny-compress-images' ) ); + } + + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename="tiny-compress-diagnostics.zip"' ); + header( 'Content-Length: ' . $wp_filesystem->size( $zip_path ) ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile + readfile( $zip_path ); + + // Clean up. + $wp_filesystem->delete( $zip_path ); + + exit; + } +} diff --git a/src/class-tiny-helpers.php b/src/class-tiny-helpers.php index 16354dff..30e9a0a5 100644 --- a/src/class-tiny-helpers.php +++ b/src/class-tiny-helpers.php @@ -140,4 +140,35 @@ public static function is_pagebuilder_request() { return false; } + + /** + * Gets or initializes the WordPress filesystem instance. + * + * Returns the global WP_Filesystem instance, initializing it if necessary. + * This helper prevents repeated initialization code throughout the plugin. + * + * @since 3.7.0 + * + * @return WP_Filesystem_Base The WP_Filesystem instance. + * @throws Exception If the filesystem cannot be initialized. + */ + public static function get_wp_filesystem() { + global $wp_filesystem; + + if ( $wp_filesystem instanceof WP_Filesystem_Base ) { + return $wp_filesystem; + } + + // Initialize the filesystem only if the function isn't available yet. + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + WP_Filesystem(); + + if ( ! ( $wp_filesystem instanceof WP_Filesystem_Base ) ) { + throw new Exception( 'Unable to initialize WordPress filesystem.' ); + } + + return $wp_filesystem; + } } diff --git a/src/class-tiny-image.php b/src/class-tiny-image.php index 3a5a2678..adb0056c 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; } @@ -212,6 +216,20 @@ public function compress() { $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, @@ -227,14 +245,23 @@ 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->get_message(), + 'size' => $size_name, + 'image_id' => $this->id, + )); } $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 new file mode 100644 index 00000000..e9497d26 --- /dev/null +++ b/src/class-tiny-logger.php @@ -0,0 +1,266 @@ +log_file_path = $this->resolve_log_file_path(); + $this->log_enabled = 'on' === get_option( 'tinypng_logging_enabled', false ); + } + + /** + * 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. + * + * @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. + * + * @return void + */ + public static function reset() { + self::$instance = null; + } + + /** + * Retrieves whether logging is currently enabled. + * + * @return bool True if logging is enabled, false otherwise. + */ + public function get_log_enabled() { + 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 + * - 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 ( $instance->get_log_enabled() ) { + $instance->clear_logs(); + } + + return $log_enabled; + } + + /** + * Retrieves the log path using wp_upload_dir. This operation + * should only be used internally. Use the getter to get the + * memoized function. + * + * @return string The 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'; + } + + /** + * Logs an error message. + * + * @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. + * + * @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. + * + * @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 ) { + return; + } + + $this->rotate_logs(); + + // Ensure log directory exists. + $log_dir = dirname( $this->log_file_path ); + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + if ( ! $wp_filesystem->exists( $log_dir ) ) { + wp_mkdir_p( $log_dir ); + self::create_blocking_files( $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}" . PHP_EOL; + + $existing_content = ''; + if ( $wp_filesystem->exists( $this->log_file_path ) ) { + $existing_content = $wp_filesystem->get_contents( $this->log_file_path ); + } + $wp_filesystem->put_contents( + $this->log_file_path, + $existing_content . $log_entry, + FS_CHMOD_FILE + ); + } + + /** + * Deletes log file and creates a new one when the + * MAX_LOG_SIZE is met. + * + * @return void + */ + private function rotate_logs() { + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + if ( ! $wp_filesystem->exists( $this->log_file_path ) ) { + return; + } + + $file_size = $wp_filesystem->size( $this->log_file_path ); + if ( $file_size < self::MAX_LOG_SIZE ) { + return; + } + + $wp_filesystem->delete( $this->log_file_path ); + } + + /** + * Clears log file + * + * @return bool True if logs were cleared successfully. + */ + public function clear_logs() { + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + if ( $wp_filesystem->exists( $this->log_file_path ) ) { + return $wp_filesystem->delete( $this->log_file_path ); + } + + return true; + } + + /** + * Creates defensive files to prevent direct access to log directory. + * Adds index.html to prevent directory listing and .htaccess to block access. + * + * @param string $log_dir The path to the log directory. + * @return void + */ + private static function create_blocking_files( $log_dir ) { + $wp_filesystem = Tiny_Helpers::get_wp_filesystem(); + + $index_file = trailingslashit( $log_dir ) . 'index.html'; + if ( ! $wp_filesystem->exists( $index_file ) ) { + $index_content = ''; + $wp_filesystem->put_contents( $index_file, $index_content, FS_CHMOD_FILE ); + } + + $htaccess_file = trailingslashit( $log_dir ) . '.htaccess'; + if ( ! $wp_filesystem->exists( $htaccess_file ) ) { + $htaccess_content = 'deny from all'; + $wp_filesystem->put_contents( $htaccess_file, $htaccess_content, FS_CHMOD_FILE ); + } + } + + /** + * Gets all log file paths. + * + * @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; + } + + return $files; + } +} diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index f6ff1d12..8c9c05f0 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(), diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 19c33082..4793a6a9 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/src/css/admin.css b/src/css/admin.css index f87d6dc3..977dadf1 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 b8a972f8..0ead0f46 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -1,4 +1,17 @@ (function() { + function downloadDiagnostics() { + 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 new file mode 100644 index 00000000..48103ec9 --- /dev/null +++ b/src/views/settings-diagnostics.php @@ -0,0 +1,29 @@ + +
| + |
+ + + ++ > + + +
+
+
+
+ |
+
|---|
diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index 184b57ad..87b99c7a 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -1,16 +1,20 @@ values = array( + public function __construct() + { + $this->values = array( 'thumbnail_size_w' => 150, 'thumbnail_size_h' => 150, 'medium_size_w' => 300, @@ -19,26 +23,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 +57,63 @@ class WordPressStubs { private $stubs; private $filters; - public function __construct( $vfs ) { + public function __construct($vfs) + { $GLOBALS['wp'] = $this; + $GLOBALS['wpdb'] = $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->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->addMethod('check_ajax_referer'); + $this->addMethod('wp_json_encode'); + $this->addMethod('wp_send_json_error'); + $this->addMethod('get_temp_dir'); $this->defaults(); $this->create_filesystem(); } - public function create_filesystem() { - vfsStream::newDirectory( self::UPLOAD_DIR ) - ->at( $this->vfs ); + 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 +122,56 @@ 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) + { + return $this->call($method, $args); + } + + public function call($method, $args) + { + $mocks = new WordPressMocks(); + $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 +179,132 @@ 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; + } elseif (method_exists($mocks, $method)) { + return call_user_func_array(array($mocks, $method), $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 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 +312,210 @@ 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 WordPressMocks +{ + /** + * Mocked function for https://developer.wordpress.org/reference/functions/trailingslashit/ + * + * @return void + */ + 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); + } + + /** + * 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/functions/wp_get_theme/ + * + * @return WP_Theme Theme object. Be sure to check the object’s exists() method if you need to confirm the theme’s existence. + */ + public function wp_get_theme() + { + return new class { + function get($val) + { + return $val; + } + }; + } + + /** + * https://developer.wordpress.org/reference/functions/get_home_url/ + * + * @return string Home URL link with optional path appended. + */ + public function get_home_url() + { + return 'http://localhost'; + } + + /** + * https://developer.wordpress.org/reference/functions/get_locale/ + * + * @return string The locale of the blog or from the 'locale' hook. + */ + public function get_locale() + { + return 'en_GB'; + } + /** + * https://developer.wordpress.org/reference/functions/wp_timezone_string/ + * + * @return string PHP timezone name or a ±HH:MM offset. + */ + public function wp_timezone_string() + { + return 'timezone'; + } + /** + * https://developer.wordpress.org/reference/functions/update_option/ + * + * @return void + */ + public function update_option() + { + } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/check_ajax_referer/ + * + * @return bool|int + */ + public function check_ajax_referer($action = -1, $query_arg = '_wpnonce', $die = true) + { + // Default mock returns 1 (valid nonce) + return 1; + } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/get_temp_dir/ + * + * @return string + */ + public function get_temp_dir() + { + return sys_get_temp_dir(); + } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/wp_send_json_error/ + * + * @return void + */ + public function wp_send_json_error($data = null, $status_code = 400) + { + // Default mock does nothing, tests should stub this + } + + /** + * Mocked function for https://developer.wordpress.org/reference/functions/wp_json_encode/ + * + * @return string + */ + public function wp_json_encode($data, $options = 0, $depth = 512) + { + return json_encode($data, $options, $depth); + } +} + +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); +} diff --git a/test/unit/TinyLoggerTest.php b/test/unit/TinyLoggerTest.php new file mode 100644 index 00000000..7140c0e9 --- /dev/null +++ b/test/unit/TinyLoggerTest.php @@ -0,0 +1,116 @@ +clear_logs(); + $logger->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_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'); + } + + 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(2.1)) + ->at($log_dir); + + $logger = Tiny_Logger::get_instance(); + + assertTrue(filesize($logger->get_log_file_path()) > 2097152, 'log file should be larger than 2MB'); + + Tiny_Logger::error('This should be logged'); + + assertTrue(filesize($logger->get_log_file_path()) < 1048576, 'log file rotated and less than 1MB'); + } +} diff --git a/test/unit/TinySettingsAdminTest.php b/test/unit/TinySettingsAdminTest.php index 0fdbf7f6..d5577aa3 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 4501fd01..40fb44b4 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' )); } } diff --git a/test/unit/Tiny_Diagnostics_Test.php b/test/unit/Tiny_Diagnostics_Test.php new file mode 100644 index 00000000..b341eabb --- /dev/null +++ b/test/unit/Tiny_Diagnostics_Test.php @@ -0,0 +1,98 @@ +collect_info(); + + // were just verifying the main structure + assertArrayHasKey('timestamp', $info); + assertArrayHasKey('site_info', $info); + assertArrayHasKey('active_plugins', $info); + assertArrayHasKey('server_info', $info); + assertArrayHasKey('tiny_info', $info); + assertArrayHasKey('image_sizes', $info); + } + + public function test_will_die_when_nonce_is_invalid() { + $tiny_settings = new Tiny_Settings(); + $tiny_diagnostics = new Tiny_Diagnostics($tiny_settings); + + + $this->wp->stub('check_ajax_referer', function($action, $query_arg) { + $this->assertEquals('tiny-compress', $action); + $this->assertEquals('security', $query_arg); + // mocking an invalid nonce here, it usually calls wp_die + throw new Exception('invalid nonce'); + }); + + try { + $tiny_diagnostics->download_diagnostics(); + } catch (Exception $e) { + $this->assertEquals('invalid nonce', $e->getMessage()); + } + } + + public function test_throws_error_when_user_lacks_permission() { + $tiny_settings = new Tiny_Settings(); + $tiny_diagnostics = new Tiny_Diagnostics($tiny_settings); + + $this->wp->stub('current_user_can', function($capability) { + $this->assertEquals('manage_options', $capability); + return false; + }); + + $this->wp->stub('wp_send_json_error', function($message, $status_code) use (&$json_error_called) { + $this->assertStringContainsString('Not allowed', $message); + $this->assertEquals(403, $status_code); + throw new Exception('wp_send_json_error'); + }); + + try { + $tiny_diagnostics->download_diagnostics(); + } catch (Exception $e) { + $this->assertEquals('wp_send_json_error', $e->getMessage()); + } + } + + public function test_can_download_zip() { + $tiny_settings = new Tiny_Settings(); + $tiny_diagnostics = new Tiny_Diagnostics($tiny_settings); + + $this->wp->stub('current_user_can', function($capability) { + $this->assertEquals('manage_options', $capability); + return true; + }); + + $zip_path = $tiny_diagnostics->create_diagnostic_zip(); + $this->assertStringContainsString('tiny-compress-diagnostics', $zip_path); + $this->assertTrue(file_exists($zip_path), 'zip should exist at the returned path'); + $file_size = filesize($zip_path); + $this->assertGreaterThan(0, $file_size, 'Zip file should have content'); + + // Clean up + if (file_exists($zip_path)) { + unlink($zip_path); + } + } +} diff --git a/test/wp-includes-for-tests/file.php b/test/wp-includes-for-tests/file.php new file mode 100644 index 00000000..3ea2aaaa --- /dev/null +++ b/test/wp-includes-for-tests/file.php @@ -0,0 +1,62 @@ +