'JS Compression', 'description' => 'Adjust JS Compression settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('advagg_js_compress_admin_settings_form'), 'type' => MENU_LOCAL_TASK, 'access arguments' => array('administer site configuration'), 'file path' => $file_path, 'file' => 'advagg_js_compress.admin.inc', 'weight' => 10, ); $items[$config_path . '/advagg/js-compress/batch'] = array( 'title' => 'Batch Generate', 'page callback' => 'advagg_js_compress_batch_callback', 'access arguments' => array('administer site configuration'), ); return $items; } /** * Implements hook_module_implements_alter(). */ function advagg_js_compress_module_implements_alter(&$implementations, $hook) { // Move advagg_js_compress below advagg. if ($hook === 'advagg_save_aggregate_alter' && array_key_exists('advagg_js_compress', $implementations)) { $advagg_key = ''; $advagg_js_compress_key = ''; $counter = 0; foreach ($implementations as $key => $value) { if ($key == 'advagg') { $advagg_key = $counter; } if ($key == 'advagg_js_compress') { $advagg_js_compress_key = $counter; } $counter++; } if ($advagg_js_compress_key > $advagg_key) { // Move advagg_js_compress to the top. $item = array('advagg_js_compress' => $implementations['advagg_js_compress']); unset($implementations['advagg_js_compress']); $implementations = array_merge($item, $implementations); // Move advagg to the very top. $item = array('advagg' => $implementations['advagg']); unset($implementations['advagg']); $implementations = array_merge($item, $implementations); } } } /** * @} End of "addtogroup hooks". */ /** * @addtogroup advagg_hooks * @{ */ /** * Implements hook_advagg_current_hooks_hash_array_alter(). */ function advagg_js_compress_advagg_current_hooks_hash_array_alter(&$aggregate_settings) { $aggregate_settings['variables']['advagg_js_compressor'] = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR); $aggregate_settings['variables']['advagg_js_compress_packer'] = variable_get('advagg_js_compress_packer', ADVAGG_JS_COMPRESS_PACKER); $aggregate_settings['variables']['advagg_js_compress_ratio'] = variable_get('advagg_js_compress_ratio', ADVAGG_JS_COMPRESS_RATIO); $aggregate_settings['variables']['advagg_js_compress_max_ratio'] = variable_get('advagg_js_compress_max_ratio', ADVAGG_JS_COMPRESS_MAX_RATIO); $aggregate_settings['variables']['advagg_js_compressor_file_settings'] = variable_get('advagg_js_compressor_file_settings', array()); $aggregate_settings['variables']['advagg_js_compress_add_license'] = variable_get('advagg_js_compress_add_license', ADVAGG_JS_COMPRESS_ADD_LICENSE); } /** * Implements hook_advagg_modify_js_pre_render_alter(). * * Used compress inline js. */ function advagg_js_compress_advagg_modify_js_pre_render_alter(&$children, &$elements) { // Get variables. $aggregate_settings['variables']['advagg_js_compressor'] = variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE); // Do nothing if the compressor is disabled. if (empty($aggregate_settings['variables']['advagg_js_compressor'])) { return; } // Do nothing if the page is not cacheable and inline compress if not // cacheable is not checked. if (!variable_get('advagg_js_compress_inline_if_not_cacheable', ADVAGG_JS_COMPRESS_INLINE_IF_NOT_CACHEABLE) && !drupal_page_is_cacheable()) { return; } // Compress any inline JS. module_load_include('inc', 'advagg_js_compress', 'advagg_js_compress.advagg'); foreach ($children as &$values) { // Compress onload. if (!empty($values['#attributes']['onload'])) { $contents = $values['#attributes']['onload']; $filename = drupal_hash_base64($contents); advagg_js_compress_prep($contents, $filename, $aggregate_settings, FALSE); $values['#attributes']['onload'] = $contents; } // Compress onerror. if (!empty($values['#attributes']['onerror'])) { $contents = $values['#attributes']['onerror']; $filename = drupal_hash_base64($contents); advagg_js_compress_prep($contents, $filename, $aggregate_settings, FALSE); $values['#attributes']['onerror'] = $contents; } // Compress inline. if (!empty($values['#value'])) { $contents = $values['#value']; $filename = drupal_hash_base64($contents); advagg_js_compress_prep($contents, $filename, $aggregate_settings, FALSE); $values['#value'] = $contents; } } unset($values); } /** * @} End of "addtogroup advagg_hooks". */ /** * @addtogroup 3rd_party_hooks * @{ */ /** * Implements hook_libraries_info(). */ function advagg_js_compress_libraries_info() { $libraries['JShrink'] = array( 'name' => 'JShrink', 'vendor url' => 'https://github.com/tedious/JShrink', 'download url' => 'https://github.com/tedious/JShrink/archive/master.zip', 'local version' => '1.2.0', 'version' => '1.2.0', 'files' => array( 'php' => array( 'src/JShrink/Minifier.php', ), ), ); $libraries['jsqueeze'] = array( 'name' => 'JSqueeze', 'vendor url' => 'https://github.com/tchwork/jsqueeze', 'download url' => 'https://github.com/tchwork/jsqueeze/archive/master.zip', 'local version' => '2.0.5', 'version' => '2.0.5', 'files' => array( 'php' => array( 'src/JSqueeze.php', ), ), ); $libraries['jsminplus'] = array( 'vendor url' => 'https://github.com/JSMinPlus/JSMinPlus', 'download url' => 'https://github.com/JSMinPlus/JSMinPlus/archive/master.zip', 'name' => 'JSMinPlus', 'local version' => '1.4', 'version arguments' => array( 'file' => 'jsminplus.php', 'pattern' => '/JSMinPlus\s+version\s+([0-9a-zA-Z\.-]+)/', 'lines' => 10, 'cols' => 40, ), 'files' => array( 'php' => array( 'jsminplus.php', ), ), ); $libraries['jspacker'] = array( 'vendor url' => 'https://github.com/tholu/php-packer', 'download url' => 'https://github.com/tholu/php-packer/archive/master.zip', 'name' => 'JSPacker', 'local version' => '1.1', 'version arguments' => array( 'file' => 'src/Packer.php', 'pattern' => '/\.\s+version\s+([0-9a-zA-Z\.-]+)/', 'lines' => 4, 'cols' => 40, ), 'files' => array( 'php' => array( 'src/Packer.php', ), ), ); return $libraries; } /** * @} End of "addtogroup 3rd_party_hooks". */ /** * Test a file, making sure it is compressible. * * @param string $filename * Path and filename of the js file to test. * @param array $compressors * List of compressors to test. * @param string $cache_id * The cache ID for this file. * * @return array * Array showing the results of the compression tests. */ function advagg_js_compress_test_file($filename, array $compressors, $cache_id) { $contents = (string) @advagg_file_get_contents($filename); // Get the JS string length before the compression operation. $contents_before = $contents; $before = strlen($contents); module_load_include('inc', 'advagg_js_compress', 'advagg_js_compress.advagg'); $results = array(); foreach ($compressors as $key => $name) { $contents = $contents_before; $aggregate_settings['variables']['advagg_js_compressor'] = $key; // Compress it. $no_errors = advagg_js_compress_prep($contents, $filename, $aggregate_settings, FALSE, FALSE, FALSE, TRUE); $after = strlen($contents); $ratio = 0; if ($before != 0) { $ratio = ($before - $after) / $before; } // Set to "-1" if the compressor threw an error. if ($no_errors === FALSE) { $results[$key] = array( 'code' => -1, 'ratio' => round($ratio, 5), 'name' => $name, ); } // Set to "-2" if compression ratio sucks (it's already compressed). elseif ($ratio < 0.001) { $results[$key] = array( 'code' => -2, 'ratio' => round($ratio, 5), 'name' => $name, ); } // Set to "-3" if the compression ratio is way too good (bad js output). elseif ($ratio > 0.999) { $results[$key] = array( 'code' => -3, 'ratio' => round($ratio, 5), 'name' => $name, ); } // Set to "1". Everything worked, mark this file as compressible. else { $results[$key] = array( 'code' => 1, 'ratio' => round($ratio, 5), 'name' => $name, ); } } $cache = cache_get($cache_id, 'cache_advagg_info'); // Merge in old cached data. if (!empty($cache->data)) { // Do not merge in -1 code. foreach ($cache->data as $key => $value) { if ($value['code'] == -1) { unset($cache->data[$key]); } } $results += $cache->data; } // CACHE_PERMANENT isn't good here. Use 2 weeks from now + 0-45 days. // The random 0 to 45 day addition is to prevent a cache stampeed. cache_set($cache_id, $results, 'cache_advagg_info', round(REQUEST_TIME + 1209600 + mt_rand(0, 3888000), -3)); return $results; } /** * Generate the js compress configuration. * * @return array * Array($options, $description, $compressors, $functions). */ function advagg_js_compress_configuration() { // Set the defaults. $description = ''; $options = array( 0 => t('Disabled'), 1 => t('JSMin+ ~1300ms'), // 2 => t('Packer ~500ms'), // 3 is JSMin c extension. // 4 is JShrink. // 5 is JSqueeze. ); if (function_exists('jsmin')) { $options[3] = t('JSMin ~2ms'); $description .= t('JSMin is the very fast C complied version. Recommend using it.'); } else { if (!defined('PHP_VERSION_ID') || constant('PHP_VERSION_ID') < 50310) { $link = 'http://www.ypass.net/software/php_jsmin/'; } else { $link = 'https://github.com/sqmk/pecl-jsmin/'; } $description .= t('You can use the much faster C version of JSMin (~2ms) by installing the JSMin PHP Extension on this server.', array('@php_jsmin' => $link)); } // Add in JShrink and JSqueeze if using php 5.3 or higher. if (defined('PHP_VERSION_ID') && constant('PHP_VERSION_ID') >= 50300) { $options += array( 4 => t('JShrink ~1000ms'), 5 => t('JSqueeze ~600ms'), ); } $compressors = array( 1 => 'jsminplus', 2 => 'packer', ); if (function_exists('jsmin')) { $compressors[3] = 'jsmin'; } if (defined('PHP_VERSION_ID') && constant('PHP_VERSION_ID') >= 50300) { $compressors += array( 4 => 'jshrink', 5 => 'jsqueeze', ); } $functions = array( 1 => 'advagg_js_compress_jsminplus', 2 => 'advagg_js_compress_jspacker', 3 => 'advagg_js_compress_jsmin', 4 => 'advagg_js_compress_jshrink', 5 => 'advagg_js_compress_jsqueeze', ); // Allow for other modules to alter this list. $options_desc = array($options, $description); // Call hook_advagg_js_compress_configuration_alter(). drupal_alter('advagg_js_compress_configuration', $options_desc, $compressors, $functions); list($options, $description) = $options_desc; return array($options, $description, $compressors, $functions); } /** * Get all js files and js files that are not compressed. * * @return array * Array($list, $redo_list). */ function advagg_js_compress_all_js_files_list() { // Get all files stored in the database. $result = db_select('advagg_files', 'af') ->fields('af') ->condition('filetype', 'js') ->orderBy('filename', 'ASC') ->execute(); if (empty($result)) { return array(); } module_load_include('inc', 'advagg_js_compress', 'advagg_js_compress.advagg'); $compressor_list = advagg_js_compress_get_enabled_compressors(array(), -1); $compressor_list_count = count($compressor_list); // Check if files have been compressed. module_load_include('inc', 'advagg', 'advagg'); $redo_list = array(); $failed_redo_list = array(); $list = array(); foreach ($result as $row) { $row = (array) $row; // Check cache for jsmin info. $info = advagg_get_info_on_file($row['filename']); if ($info['filesize'] == 0) { continue; } $list[] = $info; // No jsmin info or incomplete data => rerun compression tests. if (empty($info['advagg_js_compress']) || count($info['advagg_js_compress']) !== $compressor_list_count) { $redo_list[] = $info; continue; } $empty_ratio_count = 0; $bad_compression_count = 0; foreach ($info['advagg_js_compress'] as $values) { if (empty($values['ratio'])) { if ($values['code'] != -1) { $empty_ratio_count++; } else { $bad_compression_count++; } } } // More than one compressor has an empty ratio. if ($empty_ratio_count > 1) { $failed_redo_list[] = $info; } // All failed; try again. if ($bad_compression_count == count($info['advagg_js_compress'])) { $failed_redo_list[] = $info; } } // Filter out .min.js if they have already been ran. $reversed_needle = strrev('.min.js'); foreach ($failed_redo_list as $key => $info) { if (stripos(strrev($row['filename']), $reversed_needle) === 0 && !empty($info['advagg_js_compress'][2]['ratio']) ) { unset($failed_redo_list[$key]); } } $redo_list = array_merge($redo_list, $failed_redo_list); return array($list, $redo_list); } /** * Get all js files and js files that are not compressed. * * @param array $redo_list * JS files that need to be compressed. * @param int $max_time * Max amount of time to spend on compressing. * @param bool $drush * Set to TRUE to output drush info when running. * * @return array * Array($list, $redo_list). */ function advagg_js_compress_redo_files(array $redo_list, $max_time = 30, $drush = FALSE) { // Get the compressor list and start the clock. module_load_include('inc', 'advagg_js_compress', 'advagg_js_compress.advagg'); $compressor_list = advagg_js_compress_get_enabled_compressors(array(), -1); shuffle($redo_list); $time_start = microtime(TRUE); if ($drush && (!is_callable('drush_log') || !is_callable('dt'))) { $drush = FALSE; } // Change settings for testing. if (isset($GLOBALS['conf']['advagg_js_compress_force_run'])) { $advagg_js_compress_force_run = $GLOBALS['conf']['advagg_js_compress_force_run']; } if (isset($GLOBALS['conf']['advagg_js_compress_add_license'])) { $advagg_js_compress_add_license = $GLOBALS['conf']['advagg_js_compress_add_license']; } if (isset($GLOBALS['conf']['httprl_background_callback'])) { $httprl_background_callback = $GLOBALS['conf']['httprl_background_callback']; } $GLOBALS['conf']['advagg_js_compress_force_run'] = TRUE; $GLOBALS['conf']['advagg_js_compress_add_license'] = 0; $GLOBALS['conf']['httprl_background_callback'] = FALSE; $counter = array(); foreach ($redo_list as $key => $values) { // Test the files for up to 30 seconds. $filenames_info = array(); $filenames_info[$values['data']] = $values; $compressors = $compressor_list; if (isset($values['compressors'])) { $compressors = $values['compressors']; } if ($drush) { drush_log(dt('Compressing @data.', array( '@data' => $values['data'], )), 'ok'); } // Prime cache. advagg_js_compress_run_mutiple_tests($filenames_info, $compressors); // Add to cache. advagg_get_info_on_file($values['data'], TRUE, TRUE); $counter[$key] = $values; // Stop after 30 seconds of processing. $time_end = microtime(TRUE); $time = $time_end - $time_start; if (!empty($max_time) && $time > $max_time) { break; } } // Put them back to normal. if (isset($advagg_js_compress_force_run)) { $GLOBALS['conf']['advagg_js_compress_force_run'] = $advagg_js_compress_force_run; } if (isset($advagg_js_compress_add_license)) { $GLOBALS['conf']['advagg_js_compress_add_license'] = $advagg_js_compress_add_license; } if (isset($httprl_background_callback)) { $GLOBALS['conf']['httprl_background_callback'] = $httprl_background_callback; } return $counter; } /** * The batch callback. */ function advagg_js_compress_batch_callback() { $batch = array( 'operations' => array(), 'finished' => 'advagg_js_compress_batch_done', 'title' => t('Batch JS Minification'), 'init_message' => t('Starting'), 'progress_message' => t('Processed @current out of @total.'), 'error_message' => t('JS minification has encountered an error.'), ); list($list, $redo_list) = advagg_js_compress_all_js_files_list(); $config_path = advagg_admin_config_root_path(); if (empty($redo_list)) { $redo_list = $list; } foreach ($redo_list as $redo) { $batch['operations'][] = array('advagg_js_compress_batch_process', array($redo)); } batch_set($batch); // The path to redirect to when done. batch_process($config_path . '/advagg/js-compress'); } /** * The batch processor. */ function advagg_js_compress_batch_process($redo, &$context) { // Give it up to 3 minutes. $max_time = ini_get('max_execution_time'); if ($max_time != 0 && $max_time < 180) { set_time_limit(180); } ignore_user_abort(TRUE); // Display a progress message... $context['message'] = t("Now processing @filename...", array('@filename' => $redo['data'])); // Do heavy lifting here... advagg_js_compress_redo_files(array($redo)); } /** * The batch finish handler. */ function advagg_js_compress_batch_done($success, $results, $operations) { if ($success) { drupal_set_message(t('Done!')); } else { $error_operation = reset($operations); $message = t('An error occurred while processing %error_operation with arguments: @arguments', array( '%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE), )); drupal_set_message($message, 'error'); } }