<?php

/**
 * @file
 * Syntax highlight code using the Syntaxhighlighter javascript library.
 * See http://alexgorbatchev.com/wiki/SyntaxHighlighter
 *
 * @author: Matthew Young <www.hddigitalworks.com/contact>
 */


/**
 * Inject syntaxhighlighter on every page except the listed pages.
 */
define('SYNTAXHIGHLIGHTER_INJECT_EXCEPT_LISTED', 0);

/**
 * Inject syntaxhighlighter on only the listed pages.
 */
define('SYNTAXHIGHLIGHTER_INJECT_IF_LISTED', 1);

/**
 * Inject syntaxhighlighter if the associated PHP code returns TRUE.
 */
define('SYNTAXHIGHLIGHTER_INJECT_PHP', 2);

define('SYNTAXHIGHLIGHTER_PHP_PERMISSION', 'use PHP for syntaxhighlighter js/css code inject control');

/**
 * Use a completely none sense word for replacement when filtering so there is
 * absolutely no chance this will conflict with something in content text
 */
define('SYNTAXHIGHLGHTER_TAG_STRING', '-_sYnTaXhIgHlIgHtEr_-');


/**
 * Implements hook_permission().
 */
function syntaxhighlighter_permission() {
  return array(
    SYNTAXHIGHLIGHTER_PHP_PERMISSION => array(
      'title' => t('Use PHP for syntaxhighlighter js/css code inject control'),
      'restrict access' => TRUE,
    ),
  );
}


/**
 * Menu for admin settings page
 */
function syntaxhighlighter_menu() {
  $items['admin/config/content/syntaxhighlighter'] = array(
    'title' => 'Syntax highlighter',
    'description' => 'Configure syntax highlighter',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('syntaxhighlighter_settings_form'),
    'access arguments' => array('administer modules'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'syntaxhighlighter.admin.inc',
  );

  return $items;
}


function syntaxhighlighter_help($path, $arg) {
  switch ($path) {
    case 'admin/help#syntaxhighlighter':
      return _syntaxhighlighter_filter_tips(0, 0, TRUE);
  }
}


function syntaxhighlighter_init() {
  if (!_syntaxhighlighter_page_match()) {
    return;
  }

  $lib_location = _syntaxhighlighter_get_lib_location();
  $styles_path = $lib_location . '/styles/';
  $scripts_path = $lib_location . '/scripts/';
  $js_options = array('type' => 'file', 'group' => JS_DEFAULT, 'every_page' => TRUE);
  
  drupal_add_css($styles_path . 'shCore.css');
  $theme = variable_get('syntaxhighlighter_theme', 'shThemeDefault.css');
  drupal_add_css($styles_path . $theme);

  drupal_add_js($scripts_path . 'shCore.js', $js_options);
  if (variable_get('syntaxhighlighter_legacy_mode', 0)) {
    drupal_add_js($scripts_path . 'shLegacy.js', $js_options);
  }

  if (variable_get('syntaxhighlighter_use_autoloader', FALSE)) {
    drupal_add_js($scripts_path . 'shAutoloader.js', $js_options);
    drupal_add_js(_syntaxhighlighter_file_directory_path() . '/syntaxhighlighter.autoloader.js', $js_options);
    $settings['useAutoloader'] = TRUE;
  }
  else {
    $enabled_languages = variable_get('syntaxhighlighter_enabled_languages', array('shBrushPhp.js'));
    foreach ($enabled_languages as $lang) {
      if (!empty($lang)) {
        drupal_add_js($scripts_path . $lang, $js_options);
      }
    }
  }

  $tag_name = variable_get('syntaxhighlighter_tagname', 'pre');
  if ($tag_name !== 'pre') {
    $settings['tagName'] = $tag_name;
  }
  if (file_exists($scripts_path . 'clipboard.swf')) {
    $settings['clipboard'] = base_path() . $scripts_path . 'clipboard.swf';
  }
  if (variable_get('syntaxhighlighter_legacy_mode', 0)) {
    $settings['legacyMode'] = TRUE;
  }

  if (isset($settings)) {
    drupal_add_js(array('syntaxhighlighter' => $settings), 'setting');
  }
  
  if ($defaultExpression = variable_get('syntaxhighlighter_default_expressions', '')) {
    drupal_add_js($defaultExpression, 'inline');
  }

  drupal_add_js(drupal_get_path('module', 'syntaxhighlighter') . '/syntaxhighlighter.min.js', array('type' => 'file', 'scope' => 'footer', 'group' => JS_DEFAULT, 'every_page' => TRUE));
}


function _syntaxhighlighter_page_match() {
  $inject = variable_get('syntaxhighlighter_inject', SYNTAXHIGHLIGHTER_INJECT_EXCEPT_LISTED);
  $pages = variable_get('syntaxhighlighter_pages', "admin\nadmin/*\nuser\nuser/*\nimce\nimce/*\n");
  if ($inject != SYNTAXHIGHLIGHTER_INJECT_PHP) {
    $path = drupal_get_path_alias($_GET['q']);
    // Compare with the internal and path alias (if any).
    $page_match = drupal_match_path($path, $pages);
    if ($path != $_GET['q']) {
      $page_match = $page_match || drupal_match_path($_GET['q'], $pages);
    }
    return !($inject xor $page_match);
  }
  else {
    // if the PHP module is not enabled, we just return FALSE
    // which just ends up disabling the syntaxhighlighter
    return function_exists('php_eval') && php_eval($pages);
  }
}


function syntaxhighlighter_modules_disabled($modules) {
  if (in_array('php', $modules) && variable_get('syntaxhighlighter_inject', SYNTAXHIGHLIGHTER_INJECT_EXCEPT_LISTED) == SYNTAXHIGHLIGHTER_INJECT_PHP) {
    drupal_set_message(t('The "%syntaxhighlighter" module is currently configured to use PHP for js/css code inject control, disabling the "%module_name" module will effectively turn off syntax highlighting on all pages.', array('%syntaxhighlighter' => t('Syntax highlighter'), '%module_name' => t('PHP filter'))), 'warning');
  }
}


/**
 * Implements hook_filter_info()
 * 
 * declare the syntaxhighlighter filter
 */
function syntaxhighlighter_filter_info() {
  $filters['syntaxhighlighter'] = array(
    'title' => t('Syntax highlighter'),
    'description' => t('Process syntax highlighter filter code block'),
    'tips callback' => '_syntaxhighlighter_filter_tips',
    'prepare callback' => '_syntaxhighlighter_do_filter_prepare',
    'process callback' => '_syntaxhighlighter_do_filter_process',
    'cache' => TRUE,
  );
  return $filters;
}


function _syntaxhighlighter_filter_tips($filter, $format, $long = FALSE) {
  $tag_name = variable_get('syntaxhighlighter_tagname', 'pre');
  $tip = t('Syntax highlight code surrounded by the <code>!ex0</code> tags, where !lang is one of the following language brushes: %brushes.',
           array('!ex0' => "&lt;$tag_name class=\"brush: <i>lang</i>\"&gt;...&lt;/$tag_name&gt;",
                 '!lang' => '<i>lang</i>',
                 '%brushes' => implode(', ', _syntaxhighlighter_get_enabled_language_brushes()),
                )
           );
  if ($long) {
    $tip .= ' ' . t('See <a href="!url0">the SyntaxHighlighter javascript library site</a> for additional helps.',
                       array('!url0' => 'http://alexgorbatchev.com/'));
  }
  return $tip;
}


/**
 * @return an array of all enabled language brushes
 */
function _syntaxhighlighter_get_enabled_language_brushes() {
  $brushes = &drupal_static(__FUNCTION__);
  if (!isset($brushes)) {
    $brushes = array();
    foreach (variable_get('syntaxhighlighter_enabled_languages', array('shBrushPhp.js')) as $val) {
      if ($val) {
        $brushes[] = strtolower(substr(substr($val, 7),0,-3));
      }
    }
  }
  return $brushes;
}


/**
 * Escape the content text in preparation for filtering:
 * 
 *  - change all syntaxhighlighter <pre> tag pairs to {-_sYnTaXhIgHlIgHtEr_-}
 *    {/-_sYnTaXhIgHlIgHtEr_-} pair (so other filters would not mess with them
 *
 * Precondition: all the open/close tags much match because search is done on
 * pair by pair basis. If match is not even, do nothing.
 * 
 * All HTML tags and entities inside the SyntaxHighlighter must be properly
 * escape. For example, if you show HTML code, change
 * 
 *  - '<' to '&lt;': e.g.  <pre> -> &lt;pre>, <html> -> &lt;html>
 *  - neutralize & in entity: e.g.: &gt; -> &amp;gt;
 * 
 * @param string $text
 *   the content text to be filtered
 * @return
 *   the escape content text
 */
function _syntaxhighlighter_do_filter_prepare($text) {
  $tag_name = variable_get('syntaxhighlighter_tagname', 'pre');
  $pattern = "#<$tag_name\\s*([^>]*)>|</\\s*$tag_name>#";
  preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
  $output = '';
  $at = 0;
  $n = count($matches);
  // do nothing if open/close tag match is not even
  if ($n % 2) {
    return $text;
  }
  for ($i = 0 ; $i < $n ; ) {
    $open_tag = $matches[$i++];
    $close_tag = $matches[$i++];
    if (strpos($open_tag[1][0], 'brush:')) {
      $output .= substr($text, $at, $open_tag[0][1] - $at);
      $begin = $open_tag[0][1] + strlen($open_tag[0][0]);
      $length = $close_tag[0][1] - $begin;
      $output .= '{' . SYNTAXHIGHLGHTER_TAG_STRING . ' ' . $open_tag[1][0] . '}' .
                  substr($text, $begin, $length) .
                  '{/' . SYNTAXHIGHLGHTER_TAG_STRING . '}';
      $at = $close_tag[0][1] + strlen($close_tag[0][0]);
    }
  }
  $output .= substr($text, $at);
  return $output;
}


/**
 * Revert back to <pre> tag
 */
function _syntaxhighlighter_do_filter_process($text) {
  $patterns = array(
    '#{' . SYNTAXHIGHLGHTER_TAG_STRING . ' ([^}]+)}#',
    '#{/' . SYNTAXHIGHLGHTER_TAG_STRING . '}#',
  );
  $tag_name = variable_get('syntaxhighlighter_tagname', 'pre');
  $replacements = array(
    "<$tag_name $1>",
    "</$tag_name>",
  );
  return preg_replace($patterns, $replacements, $text);
}



/**
 * Validate on the node input text to be sure there is no bad
 * {syntaxhighlighter} tags
 */
function syntaxhighlighter_node_validate($node, $form, &$form_state) {
  if (isset($node->body)) {
    foreach ($node->body as $lang => $v) {
      if (_syntaxhighlighter_format_has_syntaxhighlighter_filter(isset($v[0]['format']) ? $v[0]['format'] : filter_fallback_format())) {
        if (!empty($v[0]['summary'])) {
          _syntaxhighlighter_validate_input("body][$lang][0][summary", $v[0]['summary']);
        }
        _syntaxhighlighter_validate_input("body][$lang][0][value", $v[0]['value']);
      }
    }
  }
}


/**
 * Install custom comment validate function
 */
function syntaxhighlighter_form_comment_form_alter(&$form, &$form_state, $form_id) {
  $form['#validate'][] = '_syntaxhighlighter_comment_validate';
}


/**
 * Validate on comment input text to be sure there is no bad
 * {syntaxhighlighter} tags
 */
function _syntaxhighlighter_comment_validate($form, &$form_state) {
  if (isset($form_state['values']['comment_body'])) {
    foreach ($form_state['values']['comment_body'] as $lang => $v) {
      if (_syntaxhighlighter_format_has_syntaxhighlighter_filter(isset($v[0]['format']) ? $v[0]['format'] : filter_fallback_format())) {
        _syntaxhighlighter_validate_input("comment_body][$lang][0][value", $v[0]['value']);
      }
    }
  }
}


/**
 * Check for error with syntaxhighlighter input
 *
 * @param string $field
 *   what input field are we checking? We do form_set_error on this if
 *   any error is found
 * @param string $text
 *   the input text to check for
 */
function _syntaxhighlighter_validate_input($field, $text) {
  $errors = array();
  $tag_name = variable_get('syntaxhighlighter_tagname', 'pre');
  
  // check for balance open/close tags
  preg_match_all("#<$tag_name\\s*[^>]*>#", $text, $matches_open);
  preg_match_all("#</\\s*$tag_name\\s*>#", $text, $matches_close);
  if (count($matches_open[0]) != count($matches_close[0])) {
    $errors[] = t('Unbalanced !tag tags.', array('!tag' => "&lt;$tag_name&gt;"));
  }

  // make sure no nesting
  preg_match_all("#<$tag_name\\s*[^>]*>.*</\\s*$tag_name\\s*>#sU", $text, $matches_pair);
  if (count($matches_pair[0]) != count($matches_open[0]) || count($matches_pair[0]) != count($matches_close[0])) {
    $errors[] = t('!tag tags cannot be nested.', array('!tag' => "&lt;$tag_name&gt;"));
  }

  if (!empty($errors)) {
    form_set_error($field, implode(' ', $errors));
  }
}


/**
 * @return the directory path where the syntaxhighlighter js lib is installed, NULL if not found
 */
function _syntaxhighlighter_get_lib_location() {
  $result = variable_get('syntaxhighlighter_lib_location', NULL);
  if (!$result) {
    $result = _syntaxhighlighter_scan_lib_location();
    variable_set('syntaxhighlighter_lib_location', $result);
    // library location may have changed, recreate the setup script if the lib
    // is found
    if ($result) {
      _syntaxhighlighter_setup_autoloader_script();
    }
  }
  return $result;
}


/**
 * Do an exhaustive scan of file directories for the location of the syntaxhighlighter js lib,
 * Allow the syntaxhighlighter js library to be installed in any of the following
 * locations and under any sub-directory (except 'src'):
 *   1) syntaxhighlighter module directory
 *   2) sites/<site_domain>/files    (whereever the file_directory_path() is)
 *   3) sites/all/libraries
 *   4) the install profile libraries directory
 * @return the file location of the js lib or NULL if not found
 */
function _syntaxhighlighter_scan_lib_location() {
  $directories = array(
    drupal_get_path('module', 'syntaxhighlighter'),
    _syntaxhighlighter_file_directory_path(),
    'sites/all/libraries',
  );
  // When this function is called during Drupal's initial installation process,
  // the name of the profile that is about to be installed is stored in the
  // global $profile variable. At all other times, the regular system variable
  // contains the name of the current profile, and we can call variable_get()
  // to determine the profile.
  global $profile;
  if (!isset($profile)) {
    $profile = variable_get('install_profile', 'default');
  }
  $directories[] = "profiles/$profile/libraries";

  foreach ($directories as $d) {
    // note: file_scan_directory() returns a empty array if no file is found
    // in which case the foreach loop is not enter
    foreach (file_scan_directory($d, '/shCore\.js$/', array('nomask' => '/(\.\.?|CVS|src|pupload)$/')) as $filename => $file_info) {
      // the path to syntaxhighlighter lib, (-18 to chop off "/scripts/shCore.js"
      // part at the end
      return substr($filename, 0, -18);
    }
  }
  return NULL;
}


/**
 * Create the autoload setup script file. Must call this whenever lib
 * location  and/or the enable brushes change.  Make sure never call this
 * if the js lib is not found
 */
function _syntaxhighlighter_setup_autoloader_script() {
  $path = 'public://syntaxhighlighter.autoloader.js';
  if (variable_get('syntaxhighlighter_use_autoloader', FALSE)) {
    // use variable_get() instead of _syntaxhighlighter_get_lib_location()
    // because this function is called only if the lib location is found
    $script_path = base_path() . variable_get('syntaxhighlighter_lib_location', NULL) . '/scripts/';
    $script_data = <<<__HERE__
/*
 * This file is generated by the Syntaxhighlighter module
 */
__HERE__;
    $script_data .= "\nfunction syntaxhighlighterAutoloaderSetup() {\n  SyntaxHighlighter.autoloader(\n";
    $need_ending = FALSE;
    $brushes = variable_get('syntaxhighlighter_enabled_languages', array('shBrushPhp.js'));
    foreach ($brushes as $b) {
      if ($b) {
        if ($need_ending) {
          $script_data .= ",\n";
        }
        $alias = strtolower(substr(substr($b, 7), 0, -3));
        $script_data .= "    '$alias $script_path$b'";
        $need_ending = TRUE;
      }
    }
    $script_data .= "\n);\n}\n";
    file_unmanaged_save_data($script_data, $path, FILE_EXISTS_REPLACE);
  }
  else {
    file_unmanaged_delete($path);
  }
}


/**
 * This does the same thing as the old D6 file_directory_path() which
 * is to give the default 'files' directory path relative the the web root
 * 
 * @return a string containing the path to the site's 'files' directory
 */
function _syntaxhighlighter_file_directory_path() {
  return variable_get('file_public_path', conf_path() . '/files');
}


function _syntaxhighlighter_format_has_syntaxhighlighter_filter($format_id) {
  return array_key_exists('syntaxhighlighter', filter_list_format($format_id));
}
