<?php
namespace CMSMSExt;
//declare(strict_types = 1); #revisit later

use CMSMSExt;
use cms_mailer;
use cms_userprefs;
use cms_utils;
use CmsApp;
use CmsLangOperations;
use CmsLayoutTemplate;
use CMSModule;
use CMSMS\Database\compatibility as db_compatibility;
use CMSMS\Database\Connection as Database;
use CMSMS\HookManager;
use CMSMSExt\Email\Email;
use CMSMSExt\Email\EmailProcessor;
use CMSMSExt\Email\FileEmailStorage;
use CMSMSExt\Email\iEmailStorage;
use CMSMSExt\Email\SimpleEmailProcessor;
use CMSMSExt\internal\settings;
use CMSMSExt\jsloader\jsloader;
use CMSMSExt\ModuleFunctions\preferences;
use UserOperations;
use xt_cached_remote_file;
use xt_dir;
use xt_encrypt;
use xt_http;
use xt_image;
use xt_param;
use xt_setup;
use xt_template_admin;
use xt_template_utils;
use xt_tmpdata;
use xt_userops;
use xt_utils;

class XTModule extends \CMSModule
{
  /**
   * @ignore
   */
  private static $_initialized;
  
  protected $_ini_data;
  /**
   * @var \CMSMSExt\ModuleFunctions\preferences
   */
  protected preferences $preferences;
  
  private $_autoload_data = [];
  
  /**
   * @ignore
   */
  public $_actionid;
  
  /**
   * @ignore
   */
  public string $_actionname;
  
  /**
   * @ignore
   */
  public $_image_directories;
  
  /**
   * @ignore
   */
  public $_current_action;
  
  /**
   * @ignore
   */
  public $_errormsg;
  
  /**
   * @ignore
   */
  public $_returnid;
  
  /**
   * Checks for a default permission
   * this is WIP and may not work properly
   * currently reserved for testing
   *
   * @return bool Returns true if the function has default permission, false otherwise.
   */
  protected function _has_default_permission()
  {
    return $this->CheckPermission( 'admin_' . $this->GetName() );
  }
  
  /**
   * Retrieves the minimum PHP version.
   *
   * Modules should override this function if they need to and return the minimal PHP version they support.
   *
   * @return string The current PHP version if not overridden.
   */
  protected function GetMinimumPhpVersion()
  {
    return \phpversion();
  }
  
  protected function IsPhpVersionSupported()
  {
    return \version_compare(\PHP_VERSION, $this->GetMinimumPhpVersion() ) >= 0;
  }
  
  protected function _ext_include()
  {
    $patterns = [
      cms_join_path(
        $this->GetModulePath(),
        'includes',
        'inc.*.php'
      ),
      cms_join_path(
        $this->GetModulePath(),
        'includes',
        '{function,functions}.*.php'
      ),
      cms_join_path(
        $this->GetModulePath(),
        'lib',
        'functions',
        '{function,functions}.*.php'
      ),
      cms_join_path(
        $this->GetModulePath(),
        '.wip',
        'functions',
        '{function,functions}.*.php'
      ),
      cms_join_path(
        $this->GetModulePath(),
        '.wip',
        '{abstract,class,interface,trait}.*.php'
      ),
    ];
    
    foreach($patterns as $pattern)
    {
      foreach(glob($pattern, GLOB_BRACE) as $filename)
      {
        include_once $filename;
      }
    }
  }
  
  protected function _load_init()
  {
    $fn = $this->GetModulePath() . '/data/module.ini';
    
    if(\file_exists($fn))
    {
      $this->_ini_data = new \CMSMSExt\FS\SimpleINI($fn);
    }
  }
    
    
    /**
   * The constructor.
   * This method does numerous things, including setup an extended autoloader,
   * create defines for the module itself.  i.e: MOD_CMSMSExt, or MOD_MAMS.
   * sets up a built in cache driver for temporarily caching data.
   * and register numerous smarty plugins (see the documentation for those).
   */
  public function __construct()
  {
    \spl_autoload_register([$this, 'autoload']);
    parent::__construct();
    $this->_load_init();
    $this->config = ConfigManager::GetConfigInstance($this);
    $this->preferences = new preferences($this);
    
    global $CMS_INSTALL_PAGE, $CMS_PHAR_INSTALL;
  
    if(isset($CMS_INSTALL_PAGE) || isset($CMS_PHAR_INSTALL)) { return; }
  
    $class = \get_class($this);
  
    if(!\defined('MOD_' . \strtoupper($class) ) )
    {
      /**
       * @ignore
       */
      \define('MOD_' . \strtoupper($class), $class);
    }
    
    $this->_ext_include();
  }
  
  public function RegisterAutoloadAlias($alias = NULL)
  {
    $this->_autoload_data[$alias] = $this->GetName();
  }
  
  /**
   * An extended autoload method.
   * Search for classes a <module>/lib/class.classname.php file.
   * or for interfaces in a <module>/lib/interface.classname.php file.
   * or as a last ditch effort, for simple classes in the <module>/lib/extraclasses.php file.
   * This method also supports namespaces,  including <module> and <module>/sub1/sub2 which should exist in files as
   * described above. in subdirectories below the <module>/lib directory.
   *
   * @param string $classname
   *
   * @return bool
   *
   * @internal
   */
  public function autoload($classname) : bool
  {
    if( !\is_object($this) ) { return FALSE; }
    
    // check for classes.
    $path = $this->GetModulePath() . '/lib';
    
    if( FALSE !== \strpos($classname, '\\') )
    {
      $t_path = \str_replace('\\', '/', $classname);
      
      if(\startswith($t_path, $this->GetName() . '/'))
      {
        $classname = \basename($t_path);
        $t_path    = \dirname($t_path);
        $t_path    = \substr($t_path, \strlen($this->GetName()) + 1);
        $path      = $this->GetModulePath() . '/lib/' . $t_path;
      }
      elseif(!empty($this->_autoload_data))
      {
        $t_parts = \explode('\\', $classname);
   
        if(\array_key_exists($t_parts[0], $this->_autoload_data))
        {
          \array_shift($t_parts);
          $t_path    = \implode('\\', $t_parts);
          $classname = \basename($t_path);
          $t_path    = \dirname($t_path);
          $t_path    = \substr($t_path, \strlen($this->GetName()) + 1);
          $path      = $this->GetModulePath() . '/lib/' . $t_path;
        }
      }
    }
    
    $fn = $path . "/{$classname}.php";
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    $fn = $path . "/class.{$classname}.php";
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    // check for abstract classes.
    $fn = $path . "/abstract.{$classname}.php";
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    // check for interfaces
    $fn = $path . "/interface.{$classname}.php";
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    // check for traits
    $fn = $path . "/trait.{$classname}.php";
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    // check for a master file
    $fn = $this->GetModulePath() . '/lib/extraclasses.php';
    
    if(\is_file($fn))
    {
      require_once($fn);
      
      return TRUE;
    }
    
    return FALSE;
  }
  
  /**
   * @return mixed
   */
  public function GetExtendedConfig()
  {
    return $this->config;
  }
  
  /**
   * @return mixed
   */
  public function GetExtendedPreferences()
  {
    return $this->preferences;
  }

  /**
   * @ignore
   */
  public function InitializeFrontend()
  {
    parent::InitializeFrontend();
    $this->SetParameterType('xt_msg', CLEAN_STRING);
    $this->SetParameterType('xt_msgkey', CLEAN_STRING);
    $this->SetParameterType('xt_error', CLEAN_INT);
    $this->SetParameterType('nocache', CLEAN_INT);
    $this->SetParameterType('xt_activetab', CLEAN_STRING);
    $this->SetParameterType('_d', CLEAN_STRING);
    
    if(!$this instanceof \CMSMSExt) { return; }
    
    $this->RestrictUnknownParams();
  }
  
  /**
   * @ignore
   * @deprecated
   */
  protected function get_utils() : internal_utils
  {
    static $_obj;
    if(!$_obj)
    {
      $_obj = new internal_utils($this->get_xt());
    }
    
    return $_obj;
  }
  
  
  /**
   * @ignore
   * @deprecated
   */
  private function _load_form()
  {
    require_once(__DIR__ . '/form_tools.php');
  }
  
  /**
   * The Friendly name for this module.  For use in the admin navigation.
   *
   * @return string
   * @see CMSModule::GetFriendlyName()
   * @abstract
   */
  public function GetFriendlyName()
  {
    if( !empty($this->_ini_data) )
    {
      return $this->_ini_data->Get('Module', 'FriendlyName');
    }
    
    if(CmsLangOperations::lang_key_exists($this->GetName(), 'friendlyname'))
    {
      return $this->Lang('friendlyname');
    }
    
    return parent::GetFriendlyName();
  }
  
  /**
   * Return the version of this module.
   *
   * @return string
   * @see CMSModule::GetVersion()
   */
  public function GetVersion()
  {
    if( !empty($this->_ini_data) )
    {
      $test = $this->_ini_data->Get('Module', 'Version');
      
      return $test;
    }
    
    return '0.0.1';
  }
  
  /**
   * Return the help of this module.
   * This method will look in a series of files to try to find the help for a module.
   *    doc/help.inc, docs/help.inc, doc/help.html, docs/help.html, help.inc and help.html in that order
   * This method will also try to find API documentation HTML files in numerous directories:
   *    doc/apidoc, doc/apidocs, docs/apidoc and docs/apidocs in that order.
   * If API documentation can be found, then the top of the help will be modified to include a link to the API docs.
   *
   * @return string
   * @see CMSModule::GetHelp()
   */
  public function GetHelp()
  {
    $dir  = $this->GetModulePath();
    
    $fn = \cms_join_path($dir, 'templates', 'help.tpl');
  
    if( \is_file($fn) )
    {
      $smarty = \cms_utils::get_smarty();
      $config = \cms_utils::get_config();
      $vars = [
        'root_url' => $config['root_url'],
        'config' => $config,
        'mod' => $this,
        'xt' => $this->get_xt(),
      ];
  
      $smarty->assign($vars);

      return $this->ProcessTemplate('help.tpl');
    }

    $files = [
      'doc/help.inc',
      'docs/help.inc',
      'doc/help.html',
      'docs/help.html',
      'help.inc',
      'help.html'
    ];
    
    foreach($files as $file)
    {
      $test = \cms_join_path($dir, $file);
      
      if( \is_file($test) ) { return \file_get_contents($test); }
    }
    
    # all else fails...
    return '<!-- error: missing help file! -->';
  }
  
  /**
   * Return the changelog for this module.
   *
   * @return string
   * @see CMSModule::GetChangeLog()
   */
  public function GetChangeLog()
  {
    $dir   = $this->GetModulePath();
  
    $fn = \cms_join_path($dir, 'templates', 'changelog.tpl');
  
    if( \is_file($fn) )
    {
      $smarty = \cms_utils::get_smarty();
      $config = \cms_utils::get_config();
      $vars = [
        'root_url' => $config['root_url'],
        'config' => $config,
        'mod' => $this,
        'xt' => $this->get_xt(),
      ];
  
      $smarty->assign($vars);
      
      return $this->ProcessTemplate('changelog.tpl');
    }
    
    $files = [
      'docs/changelog.inc',
      'doc/changelog.inc',
      'docs/changelog.html',
      'doc/changelog.html',
      'changelog.inc'
    ];
    
    foreach($files as $file)
    {
      $fn = \cms_join_path($dir, $file);
      
      if( \is_file($fn) ) { return \file_get_contents($fn); }
    }
  
    # all else fails...
    return '<!-- error: missing changelog file! -->';
  }
  
  /**
   * Get the section of the admin navigation that this module belongs to.
   *
   * @abstract
   * @return string
   */
  public function GetAdminSection()
  {
    $name = \strtolower($this->GetName() . '_adminsection');
    
    return xt_param::get_string( (array)$this->config, $name, 'extensions' );
  }
  
  /**
   * Get a human readable description for this module.
   *
   * @abstract
   * @return string
   */
  public function GetAdminDescription()
  {
    if( !empty($this->_ini_data) )
    {
      return $this->_ini_data->Get('Module', 'Description');
    }
    
    return $this->Lang('moddescription');
  }
  
  /**
   * Get a hash containing dependent modules, and their minimum versions.
   *
   * @abstract
   * @return array
   */
  public function GetDependencies(){ return []; }
  
  /**
   * Display a custom message after the module has been installed.
   *
   * @abstract
   * @return string
   */
  public function InstallPostMessage()
  {
    return $this->Lang('postinstall');
  }
  
  /**
   * Return if this is a plugin module (for the frontend of the website) or not.
   *
   * @return bool
   * @see CMSModule::IsPluginModule()
   * @abstract
   */
  public function IsPluginModule()
  {
    return parent::IsPluginModule();
  }
  
  /**
   * @ignore
   */
  function HasCapability($capability, $params = [])
  {
    switch($capability)
    {
      case 'tasks':
        return TRUE;
      default:
        return FALSE;
    }
  }
  
  /**
   * Return if this module has an admin section.
   *
   * @return string
   * @see CMSModule::HasAdmin()
   * @abstract
   */
  public function HasAdmin()
  {
    return parent::HasAdmin();
  }
  
  /**
   * Return a message to display after the module has been uninstalled.
   *
   * @abstract
   * @return string
   */
  public function UninstallPostMessage()
  {
    return $this->Lang('postuninstall');
  }
  
  /**
   * Test if this module is visible in the admin navigation to the currently logged in admin user.
   *
   * @abstract
   * @return bool
   */
  public function VisibleToAdminUser()
  {
    return (
              $this->CheckPermission('Modify Site Preferences')
              || $this->CheckPermission('Modify Templates')
           );
           //&& $this->_has_default_permission(); # TODO: test this thoroughly!
  }
  
  /**
   * Retrieve some HTML to be output in all admin requests for this module (and its descendants).
   * By default, this class calls the jsloader::render method,  and includes some standard styles
   * If a file entitled css/admin_styles.css exists in a derived module's root directory then this stylesheet
   * will be included in the header html.
   *
   * @abstract
   * @return bool
   * @throws \CmsInvalidDataException
   *
   * @see \CMSMSExt\jsloader\jsloader::render();
   */
  public function GetHeaderHTML()
  {
    $mod  = $this->get_xt();
    $out = '';
    
    $file = $mod->find_module_file('js/common.js');
    
    if($file)
    {
      $mod->get_jsloader()->add_jsfile($file);
    }
    
    $fn = $mod->find_module_file('css/admin_styles.css');
  
    if($fn)
    {
      $css = \str_replace(\CMS_ROOT_PATH, \CMS_ROOT_URL, $fn);
      $out .= '<link rel="stylesheet" href="' . $css . '"/>' . "\n";
    }
    
    $config = $this->GetExtendedConfig();
    
    if($config['add_backslash'])
    {
      # add our css toolkit
      $fn = $mod->GetModuleURLPath() . '/css/backslash/backslash.css';
      
      if($fn)
      {
        $css = \str_replace(\CMS_ROOT_PATH, \CMS_ROOT_URL, $fn);
        $out .= '<link rel="stylesheet" href="' . $css . '"/>' . "\n";
      }
    }
    
    if($config['add_uikit'])
    {
      # add uikit css toolkit
      $fn = $mod->GetModuleURLPath() . '/css/uikit/uikit.css';
      
      if($fn)
      {
        $css = \str_replace(\CMS_ROOT_PATH, \CMS_ROOT_URL, $fn);
        $out .= '<link rel="stylesheet" href="' . $css . '"/>' . "\n";
      }
      
      #and uikit js tools
      $fn = $mod->GetModulePath() . '/js/uikit/uikit.js';
  
      if($fn)
      {
        $mod->get_jsloader()->add_jsfile($fn);
      }
      
      $fn = $mod->GetModulePath() . '/js/uikit/uikit-icons.js';
  
      if($fn)
      {
        $mod->get_jsloader()->add_jsfile($fn);
      }
      
    }
  
    $out .= $mod->get_jsloader()->render(['addkey' => \session_id()]);
    
    
    return $out;
  }
  
  public function GetParentPath()
  {
    $config = \cmsms()->GetConfig();
    
    return \cms_join_path($config['root_path'], 'modules', \MOD_CMSMSEXT);
  }
  
  /**
   * A replacement for the built-in DoAction method
   * For CMSMSExt derived modules some  builtin smarty variables are created
   * module hints are handled,  and input type=image values are corrected in input parameters.
   *
   * this method also handles setting the active tab, and displaying any messages or errors
   * set with the SetError or SetMessage methods.
   *
   * This method is called automatically by the system based on the incoming request, and the page template.
   * It should almost never be called manually.
   *
   * @param string $name     the action name
   * @param string $id       The module action id
   * @param array  $params   The module parameters
   * @param string $returnid The page that will contain the HTML results.  This is empty for admin requests.
   *
   * @throws \CmsError404Exception
   * @see SetError()
   * @see SetMessage()
   * @see SetCurrentTab()
   * @see RedirectToTab()
   *
   */
  public function DoAction($name, $id, $params, $returnid = '')
  {
    $this->set_action_id($id);
    $this->set_action_name($name);
    
    $active_tab    = xt_param::get_string($params, 'xt_activetab');
    $config        = $this->GetExtendedConfig();
    $template_name = $name . '.tpl';
    
    if($active_tab) { $this->SetCurrentTab($active_tab); }
    
    // handle the stupid input type='image' problem. TODO: Revisit... JM
    foreach($params as $key => $value)
    {
      if( \endswith($key, '_x') )
      {
        $base = \substr($key, 0, -2);
        $k = $base . '_y';
        if(isset($params[$k]) && !isset($params[$base]))
        {
          $params[$base] = $base;
        }
      }
    }
    
    // handle module hints note: deprecated as the core already has this feature
    $hints = cms_utils::get_app_data('__MODULE_HINT__' . $this->GetName());
    
    if(\is_array($hints))
    {
      foreach($hints as $key => $value)
      {
        if(isset($params[$key])) { continue; }
        
        $params[$key] = $value;
      }
    }
    
    // redundant for cmsms 2.0+
    $smarty = $this->GetActionTemplateObject();
    
    if(!$smarty) { $smarty = $this->cms->GetSmarty(); }
    
    $db       = \cms_utils::get_db();
    $gCms     = \cmsms(); # for backwards compatibility but deprecated
    $CMSMSExt = \xt_utils::get_xt();

    $smarty->assign('actionid', $id);
    $smarty->assign('actionparams', $params);
    $smarty->assign('returnid', $returnid);
    $smarty->assign('mod', $this);
    $smarty->assign('xt', $CMSMSExt);
    $smarty->assign('actionname', $name);
    $smarty->assign($this->GetName(), $this);
    xt_tmpdata::set( 'module', $this->GetName() );
  
    if('' !== $name)
    {
      $name         = \preg_replace('/[^A-Za-z0-9\-_+]/', '', $name);
      
      $got_contents = FALSE;
      $contents     = '';
      
      // Check "assets/module_custom/<module_name>/action"
      $filename = \cms_join_path(
        $config['root_path'],
        'assets',
        'module_custom',
        $this->GetName(),
        'action',
        'action.' . $name . '.php'
      );
      
      if( !$got_contents && @\is_file($filename) )
      {
        \ob_start();
        include($filename);
        $contents .= \ob_get_clean();
        $got_contents = TRUE;
      }
      
      if($got_contents)
      {
        if(empty($contents))
        {
          try
          {
            $contents = $this->ProcessTemplate($template_name);
          }
          catch(\Exception $e)
          {
            // don't worry about it
          }
        }

        echo $contents;
        return;
      }
      
      // Check "module_custom/<module_name>/action" if we don't
      $filename = \cms_join_path(
        $config['root_path'],
        'module_custom',
        $this->GetName(),
        'action',
        'action.' . $name . '.php'
      );
  
      if( !$got_contents && @\is_file($filename) )
      {
        \ob_start();
        include($filename);
        $contents .= \ob_get_clean();
        $got_contents = TRUE;
      }
      
      if($got_contents)
      {
        if(empty($contents))
        {
          try
          {
            $contents = $this->ProcessTemplate($template_name);
          }
          catch(\Exception $e)
          {
            // don't worry about it
          }
        }
        
        echo $contents;
        return;
      }
      
      // Check module own action file if we don't
      $filename = \cms_join_path(
        $this->GetModulePath(),
        'action.' . $name . '.php'
      );
  
      if( !$got_contents && @\is_file($filename) )
      {
        \ob_start();
        include($filename);
        $contents .= \ob_get_clean();
        $got_contents = TRUE;
      }
      
      if($got_contents)
      {
        if(empty($contents))
        {
          try
          {
            $contents = $this->ProcessTemplate($template_name);
          }
          catch(\Exception $e)
          {
            // don't worry about it
          }
        }
        
        echo $contents;
        return;
      }
      
      // Check framework if we don't
      $filename = \cms_join_path(
        $this->GetParentPath(),
        'framework',
        'action.' . $name . '.php'
      );
  
      if( !$got_contents && @\is_file($filename) )
      {
        \ob_start();
        include($filename);
        $contents .= \ob_get_clean();
        //$contents .= ob_get_contents();
        $got_contents = TRUE;
      }
      
      if($got_contents)
      {
        if(empty($contents))
        {
          try
          {
            $contents = $this->ProcessTemplate($template_name);
          }
          catch(\Exception $e)
          {
            // don't worry about it
          }
        }
        
        echo $contents;
        return;
      }
    }
    
    $template_name = $name . '.tpl';
    \dbg()->die($template_name);
    

    
    # all else fails just go with the parent's action
    parent::DoAction($name, $id, $params, $returnid);
  }
  
  /**
   * A convenience method to encrypt some data
   *
   * @param string $key  The encryption key
   * @param string $data The data to encrypt
   *
   * @return string The encrypted data
   * @see xt_encrypt
   */
  function encrypt(string $key, $data)
  {
    return xt_encrypt::encrypt($key, $data);
  }
  
  
  /**
   * A convenience method to decrypt some data
   *
   * @param string $key  The encryption key
   * @param string $data The data to decrypt
   *
   * @return string The derypted data
   * @see xt_encrypt
   */
  function decrypt(string $key, string $data)
  {
    return xt_encrypt::decrypt($key, $data);
  }
  
  /**
   * A convenience function to create a url for a module action.
   * This method is deprecated as the CMSModule::create_url method replaces it.
   *
   * @param string $id       the module action id
   * @param string $action   The module action
   * @param string $returnid The page that the url will refer to.  This is empty for admin requests
   * @param array  $params   Module parameters
   * @param bool   $inline   For frontend requests only dictates whether this url should be inline only.
   * @param string $prettyurl
   *
   * @deprecated
   */
  function CreateURL($id, $action, $returnid, $params = array(), $inline = FALSE, $prettyurl = '')
  {
    \trigger_error('Deprecated usage of ' . __METHOD__);
    
    return $this->create_url($id, $action, $returnid, $params, $inline, FALSE, $prettyurl);
  }
  
  
  /* ======================================== */
  /* FORM FUNCTIONS                           */
  /* ======================================== */
  
  /**
   * A convenience method to create a control that contains a 'sortable list'.
   * The output control is translated, and interactive and suitable for use in forms.
   *
   * @param string $id           The module action id
   * @param string $name         The input element name
   * @param array  $items        An associative array of the items for this list.
   * @param string $selected     A comma separated string of selected item keys
   * @param bool   $allowduplicates
   * @param int    $max_selected The maximum number of items that can be selected
   * @param string $template     Specify an alternate template ffor the sortable list control.  Smarty resources are
   *                             permitted.  But if no resource is specified it is assumed that the template belongs to
   *                             CMSMSExt.
   * @param string $label_left   A label for the left column.
   * @param string $label_right  A label for the right column.
   *
   * @return string
   * @throws \SmartyException
   * @deprecated
   */
  function CreateSortableListArea(
    $id, $name, $items, $selected = '', $allowduplicates = TRUE, $max_selected = -1,
    $template = '', $label_left = '', $label_right = ''
  )
  {
    $xt = $this->get_xt();
    if(empty($label_left))
    {
      $label_left = $xt->Lang('selected');
    }
    if(empty($label_right))
    {
      $label_right = $xt->Lang('available');
    }
    if(!$template)
    {
      $template = 'sortablelist.tpl';
    }
    
    $tpl = $xt->CreateSmartyTemplate($template);
    $tpl->assign('selectarea_selected_str', NULL);
    $tpl->assign('selectarea_selected', []);
    
    if(!empty($selected))
    {
      $sel = \explode(',', $selected);
      $tmp = [];
      foreach($sel as $theid)
      {
        if(\array_key_exists($theid, $items))
        {
          $tmp[$theid] = $items[$theid];
        }
      }
      $tpl->assign('selectarea_selected_str', $selected);
      $tpl->assign('selectarea_selected', $tmp);
    }
    
    $tpl->assign('xt', $xt);
    $tpl->assign('max_selected', $max_selected);
    $tpl->assign('label_left', $label_left);
    $tpl->assign('label_right', $label_right);
    $tpl->assign('selectarea_masterlist', $items);
    $tpl->assign('selectarea_prefix', $id . $name);
    
    if($allowduplicates)
    {
      $allowduplicates = 1;
    }
    else
    {
      $allowduplicates = 0;
    }
    
    $tpl->assign('allowduplicates', $allowduplicates);
    $tpl->assign('upstr', $xt->Lang('up'));
    $tpl->assign('downstr', $xt->Lang('down'));
    
    return $tpl->fetch();
  }
  
  
  /**
   * Create a translated Yes/No dropdown.
   * The output control is translated, and suitable for use in forms.
   * This method is deprecated.  It is best to assign all data to smarty and then create input elements as necessary in
   * the smarty template.
   *
   * @param string $id            the module action id
   * @param string $name          The name for the input element
   * @param int    $selectedvalue The selected value (0 == no, 1 == yes)
   * @param string $addtext
   *
   * @return string
   * @deprecated
   */
  function CreateInputYesNoDropdown($id, $name, $selectedvalue = '', $addtext = '')
  {
    return $this->get_utils()->xt_CreateInputYesNoDropdown($id, $name, $selectedvalue, $addtext);
  }
  
  /**
   * Create a custom submit button.
   * The output control is translated, and suitable for use in forms.
   * This method is deprecated.  It is best to assign all data to smarty and then create input elements as necessary in
   * the smarty template.
   *
   * @param string $id          the module action id
   * @param string $name        The name for the input element
   * @param string $value       The value for the submit button
   * @param string $addtext     Additional text for the tag
   * @param string $image       an optional image path
   * @param string $confirmtext Optional confirmation text
   * @param string $class       Optional value for the class attribute
   *
   * @return string
   * function xtCreateInputSubmit($id,$name,$value='',$addtext='',$image='', $confirmtext='',$class='')
   * {
   * $this->_load_form();
   * return xt_CreateInputSubmit($this,$id,$name,$value,$addtext,$image,$confirmtext,$class);
   * }
   * @deprecated
   */
  
  
  /**
   * Create a custom checkbox.
   * This is similar to the standard checkbox but has a hidden field with the same name
   * before it so that some value for this field is always returned to the form handler.
   * This method is deprecated.  It is best to assign all data to smarty and then create input elements as necessary in
   * the smarty template.
   *
   * @param string $id            the module action id
   * @param string $name          The name for the input element
   * @param string $value         The value for the checkbox
   * @param string $selectedvalue The current value of the field.
   * @param string $addtext       Additional text for the tag
   *
   * @return string
   * @deprecated
   */
  function CreateInputCheckbox($id, $name, $value = '', $selectedvalue = '', $addtext = '')
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    $this->_load_form();
    
    return \xt_CreateInputCheckbox($this, $id, $name, $value, $selectedvalue, $addtext);
  }
  
  
  /**
   * A Convenience function for creating form tags.
   * This method re-organises some of the parameters of the original CreateFormStart method
   * and handles current tab functionalty, and sets the encoding type of the form to multipart/form-data
   *
   * This method is deprecated and will be replaced in CMSMS 2.0 by the core {form_start} tag.
   *
   * @param string $id       the module action id
   * @param string $action   the destination action
   * @param string $returnid The destination pagpe for the action handler.  Empty for admin requests
   * @param array  $params   additional parameters to be passed with the form
   * @param bool   $inline   wether this is an inline form request (output will replace module tag rather than the
   *                         entire content section of the template.
   * @param string $method   The form method.
   * @param string $enctype  The form encoding type
   * @param string $idsuffix
   * @param string $extra    Extra text for thhe form tag
   *
   * @return string
   * @deprecated
   */
  function XTCreateFormStart(
    $id, $action = 'default', $returnid = '', $params = array(), $inline = FALSE, $method = 'post',
    $enctype = '', $idsuffix = '', $extra = ''
  )
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    if('' == $enctype)
    {
      $enctype = 'multipart/form-data';
    }
    
    return $this->CreateFormStart($id, $action, $returnid, $method, $enctype, $inline, $idsuffix, $params, $extra);
  }
  
  
  /**
   * A convenience function for creating a frontend form
   * This method re-organises some of the parameters of the original CreateFormStart method
   * and sets the encoding type of the form to multipart/form-data
   *
   * This method is deprecated and will be replaced in CMSMS 2.0 by the core {form_start} tag.
   *
   * @param string $id       the module action id
   * @param string $action   the destination action
   * @param string $returnid The destination pagpe for the action handler.  Empty for admin requests
   * @param array  $params   additional parameters to be passed with the form
   * @param bool   $inline   wether this is an inline form request (output will replace module tag rather than the
   *                         entire content section of the template.
   * @param string $method   The form method.
   * @param string $enctype  The form encoding type
   * @param string $idsuffix
   * @param string $extra    Extra text for thhe form tag
   *
   * @return string
   * @deprecated
   */
  function XTCreateFrontendFormStart(
    $id, $action = 'default', $returnid = '', $params = array(), $inline = TRUE, $method = 'post',
    $enctype = '', $idsuffix = '', $extra = ''
  )
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    $this->_load_form();
    
    return $this->CreateFrontendFormStart(
      $id, $returnid, $action, $method, $enctype, $inline, $idsuffix, $params, $extra
    );
  }
  
  
  /**
   * A convenience method to create a hidden input element for forms.
   * This method is deprecated.  It is best to assign all data to smarty and then create input elements as necessary in
   * the smarty template.
   *
   * @param string $id      the module action id
   * @param string $name    The name of the input element
   * @param string $value   The value of the input element
   * @param string $addtext Additional text for the tag
   * @param string $delim   the delimiter for value separation.
   *
   * @return string
   * @deprecated
   */
  function CreateInputHidden($id, $name, $value = '', $addtext = '', $delim = ',')
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    $this->_load_form();
    
    return xt_CreateInputHidden($this, $id, $name, $value, $addtext, $delim);
  }
  
  
  /**
   * For admin requests only, pass variables so that the specified tab will be displayed
   * by default in the resulting action.
   *
   * @param string $ignored (old actionid param)
   * @param string $tab     The name of the parameter
   * @param string $params  Extra parameters for the request
   * @param string $action  The designated module action.  If none is specified 'defaultadmin' is assumed.
   *
   * @return void
   */
  function RedirectToTab($ignored = '', $tab = '', $params = '', $action = '')
  {
    if(!$action)
    {
      $action = $this->_current_action;
    }
    $this->RedirectToAdminTab($tab, $params, $action);
  }
  
  /**
   * @ignore
   */
  function XTRedirect($id, $action, $returnid = '', $params = array(), $inline = FALSE)
  {
    $this->Redirect($id, $action, $returnid, $params, $inline);
  }
  
  /**
   * Test if the current request is an admin request.
   *
   * @return bool True for an admin action, false otherwise.
   * @deprecated
   */
  function IsAdminAction()
  {
    $gCms = $this->cms;
    if(
      $gCms->test_state(CmsApp::STATE_ADMIN_PAGE) && !$gCms->test_state(CmsApp::STATE_INSTALL) &&
      !$gCms->test_state(CmsApp::STATE_STYLESHEET)
    )
    {
      return TRUE;
    }
    
    return FALSE;
  }
  
  /**
   * Set the current action for the next request of the admin console.
   * Used for the various admin forms.
   *
   * @param string $action The action name
   */
  function SetCurrentAction(string $action)
  {
    $action                = \trim($action);
    $this->_current_action = $action;
  }
  
  
  /**
   * A function for using a template to display an error message.
   * This method is suitable for frontend displays.
   *
   * @param string $txt   The error message
   * @param string $class An optional class attribute value.
   *
   * @deprecated
   */
  function DisplayErrorMessage(string $txt, string $class = 'alert alert-danger')
  {
    $tpl = $this->CreateSmartyTemplate('error.tpl');
    $tpl->assign('xt_errorclass', $class);
    $tpl->assign('xt_errormsg', $txt);
    
    return $tpl->fetch();
  }
  
  
  /**
   * Get the CMSMSExt module
   */
  final protected function get_xt() : CMSMSExt
  {
    static $_obj;
    
    if(!$_obj)
    {
      $_obj = $this->GetModuleInstance('CMSMSExt');
      
      if(!$_obj)
      {
        throw new \RuntimeException('CMSMSExt module not loadable.. does it need an upgrade?');
      }
    }
    
    return $_obj;
  }
  

  
  /**
   * A function to return an array of of country codes and country names.
   * This method returns data suitable for giving to smarty and displaying in a dropdown.
   *
   * @return array|null
   * @deprecated
   */
  public function get_country_list_options()
  {
    $xt = $this->get_xt();
    $fn  = $xt->find_module_file('etc/countries.json');
    if($fn)
    {
      return \json_decode(\file_get_contents($fn), TRUE);
    }
  }
  
  
  /**
   * Get a list of currency codes and their names.
   *
   * @return array|null
   * @throws \JsonException
   * @deprecated
   */
  public function get_currency_list_options()
  {
    $xt = $this->get_xt();
    $fn  = $xt->find_module_file('etc/currencies.json');
    if($fn)
    {
      return \json_decode(\file_get_contents($fn), TRUE, 512, \JSON_THROW_ON_ERROR);
    }
  }
  
  /**
   * A convenience function to get the country name given the acronym
   *
   * @param string $the_acronym
   *
   * @return string
   * @deprecated
   */
  function GetCountry(string $the_acronym)
  {
    $list = $this->get_country_list_options();
    
    return $list[$the_acronym] ?? NULL;
  }
  
  
  /**
   * A convenience function to get the state name given the acronym
   *
   * @param string $the_acronym
   *
   * @return string
   * @deprecated
   */
  function GetState(string $the_acronym)
  {
    $list = $this->get_state_list_options();
    
    return $list[$the_acronym] ?? NULL;
  }
  
  
  /**
   * A convenience function to create an image dropdown from all of the image files in a specified directory.
   * This method will not ignore thumbnails.
   *
   * @param string $id           The module action id
   * @param string $name         the name for the dropdown.
   * @param string $selectedfile The initial value for the dropdown (an image filename)
   * @param string $dir          The path (relative to the uploads path) to the directory to pull images from.  If not
   *                             specified, the image uploads path will be used.
   * @param mixed  $none         If true, then 'None' will be prepended to the list of output images.  If a string it's
   *                             value will be used.
   *
   * @return string.
   * @deprecated
   */
  function CreateImageDropdown($id, $name, $selectedfile, $dir = '', $none = '')
  {
    //trigger_error('Deprecated usage of ' . __METHOD__);
    $config = $this->config;
    if(startswith($dir, '.'))
    {
      $dir = '';
    }
    if('' == $dir)
    {
      $dir = $config['image_uploads_path'];
    }
    if(!\is_dir($dir))
    {
      $dir = \cms_join_path($config['uploads_path'], $dir);
    }
    
    $extensions = $this->xt_settings()->imageextensions;
    $filelist   = xt_dir::get_file_list($dir, $extensions);
    if($none)
    {
      if(!\is_string($none))
      {
        $xt  = $this->get_xt();
        $none = $xt->Lang('none');
      }
      $filelist = \array_merge([$none => ''], $filelist);
    }
    
    return $this->CreateInputDropdown($id, $name, $filelist, -1, $selectedfile);
  }
  
  
  /**
   * A convenience function to create a list of filenames in a specified directory.
   *
   * @param string $id            The module action id
   * @param string $name          the name for the dropdown.
   * @param string $selectedfile  The initial value for the dropdown (an image filename)
   * @param string $dir           The path (relative to the uploads path) to the directory to pull images from.  If not
   *                              specified, the image uploads path will be used.
   * @param string $extensions    A comma separated list of filename extensions to include in the list.  If not
   *                              specified the module preference will be used.
   * @param bool   $allownone     Allow no files to be selected.
   * @param bool   $allowmultiple To allow selecting multiple files.
   * @param int    $size          The size of the dropdown.
   *
   * @return string.
   * @deprecated
   */
  function CreateFileDropdown(
    $id, $name, $selectedfile = '', $dir = '', $extensions = '', $allownone = '', $allowmultiple = FALSE, $size = 3
  )
  {
    //trigger_error('Deprecated usage of ' . __METHOD__);
    $config = $this->config;
    if('' == $dir)
    {
      $dir = $config['uploads_path'];
    }
    else
    {
      while(startswith($dir, '/') && '' != $dir)
      {
        $dir = \substr($dir, 1);
      }
      $dir = $config['uploads_path'] . $dir;
    }
    
    $tmp  = xt_dir::get_file_list($dir, $extensions);
    $tmp2 = [];
    if(!empty($allownone))
    {
      $xt                      = $this->get_xt();
      $tmp2[$xt->Lang('none')] = '';
    }
    $filelist = \array_merge($tmp2, $tmp);
    
    if($allowmultiple)
    {
      if(!endswith($name, '[]'))
      {
        $name .= '[]';
      }
      
      return $this->CreateInputSelectList($id, $name, $filelist, [], $size);
    }
    
    return $this->CreateInputDropdown($id, $name, $filelist, -1, $selectedfile);
  }
  
  
  /**
   * A convenience function to create a color selection dropdown
   *
   * @param string $id            The module action id
   * @param string $name          the name for the dropdown.
   * @param string $selectedvalue The initial value for the input field.
   *
   * @return string
   * @deprecated
   */
  function CreateColorDropdown($id, $name, $selectedvalue = '')
  {
    //trigger_error('Deprecated usage of ' . __METHOD__);
    
    $this->_load_form();
    $extensions = $this->get_xt();
    
    return \xt_CreateColorDropdown($extensions, $id, $name, $selectedvalue);
  }
  
  /* ======================================== */
  /* IMAGE FUNCTIONS                         */
  /* ======================================== */
  
  /**
   * @ignore
   */
  function TransformImage($srcSpec, $destSpec, $size = '')
  {
    return xt_image::transform_image($srcSpec, $destSpec, $size);
  }
  
  /**
   * A convenience method to create an image tag.
   * This method will automatically search through added image dirs for frontend and admin requests
   * and through the admin theme directories for admin requests.
   *
   * @param string $id      The module action id
   * @param string $alt     The alt attribute for the tag
   * @param int    $width   Width in pixels
   * @param int    $height  Height in pixels
   * @param string $class   Value for the class attribute
   * @param string $addtext Additional text for the img tag.
   *
   * @return string
   * @deprecated
   * @see xt_tags::create_image
   * @see AddImageDir.
   */
  function CreateImageTag(
    string $id, string $alt = '', int $width = NULL, int $height = NULL, string $class = '', string $addtext = ''
  )
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    
    return $this->get_utils()->CreateImageTag($id, $alt, $width, $height, $class, $addtext);
  }
  
  
  /**
   * A convenience method to display an image.
   * This method will automatically search through added image dirs for frontend and admin requests
   * and through the admin theme directories for admin requests.
   *
   * @param string $image  The basename for the desired image.
   * @param string $alt    The alt attribute for the tag
   * @param string $class  Value for the class attribute
   * @param int    $width  Width in pixels
   * @param int    $height Height in pixels
   *
   * @return string
   * @deprecated
   * @see xt_tags::create_image
   * @see AddImageDir.
   */
  function DisplayImage(string $image, string $alt = '', string $class = '', int $width = NULL, $height = NULL)
  {
    return $this->get_utils()->DisplayImage($this, $image, $alt, $class, $width, $height);
  }
  
  
  /**
   * A convenience method to create a link to a module action containing an image and optionally some text.
   *
   * This method will automatically search through added image dirs for frontend and admin requests
   * and through the admin theme directories for admin requests.
   *
   * @param string $id           The module action id
   * @param string $action       The name of the destination action
   * @param int    $returnid     The page for the destination of the request.  Empty for admin requests.
   * @param string $contents     The text content of the image.
   * @param string $image        The basename of the image to display.
   * @param array  $params       Additional link parameters
   * @param string $classname    Class for the img tag.
   * @param string $warn_message An optional confirmation message
   * @param bool   $imageonly    Wether the contents (if specified) should be ignored.
   * @param bool   $inline
   * @param string $addtext
   * @param bool   $targetcontentonly
   * @param string $prettyurl    An optional pretty url slug.
   *
   * @return string
   * @deprecated
   * @see CreateLink
   * @see CreateURL())
   * @see xt_tags::create_image
   * @see AddImageDir())
   * @see DisplayImage()
   */
  function CreateImageLink(
    $id, $action, $returnid, $contents, $image, $params = array(), $classname = '',
    $warn_message = '', $imageonly = TRUE, $inline = FALSE,
    $addtext = '', $targetcontentonly = FALSE, $prettyurl = ''
  )
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    
    return $this->get_utils()->CreateImageLink(
      $this, $id, $action, $returnid, $contents, $image, $params, $classname, $warn_message,
      $imageonly, $inline, $addtext, $targetcontentonly, $prettyurl
    );
  }
  
  
  /**
   * Add a directory to the list of searchable directories
   *
   * @param string $dir A directory relative to this module installation directory.
   */
  function AddImageDir($dir)
  {
    if(!\startswith('/', $dir))
    {
      $dir = $this->GetModuleUrlPath() . '/' . $dir;
      $dir = \str_replace($this->config['root_url'], '', $dir);
    }
    $this->_image_directories[] = $dir;
  }
  
  
  /**
   * List all templates stored with this module that begin with the same prefix.
   *
   * @param string $prefix The optional prefix
   * @param bool   $trim
   *
   * @return array
   * @see xt_template_utils::get_templates_by_prefix()
   * @deprecated
   */
  function ListTemplatesWithPrefix(string $prefix = '', bool $trim = FALSE)
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    
    return xt_template_utils::get_templates_by_prefix($this, $prefix, $trim);
  }
  
  
  /**
   * Create a dropdown of all templates beginning with the specified prefix
   *
   * @param string $id            The module action id
   * @param string $name          The name for the input element.
   * @param string $prefix        The optional prefix
   * @param string $selectedvalue The default value for the input element
   * @param string $addtext
   *
   * @return string
   * @deprecated
   */
  function CreateTemplateDropdown($id, $name, $prefix = '', $selectedvalue = -1, $addtext = '')
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    
    return xt_template_utils::create_template_dropdown($id, $name, $prefix, $selectedvalue, $addtext);
  }
  
  
  /**
   * Part of the multiple database template functionality
   * this function provides an interface for adding, editing,
   * deleting and marking active all templates that match
   * a prefix.
   *
   * @param string $id                  The module action id (pass in the value from doaction)
   * @param int    $returnid            The page id to use on subsequent forms and links.
   * @param string $prefix              The template prefix
   * @param string $defaulttemplatepref The name of the template containing the system default template.  This can
   *                                    either be the name of a database template or a filename ending with .tpl.
   * @param string $active_tab          The tab to return to
   * @param string $defaultprefname     The name of the preference that contains the name of the current default
   *                                    template.  If empty string then there will be no possibility to set a default
   *                                    template for this list.
   * @param string $title               Title text to display in the add/edit template form
   * @param string $info                Information text to display in the add/edit template form
   * @param string $destaction          The action to return to.
   *
   * @deprecated Use the CmsLayoutTemplate class(es) in 2.0 capable modules.
   */
  function ShowTemplateList(
    $id, $returnid, $prefix, $defaulttemplatepref, $active_tab, $defaultprefname,
    $title, $info = '', $destaction = 'defaultadmin'
  )
  {
    return $this->get_xt()->_DisplayTemplateList(
      $this, $id, $returnid, $prefix, $defaulttemplatepref, $active_tab,
      $defaultprefname, $title, $info, $destaction
    );
  }
  
  
  /**
   * @ignore
   */
  function _DisplayTemplateList(
    &$module, $id, $returnid, $prefix, $defaulttemplatepref, $active_tab, $defaultprefname,
    $title, $info = '', $destaction = 'defaultadmin'
  )
  {
    return $this->get_utils()->_DisplayTemplateList(
      $module, $id, $returnid, $prefix, $defaulttemplatepref, $active_tab,
      $defaultprefname, $title, $info, $destaction
    );
  }
  
  
  /**
   * GetDefaultTemplateForm.
   * A function to return a form suitable for editing a single template.
   *
   * @param CMSMSExt $module A CMSMSExt derived module reference
   * @param string      $id
   * @param string      $returnid
   * @param string      $prefname
   * @param string      $action
   * @param string      $active_tab
   * @param string      $title
   * @param string      $filename
   * @param string      $info
   *
   * @return string
   * @deprecated (this functionality is irrelevant in CMSMS 2.0)
   * @see xt_template_admin::get_start_template_form
   */
  function GetDefaultTemplateForm(
    &$module, $id, $returnid, $prefname, $action, $active_tab, $title, $filename, $info = ''
  )
  {
    return xt_template_admin::get_start_template_form(
      $module, $id, $returnid, $prefname, $action, $active_tab, $title,
      $filename, $info
    );
  }
  
  
  /**
   * EditDefaultTemplateForm
   *
   * A function to return a form suitable for editing a single template.
   *
   * @param CMSMSExt $module A CMSMSExt derived module reference
   * @param string      $id
   * @param string      $returnid
   * @param string      $prefname
   * @param string      $active_tab
   * @param string      $title
   * @param string      $filename
   * @param string      $info
   * @param string      $action
   *
   * @return string
   * @deprecated (this functionality is irrelevant in CMSMS 2.0)
   * @see xt_template_admin::get_start_template_form
   */
  function EditDefaultTemplateForm(
    &$module, $id, $returnid, $prefname, $active_tab, $title, $filename, $info = '', $action = 'defaultadmin'
  )
  {
    echo xt_template_admin::get_start_template_form(
      $module, $id, $returnid, $prefname, $action, $active_tab, $title, $filename, $info
    );
  }
  
  
  /**
   * Get the username of the currently logged in admin user.
   *
   * @param int $uid
   *
   * @return string
   * @deprecated
   */
  function GetAdminUsername(int $uid)
  {
    @\trigger_error('Deprecated usage of ' . __METHOD__);
    $user = UserOperations::LoadUserByID($uid);
    
    return $user->username;
  }
  
  
  /**
   * Get a human readable error message for an upload code.
   *
   * @param string $code The upload error code.
   *
   * @return string
   * @deprecated
   * @see xt_fileupload
   */
  function GetUploadErrorMessage(string $code)
  {
    return $this->get_xt()->Lang($code);
  }
  
  
  /**
   * @ignore
   */
  function is_alias(string $str)
  {
    if(!\preg_match('/^[\-\_\w]+$/', $str)) { return FALSE; }
    
    return TRUE;
  }
  
  
  /**
   * @ignore
   */
  final protected function set_action_id(string $id = NULL){ $this->_actionid = $id; }
  
  /**
   * @ignore
   */
  final protected function set_action_name(string $name){ $this->_actionname = $name; }
  
  /**
   * @ignore
   */
  final protected function get_action_name(){ return $this->_actionname; }
  
  /**
   * Return the current action id
   *
   * @depcreated
   * @return int
   * @internal
   */
  protected function get_action_id()
  {
    if($this->_actionid)
    {
      return $this->_actionid;
    }
    if(!$this->cms->is_frontend_request())
    {
      return 'm1_';
    }
  }
  
  
  /**
   * Return the current action id
   *
   * @depcreated
   * @return int
   * @internal
   */
  function GetActionId()
  {
    if(!\method_exists($this, 'get_action_id') && \MOD_CMSMSEXT != $this->GetName())
    {
      die('FATAL ERROR: A module derived from CMSMSExt is not handling the get_action_id method');
    }
    
    return $this->get_action_id();
  }
  
  
  /**
   * Get a form for adding or editing a single template.
   *
   * @param CMSMSExt $module   A CMSMSExt module reference
   * @param string       $id
   * @param int          $returnid
   * @param string       $tmplname The name of the template to edit
   * @param string       $active_tab
   * @param string       $title
   * @param string       $filename The name of the file (in the module's template directory) containing the system
   *                               default template.
   * @param string       $info
   * @param string       $destaction
   * @param int          $simple
   *
   * @deprecated
   * @see xt_template_admin::get_single_template_form
   */
  function GetSingleTemplateForm(
    &$module, $id, $returnid, $tmplname, $active_tab, $title, $filename, $info = '', $destaction = 'defaultadmin',
    $simple = 0
  )
  {
    return xt_template_admin::get_single_template_form(
      $module, $id, $returnid, $tmplname, $active_tab, $title, $filename,
      $info, $destaction, $simple
    );
  }
  
  
  /**
   * Retrieve a human readable string for any error generated during watermarking.
   *
   * @param string $error the watermarking error code
   *
   * @return string
   * @deprecated
   */
  function GetWatermarkError(string $error)
  {
    if(empty($error) || 0 === $error) { return ''; }
  
    return $this->get_xt()->Lang('watermarkerror_' . $error);
  }
  
  
  /**
   * A convenience method to clear any session data associated with this module.
   *
   * @param string $key If not specified clear all session data relative to this module."
   */
  function session_clear(string $key = '')
  {
    $pkey = 'c' . \md5(__FILE__ . \get_class($this));
    if(empty($key))
    {
      unset($_SESSION[$pkey]);
    }
    else
    {
      unset($_SESSION[$pkey][$key]);
    }
  }
  
  /**
   * A convenience method to store some session data associated with this module.
   *
   * @param string $key   The variable key.
   * @param mixed  $value The data to store.
   */
  function session_put(string $key, $value)
  {
    $pkey = 'c' . \md5(__FILE__ . \get_class($this));
    if(!isset($_SESSION[$pkey]))
    {
      $_SESSION[$pkey] = [];
    }
    $_SESSION[$pkey][$key] = $value;
  }
  
  /**
   * A convenience method to retrieve some session data associated with this module.
   *
   * @param string $key       The variable key.
   * @param mixed  $dfltvalue "The default value to return if the specified data does not exist."
   *
   * @return mixed.
   */
  function session_get(string $key, $dfltvalue = '')
  {
    $pkey = 'c' . \md5(__FILE__ . \get_class($this));
    
    if(!isset($_SESSION[$pkey])) { return $dfltvalue; }
  
    return $_SESSION[$pkey][$key] ?? $dfltvalue;
  }
  
  
  /**
   * Return data identified by a key either from the supplied parameters, or from session.
   *
   * @param array  $params       Input parameters
   * @param string $key          The data key
   * @param string $defaultvalue The data to return if the specified data does not exist in the session or in the input
   *                             parameters.
   *
   * @return mixed.
   */
  function param_session_get(&$params, $key, $defaultvalue = '')
  {
    if(isset($params[$key]))
    {
      return $params[$key];
    }
    
    return $this->session_get($key, $defaultvalue);
  }
  
  
  /**
   * Given a page alias resolve it to a page id.
   *
   * @param mixed $txt  The page alias to resolve.  If an integer page id is passed in that is acceptable as well.
   * @param int   $dflt The default page id to return if no match can be found
   *
   * @return int
   */
  function resolve_alias_or_id($txt, int $dflt = NULL)
  {
    $txt = \trim((string)$txt);
    if(!$txt)
    {
      return $dflt;
    }
    $manager = $this->cms->GetHierarchyManager();
    $node    = NULL;
    if(\is_numeric($txt) && (int)$txt > 0)
    {
      $node = $manager->find_by_tag('id', (int)$txt);
    }
    else
    {
      $node = $manager->find_by_tag('alias', $txt);
    }
    if($node)
    {
      return (int)$node->get_tag('id');
    }
    
    return $dflt;
  }
  
  
  /**
   * Perform an HTTP post request.
   *
   * @param string $URL     the url to post to
   * @param array  $data    The array to post.
   * @param string $referer An optional referrer string.
   *
   * @return string
   * @deprecated
   * @see xt_http
   */
  function http_post($URL, $data = '', $referer = '')
  {
    return xt_http::post($URL, $data, $referer);
  }
  
  
  /**
   * Perform an HTTP GET request.
   *
   * @param string $URL     the url to post to
   * @param string $referer An optional referrer string.
   *
   * @return string
   * @see xt_http
   * @deprecated
   */
  function http_get(string $URL, string $referer = '')
  {
    return xt_http::get($URL, $referer);
  }
  
  /**
   * Similar to GetPreference except the default value is used even if the preference exists, but is blank.
   *
   * @param string $pref_name   The preference name
   * @param string $dflt_value  The default value for the preference if not set (or empty)
   * @param bool   $allow_empty Whether the default value should be used if the preference exists, but is empty.
   *
   * @return string.
   */
  public function XTGetPreference($pref_name, $dflt_value = NULL, $allow_empty = FALSE)
  {
    $tmp = \trim($this->GetPreference($pref_name, $dflt_value));
    if(!empty($tmp) || \is_numeric($tmp)) { return $tmp; }
    
    if($allow_empty) { return $tmp; }
    
    return $dflt_value;
  }
  
  /**
   * A wrapper to get a module specific user preference.
   * this method only applies to admin users.
   *
   * @param string $pref_name   The preference name
   * @param string $dflt_value  The default value for the preference if not set (or empty)
   * @param bool   $allow_empty Wether the default value should be used if the preference exists, but is empty.
   *
   * @return string.
   */
  public function XTGetUserPreference(string $pref_name, string $dflt_value = NULL, bool $allow_empty = FALSE)
  {
    $key = '__' . $this->GetName() . '_' . $pref_name;
    $tmp = cms_userprefs::get($key, $dflt_value);
    if(!empty($tmp) || \is_numeric($tmp))
    {
      return $tmp;
    }
    if($allow_empty)
    {
      return $tmp;
    }
    
    return $dflt_value;
  }
  
  /**
   * A wrapper to set a user preference that is module specific.
   * this method only applies to admin users.
   *
   * @param string $pref_name The preference name
   * @param string $value     The preference value.
   */
  public function XTSetUserPreference(string $pref_name, string $value = '')
  {
    $key = '__' . $this->GetName() . '_' . $pref_name;
    
    return cms_userprefs::set($key, $value);
  }
  
  /**
   * A wrapper to remove a user preference that is module specific.
   * this method only applies to admin users.
   *
   * @param string $pref_name The preference name
   *
   * @return null
   */
  public function XTRemoveUserPreference(string $pref_name)
  {
    $key = '__' . $this->GetName() . '_' . $pref_name;
    
    return cms_userprefs::remove($key);
  }
  
  /**
   * Get the prefererred email storage mechanism
   *
   * @return IEmailStorage
   * @example sending_an_email.php
   * @since   1.59
   */
  public function get_email_storage() : IEmailStorage
  {
    static $_obj;
    if(!$_obj)
    {
      $_obj = new FileEmailStorage($this);
    }
    
    return $_obj;
  }
  
  /**
   * Get a mailer filled with some defaults
   *
   * @return cms_mailer
   * @since 1.63
   */
  public function create_new_mailer() : cms_mailer
  {
    return new cms_mailer();
  }
  
  /**
   * Get a new email processor
   *
   * @param Email $eml The email that will be processed.
   *
   * @return IEmailProcessor
   * @since   1.63
   * @example sending_an_email.php
   */
  public function create_new_mailprocessor(Email $eml) : EmailProcessor
  {
    return new SimpleEmailProcessor($eml, $this->create_new_mailer());
  }
  
  /**
   * Find a file for this module
   * looks in module_custom, and in the module directory
   *
   * Example:
   * <pre><code>$fn = $this->find_module_file('images/icon.png');</code></pre>
   *
   * @param string $filename The file to search for
   *
   * @return string If found returns the complete file specification to the found file.
   */
  public function find_module_file(string $filename) : string
  {
    if( \startswith($filename, '/') )
    {
      $filename = \substr($filename, 1);
    }
    
    if(!$filename) { return ''; }
    
    $tmp = \realpath($filename);
    
    if($tmp) { return ''; } // absolute paths not accepted.
    
    $config  = $this->config;
    $dirlist = [];
    
    if($config['assets_path'] && \version_compare(\CMS_VERSION, '2.2-beta1') >= 0 )
    {
      $dirlist[] = $config['assets_path'] . '/module_custom/' . $this->GetName();
    }
    
    $dirlist[] = \CMS_ROOT_PATH . '/module_custom/' . $this->GetName();
    $dirlist[] = $this->GetModulePath();
    
    foreach($dirlist as $dir)
    {
      $fn = "$dir/$filename";
      
      if( \is_file($fn) ) { return $fn; }
    }
    
    return '';
  }
  
  /**
   * Get a list of module files matching a specified pattern in a specified module subdirectory.
   * This method can be used for finding a list of files matching a pattern (i.e a list of classes, or even a list of
   * templates). This method will search for files in a matching directory in the module_custom directory (if one
   * exists) and in the module directory. i.e: $this->get_module_files('templates','summary*tpl');
   *
   * @param string $dirname The directory name (relative to the module directory) to search in.
   * @param string $pattern An optional pattern, if no pattern is specified, *.* is assumed.
   *
   * @return array
   */
  public function get_module_files(string $dirname, string $pattern = '') :array
  {
    if(!$dirname) { return []; }
    
    if(\file_exists($dirname)) { return []; } // absolute paths not accepted.
    
    if(empty($pattern)) { $pattern = '*.*'; }
    
    $files = [];
    $dirs  = [\CMS_ROOT_PATH . '/module_custom/' . $this->GetName(), $this->GetModulePath()];
    
    foreach($dirs as $dir)
    {
      $fn = "$dir/$dirname";
      
      if( !\is_dir($fn) ) { continue; }
      
      $files = array_merge($files, \glob("$fn/$pattern"));
    }
    
    return $files;
  }

  
  /**
   * Given a filename, search for it in the module_custom and module's templates directory.
   * i.e.: $this->find_template_file('somereport.tpl');
   *
   * @param string $filename The template filename (only the filename) to search for
   *
   * @return string The absolute path to the filename.
   */
  public function find_template_file(string $filename)
  {
    if(!$filename) { return ''; }
    $filename = 'templates/' . \basename($filename);
    
    return $this->find_module_file($filename);
  }
  
  /**
   * A convenience method to generate a new smarty template object given a resource string,
   * and a prefix.  This method will also automatically assign a few common smarty variables
   * to the new scope.
   *
   * Note: the parent smarty scope depends on how this function is called.  If called directly from a module action for
   * the same module the parent will be the current smarty scope.  If called from any method that is using a different
   * module than the action module, then the parent scope will be the global smarty scope.
   *
   * @param string      $template_name The desired template name.
   * @param string|null $prefix        an optional prefix for database templates.
   * @param string|null $cache_id      An optional smarty cache id.
   * @param string|null $compile_id    An optional smarty compile id.
   * @param null        $parent        An optional parent template object.
   *
   * @return object
   * @throws \SmartyException
   */
  public function CreateSmartyTemplate(
    string $template_name, string $prefix = NULL, string $cache_id = NULL, string $compile_id = NULL, $parent = NULL
  )
  {
    $smarty = NULL;
    
    if($parent) { $smarty = $parent; }
    
    if(!$smarty) { $smarty = $this->GetActionTemplateObject(); }
    
    if(!$smarty) { $smarty = $this->cms->GetSmarty(); }
    
    $tpl = $smarty->createTemplate(
      $this->XTGetTemplateResource($template_name, $prefix), $cache_id, $compile_id, $smarty
    );
    
    // for convenience, I assign a few smarty variables.
    $tpl->assign('module', $this->GetName());
    $tpl->assign($this->GetName(), $this);
    $tpl->assign('mod', $this);
    $tpl->assign('actionid', $this->get_action_id());
    if(($actionparams = $smarty->getTemplateVars('actionparams')))
    {
      $tpl->assign('actionparams', $actionparams);
    }
    
    return $tpl;
  }
  
  /**
   * Given a template name/resource and some data return the results.
   *
   * @param string $tpl_name
   * @param array  $data
   *
   * @return string
   * @see CMSMSExt::CreateSmartyTemplate
   */
  public function ProcessSimpleTemplate(string $tpl_rsrc, array $data = NULL) : string
  {
    $tpl = $this->CreateSmartyTemplate($tpl_rsrc);
    foreach($data as $key => $val)
    {
      $tpl->assign($key, $val);
    }
    
    return $tpl->fetch();
  }
  
  /**
   * A convenience method to generate a smarty resource string given a template name and an optional prefix.
   * if the supplied template name begins with :: then we assume a 'cms_template' resource.
   * if the supplied template name appears to be a smarty resource name we don't do anything.
   * if the supplied template name ends with .tpl then a file template is assumed.
   *
   * @param string $template_name The desired template name
   * @param string $prefix        an optional prefix for database templates.
   *
   * @return string
   */
  public function XTGetTemplateResource(string $template_name, string $prefix = NULL)
  {
    $template_name = trim($template_name);
    if(
      startswith($template_name, 'string:') || startswith($template_name, 'eval:') ||
      startswith($template_name, 'extends:')
    )
    {
      throw new \LogicException('Invalid resource name passed to ' . __METHOD__);
    }
    if(startswith($template_name, '::'))
    {
      // assume design manager template
      $template_name = substr($template_name, 2);
      $template_name = 'cms_template:' . $template_name;
    }
    else if(!strpos($template_name, ':'))
    {
      // it's not a resource.
      if(endswith($template_name, '.tpl'))
      {
        return $this->GetFileResource($template_name);
      }
      
      // deprecated
      return $this->GetDatabaseResource($prefix . $template_name);
    }
    
    return $template_name;
  }
  
  /**
   * An advanced method to process either a file, or database template for this module
   * through smarty
   *
   * @param string $template_name The template name.  If the value of this parameter ends with .tpl then a file
   *                              template is assumed.  Otherwise a database template is assumed.
   * @param string $prefix        For database templates, optionally prefix thie template name with this value.
   *
   * @return string The output from the processed smarty template.
   * @deprecated
   */
  public function XTProcessTemplate(string $template_name, string $prefix = NULL)
  {
    trigger_error('Deprecated usage of ' . __METHOD__);
    $rsrc   = $this->XTGetTemplateResource($template_name, $prefix);
    $smarty = $this->GetActionTemplateObject();
    if(!$smarty)
    {
      $smarty = $this->cms->GetSmarty();
    }
    
    return $smarty->fetch($rsrc);
  }
  
  /**
   * Get the name of the module that the current action is for.
   * (only works with modules derived from CMSMSExt).
   * This method is useful to find the module action that was used to send an event.
   *
   * @return string
   */
  public function GetActionModule()
  {
    return xt_tmpdata::get('module');
  }
  
  /**
   * Create a url to an action that will show a message.
   *
   * This method is capable of using the \xt_message class to extract longer messages.
   *
   * @param int|string $page     A page id or alias to display the message on
   * @param string     $msg      The message to display.  Or the message key name.
   * @param bool       $is_key   If true, it indicates that the $msg parameter is a key to extract from \xt_message.
   * @param bool       $is_error Indicates if the message should be displayed as an error.
   *
   * @return string The URL to the action.
   * @see CMSMSExt::DisplayErrorMessage();
   */
  public function GetShowMessageURL(string $page, string $msg, bool $is_key = FALSE, bool $is_error = FALSE)
  {
    $page = $this->resolve_alias_or_id($page);
    if(!$page)
    {
      throw new \LogicException('Invalid page passed to ' . __METHOD__);
    }
    
    $parms = [];
    
    if($is_key)
    {
      $parms['xt_msgkey'] = trim($msg);
    }
    else
    {
      $parms['xt_msg'] = trim($msg);
    }
    
    $parms['xt_error'] = ($is_error) ? 1 : 0;
    $mod               = $this->get_xt();
    
    return $mod->create_url('cntnt01', 'showmessage', $page, $parms);
  }
  
  
  /**
   * Show a message on the frontend of the website.
   * suitable for displaying errors and brief messages.
   *
   * @param string $msg      The message to display
   * @param bool   $is_error whether or not the msg is an error
   * @param string $title    If not an error, optionally display a title to the message.
   */
  public function ShowFormattedMessage(string $msg, bool $is_error = FALSE, string $title = NULL)
  {
    if($is_error)
    {
      echo $this->DisplayErrorMessage($msg);
    }
    else
    {
      // todo... need a template for this.
      echo '<div class="xt_message">';
      if($title)
      {
        echo '<div class="xt_msgtitle">' . $title . '</div>';
      }
      echo '<div class="xt_msgbody">' . $msg . '</div>';
      echo '</div>';
    }
  }
  
  /**
   * Find and return a module that can do notifications.
   *
   * @return CMSModule|null
   */
  protected function get_notifier_module()
  {
    static $_obj;
    if(!$_obj)
    {
      $list = $this->GetModulesWithCapability('notifications');
      if(empty($list))
      {
        return;
      }
      $module_name = $list[0];
      if(!method_exists($module_name, 'send_message'))
      {
        return;
      }
      $_obj = $this->GetModuleInstance($module_name);
    }
    
    return $_obj;
  }
  
  /**
   * Create a new notification message
   *
   * @param array $opts An associative array of options.
   *
   * @return notification_message
   */
  protected function create_notification_message(array $opts = NULL) : notification_message
  {
    return new notification_message;
  }
  
  /**
   * Given an email message create a notification message.
   *
   * This method attempts to resolve the to_group and to fields of the notification message
   * but will NOT throw an exception if nothing can be resolved.  The caller may wish to check these properties
   * or override them.
   *
   * @param Email $eml
   *
   * @return notification_message
   */
  protected function create_notification_message_from_email(Email $eml) : notification_message
  {
    $resolve_group = function(Email $eml){
      if(isset($eml->to_admin_groups[0]))
      {
        $first = $eml->to_admin_groups[0];
        if(is_string($first) && (int)$first == 0)
        {
          return (int)xt_userops::get_groupid($first) * -1;
        }
        else if($first > 0)
        {
          return $first * -1;
        }
        throw new \LogicException('Could not resolve admin group ' . $first);
      }
      
      if(isset($eml->to_feu_groups[0]))
      {
        $first = $eml->to_feu_groups[0];
        if(is_string($first) && (int)$first == 0)
        {
          $feu = $this->GetModuleInstance('FrontEndUsers');
          if($feu)
          {
            $uid = $feu->GetUsername($first);
            if($uid > 0)
            {
              return $uid;
            }
          }
        }
        else if($first > 0)
        {
          return $first;
        }
      }
    };
    
    $resolve_user = function(Email $eml){
      $feu = $this->GetModuleInstance('FrontEndUsers');
      if($eml->to_current_admin)
      {
        if(($uid = get_userid(FALSE)))
        {
          return $uid * -1;
        }
      }
      else if($eml->to_current_feu)
      {
        if($feu && ($uid = $feu->LoggedInId()))
        {
          return $uid;
        }
      }
      else if($eml->to_feu_users)
      {
        if(($uid = $eml->to_feu_users[0]))
        {
          return $uid;
        }
      }
    };
    
    $proc          = $this->create_new_mailprocessor($eml);
    $msg           = $this->create_notification_message();
    $msg->html     = TRUE;
    $msg->subject  = $proc->get_email_subject($eml);
    $msg->body     = $proc->get_email_body($eml);
    $msg->to_group = $resolve_group($eml);
    if(!$msg->to_group)
    {
      $msg->to = $resolve_user($eml);
    }
    
    return $msg;
  }
  
  /**
   * A function to intelligently determine which template to use given parameters, a parameter name, and a type name.
   * This method will either return a template name (string) or null if nothing can be found.
   *
   * This method uses the LayoutTemplate API
   *
   * @param array  $params    Input module action parameters array.
   * @param strng  $paramname The param name that may hold a template name.
   * @param string $typename  The complete template TypeName i.e:  Uploads::Summary
   *
   * @return string|null
   * @deprecated
   */
  public function find_layout_template(array $params, string $paramname, string $typename)
  {
    trigger_error('Deprecated usage of ' . __METHOD__);
    $thetemplate = NULL;
    if(!is_array($params) || !($thetemplate = xt_param::get_string($params, $paramname)))
    {
      $tpl = CmsLayoutTemplate::load_dflt_by_type($typename);
      if(!is_object($tpl))
      {
        audit('', $this->GetName(), 'No default ' . $typename . ' template found');
        
        return;
      }
      $thetemplate = $tpl->get_name();
      unset($tpl);
    }
    
    return $thetemplate;
  }
  
  /**
   * Get the shared instance of the jsloader.
   *
   * Used by the xtjs_ plugins
   *
   * @param string $key a unique key.  If an instance by that key does not exist, it will be created.
   *
   * @return \CMSMSExt\jsloader\jsloader
   */
  public function get_jsloader(string $key = '_primary') : jsloader
  {
    $key = \trim($key);
    if(!$key) { $key = '_primary'; }
    
    static $_obj = [];
    if(!isset($_obj[$key]))
    {
      $_obj[$key] = new jsloader($key, $this->config);
    }
    
    return $_obj[$key];
  }
  
  /**
   * A function to get a handle to an alternate database connection that supports better error handling.
   *
   * @return Database
   * @throws \CMSMS\Database\ConnectionSpecException
   */
  public function get_extended_db() : Database
  {
    static $_obj;
    if(!$_obj)
    {
      $_error_handler = function(Database $conn, $errtype, $error_number, $error_message){
        throw new \xt_sql_error($error_message . ' -- ' . $conn->ErrorMsg(), $error_number);
      };
      $_obj           = db_compatibility::init( \cms_config::get_instance() );
      $_obj->SetErrorHandler($_error_handler);
    }
    
    return $_obj;
  }
  
  /**
   * Create a new cached file object.
   * This is a factory method
   *
   * @param string $url The URL to cache
   */
  public function cache_file(string $url) : xt_cached_remote_file
  {
    return new xt_cached_remote_file($url);
  }
  
  /**
   * @ignore
   */
  protected function get_internals() : internals
  {
    static $_obj;
    if(!$_obj)
    {
      $_obj = new internals($this->get_extended_db());
    }
    
    return $_obj;
  }
  
  /**
   * @ignore
   */
  public function get_content_list_builder() : content_list_builder
  {
    static $_obj;
    if(!$_obj)
    {
      $_obj = new content_list_builder(
        $this->cms->GetHierarchyManager(),
        $this->cms->GetContentOperations()
      );
    }
    
    return $_obj;
  }
  
  /**
   * A method to get a translator object for this module.
   *
   * @return ITranslator
   */
  protected function get_mod_translator() : ITranslator
  {
    static $_obj;
    if(!$_obj)
    {
      $_obj = new Translator($this);
    }
    
    return $_obj;
  }
  
  public function ModProcessTemplate($tpl_name)
  {
    $ok = (FALSE === \strpos($tpl_name, '..'));
    if(!$ok) {  return ''; }
    
    $smarty          = \cmsms()->GetSmarty();
    $result          = '';
    $oldcache        = $smarty->caching;
    $smarty->caching = $this->can_cache_output() ? \Smarty::CACHING_LIFETIME_CURRENT : \Smarty::CACHING_OFF;
    $result          = $smarty->fetch('xt_modfile:' . $this->GetName() . ';' . $tpl_name);
    $smarty->caching = $oldcache;
    
    return $result;
  }
  
  public function Lang()
  {
    //Push module name onto front of array
    $args = \func_get_args();
    \array_unshift($args, '');
    $args[0] = $this->GetName();
    
    $ret = CmsLangOperations::lang_from_realm($args);
    
    if( \startswith($ret, '-- Missing Languagestring: ') )
    {
      $args[0] = 'CMSMSExt';
      $ret = CmsLangOperations::lang_from_realm($args);
    }
  
    return $ret;
  }
  
  /**
   * @ignore
   */
  public function get_tasks()
  {
    $out   = NULL;
    $out[] = new HourlyHookTask();
    $out[] = new DailyHookTask();
    
    return $out;
  }
  

}

?>