<?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>  
 * 2021-03-29  v1.2  <axel.hahn@iml.unibe.ch>  support slashes in branch names
 * 2024-09-02  v1.3  <axel.hahn@unibe.ch>      php8 only; added variable types; short array syntax
 * ======================================================================
 */

$bDebug = false;
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

/**
 * Path to deployment classes
 * @var string
 */
 $sDirClasses = __DIR__ . '/../deployment/classes/';

 /**
 * Allowed time delta for client or server
 * @var integer
 */
$iMaxAge = 60;

require_once("../../config/inc_projects_config.php");

require_once($sDirClasses . '/project.class.php');
require_once($sDirClasses . 'logger.class.php');

// ----------------------------------------------------------------------
// FUNCTIONS
// ----------------------------------------------------------------------
/**
 * Write debug text (if enabled)
 * 
 * @global boolean $bDebug
 * 
 * @param string  $s       message
 * @param string  $sLevel  level; one of info|
 * @return boolean
 */
function _wd(string $s, string $sLevel = 'info'): bool
{
    global $bDebug;
    if ($bDebug) {
        echo "<div class=\"debug debug-$sLevel\">DEBUG: $s</div>";
    }
    return true;
}

/**
 * Abort execution of API request with error
 * 
 * @param string   $s        message
 * @param integer  $iStatus  http status code to send
 */
function _quit(string $s, int $iStatus = 400): void
{
    $aStatus = [
        400 => 'HTTP/1.0 400 Bad Request',
        401 => 'HTTP/1.0 401 Unauthorized',
        403 => 'HTTP/1.0 403 Access denied',
        404 => 'HTTP/1.0 404 Not found',
    ];
    header($aStatus[$iStatus]);
    _done(['status' => $iStatus, 'info' => $aStatus[$iStatus], 'message' => $s]);
}

/**
 * End with OK output
 * 
 * @param array $Data  array data to show as JSON
 * @return void
 */
function _done(array $Data): void
{
    echo is_array($Data)
        ? json_encode($Data, 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 string $sProjectSecret
 * @return boolean
 */
function _checkAuth(string $sProjectSecret): bool
{
    global $iMaxAge;
    $aReqHeaders = apache_request_headers();
    _wd('<pre>' . print_r($aReqHeaders, 1) . '</pre>');
    if (!isset($aReqHeaders['Authorization'])) {
        _quit('Access denied. Missing authorization.', 401);
    }
    if (!isset($aReqHeaders['Date'])) {
        _quit('Access denied. Missing field "Date:" in the request header.', 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.', 401);
    }

    $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. Maybe client or server is out of sync.', 403);
    }
    if ($iAge < -$iMaxAge) {
        _quit('Access denied. Hash is ' . $iAge . ' sec in future but only ' . $iMaxAge . ' sec are allowed. Maybe client or server is out of sync.', 403);
    }
    return true;
}
// ----------------------------------------------------------------------
// MAIN
// ----------------------------------------------------------------------
if (!$bDebug) {
    header('Content-Type: application/json');
}
_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/...
$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('ERROR: no param for api version was found.');
}
if (!$sApiItem) {
    _quit('ERROR: no param for item was found.');
}


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;
                $sBranch = implode('/', array_slice($aUriSplit, 4));

                // $sParam4    = isset($aUriSplit[4]) ? $aUriSplit[4] : false;
                // $sParam5    = isset($aUriSplit[5]) ? $aUriSplit[5] : false;
                $sMethod = $_SERVER['REQUEST_METHOD'];
                _wd('$sPrjId = ' . $sPrjId);
                _wd('$sPrjAction = ' . $sPrjAction);
                _wd('$sBranch = ' . $sBranch);

                $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 . '] cannot be initialized.', 400); // never reached
                }

                // get secret
                $aPrjCfg = $oProject->getConfig();
                if(!count($aPrjCfg)){
                    _quit('ERROR: project with id [' . $sPrjId . '] does not exist.', 404);
                }

                $sProjectSecret = $aPrjCfg['api']['secret'] ?? false;
                if (!$sProjectSecret) {
                    _quit('Access denied. API access is disabled.', 403);
                }

                // check authorization 
                _checkAuth($sProjectSecret);

                // echo "OK: request was authorized successfully.\n";

                $oProject->oUser->setUser('api');

                switch ($sPrjAction) {
                    case "build":
                        if ($sBranch) {
                            $aResult = $oProject->setBranchname($sBranch);
                        }
                        $sBranchname = $oProject->getBranchname();
                        $aRepodata = $oProject->getRemoteBranches(true); // ignore cache = 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([
                                '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:
                _quit('ERROR: item [' . $sApiItem . '] is invalid.');
        }
        break;
    default:
        _quit('ERROR: Wrong (unsupported) api version.');
}