Skip to content
Snippets Groups Projects
Select Git revision
  • a3e648d288f1fd561c5703804b55d0af97d4c0a5
  • master default protected
  • update-docs
3 results

20_Usage.md

Blame
  • project.class.php 173.25 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';
    
    // plugins
    // require_once 'plugins.class.php';
    require_once 'build_base.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 (isset($this->_aConfig['messenger']['slack']['presets'][$sSlack][$sKey])){
                        $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 (!is_array($this->_aPrjConfig) || !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 = '';
                }
            }
    
            // echo "DEBUG: ".__METHOD__."($sPhase, $sPlace) --> $sBase<br>";
            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 (without file extension)
         * @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"] : 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':
                    case 'zip':
                        $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 the group id of the project
         * @return string
         */
        public function getProjectGroup() {
            return isset($this->_aPrjConfig["projectgroup"]) && $this->_aPrjConfig["projectgroup"]!='-1' ? $this->_aPrjConfig["projectgroup"] : false;
        }
        /**
         * get the group label of the project
         * @return string
         */
        public function getProjectGroupLabel() {
            $sGroupid=$this->getProjectGroup();
            return isset($this->_aConfig["projectgroups"][$sGroupid]) ? $this->_aConfig["projectgroups"][$sGroupid] : false;
        }
    
        /**
         * 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__ . " sorry, Method 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
         * It build up a subkey "progress" with info if a build is queued
         * or an installation of a new package is going on
         * @return array
         */
        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 (
                        $sPlace!=='onhold'
                        && isset($aDataPhase[$sPlace]['version'])
                    ) {
                        if($bFirstVersion && !$bHasDifferentVersions && $bFirstVersion!==$aDataPhase[$sPlace]['version']){
                            $bHasDifferentVersions=true;
                        }
                        if (!$bFirstVersion){
                            $bFirstVersion = $aDataPhase[$sPlace]['version'];
                        }
                    }
                }
                // check queue
                if (!$bHasQueue && isset($aDataPhase['onhold']['version']) && $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;
            }
            // echo '<pre>'.print_r($this->_aData["phases"][$sPhase], 1).'</pre>'.__METHOD__.'<br>';
            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 (
                    $this->_aPrjConfig && isset($this->_aPrjConfig["phases"][$sPhase]["active"][0])
                        ? $this->_aPrjConfig["phases"][$sPhase]["active"][0] 
                        : false
                    );
        }
    
        /**
         * return array of all (active and inactive) phases
         * @return array
         */
        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 '<span class="btn" title="no permission [project-action-accept] for user [' . $this->oUser->getUsername() . ']">' . $sPhase . '</span>';
            }
    
            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 bool $bIgnoreCache  flag to ignore exiting cached data
         * @return string|boolean
         */
        public function getRemoteBranches($bIgnoreCache=false) {
            $this->log(__FUNCTION__ . "($bIgnoreCache) 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($bIgnoreCache);
            }
            return false;
        }
    
        /**
          }
         * get html form with selectr for remote branches
         * @param string $sActiveBranchname  force active branch name
         * @param bool $bIgnoreCache  flag to ignore exiting cached data
         * @return string
         */
        public function renderSelectRemoteBranches($sActiveBranchname = false, $bIgnoreCache=false) {
            $this->log(__FUNCTION__."(sActiveBranchname = $sActiveBranchname, bIgnoreCache = ".($bIgnoreCache ? 'true' : 'false').") start");
            $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($bIgnoreCache) 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"][$sSection] as $sPluginName=>$aItem) {
                    $aReturn[$sPluginName] = $aItem;
                }
            }
            return $aReturn;
        }
        
        /**
         * get a location of a plugin file with full path
         * @param string  $sType         type of plugin, i.e. "rollout"
         * @param string  $sPluginName   Name of plugin
         * @return string
         */
        protected function _getPluginFilename($sType, $sPluginName){
            return __DIR__.'/../plugins/'.$sType.'/'.$sPluginName.'/'.$sType.'_'.$sPluginName.'.php';
        }
        
        /**
         * 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 (isset($this->_aConfig['projects']['json']['active']) && $this->_aConfig['projects']['json']['active']) {
                $this->_aPrjConfig = json_decode(file_get_contents($this->_getConfigFile($sId)), true);
            }
            if (isset($this->_aConfig['projects']['ldap']['active']) && $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']['enabled_rollout_plugin']) && $this->_aPrjConfig['deploy']['enabled_rollout_plugin']) 
                    ? $this->_aPrjConfig['deploy']['enabled_rollout_plugin']
                    : 'default'
                ;
            $this->oRolloutPlugin = false;
            try{
                require_once $this->_getPluginFilename('rollout', $sPluginName);
                $sPluginClassname='rollout_'.$sPluginName;
                $this->oRolloutPlugin = new $sPluginClassname(array(
                    'lang'=>$this->_aConfig['lang'],
                    'phase'=>false,
                    'globalcfg'=>isset($this->_aConfig['plugins']['rollout'][$sPluginName]) ? $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"]){
                            
                # task#5047 - FIX EOL
                $sCfgContent=$this->_aPrjConfig['deploy']["configfile"];
                // detect unix, linux, mac
                if (DIRECTORY_SEPARATOR === '/') {
                    $sCfgContent=str_replace("\r\n", "\n", $sCfgContent);
                }
                # /task#5047
                
                file_put_contents($sCfgout, $sCfgContent);
                $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);
    
            // ----- loop over enabled build plugins
            // WIP
            // set name of the activated plugin for this project
            $aPlugins=(isset($this->_aPrjConfig['build']['enabled_build_plugins']) && $this->_aPrjConfig['build']['enabled_build_plugins']) 
                    ? $this->_aPrjConfig['build']['enabled_build_plugins']
                    : ['tgz']
                ;
            foreach($aPlugins as $sPluginName){
                $oPlugin = false;
                $sReturn.='<h4>'.$sPluginName.'</h4>';
                try{
                    include_once $this->_getPluginFilename('build', $sPluginName);
                    $sPluginClassname='build_'.$sPluginName;
                    $oPlugin = new $sPluginClassname(array(
                        'lang'=>$this->_aConfig['lang'],
                        'workdir'=>$sTempBuildDir,
                        'outfile'=>$sPackageFileArchiv,
                    ));
                } catch (Exception $ex) {
                    return $this->_oHtml->getBox("error", 
                        "FAILED to initialize build plugin " .$sPluginName .'<br>'
                        . $sReturn
                    );                
                }
                
                $sReturn.=sprintf(t("creating-file"), $oPlugin->getOutfile()) . "<br>";
                foreach($oPlugin->checkRequirements() as $sCommand){
                    $sReturn.=$this->_execAndSend($sCommand);
                    $this->_TempFill($sReturn, $aActionList);
                }
                
                foreach($oPlugin->getBuildCommands() as $sCommand){
                    $sReturn.=$this->_execAndSend($sCommand);
                    $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 -rLvt  $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") {
                $sReturn.=t("class-project-info-deploy-start-by-method-skip") . "<br>";
            } else {
                
                $sReturn.='<p>Plugin: '.$this->oRolloutPlugin->getId().'</p>';
    
                foreach($this->oRolloutPlugin->getDeployCommands($sPhase) as $sCmd){
                    $sReturn.=$this->_execAndSend("$sCmd");
                }
    
                /*
                $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>";
            
            if (!$this->_iRcAll == 0) {
                $sWarnlevel='error';
                $sMessage = sprintf(t('class-project-info-deploy-failed'), $sPhase);
            } else {
                $sWarnlevel='success';
                $sMessage=sprintf(t("class-project-info-deploy-successful"), $sPhase);
            }
            $sReturn.=$this->_oHtml->getBox($sWarnlevel, $sMessage);
            $this->_sendMessage($sMessage);
            $this->_logaction(t('finished') . " deploy($sPhase, $bIgnoreDeploytimes) " . $sMessage, __FUNCTION__, $sWarnlevel);
            
            $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, JSON_PRETTY_PRINT));
                $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 class="btn disabled btn-default" title="no permission [project-action-' . $sFunction . '] for user [' . $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='';
            $sTitle='';
            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>';
    
            $aBranches=$this->getRemoteBranches();
            if(!is_array($aBranches)){
                return t("project-setup-incomplete");
            }
    
            $sRepoBar = '';
            /*
                Speedup:
                
            $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>
                            ' . $this->_aPrjConfig["build"]["type"] . '</strong> ' . preg_replace('/.*\@(.*):.*/', '($1)', $this->_aPrjConfig["build"]["url"])
                            . ': <strong title="' . t('branch-select') . '">' . count($aBranches) . '</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");
    
    
                $aSelectProjectGroup = array(
                    'type' => 'select',
                    'name' => 'projectgroup',
                    'label' => t("projectgroup"),
                    'options' => array(
                        OPTION_NONE => array(
                            'label' => t('none'),
                        ),
                        '' => array(
                            'label' => '- - - - - - - - - - - - - - - - - - - - ',
                        ),
                    ),
                );
                foreach($this->_aConfig['projectgroups'] as $sGroupid=>$sGroupLabel){
                    $bActive=$this->getProjectGroup() === $sGroupid;
                    $aSelectProjectGroup['options'][$sGroupid] = array(
                        'label' => $sGroupLabel,
                        'selected' => $bActive ? 'selected' : false,
                    );
                }
                
            $aSelectSlack = array(
                    'type' => 'hidden',
                    'name' => 'messenger[slack]',
                    'value' => false,
            );
            if (
                    isset($this->_aConfig['messenger']['slack']['presets'])
                    && count($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,
                    );
                }
                
            }
            // ---------- Build plugins
            /*
            
            $aPluginsBuild = array(
                'select' => array(
                    'type' => 'checkbox',
                    'name' => 'build[enabled_build_plugins]',
                    'label' => t("build-plugins"),
                    'options' => [],
                ),
                // 'project-config' => '',
            );
            foreach (array_keys($this->getConfiguredPlugins('build')) as $sPluginName){
    
                $sPluginFile=$this->_getPluginFilename('build', $sPluginName);
                $TmpRolloutPlugin = false;
                $sMyClassname='build_'. $sPluginName;
                if(file_exists($sPluginFile)){
                try{
                    include_once $this->_getPluginFilename('build', $sPluginName);
                    $TmpRolloutPlugin = new $sMyClassname([]);
                    echo "FOUND $sMyClassname<br>";
                    $aPluginsBuild['select']['options'][$sPluginName]=array(
                            'label' => $TmpRolloutPlugin->getName(),
                            'checked' => $bActive,
                            // 'onclick' => '$(\'.'.$sMyDivClass.'\').hide(); $(\'.' . $sMyDivClassActive . '\').show();',
                        );
                    } catch (Exception $ex) {
    
                    }
                } else {
                    $aRollout['project-select']['options'][$sPluginName]=array(
                            'label' => 'not found: <span class="error">' . $sMyClassname . '</span>',
                            'checked' => false,
                            'disabled' => "disabled",
                        );
    
                    
                }
            }
            echo '<pre>'; print_r($aPluginsBuild); die(__METHOD__);
            */
    
            // ---------- /Build plugins
            
            // ---------- Rollout plugins
            $aRollout = array(
                'project-select' => array(
                    'type' => 'radio',
                    'name' => 'deploy[enabled_rollout_plugin]',
                    'label' => t("deploy-rollout-plugin"),
                ),
                'project-config' => '',
            );
            foreach (array_keys($this->getConfiguredPlugins('rollout')) as $sPluginName){
    
                $sPluginFile=$this->_getPluginFilename('rollout', $sPluginName);
                $TmpRolloutPlugin = false;
                $sMyClassname='rollout_'. $sPluginName;
                $sMyDivId='rollout-'. $sPluginName.'-config';
                $sMyDivClass='rolloutconfigdiv';
                $sMyDivClassActive='rolloutconfigdiv-'. $sPluginName;
                $bActive=$sPluginName === $this->oRolloutPlugin->getId();
    
                if(file_exists($sPluginFile)){
                    try{
                        include_once $this->_getPluginFilename('rollout', $sPluginName);
                        $TmpRolloutPlugin = new $sMyClassname(array(
                            'lang'=>$this->_aConfig['lang'],
                            'phase'=>false,
                            'globalcfg'=>$this->_aConfig['plugins']['rollout'][$sPluginName],
                            'projectcfg'=>$this->_aPrjConfig,
                        ));
                        $aRollout['project-select']['options'][$sPluginName]=array(
                                'label' => $TmpRolloutPlugin->getName(),
                                'checked' => $bActive,
                                'onclick' => '$(\'.'.$sMyDivClass.'\').hide(); $(\'.' . $sMyDivClassActive . '\').show();',
                            );
                        
                        $aRollout['project-config'].=''
                                . '<div id="'.$sMyDivId.'" class="'.$sMyDivClass.' '.$sMyDivClassActive.'"'
                                . ($bActive ? '' : 'style="display: none;"' )
                                . '>'
                                    . $TmpRolloutPlugin->renderFormdata4Project()
                                . '</div>'
                                ;
                        
                        // generate form firlds for each phase
                        foreach(array_keys($this->getPhases()) as $sMyPhase){
                            $aRollout[$sMyPhase].=''
                                . '<div id="'.$sMyDivId.'-'.$sMyPhase.'" class="'.$sMyDivClass.' '.$sMyDivClassActive.'"'
                                . ($bActive ? '' : 'style="display: none;"' )
                                . '>'
                                    . $TmpRolloutPlugin->renderFormdata4Phase($sMyPhase)
                                . '</div>'
                                ;
                        }
                    } catch (Exception $ex) {
    
                    }
                } else {
                    $aRollout['project-select']['options'][$sPluginName]=array(
                            'label' => 'not found: <span class="error">' . $sMyClassname . '</span>',
                            'checked' => false,
                            'disabled' => "disabled",
                        );
    
                    
                }
            }
            // ---------- /Rollout plugins
            
            $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 ($aForemanHostgroups && 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('deploy-rollout-plugin').t('deploy-rollout-plugin') . '</a></li>
                                    <li><a href="#tab5" data-toggle="tab">' . $this->_oHtml->getIcon('phase').t('phases') . '</a></li>
                                    <li><a href="#tab6" 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">
                                
                                ',
                        ),
    
                        // --------------------------------------------------
                        // Tab for metadata
                        // --------------------------------------------------
                        '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++ => $aSelectProjectGroup,
    
                        '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,
                        
                        // --------------------------------------------------
                        // Tab soources repository & build
                        // --------------------------------------------------
                        '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),
                                ),
                            ),
                        ),
    
                        // --------------------------------------------------
                        // Tab for config and API key
                        // --------------------------------------------------
                        '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>',
                        ),
                        
                        // --------------------------------------------------
                        // Tab rollout plugin
                        // --------------------------------------------------
                        'input' . $i++ => array(
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane" id="tab4">
                                <p>' . t('deploy-rollout-plugin-hint') . '</p>',
                        ),
                        // select box for active rollout plugin
                        $aRollout['project-select'],
                        
                        // project based config 
                        'input' . $i++ => array(
                            'type' => 'markup',
                            'value' => ''
                                . '<hr>'
                                    .'<label class="col-sm-2">'.t('deploy-rollout-plugin-config') .'</label>'
                                    .'<div class="col-sm-10">'. $aRollout['project-config'].'</div>'
                        ),
                        // --------------------------------------------------
                        'input' . $i++ => array(
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane" id="tab5">
                                <p>' . sprintf(t("class-project-info-setup-phaseinfos"), $this->getNextPhase()) . '</p>',
                        ),
                    ),
                ),
            );
            // --------------------------------------------------
            // Tab for phases
            // --------------------------------------------------
            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 (is_array($aForemanHostgroups) && 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' => 'https://' . $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\') )',
                        ),
                        'rolloutplugin' => array(
                            // 'label' => t("deploymethod-puppet").' - '.  $this->oRolloutPlugin->getName(),
                            'label' => t("deploymethod-rolloutplugin"),
                            'checked' => $sDeploymethod === "rolloutplugin",
                            '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;"') . '">'
                );
                
                // rollout plugin: phase specific overrides
                $aForms["setup"]["form"]['input' . $i++] = array(
                    'type' => 'markup',
                    'value' => ''
                        // . '<hr>'
                        .'<label class="col-sm-2">'.t('deploy-rollout-plugin-config') .'</label>'
                        .'<div class="col-sm-10">'.$aRollout[$sPhase].'</div>'
                ); 
                
                $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>',
                  );
                  }
                 */
    
                // 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' => isset($this->_aConfig["phases"][$sPhase]["deploytimes"]) ? implode(", ", $this->_aConfig["phases"][$sPhase]["deploytimes"]) : '',
                );
                $aForms["setup"]["form"]['input' . $i++] = array(
                    'type' => 'markup',
                    'value' => ''
                    . '</div>'
                );
    
                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
    
            // --------------------------------------------------
            // Tab for raw data
            // --------------------------------------------------
            
            $sRolloutDebug='<hr>DEBUG:<br>';
            foreach (array_keys($this->getPhases()) as $sPhase) {
                if ($this->isActivePhase($sPhase)){
                    $sRolloutDebug.='<strong>'.$sPhase.'</strong>'
                    . '<pre>Config = '.print_r($this->oRolloutPlugin->getConfig($sPhase), 1).'</pre>'
                    . '<pre>Commands = '.print_r($this->oRolloutPlugin->getDeployCommands($sPhase), 1).'</pre>'
                    ;
                }
            }
    
            $aForms["setup"]["form"]['input' . $i++] = array(
                'type' => 'markup',
                'value' => '</div>'
                
                    . '<div class="tab-pane" id="tab6">'
                    . '<br><pre>'.print_r($this->_aPrjConfig, 1).'</pre>'
                    . $sRolloutDebug
                    . '</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");
        }
    
    }