aDriv4 - MANAGER
Edit File: less.module
<?php /** * @file * Handles compiling of .less files. * * The theme system allows for nearly all output of the Drupal system to be * customized by user themes. */ define('LESS_PERMISSION', 'administer less'); /** * Implements hook_hook_info(). */ function less_hook_info() { $hooks = array(); $hooks['form_system_theme_settings_alter'] = array( 'group' => 'theme', ); $hooks['wysiwyg_editor_settings_alter'] = array( 'group' => 'wysiwyg', ); $hooks['form_wysiwyg_profile_form_alter'] = array( 'group' => 'wysiwyg', ); $hooks['cron_queue_info'] = array( 'group' => 'cron', ); $hooks['cron'] = array( 'group' => 'cron', ); return $hooks; } /** * Implements hook_menu(). */ function less_menu() { $items = array(); $items['admin/config/development/less'] = array( 'title' => 'LESS settings', 'description' => 'Administer LESS settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('less_settings_form'), 'access arguments' => array(LESS_PERMISSION), 'file' => 'less.admin.inc', 'type' => MENU_NORMAL_ITEM, ); $items['admin/config/development/less/settings'] = array( 'title' => $items['admin/config/development/less']['title'], 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['ajax/less/watch'] = array( 'title' => 'LESS watch callback', 'type' => MENU_CALLBACK, 'page callback' => '_less_watch', 'access callback' => TRUE, 'delivery callback' => 'drupal_json_output', ); return $items; } /** * Implements hook_permission(). */ function less_permission() { return array( LESS_PERMISSION => array( 'title' => t('Administer LESS'), 'description' => t('Access the LESS settings page and view debug messages.'), ), ); } /** * Implements hook_element_info_alter(). */ function less_element_info_alter(&$type) { // Prepend to the list of #pre_render functions so it runs first. array_unshift($type['styles']['#pre_render'], '_less_pre_render'); if (variable_get('less_devel', FALSE)) { // Must run after drupal_pre_render_styles() to attach any attributes. array_push($type['styles']['#pre_render'], '_less_attach_src'); } } /** * Add original .less file path as 'src' attribute to <link />. */ function _less_attach_src($styles) { foreach (element_children($styles) as $key) { // If its a <link />, then most likely its a compiled .less file. if ($styles[$key]['#tag'] == 'link') { // Hashes are generated based on the URL without the query portion. $file_url = drupal_substr($styles[$key]['#attributes']['href'], 0, mb_strpos($styles[$key]['#attributes']['href'], '?')); //If we have a match, it means it is a compiled .less file. if ($cache = cache_get('less:watch:' . drupal_hash_base64($file_url))) { // Some inspectors allow 'src' attribute to open from a click. $styles[$key]['#attributes']['src'] = $cache->data['data']; } } } return $styles; } /** * Processes .less files */ function _less_pre_render($styles) { $less_devel = variable_get('less_devel', FALSE); $less_dir = _less_get_dir(); if ($less_devel) { if (variable_get('less_watch', TRUE)) { drupal_add_js(drupal_get_path('module', 'less') . '/less.watch.js'); } // Warn users once every hour that less is checking for file modifications. if (user_access(LESS_PERMISSION) && flood_is_allowed('less_devel_warning', 1)) { flood_register_event('less_devel_warning'); $message = 'LESS files are being checked for modifications on every request. Remember to <a href="@url">turn off</a> this feature on production websites.'; $message_vars = array("@url" => url('admin/config/development/less')); drupal_set_message(t($message, $message_vars), 'status'); } } $less_path = 'public://less/' . $less_dir; foreach ($styles['#items'] as $key => $info) { $input_file = $info['data']; if (drupal_substr($input_file, -5) == '.less') { $file_uri = file_uri_target($input_file); $css_path = $less_path . '/' . dirname($file_uri ? $file_uri : $input_file); // Ensure the destination directory exists. if (!_less_ensure_directory($css_path)) { // Cancel the for each loop when directory doesn't exist. break; } $output_file = $css_path . '/' . basename($input_file, '.less'); // correct file names of files not following the .css.less naming convention if (drupal_substr($output_file, -4) != '.css') { $output_file .= '.css'; } $less_settings = less_get_settings(_less_file_owner($input_file)); if (!empty($info['less'])) { $less_settings = drupal_array_merge_deep($less_settings, (array) $info['less']); } // array_multisort() the data so that the hash returns the same hash regardless order of data. array_multisort($less_settings); // json_encode() is used because serialize() throws an error with lambda functions. $output_file = substr_replace($output_file, drupal_hash_base64(json_encode($less_settings)) . '.css', -3); $rebuild = FALSE; // Set $rebuild if this file or its children have been modified. if ($less_devel && is_file($output_file)) { $output_file_mtime = filemtime($output_file); if ($less_file_cache = cache_get('less:devel:' . drupal_hash_base64($input_file))) { // Iterate over each file and check if there are any changes. foreach ($less_file_cache->data as $filepath => $filemtime) { // Only rebuild if there has been a change to a file. if (filemtime($filepath) > $filemtime) { $rebuild = TRUE; break; } } } else { // No cache data, force a rebuild for later comparison. $rebuild = TRUE; } } // $output_file doesn't exist or is flagged for rebuild. if ((!is_file($output_file) || $rebuild) && _less_inc()) { $less = new lessc(); if (method_exists($less, 'registerFunction') && is_array($less_settings['functions'])) { foreach ($less_settings['functions'] as $funcion => $callback) { $less->registerFunction($funcion, $callback); } } if (method_exists($less, 'setVariables')) { $less->setVariables($less_settings['variables']); } $cache = NULL; $output_data = NULL; // Since lessphp is a php class, this will capture errors for output. try { if ($less_devel) { $less->setPreserveComments(true); $cache = $less->cachedCompile($input_file); cache_set('less:devel:' . drupal_hash_base64($input_file), $cache['files']); $output_data = $cache['compiled']; } else { $output_data = $less->compileFile($input_file); } } catch (Exception $e) { $message = 'LESS error: @message, %input_file'; $message_vars = array('@message' => $e->getMessage(), '%input_file' => $input_file); watchdog('LESS', $message, $message_vars, WATCHDOG_ERROR); if (user_access(LESS_PERMISSION)) { drupal_set_message(t($message, $message_vars), 'error'); } } if (isset($output_data)) { // Fix paths for images as .css is in different location. $output_data = _less_rewrite_paths($input_file, $output_data); file_unmanaged_save_data($output_data, $output_file, FILE_EXISTS_REPLACE); } } if (is_file($output_file)) { $styles['#items'][$key]['data'] = $output_file; if ($less_devel) { $less_watch_cache = $styles['#items'][$key]; $less_watch_cache['less'] = $less_settings; $less_watch_cache['data'] = $input_file; $less_watch_cache['output_file'] = $output_file; cache_set('less:watch:' . drupal_hash_base64(file_create_url($output_file)), $less_watch_cache); // 'preprocess' being FALSE generates a discreet <link /> rather than an @import. $styles['#items'][$key]['preprocess'] = FALSE; } } } } return $styles; } /** * Implements hook_admin_menu_cache_info(). */ function less_admin_menu_cache_info() { $caches['less'] = array( 'title' => t('LESS files'), 'callback' => 'less_flush_caches', ); return $caches; } /** * Implements hook_flush_caches(). * * Flushes compiled LESS files during cache flush, except during cron. * * @return An array of cache table names. */ function less_flush_caches() { if (!drupal_static('less_cron')) { // rebuild the less files directory. _less_get_dir(TRUE); cache_clear_all('less:', 'cache', TRUE); } return array(); } /** * Helper function to create a new less dir. */ function _less_get_dir($rebuild = FALSE) { $less_dir = variable_get('less_dir'); // If drupal variable 'less_dir' is not set, empty or manually reset then // generate a new unique id and save it. if ($rebuild || empty($less_dir)) { // Set the less directory variable variable_set('less_dir', drupal_hash_base64(uniqid('', TRUE))); } return variable_get('less_dir'); } /** * Finds and loads the lessphp library */ function _less_inc() { static $loaded = NULL; if (!isset($loaded)) { // Locations to check for lessphp, by order of preference. $include_locations = array(); // Composer created path. $include_locations[] = dirname(__FILE__) . '/vendor/autoload.php'; // Ensure libraries module is loaded. module_load_include('module', 'libraries'); if (function_exists('libraries_get_path')) { // Add libraries supported path. $include_locations[] = libraries_get_path('lessphp') . '/lessc.inc.php'; } // Add legacy path as final possible location. $include_locations[] = dirname(__FILE__) . '/lessphp/lessc.inc.php'; foreach ($include_locations as $include_location) { if (is_file($include_location)) { require_once $include_location; break; } } $loaded = class_exists('lessc', TRUE) && version_compare(lessc::$VERSION, 'v0.3.7', '>='); } return $loaded; } /** * Copied functionality from drupal_build_css_cache() for our own purposes. * * This function processes $contents and rewrites relative paths to be absolute * from web root. This is mainly used to ensure that compiled .less files still * reference images at their original paths. */ function _less_rewrite_paths($input_filepath, $contents) { $output = ''; // Build the base URL of this CSS file: start with the full URL. $css_base_url = file_create_url($input_filepath); // Move to the parent. $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/')); // Simplify to a relative URL if the stylesheet URL starts with the // base URL of the website. if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) { $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root'])); } _drupal_build_css_path(NULL, $css_base_url . '/'); // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths. $output .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents); return $output; } /** * This keeps track of which modules and themes own which .less files, and any * variable defaults those system items define. * * Only tracks .less files that are added through .info files. */ function _less_registry() { $static_stylesheets = &drupal_static('less_stylesheets'); $static_defaults = &drupal_static('less_defaults'); if (!isset($static_stylesheets) || !isset($static_defaults)) { if (($cache_stylesheets = cache_get('less:stylesheets')) && ($cache_defaults = cache_get('less:defaults'))) { $static_stylesheets = $cache_stylesheets->data; $static_defaults = $cache_defaults->data; } else { $system_types = array( 'module_enabled', 'theme', ); foreach ($system_types as $system_type) { $system_items = system_list($system_type); foreach ($system_items as $system_item_name => $system_item) { // Register all globally included .less stylesheets. if (!empty($system_item->info['stylesheets'])) { foreach ($system_item->info['stylesheets'] as $stylesheets) { foreach ($stylesheets as $stylesheet) { if (drupal_substr($stylesheet, -5) == '.less') { $static_stylesheets[$stylesheet] = $system_item_name; } } } } // Process LESS settings from .info files. if (isset($system_item->info['less']) && is_array($system_item->info['less'])) { // Register all non-global stylesheets. if (isset($system_item->info['less']['sheets']) && is_array($system_item->info['less']['sheets'])) { $system_item_path = drupal_get_path($system_item->type, $system_item->name); foreach ($system_item->info['less']['sheets'] as $stylesheet) { $static_stylesheets[$system_item_path . '/' . $stylesheet] = $system_item_name; } } // Register variable defaults. if (isset($system_item->info['less']['vars']) && is_array($system_item->info['less']['vars'])) { $static_defaults[$system_item_name] = $system_item->info['less']['vars']; } } // Invoke hook_less_variables(), results should be static. if ($module_defaults = module_invoke($system_item_name, 'less_variables')) { $static_defaults[$system_item_name] = array_merge((array) $static_defaults[$system_item_name], array_filter($module_defaults)); } } } cache_set('less:stylesheets', $static_stylesheets); cache_set('less:defaults', $static_defaults); } } } /** * Returns the owning module/theme for a passed in .less file, or NULL. * Only can resolve .less files that are added using .info files. * * @param $filepath * * @return system name or NULL. */ function _less_file_owner($filepath) { // Use the advanced drupal_static() pattern, since this is called very often. static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['cache'] = &drupal_static('less_stylesheets'); if (!isset($drupal_static_fast['cache'])) { _less_registry(); } } $stylesheets_cache = &$drupal_static_fast['cache']; return isset($stylesheets_cache[$filepath]) ? $stylesheets_cache[$filepath] : NULL; } /** * Returns the compiled list of variables and functions for a module/theme. * * @param $system_name * Module or theme system name. */ function less_get_settings($system_name = NULL) { // Use the advanced drupal_static() pattern, since this is called very often. static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__); } $less_settings_static = &$drupal_static_fast['cache']; if (!isset($less_settings_static[$system_name])) { global $theme; $theme_settings = theme_get_setting('less', $theme); $defaults_cache = &drupal_static('less_defaults'); if (!isset($defaults_cache)) { _less_registry(); } $data = array( 'variables' => array(), 'functions' => array( 'token' => '_less_token_replace', ), ); /** * Compile the LESS variables. */ // Cached default variables from .info files and hook_less_variables(). if (!empty($defaults_cache[$system_name])) { $data['variables'] = array_merge($data['variables'], array_filter($defaults_cache[$system_name])); } // Saved variable values from current theme. if (!is_null($theme_settings) && !empty($theme_settings[$system_name])) { $data['variables'] = array_merge($data['variables'], array_filter($theme_settings[$system_name])); } // Prevent $system_name from being altered. $alter_system_name = $system_name; // Invoke hook_less_variables_alter(). drupal_alter('less_variables', $data['variables'], $alter_system_name); // Invoke hook_less_variables_SYSTEM_NAME_alter(). drupal_alter('less_variables_' . $system_name, $data['variables']); /** * Grab the LESS functions. * * LESS functions are not stored in the cache table since they could be * anonymous functions. */ if (module_hook($system_name, 'less_functions')) { $data['functions'] = array_merge($data['functions'], (array) module_invoke($system_name, 'less_functions')); } // Prevent $system_name from being altered. $alter_system_name = $system_name; // Invoke hook_less_functions_alter(). drupal_alter('less_functions', $data['functions'], $alter_system_name); // Invoke hook_less_functions_system_name_alter(). drupal_alter('less_functions_' . $system_name, $data['functions']); $less_settings_static[$system_name] = $data; } // Don't need to test isset(), there will always be data at $system_name. return $less_settings_static[$system_name]; } /** * Handler for LESS function token(). */ function _less_token_replace($arg) { list($type, $delimeter, $value) = $arg; return array($type, $delimeter, array(token_replace($value[0]))); } /** * Handles AJAX requests to check for changes to files while in developer mode. */ function _less_watch() { drupal_page_is_cacheable(FALSE); $changed_files = array(); if (variable_get('less_watch', FALSE)) { $files = (isset($_POST['less_files']) && is_array($_POST['less_files'])) ? $_POST['less_files'] : array(); foreach ($files as $file) { if ($cache = cache_get('less:watch:' . drupal_hash_base64($file))) { $cached_data = $cache->data; $current_mtime = filemtime($cached_data['output_file']); $styles = array( '#items' => array( $cached_data['data'] => $cached_data, ), ); $styles = _less_pre_render($styles); if (filemtime($styles['#items'][$cached_data['data']]['data']) > $current_mtime) { $changed_files[] = array( 'old_file' => $file, 'new_file' => file_create_url($styles['#items'][$cached_data['data']]['data']), ); } } } } return $changed_files; } /** * Helper function that attempts to create a folder if it doesn't exist. * Locks are used to help avoid concurrency collisions. */ function _less_ensure_directory($directory_path) { $is_dir = is_dir($directory_path); if (!$is_dir) { $lock_id = 'less_directory_' . md5($directory_path); // Attempt to create directory only 3 times, else delay is too long. for ($i = 0; $i < 3; $i++) { if (lock_acquire($lock_id) && $is_dir = file_prepare_directory($directory_path, FILE_CREATE_DIRECTORY)) { // Creation was successful, cancel the 'for' loop; break; } lock_wait($lock_id, 1); } lock_release($lock_id); if (!$is_dir) { // There is a problem with the directory. $message = 'LESS could not create a directory in %dir'; $message_vars = array('%dir' => $directory_path); watchdog('LESS', $message, $message_vars, WATCHDOG_ERROR); if (user_access(LESS_PERMISSION)) { drupal_set_message(t($message, $message_vars), 'error', FALSE); } } } return $is_dir; }