Skip to content
Snippets Groups Projects
project.class.php 159.90 KiB
<?php

define("OPTION_DEFAULT", -999);
define("OPTION_NONE", -1);

require_once __DIR__.'/../inc_functions.php';
require_once 'base.class.php';
require_once 'htmlguielements.class.php';
require_once 'messenger.class.php';
require_once 'rollout_base.class.php';

/* ######################################################################

  IML DEPLOYMENT

  class project for all actions for single project
 * actions
 * rendering html

  ---------------------------------------------------------------------
  2013-11-08  Axel <axel.hahn@iml.unibe.ch>
  ###################################################################### */

/**
 * class for single project
 */
// class project {
class project extends base {
    // ----------------------------------------------------------------------
    // CONFIG
    // ----------------------------------------------------------------------

    /**
     * configuration ($aConfig in the config file)
     * @var array
     */
    private $_aConfig = array();

    /**
     * configuration of the project (= $aProjects[ID] in the config file)
     * @var array
     */
    private $_aPrjConfig = array();

    /**
     * version infos of all phases
     * @var array
     */
    private $_aData = array();

    /**
     * existing branches and tags
     * @var array
     */
    private $_aVcsBranches = array();

    /**
     * existing versions in the archive dir
     * @var array
     */
    private $_aVersions = array();

    /**
     * output file to fetch processing content with ajax request
     * @var string
     */
    private $_sProcessTempOut = false;

    /**
     * places of version infos in each deployment phase
     * @var array 
     */
    private $_aPlaces = array(
        "onhold" => "Queue",
        "ready2install" => "Puppet",
        "deployed" => "Installiert",
    );

    /**
     * collector for returncodes of multiple exec calls
     * @var int
     */
    private $_iRcAll = 0;

        
    /**
     * reference to html renderer class to draw output items
     * @var object
     */
    protected $_oHtml = false;
    
    /**
     * object to access a version control, .e. git
     * @var object
     */
    private $_oVcs = false;
    
    /**
     * object for rollout
     * @var type
     */
    public $oRolloutPlugin = false;
    
    private $_sBranchname = false;
    
    /**
     * send messages
     * @var messengerobject
     */
    protected $oMessenger = false;

    // ----------------------------------------------------------------------
    // constructor
    // ----------------------------------------------------------------------

    /**
     * constructor
     * @param string $sId  id of the project
     */
    public function __construct($sId = false) {
        $this->oUser = new user();
        $this->_oHtml = new htmlguielements();
        $this->_readConfig();
        if ($sId) {
            $this->setProjectById($sId);
        }
    }

    // ----------------------------------------------------------------------
    // private functions
    // ----------------------------------------------------------------------
    /**
     * add a log messsage
     * @global object $oLog
     * @param  string $sMessage  messeage text
     * @param  string $sLevel    warnlevel of the given message
     * @return bool
     */
    private function log($sMessage, $sLevel = "info") {
        global $oCLog;
        return $oCLog->add(basename(__FILE__) . " class " . __CLASS__ . " - " . $sMessage, $sLevel);
    }

    /**
     * send info messages to project specific targets (Slack, E-Mail)
     * @param string $sMessage
     * @return boolean
     */
    private function _sendMessage($sMessage){
        $aConfig=array();
        
        if (array_key_exists('messenger', $this->_aPrjConfig)
           && array_key_exists('slack', $this->_aPrjConfig['messenger'])
        ){
            $sSlack=$this->_aPrjConfig['messenger']['slack'];
            $aConfig['slack']=array('incomingurl'=>$sSlack);
            foreach(array('user', 'icon') as $sKey){
                if (array_key_exists($sKey, $this->_aConfig['messenger']['slack']['presets'][$sSlack])){
                    $aConfig['slack'][$sKey]=$this->_aConfig['messenger']['slack']['presets'][$sSlack][$sKey];
                }
            }
        }
        
        if (array_key_exists('messenger', $this->_aPrjConfig)
           && array_key_exists('email', $this->_aPrjConfig['messenger'])
           && $this->_aPrjConfig['messenger']['email']
        ){
            $aConfig['email']=$this->_aConfig['messenger']['email'];
            $aConfig['email']['to']=$this->_aPrjConfig['messenger']['email'];
        }
        if(!count($aConfig)){
            return false;
        }
        
        // init on first usage
        if (!$this->oMessenger){
            $this->oMessenger=new messenger($aConfig);
        }

        // add some metadata to the message body
        $sText=$this->getLabel().': '.html_entity_decode($sMessage)."\n" 
                . t('page-login-username'). ": ".$this->oUser->getUsername()."\n"
                ;
        if (isset($_SERVER) && is_array($_SERVER)) {
            if(array_key_exists('HTTP_HOST', $_SERVER)){
                $sText.= t('project-home').': '.$_SERVER['REQUEST_SCHEME'].'://'. $_SERVER['HTTP_HOST'].'/deployment/'.$this->getId()."\n";
            }
            /*
            if(array_key_exists('HTTP_ORIGIN', $_SERVER)){
                $sText.= t('project-home').": <".$_SERVER['HTTP_ORIGIN'].'/deployment/'.$this->getId()."/>\n";
        }
             */
        }
        return $this->oMessenger->sendMessage($sText);
    }
    /**
     * read default config file
     * @return boolean
     */
    private function _readConfig() {
        global $aConfig;
        $this->_aConfig = $aConfig;
        return true;
    }

    /**
     * validate config data
     * @return boolean
     */
    private function _verifyConfig() {
        if (!count($this->_aPrjConfig)){
            // die(t("class-project-error-no-config"));
            throw new Exception(t("class-project-error-no-config"));
        }

        if (!array_key_exists("packageDir", $this->_aConfig)) {
            die(t("class-project-error-no-packagedir"));
        }
        if (!$this->_aConfig["packageDir"]) {
            die(t("class-project-error-packagedir-empty"));
        }
        if (!file_exists($this->_aConfig["packageDir"])) {
            die(sprintf(t("class-project-error-packagedir-does-not-exist"), $this->_aConfig['packageDir']));
        }
        if (!array_key_exists("archiveDir", $this->_aConfig)) {
            die(t("class-project-error-no-archivedir"));
        }
        if (!$this->_aConfig["archiveDir"]) {
            die(t("class-project-error-archivedir-empty"));
        }
        if (!file_exists($this->_aConfig["archiveDir"])) {
            die(sprintf(t("class-project-error-packagedir-does-not-exist"), $this->_aConfig['archiveDir']));
        }

        foreach (array("fileprefix", "build", "phases") as $sKey) {
            if (!array_key_exists($sKey, $this->_aPrjConfig)) {
                die(sprintf(t("class-project-error-missing-prjkey"), $sKey, print_r($this->_aPrjConfig, true)));
            }
        }

        // TODO: verify ausbauen
        /*
          if (!$this->_aConfig["dataDir"]) {
          die(t("class-project-error-datadir-empty"));
          }
          if (!file_exists($this->_aConfig["dataDir"])) {
          die(sprintf(t("class-project-error-data-does-not-exist"), $this->_aConfig['dataDir']));
          }

          foreach (array("database", "projects", "sshkeys") as $sKey) {
          $sTestDir=$this->_aConfig["dataDir"]."/$sKey";
          if (!file_exists($sTestDir)) {
          mkdir($sTestDir);
          // die(sprintf(t("class-project-error-missing-prjkey"), $sKey, print_r($this->_aPrjConfig, true)));
          }
          }
         */
        return true;
    }

    /**
     * execute a commandline; returns a string of output of timestamp, command, output and returncode
     * @param string $sCommand
     * @return string
     */
    private function _execAndSend($sCommand, $bFlush = false) {
        $this->log(__FUNCTION__ . " start");
        $sReturn = '';
        $bUseHtml = $_SERVER ? true : false;

        if ($bFlush) {
            ob_implicit_flush(true);
        }
        // ob_end_flush();
        // stderr ausgeben
        $sCommand.=' 2>&1';
        $sReturn.="[" . date("H:i:s d.m.Y") . "] ";
        $sReturn.=$bUseHtml ? "<strong>$sCommand</strong>" : "$sCommand";
        $sReturn.=$bUseHtml ? "<br>" : "\n";
        $sOutput = false;
        $this->log(__FUNCTION__ . " start $sCommand");
        exec($sCommand, $aOutput, $iRc);
        $this->log(__FUNCTION__ . " ended command $sCommand");
        $sReturn.=(count($aOutput)) ? htmlentities(implode("\n", $aOutput)) . "\n" : "";
        /*
          $descriptorspec = array(
          0 => array("pipe", "r"), // stdin is a pipe that the child will read from
          1 => array("pipe", "w"), // stdout is a pipe that the child will write to
          2 => array("pipe", "w")    // stderr is a pipe that the child will write to
          );
          if ($bFlush) {
          flush();
          }
          $process = proc_open($sCommand, $descriptorspec, $pipes, realpath('./'), array());


          $sErrors = false;
          if (is_resource($process)) {
          while ($s = fgets($pipes[1])) {
          $sReturn.=$s;
          if ($bFlush) {
          flush();
          }
          }
          while ($s = fgets($pipes[2])) {
          $sErrors.=$s;
          if ($bFlush) {
          flush();
          }
          }
          }
          if ($sErrors) {
          $sReturn.="STDERR:\n" . $sErrors;
          }
          $oStatus = proc_get_status($process);
          $iRc = $oStatus['exitcode'];
         */

        if ($iRc != 0) {
            $this->_logaction("command failed: $sCommand - rc=" . $iRc, __FUNCTION__, "error");
        }

        $this->_iRcAll += $iRc;
        $sReturn.="[" . date("H:i:s d.m.Y") . "] " . t("exitcode") . " " . $iRc;
        if ($bUseHtml) {
            if ($iRc == 0) {
                $sReturn = '<pre class="cli">' . $sReturn;
            } else {
                $sReturn = '<pre class="cli error">' . $sReturn;
            }
            $sReturn.='</pre>';
        }

        if ($bFlush) {
            flush();
        }
        return $sReturn;
    }

    /**
     * add an action log message
     * @param string $sMessage    message
     * @param string $sAction     project action
     * @param string $sLoglevel   loglevel
     */
    private function _logaction($sMessage, $sAction = "", $sLoglevel = "info") {
        require_once("actionlog.class.php");
        $oLog = new Actionlog($this->_aConfig["id"]);
        $oLog->add($sMessage, (__CLASS__ . "->" . $sAction), $sLoglevel);
    }

    // ----------------------------------------------------------------------
    // GETTER
    // ----------------------------------------------------------------------

    private function _getConfigFile($sId) {
        if (!$sId) {
            die(t("class-project-error-_getConfigFile-requires-id"));
        }
        return $this->_aConfig["dataDir"] . '/projects/' . $sId . ".json";
    }

    /**
     * get a full ath for temp directory (for a build)
     * @return string
     */
    private function _getTempDir() {
        return $s = $this->_getBuildDir() . '/' . $this->_aPrjConfig["fileprefix"] . "_" . date("Ymd-His");
    }

    /**
     * get full path where the project builds are (a build setes a subdir)
     * @return string
     */
    private function _getBuildDir() {
        return $this->_aConfig['buildDir'] . '/' . $this->_aConfig["id"];
    }

    /**
     * get full path where the project default files are
     * @return type
     */
    private function _getDefaultsDir() {
        $s = $this->_aConfig['buildDefaultsDir'] . '/' . $this->_aConfig["id"];
        return file_exists($s) ? $s : false;
    }

    /**
     * get directory for infofile and package (without extension)
     * @param string $sPhase  one of preview|stage|live ...
     * @param string $sPlace  one of onhold|ready2install|deployed
     * @return string
     */
    private function _getFileBase($sPhase, $sPlace) {
        if (!array_key_exists($sPhase, $this->_aConfig["phases"])) {
            die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
        }
        if (!array_key_exists($sPlace, $this->_aPlaces)) {
            die(sprintf(t("class-project-error-wrong-place"), $sPlace));
        }


        // local file for onhold|ready2install
        $sBase = $this->_aConfig['packageDir'] . "/" . $sPhase . "/" . $this->_aPrjConfig["fileprefix"];
        if (!file_exists($this->_aConfig['packageDir'] . "/" . $sPhase)) {
            mkdir($this->_aConfig['packageDir'] . "/" . $sPhase);
        }

        if ($sPlace == "onhold")
            $sBase.="_onhold";
        // $sBase .= "/" . $this->_aPrjConfig["fileprefix"];
        // url for deployed
        if ($sPlace == "deployed") {
            if ($this->isActivePhase($sPhase) && array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase])) {
                // $sBase = $this->_aPrjConfig["phases"][$sPhase]["url"] . $this->_aPrjConfig["fileprefix"];
                $sBase = $this->_aPrjConfig["phases"][$sPhase]["url"];
            } else {
                $sBase = '';
            }
        }

        return $sBase;
    }

    /**
     * get filename for info/ meta file (.json file)
     * @param string $sPhase  one of preview|stage|live ...
     * @param string $sPlace  one of onhold|ready2install|deployed
     * @return string
     */
    private function _getInfofile($sPhase, $sPlace) {
        $sBase = $this->_getFileBase($sPhase, $sPlace);
        return $sBase ? $sBase . '/' . $this->_aPrjConfig["fileprefix"] . '.json' : false;
    }

    /**
     * get filename for package file (.tgz file)
     * @param string $sPhase  one of preview|stage|live ...
     * @param string $sPlace  one of onhold|ready2install|deployed
     * @return string
     */
    private function _getPackagefile($sPhase, $sPlace) {
        $sBase = $this->_getFileBase($sPhase, $sPlace);
        return $sBase ? $sBase . '/' . $this->_aPrjConfig["fileprefix"] . '.tgz' : false;
    }

    /**
     * list of files of a given phase and place
     * @param string $sPhase  one of preview|stage|live ...
     * @param string $sPlace  one of onhold|ready2install|deployed
     * @return array
     */
    public function _getBuildfilesByDir($sBase) {
        $aReturn = array();
        if (!$sBase || !is_dir($sBase)) {
            return false;
        }
        $iTotalSize = 0;
        $aReturn = array(
            'dir' => $sBase,
            'filecount' => false,
            'totalsize' => false,
            'totalsize-hr' => false,
        );

        foreach (glob($sBase . '/*') as $sFile) {
            $sFileBase = basename($sFile);
            $sExt = pathinfo($sFile, PATHINFO_EXTENSION);
            $aStat = stat($sFile);
            switch ($sExt) {
                case 'erb':
                    $sType = 'templates';
                    $sIcon = 'file-template';
                    break;
                case 'tgz':
                    $sType = 'package';
                    $sIcon = 'file-archive';
                    break;
                case 'json':
                    $sType = 'metadata';
                    $sIcon = 'file-meta';
                    break;
                default:
                    $sType = 'any';
                    $sIcon = 'file-any';
                    break;
            }
            $iTotalSize+=$aStat['size'];
            $aReturn['files'][$sFileBase] = array(
                'type' => $sType,
                'icon' => $this->_oHtml->getIcon($sIcon),
                'extension' => $sExt,
                'size' => $aStat['size'],
            );
            $aReturn['types'][$sType][] = $sFileBase;
        }
        $aReturn['totalsize'] = $iTotalSize;
        $aReturn['totalsize-hr'] = (round($iTotalSize / 1024 / 102.4) / 10) . ' MB';
        $aReturn['filecount'] = count($aReturn['files']);

        return $aReturn;
    }

    /**
     * list of files of a given phase and place
     * @param string $sPhase  one of preview|stage|live ...
     * @param string $sPlace  one of onhold|ready2install|deployed
     * @return array
     */
    public function getBuildfilesByPlace($sPhase, $sPlace) {
        $sBase = $this->_getFileBase($sPhase, $sPlace);
        return $this->_getBuildfilesByDir($sBase);
    }

    /**
     * list of files of a given version number
     * @param string $sVersion  name of version 
     * @return array
     */
    public function getBuildfilesByVersion($sVersion) {
        return $this->_getBuildfilesByDir($this->_getProjectArchiveDir() . '/' . $sVersion);
    }

    /**
     * get full path of a packed project archive
     * @param string $sVersion  version number of the build
     * @return string
     */
    private function _getArchiveDir($sVersion) {
        if (!$sVersion) {
            die(t("class-project-error-_getArchiveDir-requires-id"));
        }
        return $this->_getProjectArchiveDir() . '/' . $sVersion;
    }

    /**
     * get array of metadata of a given version. it returns 
     * - key "ok" anddata
     * or 
     * - key "error" with the message
     * @param type $sTimestamp
     * @return array
     */
    private function _getArchiveInfos($sTimestamp) {
        if (!$sTimestamp) {
            die(t("class-project-error-_getArchiveInfos-requires-id"));
        }
        $sInfoFile = $this->_getArchiveDir($sTimestamp) . '/' . $this->_aPrjConfig["fileprefix"] . '.json';
        $aReturn['infofile'] = $sInfoFile;
        $sPackageFile = $this->_getArchiveDir($sTimestamp) . '/' . $this->_aPrjConfig["fileprefix"] . '.tgz';
        $aReturn['packagefile'] = $sPackageFile;
        if (!file_exists($sInfoFile)) {
            $aReturn['error'] = sprintf(t("class-project-error-metafile-does-not-exist"), $sInfoFile);
            return $aReturn;
        }
        $aJson = json_decode(file_get_contents($sInfoFile), true);
        if (is_array($aJson) && array_key_exists("version", $aJson)) {
            $aReturn = array_merge($aReturn, $aJson);
            $aReturn['ok'] = 1;
            /*
              if (!file_exists($sPackageFile)) {
              $aReturn['error'] = sprintf(t("class-project-error-datafile-does-not-exist"), $sInfoFile);
              } else {
              $aReturn['filesizes']['packagefile']=filesize($sPackageFile);
              }
             * 
             */
            return $aReturn;
        }
        $aReturn['error'] = sprintf(t("class-project-error-metafile-wrong-format"), $sInfoFile);
        return $aReturn;
    }

    /**
     * get the directory for archive files of this project
     * @return string
     */
    public function _getProjectArchiveDir() {
        return $this->_aConfig["archiveDir"] . '/' . $this->_aConfig["id"];
    }

    /**
     * make an http get request and return the response body
     * @param string $url
     * @return string
     */
    private function _httpGet($url, $iTimeout = 5) {
        $this->log(__FUNCTION__ . " start");
        if (!function_exists("curl_init")) {
            die("ERROR: PHP CURL module is not installed.");
        }
        $this->log(__FUNCTION__ . " url: $url");
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout);
        curl_setopt($ch, CURLOPT_USERAGENT, 'IML Deployment GUI');
        $res = curl_exec($ch);
        curl_close($ch);
        $this->log(__FUNCTION__ . " done url: $url");
        return $res;
    }

    /**
     * get all existing versions in archive and its usage
     * versions are array keys; where they are used is written in values
     * @return array
     */
    public function getVersions() {

        // --- read all file entries
        $aReturn = array();
        $sDir = $this->_getProjectArchiveDir();
        if (is_dir($sDir)) {
            foreach (scandir($sDir) as $sEntry) {
                if (is_dir($sDir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..')
                    $aReturn[$sEntry] = false;
            }
        }
        $this->_aVersions = $aReturn;

        // --- check version in all phases 
        $this->getAllPhaseInfos();
        foreach ($this->_aData["phases"] as $sPhase => $aData) {
            foreach (array_keys($this->_aPlaces) as $sPlace) {
                if (array_key_exists($sPlace, $aData) && array_key_exists("version", $aData[$sPlace])) {
                    $this->_aVersions[$aData[$sPlace]["version"]][] = array('phase' => $sPhase, 'place' => $sPlace);
                }
            }
        }
        ksort($this->_aVersions);
        return $this->_aVersions;
    }
    /**
     * get an array with all existing build error output files (html)
     * @return array
     */
    public function getBuildErrors($sProject=false) {
        // --- read all file entries
        $aReturn = array();
        if(!$sProject){
            $sProject=$this->_aPrjConfig["fileprefix"].'_*';
        }
        foreach( glob($this->_getBuildDir() . '/' . $sProject . "/_output.html" ) as $sBuildDir){
            $aReturn[] = basename(dirname($sBuildDir)).'/'.basename($sBuildDir);
        }
        return $aReturn;
    }
    /**
     * get an array with all existing build error output files (html)
     * @return array
     */
    public function getBuildErrorContent($sLogfile) {
        if (!strpos('..', $sLogfile)===false){
            return false;
        }
        $sFilename=$this->_getBuildDir() . '/' . $sLogfile;
        if(file_exists($sFilename)){
            return file_get_contents($sFilename);
        }
        return false;
    }

    /**
     * get Array of all versions, metainfos, in which phases they are in use 
     * and a rollback ist possible or not
     * return array
     */
    private function _getVersionUsage() {
        $aVersionData = array();
        $sLastVersion = false;
        if (!count($this->getVersions())) {
            return array();
        }

        foreach ($this->getVersions() as $sVersion => $aData) {

            $aVersionData[$sVersion]["info"] = $this->_getArchiveInfos($sVersion);
            foreach ($this->getActivePhases() as $sPhase) {
                $bCanRollback = false;
                foreach (array_keys($this->_aPlaces) as $sPlace) {
                    $bFound = false;
                    if (is_array($aData) && count($aData)) {
                        foreach ($aData as $i => $aPhaseUsage) {
                            if ($aPhaseUsage["phase"] == $sPhase && $aPhaseUsage["place"] == $sPlace
                            )
                                $bFound = true;
                        }
                    }
                    $aVersionData[$sVersion]["usage"][$sPhase][$sPlace] = $bFound;
                    if ($bFound) {
                        $bCanRollback = false;
                    } else {
                        $bCanRollback = true;
                        if (
                                $sLastVersion && !$aVersionData[$sLastVersion]["rollback"][$sPhase]
                        ) {
                            $bCanRollback = false;
                        }
                        /*
                          if (!array_key_exists("ok", $aVersionData[$sVersion]["info"])){
                          $bCanRollback = false;
                          }
                         */
                    }
                    $aVersionData[$sVersion]["rollback"][$sPhase] = $bCanRollback;
                }
            }
            $sLastVersion = $sVersion;
        }
        return $aVersionData;
    }

    /**
     * recursive delete 
     * @param type $dir
     * @return type
     */
    private function _rmdir($dir) {
        foreach (scandir($dir) as $sEntry) {
            if (is_dir($dir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..') {
                $this->_rmdir($dir . '/' . $sEntry);
            } elseif (is_file($dir . '/' . $sEntry) || is_link($dir . '/' . $sEntry))
                unlink($dir . '/' . $sEntry);
        }
        return rmdir($dir);
    }

    /**
     * cleanup of archive directory; it returns the list of deleted
     * directories as array
     * @return array
     */
    public function cleanupArchive($bDeleteAll = false) {
        if (!$this->oUser->hasPermission("project-action-cleanup")) {
            return $this->oUser->showDenied();
        }
        $aDelete = array();
        $aUnused = array();

        $sDir = $this->_getProjectArchiveDir();
        $this->getVersions();
        if (!$this->_aVersions) {
            return $aDelete;
        }

        // find unused versions
        foreach ($this->_aVersions as $sVersion => $aUsage) {
            if (!$aUsage || count($aUsage) == 0) {
                $aUnused[] = $sVersion;
            }
        }

        // keep a few
        $iKeep = $bDeleteAll ? 0 : $this->_aConfig["versionsToKeep"];
        while (count($aUnused) && count($aUnused) > $iKeep) {
            $sVersion = array_shift($aUnused);
            $sDir2 = $sDir . '/' . $sVersion;
            if (is_dir($sDir2)) {
                if ($this->_rmdir($sDir2)) {
                    $aDelete[] = $sDir2;
                    echo t('ok') . ': ' . $sDir2;
                } else {
                    echo sprintf(t("class-project-warning-cannot-delete-archive-dir"), $sDir2);
                }
            } else {
                echo t('skip') . ': ' . $sDir2;
            }
            echo '<br>';
        }

        // rescan versions
        if (count($aDelete)) {
            $this->getVersions();
        }

        return $aDelete;
    }

    /**
     * cleanup of archive directory; it returns the list of deleted
     * directories as array
     * @return array
     */
    public function cleanupBuilds() {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-cleanup")) {
            return $this->oUser->showDenied();
        }
        $sDir = $this->_getBuildDir();
        $aDirlist = array();
        $aDelete = array();
        if (is_dir($sDir)) {
            foreach (scandir($sDir) as $sEntry) {
                if (is_dir($sDir . '/' . $sEntry) && $sEntry != '.' && $sEntry != '..')
                    $aDirlist[] = $sEntry;
            }
        }

        // keep a few
        while (count($aDirlist) > $this->_aConfig["builtsToKeep"]) {
            $sVersion = array_shift($aDirlist);
            $sDir2 = $sDir . '/' . $sVersion;
            if ($this->_rmdir($sDir2)) {
                $aDelete[] = $sDir2;
            } else {
                echo t("class-project-warning-cannot-delete-build-dir", $sDir2);
            };
        }

        return $aDelete;
    }

    /**
     * cleanup cache of vcs
     * @param type $iAge
     */
    public function cleanupVcsCache($iAge = 0) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-cleanup")) {
            return $this->oUser->showDenied();
        }
        $this->_initVcs();
        if ($this->_oVcs) {
            if (!method_exists($this->_oVcs, "cleanupCache")) {
                // the version control class does not have this method
                $this->log(__FUNCTION__ . " soory, Methos cleanupCache does not exist in this VCS class.");
                return '';
            }
            return $this->_oVcs->cleanupCache($iAge);
        }
    }

    /**
     * get conmplete config of the project
     * @return array
     */
    public function getConfig() {
        return $this->_aPrjConfig;
    }

    /**
     * get name/ label of the project
     * @return string
     */
    public function getLabel() {
        return $this->_aPrjConfig["label"];
    }

    /**
     * get description of the project
     * @return string
     */
    public function getDescription() {
        return $this->_aPrjConfig["description"];
    }

    /**
     * get the id of the current project
     * @return string
     */
    public function getId(){
        return $this->_aConfig["id"];
    }
    /**
     * get deploy and queue infos for all phases
     * @return type
     */
    public function getAllPhaseInfos() {

        $bHasQueue=false;
        $bHasDifferentVersions=false;
        $bFirstVersion=false;

        if (!array_key_exists("phases", $this->_aData)){
            $this->_aData["phases"] = array();
        }
        if (!array_key_exists("progress", $this->_aData)){
            $this->_aData["progress"] = array();
        }

        foreach (array_keys($this->_aConfig["phases"]) as $sPhase) {
            if (!array_key_exists($sPhase, $this->_aData["phases"])) {
                $this->getPhaseInfos($sPhase);
            }
            // detect progress
            $aDataPhase = $this->_aData["phases"][$sPhase];
            foreach (array_keys($this->getPlaces()) as $sPlace) {
                if (
                    array_key_exists($sPlace, $aDataPhase) 
                    && array_key_exists('version', $aDataPhase[$sPlace])
                ) {
                    if($bFirstVersion && !$bHasDifferentVersions && $bFirstVersion!==$aDataPhase[$sPlace]['version']){
                        $bHasDifferentVersions=true;
                    }
                    if (!$bFirstVersion){
                        $bFirstVersion = $aDataPhase[$sPlace]['version'];
                    }
                }
            }
            // check queue
            if (!$bHasQueue && array_key_exists('onhold', $aDataPhase) && $aDataPhase['onhold']['version']){
                $bHasQueue=true;
            }
        }
        $this->_aData["progress"]=array(
            'inprogress'=>$bHasDifferentVersions,
            'hasQueue'=>$bHasQueue,
        );
        return $this->_aData["phases"];
    }

    /**
     * get statusinfos of a named phase
     * @param string $sPhase name of the phase; one of preview|stage|live
     * @return array
     */
    public function getPhaseInfos($sPhase) {
        if (!$sPhase) {
            die(t("class-project-error-getPhaseInfos-requires-phase"));
        }
        if (!array_key_exists("phases", $this->_aData))
            $this->_aData["phases"] = array();

        if (!array_key_exists($sPhase, $this->_aData["phases"])) {
            if ($this->isActivePhase($sPhase)) {

                $this->_aData["phases"][$sPhase] = array();
                $aTmp = array();

                // a blocked package is waiting for deployment timeslot?
                $sKey = "onhold";
                $sJsonfile = $this->_getInfofile($sPhase, $sKey);
                $aTmp[$sKey] = array();
                if (file_exists($sJsonfile)) {
                    $aJson = json_decode(file_get_contents($sJsonfile), true);
                    if (array_key_exists("version", $aJson)) {
                        $aTmp[$sKey] = $aJson;
                        $aTmp[$sKey]["infofile"] = $sJsonfile;
                        $aTmp[$sKey]["ok"] = 1;
                    } else {
                        $aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
                    }
                } else {
                    $aTmp[$sKey]["info"] = t("class-project-info-no-package-in-queue");
                    $aTmp[$sKey]["ok"] = 1;
                }

                // package for puppet
                $sKey = "ready2install";
                $sJsonfile = $this->_getInfofile($sPhase, $sKey);
                $aTmp[$sKey] = array();
                if (file_exists($sJsonfile)) {
                    $sPkgfile = $this->_getPackagefile($sPhase, $sKey);
                    if (file_exists($sPkgfile)) {
                        $aJson = json_decode(file_get_contents($sJsonfile), true);
                        if (is_array($aJson) && array_key_exists("version", $aJson)) {
                            $aTmp[$sKey] = $aJson;
                            $aTmp[$sKey]["infofile"] = $sJsonfile;
                            $aTmp[$sKey]["packagefile"] = $sPkgfile;
                            $aTmp[$sKey]["ok"] = 1;
                        } else {
                            $aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
                        }
                    } else {
                        $aTmp[$sKey]["error"] = sprintf(t("class-project-error-getPhaseInfos-package-not-found"), $sPkgfile);
                    }
                } else {
                    $aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-does-not-exist"), $sJsonfile);
                }

                // published data
                $sKey = "deployed";
                $sJsonfile = $this->_getInfofile($sPhase, $sKey);
                $aTmp[$sKey] = array();

                // use version cache
                require_once(__DIR__ . '/../../valuestore/classes/valuestore.class.php');
                $oVersion = new valuestore();
                $oVersion->setProject("", $this->_aPrjConfig["fileprefix"], $sPhase, $sKey);
                $aVersions = $oVersion->getVersion();
                // echo "Place: <pre>" . print_r($oVersion->whereiam(), 1) . "</pre>";
                // echo "Versionen: <pre>" . print_r($aVersions, 1) . "</pre>";
                if (count($aVersions)){
                    $aTmp[$sKey] = array();
                    $aTmp[$sKey] = $aVersions[0]['_data'];
                    $aTmp[$sKey]["infofile"] = '[versioncache]';

                    $aTmp[$sKey]['_hosts'] = array();
                    foreach ($aVersions as $sHostname => $aHostdata) {
                        $aTmp[$sKey]['_hosts'][$aHostdata['host']] = $aHostdata;
                    }
                    $aTmp[$sKey]["ok"] = 1;
                    $aTmp[$sKey]["infofile"] = '[versioncache]';
                }

                /*
                  $sJsonData = $this->_httpGet($sJsonfile);
                  if ($sJsonData) {
                  $aJson = json_decode($sJsonData, true);
                  if (is_array($aJson) && array_key_exists("version", $aJson)) {
                  $aTmp[$sKey] = $aJson;
                  $aTmp[$sKey]["infofile"] = $sJsonfile;
                  $aTmp[$sKey]["ok"] = 1;
                  } else {
                  $aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-has-no-version"), $sJsonfile, print_r($aJson, true));
                  }
                  } else {
                  $aTmp[$sKey]["error"] = sprintf(t("class-project-error-metafile-wrong-format"), $sJsonfile);
                  }
                 * 
                 */
            } else {
                $aTmp['onhold']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
                $aTmp['ready2install']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
                $aTmp['deployed']["warning"] = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
            }

            $this->_aData["phases"][$sPhase] = $aTmp;
        }
        return $this->_aData["phases"][$sPhase];
    }

    /**
     * get a list of all existing projects as a flat array
     * <code>
     * print_r($oPrj->getProjects());
     * </code>
     * returns<br>
     * Array ( [0] => project1 [1] => project2 ) 
     * @return array
     */
    public function getProjects() {
        $aReturn = array();
        foreach (glob(dirname($this->_getConfigFile("dummy")) . "/*.json") as $filename) {
            $aReturn[] = str_replace(".json", "", basename($filename));
        }
        sort($aReturn);
        return $aReturn;
    }

    /**
     * check if the given phase is active for this project
     * @param type $sPhase
     * @return type
     */
    public function isActivePhase($sPhase) {
        return (
                array_key_exists("active", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["active"][0] : false
                );
    }

    /**
     * return array of all (active and inactive) phases
     * @return type
     */
    public function getPhases() {
        return $this->_aConfig["phases"];
    }

    /**
     * return array of all (active and inactive) phases
     * @return type
     */
    public function getPlaces() {
        return $this->_aPlaces;
    }
    /**
     * get a flat array with active phases of the project
     * @return array
     */
    public function getActivePhases() {
        $aReturn = array();
        foreach (array_keys($this->_aConfig["phases"]) as $s) {
            if ($this->isActivePhase($s)) {
                $aReturn[] = $s;
            }
        }
        return $aReturn;
    }

    /**
     * find the next active phase of a project
     * @param string $sPhase current phase; if empty the function sends back the first phase
     */
    public function getNextPhase($sPhase = false) {
        if ($sPhase) {
            if (!array_key_exists($sPhase, $this->_aConfig["phases"])) {
                die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
            }
        }

        $sNextPhase = false;
        $bUseNextPhase = $sPhase ? false : true;
        foreach (array_keys($this->_aConfig["phases"]) as $s) {
            if ($bUseNextPhase) {
                if ($this->isActivePhase($s)) {
                    $sNextPhase = $s;
                    $bUseNextPhase = false;
                    continue;
                }
            }
            if ($sPhase == $s) {
                $bUseNextPhase = true;
            }
        }

        return $sNextPhase;
    }

    /**
     * get an array with deploy status ...  
     *    'inprogress'=>do versions differ from phase to phase = rollout of a version is in progress
          'hasQueue'=>is there a package in a queue (waiting for deployment time to get ready to be installed)
     * @return array
     */
    public function getProgress(){
        $this->getAllPhaseInfos();
        return $this->_aData['progress'];
    }
    /**
     * check: is the deployment to the next phase enabled for this phase?
     * @param type $sPhase  current phase
     */
    public function canAcceptPhase($sPhase = false) {
        if (!$this->oUser->hasPermission("project-action-accept") && !$this->oUser->hasPermission("project-action-accept-$sPhase")
        ) {
            // echo $this->oUser->showDenied();
            return false;
        }

        if (!$sPhase) {
            // for better performance: skip check on overview page
            /*
              $aRepodata = $this->getRepoRevision();
              if (!array_key_exists("revision", $aRepodata)) {
              return false;
              }
             */
            $sNext = $this->getNextPhase($sPhase);
            return $sNext > '';
        }


        if (!array_key_exists($sPhase, $this->_aConfig["phases"])) {
            die(sprintf(t("class-project-error-wrong-phase"), $sPhase));
        }
        if (!$this->isActivePhase($sPhase)) {
            // die("ERROR: the phase $sPhase is not active in this project.");
            return false;
        }
        $sNext = $this->getNextPhase($sPhase);
        if (!$sNext) {
            return false;
        }

        // ensure that _aData is filled
        $this->getPhaseInfos($sPhase);

        // array key "ok" must be in the ready2install and deployed info
        // and a version must be installed
        if (
                array_key_exists($sPhase, $this->_aData["phases"]) && array_key_exists("onhold", $this->_aData["phases"][$sPhase]) && array_key_exists("ready2install", $this->_aData["phases"][$sPhase]) && array_key_exists("deployed", $this->_aData["phases"][$sPhase]) && array_key_exists("ok", $this->_aData["phases"][$sPhase]["onhold"]) && array_key_exists("ok", $this->_aData["phases"][$sPhase]["ready2install"]) && array_key_exists("ok", $this->_aData["phases"][$sPhase]["deployed"]) && array_key_exists("version", $this->_aData["phases"][$sPhase]["deployed"])
        ) {
            return true;
        }
        return false;
    }

    /**
     * get list of remote branches and tags
     * @param type $sActiveBranchname
     * @return string|boolean
     */
    public function getRemoteBranches($sActiveBranchname = false) {
        $this->log(__FUNCTION__ . " start");
        $this->_initVcs();
        if ($this->_oVcs) {
            if (!method_exists($this->_oVcs, "getRemoteBranches")) {
                // the version control class does not have this method
                return '';
            }
            return $this->_oVcs->getRemoteBranches();
        }
        return false;
    }

    /**
      }
     * get html form with selectr for remote branches
     * @param string $sActiveBranchname  force active branch name
     * @return string
     */
    public function renderSelectRemoteBranches($sActiveBranchname = false) {
        $aReturn = array();
        $aRadios = array();
        $bFoundActive = false;
        $i = 0;
        if (!$this->_oVcs) {
            $this->_initVcs();
        }
        require_once("formgen.class.php");
        if (!$sActiveBranchname) {
            $sActiveBranchname = $this->_sBranchname;
        }
        if ($this->_oVcs) {
            if (!method_exists($this->_oVcs, "getRemoteBranches")) {
                // the version control class does not have this method
                return '';
            }
            foreach ($this->_oVcs->getRemoteBranches() as $aBranch) {
                $sBranch = $aBranch['name'];
                $aRadios[$sBranch] = array(
                    'value' => $sBranch,
                    'label' => $aBranch['label'],
                );
                // if no param was given the first branch will be marked
                if (!$sActiveBranchname) {
                    $sActiveBranchname = $sBranch;
                }
                if ($sBranch == $sActiveBranchname) {
                    $bFoundActive = true;
                    // $aRadios[$sBranch]['checked'] = 'checked';
                    $aRadios[$sBranch]['selected'] = 'selected';
                } else {
                    // for SELECT we need the onclick even on select element
                    // not on the option (Chrome)
                    // $aRadios[$sBranch]['onclick'] = 'document.getElementById(\'submitBranch\').click()';
                }
            };
        }
        // no branches were found
        if (count($aRadios) == 0) {
            return '';
        }

        $aForms = array(
            'frmSelectBranch' => array(
                'meta' => array(
                    'method' => 'POST',
                    'action' => '?',
                    'id' => 'frmSelectBranch',
                ),
                'validate' => array(),
                'form' => array(
                    'branchname' => array(
                        'inline' => true,
                        'type' => 'select',
                        'onchange' => 'document.getElementById(\'submitBranch\').click()',
                        'name' => 'branchname',
                        'label' => '<strong>' . t('branch-select') . '</strong>',
                        'validate' => 'isastring',
                        'options' => $aRadios,
                    ),
                ),
            ),
        );

        // submit to switch branches - only if a selection is available
        if (count($aRadios) > 1 || !$bFoundActive) {
            $aForms['frmSelectBranch']['form']['submitBranch'] = array(
                'type' => 'submit',
                'name' => 'btnsave',
                'onclick' => 'showModalMessage(\'' . t('branch-switch') . '\'); ',
                'label' => t("change"),
                'value' => $this->_oHtml->getIcon('sign-ok').t("change"),
            );
        }

        $oFrm = new formgen($aForms);
        return $oFrm->renderHtml('frmSelectBranch')
                . '<script>$("#submitBranch").hide();</script>';
        // return $oFrm->renderHtmlElement('dummy',$aFormData);
    }

    /**
     * get current revision and log message from remote repo
     * @param boolean  $bRefresh  optional: refresh data; default: use cache
     * @return array
     */
    public function getRepoRevision($bRefresh = false) {
        $this->log(__FUNCTION__ . "($bRefresh) start");

        if (!$this->_aPrjConfig["build"]["type"]) {
            $this->_aData["phases"]["source"] = array("error" => t("class-project-error-repo-type-not-set"),);
        } else {
            $this->_initVcs();
            if ($this->_oVcs) {
                $this->_aData["phases"]["source"] = $this->_oVcs->getRepoRevision($bRefresh);
            } else {
                $this->_aData["phases"]["source"] = array(
                    "error" => sprintf(t("class-project-error-repo-type-not-supported"), $this->_aPrjConfig["build"]["type"]),
                );
            }
        }
        return $this->_aData["phases"]["source"];
    }

    /**
     * init version control system (git, ...)
     * @return vcs-object
     */
    private function _initVcs() {
        $this->log(__FUNCTION__ . " start");
        if (!$this->_oVcs) {
            if (!$this->_aPrjConfig["build"]["type"]) {
                $this->_aData["phases"]["source"] = array("error" => t("class-project-error-repo-type-not-set"),);
            } else {
                if (!@include_once("vcs." . $this->_aPrjConfig["build"]["type"] . ".class.php")) {
                    $this->_aData["phases"]["source"] = array(
                        "error" => sprintf(t("class-project-error-repo-type-not-supported"), $this->_aPrjConfig["build"]["type"]),
                    );
                } else {
                    $aConfig = $this->_aPrjConfig["build"];
                    // for vcs classes
                    $aConfig["appRootDir"] = $this->_aConfig["appRootDir"];
                    $aConfig["dataDir"] = $this->_aConfig["dataDir"];
                    $aConfig["tmpDir"] = $this->_aConfig["tmpDir"];
                    $this->_oVcs = new vcs($aConfig);
                    if ($this->_sBranchname) {
                        if (method_exists($this->_oVcs, "setCurrentBranch")) {
                            $this->_oVcs->setCurrentBranch($this->_sBranchname);
                        }
                    }
                }
            }
        }
        return $this->_oVcs;
    }

    /**
     * get an array of enabled plugins
     * @param string  $sSection  one of false|"rollout"|...
     * @return array
     */
    public function getConfiguredPlugins($sSection=false){
        $aReturn=array();
        if(!$sSection){
            $aReturn=$this->_aConfig["plugins"];
        } else {
            foreach ($this->_aConfig["plugins"]["rollout"] as $sPluginName=>$aItem) {
                $aReturn[$sPluginName] = $aItem;
            }
        }
        return $aReturn;
    }
    
    /**
     * get a flat array of all existing ssh keys
     * @return array
     */
    private function _getSshKeys() {
        $aReturn = array();
        foreach (glob($this->_aConfig["dataDir"] . "/sshkeys/*.pub") as $filename) {
            $aReturn[] = str_replace(".pub", "", basename($filename));
        }
        sort($aReturn);
        return $aReturn;
    }

    /**
     * get a flat array with regexes of deploy times
     * @param string $sPhase  phase
     * @return array
     */
    private function _getDeploytimes($sPhase) {
        if (!$this->isActivePhase($sPhase)) {
            $sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
            $this->_logaction($sError, __FUNCTION__, "error");
            return false;
        }
        $aDeploytimes = array();
        if (array_key_exists("deploytimes", $this->_aConfig["phases"][$sPhase])) {
            $aDeploytimes = $this->_aConfig["phases"][$sPhase]["deploytimes"];
        }
        if (array_key_exists("deploytimes", $this->_aPrjConfig["phases"][$sPhase]) && $this->_aPrjConfig["phases"][$sPhase]["deploytimes"]
        ) {
            $aDeploytimes = array($this->_aPrjConfig["phases"][$sPhase]["deploytimes"]);
        }
        return $aDeploytimes;
    }

    // ----------------------------------------------------------------------
    // SETTER
    // ----------------------------------------------------------------------

    private function _setProcessOutFile($sNewTempfile = false) {
        if ($this->_sProcessTempOut && file_exists($this->_sProcessTempOut)) {
            unlink($this->_sProcessTempOut);
        }
        // $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile);
        $this->_sProcessTempOut = $sNewTempfile ? sys_get_temp_dir() . "/" . basename($sNewTempfile) : false;
        return $this->_sProcessTempOut;
    }

    /**
     * get projects from ldap; it returns ldap search items with cn as 
     * array key.
     * 
     * @return array
     */
    private function _ldapProjectSearch($sSearchFilter) {
        $aReturn = array();
        require_once("ldap.class.php");
        $oLdapIML = new imlldap($this->_aConfig['projects']['ldap']);
        // $oLdapIML->debugOn();
        $aResultsIml = $oLdapIML->searchDn(
                $this->_aConfig['projects']['ldap']['DnProjects'], $sSearchFilter, array("*")
        );
        if (!$aResultsIml['count']) {
            return false;
        }
        $oLdapIML->close();

        /*
          unset($aResultsIml['count']);
          foreach ($aResultsIml as $aItem) {
          $aReturn[$aItem['cn'][0]] = array(
          'dn' => $aItem['dn'],
          'cn' => $aItem['cn'][0],
          '_description' => $aItem['description'][0],
          'title' => $sTitle,
          'description' => $sDescription,
          );
          }
          $oLdapIML->close();
          ksort($aReturn);
          return $aReturn;
         * 
         */
        return $aResultsIml;
    }

    /**
     * load config of a project
     * @return boolean
     */
    public function setProjectById($sId) {
        if ($sId !== preg_replace('/[^a-z0-9\-\_]/i', '', $sId)) {
            echo "ERROR: invalid syntax in project ID: $sId<br>";
            return false;
        }
        $this->_aPrjConfig = array();
        $this->_aConfig["id"] = $sId;


        if ($this->_aConfig['projects']['json']['active']) {
            $this->_aPrjConfig = json_decode(file_get_contents($this->_getConfigFile($sId)), true);
        }
        if ($this->_aConfig['projects']['ldap']['active']) {
            // TODO: read project after saving it - @see $this->saveConfig()
            $sQuery = '(&(objectclass=hieraSource)(documentIdentifier=' . $sId . '))';
            $aResult = $this->_ldapProjectSearch($sQuery);
            // echo '<pre>$aResult = ' . print_r($aResult, 1) . '</pre>';
            if (is_array($aResult) && $aResult[0] && array_key_exists('hieradata', $aResult[0])
            ) {
                foreach ($aResult[0]['hieradata'] as $sLine) {
                    // echo $sLine.'<br>';
                    if (preg_match('/^cfg=/', $sLine)) {

                        // echo $sLine.'<br>';
                        $this->_aPrjConfig = json_decode(preg_replace('/^cfg=/', '', $sLine), 1);
                    }
                }
            }
            // return $this->objAdd($sDn, $aItem);
        }

        // $aData=json_decode(file_get_contents($this->_getConfigFile($sId)), true);
        // echo "<pre>" . print_r($aData, true) . "</pre>";

        $this->_verifyConfig();
        
        // ----- init rollout plugin
        // set name of the activated plugin for this project
        $sPluginName=(isset($this->_aPrjConfig['deploy']['active_rollout_plugin']) && $this->_aPrjConfig['deploy']['active_rollout_plugin']) 
                ? $this->_aPrjConfig['deploy']['active_rollout_plugin']
                : 'default'
            ;
        $this->oRolloutPlugin = false;
        try{
            require_once __DIR__.'/../plugins/rollout/'.$sPluginName.'/rollout_'.$sPluginName.'.php';
            $this->oRolloutPlugin = new rollout(array(
                'lang'=>$this->_aConfig['lang'],
                'phase'=>false,
                'globalcfg'=>$this->_aConfig['plugins']['rollout'][$sPluginName],
                'projectcfg'=>$this->_aPrjConfig,
            ));
            // print_r($this->_oRolloutPlugin->getPluginfos());
            // print_r($this->_oRolloutPlugin->getName());
        } catch (Exception $ex) {
            
        }
        return true;
    }

    /**
     * set a branchname
     * @param string $sBranchname name of the branch, i.e. "origin/master"
     * @return bool
     */
    public function setBranchname($sBranchname) {
        $this->_sBranchname = $sBranchname;
        if ($this->_oVcs) {
            if (method_exists($this->_oVcs, "setCurrentBranch")) {
                $this->_oVcs->setCurrentBranch($sBranchname);
            }
        }
        return $this->_sBranchname;
    }
        
    // ----------------------------------------------------------------------
    // ACTIONS
    // ----------------------------------------------------------------------

    /**
     * send data to a tempfile for ajax polling
     * @param type $sTmpFile
     * @param type $sData
     * @return boolean
     */
    private function _TempFill($sData, $aActions = array()) {
        if (!$this->_sProcessTempOut) {
            return false;
        }
        $sActions = '';
        if (count($aActions)) {
            for ($i = 0; $i < count($aActions["actions"]); $i++) {
                $sActions.='<li';
                if ($i == $aActions["iActive"]) {
                    $sActions.=' class="active"';
                }
                $sActions.='>' . $aActions["actions"][$i]['label'] . '</li>';
            }
            if ($sActions) {
                $sData = '<div style="float: right; background: #f8f8f8; padding: 1em;">'
                        . '<strong>' . $aActions["label"] . '</strong>'
                        . '<ol class="actions">'
                        . $sActions
                        . '</ol></div>'
                        . $sData;
            }
        }
        return file_put_contents($this->_sProcessTempOut, $sData);
    }

    /**
     * delete tempfile for ajax polling; if a directory is given as parameter
     * the tmp file will be moved there 
     * @param string  $sTempDir  optional; target dir to copy; default=false (=delete file)
     * @return boolean
     */
    private function _TempDelete($sTempDir=false) {
        if (!$this->_sProcessTempOut){
            return false;
        }
        if (file_exists($this->_sProcessTempOut)) {
            if ($sTempDir && is_dir($sTempDir)){
                $sKeepOutfile=$sTempDir.'/_output.html';
                copy($this->_sProcessTempOut, $sKeepOutfile);
            }
            unlink($this->_sProcessTempOut);
        }
        return file_exists($this->_sProcessTempOut);
    }

    /**
     * get the name of the current branch (or default branch)
     * @return string
     */
    public function getBranchname() {
        $this->log(__FUNCTION__ . " start");
        $this->_initVcs();
        if ($this->_oVcs) {
            if (method_exists($this->_oVcs, "getCurrentBranch")) {
                $this->setBranchname($this->_oVcs->getCurrentBranch());
                return $this->_oVcs->getCurrentBranch();
            }
        }
        return false;
    }

    /**
     * Build a new package for the deployment. It will be put to the queue
     * of the first active phase (i.e. preview).
     * If there is no deployment time range it will be deployed too.
     * @global type $aParams
     * @return boolean|string
     */
    public function build($sTmpFile = false) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-build")) {
            return $this->oUser->showDenied();
        }
        global $aParams;
        $sReturn = false;

        $aActionList = array(
            'iActive' => 0,
            'label' => t('build'),
            'actions' => array(
                array('label' => t('class-project-build-label-cleanup-builds')),
                array('label' => t('class-project-build-label-create-workdir')),
                array('label' => t('class-project-build-label-get-sources-from-version-control')),
                array('label' => t('class-project-build-label-execute-hook-postclone')),
                array('label' => t('class-project-build-label-copy-default-structure')),
                array('label' => t('class-project-build-label-execute-hook-precompress')),
                array('label' => t('class-project-build-label-cleanup-project')),
                array('label' => t('class-project-build-label-create-package')),
                array('label' => t('class-project-build-label-remove-workdir')),
                array('label' => t('class-project-build-label-queue-to-first-active-phase')),
            ),
        );
        $this->_setProcessOutFile($sTmpFile);

        $this->_iRcAll = 0;
        // return $this->_execAndSend("bash --login -c 'ruby --version' " . $sTempDir);

        $this->_logaction(t('starting') . " build()", __FUNCTION__);
        $sReturn = "<h2>" . t("build") . " " . $this->getLabel() . "</h2>";

        // --------------------------------------------------
        // cleanup
        // --------------------------------------------------
        $aDirs = $this->cleanupBuilds();
        if (count($aDirs)) {
            $sReturn.='<h3>' . t('class-project-build-label-cleanup-builds') . '</h3><pre>' . print_r($aDirs, true) . '</pre>';
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        $this->_initVcs();
        if (!$this->_oVcs) {
            $sError = sprintf(t('class-project-error-build-type-not-supported'), $this->_aPrjConfig["build"]["type"]);
            $this->_logaction($sError, __FUNCTION__, "error");
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }

        // --------------------------------------------------
        // create workdir
        // --------------------------------------------------
        $sTempBuildDir = $this->_getTempDir();
        $sFirstLevel = $this->getNextPhase();
        if (!$sFirstLevel) {
            $this->_TempDelete();
            $this->_logaction(t('page-overview-no-phase'), __FUNCTION__, "error");
            return false;
        }

        $sReturn.='<h3>' . t('class-project-build-label-create-workdir') . '</h3>';
        if (!file_exists($sTempBuildDir)) {
            $sReturn.=$this->_execAndSend("mkdir -p " . $sTempBuildDir);
        }
        $sReturn.=$this->_execAndSend("ls -ld " . $sTempBuildDir);
        if (!file_exists($sTempBuildDir)) {
            $this->_TempDelete();
            $sError = sprintf(t('"class-project-error-build-dir-was-not-created"'), $sTempBuildDir);
            $this->_logaction($sError, __FUNCTION__, "error");
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }
        // $this->_iRcAll = 0;
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        // --------------------------------------------------
        // checkout
        // --------------------------------------------------
        $sReturn.='<h3>' . t('class-project-build-label-get-sources-from-version-control') . '</h3>';

        $sReturn.='<pre>' . $this->_oVcs->getSources($sTempBuildDir) . '</pre>';

        $aVersion = $this->_oVcs->getRevision($sTempBuildDir);
        $sRevision = $aVersion["revision"];
        $sCommitMsg = $aVersion["message"];
        $sCommitMsg = str_replace("\n", "<br>", $sCommitMsg);
        $sCommitMsg = str_replace('"', "&quot;", $sCommitMsg);
        $sReturn.=$this->_oHtml->getBox("info", $sCommitMsg);

        $sReturn.=$this->_execAndSend("ls -lisa $sTempBuildDir");

        if (!$this->_iRcAll == 0) {
            $sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
            $this->_logaction($sError, __FUNCTION__, "error");
            $this->_TempFill($sError.$sReturn, $aActionList);
            $this->_TempDelete($sTempBuildDir);
            return $this->_oHtml->getBox("error", $sError.$sReturn);
        }

        // --------------------------------------------------
        foreach (glob($sTempBuildDir . '/hooks/on*') as $filename) {
            $sReturn.='chmod 755 ' . $filename . '<br>';
            $sReturn.=$this->_execAndSend('chmod 755 ' . $filename);
            $sReturn.=$this->_execAndSend('ls -l ' . $filename);
        }
        // --------------------------------------------------
        $sCfgout=$sTempBuildDir . '/ci-custom-vars';
        if (isset($this->_aPrjConfig['deploy']["configfile"]) && $this->_aPrjConfig['deploy']["configfile"]){
            file_put_contents($sCfgout, $this->_aPrjConfig['deploy']["configfile"]);
            $sReturn.=$this->_execAndSend('ls -l ' . $sCfgout);
            $sReturn.=$this->_execAndSend('cat ' . $sCfgout);
        }

        $sReturn.=$this->_oHtml->getBox("success", t('class-project-info-build-checkout-ok'));
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // execute hook postclone
        // --------------------------------------------------
        // task#1726 - add environment
        $sSetEnv=''
                . 'export GIT_SSH="'.$this->_aConfig['appRootDir'].'/shellscripts/gitsshwrapper.sh";'
                . 'export DIR_SSH_KEYS="'.$this->_aConfig['dataDir'].'/sshkeys";'
                . 'export DIR_APPROOT="'.$sTempBuildDir.'";'
                . 'export NVMINIT="'.$this->_aConfig['appRootDir'].'/shellscripts/nvm_init.sh";'
                . (isset($this->_aConfig['build']['env']) ? $this->_aConfig['build']['env'] : '')
                ;
        
        $sHookfile = $this->_aConfig['build']['hooks']['build-postclone'];
        $sReturn.='<h3>' . t('class-project-build-label-execute-hook-postclone') . ' (' . $sHookfile . ')</h3>';
        
        if (file_exists($sTempBuildDir . '/' . $sHookfile)) {
            // $sReturn.=$this->_execAndSend('chmod 755 ' . $sTempDir . '/hooks/on*');
            // $this->_iRcAll = 0;
            $sReturn.=$this->_execAndSend('bash --login -c \'' . $sSetEnv.' '.$sTempBuildDir . '/' . $sHookfile . '\'');
            if (!$this->_iRcAll == 0) {
                $sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
                $this->_logaction($sError, __FUNCTION__, "error");
                $this->_TempFill($sError.$sReturn, $aActionList);
                $this->_TempDelete($sTempBuildDir);
                return $this->_oHtml->getBox("error", $sError . $sReturn);
            }
        } else {
            $sReturn.=t('skip') . '<br>';
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // copy default structure
        // --------------------------------------------------
        $sReturn.='<h3>' . t('class-project-build-label-copy-default-structure') . '</h3>';
        if ($this->_getDefaultsDir()) {
            $sReturn.=$this->_execAndSend("find " . $this->_getDefaultsDir() . " | head -15");
            $sReturn.=$this->_execAndSend("rsync -r " . $this->_getDefaultsDir() . "/* $sTempBuildDir");
            // $sReturn.=$this->_execAndSend("find $sTempDir");
        } else {
            $sReturn.=t('skip') . '<br>';
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        // --------------------------------------------------
        // execute hook
        // --------------------------------------------------
        $sHookfile = $this->_aConfig['build']['hooks']['build-precompress'];
        $sReturn.='<h3>' . t('class-project-build-label-execute-hook-precompress') . ' (' . $sHookfile . ')</h3>';
        if (file_exists($sTempBuildDir . '/' . $sHookfile)) {
            // $sReturn.=$this->_execAndSend('chmod 755 ' . $sTempDir . '/hooks/on*');
            // $this->_iRcAll = 0;
            $sReturn.=$this->_execAndSend('bash --login -c \'' . $sSetEnv.' '.$sTempBuildDir . '/' . $sHookfile . '\'');
            if (!$this->_iRcAll == 0) {
                $sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
                $this->_logaction($sError, __FUNCTION__, "error");
                $this->_TempFill($sError.$sReturn, $aActionList);
                $this->_TempDelete($sTempBuildDir);
                return $this->_oHtml->getBox("error", $sError . $sReturn);
            }
        } else {
            $sReturn.=t('skip') . '<br>';
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // cleanup .git, .svn, ...
        // --------------------------------------------------
        $sReturn.='<h3>' . t('class-project-build-label-cleanup-project') . '</h3>';
        if ($this->_oVcs) {
            $this->_oVcs->cleanupWorkdir($sTempBuildDir);
        }

        // $sReturn.=$this->_execAndSend("cd $sTempDir && rm -rf .git");
        // $sReturn.=$this->_execAndSend("cd $sTempDir && rm -rf .svn");
        // $sReturn.=$this->_execAndSend("find  $sTempDir -type d -name '.svn' -exec rm -rf {} \;");
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        // --------------------------------------------------
        // create package
        // --------------------------------------------------
        $sReturn.='<h3>' . t('class-project-build-label-create-package') . '</h3>';
        // public_html must exist
        if (array_key_exists('haspublic', $this->_aPrjConfig["build"])
                && $this->_aPrjConfig["build"]["haspublic"][0]
        ){
            $sWebroot = false;
            $sWebroot1 = $sTempBuildDir . '/public_html';
            $sWebroot2 = $sTempBuildDir . '/public';
            if (file_exists($sWebroot1)) {
                $sWebroot = $sWebroot1;
            }
            if (file_exists($sWebroot2)) {
                $sWebroot = $sWebroot2;
            }

            if (!$sWebroot) {
                $sError = t('class-project-error-build-docroot-not-found');
                $this->_logaction($sError, __FUNCTION__, "error");
                $this->_TempFill($sError.$sReturn, $aActionList);
                $this->_TempDelete($sTempBuildDir);
                return $this->_oHtml->getBox("error", $sError . $sReturn . $sError);
            }
        }
        if (!$this->_iRcAll == 0) {
            $sError = sprintf(t('class-project-error-command-failed'), $sTempBuildDir);
            $this->_logaction($sError, __FUNCTION__, "error");
            $this->_TempFill($sError.$sReturn, $aActionList);
            $this->_TempDelete($sTempBuildDir);
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }
        // $sReturn.=$this->_oHtml->getBox("success", "preparations ok - directory is ready for packaging now.");
        // generate info file
        $sTs = date("Y-m-d H:i:s");
        $sTs2 = date("Ymd_His");
        $sBranch = ($this->_sBranchname ? $this->_sBranchname : t("defaultbranch"));
        $sInfoFileWebroot = $sTempBuildDir . '/' . basename($this->_getInfofile($sFirstLevel, "deployed"));
        $sInfoFileArchiv = $this->_getArchiveDir($sTs2) . '/' . basename($this->_getInfofile($sFirstLevel, "deployed"));
        $sPackageFileArchiv = $this->_getArchiveDir($sTs2) . '/' . basename($this->_getPackagefile($sFirstLevel, "deployed"));

        $aInfos = array(
            'date' => $sTs,
            'version' => $sTs2,
            'branch' => $sBranch,
            'revision' => $sRevision,
            'message' => $sCommitMsg,
        );
        /*
          $sInfos = '{
          "date": "' . $sTs . '",
          "version": "' . $sTs2 . '",
          "branch": "' . $sBranch . '",
          "revision": "' . $sRevision . '",
          "message": "' . $sCommitMsg . '"
          }';
         * 
         */
        /*
          "user": "' . $aParams["inputUser"] . '",
          "remark": "' . $aParams["inputComment"] . '"
         */

        $sReturn.=t("class-project-info-build-write-meta-to-webroot") . "<br>";
        // file_put_contents($sInfoFileWebroot, $sInfos);
        file_put_contents($sInfoFileWebroot, json_encode($aInfos));
        $sReturn.=$this->_execAndSend("ls -l $sInfoFileWebroot");
        $sReturn.=$this->_execAndSend("cat $sInfoFileWebroot");

        if (!file_exists(dirname($sPackageFileArchiv))) {
            $sReturn.=sprintf(t("creating-directory"), dirname($sPackageFileArchiv)) . "<br>";
            mkdir(dirname($sPackageFileArchiv), 0775, true);
        }
        $sReturn.=$this->_execAndSend("ls -ld " . dirname($sPackageFileArchiv));
        if (!file_exists(dirname($sPackageFileArchiv))) {
            
            $sError = sprintf(t('"class-project-error-build-dir-was-not-created"'), $sTempBuildDir);
            $this->_logaction($sError, __FUNCTION__, "error");
            $this->_TempFill($sError.$sReturn, $aActionList);
            $this->_TempDelete($sTempBuildDir);
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }
        $this->_TempFill($sReturn, $aActionList);


        // create tgz archive
        $sReturn.=sprintf(t("creating-file"), $sPackageFileArchiv) . "<br>";
        $sReturn.=$this->_execAndSend("cd $sTempBuildDir && tar -czf $sPackageFileArchiv .");
        $this->_TempFill($sReturn, $aActionList);

        // write info file (.json)
        $sReturn.=sprintf(t("creating-file"), $sInfoFileArchiv) . "<br>";
        // file_put_contents($sInfoFileArchiv, $sInfos);
        file_put_contents($sInfoFileArchiv, json_encode($aInfos));

        // copy template files
        if (file_exists($sTempBuildDir . '/hooks/templates/')) {
            $sReturn.=t("class-project-info-build-write-templatefiles-to-archive") . "<br>";
            $sReturn.=$this->_execAndSend("cp $sTempBuildDir/hooks/templates/* " . $this->_getArchiveDir($sTs2));
        } else {
            $sReturn.=t("class-project-info-build-write-templatefiles-to-archive-skipped") . "<br>";
        }
        $this->_TempFill($sReturn, $aActionList);

        $sReturn.="<br>" . t("info") . ":<br>";
        $sReturn.=$this->_execAndSend("ls -l " . $this->_getArchiveDir($sTs2));

        // TEST
        // $this->_iRcAll=1;
        if (!$this->_iRcAll == 0) {
            $sError = t('class-project-error-build-packaging-failed');
            $this->_logaction($sError, __FUNCTION__, "error");
            $this->_TempFill($sError.$sReturn, $aActionList);
            $this->_TempDelete($sTempBuildDir);
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        $sReturn.='<h3>' . t("class-project-build-label-remove-workdir") . '</h3>';
        $sReturn.=$this->_execAndSend("rm -rf $sTempBuildDir");
        $sReturn.=t("class-project-info-build-remove-oldest-archives");
        $sReturn.='<pre>' . print_r($this->cleanupArchive(), true) . '</pre>';

        $sInfo = t("class-project-info-build-successful");
        $this->_logaction(t('finished') . ' ' . $sInfo, __FUNCTION__, "success");
        $sReturn.=$this->_oHtml->getBox("success", $sInfo);
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        $sReturn.=$this->queue($sFirstLevel, $sTs2);
        $this->_TempDelete();
        $this->_setProcessOutFile(false);


        return $sReturn;
    }

    /**
     * put a packaged version into the queue of a specified phase
     * @param string $sPhase    name of the phase
     * @param string $sVersion  version 
     * @return string
     */
    public function queue($sPhase, $sVersion) {
        $aActionList = array(
            'iActive' => 0,
            'label' => t("queue"),
            'actions' => array(
                array('label' => t("class-project-queue-label-checks")),
                array('label' => t("class-project-queue-label-remove-existing-version")),
                array('label' => t("class-project-queue-label-link-new-version")),
                array('label' => t("class-project-queue-label-deploy")),
            ),
        );
        $this->_logaction(t('starting') . " queue($sPhase, $sVersion)", __FUNCTION__);
        $sReturn = "<h2> " . t("queue") . " " . $this->getLabel() . " :: $sPhase</h2>";
        $this->_TempFill($sReturn, $aActionList);

        if (!$this->isActivePhase($sPhase)) {
            $sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
            $this->_logaction($sError, __FUNCTION__, "error");
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }

        $sPlace = "onhold";
        $sLinkTarget = $this->_getArchiveDir($sVersion);
        $sLinkName = $this->_getFileBase($sPhase, $sPlace);

        // --------------------------------------------------
        // Checks
        // --------------------------------------------------
        if (!$sLinkName) {
            die(t("class-project-error-queue-sLinkName-is-empty"));
        }
        if (!$sLinkTarget) {
            die(t("class-project-error-queue-sLinkTarget-is-empty"));
        }
        if (!file_exists($sLinkTarget)) {
            die(sprintf(t("class-project-error-queue-wrong-version"), $sVersion, $sLinkTarget));
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        // --------------------------------------------------
        // remove existing version
        // --------------------------------------------------
        $this->_iRcAll = 0;
        if (file_exists($sLinkName)) {
            $sReturn.=t("class-project-queue-label-remove-existing-version") . "<br>";
            $sReturn.=$this->_execAndSend("rm -f $sLinkName");
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        // --------------------------------------------------
        // create the new link
        // --------------------------------------------------
        $sReturn.=t("class-project-queue-label-link-new-version") . "<br>";

        $sReturn.=$this->_execAndSend("ln -s $sLinkTarget $sLinkName");
        $sReturn.=$this->_execAndSend("ls -l $sLinkName | fgrep $sLinkTarget");
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        if (!$this->_iRcAll == 0) {
            $this->_TempDelete();
            $sError = t("class-project-error-command-failed");
            $this->_logaction($sError, __FUNCTION__, "error");
            return $this->_oHtml->getBox("error", $sError . $sReturn);
        }
        $this->_logaction(t('finished') . " queue($sPhase, $sVersion) " . t("class-project-info-queue-successful"), __FUNCTION__);
        $sReturn.=$this->_oHtml->getBox("success", t("class-project-info-queue-successful"));
        $sReturn.=$this->deploy($sPhase);
        $this->_TempDelete();

        return $sReturn;
    }

    /**
     * deploy a queued package - this moves the queue into the repo directory
     * and will be installed on server within 30 min.
     * This method checks the deploy times
     * @param string $sPhase which queue of which phase we want to install in server
     * @param bool   $bIgnoreDeploytimes  flag; if true it will override time windows
     * @return boolean|string
     */
    public function deploy($sPhase, $bIgnoreDeploytimes = false) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-deploy") && !$this->oUser->hasPermission("project-action-deploy-$sPhase")
        ) {
            return $this->oUser->showDenied();
        }
        $aActionList = array(
            'iActive' => 0,
            'label' => t("deploy"),
            'actions' => array(
                array('label' => t("class-project-deploy-label-checks")),
                array('label' => t("class-project-deploy-label-activate-queued-version")),
                array('label' => t("class-project-deploy-label-synch-packages")),
                array('label' => t("class-project-deploy-label-install-on-target")),
            ),
        );
        $sReturn = "<h2>" . t("deploy") . " " . $this->getLabel() . " :: $sPhase</h2>";
        $this->_TempFill($sReturn, $aActionList);

        if (!$this->isActivePhase($sPhase)) {
            $sError = sprintf(t("class-project-warning-phase-not-active"), $sPhase);
            $this->_logaction($sError, __FUNCTION__, "error");
            return $sReturn . $this->_oHtml->getBox("error", $sError);
        }

        $sQueueLink = $this->_getFileBase($sPhase, "onhold");
        $sRepoLink = $this->_getFileBase($sPhase, "ready2install");

        // --------------------------------------------------
        // checks
        // --------------------------------------------------
        $sReturn.="<h3>" . t("class-project-deploy-label-checks") . "</h3>";

        $aDeploytimes = $this->_getDeploytimes($sPhase);
        if (count($aDeploytimes)) {
            // check if the a deploy time is reached
            $sNow = date("D H:i:s");
            $sReturn.=sprintf(t("class-project-info-deploy-check-deployment-times"), $sNow) . "<br>";
            $bCanDeploy = false;
            foreach ($aDeploytimes as $sRegex) {
                $sReturn.=sprintf(t("class-project-info-deploy-test-regex"), $sRegex);

                if (preg_match($sRegex, $sNow)) {
                    $sReturn.=t("ok");
                    $bCanDeploy = true;
                } else {
                    $sReturn.=t("no");
                }
                $sReturn.="<br>";
            }
            if (!$bCanDeploy) {
                if (!$bIgnoreDeploytimes) {
                    $sError = t("class-project-info-deploy-time-not-reached");
                    // $this->_logaction($sError, __FUNCTION__);
                    $sReturn.=$this->_oHtml->getBox("info", $sError);
                    $this->_TempDelete();
                    // removed: cronjob sends this message too
                    // $this->_sendMessage($sError."\n".t('phase').': '.$sPhase);
                    return $sReturn;
                } else {
                    $sReturn.=t("class-project-info-deploy-time-not-reached-and-ignored") . "<br>";
                }
            } else {
                $sReturn.=t("class-project-info-deploy-time-ok") . "<br>";
            }
            // if ()
        }
        $this->_logaction(t('starting') . " deploy($sPhase, $bIgnoreDeploytimes)", __FUNCTION__);
        if (!file_exists($sQueueLink)) {
            $sError = sprintf(t("class-project-info-deploy-nothing-in-queue"), $sQueueLink);
            $this->_logaction($sError, __FUNCTION__, "error");
            $sReturn.=$this->_oHtml->getBox("info", $sError);
            $this->_TempDelete();
            $this->_sendMessage($sError."\n".t('phase').': '.$sPhase);
            return $sReturn;
        }
        $this->_TempFill($sReturn);
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // move the queue link to the repo name
        // --------------------------------------------------
        $this->_iRcAll = 0;
        if (file_exists($sRepoLink)) {
            $sReturn.=t("class-project-info-deploy-removing-existing-version") . "<br>";
            $sReturn.=$this->_execAndSend("rm -f $sRepoLink");
        }
        $this->_TempFill($sReturn);
        $sReturn.=t("class-project-info-deploy-moving-queue-to-repo") . "<br>";
        $sReturn.=$this->_execAndSend("mv $sQueueLink $sRepoLink");


        if (!$this->_iRcAll == 0) {
            $this->_TempDelete();
            $sError = t("class-project-error-command-failed");
            $this->_logaction($sError, __FUNCTION__, "error");
            $sReturn.=$this->_oHtml->getBox("error", $sError . $sReturn);
            $this->_sendMessage($sError."\n".t('phase').': '.$sPhase);
            return $sReturn;
        }

        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // synch packages
        // --------------------------------------------------
        // $sReturn.=$this->_execAndSend("ln -s $sLinkTarget $sLinkName");
        if (array_key_exists('mirrorPackages', $this->_aConfig) && count($this->_aConfig['mirrorPackages'])) {
            foreach ($this->_aConfig['mirrorPackages'] as $sLabel => $aTarget) {
                $sReturn.='<h3>' . sprintf(t("class-project-info-deploy-synching-package"), $sLabel) . "</h3>";
                if (array_key_exists('type', $aTarget)) {
                    $sCmd = false;
                    // $sSource=$this->_aConfig["packageDir"]."/$sPhase/*";
                    $sSource = $sRepoLink;
                    $sTarget = $aTarget['target'] . "/$sPhase";
                    switch ($aTarget['type']) {
                        case 'rsync':
                            $sCmd = "ls -l $sSource 2>/dev/null && /usr/bin/rsync --delete -rLv  $sSource $sTarget";
                            break;
                        default:
                            $sReturn.=sprintf(t("class-project-info-deploy-skip-sync"), $aTarget['type']) . "<br>";
                            break;
                    } // switch
                    if ($sCmd) {
                        /*
                          if ($aTarget['runas']) {
                          $sCmd="su - " . $aTarget['runas'] . " -c \"" . $sCmd . "\"";
                          }
                         * 
                         */
                        $sReturn.=$this->_execAndSend($sCmd);
                        $this->_TempFill($sReturn);
                    }
                }
            } // foreach
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);


        // --------------------------------------------------
        // run action to install
        // --------------------------------------------------
        $sDeploymethod = array_key_exists("deploymethod", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["deploymethod"] : "none";
        $sTargethosts = array_key_exists("hosts", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["hosts"] : '';

        $sReturn.='<h3>' . t("class-project-info-deploy-start-by-method") . ' :: ' . $sDeploymethod . '</h3>'
                . '<p>'
                . t("deploymethod-$sDeploymethod") . '<br>'
                . t("phase-targethosts") . ': ' . ($sTargethosts ? $sTargethosts : t("none"))
                . '</p>'
        ;
        if ($sDeploymethod === "none" || !$sTargethosts) {
            $sReturn.=t("class-project-info-deploy-start-by-method-skip") . "<br>";
        } else {
            $aTargethosts = explode(',', $sTargethosts);
            foreach ($aTargethosts as $sTargethost) {
                $sReturn.='<h4>' . $sDeploymethod . ' - ' . $sTargethost . '</h4>';
                $sCmd = '';
                switch ($sDeploymethod) {
                    case 'puppet':
                        $sCmd = 'ssh ' . $this->_aConfig["installPackages"]["user"]
                                . '@' . $sTargethost
                                . ' ' . $this->_aConfig["installPackages"]["command"];
                        break;
                        ;
                    // TODO: we don't have any proxy yet
                    case 'sshproxy__AS_EXAMPLE_ONLY':
                        $sCmd = 'ssh ' . $this->_aConfig["installPackages"]["sshproxy"]["user"]
                                . '@' . $this->_aConfig["installPackages"]["sshproxy"]["host"]
                                . ' ' . sprintf($this->_aConfig["installPackages"]["sshproxy"]["command"], $sTargethost);
                        break;
                        ;
                }
                if ($sCmd) {
                    // $sReturn.=$sCmd.'<br>';
                    $sReturn.=$this->_execAndSend("$sCmd");
                }
            }
        }
        $aActionList['iActive'] ++;
        $this->_TempFill($sReturn, $aActionList);

        $sReturn.="<br>";
        $sReturn.=$this->_oHtml->getBox("success", t("class-project-info-deploy-successful"));
        
        $this->_sendMessage(t("class-project-info-deploy-successful")."\nphase: ${sPhase}\n");
        $this->_logaction(t('finished') . " deploy($sPhase, $bIgnoreDeploytimes) " . t("class-project-info-deploy-successful"), __FUNCTION__, "success");
        $this->_TempDelete();
        return $sReturn;
    }

    /**
     * accept a the installed version in a phase and put this version
     * to the queue of the next phase.
     * @param string $sPhase which queue of which phase we want to install in server
     * @return type
     */
    public function accept($sPhase) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-accept") && !$this->oUser->hasPermission("project-action-accept-$sPhase")
        ) {
            return $this->oUser->showDenied();
        }

        $sReturn = "<h2>" . t("accept") . " " . $this->getLabel() . " :: $sPhase</h2>";
        $this->_logaction(t('starting') . " accept($sPhase)", __FUNCTION__);

        if (!$this->canAcceptPhase($sPhase)) {
            $sError = sprintf(t("class-project-error-accept-impossible"), $sPhase);
            $this->_logaction($sError, __FUNCTION__, "error");
            return $sReturn . $this->_oHtml->getBox("error", $sError);
        }
        $sReturn.="<h3>" . sprintf(t("class-project-info-accept-overview"), $sPhase) . "</h3>";
        $this->_TempFill($sReturn);
        $aInfos = $this->getPhaseInfos($sPhase);
        $sVersion = $aInfos["deployed"]["version"];
        $sNext = $this->getNextPhase($sPhase);


        // $sReturn.='<pre>' . print_r($aInfos["deployed"], true) . '</pre>';
        $sReturn.=$this->_oHtml->getBox("info", sprintf(t("class-project-info-accept-version-and-next-phase"), $sVersion, $sNext));
        $this->_logaction(t('finished') . " accept($sPhase) " . sprintf(t("class-project-info-accept-version-and-next-phase"), $sVersion, $sNext), __FUNCTION__, "success");
        $sReturn.=$this->queue($sNext, $sVersion);
        $this->_TempFill($sReturn);
        $this->_TempDelete();

        return $sReturn;
    }

    /**
     * save POSTed data as project config
     * @return boolean
     */
    public function saveConfig($aData = false) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-setup")) {
            return $this->oUser->showDenied();
        }
        $this->_logaction(t('starting') . " saveConfig(...)", __FUNCTION__);
        if (!$aData) {
            $aData = $_POST;
        }

        foreach (array('id', 'label', 'description', 'contact', 'build', 'fileprefix', 'phases') as $sKey) {
            if (!array_key_exists($sKey, $aData)) {
                $this->_logaction(t('abortet') . " missing key $sKey in savedata", __FUNCTION__, "error");
                return false;
            }
        }
        $sId = $aData["id"];

        // remove unwanted items
        foreach (array("setupaction", "prj", "id") as $s) {
            if (array_key_exists($s, $aData)) {
                unset($aData[$s]);
            }
        }

        // save json file
        if ($this->_aConfig['projects']['json']['active']) {
            // echo "IST <pre>" . print_r($this->_aPrjConfig, true) . "</pre>"; echo "NEU <pre>" . print_r($aData, true) . "</pre>"; die();
            // make a backup of a working config
            $sCfgFile = $this->_getConfigFile($sId);
            $sBakFile = $this->_getConfigFile($sId) . ".ok";
            copy($sCfgFile, $sBakFile);

            $bReturn = file_put_contents($sCfgFile, json_encode($aData));
            $this->_aPrjConfig = json_decode(file_get_contents($this->_getConfigFile($sId)), true);
        }
        // save in ldap
        if ($this->_aConfig['projects']['ldap']['active']) {
            // TODO: 
            echo "TODO: save in LDAP<br><pre>" . print_r($aData, 1) . "</pre>";


            $sDn = 'documentIdentifier=' . $sId . ',' . $this->_aConfig['projects']['ldap']['DnProjects'];
            $aItem = array(
                'objectClass' => array(
                    'document',
                    'hieraSource',
                    'top',
                ),
                'hieraData' => array(
                    'cfg=' . json_encode($aData),
                    'updated=' . date("Y-m-d H:i:s") . ' by ' . $this->oUser->getUsername(),
                )
            );

            require_once("ldap.class.php");
            $oLdapIML = new imlldap($this->_aConfig['projects']['ldap']);
            // $oLdapIML->debugOn();
            if (!$oLdapIML->DnExists($sDn)) {
                if ($oLdapIML->objAdd($sDn, $aItem)) {
                    echo 'OK, created in LDAP.<br>';
                    $bReturn = true;
                } else {
                    echo 'ERROR, DN ' . $sDn . ' was not created in LDAP :-/<br>';
                    $bReturn = false;
                }
            } else {
                if ($oLdapIML->objUpdate($sDn, $aItem)) {
                    echo 'OK, updated in LDAP.<br>';
                    $bReturn = true;
                } else {
                    echo 'ERROR, DN ' . $sDn . ' was not updated in LDAP :-/<br>';
                    $bReturn = false;
                }
            }
            $oLdapIML->close();
        }

        $this->_logaction(t('finished') . " saveConfig(...)", __FUNCTION__, "success");
        $this->setProjectById($sId);

        $sMessage=($bReturn 
                ? t("page-setup-info-settings-were-saved")
                : t("page-setup-error-settings-were-not-saved")
        );
        $this->_sendMessage($sMessage);
        return $bReturn;
    }

    /**
     * create a new project; it returns the error message if it fails and
     * an empty string if it was successful.
     * @param string  $sId  id
     * @return string
     */
    public function create($sId) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-create")) {
            return $this->oUser->showDenied();
        }
        $this->_logaction(t('starting') . " create($sId)", __FUNCTION__);
        if (!$sId) {
            $sError = t("class-project-error-create-missing-id");
            $this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
            return $sError;
        }
        $s = preg_replace('/[a-z\-\_0-9]*/', "", $sId);
        if ($s) {
            $sError = sprintf(t("class-project-error-create-wrcng-chars-in-id"), $sId);
            $this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
            return $sError;
        }
        if ($sId == "all") {
            $sError = sprintf(t("class-project-error-create-id-has-reserved-name"), $sId);
            $this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
            return $sError;
        }
        if (array_search($sId, $this->getProjects()) !== false) {
            $sError = sprintf(t("class-project-error-create-id-exists"), $sId);
            $this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
            return $sError;
        }

        // reset config and create a skeleton
        $this->_readConfig();
        $this->_aConfig["id"] = $sId;

        $this->_aPrjConfig = array(
            "id" => $sId, // for saveConfig
            "label" => "$sId",
            "fileprefix" => "$sId",
            "description" => '',
            "contact" => '',
            "build" => array(
                "type" => "",
                "ssh" => "",
                "auth" => "",
                "webaccess" => "",
            ),
            "phases" => array(
                "preview" => array(),
                "stage" => array(),
                "live" => array(),
            ),
        );
        $this->_verifyConfig(); // check skeleton
        $bReturn = $this->saveConfig($this->_aPrjConfig);
        if (!$bReturn) {
            $sError = t("class-project-error-create-save-failed");
            $this->_logaction(t('aborted') . " create($sId)" . $sError, __FUNCTION__, "error");
            return $sError;
        }

        // alles OK - dann leeren String
        $this->_logaction(t('finished') . " create($sId)", __FUNCTION__, "success");
        return "";
    }

    /**
     * delete a project; it returns a string with errormessage; false = no error
     * @param array $aOptions
     * @return boolean|string
     */
    public function delete($aOptions = array()) {
        $this->log(__FUNCTION__ . " start");
        if (!$this->oUser->hasPermission("project-action-delete")) {
            return $this->oUser->showDenied();
        }
        $sCfgfile = $this->_getConfigFile($this->_aConfig["id"]);
        if (!file_exists($sCfgfile)) {
            return t("class-project-error-delete-project-no-configfile");
        }
        $this->_logaction(t('starting') . " delete()", __FUNCTION__);

        // (array("bRemoveRepolinks", "bRemoveArchive", "bRemoveConfig")
        // --- remove links in phases directory to built archives
        if (array_key_exists("bRemoveRepolinks", $aOptions) && $aOptions["bRemoveRepolinks"]) {
            echo "DELETE Repo-Links ...<br>";

            foreach (array_keys($this->getPhases()) as $sPhase) {
                foreach (array_keys($this->_aPlaces) as $sPlace) {
                    $sLink = $this->_getFileBase($sPhase, $sPlace);
                    if (file_exists($sLink)) {
                        echo "Removing $sLink ($sPhase - $sPlace)...<br>";
                        if (!unlink($sLink)) {
                            $sError = t("class-project-error-delete-project-deletion-failed-data");
                            $this->_logaction(t('aborted') . " " . $sError, __FUNCTION__);
                            return $sError;
                        }
                    }
                }
            }
        }
        if (array_key_exists("bRemoveArchive", $aOptions) && $aOptions["bRemoveArchive"]) {
            echo "DELETE built Archives ...<br>";
            $this->cleanupArchive(true); // true to delete all
        }
        if (array_key_exists("bRemoveConfig", $aOptions) && $aOptions["bRemoveConfig"]) {
            echo "DELETE Config ...<br>";
            // echo "config file: $sCfgfile<br>";
            if (file_exists($sCfgfile . ".ok")) {
                // echo "Delete ${sCfgfile}.ok<br>";
                unlink($sCfgfile . ".ok");
            }
            if (file_exists($sCfgfile)) {
                // echo "Delete ${sCfgfile}<br>";
                if (!unlink($sCfgfile)) {
                    $sError = t("class-project-error-delete-project-deletion-failed-configfile");
                    $this->_logaction(t('aborted') . " " . $sError, __FUNCTION__);
                    return $sError;
                }
            }
        }
        $this->_sendMessage(t('finished') . " delete()");
        $this->_logaction(t('finished') . " delete()", __FUNCTION__, "success");
        return false;
    }

    // ----------------------------------------------------------------------
    // RENDERING
    // ----------------------------------------------------------------------

    /**
     * return html code for a div with background color based on a checksum of the given text
     * @param string $sText      text that is used for checksum; if false ist returns a gray
     * @param string $sContent   optional: text to show
     * @return string
     */
    private function _getChecksumDiv($sText, $sContent='') {
        if ($sText){
            
            // color ranges in decimal values for RGB from ... to
            $iFgStart=60;  $iFgEnd=160;
            $iBgStart=200; $iBgEnd=250;

            // deivider: 3 digits of md5 will be extracted
            $iFgDivider=16*16*16/($iFgEnd-$iFgStart);
            $iBgDivider=16*16*16/($iBgEnd-$iBgStart);
            
            $sHash=md5($sText);
            $sColor=''
                . 'color: rgba(' 
                . ($iFgStart + round(hexdec(substr($sHash,0,3))/$iFgDivider)) . ','
                . ($iFgStart + round(hexdec(substr($sHash,3,3))/$iFgDivider)) . ','
                . ($iFgStart + round(hexdec(substr($sHash,6,3))/$iFgDivider)) . ','
                . '1'
                . ');'
                . 'background: rgba(' 
                . ($iBgStart + round(hexdec(substr($sHash,0,3))/$iBgDivider)) . ','
                . ($iBgStart + round(hexdec(substr($sHash,3,3))/$iBgDivider)) . ','
                . ($iBgStart + round(hexdec(substr($sHash,6,3))/$iBgDivider)) . ','
                . '1'
                . ');'
                ;
        } else {
            $sColor = "color: #888; background: #ccc;";
        }
        return '<div style="' . $sColor . '; border-top: 3px solid; ">'.($sContent ? $sContent : ' ').'</div>';
    }

    /**
     * generate css color based on a checksum of the given text
     * @param string $sText text that is used for checksum
     * @return string 
     */
    private function _getChecksumColor($sText, $sFormat = "hex", $fAlpha = 1.0) {
        $sReturn = '';        
        if ($sText){
                $sHash=md5($rssItem["feedtitle"]);
                $iStartFg=100;
                $iStartBg=220;
                $sColor=''
                        . ''
                        . 'color: rgba(' 
                        . ($iStartFg + round(hexdec(substr($sHash,0,2))/4)) . ','
                        . ($iStartFg + round(hexdec(substr($sHash,2,2))/4)) . ','
                        . ($iStartFg + round(hexdec(substr($sHash,4,2))/4))
                        . ');'
                        . 'background: rgba(' 
                        . ($iStartBg + round(hexdec(substr($sHash,0,2))/8)) . ','
                        . ($iStartBg + round(hexdec(substr($sHash,2,2))/8)) . ','
                        . ($iStartBg + round(hexdec(substr($sHash,4,2))/8))
                        . ');'
                        ;
            
            $s = md5($sText);
            $sRH = substr($s, 0, 2);
            $sGH = substr($s, 2, 2);
            $sBH = substr($s, 4, 2);
        } else {
            $sReturn = "background: #aaaaaa;";
        }
        switch ($sFormat) {
            case "rgba":
                $sReturn = "background: rgba(" . hexdec($sRH) . ", " . hexdec($sGH) . ", " . hexdec($sBH) . ", " . $fAlpha . ")";
                break;

            default:
                $sReturn = "background: #$sRH$sGH$sBH";
                break;
        }
        return $sReturn;
    }

    /**
     * get html code for the colored bar on top of each phase detail items
     * @param string $sPhase  phase of a project
     * @param string $sPlace  place in the given phase
     * @return string
     */
    private function _renderBar($sPhase, $sPlace) {
        $aDataPhase = $this->getPhaseInfos($sPhase);
        $aData = $aDataPhase[$sPlace];
        if (!array_key_exists("revision", $aData)) {
            return false;
        }
        return $this->_getChecksumDiv($aData["revision"]);
    }

    private function _renderHostsData($aData) {
        $sReturn = '';
        if (array_key_exists('_hosts', $aData)) {
            
            // $sReturn.= print_r($aData['_hosts'], 1);
            $sReturn.= '<div class="hosts">'
                    . '<br><strong>' . t('hosts') . ':</strong><br>'
            ;
            foreach ($aData['_hosts'] as $sHostname => $aHostinfos) {
                $oUpdateDate = date("U", strtotime($aHostinfos['time']));
                $iAgeUpdate = round((date("U") - $oUpdateDate) / 60);
                $sAge = $iAgeUpdate < 60 * 60 * 13 ? $iAgeUpdate . " min" : "??";

                $sReturn.= '<div class="host">'
                        . $this->_getChecksumDiv(
                            $aHostinfos['_data']['revision'],
                            $this->_oHtml->getIcon('host').'<br>' . $sHostname
                        )
                        . "($sAge)"
                        . '</div>'
                ;
            }
            $sReturn.= '</div><div style="clear: both;"></div>';
        }
        return $sReturn;
    }

    /**
     * get html code for list of hosts in a phase
     * @param string $sPhase  phase of a project
     * @return string
     */
    private function _renderHosts($sPhase) {
        $aDataPhase = $this->getPhaseInfos($sPhase);
        if (is_array($aDataPhase) && array_key_exists('deployed', $aDataPhase)) {
            return $this->_renderHostsData($aDataPhase['deployed']);
        }
        return '';
    }

    /**
     * get html code for list of files in a phase
     * @param string $sPhase  phase of a project
     * @return string
     */
    private function _renderFiles($sPhase) {
        $sReturn = '';
        $aFiles = $this->getBuildfilesByPlace($sPhase, 'ready2install');
        if (!$aFiles || !$aFiles['filecount']) {
            return '';
        }
        $sReturn.='<strong>' . t("filelist") . '</strong> (' . $aFiles['filecount'] . '):<br>';
        foreach ($aFiles['files'] as $sFilename => $aData) {
            $sReturn.='<div class="file file-' . $aData['type'] . ' fileext-' . $aData['extension'] . '" title="' . $sFilename . ' (' . $aData['type'] . ')">'
                    . $aData['icon'] . $sFilename
                    // . ' ('.$aData['type'].')'
                    . '</div>'
            ;
        }
        $sReturn.='(' . $aFiles['totalsize-hr'] . ')';
        return $sReturn;
    }

    /**
     * render html for a colored link to any project action
     * @param string $sFunction name of the action; one of accept|build|cleanup|deploy|new|overview|phase|rollback|setup
     * @param string $sPhase    current phase where to place the link
     * @return string
     */
    public function renderLink($sFunction, $sPhase = false, $sVersion = false) {
        $sFirst = $this->getNextPhase();
        $sNext = $this->getNextPhase($sPhase);
        $aLinkdata = array(
            'default' => array('class' => ''),
            'accept' => array('class' => $sNext,
                'hint' => sprintf(t("accept-hint"), $sPhase, $sNext),
                'label' => t('accept'),
            ),
            'build' => array('class' => $sFirst,
                'hint' => sprintf(t("build-hint"), $sFirst),
                'label' => t('build'),
                'role' => 'buildProject'
            ),
            'cleanup' => array('class' => ''),
            'deploy' => array('class' => $sPhase,
                'hint' => sprintf(t("deploy-hint"), $sPhase),
                'label' => t('deploy'),
            ),
            'new' => array(
                'hint' => t("new-project-hint"),
                'label' => t('new-project'),
            ),
            'overview' => array('class' => '',
                'hint' => t('menu-project-home') . ' [' . $this->getLabel() . ']',
                'label' => $this->getLabel()
            ),
            'phase' => array('icon' => $this->_oHtml->getIcon('phase'), 'class' => $sPhase,
                'hint' => sprintf(t('phase-details-hint'), $sPhase),
                'label' => t('phase-details')
            ),
            'rollback' => array('class' => $sPhase,
                'hint' => sprintf(t('rollback-hint'), $sPhase, $sVersion),
                'label' => t('rollback')
            ),
            'setup' => array('class' => $sPhase,
                'hint' => sprintf(t('setup-hint'), $sPhase, $sVersion),
                'label' => t('setup')
            ),
        );
        /*
          if (!$this->oUser->hasRole("project-action-$sFunction")){
          // $sClass .= ' disabled';
          // return '<span title="no permission [project-action-'.$sFunction.']">[ ]</span>';
          }
         * 
         */
        // fuer wen ist der Link
        $sRole = '';
        $sOnMouseover = '';
        $sOnMouseout = '';
        switch($sFunction){
            case 'accept';
                $sRole = 'developer';
                if ($sNext == "live") {
                    $sRole = 'pl';
                    // $aLinkdata[$sFunction]['icon']='glyphicon glyphicon-star';
                }
                $sOnMouseover = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');';
                $sOnMouseout = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');';
                break;
            case 'build';
                $sRole = 'developer';
                $sOnMouseover = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');';
                $sOnMouseout = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');';
                break;
            case 'deploy';
                $sRole = 'developer';
                $sOnMouseover = '$(\'.td-phase-' . $sPhase . '.td-place-ready2install.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');'
                        .'$(\'.td-phase-' . $sPhase . '.td-place-deployed.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');'
                        ;
                $sOnMouseout = '$(\'.td-phase-' . $sPhase . '.td-place-ready2install.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');'
                        .'$(\'.td-phase-' . $sPhase . '.td-place-deployed.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');'
                        ;
                break;
        }

        // $sClass = $sPhase;
        $sIconClass = (array_key_exists($sFunction, $aLinkdata)) ? $aLinkdata[$sFunction]['icon'] : $aLinkdata['default']['icon'];
        $sHint = (
                array_key_exists($sFunction, $aLinkdata) && array_key_exists("hint", $aLinkdata[$sFunction])
                ) ? $aLinkdata[$sFunction]['hint'] : "";
        $sLabel = (
                array_key_exists($sFunction, $aLinkdata) && array_key_exists("label", $aLinkdata[$sFunction])
                ) ? $aLinkdata[$sFunction]['label'] : $sFunction;
        $sClass = (
                array_key_exists($sFunction, $aLinkdata) && array_key_exists("class", $aLinkdata[$sFunction])
                ) ? $aLinkdata[$sFunction]['class'] : '';
        if ($sRole) {
            $sClass .= " role role" . $sRole;
        }

        $sLink = "/deployment/" . ($this->_aConfig["id"] ? $this->_aConfig["id"] : 'all/setup') . "/";
        if ($sFunction != "overview") {
            $sLink.="$sFunction/";
        }
        if ($sPhase) {
            $sLink.="$sPhase/";
        }
        if ($sVersion) {
            $sLink.="$sVersion/";
        }
        if (!$this->oUser->hasPermission("project-action-$sFunction")) {
            // $sClass .= ' disabled';
            return '<span title="no permission [project-action-' . $sFunction . '] for ' . $this->oUser->getUsername() . '">[ <i class="' . $sIconClass . '"></i> ' . $sLabel . ' ]</span>';
        }

        return $this->_oHtml->getLinkButton(array(
                    'href' => $sLink,
                    'title' => $sHint,
                    'class' => 'btn btn-default ' . $sClass,
                    'type' => $sFunction,
                    'onmouseover' => $sOnMouseover,
                    'onmouseout' => $sOnMouseout,
                    'label' => $sLabel,
        ));
        // return '<a href="' . $sLink . '" ' . $sOnMouseover . ' title="' . $sHint . '" class="btn  btn-default ' . $sClass . '"><i class="' . $sIconClass . '"></i> ' . $sLabel . '</a>';
    }

    /**
     * render html for the project overview; it shows the defined phases for 
     * the project as a table
     * @return type
     */
    public function renderPhaseInfo() {
        $sRow1 = false;
        $sRow2 = false;
        foreach ($this->getActivePhases() as $sPhase) {
            $sRow1.='<th class="' . $sPhase . '">' . $sPhase . '</th>';
            $sRow2.='<td class="' . $sPhase . '">'
                    . t('url') . ': <a href="' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '">' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '</a><br>'
                    . '<br>' . t('deploytimes') . ':<br>';
            if (count($this->_getDeploytimes($sPhase))) {
                $sRow2.=implode("<br>", $this->_getDeploytimes($sPhase));
            } else {
                $sRow2.=t('deploytimes-immediately');
            }
            $sRow2.='<br>' . $this->renderLink("phase", $sPhase)
                    . $this->_renderHosts($sPhase)
                    . '<br>'
                    . $this->_renderFiles($sPhase)
                    . '</td>';
        }
        return '<table><thead><tr>' . $sRow1 . '</tr></thead><tbody><tr>' . $sRow2 . '</tr></tbody></table>';
    }

    /**
     * render html for a place of a phase
     * @param string  $sPhase    phase
     * @param string  $sPlace    name of the place; one of onhold|ready2install|deployed
     * @param bool    $bActions  draw action links (deploy, accept) on/ off
     * @param bool    $bLong     use long variant to display infos? 
     * @return string|boolean
     */
    public function renderPhaseDetail($sPhase, $sPlace, $bActions = true, $bLong = true) {

        if (!$sPhase) {
            return false;
        }
        if (!$sPlace) {
            return false;
        }
        if (!$this->isActivePhase($sPhase)) {
            return false;
        }
        if (!array_key_exists($sPlace, $this->_aPlaces)) {
            return false;
        }

        $sReturn = false;
        $aDataPhase = $this->getPhaseInfos($sPhase);
        $aData = $aDataPhase[$sPlace];
        // foreach($aDataPhase[$sPlace] as $aData) {
        if (array_key_exists("ok", $aData) && array_key_exists("version", $aData)) {
            // TODO: getChecksumDiv anhand der Repo-Versionsnummer - dann kann man beim build auch die Farbe mit dem Repo HEAD vergleichen
            // time
            $sDateFormat = "d.m.Y H:i";
            $oPkgDate = date("U", strtotime($aData["date"]));
            /*
              $iAge=date("U")-$oPkgDate;
              $sAgeClass="";
              if ($iAge< 60*60*24*3){
              $sAgeClass="last1d";
              }
              if ($iAge< 60*60){
              $sAgeClass="last1h";
              }
             */

            if ($bLong) {
                // long display of the revision
                // $sJsonUrl = $this->_getInfofile($sPhase, $sPlace);
                    $sReturn .=$this->_getChecksumDiv(
                        $aData["revision"], 
                        $this->_oHtml->getIconByType('calendar') .' ' . date($sDateFormat, $oPkgDate)
                        . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aData["branch"] . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aData["revision"]) . '<br>'
                        . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ':<br>'
                    )
                    . '<pre>' . strip_tags($aData["message"], '<br>') . '</pre>'
                // . '<i class="glyphicon glyphicon-globe"></i> ' . t('url') . ': <a href="' . $sJsonUrl . '">' . $sJsonUrl . '</a><br>'
                ;
                if ($sPlace == "deployed" && array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase])) {
                    $sUrl = $this->_aPrjConfig["phases"][$sPhase]["url"];
                    $sReturn.=$this->_oHtml->getIconByType('link-extern') . ' '. t('url') . ': <a href="' . $sUrl . '">' . $sUrl . '</a><br>';
                }
            } else {
                $sReturn .= $this->_getChecksumDiv(
                            $aData["revision"], 
                            $this->_oHtml->getIconByType('calendar') .' ' . date($sDateFormat, $oPkgDate)
                    );
                if ($sPlace == "deployed" && array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase])) {
                    $sMore = $this->_oHtml->getIconByType('link-extern').' '
                            . t('url')
                            . ': <a href="' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '">' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '</a><br>';
                }

                $sReturn.=' ' . $this->renderInfoLink(
                                $aData, array(
                            'title' => $this->getLabel() . " :: $sPhase :: $sPlace",
                            'more' => $sMore,
                                )
                );
            }

            switch ($sPlace) {
                case "onhold":
                    if (array_key_exists("phases", $this->_aConfig) && array_key_exists($sPhase, $this->_aConfig["phases"])) {
                        // $sReturn .= print_r($this->_aConfig["phases"][$sPhase], true);
                        if (count($this->_getDeploytimes($sPhase))) {
                            $sReturn .= '<br>'.$this->_oHtml->getIcon('time').t('deploytimes') . ':<br>'
                                    . implode("<br>", array_values($this->_getDeploytimes($sPhase)))
                                    . '<br>';
                        }
                        if ($bActions) {
                            $sReturn .= ' ' . $this->renderLink("deploy", $sPhase);
                        }
                    }
                    break;

                case "ready2install":
                    break;

                case "deployed":
                    if ($bActions && $this->canAcceptPhase($sPhase)) {
                        $sReturn .= ' ' . $this->renderLink("accept", $sPhase);
                    }
                    break;
                default:
                    break;
            }
            // $this->_getChecksumDiv($aData["revision"])
        } else {
            if (array_key_exists("error", $aData)) {
                $sReturn.=''
                        . $this->renderInfoLink(array('error' => $aData["error"]), array())
                ;
            } else if (array_key_exists("warning", $aData)) {
                $sReturn.= '<div class="warning">'.$this->_oHtml->getIcon('sign-info'). t('warning') . ':<br>' . $aData["warning"] . '</div>';
            } else {
                return false;
                // $sReturn.= t('empty');
            }
        } // if
        // } // for
        return $sReturn;
    }

    /**
     * render html for a row with td for all places (first row)
     * @param string $sPhase  phase (just needed for coloring)
     * @return string
     */
    public function renderPlacesAsTd($sPhase) {
        $sRow1 = '';
        foreach (array_keys($this->_aPlaces) as $sPlace) {
            $sRow1.='<td class="' . $sPhase . ' ' . $this->_aConfig["id"] . ' tdphase">' . t($sPlace) . '</td>';
        }
        return $sRow1;
    }

    /**
     * render html for a row with td for all places of a phase
     * @param string $sPhase   phase
     * @param bool   $bActions draw action links (deploy, accept) on/ off
     * @param bool   $bLong    use long variant to display infos? 
     * @return string|boolean
     */
    public function renderAllPhaseDetails($sPhase, $bActions = true, $bLong = true) {
        if (!$sPhase) {
            return false;
        }
        if (!$this->isActivePhase($sPhase)) {
            return '
                        <td class="td-phase-' . $sPhase . ' td-phase-inactive ' . $this->_aConfig["id"] . '" colspan="' . count($this->_aPlaces) . '">
                            <div class="versioninfo center inactive">' . $this->_oHtml->getIcon('sign-info').t('inactive') . '</div>
                        </td>';
        }
        $sRow2 = false;

        $aRows = array();
        $sLastPlace = '';
        
        foreach (array_keys($this->_aPlaces) as $sPlace) {
            $aRows[$sPlace] = $this->renderPhaseDetail($sPhase, $sPlace, $bActions, $bLong);
            
            // generate ">>" sign for lastly generated td
            if ($sLastPlace && array_key_exists("version", $this->_aData["phases"][$sPhase][$sLastPlace]) 
                    && array_key_exists("version", $this->_aData["phases"][$sPhase][$sPlace]) 
                    && $this->_aData["phases"][$sPhase][$sLastPlace]["version"] == $this->_aData["phases"][$sPhase][$sPlace]["version"] 
                    && !$bLong
            ) {
                $aRows[$sLastPlace] = $this->_renderBar($sPhase, $sPlace) . "&raquo;";
            }
            $sLastPlace = $sPlace;
        }
        
        foreach (array_keys($this->_aPlaces) as $sPlace) {
            $sRow2.='<td class=" td-phase-'.$sPhase.' td-place-'.$sPlace.' td' . $this->_aConfig["id"] . '">' . $aRows[$sPlace] . '</td>';
        }
        return $sRow2;
    }

    /**
     * return html code for the installed version in the repository
     * @param boolean  $bRefresh  optional: refresh flag; default: use cached information
     * @return string
     */
    public function renderRepoInfo($bRefresh=false) {
        $sReturn = "";
        switch ($this->_aPrjConfig["build"]["type"]) {
            case "git":

                $aRepodata = $this->getRepoRevision($bRefresh);
                if (array_key_exists("revision", $aRepodata)) {
                    $sReturn.=$this->_getChecksumDiv($aRepodata["revision"],
                        $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . (array_key_exists("branch", $aRepodata) ? $aRepodata["branch"] : '-') . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aRepodata["revision"]) . '<br>'
                        . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ':<br>'
                        )
                        ."<pre>" . strip_tags($aRepodata["message"], '<br>') . "</pre>";
                } else {
                    $sReturn .= $this->_oHtml->getBox("error", sprintf(t('class-project-error-no-repoaccess'), $aRepodata["error"]))
                            . $this->renderLink("setup") . '<br>';
                }

                break;

            default:
                $sReturn .= $this->_oHtml->getBox("error", sprintf(t('class-project-error-wrong-buildtype'), $this->_aPrjConfig["build"]["type"]));
        }
        if (array_key_exists("url", $this->_aPrjConfig["build"])) {
            $sReturn.=t('repository-url') . ': ' . $this->_aPrjConfig["build"]["url"] . '<br>';
        }
        if (array_key_exists("webaccess", $this->_aPrjConfig["build"])) {
            $sReturn.=t('repository-access-browser') . ':<br><a href="' . $this->_aPrjConfig["build"]["webaccess"] . '">' . $this->_aPrjConfig["build"]["webaccess"] . '</a><br>';
        }
        return $sReturn;
    }

    /**
     * get html code for a link to the commit
     * (works for guithub and gitlab instances)
     * 
     * @param string $sRevision
     * @return string
     */
    public function _renderRevision($sRevision) {
        $sUrl = str_replace('/tree/master', '', $this->_aPrjConfig["build"]["webaccess"]) . '/commit/' . $sRevision;
        return '<a href="' . $sUrl . '">' . $sRevision . '</a>';
        return $sUrl;
    }

    /**
     * render html code for info link that shows popup with metadata on mouseover
     * @param array $aInfos   metainfos of the package (from json file)
     * @param array $aOptions options
     *                   title - tile in popover; default: empty
     *                   label - link label; default: empty (results in Infos|ERROR)
     *                   more  - additional infos in popover; default: empty
     *                   hpos  - horizontal position; one of left|right; default: right
     * @return string
     */
    public function renderInfoLink($aInfos, $aOptions = array()) {
        $sReturn = '';
        $bIsError = false;
        $this->_oHtml = new htmlguielements();

        $sInfos.='';
        if (array_key_exists("title", $aOptions) && $aOptions["title"]) {
            $sTitle.=$aOptions["title"];
        }
        if (array_key_exists("ok", $aInfos)) {
            $sLinktitle = t('infos');
            if (array_key_exists("message", $aInfos)) {
                $sInfos.=$this->_getChecksumDiv($aInfos["revision"],
                        $this->_oHtml->getIconByType('calendar') . t('build-from') . ' ' . date("d.m.Y H:i:s", strtotime($aInfos["date"])) . '<br>'
                        . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aInfos["branch"] . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aInfos["revision"]) . '<br>'
                        . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ': '
                        )
                        . '<pre>' . strip_tags($aInfos["message"], '<br>') . '</pre>';
                if (array_key_exists("more", $aOptions)) {
                    $sInfos.=$aOptions["more"];
                }
            }
        } else {
            $bIsError = true;
            if (!$sTitle) {
                $sTitle.=' ' . t('error');
            }
            $sLinktitle = t('error');
            $sInfos = $aInfos["error"];
        }
        $sInfos.=$this->_renderHostsData($aInfos);

        if (array_key_exists("label", $aOptions) && $aOptions["label"]) {
            $sLinktitle.=$aOptions["label"];
        }

        // render html
        $sId = 'info' . md5($sInfos);
        $sReturn = '<a href="#" class="btn ' . ($bIsError ? 'btn-danger' : 'btn-default') . '" title="" onclick="showIdAsModalMessage(\'' . $sId . '\'); return false;">'
                // . '<i class="fa fa-info"></i> '
                . $sLinktitle
                . '</a><div id="' . $sId . '" style="display: none;" ';
        if (array_key_exists("hpos", $aOptions)) {
            $sReturn.=' class="' . $aOptions["hpos"] . '"';
        }
        $sReturn.='>';
        if ($sTitle) {
            $sReturn.='<span class="title">' . $sTitle . '</span><br><br>';
        }

        $sReturn.=$sInfos . '</div>';

        if ($bIsError) {
            // $sReturn = '<div class="error">' . $sReturn . '</div>';
        }

        return $sReturn;
    }

    /**
     * return html code for a list of all built packages and their usage
     * @return string
     */
    public function renderVersionUsage() {
        $sReturn = false;
        $sRowHead1 = false;
        $sRowHead2 = '<td></td>';

        $aAllVersions = $this->_getVersionUsage();
        if (!count($aAllVersions)) {
            return $this->_oHtml->getBox("info", t('class-project-info-no-package'));
        }

        foreach ($this->getActivePhases() as $sPhase) {
            $sRowHead1.='<th class="' . $sPhase . '" colspan="' . (count($this->_aPlaces) + 1) . '">' . $sPhase . '</th>';
            $sRowHead2.='<td></td>' . $this->renderPlacesAsTd($sPhase);
        }
        
        krsort($aAllVersions);
        foreach ($aAllVersions as $sVersion => $aData) {
            $sReturn.='<tr>';

            $sInfos = $this->renderInfoLink($aData["info"], array('hpos' => 'left'));
            $sReturn.='<td>'
                        . $this->_getChecksumDiv(
                            $aData['info']['revision'],
                            $this->_oHtml->getIconByType('calendar') . t('build-from') . ': ' . $sVersion .'<br>'
                                . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aData['info']["branch"] . '<br>'
                                . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aData['info']["revision"]) . '<br>'
                          )
                    . '</td><td>'
                    . '&nbsp;&nbsp;' . $sInfos . '&nbsp;&nbsp;'
                    . '</td>'
                    ;

            foreach ($this->getActivePhases() as $sPhase) {
                $sTLine = '';
                $bCanRollback = $aData["rollback"][$sPhase];

                // $sReturn.=$aData["rollback"][$sPhase] ? '<td>'.$this->renderLink("rollback", $sPhase, $sVersion).'</td>' : '<td>Rollback NOT possible</td>';
                // $sReturn.=$aData["rollback"][$sPhase] ? '<td> Y </td>' : '<td> N </td>';
                $sReturn.='<td>  </td>';

                foreach (array_keys($this->_aPlaces) as $sPlace) {
                    $bFound = false;
                    $sReturn.=$aData["usage"][$sPhase][$sPlace] 
                            ? '<td class="' . $sPhase . '" style="text-align: center;">'
                            . $this->_getChecksumDiv($aData['info']['revision'], 'X')
                            . '</td>' 
                            : '<td> </td>'
                        ;
                }
            }
            $sReturn.='</tr>';
        }
        $sReturn = t('class-project-info-table-packages') . '<br><br>'
                . '<table>'
                . '<thead><tr><td>Version</td><td></td>'
                . $sRowHead1
                . '</tr><tr><td>'
                . $sRowHead2
                . '</tr></thead>'
                . '<tbody>'
                . $sReturn
                . '</tbody>'
                . '</table>';
        return $sReturn;
    }

    /**
     * render graphical overview of process (in project overview)
     * @return string
     */
    public function renderVisual() {
        $sReturn = '';
        $sContinue = '<span style="font-size: 300%; color:#ace;">&raquo;&raquo;</span><br><br>';


        $sRepoBar = '';
        $aRepodata = $this->getRepoRevision();
        if (array_key_exists("revision", $aRepodata)) {
            $sRepoBar = $this->_getChecksumDiv($aRepodata["revision"]);
        } else {
            $sRepoBar = '<span class="error">' . t("error") . '</span>';
        }

        $sPackagebar = '';
        $aVersions = $this->_getVersionUsage();
        foreach ($aVersions as $sVersion => $aData) {
            $sBar = $aData["info"]["revision"] ? $this->_getChecksumDiv($aData["info"]["revision"]) : '';
            $sPackagebar.='<span title="' . $sVersion . '" style="float: left; background:#eee; height: 3px; width:' . (100 / count($aVersions)) . '%">' . $sBar . '&nbsp;</span>';
        }

        $sPhaseImg = '';
        $sLastPhase = '';
        foreach ($this->getActivePhases() as $sPhase) {
            if ($sPhaseImg) {
                $sAction = $sContinue;
                if ($this->canAcceptPhase($sLastPhase)) {
                    $sAction .= $this->renderLink("accept", $sLastPhase);
                }
                $sPhaseImg.='<div class="action">' . $sAction . '</div>';
            }
            $sLastPhase = $sPhase;

            $sFullbar = '';
            foreach (array_keys($this->_aPlaces) as $sPlace) {
                $sFullbar.='<span title="' . $this->_aPlaces[$sPlace] . '" style="float: left; background:#eee; height: 3px; width:' . (100 / count($this->_aPlaces)) . '%">' . $this->_renderBar($sPhase, $sPlace) . '&nbsp;</span>';
            }
            // $sDetail = $sFullbar . '<br><a href="#h3phases" class="scroll-link">' . $sPhase . '</a>';
            $sDetail = $sFullbar . '<br>' . $sPhase;

            $sPhaseImg.='
            <div class="process ' . $sPhase . '">
                <div class="details">' . $sDetail . ' </div>
                <div><img src="/deployment/images/process/bg_phase.png" alt="' . t("phase") . ' ' . $sPhase . '"></div>
            </div>';
        }
        $sReturn = '
        <div class="visualprocess">
            <div class="process box">
                <div class="title">' . $this->_oHtml->getIcon('repository') . t("versioncontrol") . '</div>
                <div class="details">
                    ' . $sRepoBar . '<br>
                    <!--
                    <a href="#h3repo" class="scroll-link">' . t("repositoryinfos") . '</a><br>
                    -->
                    ' . t("repositoryinfos") . '<br>
                    <strong>
                    ' . $this->_aPrjConfig["build"]["type"] . '</strong> ' . preg_replace('/.*\@(.*):.*/', '($1)', $this->_aPrjConfig["build"]["url"])
                . ': <strong title="' . t('branch-select') . '">' . count($this->getRemoteBranches()) . '</strong>'
                . '<br>
                </div>
                <div>
                    <img src="/deployment/images/process/bg_vcs.png" alt="' . t("versioncontrol") . '">
                </div>
            </div>
            
            <div class="process">
                <div class="title">&nbsp;</div>
                <div class="action">' . $sContinue . t("build-hint-overview") . '<br><br>' . ($this->canAcceptPhase() ? $this->renderLink("build") : '') . '</div>
            </div>

            
            <div class="process box">
                <div class="title">' . $this->_oHtml->getIcon('package') . t("archive") . '</div>
                <div class="details">
                    ' . $sPackagebar . '<br>
                    <!--
                    <a href="#h3versions" class="scroll-link">' . t("packages") . '</a><br>
                    -->
                    ' . t("packages") . '<br>
                    (<strong>' . count($this->_getVersionUsage()) . '</strong>)
                </div>
                <div><img src="/deployment/images/process/bg_archive.png" alt="' . t("archive") . '"></div>
            </div>
            
            <div class="process">
                <div class="title">&nbsp;</div>
                <div class="action">'.$sContinue . sprintf(t("queue-hint-overview"), $this->getNextPhase()).'</div>
            </div>
            
            <div class="process phases box">
                <div class="title">'  . $this->_oHtml->getIcon('phase') . t("phases") . '</div>
                ' . ($sPhaseImg ? $sPhaseImg : '<div class="process">' . t("none") . '</div>') . '
            </div>
        </div>
        ';

        return $sReturn;
    }

    /**
     * return html code for the setup form of an exsiting project
     * @return string
     */
    public function renderProjectSetup() {
        if (!$this->oUser->hasPermission("project-action-setup")) {
            return $this->oUser->showDenied();
        }
        $sMessages = '';
        require_once ("formgen.class.php");

        
        $aSelectSlack = array(
                'type' => 'hidden',
                'name' => 'messenger[slack]',
                'value' => false,
        );
        if (
                array_key_exists('messenger', $this->_aConfig)
                && array_key_exists('slack', $this->_aConfig['messenger'])
                && array_key_exists('presets', $this->_aConfig['messenger']['slack'])
                && count(array_key_exists('presets', $this->_aConfig['messenger']['slack']['presets']))
        ) {
            $aSelectSlack = array(
                'type' => 'select',
                'name' => 'messenger[slack]',
                'label' => t("messenger-slack"),
                'options' => array(
                    OPTION_NONE => array(
                        'label' => t('none'),
                    ),
                    '' => array(
                        'label' => '- - - - - - - - - - - - - - - - - - - - ',
                    ),
                ),
            );
            foreach($this->_aConfig['messenger']['slack']['presets'] as $sSlackUrl=>$aSlackCfg){
                $bActive=$this->_aPrjConfig['messenger']['slack'] === $sSlackUrl;
                $aSelectSlack['options'][$sSlackUrl] = array(
                    'label' => array_key_exists('label', $aSlackCfg) ? $aSlackCfg['label'] : $sSlackUrl,
                    'selected' => $bActive ? 'selected' : false,
                );
            }
            
        }
        
        $aForemanHostgroups = false;
        $iForemanHostgroupDefault = false;
        $sForemanHostgroupDefault = false;
        if (array_key_exists('foreman', $this->_aConfig)) {
            // echo '<pre>' . print_r($this->_aPrjConfig, 1) . '</pre>';
            $iForemanHostgroupDefault = (int) $this->_aPrjConfig['deploy']['foreman']['hostgroup'];
            require_once('foremanapi.class.php');
            $oForeman = new ForemanApi($this->_aConfig['foreman']);
            // $oForeman->setDebug(1);
            // $oForeman->selfcheck(); die(__FUNCTION__);

            $aForemanHostgroups = $oForeman->read(array(
                'request' => array(
                    array('hostgroups'),
                // array('operatingsystems',4),
                ),
                'response' => array(
                    'id', 'title'
                ),
            ));
            $aSelectForemanGroups = array(
                'type' => 'select',
                'name' => 'deploy[foreman][hostgroup]',
                'label' => $this->_oHtml->getIcon('foreman') . t("foreman-hostgroup"),
                'options' => array(
                    OPTION_NONE => array(
                        'label' => t('none'),
                    ),
                    '' => array(
                        'label' => '- - - - - - - - - - - - - - - - - - - - ',
                    ),
                ),
            );
            if (count($aForemanHostgroups)) {
                foreach ($aForemanHostgroups as $aItem) {
                    $bActive=$iForemanHostgroupDefault === (int) $aItem['id'];
                    $aSelectForemanGroups['options'][$aItem['id']] = array(
                        'label' => $aItem['title'],
                        'selected' => $bActive ? 'selected' : false,
                    );
                    $sForemanHostgroupDefault = $bActive ? $aItem['title'] : $sForemanHostgroupDefault;
                }
            }
        }


        $i = 0;

        $aPrefixItem = count($this->getVersions()) ?
                array(
            'type' => 'markup',
            'value' => '<div class="form-group">
                        <label class="col-sm-2">' . t('fileprefix') . '</label>
                        <div class="col-sm-10">
                            <input id="inputprefix" type="hidden" name="fileprefix" value="' . $this->_aPrjConfig["fileprefix"] . '">
                            ' . $this->_aPrjConfig["fileprefix"] . '
                        </div></div>
                            ',
                ) : array(
            'type' => 'text',
            'name' => 'fileprefix',
            // 'disabled' => 'disabled',
            'label' => t('fileprefix-label'),
            'value' => $this->_aPrjConfig["fileprefix"],
            'required' => 'required',
            'validate' => 'isastring',
            'pattern' => '[a-z0-9\-\_]*',
            'size' => 100,
            'placeholder' => '',
        );

        $aRepodata = $this->getRepoRevision();
        if (is_array($aRepodata) && array_key_exists("message", $aRepodata)) {
            $sRepoCheck = '<span class="ok">' . t('class-project-info-repoaccess') . '</span>';
        } else {
            $sRepoCheck = '<span class="error">' . sprintf(t('class-project-error-no-repoaccess'), $aRepodata["error"]) . '</span>';
            $sMessages.=$this->_oHtml->getBox("error", sprintf(t('class-project-error-no-repoaccess'), $aRepodata["error"]));
        }

        // generate datalist with exisating ssh keys for auth field
        $sAuthListitems = '';
        foreach ($this->_getSshKeys() as $sKey) {
            $sAuthListitems.='<option value="' . $sKey . '">';
        }
        $aForms = array(
            'setup' => array(
                'meta' => array(
                    'method' => 'POST',
                    'action' => '?',
                ),
                'validate' => array(),
                'form' => array(
                    'input' . $i++ => array(
                        'type' => 'hidden',
                        'name' => 'setupaction',
                        'value' => 'save',
                    ),
                    'input' . $i++ => array(
                        'type' => 'hidden',
                        'name' => 'id',
                        'value' => $this->_aConfig["id"],
                    ),
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<div class="tabbable">
                            <ul class="nav nav-tabs">
                                <li class="active"><a href="#tab1" data-toggle="tab">' . $this->_oHtml->getIcon('list').t('setup-metadata') . '</a></li>
                                <li><a href="#tab2" data-toggle="tab">' . $this->_oHtml->getIcon('repository').t('repositoryinfos') . '</a></li>

                                <li><a href="#tab3" data-toggle="tab">' . $this->_oHtml->getIcon('deploy-configfile').t('deploy-configfile') . '</a></li>
                                <li><a href="#tab4" data-toggle="tab">' . $this->_oHtml->getIcon('phase').t('phases') . '</a></li>
                                <li><a href="#tab5" data-toggle="tab">' . $this->_oHtml->getIcon('raw-data').t('raw-data') . '</a></li>
                            </ul>
                            <div class="tab-content">
                            <div class="tab-pane active" id="tab1">
                            
                            ',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'label',
                        'label' => t('projectname'),
                        'value' => $this->_aPrjConfig["label"],
                        'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => 'Projekt',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'description',
                        'label' => t('projectdescription'),
                        'value' => $this->_aPrjConfig["description"],
                        'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'contact',
                        'label' => t('contact'),
                        'value' => $this->_aPrjConfig["contact"],
                        'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<p>' . t('messenger') . '</p>',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'messenger[email]',
                        'label' => t("messenger-email"),
                        'value' => $this->_aPrjConfig["messenger"]["email"],
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    
                    'input' . $i++ => $aSelectSlack,
                    // --------------------------------------------------
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => ' </div><div class="tab-pane" id="tab2">
                            <p>' . t('setup-hint-build') . '</p>',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'build[type]',
                        'label' => t("build-type"),
                        'value' => $this->_aPrjConfig["build"]["type"],
                        'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'build[url]',
                        'label' => t("repository-url"),
                        'value' => $this->_aPrjConfig["build"]["url"],
                        // 'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'build[auth]',
                        'label' => t("repository-auth"),
                        'value' => $this->_aPrjConfig["build"]["auth"],
                        // 'required' => 'required',
                        'list' => 'listauth', // listauth is the next form id below
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<datalist id="listauth">' . $sAuthListitems . '</datalist>',
                    ),
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<div class="form-group">'
                        . '<label class="col-sm-2"> </label><div class="col-sm-10">'
                        . $sRepoCheck
                        . '</div></div>',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'build[webaccess]',
                        'label' => t("repository-urlwebgui"),
                        'value' => $this->_aPrjConfig["build"]["webaccess"],
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),
                    'input' . $i++ => $aPrefixItem,
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<div style="clear: both"></div>',
                    ),
                    // task#1498 - handle project without "public" directory
                    'input' . $i++ => array(
                        'type' => 'checkbox',
                        'name' => 'build[haspublic]',
                        'label' => t("repository-has-public-dir"),
                        'required' => false,
                        'validate' => 'isastring',
                        'options' => array(
                            '1' => array(
                                'label' => t("yes"),
                                'checked' => (array_key_exists('haspublic', $this->_aPrjConfig["build"]) ? $this->_aPrjConfig["build"]["haspublic"] : 0),
                            ),
                        ),
                    ),
                    // --------------------------------------------------
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => ' </div><div class="tab-pane" id="tab3">
                            <p>' . t('deploy-configfile-hint') . '</p>',
                    ),
                    'textarea' . $i++ => array(
                        'type' => 'textarea',
                        'name' => 'deploy[configfile]',
                        'label' => t("deploy-configfile"),
                        'value' => $this->_aPrjConfig['deploy']["configfile"],
                        // 'required' => 'required',
                        'validate' => 'isastring',  
                        'cols' => 100,
                        'rows' => 10,
                        'placeholder' => 'export myvariable=&quot;hello world&quot;',
                    ),
   
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'api[secret]',
                        'label' => t("api-secret"),
                        'value' => $this->_aPrjConfig["api"]["secret"],
                        'validate' => 'isastring',
                        'size' => 100,
                        'placeholder' => '',
                    ),                    
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => '<div class="col-sm-12">'
                        . '<p>' . t('api-secret-hint') . '<br>'
                            . '<a href="#" class="btn btn-default" onclick="$(\'#input'.($i-2).'\').val(generateSecret(64)); return false">'.t("api-secret-generate").'</a>'
                        . '</p></div>',
                    ),
                    
                    // --------------------------------------------------
                    'input' . $i++ => array(
                        'type' => 'markup',
                        'value' => ' </div><div class="tab-pane" id="tab4">
                            <p>' . sprintf(t("class-project-info-setup-phaseinfos"), $this->getNextPhase()) . '</p>',
                    ),
                ),
            ),
        );
        if ($aSelectForemanGroups) {
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => '<strong>'.t("defaults-all-phases").'</strong><br><br>',
            );
            $aForms["setup"]["form"]['input' . $i++] = $aSelectForemanGroups;
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => '<br><br>',
            );
        }
        foreach (array_keys($this->getPhases()) as $sPhase) {

            $bActivePhase = $this->isActivePhase($sPhase);
            $sUrl = array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["url"] : "";
            $sDeploymethod = array_key_exists("deploymethod", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["deploymethod"] : "";
            $sDeployhosts = array_key_exists("hosts", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["hosts"] : "";

            /*
             * task-1847 - reove adding ssh key
            if($sDeployhosts){
                echo "$sDeployhosts<br>";
                if(!strpos($sDeployhosts, ",")){
                    $sCmd=sprintf($this->_aConfig["installPackages"]["addkeycommand"], $sDeployhosts, $sDeployhosts);
                    exec($sCmd . " 2>&1", $aOut);
                    echo "<pre>\$ $sCmd<br>"
                        . implode('<br>', $aOut)
                        ."</pre>"
                        ;
                }
            }
             */
            $sDeploytimes = array_key_exists("deploytimes", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["deploytimes"] : "";
            $sDivId4PhaseSettings = 'divSettings' . $sPhase;
            $sDivId4TargetHosts = 'divSettings' . $sPhase . 'hosts';

            if ($aSelectForemanGroups) {
                $iForemanHostgroup = (int) $this->_aPrjConfig['phases'][$sPhase]['foreman-hostgroup'];
                $aSelectForemanHostGroup = array(
                    'type' => 'select',
                    'name' => 'phases[' . $sPhase . '][foreman-hostgroup]',
                    'label' => $this->_oHtml->getIcon('foreman') . t("foreman-hostgroup"),
                    'options' => array(
                        OPTION_DEFAULT => array(
                            'label' => t('default') . ' (' . $sForemanHostgroupDefault . ')',
                            'selected' => $iForemanHostgroup === OPTION_DEFAULT ? 'selected' : false,
                        ),
                        OPTION_NONE => array(
                            'label' => t('none'),
                            'selected' => $iForemanHostgroup === OPTION_NONE ? 'selected' : false,
                        ),
                        '' => array(
                            'label' => '- - - - - - - - - - - - - - - - - - - - ',
                        ),
                    ),
                );
                if (count($aForemanHostgroups)) {
                    foreach ($aForemanHostgroups as $aItem) {
                        $aSelectForemanHostGroup['options'][$aItem['id']] = array(
                            'label' => $aItem['title'],
                            'selected' => ($iForemanHostgroup === $aItem['id']) ? 'selected' : false,
                        );
                    }
                }
            }
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => ''
                // .'<pre>'.print_r($this->_aPrjConfig["phases"][$sPhase], 1).'</pre>'
                /*
                  . '<a class="'.$sPhase.'">'
                  . t("phase") . ' ' . $sPhase
                  . '</a>'
                 */
                . '<table class="table">'
                . '<tbody>'
                . '<tr><th class="' . $sPhase . '">' . $this->_oHtml->getIcon('phase') . t("phase") . ' ' . $sPhase . '</th></tr>'
                . '<tr><td class="' . ($bActivePhase ? $sPhase : '') . '">'
                . ''
            );

            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'checkbox',
                'name' => 'phases[' . $sPhase . '][active]',
                'label' => t("phase-is-active"),
                // 'value' => $bUsePuppet,
                'required' => false,
                'validate' => 'isastring',
                // 'size' => 100,
                // 'placeholder' => '...',
                'options' => array(
                    '1' => array(
                        'label' => t("yes"),
                        'checked' => $bActivePhase,
                        'onclick' => '$(\'#' . $sDivId4PhaseSettings . '\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                    ),
                ),
            );
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => ''
                . '<div id="' . $sDivId4PhaseSettings . '" ' . ($bActivePhase ? '' : ' style="display: none;"') . '">'
            );
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'text',
                'name' => 'phases[' . $sPhase . '][url]',
                'label' => $this->_oHtml->getIcon('url') . t("url-project-website"),
                'value' => $sUrl,
                // 'required' => 'required',
                'validate' => 'isastring',
                'size' => 100,
                'placeholder' => 'http://' . $sPhase . '.[' . t("project") . '].[...]/',
            );
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'radio',
                'name' => 'phases[' . $sPhase . '][deploymethod]',
                'label' => $this->_oHtml->getIcon('method') . t("deploymethod"),
                // 'value' => $bUsePuppet,
                // 'required' => 'required',
                'validate' => 'isastring',
                // 'size' => 100,
                // 'placeholder' => '...',
                'options' => array(
                    'none' => array(
                        'label' => t("deploymethod-none"),
                        'checked' => $sDeploymethod === "none",
                        'onclick' => '$(\'#' . $sDivId4TargetHosts . '\').css(\'display\', (this.checked ? \'none\' : \'block\') )',
                    ),
                    'puppet' => array(
                        'label' => t("deploymethod-puppet"),
                        'checked' => $sDeploymethod === "puppet",
                        'onclick' => '$(\'#' . $sDivId4TargetHosts . '\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                    ),
                /*
                 * see deploy method to handle an action
                  'sshproxy' => array(
                  'label' => t("deploymethod-sshproxy"),
                  'checked' => $sDeploymethod==="sshproxy",
                  'onclick' => '$(\'#'.$sDivId4TargetHosts.'\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                  ),
                 */
                ),
            );
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => ''
                . '<div id="' . $sDivId4TargetHosts . '" ' . ($sDeploymethod !== "none" ? '' : ' style="display: none;"') . '">'
            );
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'text',
                'name' => 'phases[' . $sPhase . '][hosts]',
                'label' => $this->_oHtml->getIcon('host') . t("phase-targethosts"),
                'value' => $sDeployhosts,
                // 'required' => 'required',
                'validate' => 'isastring',
                'size' => 100,
                'placeholder' => 'FQDN1,FQDN2',
            );

            /*
              if ($sPuppethost) {

              // add ssh host key
              $sOut0 = shell_exec(sprintf($this->_aConfig["installPackages"]["addkeycommand"], $sPuppethost, $sPuppethost));

              $sCmd2 = 'ssh ' . $this->_aConfig["installPackages"]["user"]
              . '@' . $sPuppethost
              . ' ' . $this->_aConfig["installPackages"]["testcommand"];
              $sOut = 'skip';
              // $sOut = shell_exec($sCmd2);
              // Check auf Versionsnummer - mehr als n Zeichen ist mutmasslich eine Fehlermeldung
              if (strlen($sOut) > 7) {
              $sMessages.=$this->getBox("error", sprintf(t("class-project-error-setup-sudo-pupet-agent-failed"), $sPhase, $sCmd, $sOut));
              $sOut = '<span class="error" title="' . $sCmd . '">' . $sOut . '</span>';
              } else {
              $sOut = '<span class="ok">' . sprintf(t("class-project-info-setup-ssh-and-puppet-ok"), $sPuppethost) . '</span>';
              }
              $aForms["setup"]["form"]['input' . $i++] = array(
              'type' => 'markup',
              'value' => '<div class="form-group">'
              . '<label class="col-sm-2"> </label><div class="col-sm-10">'
              . $sOut
              . '</div></div>',
              );
              }
             */

            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => ''
                . '</div>'
            );
            // when to deploy
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'text',
                'name' => 'phases[' . $sPhase . '][deploytimes]',
                'label' => $this->_oHtml->getIcon('time') . t("deploytimes"),
                'value' => $sDeploytimes,
                // 'required' => 'required',
                'validate' => 'isastring',
                'size' => 100,
                'placeholder' => implode(", ", $this->_aConfig["phases"][$sPhase]["deploytimes"]),
            );

            if ($aSelectForemanGroups) {
                $aForms["setup"]["form"]['input' . $i++] = $aSelectForemanHostGroup;
            }

            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => ''
                . '</div>'
            ); // close div for active phase


            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => '</td></tr></tbody></table>',
            );
        } // END: loop over phases
        $aForms["setup"]["form"]['input' . $i++] = array(
            'type' => 'markup',
            'value' => '</div>'
            
                . '<div class="tab-pane" id="tab5">'
                . '<br><pre>'.print_r($this->_aPrjConfig, 1).'</pre>'
                . '</div>'
            
            . '</div>'
            . '</div>'
            . '<div style="clear: both; margin-bottom: 1em;"></div>'
            
            
            . '<hr>',
        );
        $aForms["setup"]["form"]['input' . $i++] = array(
            'type' => 'submit',
            'name' => 'btnsave',
            'label' => t("save"),
            'value' => $this->_oHtml->getIcon('sign-ok').t("save"),
        );

        $oForm = new formgen($aForms);
        return $sMessages . $oForm->renderHtml("setup");
    }

    /**
     * return html code for the setup form for a new project
     * @return string
     */
    public function renderNewProject() {
        global $aParams;
        if (!$this->oUser->hasPermission("project-action-create")) {
            return $this->oUser->showDenied();
        }

        require_once ("formgen.class.php");
        $i = 0;
        $sID = array_key_exists("id", $aParams) ? $aParams["id"] : "";

        $aForms = array(
            'setup' => array(
                'meta' => array(
                    'method' => 'POST',
                    'action' => '?',
                ),
                'validate' => array(),
                'form' => array(
                    'input' . $i++ => array(
                        'type' => 'hidden',
                        'name' => 'setupaction',
                        'value' => 'create',
                    ),
                    'input' . $i++ => array(
                        'type' => 'text',
                        'name' => 'id',
                        'label' => t("class-project-info-setup-projectId"),
                        'value' => $sID,
                        'required' => 'required',
                        'validate' => 'isastring',
                        'size' => 100,
                        'pattern' => '[a-z0-9\-\_]*',
                        'placeholder' => t("class-project-info-setup-projectId-placeholder"),
                    ),
                ),
            ),
        );
        $aForms["setup"]["form"]['input' . $i++] = array(
            'type' => 'submit',
            'name' => 'btnsave',
            'label' => t("save"),
            'value' => $this->_oHtml->getIcon('sign-ok') . t("save"),
        );

        $oForm = new formgen($aForms);
        return $oForm->renderHtml("setup");
    }

}