cloud.php

<?php

/*  ------------ google auth2: client and ResourceOwneR Authorization 
        see https://developers.google.com/identity/protocols/oauth2/web-server 
        create client/project and oauth consent screen: https://console.cloud.google.com/apis
            unter Anmeldedaten (credentials) oauth clients (nach erstelle neue Anmeldedaten)
                Typ=Webanwedung (auto generated?) für online und cli
                    nur hier bekomme ich beim editieren: Autorisierte Weiterleitungs-URIs und gebe sowohl das callback (für cli) und das online web
                Typ = Desktop funktionier für cli, cannot specify redirect uri, but is not necessary for cli
            und json herunterladen und als client authorisierungs config ==> $client->setAuthConfig(...)
        installation for google see googledrive.php

*/


require_once 'env.php';
dtzIni();
function dtzIni() {
    define("DTLTZ", new DateTimeZone(date_default_timezone_get()));
    define("DTLFMT", 'Y-m-d\TH:i:s');
    for ($c=0; $c<3; $c++) {
        $nE = exec("date '+%Y-%m-%dT%H:%M:%S%:z=%Z'");
        $nT = new DateTime();
        $nS = toLocalTst($nT) . $nT->format('P=e');
        $nI = toIsoTst($nT);
        if (str_starts_with($nE, $nI) and str_starts_with($nS, $nI))
            return dbg1("local tst matching exec $nE, local $nS, iso $nI", ", php default zone", date_default_timezone_get());
    }
    err("local tst mismatch exec $nE, local $nS, iso $nI", ", php default zone", date_default_timezone_get(), "retry $c");
}

const APILIB = 0;

function toLocalTst($t) {
    /*  DateTime constructor uses timezone in timestring, and does not ignore it as date_create_from_format(DATE_RFC7231, $t) etc..
        setTimezonew will change the time digits, so its the same moment, it even knows about summertime changes in the past years
        so ->getOffset() it a method of DateTime not TimeZone!!!
    */
    return is_null($t) ? null : (is_string($t) ? new DateTime($t) : $t)->setTimezone(DTLTZ)->format(DTLFMT);
}

function toIsoTst($t) { # ISO 8601 date (and time) including timezone offset
    return is_null($t) ? null : (is_string($t) ? new DateTime($t) : $t)->setTimezone(DTLTZ)->format('c');
}

function fRewContClo($f) {
#    if (! is_resource($f))
    $sz = ftell($f);
    dbg1("fRewContClo size $sz");
    rewind($f);
    $r = fread($f, $sz);
    fclose($f);
    return $r;
}

/*-----  (very simple) pretty print xml -
    each element on a separate line
    intendation showing the nesting level
    linefeed and intendations are inserted only before the < (tag begin char) text remains unchanged
-----*/
function xmlPP($s) {
    return preg_replace_callback(
        '=<[/?]?|[/?]?>='
        , function ($m) { 
            static $lv = 0, $laO = 0;
            if ($m[0] ==='<') {
                $laO = 1;
                $r = "\n" . str_repeat('  ', $lv++);
            } elseif ($m[0] ==='<?') {
                $r = "\n" . str_repeat('  ', $lv);
            } elseif ($m[0] ==='</') {
                $lv--;
                $r = $laO ? '' : "\n" . str_repeat('  ', $lv);
                $laO = 0;
            } else {
                $r = '';
                if ($m[0] ==='/>') {
                    $lv--; 
                    $laO = 0;
                }
            }
            return $r . $m[0];
        }
        , $s);
}

class Cloud {
    public const 
        ROWKYS = ['%' => ['name', 'type', 'id', 'modTst', 'creTst', 'size', 'owner', 'path', 'parent']]
      , DIR = '%dir' 
      , ROOT = '~/'
      , ROOTPA = '//'
      ;

    public static function pathCat($pa, $nm, $ty) { # append a further level to a path, root = ~/, undeRoot = //, append / for directories
        if (! $nm)
            return $pa;
        elseif ($pa === Cloud::ROOTPA)
            return strPos($nm, '/') === false ? Cloud::ROOT : err("pathCat($pa, $nm,)");
        else
            return ($pa === Cloud::ROOT ? '' : $pa) . ($nm[-1] !== '/' ? $nm : substr($nm, 0, -1)) . ($ty === Cloud::DIR ? '/' : '');
#        return $pa === Cloud::ROOTPA ? Cloud::ROOT : (($pa === Cloud::ROOT ? $nm : "$pa$nm") . 
    }

    public static function pathPar($pa) { # return parent directory of the given path
        return $pa === Cloud::ROOTPA ? err("no parent of " . Cloud::ROOTPA . " ") : ($pa === Cloud::ROOT ? Cloud::ROOTPA 
            : (($px=strrpos($pa, '/', -2)) ? substr($pa, 0, $px+1) : Cloud::ROOT));
    }

    public static function fn2valid($f, $isPath=false) { # in the nfts filesystem certain file names are not allowed, they are replace here by similair valid names
        if ($isPath) # no leading/trailing spaces or dots
            $r = preg_replace([';\s*/\s*;', ';\./;', ';\.$;'], ['/', '@/', '@'], trim($f));
        else
            ($r = str_replace('/', '!', trim($f, ' '))) and str_ends_with($r, '.') and $r[-1] = '@';
        $r = str_replace('*', '@', $r); // stars are illegal
        return $r === '' ? '___' : $r;
    }
}

class CloudFactory {
    public $sapi, $cfgPa, $redirUri, $goAuthToken,$goAuthCode, $goAutState, $key;
    public function __construct($at=null, $cp=null, $ru=null) {
        $this->sapi = $at ?? php_sapi_name() === 'cli' ? 'Cli' : 'Sess';
        $this->cfgPa = $cp ??  str_starts_with($rp = realpath(__FILE__), '/wkData/www/') ? '/ppKey/cloud/'  # path prefix for locallly stored keys
             : (str_starts_with($rp, '/home/ch45859/web/') ? substr($rp, 0, strpos($rp, '/', 18)) . 'private/cloudKey/'
             : err("realpath(__FILE__) $rp not supported"));
        $this->redirUri = $ru ?? $this->sapi === 'Sess' ? "$_SERVER[REQUEST_SCHEME]://$_SERVER[HTTP_HOST]$_SERVER[PHP_SELF]"
                : (gethostname() === 'wk13' ? 'http://localhost/home/inf/php/goAuth2callback.php' 
                : err("no redirUri for host " . gethostname()));
        if ($this->sapi === 'Sess')
            $this->beginSess();
    }

    /*----- create a Cloud (googledrive or nextcloud)
            $aa is either a key into cloudKey.csv or an [cloudid,type,host/client,user,pw,dir] the arguments for the constructor
                

    */

    public function key() {
        if (! isset($this->key)) {
            $this->key = [];
            $hdr = fgetcsv($fk = fopen("{$this->cfgPa}cloudKey.csv", 'r'));
            while ($r = fgetcsv($fk))
                if (count($r) > 2)
                    $this->key[$r[0]] = $r;
            fclose($fk);
            dbg1('got key', $this->key);
        }
        return $this->key;
    }
    public function make($aa) {
        if (is_array($aa)) {
            $bb = $aa;
        } else {
            $bb = $this->key()[$aa] ?? err("key $aa not found in {$this->cfgPa}cloudKey.csv");
        }
        if ($bb[1] === 'nc') {
            require_once 'nextcloud.php';
            return new NextCloud(...$bb);
        } elseif ($bb[1] === 'godr') {
            require_once 'googledrive.php';
            $clc = new ("GoAuth$this->sapi")($this, ...$bb);
            #var_dump($a);
            $dr = new GoogleDrive($clc);
            APILIB ? $dr->authorizeApiLib() : $dr->authorize();
            # var_dump($dr);
            return $dr;
        } else {
            err('cloud type $bb[1] not supported', $bb);
        }
    } 

    public function beginSess() {
        if (session_status() !== PHP_SESSION_ACTIVE)
            if (! session_start())
                err('could not start session');
        #echo "<br>\$_SESSION " . print_r($_SESSION);
        #echo "<br>\$_GET " . print_r($_GET);
        if (! (isset($_GET['code']) and isset($_GET['state']))) {
            # echo "goauth does not seem redirect from google OAUTH2";
            return;
        }
        if (! isset($_SESSION['goAuthOriginalState']))
            err ('not set $_SESSION[goAuthOriginalState]');
        if (! isset($_SESSION['goAuthOriginalGet']))
            err ('not set $_SESSION[goAuthOriginalGet]');
        if ($_GET['state'] !== $_SESSION['goAuthOriginalState'])
            err('goautSessionStart state mismatch from google OAUTH2');
        $this->goAuthCode = $_GET['code'];
        $_GET = $_SESSION['goAuthOriginalGet'];
        out("after swap goAuthCode $this->goAuthCode");
        OUT("\$_GET after swap", $_GET);
        unset($_SESSION['goAuthOriginalGet']);
        unset($_SESSION['goAuthOriginalState']);
    }

} # end class CloudFactory

/*----- GoAuthCli: Cloud Configuration (clc) for google drive in cli ----*/
class GoAuthCli {
    public $cliBsnm, $rorBsnm
        , $scopes =  
            [ 'https://www.googleapis.com/auth/drive.metadata.readonly' # drive readonly
            , 'https://www.googleapis.com/auth/drive'                   # drive file update
            , 'https://www.googleapis.com/auth/documents.readonly'      # docs readonly
            #, 'https://www.googleapis.com/auth/documents'               # docs readWrite
            ];
    public function __construct(public $fact, public $rorId, $ty, public $cliN, public $user) {
        $this->cliBsnm = "goAuthCli$cliN"; // basename without filetype to client (application) infos/keys
        $this->rorBsnm = "{$this->cliBsnm}Ror{$rorId}Token";
     }

    /*----- get the info for the client (application) from the Json exported by the google console  -----*/
    public function clientInfo($retData = true) { 
        $cliPa  = "{$this->fact->cfgPa}$this->cliBsnm.json"; // path to client (application) infos/keys
        return $retData ? json_decode(file_get_contents($cliPa), true) : $cliPa;
    }

    public function tokenGet() { # get (Authorization Access) Token for ResourceOwner for client from a file
        $pa = "{$this->fact->cfgPa}$this->rorBsnm.json"; // path to resource owner tokens
        if (! file_exists($pa))
                return null;
        $aT = json_decode(file_get_contents($pa), true);
        return $aT;
    }

    public function tokenPut($tk) {
        $pa = "{$this->fact->cfgPa}$this->rorBsnm.json"; // path to resource owner tokens
        if (!file_exists(dirname($pa))) 
            mkdir(dirname($pa), 0700, true);
        file_put_contents($pa, json_encode($tk));
        out("written $this->rorId new resource owner token for $this->cliN to $pa");
    }

    function codeGet($authUrl, $state) {
        /*  we request from google a code to authorize the resource owner
            because we are in the cli interface, we start a web browseer, and google will send the answer to an url
            our url will write the received code into a file RRCPA, that we can read
        */
        $pa = $this->fact->cfgPa . "goAuthCode.csv"; // path to resource owner tokens
        unlink($pa);
        if (0) {
            out("opening google authorization: chronium $authUrl");
            system("chromium --ozone-platform=wayland '$authUrl' &");
        } else {
            out("opening google authorization: xdg-open $authUrl");
            system("xdg-open '$authUrl' &");
        }
        out("opened google authorization: $authUrl");
        do {
            sleep(2);
            out("waiting for you to give google authorization in browser");            
        } while(! is_file($pa));
        $cd = fgetcsv($f = fopen($pa, 'r'));
        fclose($f);
        if ($cd[0] !== $state)
            err("state mismatch got $cd[0] not as expected $state");
        out("found code $cd[1] scope $cd[2]");
        unlink($pa);
        return $cd[1];
    }

   function codePut() {
        $pa = $this->fact->cfgPa . "goAuthCode.csv"; // path to resource owner tokens
        $cd = [ $_GET['state'] ?? err("codePut state not defined", $_GET)
              , $_GET['code'] ?? err("codePut code not defined", $_GET)
              , $_GET['scope'] ?? err("codePut scope not defined", $_GET)];
        out("codePut", $cd, ", writing to $pa");
        fputcsv($f = fopen($pa, 'w'), $cd);
        fclose($f);
    }

} # end class GoAuthCli

class GoAuthSess extends GoAuthCli {

    public function tokenGet() { # get Authorization Token for $cliRor (client . ResourceOwneR) from a file
        return $_SESSION[$this->rorBsnm] ?? null;
    }    

    public function tokenPut($tk) {
        $_SESSION[$this->rorBsnm] = $tk;
    }

    function codeGet($authUrl, $state) {
        /*  we request from google at $authUrl a code to authorize the resource owner
            thus we will redirect to $authUrl
            our redirectURI is this same script, that will detect the code and state in $_GET (in goAuthBeginWeb()) and put it to $goAuthWeb RRCPA, that we can read
            $state is a random string, that will be used to check, that we do not use a missdirected code
            we must save for later restore the $_GET variables
        */
        if (isset($this->fact->goAuthCode)) {
            out("returniong code". $this->fact->goAuthCode);
            return $this->fact->goAuthCode;
        }
        $_SESSION['goAuthOriginalState'] = $state;
        $_SESSION['goAuthOriginalGet'] = $_GET;
        header("location: $authUrl"); // redirect to
        exit();                       // exit the script, the redirect from google will start it new
    }

    function codePut() {
        err( __METHOD__ . ' should not be called - work is done in Cloud->beginSess');
    }
} # end class GoAuthSess