Same filename in other branches
  1. 7.x-2.x advagg_js_compress/advagg_js_compress.module

Advanced CSS/JS aggregation js compression module.

File

advagg_js_compress/advagg_js_compress.module

View source
<?php


/**
 * @file
 * Advanced CSS/JS aggregation js compression module.
 *
 */

/**
 * Default value to see if the callback is working.
 */
define('ADVAGG_JS_COMPRESS_CALLBACK', FALSE);

/**
 * Default value to see packer is enabled.
 */
define('ADVAGG_JS_COMPRESS_PACKER_ENABLE', FALSE);

/**
 * Default value to see what compressor to use. 0 is JSMin+.
 */
define('ADVAGG_JS_COMPRESSOR', 0);

/**
 * Default value for the compression ratio test.
 */
define('ADVAGG_JS_COMPRESS_RATIO', 0.1);

/**
 * Default value for the compression ratio test.
 */
define('ADVAGG_JS_MAX_COMPRESS_RATIO', 0.98);

/**
 * Default value to see if this will compress aggregated files.
 */
define('ADVAGG_JS_COMPRESS_AGG_FILES', TRUE);

/**
 * Default value to see if this will compress inline js.
 */
define('ADVAGG_JS_COMPRESS_INLINE', TRUE);

/**
 * Default value to see if this will cache the compressed inline js.
 */
define('ADVAGG_JS_COMPRESS_INLINE_CACHE', TRUE);

/**
 * Default value to see if this will cache the compressed inline js.
 */
define('ADVAGG_JS_COMPRESS_FILE_CACHE', TRUE);

/**
 * Implements hook_menu().
 */
function advagg_js_compress_menu() {
    $items = array();
    $file_path = drupal_get_path('module', 'advagg_js_compress');
    $items['advagg/js_compress_test_file'] = array(
        'page callback' => 'advagg_js_compress_test_file',
        'type' => MENU_CALLBACK,
        'access callback' => TRUE,
    );
    $items['admin/config/advagg/js-compress'] = array(
        'title' => 'JS Compression',
        'description' => 'Adjust JS Compression settings.',
        'page callback' => 'advagg_js_compress_admin_page',
        'type' => MENU_LOCAL_TASK,
        'access arguments' => array(
            'administer site configuration',
        ),
        'file path' => $file_path,
        'file' => 'advagg_js_compress.admin.inc',
        'weight' => 10,
    );
    return $items;
}

/**
 * Implements hook_init().
 */
function advagg_js_compress_init() {
    global $conf;
    if (variable_get('advagg_js_compress_packer_enable', ADVAGG_JS_COMPRESS_PACKER_ENABLE)) {
        $conf['advagg_file_save_function'] = 'advagg_js_compress_file_saver';
    }
}

/**
 * Implements hook_advagg_files_table().
 */
function advagg_js_compress_advagg_files_table($row, $checksum) {
    // IF the file has changed, test it's compressibility.
    if ($row['filetype'] = 'js' && $checksum != $row['checksum']) {
        $files_to_test[] = array(
            'md5' => $row['filename_md5'],
            'filename' => $row['filename'],
        );
        advagg_js_compress_test_compression($files_to_test);
    }
}

/**
 * Implements hook_advagg_js_pre_alter().
 */
function advagg_js_compress_advagg_js_pre_alter(&$javascript, $preprocess_js, $public_downloads, $scope) {
    if (module_exists('jquery_update')) {
        return;
    }
    foreach ($javascript as $type => $data) {
        if (!$data) {
            continue;
        }
        if ($type == 'setting' || $type == 'inline') {
            continue;
        }
        foreach ($data as $path => $info) {
            if ($path == 'misc/jquery.form.js') {
                $new_path = drupal_get_path('module', 'advagg_js_compress') . '/jquery.form.js';
                $javascript[$type][$new_path] = $info;
                unset($javascript[$type][$path]);
            }
        }
    }
}

/**
 * Implements hook_advagg_js_alter().
 */
function advagg_js_compress_advagg_js_alter(&$contents, $files, $bundle_md5) {
    if (!variable_get('advagg_js_compress_agg_files', ADVAGG_JS_COMPRESS_AGG_FILES)) {
        return;
    }
    advagg_js_compress_prep($contents, $files, $bundle_md5);
}

/**
 * Implements hook_advagg_js_inline_alter().
 */
function advagg_js_compress_advagg_js_inline_alter(&$contents) {
    if (!variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE)) {
        return;
    }
    $compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
    // If using a cache, try to get the contents of it.
    if (variable_get('advagg_js_compress_inline_cache', ADVAGG_JS_COMPRESS_INLINE_CACHE)) {
        $key = md5($contents) . $compressor;
        $table = 'cache_advagg_js_compress_inline';
        $data = cache_get($key, $table);
        if (!empty($data->data)) {
            $contents = $data->data;
            return;
        }
    }
    if ($compressor == 0) {
        $original_contents = $contents;
        list($before, $after) = advagg_js_compress_jsminplus($contents);
        $ratio = ($before - $after) / $before;
        // Make sure the returned string is not empty or has a VERY high
        // compression ratio.
        if (empty($contents) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
            $contents = $original_contents;
        }
    }
    if ($compressor == 1) {
        $contents = jsmin($contents);
    }
    // If using a cache set it.
    if (isset($key)) {
        cache_set($key, $contents, $table, CACHE_TEMPORARY);
    }
}

/**
 * Compress a JS string
 *
 * @param $contents
 *   Javascript string.
 */
function advagg_js_compress_prep(&$contents, $files, $bundle_md5) {
    // Make sure every file in this aggregate is compressible.
    $files_to_test = array();
    $list_bad = array();
    foreach ($files as $filename) {
        $filename_md5 = md5($filename);
        $data = advagg_get_file_data($filename_md5);
        // File needs to be tested.
        if (empty($data['advagg_js_compress']['tested'])) {
            $files_to_test[] = array(
                'md5' => $filename_md5,
                'filename' => $filename,
            );
        }
        elseif ($data['advagg_js_compress']['tested']['jsminplus'] != 1) {
            $list_bad[$filename] = $filename;
        }
    }
    $advagg_js_compress_callback = variable_get('advagg_js_compress_callback', ADVAGG_JS_COMPRESS_CALLBACK);
    if ($advagg_js_compress_callback) {
        // Send test files to worker.
        if (!empty($files_to_test)) {
            $compressible = advagg_js_compress_test_compression($files_to_test);
            // If an array then it is a list of files that can not be compressed.
            if (is_array($compressible)) {
                // Place filename in an array key.
                foreach ($compressible as $filedata) {
                    $filename = $filedata['filename'];
                    $list_bad[$filename] = $filename;
                }
            }
        }
    }
    $contents = '';
    // Do not compress the file that it bombs on.
    // Compress each file individually.
    foreach ($files as $file) {
        if (!empty($list_bad[$file])) {
            $contents .= advagg_build_js_bundle(array(
                $file,
            ));
        }
        else {
            $data = advagg_build_js_bundle(array(
                $file,
            ));
            // If using a cache, try to get the contents of it.
            $cached = FALSE;
            if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
                $key = $file;
                $table = 'cache_advagg_js_compress_file';
                $cached_data = cache_get($key, $table);
                if (!empty($cached_data->data)) {
                    $data = $cached_data->data;
                    $cached = TRUE;
                }
            }
            if (!$cached && !empty($data)) {
                $compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
                if ($compressor == 0) {
                    list($before, $after) = advagg_js_compress_jsminplus($data);
                    $ratio = ($before - $after) / $before;
                    // Make sure the returned string is not empty or has a VERY high
                    // compression ratio.
                    if (empty($data) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
                        $data = advagg_build_js_bundle(array(
                            $file,
                        ));
                    }
                    elseif (isset($key)) {
                        // If using a cache set it.
                        cache_set($key, $data, $table);
                    }
                }
                elseif ($compressor == 1) {
                    $contents = jsmin($contents);
                }
            }
            $contents .= $data . ";\n";
        }
    }
}

/**
 * Compress a JS string using jsmin+
 *
 * @param $contents
 *   Javascript string.
 * @return
 *   array with the size before and after.
 */
function advagg_js_compress_jsminplus(&$contents) {
    // Try to allocate enough time to run JSMin+.
    if (function_exists('set_time_limit')) {
        @set_time_limit(240);
    }
    // JSMin+ the contents of the aggregated file.
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'advagg_js_compress') . '/jsminplus.inc';
    // Strip Byte Order Marks (BOM's) from the file, JSMin+ cannot parse these.
    $before = strlen($contents);
    $original_contents = $contents;
    try {
        $contents = str_replace(pack("CCC", 0xef, 0xbb, 0xbf), "", $contents);
        ob_start();
        $contents = JSMinPlus::minify($contents);
        $error = trim(ob_get_contents());
        if (!empty($error)) {
            throw new Exception($error);
        }
        $after = strlen($contents);
    } catch (Exception $e) {
        // Log the exception thrown by JSMin+ and roll back to uncompressed content.
        watchdog('advagg', $e->getMessage() . '<pre>' . $original_contents . '</pre>', NULL, WATCHDOG_WARNING);
        $contents = $original_contents;
        $after = $before;
    }
    ob_end_clean();
    return array(
        $before,
        $after,
    );
}

/**
 * Run various theme functions so the cache is primed.
 *
 * @param $files_to_test
 *   array with md5 and filename.
 * @return
 *   TRUE if all files are compressible. List of files that failed otherwise.
 */
function advagg_js_compress_test_compression($files_to_test) {
    global $base_path;
    $bad_files = array();
    // Blacklist jquery.min.js from getting compressed.
    if (module_exists('jquery_update')) {
        foreach ($files_to_test as $key => $info) {
            if (strpos($info['filename'], 'jquery.min.js') !== FALSE) {
                // Add file to the bad list.
                $bad_files[] = $info;
                unset($files_to_test[$key]);
                // Get file data.
                $filename_md5 = md5($info['filename']);
                $lock_name = 'advagg_set_file_data_' . $filename_md5;
                if (!lock_acquire($lock_name, 10)) {
                    lock_wait($lock_name);
                    continue;
                }
                $data = advagg_get_file_data($filename_md5);
                // Set to -2
                if (!isset($data->data['advagg_js_compress']['tested']['jsminplus']) || $data->data['advagg_js_compress']['tested']['jsminplus'] != -2) {
                    $data['advagg_js_compress']['tested']['jsminplus'] = -2;
                    advagg_set_file_data($filename_md5, $data);
                }
                lock_release($lock_name);
            }
        }
    }
    foreach ($files_to_test as $info) {
        $key = variable_get('advagg_js_compress_url_key', FALSE);
        if (empty($key)) {
            $key = mt_rand();
            variable_set('advagg_js_compress_url_key', $key);
        }
        // Clear the cache for this file
        cache_clear_all($info['filename'], 'cache_advagg_js_compress_file');
        // Setup request URL and headers.
        $query['values'] = $info;
        $query['key'] = $key;
        $query_string = http_build_query($query, '', '&');
        $url = _advagg_build_url('advagg/js_compress_test_file');
        $headers = array(
            'Host' => $_SERVER['HTTP_HOST'],
            'Content-Type' => 'application/x-www-form-urlencoded',
            'Connection' => 'close',
        );
        $results = drupal_http_request($url, array(
            'headers' => $headers,
            'method' => 'POST',
            'data' => $query_string,
        ));
        // Get file data.
        $filename_md5 = md5($info['filename']);
        $data = advagg_get_file_data($filename_md5);
        // Mark as a bad file.
        if (empty($data['advagg_js_compress']['tested']['jsminplus']) || $data['advagg_js_compress']['tested']['jsminplus'] != 1) {
            $bad_files[] = $info;
        }
    }
    if (empty($bad_files)) {
        return TRUE;
    }
    return $bad_files;
}

/**
 * Run various theme functions so the cache is primed.
 *
 * @param $values
 *   object File info
 */
function advagg_js_compress_test_file($values = NULL) {
    //   watchdog('debug', str_replace('    ', '&nbsp;&nbsp;&nbsp;&nbsp;', nl2br(htmlentities(print_r($values, TRUE) . print_r($_REQUEST, TRUE)))));
    // Exit if key does not match & called with $file not set.
    if (is_null($values)) {
        if (empty($_POST['key']) || empty($_POST['values'])) {
            return;
        }
        $key = variable_get('advagg_js_compress_url_key', FALSE);
        if ($key != $_POST['key']) {
            return;
        }
        $values = array();
        $values['values'] = $_POST['values'];
    }
    $filename = $values['values']['filename'];
    $md5 = $values['values']['md5'];
    // Compression test file if it exists.
    advagg_clearstatcache(TRUE, $filename);
    if (file_exists($filename)) {
        $contents = file_get_contents($filename);
        $filesize = filesize($filename);
        $lock_name = 'advagg_set_file_data_' . $md5;
        if (!lock_acquire($lock_name, 45)) {
            lock_wait($lock_name);
            echo $md5;
            exit;
        }
        $data = advagg_get_file_data($md5);
        // Set to "-1" so if php bombs out, the file will be marked as bad.
        $data['advagg_js_compress']['tested']['jsminplus'] = -1;
        advagg_set_file_data($md5, $data);
        // Compress the data.
        list($before, $after) = advagg_js_compress_jsminplus($contents);
        // Set to "-2" if compression ratio sucks.
        $ratio = ($before - $after) / $before;
        if ($ratio < variable_get('advagg_js_compress_ratio', ADVAGG_JS_COMPRESS_RATIO)) {
            $data['advagg_js_compress']['tested']['jsminplus'] = -2;
            advagg_set_file_data($md5, $data);
            lock_release($lock_name);
            echo $md5;
            exit;
        }
        // Set to "-3" if the compression ratio is way too good.
        if ($ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
            $data['advagg_js_compress']['tested']['jsminplus'] = -3;
            advagg_set_file_data($md5, $data);
            lock_release($lock_name);
            echo $md5;
            exit;
        }
        // Everything worked, mark this file as compressable.
        $data['advagg_js_compress']['tested']['jsminplus'] = 1;
        advagg_set_file_data($md5, $data);
        // Set the file cache.
        if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
            $key = $filename;
            $table = 'cache_advagg_js_compress_file';
            cache_set($key, $contents, $table);
        }
    }
    if (isset($lock_name)) {
        lock_release($lock_name);
    }
    echo $md5;
    exit;
}

/**
 * Save a string to the specified destination. Verify that file size is not zero.
 *
 * @param $data
 *   A string containing the contents of the file.
 * @param $dest
 *   A string containing the destination location.
 * @return
 *   Boolean indicating if the file save was successful.
 */
function advagg_js_compress_file_saver($data, $dest, $force, $type) {
    if ($type == 'css') {
        return advagg_file_saver($data, $dest, $force, $type);
    }
    if (!variable_get('advagg_gzip_compression', ADVAGG_GZIP_COMPRESSION) || !extension_loaded('zlib')) {
        return advagg_file_saver($data, $dest, $force, $type);
    }
    // Get file save function
    $file_save_data = 'file_save_data';
    $custom_path = variable_get('advagg_custom_files_dir', ADVAGG_CUSTOM_FILES_DIR);
    if (!empty($custom_path)) {
        $file_save_data = 'advagg_file_save_data';
    }
    // Gzip first.
    $gzip_dest = $dest . '.gz';
    advagg_clearstatcache(TRUE, $gzip_dest);
    if (!file_exists($gzip_dest) || $force) {
        $gzip_data = gzencode($data, 9, FORCE_GZIP);
        if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
            return FALSE;
        }
        // Make sure filesize is not zero.
        advagg_clearstatcache(TRUE, $gzip_dest);
        if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
            if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
                return FALSE;
            }
            advagg_clearstatcache(TRUE, $gzip_dest);
            if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
                // Filename is bad, create a new one next time.
                file_delete($gzip_dest);
                return FALSE;
            }
        }
    }
    // Use packer on JS data.
    advagg_js_compress_jspacker($data);
    // Write File.
    if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
        return FALSE;
    }
    // Make sure filesize is not zero.
    advagg_clearstatcache(TRUE, $dest);
    if (@filesize($dest) == 0 && !empty($data)) {
        if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
            return FALSE;
        }
        advagg_clearstatcache(TRUE, $dest);
        if (@filesize($dest) == 0 && !empty($data)) {
            // Filename is bad, create a new one next time.
            file_delete($dest);
            return FALSE;
        }
    }
    // Make sure .htaccess file exists.
    advagg_htaccess_check_generate($dest);
    cache_set($dest, REQUEST_TIME, 'cache_advagg', CACHE_PERMANENT);
    return TRUE;
}

/**
 * Compress a JS string using packer.
 *
 * @param $contents
 *   Javascript string.
 */
function advagg_js_compress_jspacker(&$contents) {
    // Use Packer on the contents of the aggregated file.
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'advagg_js_compress') . '/jspacker.inc';
    // Add semicolons to the end of lines if missing.
    $contents = str_replace("}\n", "};\n", $contents);
    $contents = str_replace("\nfunction", ";\nfunction", $contents);
    // Remove char returns, looking at you lightbox2.
    $contents = str_replace("\n\r", "", $contents);
    $contents = str_replace("\r", "", $contents);
    $contents = str_replace("\n", "", $contents);
    $packer = new JavaScriptPacker($contents, 62, TRUE, FALSE);
    $contents = $packer->pack();
}

/**
 * Implements hook_flush_caches().
 */
function advagg_js_compress_flush_caches() {
    return array(
        'cache_advagg_js_compress_inline',
    );
}

/**
 * Implements hook_advagg_master_reset().
 */
function advagg_js_compress_advagg_master_reset() {
    cache_clear_all('*', 'cache_advagg_js_compress_inline', TRUE);
    cache_clear_all('*', 'cache_advagg_js_compress_file', TRUE);
}

Functions

Title Deprecated Summary
advagg_js_compress_advagg_files_table Implements hook_advagg_files_table().
advagg_js_compress_advagg_js_alter Implements hook_advagg_js_alter().
advagg_js_compress_advagg_js_inline_alter Implements hook_advagg_js_inline_alter().
advagg_js_compress_advagg_js_pre_alter Implements hook_advagg_js_pre_alter().
advagg_js_compress_advagg_master_reset Implements hook_advagg_master_reset().
advagg_js_compress_file_saver Save a string to the specified destination. Verify that file size is not zero.
advagg_js_compress_flush_caches Implements hook_flush_caches().
advagg_js_compress_init Implements hook_init().
advagg_js_compress_jsminplus Compress a JS string using jsmin+
advagg_js_compress_jspacker Compress a JS string using packer.
advagg_js_compress_menu Implements hook_menu().
advagg_js_compress_prep Compress a JS string
advagg_js_compress_test_compression Run various theme functions so the cache is primed.
advagg_js_compress_test_file Run various theme functions so the cache is primed.

Constants

Title Deprecated Summary
ADVAGG_JS_COMPRESSOR Default value to see what compressor to use. 0 is JSMin+.
ADVAGG_JS_COMPRESS_AGG_FILES Default value to see if this will compress aggregated files.
ADVAGG_JS_COMPRESS_CALLBACK Default value to see if the callback is working.
ADVAGG_JS_COMPRESS_FILE_CACHE Default value to see if this will cache the compressed inline js.
ADVAGG_JS_COMPRESS_INLINE Default value to see if this will compress inline js.
ADVAGG_JS_COMPRESS_INLINE_CACHE Default value to see if this will cache the compressed inline js.
ADVAGG_JS_COMPRESS_PACKER_ENABLE Default value to see packer is enabled.
ADVAGG_JS_COMPRESS_RATIO Default value for the compression ratio test.
ADVAGG_JS_MAX_COMPRESS_RATIO Default value for the compression ratio test.