<?php
/**
 * YANA library
 *
 * Primary controller class
 *
 * Software:  Yana PHP-Framework
 * Version:   {VERSION} - {DATE}
 * License:   GNU GPL  http://www.gnu.org/licenses/
 *
 * This program: can be redistributed and/or modified under the
 * terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see http://www.gnu.org/licenses/.
 *
 * This notice MAY NOT be removed.
 *
 * @package  yana
 * @license  http://www.gnu.org/licenses/gpl.txt
 */

/**
 * <<Facade>> <<Singleton>> Yana
 *
 * This is a primary controller and application loader for the Yana Framework.
 * It implements the "facade" pattern and thus delegates calls to underlying classes and methods.
 *
 * Example:
 * <code>
 * // get the current instance
 * global $YANA;
 * // handle request
 * $YANA->callAction($_REQUEST['action']);
 * // output results
 * $YANA->outputResults();
 * </code>
 *
 * @access      public
 * @package     yana
 * @subpackage  core
 */
final class Yana extends Singleton implements IsReportable
{
    /**
     * This is a place-holder for the singleton's instance
     *
     * @access  private
     * @static
     * @var     Yana
     */
    private static $instance = null;

    /**
     * Name of system configuration file
     *
     * @access  private
     * @static
     * @var     array
     */
    private static $config = null;

    /**
     * Request vars send via method post
     *
     * Note that this setting may be manually overwritten.
     *
     * @access  private
     * @static
     * @var     array
     */
    private static $post = null;

    /**
     * Request vars send via method get
     *
     * Note that this setting may be manually overwritten.
     *
     * @access  private
     * @static
     * @var     array
     */
    private static $get = null;

    /**
     * Request vars
     *
     * Note that this setting may be manually overwritten.
     *
     * @access  private
     * @static
     * @var     array
     */
    private static $request = null;

    /**
     * profile id
     *
     * @access  private
     * @static
     * @var string
     */
    private static $id = null;

    /**
     * request uri
     *
     * @access  private
     * @static
     * @var     array
     */
    private static $requestUri = null;

    /**
     * safe-mode settings
     *
     * false = default-mode (use profile settings)
       true  = safe-mode    (use default profile)
     *
     * @access  protected
     * @var     bool
     * @ignore
     */
    protected $isSafeMode = null;

    /**#@+
     * This variable is <<readonly>>
     * @access  public
     */

    /**
     * to communicate with plugins
     * @var PluginManager
     */
    public $plugins = null;

    /**
     * to load language strings
     * @var Language
     */
    public $language = null;

    /**
     * to load skins and templates
     * @var Skin
     */
    public $skin = null;

    /**
     * to read and write data to the global registry
     * @var Registry
     */
    public $registry = null;

    /**
     * to read and write user data and permissions
     * @var SessionManager
     */
    public $session = null;

    /**
     * the currently selected template
     * @var SmartView
     */
    public $view = null;

    /**#@-*/

    /**
     * get instance of this class
     *
     * Creates an instance if there is none.
     * Then it returns a reference to this (single) instance.
     *
     * Example:
     * <code>
     * Yana::setConfiguration("config/system.config");
     * Yana::setRequestVar($_REQUEST);
     * global $YANA;
     * </code>
     *
     * Note: you only need to call Yana::setConfiguration() and
     * Yana::setRequestVar() once, prior to the initialization
     * of the framework and even then you only need to do it
     * only if you wish to use other then the default values.
     * Otherwise it's enough to use Yana::getInstance() without
     * anything else.
     *
     * @access  public
     * @static
     * @return  Yana
     */
    public static function &getInstance()
    {
        if (!isset(self::$instance)) {
            /* auto-load configuration file */
            if (!isset(self::$config)) {
                self::setConfiguration();
            }
            /* auto-load request vars */
            if (!isset(self::$request)) {
                self::setRequestVar($_REQUEST);
            }
            self::$instance = new Yana();
        }
        return self::$instance;
    }

    /**
     * <<Singleton>> Constructor
     *
     * This function creates a new instance of the framework.
     * Note that you may only operate one instance at a time.
     *
     * @uses    new Yana()
     * @access  private
     * @throws  NotReadableException    when Registry file is not readable
     * @throws  InvalidSyntaxException  when Registry file could not be read or contains invalid syntax
     */
    private function __construct()
    {
        // The pluginManager holds repositories for interfaces and implementations of plugins.
        $this->_getPlugins();

        // set action by request var
        $this->_checkAction();

        // Build configuration. These settings may be read later by using Yana::getVar().
        $this->_getRegistry(); // may throw NotReadableException, InvalidSyntaxException

        // Prepare template. This is an auxiliary class that provides access to output-specific functions.
        $this->_getView();

        // Create  translation-repository. This setting can be accessed via Yana::$language.
        $this->_getLanguage();

        // Create skin-repository. This setting can be accessed via Yana::$skin.
        $this->_getSkin();
    }

    /**
     * application is in safe-mode
     *
     * @access  protected
     * @return  bool
     * @ignore
     */
    protected function isSafemode()
    {
        if (!isset($this->isSafemode)) {
            $eventConfiguration = $this->_getPlugins()->getEventConfiguration($this->getAction());
            if ($eventConfiguration instanceof PluginConfigurationMethod) {
                $this->isSafeMode = ($eventConfiguration->getSafemode() === true);
            } else {
                $this->isSafeMode = !empty(self::$config['DEFAULT']['EVENT'][PluginAnnotation::SAFEMODE]);
            }
        }
        return $this->isSafeMode;
    }

    /**
     * get canonical request URI
     *
     * Returns a canonical request URI that is identical, no matter if the data was sent via GET,
     * or POST. You may use this function to calculate IDs or identify the page.
     *
     * The request URI is the URI which was given in order to access this page; for instance,
     * '/index.php'.
     *
     * Remember some properties of $_SERVER['REQUEST_URI']:
     * <ol>
     *   <li> it is not available from CLI.  </li>
     *   <li> it is only 'complete' if $_SERVER['REQUEST_METHOD'] is 'get', because ...  </li>
     *   <li> it does not include POST-vars!  </li>
     *   <li> even when the request method is 'post', there might also have been GET-vars sent.  </li>
     * </ol>
     *
     * Also REQUEST_URI is not listed in the default CGI environment variables.
     * This means, servers are allowed to omit it!
     * See the complete list here: http://hoohoo.ncsa.illinois.edu/cgi/env.html
     *
     * @access  public
     * @static
     * @return  string
     */
    public static function getRequestUri()
    {
        if (!isset(self::$requestUri)) {

            /*
             * Method: GET
             */
            if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_METHOD'] === 'GET') {
                self::$requestUri = str_replace('&', '&amp;', $_SERVER['REQUEST_URI']);

            /*
             * Method: POST
             *
             * arguments are NOT stored in request uri, but in $_POST array.
             */
            } else {
                // Got an uri for the current request? Then use it.
                if (isset($_SERVER['REQUEST_URI'])) {
                    self::$requestUri = $_SERVER['REQUEST_URI'];

                } elseif (isset($_SERVER['HTTP_HOST']) && isset($_SERVER['REQUEST_URI'])) {
                    self::$requestUri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

                } elseif (isset($_SERVER['SERVER_ADDR']) && isset($_SERVER['PHP_SELF'])) {
                    self::$requestUri = 'http://' . $_SERVER['SERVER_ADDR'] . $_SERVER['PHP_SELF'];

                } elseif (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) {
                    assert('!isset($argv) && !isset($website_url);');
                    $argv = $_SERVER['argv'];
                    self::$requestUri = 'php ' . print_r(array_shift($argv), true);
                    while (count($argv) > 0)
                    {
                        self::$requestUri .= ' "' . print_r(array_shift($argv), true) . '"';
                    }
                    unset($argv);

                } elseif (isset($_SERVER['PHP_SELF'])) {
                    self::$requestUri = $_SERVER['PHP_SELF'];

                } else {
                    $message = 'Unable to resolve website URL. ' .
                        'This feature will not be available on this server.';
                    trigger_error($message, E_USER_NOTICE);
                    self::$requestUri = "";

                    return self::$requestUri;
                }

                // append delimiter
                if (mb_strpos('?', self::$requestUri) !== false) {
                    self::$requestUri .= '&amp;';
                } else {
                    self::$requestUri .= '?';
                }

                // append posted vars
                foreach ($_POST as $a => $b)
                {
                    if (is_scalar($b)) {
                        self::$requestUri .= urlencode($a).'='.urlencode($b).'&amp;';
                    }
                }
            }
        }

        return self::$requestUri;
    }

    /**
     * activate CD-ROM settings
     *
     * Sets the configuration to CD-ROM settings.
     * Configuration is expected to be loaded prior to calling this function.
     *
     * @access  private
     * @static
     */
    private static function _activateCDApplication()
    {
        assert('isset(self::$config); // Configuration must be loaded first');
        if (!file_exists(YANA_CDROM_DIR)) {
            mkdir(YANA_CDROM_DIR);
            chmod(YANA_CDROM_DIR, 0777);
        }
        $configDir = self::$config['CONFIGDIR'];
        self::_setRealPaths(YANA_CDROM_DIR);
        if (!file_exists(self::$config['TEMPDIR'])) {
            mkdir(self::$config['TEMPDIR']);
            chmod(self::$config['TEMPDIR'], 0777);
        }
        if (!file_exists(self::$config['CONFIGDIR'])) {
            $configSrc = new Dir($configDir);
            $configSrc->copy(self::$config['CONFIGDIR'], true, 0777, true, null, '/^(?!\.blob$)/i', true);
            unset($configSrc);
        }
        unset($configDir);
    }

    /**
     * set directory references to real paths
     *
     * @access  private
     * @static
     * @param   string  $cwd  current working directory
     */
    private static function _setRealPaths($cwd)
    {
        $cwd .= '/';
        self::$config['TEMPDIR'] = $cwd . self::$config['TEMPDIR'];
        self::$config['CONFIGDIR'] = $cwd . self::$config['CONFIGDIR'];
        self::$config['CONFIGDRIVE'] = $cwd . self::$config['CONFIGDRIVE'];
        self::$config['PLUGINFILE'] = $cwd . self::$config['PLUGINFILE'];
    }

    /**
     * set up a system configuration file
     *
     * The system config file contains default- and startup-settings
     * to initialize this class.
     *
     * Example:
     * <code>
     * Yana::setConfiguration("config/system.config");
     * </code>
     *
     * @param   string  $filename  path to system.config
     * @access  public
     * @static
     */
    public static function setConfiguration($filename = null)
    {
        if ($filename === null) {
            $filename = dirname(__FILE__) . "/../config/system.config.xml";
        }
        assert('is_string($filename);   // Wrong type for argument 1. String expected');
        assert('is_file($filename);     // Invalid argument 1. Input is not a file.');
        assert('is_readable($filename); // Invalid argument 1. Configuration file is not readable.');
        // get System Config file
        self::$config = SXML::getFile($filename, CASE_UPPER);
        // load CD-ROM application settings on demand
        if (YANA_CDROM === true) {
            self::_activateCDApplication();
        } else {
            self::_setRealPaths(getcwd());
        }
    }

    /**
     * execute an action
     *
     * Resolves event and calls plugin(s), with the given arguments.
     *
     * Example:
     * <code>
     * // handle current action
     * $YANA->callAction();
     * // same as above
     * $YANA->callAction($_REQUEST['action'], $_REQUEST);
     * // handle user defined event 'test'
     * $myArgs = array('foo' => 'bar');
     * $success = $YANA->callAction('test', $myArgs);
     * if ($success) {
     *     print "Success!\n";
     * } else {
     *     print "Encountered an error.\n";
     * }
     * </code>
     *
     * @access  public
     * @param   string  $action  script action parameter
     * @param   array   $args    array of passed arguments
     * @return  bool
     */
    public function callAction($action = "", array $args = null)
    {
        if (!is_string($action)) {
            trigger_error(sprintf(YANA_ERROR_WRONG_ARGUMENT, 1, 'String', gettype($action)), E_USER_WARNING);
            return false;
        }

        /**
         * 2) check for default arguments
         */
        if (empty($action)) {
            $action = $this->getAction();
        }
        if (is_null($args)) {
            $args = self::$request;
        }

        /**
         * 3) create session manager instance
         *
         * The SessionManager class is used to manage user information
         * and resolve permissions.
         */
        if (!isset($this->session)) {
            $this->session = SessionManager::getInstance();
        }

        /**
         * 4) load language strings
         */
        assert('!isset($eventConfiguration); // Cannot redeclare var $eventConfiguration');
        assert('!isset($plugins); // Cannot redeclare var $plugins');
        $plugins = $this->_getPlugins();
        $eventConfiguration = $plugins->getEventConfiguration($action);
        if (!($eventConfiguration instanceof PluginConfigurationMethod)) {
            Error::report("invalid_action", E_USER_ERROR, array('ACTION' => $action));
            return false;
        }

        assert('!isset($language); // Cannot redeclare var $language');
        $language = $this->_getLanguage();
        // mount language directory, if it exists
        assert('!isset($langDir); // Cannot redeclare var $langDir');
        foreach ($eventConfiguration->getPaths() as $langDir)
        {
            $langDir = $langDir . "/languages/";
            if (is_dir($langDir)) {
                $language->addDirectory($langDir);
            }
        }
        unset($langDir);
        // load language files
        assert('!isset($languageId); // Cannot redeclare var $languageId');
        foreach ($eventConfiguration->getLanguages() as $languageId)
        {
            $language->readFile($languageId);
        }
        unset($languageId);
        SmartView::addStyles($eventConfiguration->getStyles());
        SmartView::addScripts($eventConfiguration->getScripts());
        assert('!isset($template); // Cannot redeclare var $template');
        $template = $eventConfiguration->getTemplate();
        if (is_file($template)) {
            $this->skin->setFile($action, $template);
            $this->view->setTemplate($action);
        }
        unset($template, $eventConfiguration);

        /**
         * 5) handle event
         *
         * Returns bool(true) on success and bool(false) otherwise.
         */
        try {

            $result = $plugins->broadcastEvent($action, $args);

        } catch (Exception $e) {
            $message = get_class($e) . ': ' . $e->getMessage() . ' Thrown in ' . $e->getFile() .
                ' on line ' . $e->getLine();
            trigger_error($message, $e->getCode());
            return false;
        }
        if ($result !== false) {
            /* Create timestamp to provide information for read-stability isolation level */
            $_SESSION['transaction_isolation_created'] = time();
            return true;
        } else {
            return false;
        }
    }

    /**
     * get a value from the request vars
     *
     * Returns the value identified by $key from an untainted, merged copy of the $_POST
     * and $_GET arrays. (Or the input arguments, if the framework is run via a command
     * line interface)
     *
     * In this case "untainted" means, the result is either a string with at most
     * 50000 characters, or an array of such strings, or the constant NULL if the var
     * does not exist.
     *
     * If an input string contains a template token, or a '$' character, they are
     * automatically escaped to HTML entities. This is done to reduce the risk of code
     * injection.
     *
     * Note that this version is case-insensitive. This means "var", "Var" and "VAR"
     * all refer to the same input.
     *
     * If you call this function without the $key parameter or you use the wildcard '*'
     * the whole "request" array is returned.
     *
     * The argument $method was added in version 2.9.5.
     * Use this argument to choose wether you want to get vars sent via:
     * <ul>
     *  <li> YANA_POST = post method </li>
     *  <li> YANA_GET = get method </li>
     *  <li> YANA_REQUEST = both </li>
     * </ul>
     *
     * Important note: as a feature this function always returns unquoted
     * input, regardless if "magic quotes" is turned on or off. However this does leave
     * it to you to ensure for yourself that input is quoted where needed.
     * If you do not want this feature to be used, this behaviour can be turned off by
     * setting the constant YANA_AUTODEQUOTE to false.
     *
     * @uses    Yana::getRequestVar('CONTAINER.DATA')
     * @access  public
     * @static
     * @param   string  $key     adress of data in memory (case insensitive)
     * @param   int     $method  YANA_REQUEST, YANA_POST, YANA_GET
     * @return  mixed
     * @name    Yana::getRequestVar()
     */
    public static function getRequestVar($key = '*', $method = YANA_REQUEST)
    {
        assert('is_string($key); // Wrong argument type for argument 1. String expected.');

        /**
         * 1) switch by method
         */
        switch ($method)
        {
            case YANA_REQUEST:
                $property =& self::$request;
                if (is_null($property)) {
                    if (!Yana::setRequestVar($_REQUEST, $method)) {
                        return null;
                    }
                }
            break;
            case YANA_POST:
                $property =& self::$post;
                if (is_null($property)) {
                    if (!Yana::setRequestVar($_POST, $method)) {
                        return null;
                    }
                }
            break;
            case YANA_GET:
                $property =& self::$get;
                if (is_null($property)) {
                    if (!Yana::setRequestVar($_GET, $method)) {
                        return null;
                    }
                }
            break;
            default:
                trigger_error("Argument 2 is invalid in " . __METHOD__ . "().\n\t\t" .
                "Exected one of the following constants: YANA_REQUEST, YANA_POST, YANA_GET.", E_USER_WARNING);
                return null;
            break;
        }

        /**
         * 2) return result
         */

        // if key is not set, return whole array (default)
        if ($key === '*') {
            return $property;
        }

        // if key is set, check which index to return
        $key = mb_strtolower("$key");
        if (isset($property[$key])) {
            return $property[$key];
        } else {
            return Hashtable::get($property, $key);
        }
    }

    /**
     * set request vars
     *
     * This function sets the vars given with the current request.
     *
     * Examples:
     *
     * If you want to use a web interface:
     * <code>
     * Yana::setRequestVar($_REQUEST);
     * </code>
     *
     * If you want to use a command line interface:
     * <code>
     * Yana::setRequestVar($_SERVER['argv]);
     * </code>
     *
     * The argument $method chooses wether you want to set vars sent via:
     * <ul>
     *  <li> YANA_POST = post method </li>
     *  <li> YANA_GET = get method </li>
     *  <li> YANA_REQUEST = both </li>
     * </ul>
     * The default is YANA_REQUEST
     *
     * @access  public
     * @static
     * @param   array   $value   request vars
     * @param   int     $method  YANA_REQUEST, YANA_POST, YANA_GET
     * @return  bool
     * @name    Yana::setRequestVar()
     * @see     Yana::getRequestVar()
     */
    public static function setRequestVar(array $value, $method = YANA_REQUEST)
    {
        /**
         * 1) untaint value
         */
        $value = Hashtable::changeCase($value, CASE_LOWER);
        Yana::_untaintRequestVar($value, YANA_AUTODEQUOTE && get_magic_quotes_gpc());

        /**
         * 2) switch by method
         */
        switch ($method)
        {
            case YANA_REQUEST:
                self::$request = $value;
                return true;
            break;
            case YANA_POST:
                self::$post = $value;
                return true;
            break;
            case YANA_GET:
                self::$get = $value;
                return true;
            break;
            default:
                trigger_error("Argument 2 is invalid in " . __METHOD__ . "().\n\t\t" .
                "Exected one of the following constants: YANA_REQUEST, YANA_POST, YANA_GET.", E_USER_WARNING);
                return false;
            break;
        }
    }

    /**
     * check current action for validity
     *
     * @internal Work-around for IE-bug.
     *
     * Example:
     * <code>
     * <form><button type="submit" name="a" value="1">2</button></form>
     * </code>
     *
     * IE sends a=2 instead of a=1. This is because IE automatically
     * handles button-tags as input tags and copies the caption text
     * to the value attribute. This is WRONG according to W3C.
     *
     * Solution:
     * <code>
     * <form><input type="submit" name="a[1]" value="2"/></form>
     * if ($a[1]) $a = 1;
     * </code>
     *
     * @access  private
     */
    private function _checkAction()
    {
        // work-around for IE-bug
        if (isset(self::$request['action']) && is_array(self::$request['action'])) {
            if (count(self::$request['action']) === 1) {
                // action[name]=1 -> action=name
                reset(self::$request['action']); // rewind iterator
                self::$request['action'] = key(self::$request['action']); // get first key
            }
        }
        // error checking
        switch (true)
        {
            case isset(self::$request['action']) && !is_string(self::$request['action']):
            case isset(self::$request['action']) && !$this->_getPlugins()->isEvent(self::$request['action']):
                $this->_getLanguage();
                Alert::report('invalid_action', E_USER_NOTICE, array('ACTION' => self::$request['action']));
            // fall through
            case empty(self::$request['action']):
                assert('!empty(self::$config["DEFAULT"]["HOMEPAGE"]); // Configuration missing default homepage.');
                self::$request['action'] = self::$config['DEFAULT']['HOMEPAGE'];
            // fall through
            default:
                self::$request['action'] = mb_strtolower(self::$request['action']);
            break;
        }
    }

    /**
     * get current action
     *
     * @access  protected
     * @return  string
     * @ignore
     */
    protected function getAction()
    {
        return self::$request['action'];
    }

    /**
     * get registry
     *
     * This returns the registry. If none exists, a new instance is created.
     *
     * @access  private
     * @return  Registry
     * @throws  NotReadableException    when Registry file is not readable
     * @throws  InvalidSyntaxException  when Registry file could not be read or contains invalid syntax
     */
    private function _getRegistry()
    {
        if (!isset($this->registry)) {
            // path to cache file
            $cacheFile = self::$config['TEMPDIR'] . 'registry_' . self::getId() . '.tmp';

            // get configuration mode
            Registry::useDefaults($this->isSafemode());

            if (YANA_CACHE_ACTIVE === true && file_exists($cacheFile)) {
                $this->registry = Registry::unserialize(file_get_contents($cacheFile));
            } else {
                $this->registry = new Registry(self::$config['CONFIGDRIVE'], "");
                $this->registry->setVar("ID", self::getId());
                $this->registry->mergeVars('*', self::$config);
            }
            $this->registry->mergeVars('*', self::$request);
            $this->registry->setAsGlobal();

            // set user name
            if (!empty($_SESSION['user_name'])) {
                $this->registry->setVar("SESSION_USER_ID", $_SESSION['user_name']);
            }

            // set CD-ROM temp-dir
            if (YANA_CDROM === true) {
                $this->registry->setVar('YANA_CDROM_DIR', YANA_CDROM_DIR);
            }

            // create cache file
            if (YANA_CACHE_ACTIVE === true && !file_exists($cacheFile)) {
                file_put_contents($cacheFile, $this->registry->serialize());

            } else {
                $this->registry->read();
            }

            if (!empty(self::$request['page'])) {
                $this->registry->setVar('PAGE', (int) self::$request['page']);
            }
            if (!empty(self::$request['target'])) {
                $this->registry->setVar('TARGET', (string) self::$request['target']);
            }
            if (!empty($_SERVER['REMOTE_ADDR'])) {
                $this->registry->setVar('REMOTE_ADDR', $_SERVER['REMOTE_ADDR']);
            } else {
                $this->registry->setVar('REMOTE_ADDR', '0.0.0.0');
            }
            if (!empty($_SERVER['HTTP_REFERER'])) {
                $referer = preg_replace("/(.*\/).*(\?.*)?/", "\\1", $_SERVER['HTTP_REFERER']);
                $this->registry->setVar("REFERER", $referer);
            }
        }
        return $this->registry;
    }

    /**
     * get plugin-manager
     *
     * This returns the plugin manager. If none exists, a new instance is created.
     *
     * @access  private
     * @return  PluginManager
     */
    private function _getPlugins()
    {
        if (!isset($this->plugins)) {
            $cacheFile = self::$config['PLUGINCACHE'];
            PluginManager::setPath(self::$config['PLUGINFILE'], self::$config['PLUGINDIR']);

            if (YANA_CACHE_ACTIVE === true && file_exists($cacheFile)) {
                $this->plugins = PluginManager::unserialize(file_get_contents($cacheFile));

            } else {
                $this->plugins = PluginManager::getInstance();
                file_put_contents($cacheFile, $this->plugins->serialize());
            }
        }
        return $this->plugins;
    }

    /**
     * get view
     *
     * This returns the view component. If none exists, a new instance is created.
     *
     * @access  private
     * @return  SmartView
     */
    private function _getView()
    {
        if (!isset($this->view)) {
            $this->view = new SmartView();
            $this->setVar("ACTION", $this->getAction());
        }
        return $this->view;
    }

    /**
     * get language
     *
     * This returns the language component. If none exists, a new instance is created.
     *
     * @access  private
     * @return  Language
     */
    private function _getLanguage()
    {
        if (!isset($this->language)) {
            $languageDir = $this->getVar('LANGUAGEDIR');
            $this->language = Language::getInstance();
            $this->language->addDirectory($languageDir);
            $this->language->setLocale(self::$config['DEFAULT']['LANGUAGE']);
            if (isset($_SESSION['language'])) {
                try {
                    $this->language->setLocale($_SESSION['language']);
                } catch (InvalidArgumentException $e){
                    unset($_SESSION['language']);
                }
            }
            $this->language->readFile('default');
            $array = array();
            foreach (glob("$languageDir*", GLOB_ONLYDIR) as $dir)
            {
                $array[basename($dir)] = 1;
            }
            $this->setVar('INSTALLED_LANGUAGES', $array);
            if (isset($_SESSION['language'])) {
                $this->setVar('SELECTED_LANGUAGE', $_SESSION['language']);
            }
        }
        return $this->language;
    }

    /**
     * get skin
     *
     * This returns the skin component. If none exists, a new instance is created.
     *
     * @access  private
     * @return  Skin
     */
    private function _getSkin()
    {
        if (!isset($this->skin)) {
            $registry = $this->_getRegistry();
            $registry->mount('system:/skincache.textfile');
            $cacheFile = $registry->getResource('system:/skincache.textfile');

            Skin::setBaseDirectory($registry->getResource('system:/skin'));

            if (YANA_CACHE_ACTIVE === true && $cacheFile->exists()) {
                assert('!isset($skin); // Cannot redeclare var $skin');
                $this->skin = Skin::unserialize(file_get_contents($cacheFile->getPath()));

            } else {
                $this->skin = Skin::getInstance($this->getVar('PROFILE.SKIN'));
                $this->skin->selectMainSkin();

                if (YANA_CACHE_ACTIVE === true) {
                    $cacheFile->create();
                    $cacheFile->setContent($this->skin->serialize());
                    $cacheFile->write();
                }
            }
        }
        return $this->skin;
    }

    /**
     * untaint request vars
     *
     * @access  private
     * @static
     * @param   array   &$value   request vars
     * @param   bool    $unquote  true: strip slashes,
     *                            false: leave slashes alone
     * @name    Yana::_untaintRequestVar()
     * @see     Yana::setRequestVar()
     */
    private static function _untaintRequestVar(array &$value, $unquote = false)
    {
        foreach ($value as $i => $item)
        {
            if (is_array($item)) {
                Yana::_untaintRequestVar($value[$i], $unquote);
            } else {
                if ($unquote === true) {
                    $item = stripcslashes($item);
                }
                $value[$i] = untaintInput($item, "string", 50000, YANA_ESCAPE_TOKEN, true);
            }
        }
    }

    /**
     * get current profile id
     *
     * Returns the id of the profile the data of the current profile is to be associated with.
     *
     * This is a shortcut for $YANA->getVar('ID').
     *
     * However it is important to note a slight difference.
     * <ul>
     *   <li> $YANA->getVar('ID'):
     *     The value you get via  is
     *     available to all plugins and all plugins may read
     *     AND write this setting as the developer sees fit.
     *     This may mean, that this setting has been subject
     *     to changes by some plugin, e.g. to switch between
     *     profiles.
     *   </li>
     *   <li> Yana::getId():
     *     Always returns the original value, regardless of
     *     changes by plugins.
     *   </li>
     * </ul>
     *
     * You may want to decide for the behaviour you prefer
     * and choose either one or the other.
     *
     * @access  public
     * @static
     * @return  string
     */
    public static function getId()
    {
        if (!isset(self::$id)) {
            if (!empty(self::$request['id'])) {
                self::$id = mb_strtolower(self::$request['id']);
            } elseif (!empty(self::$config['DEFAULT']['PROFILE'])) {
                self::$id = self::$config['DEFAULT']['PROFILE'];
            } elseif (!empty($_REQUEST['id'])) {
                self::$id = mb_strtolower($_REQUEST['id']);
            } else {
                self::$id = 'default';
            }
        }
        return self::$id;
    }

    /**
     * get value from registry
     *
     * Returns var from registry (memory shared by all plugins)
     *
     * Example:
     * <code>
     * $YANA->setVar('foo.bar', 'Hello World');
     * // outputs 'Hello World'
     * print $YANA->getVar('foo.bar');
     * </code>
     *
     * @uses    $YANA->getVar('CONTAINER1.CONTAINER2.DATA')
     * @access  public
     * @param   string  $key  adress of data in memory (case insensitive)
     * @return  mixed
     * @name    Yana::getVar()
     * @see     Yana::setVarByReference()
     * @see     Yana::setVar()
     */
    public function getVar($key = '*')
    {
        assert('is_scalar($key); /* Wrong argument type for argument 1. String expected. */');
        $registry = $this->_getRegistry();
        return $registry->getVar("$key");
    }

    /**
     * sets var on registry by Reference
     *
     * The "registry" is memory shared by all plugins.
     *
     * Example:
     * <code>
     * $bar = 'Hello';
     * $YANA->setVarByReference('foo.bar', $bar);
     * $bar .= ' World';
     * // outputs 'Hello World'
     * print $YANA->getVar('foo.bar');
     * </code>
     *
     * @uses    $YANA->setVar('CONTAINER1.CONTAINER2.DATA', $value)
     * @access  public
     * @param   string  $key     adress of data in memory (case insensitive)
     * @param   mixed   &$value  new value (may be scalar value or array)
     * @return  bool
     * @name    Yana::setVarByReference()
     * @see     Yana::setVar()
     * @see     Yana::getVar()
     */
    public function setVarByReference($key, &$value)
    {
        assert('is_scalar($key); /* Wrong argument type for argument 1. String expected. */');
        /* settype to STRING */
        $key = (string) $key;
        $registry = $this->_getRegistry();
        return $registry->setVarByReference($key, $value);
    }

    /**
     * sets var on registry
     *
     * The "registry" is memory shared by all plugins.
     *
     * Example:
     * <code>
     * $YANA->setVar('foo.bar', 'Hello World');
     * // outputs 'Hello World'
     * print $YANA->getVar('foo.bar');
     * </code>
     *
     * @uses    $YANA->setVar('CONTAINER1.CONTAINER2.DATA', $value)
     * @access  public
     * @param   string  $key    adress of data in memory (case insensitive)
     * @param   mixed   $value  new value (may be scalar value or array)
     * @return  bool
     * @name    Yana::setVar()
     * @see     Yana::setVarByReference()
     * @see     Yana::getVar()
     */
    public function setVar($key, $value)
    {
        return $this->setVarByReference($key, $value);
    }

    /**
     * sets the type of a var on registry (memory shared by all plugins)
     *
     * @uses    $YANA->setType('CONTAINER1.CONTAINER2.DATA', 'string')
     * @access  public
     * @param   string  $key   adress of data in memory (case insensitive)
     * @param   string  $type  new type of variable
     * @return  bool
     */
    public function setType($key, $type)
    {
        assert('is_scalar($key);  /* Wrong argument type for argument 1. String expected. */');
        assert('is_string($type); /* Wrong argument type for argument 2. String expected. */');
        /* settype to STRING */
        $key  = (string) $key;
        $type = (string) $type;
        $registry = $this->_getRegistry();
        return $registry->setType($key, $type);
    }

    /**
     * remove var from registry
     *
     * Removes var from registry (memory shared by all plugins).
     * Returns bool(true) on success and bool(false) on error.
     *
     * Example:
     * <code>
     * // set foo.bar
     * $YANA->setVar('foo.bar', 'Hello World');
     * // remove foo.bar
     * $YANA->unsetVar('foo.bar');
     * // foo.bar now is false
     * var_dump($YANA->getVar('foo.bar'));
     * </code>
     *
     * @uses    $YANA->unsetVar('CONTAINER1.CONTAINER2.DATA')
     * @access  public
     * @param   string  $key  adress of data in memory (case insensitive)
     * @return  bool
     */
    public function unsetVar($key)
    {
        assert('is_scalar($key); /* Wrong argument type for argument 1. String expected. */');
        /* settype to STRING */
        $key = (string) $key;
        $registry = $this->_getRegistry();
        return $registry->unsetVar($key);
    }

    /**
     * merges value in registry
     *
     * Merges the value at adresse $key with the provided array data.
     * If a key already exists, it is replaced by the new data.
     *
     * Example:
     * <code>
     * $bar = array('foo' => 'bar');
     * $YANA->mergeVars('FOO', $bar);
     * // outputs 'bar'
     * print $YANA->getVar('FOO.FOO');
     * </code>
     *
     * @uses    $YANA->mergeVars('CONTAINER1.CONTAINER2.DATA', $array)
     * @access  public
     * @param   string  $key    adress of data in memory (case insensitive)
     * @param   array   $array  associative array to merge
     * @return  bool
     */
    public function mergeVars($key, array $array)
    {
        assert('is_scalar($key);  /* Wrong argument type for argument 1. String expected. */');
        $registry = $this->_getRegistry();
        return $registry->mergeVars("$key", $array);
    }

    /**
     * get a resource
     *
     * This function takes a virtual file or directory path and returns the resource.
     * The requested resource must be defined within the virtual drive.
     * See the manual for more details on the proper use of virtual drives.
     *
     * If the mountpoint for the requested resource does not exist, or doesn't return any results,
     * the function returns bool(false) instead and issues a warning.
     *
     * @access  public
     * @param   string  $path  virtual file path
     * @return  FileSystemResource
     */
    public function getResource($path)
    {
        return $this->_getRegistry()->getResource($path);
    }

    /**
     * exit the current script
     *
     * This will flush error messages and warnings to the screen,
     * write all reported errors (if any) to the framework's logs
     * and then exit the current script.
     * After that it will call itself again to handle the event
     * provided by the argument $event.
     *
     * You may use the special event 'null' to prevent the
     * framework from handling an event. In this case it will just
     * exit.
     *
     * If the argument $event is not provided, the default event
     * will be used instead.
     *
     * Examples:
     * <code>
     * global $YANA;
     *
     * // print an error and go to start page
     * Error::report('404');
     * $YANA->exitTo();
     *
     * // same as:
     * $YANA->exitTo('');
     *
     * // Use special event 'null' if you just want to
     * // view the error message and exit the script
     * // without handling another event.
     * // ( You may translate this to: "exit to 'nowhere'" )
     * Error::report('500');
     * $YANA->exitTo('null');
     *
     * // output message and route to 'login' page
     * Error::report('access denied');
     * $YANA->exitTo('login');
     * </code>
     *
     * Please note: any code followed after a call to this function
     * will never be executed.
     *
     * @access  public
     * @param   string  $event  upcoming event to route to
     * @since   2.9.0 RC2
     */
    public function exitTo($event = 'null')
    {
        assert('is_string($event); // Wrong argument type argument 1. String expected');
        $event = mb_strtolower("$event");

        /*
         * save log-files (if any)
         *
         * By default this will output any messages to a table of the database named 'log'.
         */
        $this->_writeLog();
        $level = $this->_prepareMessages();
        $view = $this->_getView();

        /*
         * is an AJAX request
         */
        if (!empty(self::$request['is_ajax_request'])) {
            $view->showMessage($level, '', 'null', 'STDOUT');
            exit(0);
        }

        /*
         * output a message and DO NOT RELOCATE, when
         *   1) headers are already sent, OR
         *   2) the template explicitely requests a message, OR
         *   3) the special 'NULL-event' (no event) is requested.
         */
        if ($event === 'null' || self::getDefault('MESSAGE') === true || headers_sent() === true) {

            $view->showMessage($level, '', $event);
            exit(0);
        }

        /*
         * save message and relocate.
         */
        $stdout = $this->getVar('STDOUT');
        if (!is_array($stdout)) {
            $stdout = array();
        }
        $_SESSION['STDOUT'] = $stdout;

        header("Location: ".SmartUtility::url("action=$event", true));
        exit(0);
    }

    /**
     * provides GUI from current data
     *
     * @access  public
     */
    public function outputResults()
    {
        /* 0 initialize vars */
        $event = PluginManager::getFirstEvent();
        $result = PluginManager::getLastResult();
        $eventConfiguration = $this->_getPlugins()->getEventConfiguration($event);
        if ($eventConfiguration instanceof PluginConfigurationMethod) {
            $template = $eventConfiguration->getTemplate();

        } else {
            // error - unable to continue
            return;
        }
        unset($eventConfiguration);

        /**
         * resolve selected templates
         */

        /* 1) the reserved template 'NULL' is an alias
         * for 'no template' and will surpress any output.
         *
         * This may mean the plugin will create some output
         * itself using print(), or it is a triggered
         * cron-job that is not meant to produce any output
         * at all.
         */
        if (strcasecmp($template, 'NULL') === 0) {

            if (!empty(self::$request['is_ajax_request']) && !is_bool($result)) {
                if (!headers_sent()) {
                    header('Content-Type: text/plain');
                    header('Content-Encoding: UTF-8');
                    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
                    header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
                    header("Cache-Control: no-store, no-cache, must-revalidate");
                    header("Cache-Control: post-check=0, pre-check=0", false);
                    header("Pragma: no-cache");
                }
                if (!is_null($result)) {
                    print json_encode($result); // print results
                }
            }
            // don't produce any output;
            return;
        }

        /* 2) the reserved template 'MESSAGE' is a special
         * template that produces a text message.
         *
         * The text usually is an ID of some text.
         * The actual message is stored in the language files
         * and the translated message will be read from there
         * depending on the user's prefered language setting.
         */
        if (strcasecmp($template, 'MESSAGE') === 0 || ($result === false && Report::countMessages() === 0)) {

            // get vars
            $route = PluginManager::getNextEvent();

            if (!empty($route[PluginAnnotation::TEMPLATE]) && class_exists($route[PluginAnnotation::TEMPLATE])) {
                $messageClass = $route[PluginAnnotation::TEMPLATE];
            } else {
                if ($result !== false) {
                    $messageClass = 'message';
                } else {
                    $messageClass = 'error';
                }
            }
            if (empty($route[PluginAnnotation::TEXT])) {
                if ($result !== false) {
                    $route[PluginAnnotation::TEXT] = '200';
                } else {
                    $route[PluginAnnotation::TEXT] = '500';
                }
            }

            // create default message if there is none
            if (Message::countMessages() === 0) {
                new $messageClass($route[PluginAnnotation::TEXT]);
            }
            if (!is_array($route)) {
                $route = array();
            }

            // if not other destination is defined, route back to default homepage
            if (!isset($route[PluginAnnotation::GO])) {
                $route[PluginAnnotation::GO] = self::getDefault("homepage");
            }

            $this->exitTo($route[PluginAnnotation::GO]);

        /* 3) all other template settings go here
         */
        } else {

            $view = $this->_getView();
            if (!empty(self::$config['DEFAULT']['EVENT'][mb_strtoupper(PluginAnnotation::TEMPLATE)])) {
                $baseTemplate = self::$config['DEFAULT']['EVENT'][mb_strtoupper(PluginAnnotation::TEMPLATE)];
                $view->setPath($baseTemplate);
            }
            /* register templates with view sub-system */
            $view->setTemplate($template);
            /* there is a special var called 'STDOUT' that is used to output messages */
            if (!empty($_SESSION['STDOUT']['MESSAGES']) && is_array($_SESSION['STDOUT']['MESSAGES'])) {
                $this->setVar('STDOUT', $_SESSION['STDOUT']);
                unset($_SESSION['STDOUT']);
            }

            /* print message queue to client */
            $this->_prepareMessages();

            /* print the page to the client */
            echo $view->toString();

            /**
             * save log-files (if any)
             *
             * By default this will output any messages
             * to a table of the database named 'log'.
             */
            $this->_writeLog();
        }

    }

    /**
     * get default configuration value
     *
     * Returns the default value for a given var if any,
     * returns NULL (not false!) if there is none.
     *
     * Example 1:
     * <code>
     * Yana::getDefault('CONTAINER1.CONTAINER2.DATA');
     * </code>
     *
     * Example 2:
     * <code>
     * if (!isset($foo)) {
     *     $foo = Yana::getDefault('FOO');
     * }
     * </code>
     *
     * Note: system default values are typically defined in the
     * 'default' section of the 'config/system.config' configurations file.
     *
     * @access  public
     * @static
     * @param   string  $key  adress of data in memory (case insensitive)
     * @return  mixed
     */
    public static function getDefault($key)
    {
        assert('is_scalar($key); /* Wrong argument type for argument 1. String expected. */');
        if (!isset(self::$config['DEFAULT'])) {
            return null;
        }
        $key = mb_strtoupper("$key");
        if (isset(self::$config['DEFAULT'][$key])) {
            return self::$config['DEFAULT'][$key];
        } else {
            return Hashtable::get(self::$config['DEFAULT'], $key);
        }
    }

    /**
     * clear system cache
     *
     * Deletes all temporary files in the 'cache/' directory.
     *
     * This includes templates and preinitialized instances of system objects.
     * Use this function where system settings or profile systems are changed,
     * to make sure changes are applied without delay.
     *
     * @access  public
     * @static
     */
    public static function clearCache()
    {
        SmartTemplate::clearCache();
        PluginMenu::clearCache();
    }

    /**
     * <<factory>> connect()
     *
     * Returns a ready-to-use database connection.
     *
     * Example:
     * <code>
     * // Connect to database using 'config/db/user.config'
     * $db = Yana::connect('user');
     * </code>
     *
     * @access  public
     * @static
     * @param   string|DDLDatabase  $source  name of the database schema file (see config/db/*.xml),
     *                                       or instance of DDLDatabase
     * @return  DbStream
     */
    public static function connect($source)
    {
        if ($source instanceof DDLDatabase) {
            $schema = $source;
        } else {
            assert('is_string($source); // Wrong argument type for argument 1. String expected.');
            $schema = XDDL::getDatabase($source);
        }
        if (YANA_DATABASE_ACTIVE) {
            $db = new DbStream($schema);
        } else {
            $db = new FileDb($schema);
        }
        return $db;
    }

    /**
     * run diagnostics and get a system report
     *
     * This function runs full diagnosicts on all mounted sub-systems.
     *
     * Returns the a report object.
     *
     * Example:
     * <code>
     * <?xml version="1.0"?>
     * <report>
     *   <text>Base directory: foo/</text>
     *   <report>
     *     <title>bar.file</title>
     *     <text>Type: file</text>
     *     <text>Path: bar.txt</text>
     *     <error>Is not readable ...</error>
     *   </report>
     *   <report>
     *     <title>foo</title>
     *     <text>Type: dir</text>
     *     <text>Path: bar/foo/</text>
     *   </report>
     * </report>
     * </code>
     *
     * @access  public
     * @param   ReportXML  $report  base report
     * @return  ReportXML
     * @name    Yana::getReport()
     * @ignore
     */
    public function getReport(ReportXML $report = null)
    {
        if (is_null($report)) {
            $report = ReportXML::createReport(__CLASS__);
        }

        /**
         * 1) General system information
         */
        $report->addNotice("installed version of Yana Framework is: " . YANA_VERSION);
        $report->addNotice("installed version of PHP is: " . PHP_VERSION);
        $report->addNotice("current server time is: " . date("r", time()));
        $report->addNotice("running diagnostics on profile: " . self::getId());

        $subreport = $report->addReport("Testing installation");

        /**
         * 2) Check for availability of PEAR-DB
         */
        @include_once "MDB2.php";
        if (!class_exists("MDB2")) {
            $message = "PHP PEAR-MDB2 module not found. " .
                "Database plugins require PEAR-MDB2 and will not run unless you install it.";
            $subreport->addError($message);
        } else {
            $subreport->addText("PHP PEAR-MDB2 found (required to run database plugins)");
        }

        /**
         * 3) Check if primary controller is registered under the expected name
         */
        if (!isset($GLOBALS['YANA'])) {
            $message = "Unable to access Yana instance under global name 'YANA'. " .
                "The framework will not run properly. Please reinstall the application.";
            $subreport->addError($message);
        }

        /**
         * 4) Check availability of configuration file and configuration directory
         */
        if (YANA_CDROM === true) {
            if (!is_writeable(YANA_CDROM_DIR)) {
                $message = "Temporary directory " . YANA_CDROM_DIR . " is not writeable. " .
                    "Set access rights for directory '" .
                    YANA_CDROM_DIR . "' to 777, including all subdirectories and files.";
                $subreport->addError($message);
            }
        } else {
            if (!is_writeable($this->getVar('CONFIGDIR'))) {
                $message = "Configuration directory is not writeable. " .
                    "Set access rights for directory '" . $this->getVar('CONFIGDIR') .
                    "' to 777, including all subdirectories and files.";
                $subreport->addError($message);
            }
            if (!is_writeable($this->getVar('TEMPDIR'))) {
                $message = "Directory for temporary files is not writeable. " .
                    "Set access rights for directory '" . $this->getVar('TEMPDIR') .
                    "' to 777, including all subdirectories and files.";
                $subreport->addError($message);
            }
        }

        /**
         * 4) Add a list of MD5 checksums for several important files
         */
        $subreport = $report->addReport("System-integrity check");
        $message = "The following list contains the MD5 checksums of several important files. " .
            "Compare these with your own list to see, " .
            "if any of these files have recently been modified without your knowledge.";
        $subreport->addText($message);

        if (is_dir('manual')) {
            $message = "You do not need to copy the directory 'manual' to your website. " .
                "It is not required to run the program. You might want to remove it to safe space.";
            $subreport->addNotice($message);
        }

        $root = dirlist(".", '.php');
        assert('!isset($i); /* Cannot redeclare variable $i */');
        for ($i = 0; $i < count($root); $i++)
        {
            if ($root[$i] != 'index.php' && $root[$i] != 'library.php' && $root[$i] != 'cli.php') {
                if (mb_strpos($root[$i], 'update') !== false) {
                    $message = "Unexpected file '" . $root[$i] . "' found. " .
                        "If you did'nt place this file here, " .
                        "it might be the result of an hijacking attempt. " .
                        "You should consider removing this file.";
                    $subreport->addWarning($message);
                }
            } else {
                $subreport->addText("{$root[$i]} = " . md5_file($root[$i]));
            }
        } /* end for */
        unset($i);
        $root = dirlist("./includes/", '.php');
        assert('!isset($i); /* Cannot redeclare variable $i */');
        for ($i = 0; $i < count($root); $i++)
        {
            $subreport->addText("includes/{$root[$i]} = " . md5_file("includes/{$root[$i]}"));
        }
        unset($i);

        /**
         *  5) Add subreports
         */
        foreach ($this as $name => $member)
        {
            if (is_object($member) && $member instanceof IsReportable) {
                $subReport = $report->addReport("$name");
                $member->getReport($subReport);
            }
        }

        /**
         * 6) check icons
         */
        $subreport = $report->addReport("Check icons");
        SmartUtility::loadSmilies();
        $smilies = $this->getVar('SMILIES');
        assert('!isset($dir); // Cannot redeclare var $dir');
        $registry = $this->_getRegistry();
        $dir = $registry->getResource('system:/smile');
        if (count($smilies)==0) {
            $message = "No Icons found. Please check if the given directory is correct: '" .
                $dir->getPath() . "'.";
            $subreport->addWarning($message);
        } else {
            $subreport->addText(count($smilies) . " Icons found in directory '" . $dir->getPath() . "'.");
            $subreport->addText("No problems found: Directory setting seems to be correct.");
        }
        unset($dir);

        return $report;
    }

    /**
     * iterate through message queue
     *
     * @access  private
     * @param   bool   $isError  true: is error message,
     *                           false: is not an error message,
     *                           null: don't know
     * @return  string
     * @ignore
     */
    private function _prepareMessages($isError = null)
    {
        $messageClass = "";
        $stdout = array();

        assert('!isset($messages); // Cannot redeclare variable $messages');
        if (defined('YANA_ERROR_REPORTING') && YANA_ERROR_REPORTING === YANA_ERROR_ON) {
            $messages = Report::getMessages();
        } else {
            $messages = Message::getMessages();
        }
        assert('is_array($messages); // unexpected result: List of messages is not an array');

        if (empty($messages)) {
            return "";
        }

        // event logging
        assert('!isset($message); // Cannot redeclare variable $message');
        foreach ($messages as $message)
        {
            // get message text and header
            assert('$message instanceof Report; // unexpected result: Entry is not a subclass of Report');
            if ($message instanceof Message) {
                assert('!isset($messageHeader); // Cannot redeclare variable $messageHeader');
                $messageHeader = $message->getMessageHeader();
                if (!empty($messageHeader)) {
                    $messageInfo = array(
                        'h' => $messageHeader,
                        'p' => $message->getMessageText()
                    );
                } else {
                    $messageInfo = $message->getMessageText();
                }
                unset($messageHeader);

            } else {
                $messageInfo = $message->getMessage();
            } // end if (instanceof Message)

            // checks whether type of report is known and pushes message into output-queue
            assert('is_array($stdout); // unexpected result: List of outputs is not an array');
            switch ($message->getCode())
            {
                case E_NOTICE:
                case E_USER_NOTICE:
                    if ($isError !== true) {
                        $isError = false;
                        $messageClass = 'message';
                    } else {
                        $messageClass = 'alert';
                    }
                    $stdout[] = $messageInfo;
                break;
                case E_WARNING:
                case E_USER_WARNING:
                    $stdout[] = $messageInfo;
                    $messageClass = 'warning';
                break;
                case E_ERROR:
                case E_USER_ERROR:
                    $isError = true;
                    $messageClass = 'error';
                    $stdout[] = $messageInfo;
                break;
                default:
                    if ($isError !== false) {
                        $messageClass = 'error';
                    } else {
                        $messageClass = 'alert';
                    }
                    $stdout[] = $messageInfo;
                break;
            } // end switch (code)
        } // end foreach (message)
        unset($message);
        $this->setVar('STDOUT.MESSAGES', $stdout);
        $this->setVar('STDOUT.LEVEL', $messageClass);
        return $messageClass;
    }

    /**
     * flush events to log
     *
     * @access  private
     * @ignore
     */
    private function _writeLog()
    {
        // skip if logging is turned off
        if (!$this->getVar('PROFILE.LOGGING')) {
            return;
        }

        // get message-queue
        $messages = Log::getMessages();
        assert('is_array($messages); // unexpected result: List of messages is not an array');

        // skip if message-queue is empty
        if (empty($messages)) {
            return;
        }

        $db = self::connect('log');
        $logChanged = false;
        $messageCount = 0;

        assert('!isset($previousLog); /* cannot redeclare variable $previousLog */');
        $previousLog = $db->select("log.?");

        assert('!isset($message); /* cannot redeclare variable $message */');
        assert('!isset($newLog); // Cannot redeclare var $newLog');
        foreach ($messages as $message)
        {
            assert('$message instanceof Log; // unexpected result: Entry is not a subclass of Log');
            $newLog = Log::getLog($message, "log_");
            assert('is_array($newLog); // unexpected result: Log entry is expected to be an array');

            // check if new log entry is valid
            if (empty($newLog)) {
                continue;
            }

            // do not create duplicate entries
            if (Log::logEquals($newLog, $previousLog, "log_") === true) {
                continue;
            }

            // abort if insert failed
            if ($db->insert("log", $newLog) === false) {
                trigger_error("Unable to insert log. Insert on table 'log' failed.", E_USER_NOTICE);
                continue;
            }

            $previousLog = $newLog;
            $logChanged = true;
            $messageCount++;
        } // end foreach ($message)
        unset($message);

        // skip if nothing has changed
        if ($logChanged !== true) {
            return;
        }

        // abort if database commit failed
        if ($db->commit() === false) {
            trigger_error("Unable to write log. Commit on table 'log' failed.", E_USER_WARNING);
            return;
        }

        // get profile settings
        assert('!isset($logLength); // Cannot redeclare var $logLength');
        $maxLogLength = (int) $this->getVar('PROFILE.LOG_LENGTH');
        if ($maxLogLength < 1) {
            $maxLogLength = 50;
        }

        // finished, if number of log entries is still smaller than maximum
        if ($db->length('log') <= $maxLogLength) {
            return;
        }

        // get profile settings
        assert('!isset($useMail); // Cannot redeclare var $useMail');
        $useMail = (bool) $this->getVar('PROFILE.LOG.USE_MAIL');
        assert('!isset($logMail); // Cannot redeclare var $logMail');
        $logMail = $this->getVar('PROFILE.LOG.MAIL');

        // remove old entries
        if ($useMail && $logMail) {

            if (!empty($logMail)) {
                $mail = new FormMailer();
                $mail->content = $db->select('log', array(), array('LOG_ID'));
                $mail->subject = 'JOURNAL';
                $mail->send($logMail);
                unset($mail);
            }
            unset($logMail);

            // truncate table log
            if ($db->remove("log", array(), 0) === false) {
                trigger_error("Unable to remove entry from table 'log'.", E_USER_WARNING);
                return;
            }

            // commit changes
            if ($db->commit() === false) {
                trigger_error("Unable to commit changes to table 'log'.", E_USER_WARNING);
                return;
            }

        } else {
            $logIds = $db->select("log.*.log_id", array(), "", $maxLogLength, $db->length('log') + $messageCount, true);
            if (is_array($logIds)) {
                foreach ($logIds as $id)
                {
                    if (is_scalar($id) && !empty($id)) {
                        if ($db->remove("log.{$id}") === false) {
                            trigger_error("Unable to remove entry from table 'log'.", E_USER_WARNING);
                            return null;
                        }
                    }
                } /* end for */
                $db->commit();
            } else {
                $db->rollback();
            }
        }
    }

}

?>