diff --git a/admin.css b/admin.css index 5199a48..3c63c93 100644 --- a/admin.css +++ b/admin.css @@ -14,3 +14,16 @@ .fixed .column-expires { width: 12%; } + +.nav-tab-content { + display: none; +} +.form-table th small { + display: block; + clear: both; + color: #999; +} +.form-table td label { + display: block; + clear:both; +} \ No newline at end of file diff --git a/admin.js b/admin.js index e69de29..2b6d574 100644 --- a/admin.js +++ b/admin.js @@ -0,0 +1,20 @@ +jQuery(function($){ + + // Admin page tabs + var $tabs = $('.nav-tab-wrapper'), + $panels = $('.nav-tab-content'), + currentHash = window.location.hash; + + $tabs.on('click', 'a', function(e){ + var hash = $(this).attr('href').replace('#tab-', '#tab-content-'); // prevents page scrolling if hash is present + $panels.hide().filter(hash).show(); + $tabs.find('a').removeClass('nav-tab-active').filter($(this)).addClass('nav-tab-active'); + }); + $tabs.find( currentHash ? 'a[href="'+currentHash+'"]' : ':first').trigger('click'); + + // Fix input-inside-label glitch + $panels.on('click', 'input[type=text]', function(e){ + e.preventDefault(); + }) + +}); \ No newline at end of file diff --git a/dependency-minification.php b/dependency-minification.php index 60c8c91..7f4ae98 100644 --- a/dependency-minification.php +++ b/dependency-minification.php @@ -2,7 +2,7 @@ /** * Plugin Name: Dependency Minification * Description: Concatenates and minifies scripts and stylesheets. Please install and activate scribu's Proper Network Activation plugin before activating this plugin network-wide. - * Version: 0.9.7 + * Version: 1.0 * Author: X-Team * Author URI: http://x-team.com/wordpress/ * Text Domain: dependency-minification @@ -28,1010 +28,277 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -class Dependency_Minification { - static $options = array(); - protected static $minified_count = 0; - static $admin_page_hook; - - const DEFAULT_ENDPOINT = '_minify'; - const CRON_MINIFY_ACTION = 'minify_dependencies'; - const CACHE_KEY_PREFIX = 'depmin_cache_'; - const FILENAME_PATTERN = '([^/]+?)\.([0-9a-f]+)(?:\.([0-9a-f]+))?\.(css|js)'; - const AJAX_ACTION = 'dependency_minification'; - const ADMIN_PAGE_SLUG = 'dependency-minification'; - const ADMIN_PARENT_PAGE = 'tools.php'; - - static $query_vars = array( - 'depmin_handles', - 'depmin_src_hash', - 'depmin_ver_hash', - 'depmin_file_ext', - ); - - static function setup() { - self::$options = apply_filters( 'dependency_minification_options', array_merge( - array( - 'endpoint' => self::DEFAULT_ENDPOINT, - 'default_exclude_remote_dependencies' => true, - 'cache_control_max_age_cache' => 2629743, // 1 month in seconds - 'cache_control_max_age_error' => 60 * 60, // 1 hour, to try minifying again - 'allow_not_modified_responses' => true, // only needs to be true if not Akamaized and max-age is short - 'admin_page_capability' => 'edit_theme_options', - 'show_error_messages' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ), - 'disable_if_wp_debug' => true, - ), - self::$options - ) ); - - $is_frontend = ! ( - is_admin() - || - in_array( $GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ) ) - ); - if ( $is_frontend ) { - add_filter( 'print_scripts_array', array( __CLASS__, 'filter_print_scripts_array' ) ); - add_filter( 'print_styles_array', array( __CLASS__, 'filter_print_styles_array' ) ); - } - add_action( 'init', array( __CLASS__, 'hook_rewrites' ) ); - add_action( self::CRON_MINIFY_ACTION, array( __CLASS__, 'minify' ), 10, 4 ); - add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) ); - add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) ); - add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) ); - add_action( 'wp_ajax_' . self::AJAX_ACTION, array( __CLASS__, 'admin_ajax_handler' ) ); - add_filter( 'plugin_action_links', array( __CLASS__, 'admin_plugin_action_links' ), 10, 2 ); - } - - static function hook_rewrites() { - add_filter( 'query_vars', array( __CLASS__, 'filter_query_vars' ) ); - add_action( 'pre_get_posts', array( __CLASS__, 'handle_request' ) ); - self::add_rewrite_rule(); - } +// Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) exit; - static function get_rewrite_regex() { - return sprintf( '^%s/%s', self::$options['endpoint'], self::FILENAME_PATTERN ); - } +/** + * Main Dependency Minification plugin class. + * + * @since 0.1 + */ +class Dependency_Minification { - static function add_rewrite_rule() { - $regex = self::get_rewrite_regex(); - $redirect = 'index.php?'; - for ( $i = 0; $i < count( self::$query_vars ); $i += 1 ) { - $redirect .= sprintf( '%s=$matches[%d]&', self::$query_vars[$i], $i + 1 ); - } - add_rewrite_rule( $regex, $redirect, 'top' ); - } + /** + * @var float + * @since 1.0 + */ + const VERSION = '1.0'; - static function remove_rewrite_rule() { - $regex = self::get_rewrite_regex(); - global $wp_rewrite; - unset( $wp_rewrite->extra_rules_top[ $regex ] ); - } - - protected static $is_footer = array( - 'scripts' => false, - 'styles' => false, - ); + /*** Properties ***********************************************************/ /** - * register_activation_hook + * @var DepMin_Admin + * @since 1.0 */ - static function activate() { - self::setup(); - self::add_rewrite_rule(); - flush_rewrite_rules(); - } + public static $admin; /** - * register_deactivation_hook + * @var DepMin_Options + * @since 1.0 */ - static function deactivate() { - self::remove_rewrite_rule(); - flush_rewrite_rules(); - } + public static $options; /** - * @filter query_vars + * @var DepMin_Handler + * @since 1.0 */ - static function filter_query_vars( $query_vars ) { - return array_merge( $query_vars, self::$query_vars ); - } + public static $handler; /** - * @filter print_styles_array + * @var DepMin_Collation + * @since 1.0 */ - static function filter_print_styles_array( $handles ) { - $handles = self::filter_print_dependency_array( $handles, 'styles' ); - return $handles; - } + public static $collation; /** - * @filter print_scripts_array + * @var array + * @since 1.0 */ - static function filter_print_scripts_array( $handles ) { - $handles = self::filter_print_dependency_array( $handles, 'scripts' ); - return $handles; - } + public static $query_vars = array( + 'depmin_handles', + 'depmin_src_hash', + 'depmin_ver_hash', + 'depmin_file_ext', + ); + + /*** Methods **************************************************************/ /** - * @action admin_enqueue_scripts + * @access private + * @return void + * @since 1.0 */ - static function admin_enqueue_scripts( $hook ) { - if ( $hook !== self::$admin_page_hook ) { - return; - } - $meta = get_plugin_data( __FILE__ ); - wp_enqueue_script( 'depmin-admin', plugin_dir_url( __FILE__ ) . 'admin.js', array( 'jquery' ), $meta['Version'] ); - wp_enqueue_style( 'depmin-admin', plugin_dir_url( __FILE__ ) . 'admin.css', array(), $meta['Version'] ); + private function load_includes() { + + // Load the helpers functions. + require self::path( 'inc/helpers.php' ); + } /** - * @action admin_menu + * @access private + * @return void + * @since 1.0 */ - static function admin_menu() { - self::$admin_page_hook = add_submenu_page( - self::ADMIN_PARENT_PAGE, - __( 'Dependency Minification', 'dependency-minification' ), - __( 'Dep. Minification', 'dependency-minification' ), - self::$options['admin_page_capability'], - self::ADMIN_PAGE_SLUG, - array( __CLASS__, 'admin_page' ) - ); + private function setup_actions() { + + DepMin_Minify::hook_cron_action(); + add_action( 'init', array( __CLASS__, 'add_rewrite_rules' ) ); + add_filter( 'query_vars', array( __CLASS__, 'filter_query_vars' ) ); + } /** - * @action admin_notices + * @access private + * @return void + * @since 1.0 */ - static function admin_notices() { - // Show a notice to notify user that pretty urls is disabled, hence the plugin won't work - if ( empty( $GLOBALS['wp_rewrite']->permalink_structure ) ) { - ?> -
-

%1$s: %2$s', - __( 'Dependency Minification', 'dependency-minification' ), - sprintf( - __( 'Pretty permalinks are not enabled in your %1$s, which is required for this plugin to operate. Select something other than Default (e.g. ?p=123)', 'dependency-minification' ), - sprintf( - '%2$s', - admin_url( 'options-permalink.php' ), - __( 'Permalinks Settings', 'dependency-minification' ) - ) - ) - ); - ?>

-
- id !== self::$admin_page_hook ) { - return; - } - if ( empty( $_GET['updated-count'] ) ) { - return; - } - if ( empty( $_GET['updated-action'] ) ) { - return; } - $updated_count = intval( $_REQUEST['updated-count'] ); - $updated_task = sanitize_title( $_REQUEST['updated-action'] ); - ?> -
- -

- -

- -

- -
- %s', esc_url( $admin_page_url ), esc_html__( 'Settings', 'dependency-minification' ) ); - array_push( $links, $admin_page_link ); - } - return $links; - } + public static function add_rewrite_rules() { - static function admin_page() { - if ( ! current_user_can( self::$options['admin_page_capability'] ) ) { - wp_die( __( 'You cannot access this page.', 'dependency-minification' ) ); - } - $nonce = wp_create_nonce( self::AJAX_ACTION ); - ?> -
-

-

- - -
- - - - prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", self::CACHE_KEY_PREFIX . '%' ); - $option_names = $wpdb->get_col( $sql ); - $minified_dependencies = array(); - foreach ( $option_names as $option_name ) { - $minified_dependencies[$option_name] = get_option($option_name); - } - $minified_dependencies = array_filter( $minified_dependencies ); - - $minify_crons = array(); - foreach ( _get_cron_array() as $timestamp => $cron ) { - if ( isset( $cron[self::CRON_MINIFY_ACTION] ) ) { - foreach ( $cron[self::CRON_MINIFY_ACTION] as $key => $min_cron ) { - $cached = $min_cron['args'][0]; - $src_hash = self::hash_array( wp_list_pluck( $cached['deps'], 'src' ) ); - $cache_option_name = self::get_cache_option_name( $src_hash ); - if ( array_key_exists( $cache_option_name, $minified_dependencies ) ) { - $minified_dependencies[$cache_option_name] = array_merge( - $minified_dependencies[$cache_option_name], - $cached - ); - } else { - $minified_dependencies[$cache_option_name] = $cached; - } - } - } - } - ?> - - -
-

-
- - - -

- -

- -
-
- - -
-
-
- - - $tcontainer ) : ?> - <> - - - - - - - - - - > - - - - $minified_dependency ) : ?> - $deps, $type, $pending, $scheduled, $error - $handles = wp_list_pluck( $deps, 'handle' ); - $minified_src = self::get_dependency_minified_url( $deps, $type ); - $link_params = array( - '_wpnonce' => $nonce, - 'action' => self::AJAX_ACTION, - 'depmin_option_name[]' => $option_name, - ); - ?> - - - - - - - $last_modified, - 'expires' => $expires, - ); - ?> - $time ) : ?> - - - - - -
- -
- - > - - - - - - - -
-

- - - - - - - -

-
- - -
- - - - | - - - - | - - - - - - - - - - -
- -
', $percentage, esc_attr( sprintf( __( '(%1$d / %2$d)', 'dependency-minification' ), $min, $max ) ) ); - print esc_html( sprintf( __( '%1$d%%', 'dependency-minification' ), $percentage ) ); - print ''; - } - - ?> - - - - - -
- -
-
- extra_rules_top[ $regex ] ); } /** - * Separate external from internal (local) dependencies and then group the - * internal resources into maximal groups. - * @param {array} $handles - * @param {string} $type (scripts|styles) + * @return void + * @since 1.0 */ - static function filter_print_dependency_array( array $handles, $type ) { - assert( in_array($type, array( 'scripts', 'styles' ) ) ); - assert( isset($GLOBALS["wp_{$type}"]) ); - $wp_deps = &$GLOBALS["wp_{$type}"]; - assert( is_a($wp_deps, 'WP_Dependencies') ); - - /** - * Determine if minification is enabled for the provided $handles. - * Note that we cannot use the $concatenate_scripts global set by script_concat_settings - * because it is intended to only be used in the WP Admin - * Plugin is automatically disabled if pretty permalinks is not activated - */ - $disabled = self::$options['disable_if_wp_debug'] ? ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : false; - $disabled = $disabled || ( defined( 'DEPENDENCY_MINIFICATION_DEFAULT_DISABLED' ) && DEPENDENCY_MINIFICATION_DEFAULT_DISABLED ); - $disabled = apply_filters( 'dependency_minification_disabled', $disabled, $handles, $type ); - $disabled = apply_filters( "dependency_minification_disabled_{$type}", $disabled, $handles ); - $disabled = $disabled || empty( $GLOBALS['wp_rewrite']->permalink_structure ); - if ( $disabled ) { - return $handles; - } + public static function autoload( $class_name ) { - // @todo There should be a better way to determine which group we are in - $current_group = (int) self::$is_footer[$type]; // false => 0, true => 1 - - $handles_in_group = array(); - foreach ( $handles as $handle ) { - $must_process_handle = ( - $wp_deps->groups[$handle] === $current_group - || - // Handle case where script is erroneously enqueued without in_footer=true (here's lookin at you, PollDaddy) - ( - $wp_deps->groups[$handle] < $current_group - && - !in_array($handle, $wp_deps->done) - ) - ); - - if ( $must_process_handle ) { - $handles_in_group[] = $handle; - } - } + switch( $class_name ) { - $filtered_handles = array(); - $groups = self::group_dependencies_by_exclusion( $handles_in_group, $wp_deps ); - - foreach ( $groups as $group ) { - // $internal_groups as $extra => $handles_in_group - if ( empty( $group['handles'] ) ) { - continue; - } - - if ( $group['excluded'] ) { - $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); - continue; - } - $extra = empty( $group['extra'] ) ? array() : $group['extra']; - - $deps = array(); - foreach ( $group['handles'] as $handle ) { - $deps[] = array( - 'handle' => $handle, - 'src' => $wp_deps->registered[$handle]->src, - 'ver' => $wp_deps->registered[$handle]->ver, - ); - } - $srcs = wp_list_pluck( $deps, 'src' ); - $src_hash = self::hash_array( $srcs ); - $ver_hash = self::hash_array( wp_list_pluck( $deps, 'ver' ) ); - - $cache_option_name = self::get_cache_option_name( $src_hash ); - $cached = get_option( $cache_option_name ); - $cached_ver_hash = null; - if ( ! empty( $cached['deps'] ) ) { - $cached_ver_hash = self::hash_array( wp_list_pluck( $cached['deps'], 'ver' ) ); - } - - $is_error = ( - ! empty( $cached['error'] ) - && - $ver_hash === $cached_ver_hash - && - time() < $cached['expires'] - ); - - $is_stale = ( - empty( $cached ) - || - time() > $cached['expires'] - || - $ver_hash !== $cached_ver_hash - ); - - if ( $is_error ) { - if ( self::$options['show_error_messages'] ) { - print "\n\n" ); - } - $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); - } elseif ( $is_stale ) { - printf( "\n\n" ); - - // @todo We could store the info in the option, and just pass the cache key to the cron; this would allow reliable passing of request_uri - - $scheduled = time(); - $args = array_merge( - array( - 'expires' => false, - 'last_modified' => false, - 'etag' => false, - 'unminified_size' => false, - 'contents' => false, - 'pending' => true, - ), - compact( 'type', 'deps', 'scheduled' ) - ); - - wp_schedule_single_event( $scheduled, self::CRON_MINIFY_ACTION, array( $args ) ); - // The bundle is not ready yet, so re-use the existing dependencies - $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); - } else { - self::$minified_count += 1; - $new_handle = sprintf('minified-%d', self::$minified_count); - $filtered_handles[] = $new_handle; - $src = self::get_dependency_minified_url( $deps, $type ); - - // Deps are registered without versions since the URL includes the version (ver_hash) - if ( 'scripts' === $type ) { - $in_footer = !empty( $extra['group'] ); // @todo what if the group is not 0 or 1? - wp_register_script( $new_handle, $src, array(), null, $in_footer ); - } elseif ( 'styles' === $type ) { - wp_register_style( $new_handle, $src, array(), null, $extra['media'] ); - } - $wp_deps->set_group( $new_handle, /*recursive*/false, $current_group ); - $new_dep = $wp_deps->registered[$new_handle]; - $new_extra = array( - 'data' => '', - ); - foreach ( $group['handles'] as $handle ) { - - // Aggregate data from scripts (e.g. wp_localize_script) - if ( ! empty( $wp_deps->registered[$handle]->extra ) ) { - - foreach ( array_keys( $wp_deps->registered[$handle]->extra ) as $extra_key ) { - $data = $wp_deps->get_data( $handle, $extra_key ); - - if ( 'data' === $extra_key ) { - $new_extra['data'] .= "/* wp_localize_script($handle): */\n"; - $new_extra['data'] .= "$data\n\n"; - } else { - if ( isset( $new_extra[$extra_key] ) ) { - // The handles should have been grouped so that they have the same extras - assert( $new_extra[$extra_key] === $data ); - } - $new_extra[$extra_key] = $data; - } - } - } - - // Mark the handles as done for the resources that have been minified - $wp_deps->done[] = $handle; - } - - // Add aggregated extra to new dependency - foreach ( $new_extra as $key => $value ) { - $new_dep->add_data( $key, $value ); - } - } - } + case 'DepMin_Admin': + require self::path( 'inc/admin.php' ); + break; + + case 'DepMin_Options': + require self::path( 'inc/options.php' ); + break; + + case 'DepMin_SrcInfo': + require self::path( 'inc/helpers.php' ); + break; - // @todo Must be a better way to do this - self::$is_footer[$type] = true; // for the next invocation + case 'DepMin_Handler': + require self::path( 'inc/handler.php' ); + break; + + case 'DepMin_Collation': + require self::path( 'inc/collation.php' ); + break; + + case 'DepMin_Minify': + case 'DepMin_Minifier': + case 'DepMin_Minifier_Default': + require self::path( 'inc/minifier.php' ); + break; + + case 'DepMin_Cache': + case 'DepMin_Cache_Default': + case 'DepMin_Cache_Interface': + require self::path( 'inc/cache.php' ); + break; + + } - return $filtered_handles; } /** - * @param string $src - * @return bool + * @return string + * @since 1.0 */ - static function is_self_hosted_src( $src ) { - $parsed_url = parse_url( $src ); - return ( - ( - empty( $parsed_url['host'] ) - && - substr( $parsed_url['path'], 0, 1) === '/' - ) - || - ( - ! empty( $parsed_url['host'] ) - && - $parsed_url['host'] === parse_url( get_home_url(), PHP_URL_HOST ) - ) - ); + public static function url( $path = '' ) { + return plugins_url( $path, __FILE__ ); } /** - * @return {array} Two members, the 1st containing external handles and the 2nd containing internal handles + * @return string + * @since 1.0 */ - static function group_dependencies_by_exclusion( $handles, WP_Dependencies $wp_deps ) { - $groups = array(); - - // First create groups based on whether they are excluded from minification - $last_was_excluded = null; - foreach ( $handles as $handle ) { - $src = $wp_deps->registered[$handle]->src; - $is_local = self::is_self_hosted_src( $src ); - $is_excluded = !$is_local && self::$options['default_exclude_remote_dependencies']; - $is_excluded = apply_filters( 'dependency_minification_excluded', $is_excluded, $handle, $src ); - - if ( $last_was_excluded !== $is_excluded ) { - $groups[] = array( - 'excluded' => $is_excluded, - 'handles' => array(), - ); - } - $groups[ count( $groups ) - 1 ]['handles'][] = $handle; - $last_was_excluded = $is_excluded; - } + public static function path( $path = '' ) { - // Now divide up the groups to create bundles that share the same extras (e.g. stylesheet media or conditional) - $bundled_groups = array(); - foreach ( $groups as $group ) { - if ( $group['excluded'] ) { - $bundled_groups[] = $group; - } else { - $handles_bundles = self::group_handles_by_extra( $group['handles'], $wp_deps ); - foreach ( $handles_bundles as $extra => $handles_bundle ) { - $bundled_groups[] = array( - 'excluded' => false, - 'extra' => unserialize( $extra ), - 'handles' => $handles_bundle, - ); - } - } - } + $base = plugin_dir_path( __FILE__ ); + + if ( ! empty( $path ) ) + $path = path_join( $base, $path ); + else + $path = untrailingslashit( $base ); - return $bundled_groups; + return $path; } /** - * @todo This is only applicable for styles, right? The media and conditional extras. - * @param {array} $handles - * @return {array} Associative array where the keys are the args and extras + * @return void + * @since 1.0 */ - static function group_handles_by_extra( array $handles, WP_Dependencies $wp_deps ) { - $bundles = array(); - foreach ( $handles as $handle ) { - $dep = &$wp_deps->registered[$handle]; - $extra = (array) $dep->extra; - if ( is_a($wp_deps, 'WP_Styles') ) { - $extra['media'] = is_string($dep->args) ? $dep->args : 'all'; - } - unset($extra['suffix']); - unset($extra['rtl']); - unset($extra['data']); - // Default scripts are not assigned 'group', so we use the original 'deps->args' value - if ( is_a( $wp_deps, 'WP_Scripts' ) && empty( $extra['group'] ) && is_int( $dep->args ) ) { - $extra['group'] = $dep->args; - } - ksort($extra); - $key = serialize($extra); - $bundles[$key][] = $handle; - } - return $bundles; + public static function activate() { + self::add_rewrite_rule(); + flush_rewrite_rules(); } /** - * @action minify_dependencies - * + * @return void + * @since 1.0 */ - static function minify( $cached ) { - extract( $cached ); - $host_domain = parse_url( home_url(), PHP_URL_HOST ); - $ver_hash = self::hash_array( wp_list_pluck( $deps, 'ver' ) ); - $src_hash = self::hash_array( wp_list_pluck( $deps, 'src' ) ); - $cache_option_name = self::get_cache_option_name( $src_hash ); - - try { - $is_css = ( 'styles' === $type ); - if ( 'scripts' === $type ) { - require_once( dirname(__FILE__) . '/minify/JS/JSMin.php' ); - } elseif ( 'styles' === $type ) { - require_once( dirname(__FILE__) . '/minify/CSS/UriRewriter.php' ); - require_once( dirname(__FILE__) . '/minify/CSS/Compressor.php' ); - } - - $unminified_size = 0; - $srcs = wp_list_pluck( $deps, 'src' ); - - // Get the contents of each script - $contents_for_each_dep = array(); - foreach ( $srcs as $src ) { - - if ( ! preg_match( '|^(https?:)?//|', $src ) ) { - $src = site_url( $src ); - } - - // First attempt to get the file from the filesystem - $contents = false; - $is_self_hosted = self::is_self_hosted_src( $src ); - if ( $is_self_hosted ) { - $src_abspath = ltrim( parse_url( $src, PHP_URL_PATH ), '/' ); - $src_abspath = path_join( $_SERVER['DOCUMENT_ROOT'], $src_abspath ); - $contents = file_get_contents( $src_abspath ); - } - - // Dependency is not self-hosted or it the filesystem read failed, so do HTTP request - if ( false === $contents ) { - $r = wp_remote_get( $src ); - if ( is_wp_error($r) ) { - throw new Exception("Failed to retrieve $src: " . $r->get_error_message()); - } elseif ( intval( wp_remote_retrieve_response_code( $r ) ) !== 200 ) { - throw new Dependency_Minification_Exception( sprintf('Request for %s returned with HTTP %d %s', $src, wp_remote_retrieve_response_code( $r ), wp_remote_retrieve_response_message( $r )) ); - } - $contents = wp_remote_retrieve_body( $r ); - } - $unminified_size += strlen( $contents ); - - // Remove the BOM - $contents = preg_replace("/^\xEF\xBB\xBF/", '', $contents); - - // Rewrite relative paths in CSS - $src_dir_path = dirname( parse_url( $src, PHP_URL_PATH ) ); - if ( 'styles' === $type && is_dir( ABSPATH . $src_dir_path ) ) { - $contents = Minify_CSS_UriRewriter::rewrite( $contents, ABSPATH . $src_dir_path ); - } - - $contents_for_each_dep[$src] = $contents; - } - - $contents = ''; - - // Print a manifest of the dependencies - $contents .= sprintf("/*! This minified dependency bundle includes:\n"); - $i = 0; - foreach ( $srcs as $src ) { - $i += 1; - $contents .= sprintf( " * %02d. %s\n", $i, $src ); - } - $contents .= sprintf(" */\n\n"); - - // Minify - // Note: semicolon needed in case a file lacks trailing semicolon - // like `x = {a:1}` and the next file is IIFE (function(){}), - // then it would get combined as x={a:1}(function(){}) and attempt - // to pass the anonymous function into a function {a:1} which - // is of course an object and not a function. Culprit here - // is the comment-reply.js in WordPress. - if ( 'scripts' === $type ) { - $minified_contents = join( "\n;;\n", $contents_for_each_dep ); - $minified_contents = JSMin::minify($minified_contents); - if ( false === $minified_contents ) { - throw new Dependency_Minification_Exception( 'JavaScript parse error' ); - } - } elseif ( 'styles' === $type ) { - $minified_contents = join( "\n\n", $contents_for_each_dep ); - $minified_contents = Minify_CSS_Compressor::process($minified_contents); - } - - $contents .= $minified_contents; - $cached['unminified_size'] = $unminified_size; - $max_age = apply_filters( 'dependency_minification_cache_control_max_age', (int) self::$options['cache_control_max_age_cache'], $srcs ); - $cached['contents'] = $contents; - $cached['expires'] = time() + $max_age; - $cached['error'] = null; - } - catch (Exception $e) { - error_log( sprintf( '%s in %s: %s for srcs %s', - get_class( $e ), - __FUNCTION__, - $e->getMessage(), - join( ',', $srcs ) - ) ); - $cached['error'] = $e->getMessage(); - $max_age = apply_filters( 'dependency_minification_cache_control_max_age_error', (int) self::$options['cache_control_max_age_error'], $srcs ); - $cached['expires'] = time() + $max_age; - } - $cached['etag'] = self::generate_etag( $src_hash, $ver_hash ); - $cached['last_modified'] = time(); - $cached['pending'] = false; - - if ( false === get_option( $cache_option_name ) ) { - add_option( $cache_option_name, $cached, '', 'no' ); - } else { - update_option( $cache_option_name, $cached ); - } + public static function deactivate() { + self::remove_rewrite_rule(); + flush_rewrite_rules(); } + /*** SingleTone ***********************************************************/ + /** - * Handle a request for the minified resource + * @return void + * @since 1.0 */ - static function handle_request() { - $src_hash = get_query_var( 'depmin_src_hash' ); - $ext = get_query_var( 'depmin_file_ext' ); - if ( empty( $src_hash ) || empty( $ext ) ) { - return; - } + public static function instance() { + + static $instance; + + if ( is_null( $instance ) ) { + + $instance = new Dependency_Minification(); + $instance->load_includes(); + $instance->setup_actions(); + $instance->setup(); - try { - ob_start(); - - if ( 'js' === $ext ) { - header( 'Content-Type: application/javascript; charset=utf-8' ); - } else { - header( 'Content-Type: text/css; charset=utf-8' ); - } - - $cache_option_name = self::get_cache_option_name( $src_hash ); - $cached = get_option($cache_option_name); - if ( empty( $cached ) ) { - throw new Dependency_Minification_Exception( 'Unknown minified dependency bundle.', 404 ); - } - if ( ! empty( $cached['error'] ) ) { - throw new Dependency_Minification_Exception( $cached['error'], 500 ); - } - - // Send the response headers for caching - header( 'Expires: ' . str_replace('+0000', 'GMT', gmdate('r', $cached['expires'])) ); - if ( ! empty( $cached['last_modified'] ) ) { - header( 'Last-Modified: ' . str_replace('+0000', 'GMT', gmdate('r', $cached['last_modified'])) ); - } - if ( ! empty( $cached['etag'] ) ) { - header( 'ETag: ' . $cached['etag'] ); - } - - $is_not_modified = false; - if ( time() < $cached['expires'] ) { - $is_not_modified = self::$options['allow_not_modified_responses'] && ( - ( - ! empty( $_SERVER['HTTP_IF_NONE_MATCH'] ) - && - ! empty( $cached['etag'] ) - && - trim( $_SERVER['HTTP_IF_NONE_MATCH'] ) === $cached['etag'] - ) - || - ( - ! empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) - && - ! empty( $cached['last_modified'] ) - && - strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) <= $cached['last_modified'] - ) - ); - } - - if ( $is_not_modified ) { - status_header(304); - } else { - status_header(200); - $out = $cached['contents']; - - global $compress_scripts, $compress_css; - script_concat_settings(); - $compress = ( 'js' === $ext ? $compress_scripts : $compress_css ); - $force_gzip = ( $compress && defined('ENFORCE_GZIP') && ENFORCE_GZIP ); - - // Copied from /wp-admin/load-scripts.php - if ( $compress && ! ini_get('zlib.output_compression') && 'ob_gzhandler' != ini_get('output_handler') && isset($_SERVER['HTTP_ACCEPT_ENCODING']) ) { - header('Vary: Accept-Encoding'); // Handle proxies - if ( false !== stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') && function_exists('gzdeflate') && ! $force_gzip ) { - header('Content-Encoding: deflate'); - $out = gzdeflate( $out, 3 ); - } elseif ( false !== stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') && function_exists('gzencode') ) { - header('Content-Encoding: gzip'); - $out = gzencode( $out, 3 ); - } - } - - print $out; // xss ok - } - ob_end_flush(); - } - catch(Exception $e) { - ob_end_clean(); - $status = null; - $message = ''; - if ( $e instanceof Dependency_Minification_Exception || self::$options['show_error_messages'] ) { - $status = $e->getCode(); - $message = $e->getMessage(); - } else { - error_log( sprintf('%s: %s via URI %s', __METHOD__, $e->getMessage(), esc_url_raw( $_SERVER['REQUEST_URI'] )) ); - $message = 'Unexpected error occurred.'; - } - if ( empty($status) ) { - $status = 500; - } - status_header( $status ); - nocache_headers(); - header( 'Content-Type: text/plain' ); - print $message; // xss ok } - exit; + + return $instance; } } -add_action( 'plugins_loaded', array( 'Dependency_Minification', 'setup' ), 100 ); -register_activation_hook( __FILE__, array( 'Dependency_Minification', 'activate' ) ); +/** + * @since 1.0 + */ +class DepMin_Exception extends Exception {} + +// Register the plugin activation and deactivation hooks. register_deactivation_hook( __FILE__, array( 'Dependency_Minification', 'deactivate' ) ); +register_activation_hook( __FILE__, array( 'Dependency_Minification', 'activate' ) ); + +// Hook the plugin early onto the 'plugins_loaded' action. +add_action( 'plugins_loaded', array( 'Dependency_Minification', 'instance' ), 100 ); -class Dependency_Minification_Exception extends Exception {} +// Register the plugin classes autoloader. +spl_autoload_register( array( 'Dependency_Minification', 'autoload' ) ); \ No newline at end of file diff --git a/inc/admin.php b/inc/admin.php new file mode 100644 index 0000000..f5758bf --- /dev/null +++ b/inc/admin.php @@ -0,0 +1,596 @@ +page_hook = add_submenu_page( + self::PARENT_PAGE, + __( 'Dependency Minification', 'dependency-minification' ), + __( 'Dep. Minification', 'dependency-minification' ), + Dependency_Minification::$options['admin_page_capability'], + self::PAGE_SLUG, + array( $this, 'page_content' ) + ); + + } + + /** + * @return void + * @since 1.0 + */ + public function page_content() { ?> + +
+ + + +

+ + + + page_tab_content_status() ?> + + page_tab_content_settings() ?> + +
+ + + + + + + + + getArrayCopy(); + + $options['exclude_dependencies'] = array_filter( preg_split( "#[\n\r]+#", esc_attr( $_POST['options']['exclude_dependencies'] ) ) ); + $options['disabled_on_conditions'] = ( isset( $_POST['options']['disabled_on_conditions'] ) ) + ? $_POST['options']['disabled_on_conditions'] + : array(); + $options['default_exclude_remote_dependencies'] = isset( $_POST['options']['default_exclude_remote_dependencies'] ); + + Dependency_Minification::$options->exchangeArray( $options ); + + } + + $redirect_url = add_query_arg( 'page', self::PAGE_SLUG, admin_url( self::PARENT_PAGE ) ); + $redirect_url = add_query_arg( 'updated', 1, $redirect_url ); + $redirect_url .= '#tab-settings'; + wp_redirect( $redirect_url ); + + die(); + } + + /** + * @action admin_enqueue_scripts + * @since 1.0 + */ + public function enqueue_scripts( $hook ) { + + if ( $hook !== $this->page_hook ) + return; + + wp_enqueue_style( 'depmin-admin', Dependency_Minification::url( 'admin.css' ), array(), Dependency_Minification::VERSION ); + wp_enqueue_script( 'depmin-admin', Dependency_Minification::url( 'admin.js' ), array( 'jquery' ), Dependency_Minification::VERSION ); + + } + + /** + * @action admin_notices + */ + public function admin_notices() { + + // Show a notice to notify user that pretty urls is disabled, hence the plugin won't work + if ( empty( $GLOBALS['wp_rewrite']->permalink_structure ) ) { ?> + +
+

%1$s: %2$s', + __( 'Dependency Minification', 'dependency-minification' ), + sprintf( + __( 'Pretty permalinks are not enabled in your %1$s, which is required for this plugin to operate. Select something other than Default (e.g. ?p=123)', 'dependency-minification' ), + sprintf( + '%2$s', + admin_url( 'options-permalink.php' ), + __( 'Permalinks Settings', 'dependency-minification' ) + ) + ) + ); // xss ok + ?>

+
+ + id !== $this->page_hook ) + return; + + if ( empty( $_GET['updated-action'] ) ) + return; + + if ( empty( $_GET['updated-count'] ) ) + return; + + $updated_count = intval( $_GET['updated-count'] ); + $updated_task = filter_input( INPUT_GET, 'updated-action' ); + ?> +
+ +

+ +

+ +

+ +
+ %s', esc_url( $admin_page_url ), esc_html__( 'Settings', 'dependency-minification' ) ); + array_push( $links, $admin_page_link ); + + } + + return $links; + } + +} \ No newline at end of file diff --git a/inc/cache.php b/inc/cache.php new file mode 100644 index 0000000..3c244cf --- /dev/null +++ b/inc/cache.php @@ -0,0 +1,271 @@ +get_all(); + } + + /** + * @return string + * @since 1.0 + */ + public static function get_key( $key ) { + return self::get_object()->get_key( $key ); + } + + /** + * @return bool + * @since 1.0 + */ + public static function exists( $key ) { + return self::get_object()->exists( $key ); + } + + /** + * @return bool + * @since 1.0 + */ + public static function delete( $key ) { + return self::get_object()->delete( $key ); + } + + /** + * @return bool + * @since 1.0 + */ + public static function set( $key, $value ) { + return self::get_object()->set( $key, $value ); + } + + /** + * @return bool + * @since 1.0 + */ + public static function add( $key, $value ) { + return self::get_object()->add( $key, $value ); + } + + /** + * @return bool + * @since 1.0 + */ + public static function replace( $key, $value ) { + return self::get_object()->replace( $key, $value ); + } + + /** + * @return mixed + * @since 1.0 + */ + public static function get( $key, $default = false ) { + return self::get_object()->get( $key, $default ); + } + + /** + * @return void + * @since 1.0 + */ + public static function set_object( $object ) { + + if ( $object instanceof DepMin_Cache_Interface ) + self::$object = $object; + + } + + /** + * @return DepMin_Cache_Interface + * @since 1.0 + */ + public static function get_object() { + + if ( is_null( self::$object ) ) + self::$object = new DepMin_Cache_Default(); + + return self::$object; + + } + +} + +/** + * @since 1.0 + */ +class DepMin_Cache_Default implements DepMin_Cache_Interface { + + /** + * @return array + * @since 1.0 + */ + public function get_all() { + + global $wpdb; + $list = array(); + + foreach ( $wpdb->get_col( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'depmin_cache_%'" ) as $key ) { + + if ( ( $value = get_option( $key ) ) ) + $list[ $key ] = $value; + + } + + return $list; + } + + /** + * @return string|bool + * @since 1.0 + */ + public function get_key( $key ) { + + if ( is_array( $key ) ) { + $key = DepMin_hash_array( $key ); + + } elseif ( is_object( $key ) ) { + $key = spl_object_hash( $key ); + + } + + if ( empty( $key ) ) + return false; + + return 'depmin_cache_' . trim( $key ); + } + + /** + * @return bool + * @since 1.0 + */ + public function set( $key, $value ) { + + $key = trim( $key ); + + if ( empty( $key ) ) + return false; + + if ( ! $this->exists( $key ) ) { + return $this->add( $key, $value ); + + } else { + return $this->replace( $key, $value ); + + } + + } + + /** + * @return bool + * @since 1.0 + */ + public function add( $key, $value ) { + + if ( empty( $key ) || $this->exists( $key ) ) + return false; + + return add_option( $key, $value, '', 'no' ); + } + + /** + * @return bool + * @since 1.0 + */ + public function replace( $key, $value ) { + + if ( empty( $key ) || ! $this->exists( $key ) ) + return false; + + return update_option( $key, $value ); + } + + /** + * @return mixed + * @since 1.0 + */ + public function get( $key, $default = false ) { + return get_option( $key, $default ); + } + + /** + * @return bool + * @since 1.0 + */ + public function exists( $key ) { + return (bool) $this->get( $key ); + } + + /** + * @return bool + * @since 1.0 + */ + public function delete( $key ) { + return delete_option( $key ); + } + +} + +/** + * @since 1.0 + */ +interface DepMin_Cache_Interface { + + /** + * @return array + * @since 1.0 + */ + public function get_all(); + + /** + * @return bool + * @since 1.0 + */ + public function exists( $key ); + + /** + * @return bool + * @since 1.0 + */ + public function delete( $key ); + + /** + * @return string + * @since 1.0 + */ + public function get_key( $key ); + + /** + * @return bool + * @since 1.0 + */ + public function set( $key, $value ); + + /** + * @return bool + * @since 1.0 + */ + public function add( $key, $value ); + + /** + * @return bool + * @since 1.0 + */ + public function replace( $key, $value ); + + /** + * @return mixed + * @since 1.0 + */ + public function get( $key, $default = false ); + +} diff --git a/inc/collation.php b/inc/collation.php new file mode 100644 index 0000000..f9259e2 --- /dev/null +++ b/inc/collation.php @@ -0,0 +1,347 @@ + false, + 'styles' => false, + ); + + /** + * @return void + * @since 1.0 + */ + public function __construct() { + + $disabled = ( + ( isset( Dependency_Minification::$options['disabled_on_conditions']['all'] ) && ! empty( Dependency_Minification::$options['disabled_on_conditions']['all'] ) ) + || ( isset( Dependency_Minification::$options['disabled_on_conditions']['loggedin'] ) && ! empty( Dependency_Minification::$options['disabled_on_conditions']['loggedin'] ) && is_user_logged_in() ) + || ( ! empty( Dependency_Minification::$options['disabled_on_conditions']['admin'] ) && is_user_logged_in() && current_user_can( 'manage_options' ) ) + || ( ! empty( Dependency_Minification::$options['disabled_on_conditions']['queryvar']['enabled'] ) + && ! empty( Dependency_Minification::$options['disabled_on_conditions']['queryvar']['enabled'] ) + && ! empty( $_GET[ Dependency_Minification::$options['disabled_on_conditions']['queryvar']['value'] ] ) + ) + ); + + if ( DepMin_is_frontend() && ! $disabled ) { + + add_filter( 'print_scripts_array', array( $this, 'filter_print_scripts_array' ) ); + add_filter( 'print_styles_array', array( $this, 'filter_print_styles_array' ) ); + } + + } + + /** + * @filter print_styles_array + * @return array + * @since 1.0 + */ + public function filter_print_styles_array( $handles ) { + $handles = $this->filter_print_dependency_array( $handles, 'styles' ); + return $handles; + } + + /** + * @filter print_scripts_array + * @return array + * @since 1.0 + */ + public function filter_print_scripts_array( $handles ) { + $handles = $this->filter_print_dependency_array( $handles, 'scripts' ); + return $handles; + } + + /** + * Separate external from internal (local) dependencies and then group the + * internal resources into maximal groups. + * @param array $handles + * @param string $type (scripts|styles) + * @return array + */ + public function filter_print_dependency_array( array $handles, $type ) { + assert( in_array($type, array( 'scripts', 'styles' ) ) ); + assert( isset($GLOBALS["wp_{$type}"]) ); + $wp_deps = &$GLOBALS["wp_{$type}"]; + assert( is_a($wp_deps, 'WP_Dependencies') ); + + /** + * Determine if minification is enabled for the provided $handles. + * Note that we cannot use the $concatenate_scripts global set by script_concat_settings + * because it is intended to only be used in the WP Admin + * Plugin is automatically disabled if pretty permalinks is not activated + */ + $disabled = Dependency_Minification::$options['disable_if_wp_debug'] ? ( defined( 'WP_DEBUG' ) && WP_DEBUG ) : false; + $disabled = $disabled || ( defined( 'DEPENDENCY_MINIFICATION_DEFAULT_DISABLED' ) && DEPENDENCY_MINIFICATION_DEFAULT_DISABLED ); + $disabled = apply_filters( 'dependency_minification_disabled', $disabled, $handles, $type ); + $disabled = apply_filters( "dependency_minification_disabled_{$type}", $disabled, $handles ); + $disabled = $disabled || empty( $GLOBALS['wp_rewrite']->permalink_structure ); + if ( $disabled ) { + return $handles; + } + + // @todo There should be a better way to determine which group we are in + $current_group = (int) $this->is_footer[$type]; // false => 0, true => 1 + + $handles_in_group = array(); + foreach ( $handles as $handle ) { + $must_process_handle = ( + $wp_deps->groups[$handle] === $current_group + || + // Handle case where script is erroneously enqueued without in_footer=true (here's lookin at you, PollDaddy) + ( + $wp_deps->groups[$handle] < $current_group + && + ! in_array( $handle, $wp_deps->done ) + ) + ); + + if ( $must_process_handle ) { + $handles_in_group[] = $handle; + } + } + + $filtered_handles = array(); + $groups = $this->group_dependencies_by_exclusion( $handles_in_group, $wp_deps ); + + foreach ( $groups as $group ) { + // $internal_groups as $extra => $handles_in_group + if ( empty( $group['handles'] ) ) { + continue; + } + + if ( $group['excluded'] ) { + $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); + continue; + } + $extra = empty( $group['extra'] ) ? array() : $group['extra']; + + $deps = array(); + foreach ( $group['handles'] as $handle ) { + $deps[] = array( + 'handle' => $handle, + 'src' => $wp_deps->registered[$handle]->src, + 'ver' => $wp_deps->registered[$handle]->ver, + ); + } + + $srcs = wp_list_pluck( $deps, 'src' ); + $vers = wp_list_pluck( $deps, 'ver' ); + + $src_hash = DepMin_hash_array( $srcs ); + $ver_hash = DepMin_hash_array( $vers ); + + $cached_ver_hash = null; + $cached = DepMin_Cache::get( DepMin_Cache::get_key( $src_hash ) ); + + if ( ! empty( $cached['deps'] ) ) { + $cached_ver_hash = DepMin_hash_array( wp_list_pluck( $cached['deps'], 'ver' ) ); + } + + $is_error = ( + ! empty( $cached['error'] ) + && + $ver_hash === $cached_ver_hash + && + time() < $cached['expires'] + ); + + $is_stale = ( + empty( $cached ) + || + time() > $cached['expires'] + || + $ver_hash !== $cached_ver_hash + ); + + if ( $is_error ) { + if ( Dependency_Minification::$options['show_error_messages'] ) { + print "\n\n" ); + } + $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); + } elseif ( $is_stale ) { + printf( "\n\n" ); + + // @todo We could store the info in the option, and just pass the cache key to the cron; this would allow reliable passing of request_uri + + $scheduled = time(); + $args = array_merge( + array( + 'expires' => false, + 'last_modified' => false, + 'etag' => false, + 'unminified_size' => false, + 'contents' => false, + 'pending' => true, + ), + compact( 'type', 'deps' ) + ); + + wp_schedule_single_event( $scheduled, DepMin_Minify::CRON_ACTION, array( $args ) ); + // The bundle is not ready yet, so re-use the existing dependencies + $filtered_handles = array_merge( $filtered_handles, $group['handles'] ); + } else { + $this->minified_count += 1; + $new_handle = sprintf( 'minified-%d', $this->minified_count ); + $filtered_handles[] = $new_handle; + $src = DepMin_Minify::get_minified_dependency_url( $deps, $type ); + + // Deps are registered without versions since the URL includes the version (ver_hash) + if ( 'scripts' === $type ) { + $in_footer = ! empty( $extra['group'] ); // @todo what if the group is not 0 or 1? + wp_register_script( $new_handle, $src, array(), null, $in_footer ); + } elseif ( 'styles' === $type ) { + wp_register_style( $new_handle, $src, array(), null, $extra['media'] ); + } + $wp_deps->set_group( $new_handle, /*recursive*/false, $current_group ); + $new_dep = $wp_deps->registered[$new_handle]; + $new_extra = array( + 'data' => '', + ); + foreach ( $group['handles'] as $handle ) { + + // Aggregate data from scripts (e.g. wp_localize_script) + if ( ! empty( $wp_deps->registered[$handle]->extra ) ) { + + foreach ( array_keys( $wp_deps->registered[$handle]->extra ) as $extra_key ) { + $data = $wp_deps->get_data( $handle, $extra_key ); + + if ( 'data' === $extra_key ) { + $new_extra['data'] .= "/* wp_localize_script($handle): */\n"; + $new_extra['data'] .= "$data\n\n"; + } else { + if ( isset( $new_extra[$extra_key] ) ) { + // The handles should have been grouped so that they have the same extras + assert( $new_extra[$extra_key] === $data ); + } + $new_extra[$extra_key] = $data; + } + } + } + + // Mark the handles as done for the resources that have been minified + $wp_deps->done[] = $handle; + } + + // Add aggregated extra to new dependency + foreach ( $new_extra as $key => $value ) { + $new_dep->add_data( $key, $value ); + } + } + } + + // @todo Must be a better way to do this + $this->is_footer[$type] = true; // for the next invocation + + return $filtered_handles; + + } + + /** + * @param array $handles + * @param WP_Dependencies $wp_deps + * @return array Two members, the 1st containing external handles and the 2nd containing internal handles + */ + public function group_dependencies_by_exclusion( $handles, WP_Dependencies $wp_deps ) { + $groups = array(); + + // First create groups based on whether they are excluded from minification + $last_was_excluded = null; + foreach ( $handles as $handle ) { + $src = $wp_deps->registered[$handle]->src; + $is_local = DepMin_is_self_hosted_src( $src ); + $is_excluded = ! $is_local && Dependency_Minification::$options['default_exclude_remote_dependencies']; + $is_excluded = $is_excluded || $this->is_url_included( $src, Dependency_Minification::$options['exclude_dependencies'] ); + $is_excluded = apply_filters( 'dependency_minification_excluded', $is_excluded, $handle, $src ); + + if ( $last_was_excluded !== $is_excluded ) { + $groups[] = array( + 'excluded' => $is_excluded, + 'handles' => array(), + ); + } + $groups[ count( $groups ) - 1 ]['handles'][] = $handle; + $last_was_excluded = $is_excluded; + } + + // Now divide up the groups to create bundles that share the same extras (e.g. stylesheet media or conditional) + $bundled_groups = array(); + foreach ( $groups as $group ) { + if ( $group['excluded'] ) { + $bundled_groups[] = $group; + } else { + $handles_bundles = $this->group_handles_by_extra( $group['handles'], $wp_deps ); + foreach ( $handles_bundles as $extra => $handles_bundle ) { + $bundled_groups[] = array( + 'excluded' => false, + 'extra' => unserialize( $extra ), + 'handles' => $handles_bundle, + ); + } + } + } + + return $bundled_groups; + } + + /** + * @todo This is only applicable for styles, right? The media and conditional extras. + * @param array $handles + * @param WP_Dependencies $wp_deps + * @return array Associative array where the keys are the args and extras + */ + public function group_handles_by_extra( array $handles, WP_Dependencies $wp_deps ) { + $bundles = array(); + foreach ( $handles as $handle ) { + $dep = &$wp_deps->registered[$handle]; + $extra = (array) $dep->extra; + if ( is_a( $wp_deps, 'WP_Styles' ) ) { + $extra['media'] = is_string( $dep->args ) ? $dep->args : 'all'; + } + unset($extra['suffix']); + unset($extra['rtl']); + unset($extra['data']); + // Default scripts are not assigned 'group', so we use the original 'deps->args' value + if ( is_a( $wp_deps, 'WP_Scripts' ) && empty( $extra['group'] ) && is_int( $dep->args ) ) { + $extra['group'] = $dep->args; + } + ksort( $extra ); + $key = serialize( $extra ); + $bundles[$key][] = $handle; + } + return $bundles; + } + + public function is_url_included( $needle, $haystack ) { + foreach ( $haystack as $entry ) { + if ( strpos( $needle, $entry ) !== false ) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/inc/handler.php b/inc/handler.php new file mode 100644 index 0000000..77b248c --- /dev/null +++ b/inc/handler.php @@ -0,0 +1,132 @@ +getCode(); + $message = $e->getMessage(); + } else { + error_log( + sprintf( + '%s: %s via URI %s', + __METHOD__, + $e->getMessage(), + esc_url_raw( $_SERVER['REQUEST_URI'] ) + ) + ); + $message = 'Unexpected error occurred.'; + } + if ( empty($status) ) { + $status = 500; + } + status_header( $status ); + nocache_headers(); + header( 'Content-Type: text/plain' ); + print $message; // xss ok + } + exit; + } + +} \ No newline at end of file diff --git a/inc/helpers.php b/inc/helpers.php new file mode 100644 index 0000000..a06d687 --- /dev/null +++ b/inc/helpers.php @@ -0,0 +1,220 @@ +set_url( $url ); + + if ( ! empty( $path ) ) + $this->set_path( $path ); + + if ( empty( $url ) && empty( $path ) ) + throw new DepMin_Exception( 'Specific the file path or URL' ); + + } + + /** + * @throw DepMin_Exception + * @return string|bool + * @since 1.0 + */ + public function is_self_hosted() { + + $url = $this->get_url(); + + if ( ! empty( $url ) ) { + + $parsed_url = parse_url( $url ); + + if ( empty( $parsed_url['host'] ) && substr( $parsed_url['path'], 0, 1 ) === '/' ) + return true; + + if ( ! empty( $parsed_url['host'] ) && $parsed_url['host'] === parse_url( get_site_url(), PHP_URL_HOST ) ) + return true; + + } + + return false; + } + + /** + * @throw DepMin_Exception + * @return string|bool + * @since 1.0 + */ + public function get_contents() { + + $contents = false; + $url = $this->get_url(); + $path = $this->get_path(); + + if ( ! empty( $path ) ) + $contents = file_get_contents( $path ); + + if ( false === $contents && ! empty( $url ) ) { + + $response = wp_remote_get( $url ); + + if ( is_wp_error( $response ) ) { + + throw new DepMin_Exception( + sprintf( 'Failed to retrieve {%s}: %s', + $url, + $response->get_error_message() + ) + ); + + } elseif ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + + throw new DepMin_Exception( + sprintf( 'Request for %s returned with HTTP %d %s', + $url, + wp_remote_retrieve_response_code( $response ), + wp_remote_retrieve_response_message( $response ) + ) + ); + + } + + $contents = wp_remote_retrieve_body( $response ); + + } + + // Remove the BOM + if ( ! empty( $contents ) ) + $contents = preg_replace( "/^\xEF\xBB\xBF/", '', $contents ); + + return $contents; + } + + /** + * @throw DepMin_Exception + * @return void + * @since 1.0 + */ + protected function set_path( $path ) { + + if ( ! file_exists( $path ) ) + throw new DepMin_Exception( 'Invalid file path' ); + + $this->path = $path; + + } + + /** + * @throw DepMin_Exception + * @return void + * @since 1.0 + */ + protected function set_url( $url ) { + + if ( ! DepMin_is_vaild_url( $url ) ) + throw new DepMin_Exception( 'Invalid file URL' ); + + $this->url = $url; + + } + + /** + * @return string + * @since 1.0 + */ + public function get_path() { + + if ( empty( $this->path ) && $this->is_self_hosted() ) { + + $this->path = ltrim( parse_url( $this->url, PHP_URL_PATH ), '/' ); + $this->path = path_join( $_SERVER['DOCUMENT_ROOT'], $this->path ); + + } + + return $this->path; + } + + /** + * @return string + * @since 1.0 + */ + public function get_url() { + return $this->url; + } + +} + +/** + * URL Validation. + * + * @param string URL to be validated + * @return bool Validation result + */ +function DepMin_is_vaild_url( $url ) { + return (bool) filter_var( utf8_uri_encode( $url ), FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED + FILTER_FLAG_HOST_REQUIRED ); +} + +/** + * @var bool + * @since 1.0 + */ +function DepMin_is_frontend() { + + return ! ( + is_admin() + || + in_array( $GLOBALS['pagenow'], array( 'wp-login.php', 'wp-register.php' ) ) + ); + +} + +/** + * @var string + * @since 1.0 + */ +function DepMin_hash_array( array $r ) { + return md5( serialize( $r ) ); +} + +/** + * @var bool + * @since 1.0 + */ +function DepMin_is_self_hosted_src( $src ) { + + $parsed_url = parse_url( $src ); + return ( + ( + empty( $parsed_url['host'] ) + && + substr( $parsed_url['path'], 0, 1 ) === '/' + ) + || + ( + ! empty( $parsed_url['host'] ) + && + $parsed_url['host'] === parse_url( get_home_url(), PHP_URL_HOST ) + ) + ); + +} \ No newline at end of file diff --git a/inc/minifier.php b/inc/minifier.php new file mode 100644 index 0000000..65d0ffb --- /dev/null +++ b/inc/minifier.php @@ -0,0 +1,361 @@ +args = wp_parse_args( $args, array( + // @TODO: Add more minification options. + 'type' => '', // 'styles' or 'scripts' + ) ); + + return $this; + + } + + /** + * @param DepMin_SrcInfo[] $srcs + * @return DepMin_Minifier + * @since 1.0 + */ + public function set_srcs( array $srcs ) { + + foreach( $srcs as $src ) { + + if ( ! is_a( $src, 'DepMin_SrcInfo' ) ) + $src = new DepMin_SrcInfo( $src ); + + $this->srcs[]= $src; + + } + + return $this; + } + + /** + * @return mixed + * @since 1.0 + */ + public function get_args( $key = '' ) { + + if ( ! empty( $key ) ) { + + if ( isset( $this->args[ $key ] ) ) + return $this->args[ $key ]; + + return false; + + } + + return $this->args; + + } + + /** + * @return DepMin_SrcInfo[] + * @since 1.0 + */ + public function get_srcs() { + return $this->srcs; + } + + /** + * @throw DepMin_Exception + * @return array + * @since 1.0 + */ + abstract function minify(); + +} + +/** + * @since 1.0 + */ +class DepMin_Minifier_Default extends DepMin_Minifier { + + /** + * @return string + * @since 1.0 + */ + private function get_unminified_contents() { + + $i = 0; + $unminified = ''; + + foreach( $this->get_srcs() as $source ) { + + $contents = $source->get_contents(); + + if ( 'styles' === $this->get_args( 'type' ) ) { + + $dir_path = dirname( $source->get_path() ); + + // Rewrite relative paths in CSS. + if ( ! empty( $dir_path ) ) { + require_once Dependency_Minification::path( 'minify/CSS/UriRewriter.php' ); + $contents = Minify_CSS_UriRewriter::rewrite( $contents, $dir_path ); + } + + } + + $unminified .= $contents; + + if ( $i < ( count( $this->get_srcs() ) - 1 ) ) { + + /* + * @note + * Semicolon needed in case a file lacks trailing semicolon + * like `x = {a:1}` and the next file is IIFE (function(){}), + * then it would get combined as x={a:1}(function(){}) and attempt + * to pass the anonymous function into a function {a:1} which + * is of course an object and not a function. Culprit here + * is the comment-reply.js in WordPress. + */ + switch( $this->get_args( 'type' ) ) { + + case 'scripts': + $unminified .= "\n;;\n"; + break; + + case 'styles': + $unminified .= "\n\n"; + break; + + } + + } + + $i++; + + } + + return $unminified; + } + + /** + * @return array + * @since 1.0 + */ + public function minify() { + + $minified = ''; + $unminified = $this->get_unminified_contents(); + + switch( $this->get_args( 'type' ) ) { + + case 'styles': + require_once Dependency_Minification::path( 'minify/CSS/Compressor.php' ); + $minified = Minify_CSS_Compressor::process( $unminified ); + break; + + case 'scripts': + require_once Dependency_Minification::path( 'minify/JS/JSMin.php' ); + $minified = JSMin::minify( $unminified ); + break; + + } + + return array( + 'contents' => $minified, + 'minified_size' => strlen( $minified ), + 'unminified_size' => strlen( $unminified ), + ); + + } + +} + +/** + * @since 1.0 + */ +class DepMin_Minify { + + /** + * @var string + * @since 1.0 + */ + const CRON_ACTION = 'minify_dependencies'; + + /** + * @return array|bool + * @since 1.0 + */ + public static function minify( array $srcs, $type ) { + + $minifier = apply_filters( 'DepMin_minifier_class', 'DepMin_Minifier_Default' ); + + if ( ! empty( $minifier ) && class_exists( $minifier ) ) { + + $minifier = new $minifier(); + $minifier->set_srcs( $srcs ) + ->set_args( array( + 'type' => $type, + ) ); + + return $minifier->minify(); + + } + + return false; + } + + /** + * @return void + * @since 1.0 + */ + public static function cron_action( $args = '' ) { + + $args = wp_parse_args( $args, array( + 'unminified_size' => false, + 'minified_size' => false, + 'last_modified' => false, + 'contents' => false, + 'expires' => false, + 'pending' => true, + 'deps' => array(), + 'etag' => false, + 'type' => '', + ) ); + + $srcs = wp_list_pluck( $args['deps'], 'src' ); + $vers = wp_list_pluck( $args['deps'], 'ver' ); + + $src_hash = DepMin_hash_array( $srcs ); + $ver_hash = DepMin_hash_array( $vers ); + + try { + + foreach( $srcs as &$src ) { + + if ( ! preg_match( '|^(https?:)?//|', $src ) ) + $src = site_url( $src ); + + } + + if ( ( $minified = DepMin_Minify::minify( $srcs, $args['type'] ) ) ) { + + $args['contents'] = "/*! This minified dependency bundle includes:\n"; + + foreach ( $srcs as $key => $src ) + $args['contents'] .= sprintf( " * %02d. %s\n", $key + 1, $src ); + + $args['contents'] .= " */\n\n" . $minified['contents']; + $args['unminified_size'] = $minified['unminified_size']; + $args['minified_size'] = $minified['minified_size']; + + } + + $max_age = apply_filters( 'dependency_minification_cache_control_max_age', + (int) Dependency_Minification::$options['cache_control_max_age_cache'], + $srcs + ); + + $args['expires'] = time() + $max_age; + $args['error'] = null; + + } catch ( Exception $e ) { + + error_log( + sprintf( + '%s in %s: %s for srcs %s', + get_class( $e ), + __FUNCTION__, + $e->getMessage(), + implode( ',', $srcs ) + ) + ); + + $args['error'] = $e->getMessage(); + + $max_age = apply_filters( 'dependency_minification_cache_control_max_age_error', + (int) Dependency_Minification::$options['cache_control_max_age_error'], + $srcs + ); + + $args['expires'] = time() + $max_age; + + } + + $args['etag'] = implode( '.', array( $src_hash, $ver_hash ) ); + $args['last_modified'] = time(); + $args['pending'] = false; + + DepMin_Cache::set( DepMin_Cache::get_key( $src_hash ), $args ); + + } + + /** + * @param array $deps + * @param string $type (scripts or styles) + * @return string + * @since 1.0 + */ + public static function get_minified_dependency_url( array $deps, $type ) { + $srcs = wp_list_pluck( $deps, 'src' ); + $vars = wp_list_pluck( $deps, 'ver' ); + $handles = wp_list_pluck( $deps, 'handle' ); + + $src = trailingslashit( home_url( Dependency_Minification::$options['endpoint'] ) ); + $src .= implode( '.', array( + implode( ',', $handles ), + DepMin_hash_array( $srcs ), + DepMin_hash_array( $vars ), + $type === 'scripts' ? 'js' : 'css', + ) ); + + return $src; + } + + /** + * @return array + * @since 1.0 + */ + public static function get_pending_dependencies() { + + $list = array(); + foreach ( _get_cron_array() as $cron ) { + if ( isset( $cron[ self::CRON_ACTION ] ) ) { + foreach ( $cron[ self::CRON_ACTION ] as $event ) { + + $data = reset( $event['args'] ); + + if ( ! empty( $data['pending'] ) ) + $list[] = $data; + + } + } + } + + return $list; + } + + /** + * @return void + * @since 1.0 + */ + public static function hook_cron_action() { + add_action( self::CRON_ACTION, array( __CLASS__, 'cron_action' ) ); + } + +} \ No newline at end of file diff --git a/inc/options.php b/inc/options.php new file mode 100644 index 0000000..8e7d028 --- /dev/null +++ b/inc/options.php @@ -0,0 +1,84 @@ +get_options(), array( + 'endpoint' => '_minify', + 'default_exclude_remote_dependencies' => true, + 'cache_control_max_age_cache' => 2629743, // 1 month in seconds + 'cache_control_max_age_error' => 60 * 60, // 1 hour, to try minifying again + 'allow_not_modified_responses' => true, // only needs to be true if not Akamaized and max-age is short + 'admin_page_capability' => 'edit_theme_options', + 'show_error_messages' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ), + 'disable_if_wp_debug' => true, + 'exclude_dependencies' => array(), + 'disabled_on_conditions' => array( + 'all' => false, + 'loggedin' => false, + 'admin' => false, + 'queryvar' => false, + ), + ) ); + + $options = apply_filters( 'dependency_minification_options', $options ); + + parent::__construct( $options ); + + } + + /** + * @access protected + * @return array + * @since 1.0 + */ + protected function get_options() { + return get_option( 'dependency_minification_options', array() ); + } + + /** + * @access protected + * @return bool + * @since 1.0 + */ + protected function set_options( array $options ) { + return update_option( 'dependency_minification_options', $options ); + } + + /*** ArrayObject Methods **************************************************/ + + public function exchangeArray( array $input ) { + + if ( $this->set_options( $input ) ) + return parent::exchangeArray( $input ); + + } + + public function offsetSet( $index, $newval ) { + + $options = $this->get_options(); + $options[ $index ] = $newval; + + if ( $this->set_options( $options ) ) + parent::offsetSet( $index, $newval ); + + } + + public function offsetUnset( $index ) { + + $options = $this->get_options(); + unset( $options[ $index ] ); + + if ( $this->set_options( $options ) ) + parent::offsetUnset( $index ); + + } + +} diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..50eddd7 --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,8 @@ + + + We want all WordPress standards but we want to exclude the vendor directories + + /minify/* + + +