You are here

live_css.module in Live CSS 7.2

Allows editing and a live view of all changes in real-time in the browser.

File

live_css.module
View source
<?php

/**
 * @file
 * Allows editing and a live view of all changes in real-time in the browser.
 */

/**
 * Implements hook_menu().
 */
function live_css_menu() {
  $items = array(
    'css/save' => array(
      'page callback' => 'live_css_save',
      'access callback' => 'live_css_access',
      'type' => MENU_CALLBACK,
    ),
    'admin/config/development/live_css' => array(
      'title' => 'Live CSS',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'live_css_admin',
      ),
      'description' => 'Configure the live CSS editor.',
      'access arguments' => array(
        'edit css admin',
      ),
      'type' => MENU_NORMAL_ITEM,
    ),
  );
  return $items;
}

/**
 * Implements hook_permission().
 *
 * Permissions available are:
 *    'edit css limited'
 *      - Restrict access to specific folder(s)/file(s) using a Regular
 *        Expression (RegEx) available on the configuration page.
 *    'edit css'
 *      - Full access to all CSS files (given that they have a correct set
 *        of filesystem permissions for writing by the server).
 *    'edit css admin'
 *      - Full access to CSS files and allows editing of configuration.
 */
function live_css_permission() {
  return array(
    'edit css admin' => array(
      'title' => t('Administer Live CSS settings'),
      'restrict acess' => TRUE,
      'warning' => t('Warning:  Give to only trusted users and do not combine with "edit css limited"'),
    ),
    'edit css' => array(
      'title' => t('Edit and save CSS - Full Access'),
      'description' => t('Edit and save any CSS with the live editor (filesystem permitting).'),
      'warning' => t('Warning:  Combining this with "edit css limited" negates the lesser permission.'),
    ),
    'edit css limited' => array(
      'title' => t('Edit and save CSS - Limited Access'),
      'description' => t('Edit and save CSS with the live editor.  Enabling this permission will result in restricted access to CSS files based on what path is enabled on the Live CSS settings page.'),
    ),
  );
}

/**
 * Implements live_css_access().
 *
 * Restrict users from accessing menu callback css/save with no POST data
 * or if they don't have permission.
 */
function live_css_access() {
  if (!isset($_POST['href']) || !isset($_POST['css']) || is_null($_POST)) {
    return drupal_access_denied();
  }
  return user_access('edit css limited') || user_access('edit css') || user_access('edit css admin') || drupal_access_denied();
}

/**
 * Utilizes pre_render callback to alter the LESS files prior to rendering.
 */
function live_css_pre_render($styles) {
  foreach ($styles['#items'] as $key => $info) {
    $input_file = $info['data'];
    if (drupal_substr($input_file, -5) == '.less') {
      $styles['#items'][$key]['type'] = 'external';
      $styles['#items'][$key]['data'] = base_path() . $styles['#items'][$key]['data'];
    }
  }
  return $styles;
}

/**
 * Implements hook_element_info_alter().
 */
function live_css_element_info_alter(&$type) {
  array_unshift($type['styles']['#pre_render'], 'live_css_pre_render');
}

/**
 * Implements hook_settings().
 */
function live_css_admin() {
  $form = array();
  $form['live_css_switchsave'] = array(
    '#type' => 'checkbox',
    '#title' => t('Switch/Save <strong>[NEW]</strong>'),
    '#default_value' => variable_get('live_css_switchsave', 0),
    '#description' => t('Choose to save your work automatically upon switching between multiple files.'),
  );
  $form['live_css_less'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable LESS Support'),
    '#default_value' => variable_get('live_css_less', 0),
    '#description' => t('Allows the live editing and display of LESS files on the site, by simply embedding stylesheets with a "less" extension instead of "css". The Less is parsed on each page load, even for anonymous users. In production you may wish to disable this feature and use the LESS module instead.'),
  );
  $form['live_css_flush'] = array(
    '#type' => 'checkbox',
    '#title' => t('CSS and JS cache flush'),
    '#default_value' => variable_get('live_css_flush', 1),
    '#description' => t('Flush CSS and Javascript cache on every save.'),
  );
  $form['live_css_hideadmin'] = array(
    '#type' => 'checkbox',
    '#title' => t('Hide Admin Menu'),
    '#default_value' => variable_get('live_css_hideadmin', 1),
    '#description' => t('Automatically hides the administration menu when editing CSS.'),
  );
  $form['live_css_hidemodules'] = array(
    '#type' => 'checkbox',
    '#title' => t('Only show theme CSS'),
    '#default_value' => variable_get('live_css_hidemodules', 1),
    '#description' => t('Removes module and other styles from the CSS list.'),
  );
  $form['live_css_storage'] = array(
    '#type' => 'checkbox',
    '#title' => t('Consistent Editor State'),
    '#default_value' => variable_get('live_css_storage', 1),
    '#description' => t('Remembers the current file and file position to maintain this between page loads.'),
  );
  $form['live_css_theme'] = array(
    '#type' => 'select',
    '#title' => t('Editor Theme'),
    '#default_value' => variable_get('live_css_theme', 'twilight'),
    '#options' => live_css_list_themes(),
  );
  $form['live_css_fontsize'] = array(
    '#type' => 'select',
    '#title' => t('Font Size'),
    '#default_value' => variable_get('live_css_fontsize', '12px'),
    '#options' => array(
      '8px' => '8px',
      '10px' => '10px',
      '11px' => '11px',
      '12px' => '12px',
      '14px' => '14px',
      '16px' => '16px',
      '18px' => '18px',
    ),
  );
  $form['live_css_softtabs'] = array(
    '#type' => 'checkbox',
    '#title' => t('Soft Tabs'),
    '#default_value' => variable_get('live_css_softtabs', 1),
    '#description' => t('Use spaces instead of a tab character.'),
  );
  $form['live_css_tabsize'] = array(
    '#type' => 'select',
    '#title' => t('Tab Size'),
    '#default_value' => variable_get('live_css_tabsize', 2),
    '#description' => t('When using soft tabs, specify how many spaces to insert for the tab character.<p><em>HINT: Drupal CSS coding standards dictate 2 spaces in lieu of traditional tabs</em></p>'),
    '#options' => array(
      1 => '1',
      2 => '2',
      3 => '3',
      4 => '4',
    ),
  );
  $form['live_css_path_to_files'] = array(
    '#type' => 'textfield',
    '#attributes' => array(
      'placeholder' => t('Example:  sites\\/[sitename]\\/themes\\/myTheme'),
    ),
    '#title' => t('Limit access to CSS files <strong>[NEW]</strong>'),
    '#default_value' => variable_get('live_css_path_to_files', ''),
    '#description' => t('Show only the CSS files found in this regular expression (RegEx) - <em>Trailing slash following path should be removed.</em><p><strong>Overrides "Only show theme CSS" setting for limited user.</strong></p>'),
  );
  return system_settings_form($form);
}

/**
 * Lists all the live editor's themes available.
 */
function live_css_list_themes() {
  $result = array();
  $files = live_css_list_files('sites/all/libraries/ace/src');
  foreach ($files as $file) {
    if (drupal_substr($file, 0, 5) == 'theme' && drupal_substr($file, -15) != 'uncompressed.js' && drupal_substr($file, -13) != 'noconflict.js') {
      $theme = drupal_substr($file, 6, drupal_strlen($file) - 9);
      $name = ucwords(str_replace('_', ' ', $theme));
      $result[$theme] = $name;
    }
  }
  return $result;
}

/**
 * Get the directory listing for the theme files of ace.
 */
function live_css_list_files($folder) {
  $results = array();
  $handler = opendir($folder);
  while ($file = readdir($handler)) {
    if ($file != '.' && $file != '..') {
      $results[] = $file;
    }
  }
  closedir($handler);
  return $results;
}

/**
 * Implements hook_init().
 */
function live_css_init() {

  // 4, 6, 7 = admin access
  // 2, 3, 6, 7 = full access
  // 1 = limited
  // **NOTE**
  // This check is necessary to combat potential double permissions
  // Example:  'edit css admin' paired with 'edit css limited' would otherwise still result
  // in a dominant limitation in the files shown.
  $access = 4 * (int) user_access('edit css admin') + 2 * (int) user_access('edit css') + (int) user_access('edit css limited');
  if ($access > 0 && (bool) variable_get('preprocess_css', FALSE) === FALSE) {
    $path = drupal_get_path('module', 'live_css');
    $theme = variable_get('live_css_theme', 'tomorrow');
    $less = variable_get('live_css_less', 0);
    $allowed_path = $access === 1 ? variable_get('live_css_path_to_files', '') : '';

    // Collect PHP settings to be sent to Javascript for easy access.
    $settings = array(
      'theme' => $theme,
      'autoLoad' => FALSE,
      'hideAdmin' => variable_get('live_css_hideadmin', 1),
      'hideModules' => variable_get('live_css_hidemodules', 1),
      'fontSize' => variable_get('live_css_fontsize', '12px'),
      'tabSize' => (int) variable_get('live_css_tabsize', 2),
      'softTabs' => (bool) variable_get('live_css_softtabs', 1),
      'storage' => variable_get('live_css_storage', 1),
      'less' => $less,
      'savePath' => url('css/save'),
      'menuMargin' => variable_get('admin_menu_top_margin', 0),
      'switchSave' => (bool) variable_get('live_css_switchsave', 0),
      'pathGoggles' => $allowed_path,
    );

    // Transfer variables to JS and load the module and its files.
    if ($less) {
      drupal_add_js($path . '/js/less-1.3.0.min.js');
      drupal_add_js($path . '/js/less-display.js');
    }
    drupal_add_js(array(
      'liveCSS' => $settings,
    ), 'setting');
    libraries_load('ace');
    drupal_add_css($path . '/css/live_css.css', array(
      'group' => 'CSS_THEME',
      'every_page' => TRUE,
      'weight' => 100,
      'preprocess' => FALSE,
    ));
    drupal_add_js($path . '/js/live_css.js', array(
      'group' => 'CSS_THEME',
      'every_page' => TRUE,
      'weight' => 100,
    ));
  }
}

/**
 * Callback to save a file edited live.
 */
function live_css_save() {
  $href = $_POST['href'];
  $css = $_POST['css'];
  $resetcache = (bool) variable_get('live_css_flush', 1);

  // The URL may contain cache data. In that case, we need to strip them
  // i.e. http://.../css/my_file.css?m1unhm
  $sanitized_url = _live_css_sanitize_css_url($href);

  // File path relative to Drupal installation folder.
  global $base_url;
  $stripped_url = drupal_substr($sanitized_url, drupal_strlen($base_url), drupal_strlen($sanitized_url));
  $relative_file_path = _live_css_document_root() . $stripped_url;
  if (substr($relative_file_path, -4) != '.css' && substr($relative_file_path, -5) != '.less') {
    echo drupal_json_encode(array(
      'result' => 'failure',
      'filename' => $href,
      'msg' => 'Can\'t save to files without a \'less\' or \'css\' extension!',
    ));
    return;
  }
  $filename = array_pop(explode('/', 'asdf/asdf.g'));
  if (file_munge_filename($filename, 'css less') != $filename) {
    echo drupal_json_encode(array(
      'result' => 'failure',
      'filename' => $href,
      'msg' => 'The url used contains a sub-filextension which poses a security threat. Saving not allowed.',
    ));
    return;
  }

  // Save file back.
  $msg = '';
  $fh = fopen($relative_file_path, 'w');
  if ($fh !== FALSE) {
    fwrite($fh, $css);
    fclose($fh);
    $result = 'success';
    if ($resetcache) {
      drupal_clear_css_cache();
      drupal_clear_js_cache();
      _drupal_flush_css_js();
    }
  }
  else {
    $result = 'failure';
    $msg = 'Can\'t open file ' . $relative_file_path . ' from ' . $href . '. Ensure that you have full write access and that the path is correct.';
  }
  echo drupal_json_encode(array(
    'result' => $result,
    'filename' => $href,
    'msg' => $msg,
  ));
}

/**
 * Helper function to sanitize a URL.  Removes cache information from url.
 */
function _live_css_sanitize_css_url($url) {
  $result = $url;
  $pos = strpos($url, '.css?');
  if ($pos !== FALSE) {
    $result = substr($url, 0, $pos + 4);
  }
  $pos = strpos($url, '.less?');
  if ($pos !== FALSE) {
    $result = substr($url, 0, $pos + 5);
  }
  return $result;
}

/**
 * Helper function to get the document root for the current Drupal installation.
 *
 * $_SERVER['DOCUMENT_ROOT'] is not reliable across all systems, so we need a
 * way to get the correct value.
 */
function _live_css_document_root() {
  $absolute_dir = dirname(__FILE__);
  $relative_dir = drupal_get_path('module', 'live_css');
  return drupal_substr($absolute_dir, 0, -1 * (1 + drupal_strlen($relative_dir)));
}

/**
 * Implements hook_libraries_info().
 */
function live_css_libraries_info() {
  $theme = variable_get('live_css_theme', 'tomorrow');
  $libraries['ace'] = array(
    'name' => 'Ace',
    'vendor url' => 'http://ace.c9.io/',
    'download url' => 'https://github.com/ajaxorg/ace-builds/archive/master.zip',
    'version arguments' => array(
      'file' => 'ChangeLog.txt',
      'pattern' => '@Version\\s+([0-9a-zA-Z\\.-]+)@',
      'lines' => 30,
    ),
    'files' => array(
      'js' => array(
        'src/ace.js',
        'src/mode-css.js',
        'src/theme-' . $theme . '.js',
      ),
    ),
  );
  return $libraries;
}

Functions

Namesort descending Description
live_css_access Implements live_css_access().
live_css_admin Implements hook_settings().
live_css_element_info_alter Implements hook_element_info_alter().
live_css_init Implements hook_init().
live_css_libraries_info Implements hook_libraries_info().
live_css_list_files Get the directory listing for the theme files of ace.
live_css_list_themes Lists all the live editor's themes available.
live_css_menu Implements hook_menu().
live_css_permission Implements hook_permission().
live_css_pre_render Utilizes pre_render callback to alter the LESS files prior to rendering.
live_css_save Callback to save a file edited live.
_live_css_document_root Helper function to get the document root for the current Drupal installation.
_live_css_sanitize_css_url Helper function to sanitize a URL. Removes cache information from url.