<?php
/*******************************************************************************
 * Copyright (C) 2007 Easter-eggs
 * https://ldapsaisie.org
 *
 * Author: See AUTHORS file in top-level directory.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

******************************************************************************/

LSsession :: loadLSclass('LSlog_staticLoggerClass');

/**
 * Manage template
 *
 * This class is use to manage template in LdapSaisie.
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
class LStemplate extends LSlog_staticLoggerClass {

  /**
   * Javascript files to load on page
   * @see self::addJSscript()
   * @var array
   */
  private static $JSscripts = array();

  /**
   * Libs JS files to load on page
   * @see self::addLibJSscript()
   * @var array
   */
  private static $LibsJSscripts = array();

  /**
   * Javascript configuration parameter to set on page
   * @see self::addJSconfigParam()
   * @var array
   */
  private static $JSconfigParams = array();

  /**
   * CSS files to load on page
   * @see self::addCssFile()
   * @var array
   */
  private static $CssFiles = array();

  /**
   * Libs CSS files to load on page
   * @see self::addLibCssFile()
   * @var array
   */
  private static $LibsCssFiles = array();

  /**
   * LStemplate configuration
   *
   * array(
   *   'smarty_path' => '/path/to/Smarty.php',
   *   'template_dir' => '/path/to/template/directory',
   *   'image_dir' => '/path/to/image/directory',
   *   'css_dir' => '/path/to/css/directory',
   *   'compile_dir' => '/path/to/compile/directory',
   *   'debug' => True,
   *   'debug_smarty' => True
   * )
   *
   * @var array<string,mixed>
   **/
  private static $config = array (
    'smarty_path' => 'smarty/libs/Smarty.class.php',
    'template_dir' => 'templates',
    'image_dir' => 'images',
    'css_dir' => 'css',
    'js_dir' => 'includes/js',
    'libs_dir' => 'includes/libs',
    'compile_dir' => 'tmp',
    'debug' => False,
    'debug_smarty' => False
  );

  /**
   * Smarty object
   * @var Smarty
   */
  public static $_smarty;

  /**
   * Smarty version
   * @var int
   */
  private static $_smarty_version;

  /**
   * Array of directories where file have to be search
   * @var array
   */
  private static $directories = array('local', LS_THEME, './');

  /**
   * Registered events
   * @see self::addEvent()
   * @see self::fireEvent()
   * @var array
   */
  private static $_events = array();

  /**
   * Deprecated templates files
   * @var array<string>
   */
  private static $deprecated_template_files = array (
    'accueil.tpl', 'blank.tpl', 'empty.tpl',
    'top.tpl', 'bottom.tpl',
  );

  /**
   * Keep trace of last displayed template (for loop detection)
   * @var string|null
   */
  private static $last_displayed_template = null;

 /**
  * Start LStemplate
  *
  * Set configuration from parameter $config and initialize
  * Smarty object.
  *
  * @param array $config LStemplate configuration
  *
  * @return boolean True on success, False instead
  **/
  public static function start($config) {
    // Trigger starting event
    self :: fireEvent('starting');

    foreach ($config as $key => $value) {
      self :: $config[$key] = $value;
    }

    if (LSsession :: includeFile(self :: $config['smarty_path'], true)) {
      self :: $_smarty = new Smarty();
      self :: $_smarty -> setTemplateDir(self :: $config['template_dir']);

      if ( ! is_writable(self :: $config['compile_dir']) ) {
        self :: log_fatal(getFData(_("LStemplate : compile directory is not writable (dir : %{dir})"), self :: $config['compile_dir']));
      }
      self :: $_smarty -> setCompileDir(self :: $config['compile_dir']);

      if (self :: $config['debug']) {
        self :: $_smarty -> caching = 0;
        // cache files are always regenerated
        self :: $_smarty -> force_compile = TRUE;
        // recompile template if it is changed
        self :: $_smarty -> compile_check = 1;
        if (self :: $config['debug_smarty']) {
          // debug smarty
          self :: $_smarty -> debugging = true;
        }
      }

      if (method_exists(self :: $_smarty,'register_function')) {
        self :: $_smarty_version=2;
        if (!LSsession :: loadLSclass('LStemplate_smarty2_support')) {
          die(_("LStemplate : Can't load Smarty 2 support file"));
        }

      }
      elseif (method_exists(self :: $_smarty,'registerPlugin')) {
        self :: $_smarty_version=3;
        if (!LSsession :: loadLSclass('LStemplate_smarty3_support')) {
          die(_("LStemplate : Can't load Smarty 3 support file"));
        }
      }
      else {
        die(_("LStemplate : Smarty version not recognized."));
      }

      self :: registerFunction("getFData", "LStemplate_smarty_getFData");
      self :: registerFunction("tr", "LStemplate_smarty_tr");
      self :: registerFunction("img", "LStemplate_smarty_img");
      self :: registerFunction("css", "LStemplate_smarty_css");
      self :: registerFunction("uniqid", "LStemplate_smarty_uniqid");
      self :: registerFunction("var_dump", "LStemplate_smarty_var_dump");

      // Define public root URL
      self :: assign('public_root_url', LSsession :: loadLSclass("LSurl") ? LSurl :: public_url() : null);

      // Trigger started event
      self :: fireEvent('started');

      return True;
    }
    else {
      die(_("LStemplate : Can't load Smarty."));
      return False;
    }
  }

 /**
  * Return the default directory path of files
  *
  * Return LS_THEME contanst value or 'default' if not defined
  *
  * @return string The default directory path of files
  **/
  public static function getDefaultDir() {
    if (defined('LS_THEME'))
      return LS_THEME;
    else
      return 'default';
  }

 /**
  * Return the path of the file to use
  *
  * @param string $file The file name (eg: mail.png)
  * @param string $root_dir The root directory (eg: images)
  * @param string|null $default_dir The default directory (eg: default)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string|false The path of the file, or false if file can't be located
  **/
  public static function getFilePath($file, $root_dir, $default_dir=null, $with_nocache=false) {
    if ($default_dir === null)
      $default_dir = self :: getDefaultDir();
    $path = false;
    foreach(self :: $directories as $dir) {
      $dir_path = realpath($root_dir.'/'.$dir);
      if ($dir_path === false)
        // Directory not found or not accessible
        continue;
      $file_path = realpath($dir_path.'/'.$file);
      if ($file_path === false)
        // File not found or not accessible
        continue;
      // Checks that the file is in the actual folder location
      $pos = strpos($file_path, $dir_path);
      if (!is_int($pos) || $pos != 0) {
        self :: log_error("LStemplate :: getFilePath($file, $root_dir, $default_dir, $with_nocache) : File '$file_path' is not in root directory '$dir_path' (".varDump($pos).").");
      }
      elseif (file_exists($file_path)) {
        $path = $file_path;
        break;
      }
    }
    if (!$path) {
      if (!$default_dir)
        return false;
      $path = $root_dir.'/'.$default_dir.'/'.$file;
    }
    if ($with_nocache)
      $path .= "?nocache=".self::getNoCacheFileValue($path);
    return $path;
  }

 /**
  * Return the path of the image file to use
  *
  * @param string $image The image name (eg: mail)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string The path of the image file
  **/
  public static function getImagePath($image, $with_nocache=false) {
    $exts=array('svg', 'png', 'gif', 'jpg');
    foreach($exts as $ext) {
      $path = self :: getFilePath("$image.$ext", self :: $config['image_dir'], False, $with_nocache);
      if ($path) return $path;
    }
    return self :: $config['image_dir']."/".self :: getDefaultDir()."/$image.png";
  }

 /**
  * Return the path of the CSS file to use
  *
  * @param string $css The CSS name (eg: main.css)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string The path of the CSS file
  **/
  public static function getCSSPath($css, $with_nocache=false) {
    return self :: getFilePath($css, self :: $config['css_dir'], Null, $with_nocache);
  }

 /**
  * Return the path of the JS file to use
  *
  * @param string $js The JS name (eg: LSdefaults.js)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string The path of the CSS file
  **/
  public static function getJSPath($js, $with_nocache=false) {
    return self :: getFilePath($js, self :: $config['js_dir'], Null, $with_nocache);
  }

 /**
  * Return the path of the libary file to use
  *
  * @param string $file_path The lib file path (eg: arian-mootools-datepicker/Picker.js)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string The path of the Lib file
  **/
  public static function getLibFilePath($file_path, $with_nocache=false) {
    return self :: getFilePath($file_path, self :: $config['libs_dir'], Null, $with_nocache);
  }

 /**
  * Return the path of the Smarty template file to use
  *
  * @param string $template The template name (eg: base.tpl)
  * @param bool $with_nocache If true, include nocache URL param (default: false)
  *
  * @return string The path of the Smarty template file
  **/
  public static function getTemplatePath($template, $with_nocache=false) {
    if (in_array($template, self :: $deprecated_template_files))
      self :: log_fatal(
        getFData(
          _("LStemplate : Request template '%{tpl}' is now deprecated. Please refer to upgrade documentation to adapt your templates."),
          $template
        )
      );
    return self :: getFilePath($template, self :: $config['template_dir'], null, $with_nocache);
  }

 /**
  * Return the nocache value of the specify file
  *
  * @param string $file The file path
  *
  * @return string The specified file's nocache value
  **/
  public static function getNoCacheFileValue($file) {
    $stat = @stat($file);
    if (is_array($stat) && isset($stat['mtime']))
      return md5($stat['mtime']);
    return md5(time());
  }

 /**
  * Return the content of a Smarty template file.
  *
  * @param string $template The template name (eg: base.tpl)
  *
  * @return string The content of the Smarty template file
  **/
  public static function getTemplateSource($template) {
    $tpl_path=self :: getTemplatePath($template);
    if (!is_readable($tpl_path)) {
      if (self :: $_smarty_version > 2) {
        // No error return with Smarty3 and highter because it's call
        // template name in lower first systematically
        return '';
      }
      $tpl_path = self :: getTemplatePath(LSsession :: isConnected()?'base_connected.tpl':'base.tpl');
      LSerror::addErrorCode('LStemplate_01',$template);
    }
    return implode('',file($tpl_path));
  }

 /**
  * Return the timestamp of the last change of a Smarty
  * template file.
  *
  * @param string $template The template name (eg: base.tpl)
  *
  * @return int|null The timestamp of the last change of the Smarty template file
  **/
  public static function getTemplateTimestamp($template) {
    $tpl_path=self :: getTemplatePath($template);
    if (is_file($tpl_path)) {
      $time=filemtime($tpl_path);
      if ($time)
        return $time;
    }
    return null;
  }

 /**
  * Assign template variable
  *
  * @param string $name The variable name
  * @param string $value The variable value
  *
  * @return void
  **/
  public static function assign($name,$value) {
    self :: $_smarty -> assign($name,$value);
  }

  /**
   * Assign common template variables
   *
   * @return void
   **/
  public static function assignCommonVars() {
    // JS config
    LStemplate :: addHelpInfo(
      'LSdefault',
      array(
        'copy_to_clipboard' => _('Copy to clipboard'),
        'copied' => _('Copied!'),
      )
    );
    LStemplate :: assign('LSjsConfig', base64_encode(json_encode(self :: $JSconfigParams)));

    // JS files
    $defaultJSscripts = array(
      'mootools-core.js',
      'mootools-more.js',
      'functions.js',
      'LSdefault.js',
      'LSinfosBox.js',
    );
    if (isset($GLOBALS['defaultJSscripts']) && is_array($GLOBALS['defaultJSscripts']))
      foreach ($GLOBALS['defaultJSscripts'] as $file)
        if (!in_array($file, $defaultJSscripts))
          $defaultJSscripts[] = $file;
    LStemplate :: assign('defaultJSscripts', $defaultJSscripts);

    $JSscripts = array();
    foreach (self :: $JSscripts as $script)
      if (!in_array($script, $JSscripts) && !in_array($script, $defaultJSscripts))
        $JSscripts[] = $script;
    LStemplate :: assign('JSscripts', $JSscripts);
    LStemplate :: assign('LibsJSscripts', self :: $LibsJSscripts);
    LStemplate :: assign('LSdebug', boolval(LSdebug));

    // CSS files
    $defaultCssFiles = array("LSdefault.css");
    if (isset($GLOBALS['defaultCSSfiles']) && is_array($GLOBALS['defaultCSSfiles']))
      foreach ($GLOBALS['defaultCSSfiles'] as $file)
        if (!in_array($file, $defaultCssFiles))
          $defaultCssFiles[] = $file;
    LStemplate :: assign('defaultCssFiles', $defaultCssFiles);
    LStemplate :: assign('CssFiles', self :: $CssFiles);
    LStemplate :: assign('LibsCssFiles', self :: $LibsCssFiles);
    LStemplate :: assign('LS_VERSION', LS_VERSION);
    LStemplate :: assign('request', class_exists('LSurl') ? LSurl :: $request : null);
  }

 /**
  * Display a template
  *
  * @param string $template The template name (eg: base_connected.tpl)
  *
  * @return void
  **/
  public static function display($template) {
    // Trigger displaying event
    self :: fireEvent('displaying');

    // Handle loop detection
    if (self :: $last_displayed_template == $template) {
       self :: log_fatal("display($template): loop detected, stop");
       return;
    }

    try {
      self :: $last_displayed_template = $template;
      self :: assignCommonVars();
      self :: $_smarty -> display("ls:$template");
    }
    catch (Exception $e) {
      self :: log_exception($e, getFData(_("Smarty - An exception occured displaying template '%{template}'"), $template));
      exit();
    }

    // Trigger displayed event
    self :: fireEvent('displayed');
  }

 /**
  * Fetch a template
  *
  * @param string $template The template name (eg: base_connected.tpl)
  *
  * @return string|false The template compiled or false in case of error
  **/
  public static function fetch($template) {
    try {
      return self :: $_smarty -> fetch("ls:$template");
    }
    catch (Exception $e) {
      self :: log_exception($e, getFData(_("Smarty - An exception occured fetching template '%{template}'"), $template), false);
    }
    return false;
  }

  /**
   * Handle fatal error
   *
   * @param string|null $error Error message (optional)
   *
   * @return void
   **/
  public static function fatal_error($error=null) {
    http_response_code(500);
    if (LSsession :: get('api_mode') || LSsession :: getAjaxDisplay()) {
      header('Content-Type: application/json');
      $errors = array(_("A fatal error occured. If problem persist, please contact support."));
      if ($error)
        $errors[] = $error;
      echo json_encode(
        array('errors' => $errors, 'success' => false),
        (isset($_REQUEST['pretty'])?JSON_PRETTY_PRINT:0)
      );
    }
    elseif (self :: $last_displayed_template == 'error.tpl') {
      // Detect & stop loop displaying error
      die(getFData(_('<h1>Loop detected displaying this error:</h1><pre>%{error}</pre>'), $error));
    }
    else {
      self :: assign('pagetitle', _("A fatal error occured."));
      self :: assign('error', _("A fatal error occured. If problem persist, please contact support."));
      self :: assign('details', $error);
      self :: display("error.tpl");
    }
    exit();
  }

 /**
  * Register a template function
  *
  * @param string $name The function name in template
  * @param string $function_name The function name in PHP
  *
  * @return void
  */
  public static function registerFunction($name,$function_name) {
    LStemplate_register_function($name,$function_name);
  }

  /**
   * Registered an action on a specific event
   *
   * @param string $event The event name
   * @param callable $callable The callable to run on event
   * @param array $param Paremeters that will be pass to the callable
   *
   * @return void
   */
  public static function addEvent($event,$callable,$param=NULL) {
    self :: $_events[$event][] = array(
      'callable' => $callable,
      'param'    => $param,
    );
  }

  /**
   * Run triggered actions on specific event
   *
   * @param string $event Event name
   *
   * @return boolean True if all triggered actions succefully runned, false otherwise
   */
  public static function fireEvent($event) {
    $return = true;

    // Binding via addEvent
    if (isset(self :: $_events[$event]) && is_array(self :: $_events[$event])) {
      foreach (self :: $_events[$event] as $e) {
        if (is_callable($e['callable'])) {
          try {
            call_user_func_array($e['callable'],array(&$e['param']));
          }
          catch(Exception $er) {
            LSerror :: addErrorCode('LStemplate_03',array('callable' => format_callable($e['callable']),'event' => $event));
            $return = false;
          }
        }
        else {
          LSerror :: addErrorCode('LStemplate_02',array('callable' => format_callable($e['callable']),'event' => $event));
          $return = false;
        }
      }
    }

    return $return;
  }

  /*
   * Javascript & CSS files helpers methods
   */


  /**
   * Add a JS script to load on page
   *
   * @param string $file The JS filename
   *
   * Note: about old $path of the LStemplate :: addJSscript() method, corresponding to
   * the sub-directory path that contain the file, you could just prefix the file name.
   *
   * @return void
   */
  public static function addJSscript($file) {
   if (!in_array($file, self :: $JSscripts))
     self :: $JSscripts[] = $file;
  }

  /**
   * Add a library JS file to load on page
   *
   * @param string $file The JS filename
   *
   * @return void
   */
  public static function addLibJSscript($file) {
   if (!in_array($file, self :: $LibsJSscripts))
     self :: $LibsJSscripts[] = $file;
  }

  /**
   * Add Javascript configuration parameter
   *
   * @param string $name Name of the configuration parameter
   * @param mixed $val Value of the configuration parameter
   *
   * @return void
   */
  public static function addJSconfigParam($name,$val) {
    self :: $JSconfigParams[$name]=$val;
  }

  /**
   * Get Javascript configuration parameters
   *
   * @return array Javascript configuration parameters
   */
  public static function getJSconfigParam() {
    return self :: $JSconfigParams;
  }

  /**
   * Add help info
   *
   * @param string $group The group name of this information
   * @param array $info Array of the information to add (name => value)
   *
   * @return void
   */
  public static function addHelpInfo($group, $info) {
    if (is_array($info)) {
      if (isset(self :: $JSconfigParams['helpInfo'][$group]) && is_array(self :: $JSconfigParams['helpInfo'][$group])) {
        self :: $JSconfigParams['helpInfo'][$group] = array_merge(self :: $JSconfigParams['helpInfo'][$group],$info);
      }
      else {
        self :: $JSconfigParams['helpInfo'][$group] = $info;
      }
    }
  }

  /**
   * Add a CSS file to load on page
   *
   * @param string $file The CSS filename
  *
  * Note: about old $path of the LStemplate :: addCssFile() method, corresponding to
  * the sub-directory path that contain the file, you could just prefix the file name.
  *
  * @return void
  */
  public static function addCssFile($file) {
   if (!in_array($file, self :: $CssFiles))
     self :: $CssFiles[] = $file;
  }

  /**
   * Add a library CSS file to load on page
   *
   * @param string $file The CSS filename
   *
   * @return void
   */
  public static function addLibCssFile($file) {
   if (!in_array($file, self :: $LibsCssFiles))
     self :: $LibsCssFiles[] = $file;
  }

}

function LStemplate_smarty_getFData($params) {
    echo getFData($params['format'], $params['data'], $meth=NULL);
}

function LStemplate_smarty_tr($params) {
  $msg = __($params['msg']);
  unset($params['msg']);
  echo $params?getFData($msg, $params):$msg;
}

function LStemplate_smarty_img($params) {
  echo "image/".$params['name'];
}

function LStemplate_smarty_css($params) {
  echo "css/".$params['name'];
}

function LStemplate_smarty_uniqid($params, &$smarty) {
  if (!isset($params['var']))
    $params['var'] = 'uniqid';
  $smarty -> assign($params['var'], uniqid());
}

function LStemplate_smarty_var_dump($params, &$smarty) {
  var_dump($params['data']);
}

// Errors
LSerror :: defineError('LStemplate_01',
___("LStemplate : Template %{file} not found.")
);
LSerror :: defineError('LStemplate_02',
___("LStemplate : Fail to execute trigger %{callable} on event %{event} : is not callable.")
);
LSerror :: defineError('LStemplate_03',
___("LStemplate : Error during the execution of the trigger %{callable} on event %{event}.")
);