diff --git a/config/lang/de.json b/config/lang/de.json index 1a5ef39f412236844859fc6d0ce224c5575ace9a..2701d0da5376e11c67043aba759f5832904655e2 100644 --- a/config/lang/de.json +++ b/config/lang/de.json @@ -214,6 +214,9 @@ "accept": "Accept", "accept-hint": "Accept Phase [%s] und in die Queue von Phase [%s] stellen.", "all": "alle", + "api-secret": "Secret für API Zugriff", + "api-secret-hint": "Hinweise: Bei leeren Secret ist der Zugriff via API deaktiviert. Um den API Zugriff zu aktivieren, ist ein geshartes Secret zu setzen. Ein Neusetzen eines Secrets macht den bisherigen Key ungültig.", + "api-secret-generate": "Neues Secret erzeugen", "archive": "Archiv", "back": "zurück", "branch": "Branch/ Tag", @@ -238,7 +241,7 @@ "delete": "Löschen", "deploy": "Deploy", "deploy-configfile": "Konfiguration", - "deploy-configfile-hint": "Hier können Variablen in Bash-Syntax hinterlegt werden, die sich vom onbuild oder ondeploy Hook lesen lassen.", + "deploy-configfile-hint": "Hier können Variablen in Bash-Syntax hinterlegt werden. Bei einem Build werden diese in [root]/ci-custom-vars geschrieben und lassen sich vom onbuild oder ondeploy Hook lesen.", "deploy-hint": "Deploy der Queue von Phase [%s]", "deploy-impossible": "Deploy der Queue von Phase [%s] ist nicht möglich.", "deploy-settings": "Deployment-Einstellungen", diff --git a/config/lang/en.json b/config/lang/en.json index 969d944b89cc275167bb6fadbe8ab47123f1ed0a..ce3575c1c14d687f50108d2aeaa856c8c5b5a693 100644 --- a/config/lang/en.json +++ b/config/lang/en.json @@ -216,6 +216,9 @@ "accept": "Accept", "accept-hint": "Accept phase [%s] and put package to the queue of phase [%s].", "all": "all", + "api-secret": "Secret for API access", + "api-secret-hint": "To enable access via API it is required to set a shared secret. If you set a new key then the former key is invalid.", + "api-secret-generate": "Generate new secret", "archive": "Archive", "back": "back", "branch": "Branch/ tag", @@ -240,7 +243,7 @@ "delete": "Delete", "deploy": "Deploy", "deploy-configfile": "Configuration", - "deploy-configfile-hint": "Here you can place variables in Bash syntax that onbuild oder ondeploy hook can read.", + "deploy-configfile-hint": "Here you can place variables in Bash syntax. During the build it will be writen as [root]/ci-custom-vars and is readable in the onbuild oder ondeploy hook.", "deploy-hint": "Deploy queue of phase [%s]", "deploy-impossible": "Deploy queue of phase [%s] is not possible.", "deploy-settings": "Deployment settings", diff --git a/public_html/api/index.php b/public_html/api/index.php new file mode 100644 index 0000000000000000000000000000000000000000..4ded2b2a627edcaa53e202ee41d17e94d9b2aebb --- /dev/null +++ b/public_html/api/index.php @@ -0,0 +1,254 @@ +<?php +/* ====================================================================== + * + * A P I F O R C I S E R V E R + * + * GET /api/v1/projects/ + * GET /api/v1/project/[ID]/build/[name-of-branch] + * POST /api/v1/project/[ID]/build/[name-of-branch] + * get /api/v1/project/[ID]/phases + * + * ---------------------------------------------------------------------- + * 2020-06-16 v0.9 <axel.hahn@iml.unibe.ch> + * ====================================================================== + */ + + $bDebug=false; + ini_set('display_errors', 1); + ini_set('display_startup_errors', 1); + error_reporting(E_ALL); + + require_once("../../config/inc_projects_config.php"); + + $sDirClasses=__DIR__.'/../deployment/classes/'; + require_once($sDirClasses.'/project.class.php'); + + require_once($sDirClasses.'logger.class.php'); + + $aApiItems=array( + 'project', + 'projects', + 'help', + ); + $iMaxAge=60; + + // ---------------------------------------------------------------------- + // FUNCTIONS + // ---------------------------------------------------------------------- + /** + * write debug text (if enabled) + * @global boolean $bDebug + * @param string $s message + * @param string $sLevel level; one of info| + * @return boolean + */ + function _wd($s, $sLevel='info'){ + global $bDebug; + if ($bDebug){ + echo '<div class="debug debug-'.$sLevel.'">DEBUG: '.$s.'</div>'; + } + return true; + } + + /** + * abort execution with error + * @param string $s message + * @param integer $iStatus http status code to send + */ + function _quit($s, $iStatus=400){ + $aStatus=array( + 400=>'HTTP/1.0 400 Bad Request', + 403=>'HTTP/1.0 403 Access denied', + 404=>'HTTP/1.0 404 Not found', + ); + header($aStatus[$iStatus]); + _done(array('status'=>$iStatus, 'info'=>$aStatus[$iStatus], 'message'=>$s)); + } + /** + * end with OK output + * @param type $Data + */ + function _done($Data){ + echo is_array($Data) + ? json_encode($Data, 1, JSON_PRETTY_PRINT) + : $Data + ; + die(); + } + + /** + * Check authorization in the http request header and age of timestamp + * On a failed check the request will be terminated + * @global int $iMaxAge max allowed age + * @param type $sProjectSecret + * @return boolean + */ + function _checkAuth($sProjectSecret){ + global $iMaxAge; + $aReqHeaders=apache_request_headers(); + _wd('<pre>'.print_r($aReqHeaders, 1).'</pre>'); + if(!isset($aReqHeaders['Authorization'])){ + _quit('Access denied. Missing authorization.', 403); + } + + $sGotHash= preg_replace('/^.*\:/', '', $aReqHeaders['Authorization']); + $sGotDate= $aReqHeaders['Date']; + $sGotMethod=$_SERVER['REQUEST_METHOD']; + $sGotReq=$_SERVER['REQUEST_URI']; + + + $sMyData="${sGotMethod}\n${sGotReq}\n${sGotDate}\n"; + $sMyHash= base64_encode(hash_hmac("sha1", $sMyData, $sProjectSecret)); + + _wd('Hash: '.$sGotHash.' -- from header'); + _wd('Hash: '.$sMyHash.' -- rebuilt'); + if($sGotHash!==$sMyHash){ + _quit('Access denied. Invalid hash.', 403); + } + + $iAge=date('U')-date('U', strtotime($sGotDate)); + _wd('Date: '.$sGotDate.' - age: '.$iAge.' sec'); + if($iAge>$iMaxAge){ + _quit('Access denied. Hash is out of date: '.$iAge. ' sec is older '.$iMaxAge.' sec', 403); + } + return true; + } + // ---------------------------------------------------------------------- + // MAIN + // ---------------------------------------------------------------------- + + _wd('Start: '.date('Y-m-d H:i:s').'<style>body{background:#eee; color:#456;} + .debug{background:#ddd; margin-bottom: 2px;} + </style>'); + + _wd('request uri is '.$_SERVER["REQUEST_URI"]); + _wd('<pre>GET: '.print_r($_GET, 1).'</pre>'); + + + // ---------- SPLIT URL + + $aUriSplit= explode('/', preg_replace('/\?.*$/', '', $_SERVER["REQUEST_URI"])); + + array_shift($aUriSplit); + array_shift($aUriSplit); + _wd('<pre>$aUriSplit: '.print_r($aUriSplit, 1).'</pre>'); + /* + + /api/v1/projects/ci/build?auth=123 + $aUriSplit: Array + ( + [0] => v1 + [1] => projects + [2] => ci + [3] => build + ) + */ + $sApiVersion = isset($aUriSplit[0]) ? $aUriSplit[0] : false; + $sApiItem = isset($aUriSplit[1]) ? $aUriSplit[1] : false; + + + if(!$sApiVersion){ + _quit('no paramm for api version was found.'); + } + if(!$sApiItem){ + _quit('ERROR: no paramm for item was found.'); + } + if(!in_array($sApiItem, $aApiItems)){ + _quit('ERROR: item ['.$sApiItem.'] is invalid.'); + } + + + + switch ($sApiVersion){ + case 'v1': + switch($sApiItem){ + case 'projects': + + $oProject=new project(); + $aList=$oProject->getProjects(); + _wd('<pre>'.print_r($aList,1).'</pre>'); + _done($aList); + break;; + + case 'project': + // path /api/v1/project + + $sPrjId = isset($aUriSplit[2]) ? $aUriSplit[2] : false; + $sPrjAction = isset($aUriSplit[3]) ? $aUriSplit[3] : false; + $sParam4 = isset($aUriSplit[4]) ? $aUriSplit[4] : false; + $sParam5 = isset($aUriSplit[5]) ? $aUriSplit[5] : false; + $sMethod = $_SERVER['REQUEST_METHOD']; + _wd('$sPrjId = '.$sPrjId); + _wd('$sPrjAction = '.$sPrjAction); + + $oCLog = new logger(); + // try to init the given project + try{ + ob_start(); + $oProject=new project($sPrjId); + + // $oProject->setProjectById($sPrjId); + ob_end_clean(); + + } catch (Exception $exc) { + _quit('ERROR: project with id ['.$sPrjId.'] does not exist.', 404); + } + + // get secret + $aPrjCfg=$oProject->getConfig(); + $sProjectSecret=isset($aPrjCfg['api']['secret']) ? $aPrjCfg['api']['secret'] : false; + if(!$sProjectSecret){ + _quit('Access denied. API access is disabled.'); + } + + // check authorization + _checkAuth($sProjectSecret); + + // echo "OK: request was authorized successfully.\n"; + + $oProject->oUser->setUser('api'); + + switch($sPrjAction){ + case "build": + if ($sParam4){ + $aResult=$oProject->setBranchname($sParam4 . ($sParam5 ? '/'.$sParam5 : '')); + } + $sBranchname=$oProject->getBranchname(); + $aRepodata = $oProject->getRemoteBranches(true); + if(!isset($aRepodata[$sBranchname])){ + _quit('ERROR: branch not found: '.$sBranchname, 404); + } + + + // echo "branch is set to ".$oProject->getBranchname()."\n"; + if ($sMethod==='GET'){ + $sNext=$oProject->getNextPhase(); + _done(array( + 'branch'=>$sBranchname, + 'phase'=>$sNext, + 'repo'=>$aRepodata[$sBranchname] + )); + } + if ($sMethod==='POST'){ + echo "starting build() ..."; + flush(); + echo $oProject->build(); + } + break;; + case "phases": + _done($oProject->getAllPhaseInfos()); + break;; + default: + _quit('ERROR: Wrong action ['.$sApiItem.'].'); + } + + break;; + + default: + // unreachable - see in_array before switch + _quit('ERROR: item ['.$sApiItem.'] is invalid.'); + } + break; + default: + _quit('ERROR: Wrong (unsupported) api version.'); + } \ No newline at end of file diff --git a/public_html/deployment/classes/project.class.php b/public_html/deployment/classes/project.class.php index 976db48df1314270be79fc3a178d3d976bfa753b..dd56aceb4dae443f73a2400b2a40150453bc690e 100644 --- a/public_html/deployment/classes/project.class.php +++ b/public_html/deployment/classes/project.class.php @@ -195,8 +195,10 @@ class project extends base { * @return boolean */ private function _verifyConfig() { - if (!count($this->_aPrjConfig)) - die(t("class-project-error-no-config")); + if (!count($this->_aPrjConfig)){ + // die(t("class-project-error-no-config")); + throw new Exception(t("class-project-error-no-config")); + } if (!array_key_exists("packageDir", $this->_aConfig)) { die(t("class-project-error-no-packagedir")); @@ -1351,8 +1353,8 @@ class project extends base { if ($this->_sProcessTempOut && file_exists($this->_sProcessTempOut)) { unlink($this->_sProcessTempOut); } - $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile); - $this->_sProcessTempOut = $sNewTempfile; + // $sNewTempfile = sys_get_temp_dir() . "/" . basename($sNewTempfile); + $this->_sProcessTempOut = $sNewTempfile ? sys_get_temp_dir() . "/" . basename($sNewTempfile) : false; return $this->_sProcessTempOut; } @@ -1450,7 +1452,7 @@ class project extends base { } return $this->_sBranchname; } - + // ---------------------------------------------------------------------- // ACTIONS // ---------------------------------------------------------------------- @@ -3508,7 +3510,25 @@ class project extends base { 'cols' => 100, 'rows' => 10, 'placeholder' => 'export myvariable="hello world"', + ), + + 'input' . $i++ => array( + 'type' => 'text', + 'name' => 'api[secret]', + 'label' => t("api-secret"), + 'value' => $this->_aPrjConfig["api"]["secret"], + 'validate' => 'isastring', + 'size' => 100, + 'placeholder' => '', ), + 'input' . $i++ => array( + 'type' => 'markup', + 'value' => '<div class="col-sm-12">' + . '<p>' . t('api-secret-hint') . '<br>' + . '<a href="#" class="btn btn-default" onclick="$(\'#input'.($i-2).'\').val(generateSecret(64)); return false">'.t("api-secret-generate").'</a>' + . '</p></div>', + ), + // -------------------------------------------------- 'input' . $i++ => array( 'type' => 'markup', diff --git a/public_html/deployment/classes/sws.class.php b/public_html/deployment/classes/sws.class.php index 5da960db10976988d6108636fb9dc48f63538909..433ab7c47eae18fcf53f741415ce1d5b5ce57820 100644 --- a/public_html/deployment/classes/sws.class.php +++ b/public_html/deployment/classes/sws.class.php @@ -103,7 +103,7 @@ class sws { * version * @var string */ - private $_sVersion = "0.06 (beta)"; + private $_sVersion = "0.07 (beta)"; /** * title @@ -166,6 +166,23 @@ class sws { return false; } + /** + * helper for _parseParams + * check if parameter args contain safe chars + * @param type $sParamValue + * @return boolean + */ + private function _verifyParamValue($sParamValue){ + $sOKChars='a-z0-9\"\{\}\[\]\.\,\ \:\-\+'; + if(isset($this->_aParams[$sParamValue])){ + $sVal=urldecode($this->_aParams[$sParamValue]); + if(preg_match('/[^'.$sOKChars. ']/i', $sVal)){ + $this->_quit("ERROR: parameter $sParamValue=.. contains unsupported character(s): [". preg_replace('/['.$sOKChars. ']/i', '',$sVal)."]"); + } + + } + return true; + } /** * parse parameters (given GET/ POST is in by _aParams @see setParams) * class - class to initialize @@ -176,7 +193,7 @@ class sws { */ private function _parseParams() { $aMinParams = array("class", "action"); - $aMaxParams = array_merge($aMinParams, array("args")); + $aMaxParams = array_merge($aMinParams, array("init", "args", "type")); $sErrors = ''; // set defaults @@ -202,7 +219,12 @@ class sws { $sErrors.="- <em>" . $sKey . "</em><br>"; } } - // TODO: checkMaxParams + // check max Params + foreach(array_keys($this->_aParams) as $sKey){ + if(!in_array($sKey, $aMaxParams)){ + $this->_quit("ERROR: parameter ". htmlentities($sKey)." is unknown."); + } + } // check if classname and action exist in configuration if (array_key_exists("class", $this->_aParams)) { @@ -214,6 +236,9 @@ class sws { $this->_sClassfile = $this->_aKnownClasses["classes"][$this->_aParams["class"]]["file"]; // get arguments for the method + $this->_verifyParamValue("init"); + $this->_verifyParamValue("args"); + if (array_key_exists("init", $this->_aParams)) { try { $aTmp = json_decode($this->_aParams["init"], 1); @@ -222,7 +247,7 @@ class sws { } if (!is_array($aTmp)) { $this->_quit( - 'ERROR: wrong request - init value must be a json string<br>' + 'ERROR: wrong request - init value must be a json string (in url encoded form)<br>' . 'examples:<br>' . '- one arg <code>(...)&init=["my string"]</code><br>' . '- two args <code>(...)&init=["my string", 123]</code> ' @@ -249,7 +274,7 @@ class sws { } if (!is_array($aTmp)) { $this->_quit( - 'ERROR: wrong request - args value must be a json string<br>' + 'ERROR: wrong request - args value must be a json string (in url encoded form)<br>' . 'examples:<br>' . '- one arg <code>(...)&args=["my string"]</code><br>' . '- two args <code>(...)&args=["my string", 123]</code> ' @@ -319,7 +344,7 @@ class sws { * @param type $sError */ private function _quit($sError, $sMore = "") { - header("Status: 400 Bad Request"); + header("HTTP1.0 400 Bad Request"); echo $this->_wrapAsWebpage( $sMore, '<div class="error">' . $sError . '</div>' ); @@ -623,7 +648,7 @@ class sws { $sClassInit = '<span class="urlvalue">[initparams]</span>'; $sActionSelect = '<span class="urlvalue">[action]</span>'; $sParamSelect = '<span class="urlvalue">[parameters]</span>'; - $sTypeSelect = '<span class="urlvalue">[type: raw|json]</span>'; + $sTypeSelect = '<span class="urlvalue">[raw|json]</span>'; $sSyntax = sprintf( '<pre>?' @@ -701,6 +726,9 @@ class sws { . '.warning{ color:#a96; background:#fc8; padding: 0.5em; margin-bottom: 2em; border-left: 4px solid;}' . '.defaultvalue{color: #33c;}' . '</style>' + ; + if($this->_aOptions["enableGui"]){ + $sReturn.= '' . '<script>' . 'function toggleDesciption(sId, a){' . 'var o=document.getElementById(sId);' @@ -781,6 +809,9 @@ class sws { ' . '</script>' + ; + } + $sReturn.= '' . '</head>' . '<body>'; diff --git a/public_html/deployment/classes/user.class.php b/public_html/deployment/classes/user.class.php index da03b92599e7a37c54f77066a3c5bf94f1b0febf..756b6ae9efa712f02f0fb60c13297a8d23fffe00 100644 --- a/public_html/deployment/classes/user.class.php +++ b/public_html/deployment/classes/user.class.php @@ -53,15 +53,15 @@ class user { /** - * detect a user - * @return type + * get string with detected user from current session / basic auth / cli access + * @return string */ private function _autoDetectUser(){ $sUser=false; - if (isset($_SESSION) && is_array($_SESSION) && array_key_exists("PHP_AUTH_USER", $_SESSION)){ + if (isset($_SESSION) && isset($_SESSION["PHP_AUTH_USER"])){ $sUser=$_SESSION["PHP_AUTH_USER"]; } - if (!$sUser && is_array($_SERVER) && array_key_exists("PHP_AUTH_USER", $_SERVER)){ + if (!$sUser && isset($_SERVER["PHP_AUTH_USER"])){ $sUser=$_SERVER["PHP_AUTH_USER"]; } if (php_sapi_name() == "cli") { @@ -137,7 +137,7 @@ class user { */ public function authenticate(){ global $aConfig, $aParams; - + print_r($aConfig); if(!array_key_exists('auth', $aConfig) || !count($aConfig['auth']) || !array_key_exists('user', $aParams)){ return false; } @@ -162,7 +162,7 @@ class user { // set a session - it must correspondent with _autoDetectUser() $_SESSION["PHP_AUTH_USER"]=$sUser; - $this->setUser(); + $this->setUser(''); return true; } } @@ -180,10 +180,15 @@ class user { } /** - * set a authenticated user and get its roles + * set an authenticated user and get its roles */ - public function setUser(){ - $this->_sUsername=$this->_autoDetectUser(); + public function setUser($sUser=false){ + if($sUser!==false){ + $this->_sUsername=$sUser; + $_SESSION["PHP_AUTH_USER"]=$sUser; + } else { + $this->_sUsername=$this->_autoDetectUser(); + } $this->_getUserGroups(); $this->_getUserPermission(); } diff --git a/public_html/deployment/classes/vcs.git.class.php b/public_html/deployment/classes/vcs.git.class.php index c994e34251c07740041a30b773ed24b142237d53..d361b364e4071507b2cec9b65ac115b92a399c76 100644 --- a/public_html/deployment/classes/vcs.git.class.php +++ b/public_html/deployment/classes/vcs.git.class.php @@ -357,6 +357,9 @@ class vcs implements iVcs { } else { $a = $this->getRevision(false); } + if(!isset($a['branch'])){ + return false; + } // merge with cached info ... to add type and label if(isset($this->_aRemoteBranches[$a['branch']])){ $this->_aRemoteBranches[$a['branch']]=array_merge($this->_aRemoteBranches[$a['branch']], $a); diff --git a/public_html/deployment/js/functions.js b/public_html/deployment/js/functions.js index c8c2af19bc169bbe1ee0b60ac532ad08b62d7378..87a3db1e183204992d7ea58adf1edcfd5bca24fc 100644 --- a/public_html/deployment/js/functions.js +++ b/public_html/deployment/js/functions.js @@ -1,305 +1,320 @@ - -/** - * initialize soft scrolling for links with css class "scroll-link" - * @see http://css-tricks.com/snippets/jquery/smooth-scrolling/ - * @returns {undefined} - */ -function initSoftscroll(){ - $(function() { - // $('a[href*=#]:not([href=#])').click(function() { - $('a.scroll-link').click(function() { - if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) { - var target = $(this.hash); - target = target.length ? target : $('[name=' + this.hash.slice(1) +']'); - if (target.length) { - $('html,body').animate({ - scrollTop: target.offset().top - 70 - }, 300); - return false; - } - } - }); - }); -} - -function showModalMessage(sMessage){ - $('#divmodalmessage').html(sMessage); - $('#divmodal').show(); -} -function showIdAsModalMessage(sId){ - var o=$('#'+sId); - var sHtml='<a href="#" onclick="return hideModalMessage()" class="btn btn-danger" style="float:right"> X </a>' - + o.html() - + '<hr><a href="#" onclick="return hideModalMessage()" class="btn btn-primary"><i class="fa fa-check"></i> OK </a>' - ; - $('#divmodalmessage').html(sHtml); - $('#divmodal').show(); - return false; -} - -function hideModalMessage(){ - $('#divmodal').hide(); - return false; -} - -// ---------------------------------------------------------------------- -// general init in each page -// ---------------------------------------------------------------------- - -$(document).ready(function() { - initSoftscroll(); - // $(".optionName").popover({trigger: "hover"}); - // $("#content").hide().fadeIn(300); -}); - - -// ---------------------------------------------------------------------- -// action log -// ---------------------------------------------------------------------- - -/** - * get filtered action log table - * @returns {undefined} - */ -function updateActionlog(){ - var sUrlBase="/webservice/?class=Actionlog&action=getLogs&type=json&args="; - var aArgs={}; - - var aFilteritems=["project", "where", "order", "limit"]; - var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"]; - - // --- create query url - - for (i=0; i<aFilteritems.length; i++){ - sValue=$('#select' + aFilteritems[i]).val(); - if(sValue){ - aArgs[aFilteritems[i]]=sValue; - } - } - - var sWhere=''; - for (j=0; j<aTableitems.length; j++){ - sValue=$('#selectWhere' + aTableitems[j]).val(); - if(sValue){ - if (sWhere){ - sWhere+' AND '; - } - sWhere+='`'+aTableitems[j]+'`'+sValue; - } - } - if (sWhere) { - aArgs["where"]=sWhere; - } - - // --- get data - - var sUrl=sUrlBase+'['+JSON.stringify(aArgs)+']'; - $.post( sUrl, function( aData ) { - var sHtml=''; - - // --- generate output - if (aData.length && aData[0]["id"]){ - for (i=0; i<aData.length; i++){ - sHtml+='<tr class="tractionlogs loglevel-'+aData[i]["loglevel"]+' '+aData[i]["project"]+'">'; - for (j=0; j<aTableitems.length; j++){ - sHtml+='<td>'+aData[i][aTableitems[j]]+'</td>'; - } - sHtml+='</tr>'; - } - } - drawTimeline(aData); - - if (!sHtml){ - sHtml=sMsgNolog; // variable is set in actionlog.class.php - } else { - sHead=''; - for (j=0; j<aTableitems.length; j++){ - sHead+='<th>'+aTableitems[j]+'</th>'; - } - sHead='<thead><tr>'+sHead+'</tr></thead>'; - sHtml='<table class="table table-condensed">'+sHead+'<tbody>'+sHtml+'</tbody></table>'; - } - $('#tableLogactions').html(sHtml); - filterLogTable(); - }); - -} - -/** - * render timeline with Visjs - * - * @param {array} aData - * @returns {undefined} - */ -function drawTimeline(aData){ - var sDataset=''; - - var container = document.getElementById('divTimeline'); - if(!container){ - return false; - } - container.innerHTML=''; // empty the div - - if (aData.length && aData[0]["id"]){ - for (i=0; i<aData.length; i++){ - // keys are - // var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"]; - sLabel=aData[i]["project"]+'<br>'+aData[i]["action"]; - sTitle=aData[i]["time"] + '<br>'+aData[i]["loglevel"]+'<br><br>Projekt: ' + aData[i]["project"] +'<br>User: ' + aData[i]["user"] + ' (' + aData[i]["ip"] +')<br>'+ aData[i]["message"] ; - sDataset+= (sDataset ? ', ': '' ) - + '{"id": '+i+', "content": "'+sLabel+'", "start": "'+aData[i]["time"].replace(/\ /, "T") +'", "title": "'+sTitle+'", "group": "'+aData[i]["project"]+'", "className": "loglevel-'+aData[i]["loglevel"]+'" }'; - } - aDataset=JSON.parse('['+sDataset+']'); - - var items = new vis.DataSet(aDataset); - - // Configuration for the Timeline - var options = { - // verticalScroll: false, - clickToUse: true - }; - - // Create a Timeline - var timeline = new vis.Timeline(container, items, options); - } -} - -/** -* filter table with action logs by filtertext (input field) -*/ -function filterLogTable(){ - var sSearch=$("#efilterlogs").val(); - var Regex = new RegExp(sSearch, "i"); - $(".tractionlogs").each(function() { - sVisible="none"; - if ( Regex.exec(this.innerHTML)) { - sVisible=""; - } - $(this).css("display", sVisible); - }); - return false; -} - -// ---------------------------------------------------------------------- -// tables -// ---------------------------------------------------------------------- -var localStoreTablefilter="tblvalue" + location.pathname; - - -// http://blog.mastykarz.nl/jquery-regex-filter/ -jQuery.extend( - jQuery.expr[':'], { - regex: function (a, i, m) { - var r = new RegExp(m[3], 'i'); - return r.test(jQuery(a).text()); - } -} -); - -/* - highlight v4 - Highlights arbitrary terms. - - <http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html> - - MIT license. - - Johann Burkard - <http://johannburkard.de> - <mailto:jb@eaio.com> - */ - -jQuery.fn.highlight = function (pat) { - function innerHighlight(node, pat) { - var skip = 0; - if (node.nodeType == 3) { - var pos = node.data.toUpperCase().indexOf(pat); - if (pos >= 0) { - var spannode = document.createElement('span'); - spannode.className = 'highlight'; - var middlebit = node.splitText(pos); - var endbit = middlebit.splitText(pat.length); - var middleclone = middlebit.cloneNode(true); - spannode.appendChild(middleclone); - middlebit.parentNode.replaceChild(spannode, middlebit); - skip = 1; - } - } - else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { - for (var i = 0; i < node.childNodes.length; ++i) { - i += innerHighlight(node.childNodes[i], pat); - } - } - return skip; - } - return this.length && pat && pat.length ? this.each(function () { - innerHighlight(this, pat.toUpperCase()); - }) : this; -}; - -jQuery.fn.removeHighlight = function () { - return this.find("span.highlight").each(function () { - this.parentNode.firstChild.nodeName; - with (this.parentNode) { - replaceChild(this.firstChild, this); - normalize(); - } - }).end(); -}; - - -/** - * add a filter form to a table - * @returns {undefined} - */ -function addFilterToTable(){ - var sValue=localStorage.getItem(localStoreTablefilter) ? localStorage.getItem(localStoreTablefilter) : ''; - var sForm='<form class="pure-form">\n\ - <fieldset>\n\ - <label for="eFilter">\n\ - <i class="fa fa-filter"></i> Tabelle filtern\n\ - </label>\n\ - <input type="text" id="eFilter" size="40" name="q" placeholder="Suchbegriff..." value="'+sValue+'" onkeypress="filterTable(this);" onkeyup="filterTable(this);" onchange="filterTable(this);">\n\ - <button class="pure-button" onclick="$(\'#eFilter\').val(\'\'); filterTable(); return false;"><i class="fa fa-times"></i> </button>\n\ - <span id="filterinfo"></span>\n\ - </fieldset>\n\ - </form><div style="clear: both;"></div>'; - $(sForm).insertBefore($("table").first()); -} - - -/** - * callback ... filter the table - * use addFilterToTable() before. - * @returns {undefined} - */ -function filterTable() { - var filter = $('#eFilter').val(); - localStorage.setItem(localStoreTablefilter, filter); - $("table").removeHighlight(); - if (filter) { - $("tr:regex('" + filter + "')").show(); - $("tr:not(:regex('" + filter + "'))").hide(); - $("tr").first().show(); - - $("td").highlight(filter); - } else { - $("td").removeHighlight(); - $('tr').show(); - } - - var sInfo = ''; - var iVisible = -1; // 1 abziehen wg. tr der ersten Zeile - $("tr").each(function () { - if ($(this).css('display') != 'none') { - iVisible++; - } - }); - - sInfo = (iVisible == ($("tr").length - 1)) - ? "ges.: <strong>" + ($("tr").length - 1) + "</strong> Einträge" - : "<strong>" + iVisible + "</strong> von " + ($("tr").length - 1) + " Einträgen" - ; - $('#filterinfo').html(sInfo); - -} + +/** + * initialize soft scrolling for links with css class "scroll-link" + * @see http://css-tricks.com/snippets/jquery/smooth-scrolling/ + * @returns {undefined} + */ +function initSoftscroll(){ + $(function() { + // $('a[href*=#]:not([href=#])').click(function() { + $('a.scroll-link').click(function() { + if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) { + var target = $(this.hash); + target = target.length ? target : $('[name=' + this.hash.slice(1) +']'); + if (target.length) { + $('html,body').animate({ + scrollTop: target.offset().top - 70 + }, 300); + return false; + } + } + }); + }); +} + +function showModalMessage(sMessage){ + $('#divmodalmessage').html(sMessage); + $('#divmodal').show(); +} +function showIdAsModalMessage(sId){ + var o=$('#'+sId); + var sHtml='<a href="#" onclick="return hideModalMessage()" class="btn btn-danger" style="float:right"> X </a>' + + o.html() + + '<hr><a href="#" onclick="return hideModalMessage()" class="btn btn-primary"><i class="fa fa-check"></i> OK </a>' + ; + $('#divmodalmessage').html(sHtml); + $('#divmodal').show(); + return false; +} + +function hideModalMessage(){ + $('#divmodal').hide(); + return false; +} + +// ---------------------------------------------------------------------- +// general init in each page +// ---------------------------------------------------------------------- + +$(document).ready(function() { + initSoftscroll(); + // $(".optionName").popover({trigger: "hover"}); + // $("#content").hide().fadeIn(300); +}); + + +// ---------------------------------------------------------------------- +// action log +// ---------------------------------------------------------------------- + +/** + * get filtered action log table + * @returns {undefined} + */ +function updateActionlog(){ + var sUrlBase="/webservice/?class=Actionlog&action=getLogs&type=json&args="; + var aArgs={}; + + var aFilteritems=["project", "where", "order", "limit"]; + var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"]; + + // --- create query url + + for (i=0; i<aFilteritems.length; i++){ + sValue=$('#select' + aFilteritems[i]).val(); + if(sValue){ + aArgs[aFilteritems[i]]=sValue; + } + } + + var sWhere=''; + for (j=0; j<aTableitems.length; j++){ + sValue=$('#selectWhere' + aTableitems[j]).val(); + if(sValue){ + if (sWhere){ + sWhere+' AND '; + } + sWhere+='`'+aTableitems[j]+'`'+sValue; + } + } + if (sWhere) { + aArgs["where"]=sWhere; + } + + // --- get data + + var sUrl=sUrlBase+'['+JSON.stringify(aArgs)+']'; + $.post( sUrl, function( aData ) { + var sHtml=''; + + // --- generate output + if (aData.length && aData[0]["id"]){ + for (i=0; i<aData.length; i++){ + sHtml+='<tr class="tractionlogs loglevel-'+aData[i]["loglevel"]+' '+aData[i]["project"]+'">'; + for (j=0; j<aTableitems.length; j++){ + sHtml+='<td>'+aData[i][aTableitems[j]]+'</td>'; + } + sHtml+='</tr>'; + } + } + drawTimeline(aData); + + if (!sHtml){ + sHtml=sMsgNolog; // variable is set in actionlog.class.php + } else { + sHead=''; + for (j=0; j<aTableitems.length; j++){ + sHead+='<th>'+aTableitems[j]+'</th>'; + } + sHead='<thead><tr>'+sHead+'</tr></thead>'; + sHtml='<table class="table table-condensed">'+sHead+'<tbody>'+sHtml+'</tbody></table>'; + } + $('#tableLogactions').html(sHtml); + filterLogTable(); + }); + +} + +/** + * render timeline with Visjs + * + * @param {array} aData + * @returns {undefined} + */ +function drawTimeline(aData){ + var sDataset=''; + + var container = document.getElementById('divTimeline'); + if(!container){ + return false; + } + container.innerHTML=''; // empty the div + + if (aData.length && aData[0]["id"]){ + for (i=0; i<aData.length; i++){ + // keys are + // var aTableitems=["id", "time", "loglevel", "ip", "user", "project", "action", "message"]; + sLabel=aData[i]["project"]+'<br>'+aData[i]["action"]; + sTitle=aData[i]["time"] + '<br>'+aData[i]["loglevel"]+'<br><br>Projekt: ' + aData[i]["project"] +'<br>User: ' + aData[i]["user"] + ' (' + aData[i]["ip"] +')<br>'+ aData[i]["message"] ; + sDataset+= (sDataset ? ', ': '' ) + + '{"id": '+i+', "content": "'+sLabel+'", "start": "'+aData[i]["time"].replace(/\ /, "T") +'", "title": "'+sTitle+'", "group": "'+aData[i]["project"]+'", "className": "loglevel-'+aData[i]["loglevel"]+'" }'; + } + aDataset=JSON.parse('['+sDataset+']'); + + var items = new vis.DataSet(aDataset); + + // Configuration for the Timeline + var options = { + // verticalScroll: false, + clickToUse: true + }; + + // Create a Timeline + var timeline = new vis.Timeline(container, items, options); + } +} + +/** +* filter table with action logs by filtertext (input field) +*/ +function filterLogTable(){ + var sSearch=$("#efilterlogs").val(); + var Regex = new RegExp(sSearch, "i"); + $(".tractionlogs").each(function() { + sVisible="none"; + if ( Regex.exec(this.innerHTML)) { + sVisible=""; + } + $(this).css("display", sVisible); + }); + return false; +} + +// ---------------------------------------------------------------------- +// tables +// ---------------------------------------------------------------------- +var localStoreTablefilter="tblvalue" + location.pathname; + + +// http://blog.mastykarz.nl/jquery-regex-filter/ +jQuery.extend( + jQuery.expr[':'], { + regex: function (a, i, m) { + var r = new RegExp(m[3], 'i'); + return r.test(jQuery(a).text()); + } +} +); + +/* + highlight v4 + Highlights arbitrary terms. + + <http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html> + + MIT license. + + Johann Burkard + <http://johannburkard.de> + <mailto:jb@eaio.com> + */ + +jQuery.fn.highlight = function (pat) { + function innerHighlight(node, pat) { + var skip = 0; + if (node.nodeType == 3) { + var pos = node.data.toUpperCase().indexOf(pat); + if (pos >= 0) { + var spannode = document.createElement('span'); + spannode.className = 'highlight'; + var middlebit = node.splitText(pos); + var endbit = middlebit.splitText(pat.length); + var middleclone = middlebit.cloneNode(true); + spannode.appendChild(middleclone); + middlebit.parentNode.replaceChild(spannode, middlebit); + skip = 1; + } + } + else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { + for (var i = 0; i < node.childNodes.length; ++i) { + i += innerHighlight(node.childNodes[i], pat); + } + } + return skip; + } + return this.length && pat && pat.length ? this.each(function () { + innerHighlight(this, pat.toUpperCase()); + }) : this; +}; + +jQuery.fn.removeHighlight = function () { + return this.find("span.highlight").each(function () { + this.parentNode.firstChild.nodeName; + with (this.parentNode) { + replaceChild(this.firstChild, this); + normalize(); + } + }).end(); +}; + + +/** + * add a filter form to a table + * @returns {undefined} + */ +function addFilterToTable(){ + var sValue=localStorage.getItem(localStoreTablefilter) ? localStorage.getItem(localStoreTablefilter) : ''; + var sForm='<form class="pure-form">\n\ + <fieldset>\n\ + <label for="eFilter">\n\ + <i class="fa fa-filter"></i> Tabelle filtern\n\ + </label>\n\ + <input type="text" id="eFilter" size="40" name="q" placeholder="Suchbegriff..." value="'+sValue+'" onkeypress="filterTable(this);" onkeyup="filterTable(this);" onchange="filterTable(this);">\n\ + <button class="pure-button" onclick="$(\'#eFilter\').val(\'\'); filterTable(); return false;"><i class="fa fa-times"></i> </button>\n\ + <span id="filterinfo"></span>\n\ + </fieldset>\n\ + </form><div style="clear: both;"></div>'; + $(sForm).insertBefore($("table").first()); +} + + +/** + * callback ... filter the table + * use addFilterToTable() before. + * @returns {undefined} + */ +function filterTable() { + var filter = $('#eFilter').val(); + localStorage.setItem(localStoreTablefilter, filter); + $("table").removeHighlight(); + if (filter) { + $("tr:regex('" + filter + "')").show(); + $("tr:not(:regex('" + filter + "'))").hide(); + $("tr").first().show(); + + $("td").highlight(filter); + } else { + $("td").removeHighlight(); + $('tr').show(); + } + + var sInfo = ''; + var iVisible = -1; // 1 abziehen wg. tr der ersten Zeile + $("tr").each(function () { + if ($(this).css('display') != 'none') { + iVisible++; + } + }); + + sInfo = (iVisible == ($("tr").length - 1)) + ? "ges.: <strong>" + ($("tr").length - 1) + "</strong> Einträge" + : "<strong>" + iVisible + "</strong> von " + ($("tr").length - 1) + " Einträgen" + ; + $('#filterinfo').html(sInfo); + +} + + +// ---------------------------------------------------------------------- +// API secret +// ---------------------------------------------------------------------- +function generateSecret(length){ + + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for ( var i = 0; i < length; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} diff --git a/public_html/webservice/sws-config.json b/public_html/webservice/sws-config.json index a7b7e3273c59e06c4baf89e8dfb4962564307f84..a2cdf80e167968735114c65e56b12c18bc165851 100644 --- a/public_html/webservice/sws-config.json +++ b/public_html/webservice/sws-config.json @@ -1,7 +1,7 @@ { "options": { "enableGui": 1, - "enableDump": 1 + "enableDump": 0 }, "classes": { "Actionlog": { @@ -9,6 +9,12 @@ "actions": { "getLogs": {} } + }, + "project": { + "file": "project.class.php", + "actions": { + "getProjects": {} + } } } } \ No newline at end of file diff --git a/shellscripts/api-client.sh b/shellscripts/api-client.sh new file mode 100644 index 0000000000000000000000000000000000000000..25a881567b012a52f398fffdd99adeb70463dd4e --- /dev/null +++ b/shellscripts/api-client.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# ====================================================================== +# +# API CLIENT :: proof of concept +# +# This is a demo api client +# +# ---------------------------------------------------------------------- +# +# my projects and secrets +# DATA:ci-webgui:cOiScVAElvcJKmJ1eGrKXZvv6ZROlSgZ9VpSVFK1uxZI8J5ITXuZZb8jIYobuoAB +# +# ---------------------------------------------------------------------- +# 2020-06-12 first lines <axel.hahn@iml.unibe.ch> +# ====================================================================== + +# ---------------------------------------------------------------------- +# CONFIG +# ---------------------------------------------------------------------- + +apiHost="http://dev.ci.iml.unibe.ch:8002" +line="----------------------------------------------------------------------" + + +# ---------------------------------------------------------------------- +# FUNCTIONS +# ---------------------------------------------------------------------- + +function showhelp(){ +echo " +SYNTAX + projects show projects + + buildinfo [project] [branch] + show infos about what happens on build + + build [project] [branch] + execute build + + phases [project] + show status of phases + +PARAMETERS: + project project id in ci; see output or projects + branch name of branch (optional), i.e. origin/feature-123 +" +} + + +function makeRequest(){ + + local apiMethod=$1 + local apiRequest=$2 + local secret=$3 + + echo $apiMethod ${apiHost}${apiRequest} + echo $line + + if [ ! -z $secret ]; then + + # --- date in http format + LANG=en_EN + # export TZ=GMT + apiTS=`date "+%a, %d %b %Y %H:%M:%S %Z"` + + +# --- generate data to hash: method + uri + timestamp; delimited with line break +data="${apiMethod} +${apiRequest} +${apiTS} +" + + # generate hash - split in 2 commands (piping "cut" sends additional line break) + myHash=`echo -n "$data" | openssl sha1 -hmac "${secret}" | cut -f 2 -d" "` + myHash=`echo -n "$myHash" | base64` + + curl -i \ + -H "Accept: application/json" -H "Content-Type: application/json" \ + -H "Date: ${apiTS}" \ + -H "Authorization: demo-bash-client:${myHash}" \ + -X $apiMethod \ + ${apiHost}${apiRequest} + else + curl -i \ + -H "Accept: application/json" -H "Content-Type: application/json" \ + -X $apiMethod \ + ${apiHost}${apiRequest} + fi +} + + +# ---------------------------------------------------------------------- +# MAIN +# ---------------------------------------------------------------------- + +if [ $# -lt 1 ]; then + showhelp + exit 1 +fi + +myProject=$2 +secret=`grep "^#\ DATA:${myProject}:" $0 | cut -f 3- -d ":"` + + +case $1 in + + # --- projects is an access without autorization + "projects") + makeRequest GET /api/v1/projects + ;; + + # --- access WITH autorization only + "build") + makeRequest POST /api/v1/project/$myProject/build/$3 "$secret" + ;; + "buildinfo") + makeRequest GET /api/v1/project/$myProject/build/$3 "$secret" + ;; + "phases") + makeRequest GET /api/v1/project/$myProject/phases "$secret" + ;; + *) + echo "ERROR: unknown parameter $1" + exit 2 +esac + +rc=$? +echo +echo $line +echo rc=$rc + +exit $rc \ No newline at end of file