php/googleClientLib/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';
define("DTLTZ", new DateTimeZone(date_default_timezone_get()));
define("DTLFMT", 'Y-m-d\tH:i:s');

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);
}

class CloudFactory {
    public $sapi, $cfgPa, $redirUri, $goAuthToken,$goAuthCode, $goAutState;
    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/') ? '/var/cloudKey/'  # 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();
    }

    public function makenc(...$opt) {
        require_once 'nextcloud.php';
        return new NextCloud(...$opt);
    }

    public function makegodr($id, $ty, $cliN, $usr) { # make a google drive object for client/application $cliN and ResourceOwneR $rorN
        # echo "makegodr($id, $ty, $cliN, $usr)\n";
        $a = new ("GoAuth$this->sapi")($this, $cliN, $id);
        #var_dump($a);
        $gc = $a->goClient($cliN, $id);
        $dr = new GoogleDrive($gc, $id, $ty, $cliN, $usr);
        # var_dump($dr);
        return $dr;
    }

    public function make($ai) {
        if (is_array($ai))
            return $this->{"make$ai[1]"}(...$ai);
        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;
            dbg(2, 'got key', $this->key);
        }
    $j = $this->key[$ai] ?? err("cfgKey $ai not found");
    return $this->{"make$j[1]"}(...$j);
    } 

    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

class GoAuthCli {
    public $fact;
    public $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($f) {
        $this->fact = $f;
    }

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

    public function tokenPut($cliRor, $tk) {
        $pa = $this->fact->cfgPa . "{$cliRor}Token.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 $cliRor new resource owner token 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);
        out("opening google authorization: xdg-open $authUrl");
        #system("chromium --ozone-platform=wayland '$authUrl' &");
        system("xdg-open '$authUrl' &");
        out("opened google authorization: xdg-open $authUrl");
        do {
            sleep(2);
            out("waiting for you to give google authorization in browser");            
        } while(! is_file($pa));
        $cc = fgetcsv($f = fopen($pa, 'r'));
        fclose($f);
        if ($cc[0] !== $state)
            err("state mismatch got $cc[0] not as expected $state");
        out("found code $cc[1] scope $cc[2]");
        unlink($pa);
        return $cc[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);
    }

    public function goClient($cliN, $rorN) {
        /*  build and return an authorized google client
            $cliN = client name, i.e. the application/project as register at google a
                           at Konto ...Drittanbieter-Apps und -Dienste:  https://myaccount.google.com/connections 
            $rorN = resource owner Name, i.e.the user owning the drive
        */
        require_once 'googledrive.php';
        $cliPa  = $this->fact->cfgPa . "goAuthCli$cliN.json"; // path to client (application) infos/keys
        $cliRor = "goAuthCli{$cliN}Ror$rorN";
        $client = new Google_Client();
        # $client->setApplicationName($cliN); # "NQuickstart"); # seems unnecessary ....
        $client->setAuthConfig($cliPa);
        $client->setScopes($this->scopes);
        $client->setAccessType('offline');
        # $client->setDeveloperKey($apikey); # does not work: apiKey only identifies client, and prohibits any access to private data

        if ($oldToken = $this->tokenGet($cliRor)) { // get previously obtained accessToken
            $client->setAccessToken($oldToken);
            if (! $client->isAccessTokenExpired())
                return $client;
            out("resource owner $cliRor access token is expired - refreshing");
            // Refresh the token if possible, else fetch a new one.
            if ($newToken = $client->fetchAccessTokenWithRefreshToken()) {
                if ( ! isset($newToken['access_token']))
                    unset($newToken);
                else
                    out("refreshed ok");
            } 
        }
        if (! isset($newToken)) {
            // Request authorization from the user using google authorization.
            $client->setState($state = base64_encode(random_bytes(16)));
            $client->setRedirectUri($this->fact->redirUri);
            #echo "aredirUri {$this->fact->redirUri}\n";
            $authUrl = $client->createAuthUrl();
            $code = $this->codeGet($authUrl, $state);
             // Exchange authorization code for an access token.
            out("got code $code");
            $newToken = $client->fetchAccessTokenWithAuthCode($code);
            out("got newresource owner access token", $newToken);
        }
        if (! isset($newToken['access_token']))
            err("accesstoken invalid ", $newToken);
        $this->tokenPut($cliRor, $newToken);
        return $client;
    }
} # end class GoAuthCli

class GoAuthSess extends GoAuthCli {

    public function __construct($f) {
        parent::__construct($f);
     }

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

    public function tokenPut($cliRor, $tk) {
        $_SESSION[$cliRor] = $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