update
This commit is contained in:
281
application/libraries/GCalService.php
Normal file
281
application/libraries/GCalService.php
Normal file
@ -0,0 +1,281 @@
|
||||
<?php
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
class GCalService
|
||||
{
|
||||
private $jsonPath;
|
||||
private $tokenUri = 'https://oauth2.googleapis.com/token';
|
||||
private $scopes = 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events';
|
||||
|
||||
public function __construct($params = [])
|
||||
{
|
||||
$this->jsonPath = $params['jsonPath'] ?? APPPATH.'credentials/service_account.json';
|
||||
if (!file_exists($this->jsonPath)) {
|
||||
throw new Exception("Service account JSON introuvable: ".$this->jsonPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Utils
|
||||
private function b64url($data){ return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
|
||||
|
||||
private function http($method, $url, $headers = [], $body = null)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$opts = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => $headers
|
||||
];
|
||||
if ($body !== null) {
|
||||
$opts[CURLOPT_POSTFIELDS] = $body;
|
||||
}
|
||||
curl_setopt_array($ch, $opts);
|
||||
$res = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($res === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception("cURL error: $err");
|
||||
}
|
||||
curl_close($ch);
|
||||
return [$code, $res];
|
||||
}
|
||||
|
||||
private function getAccessToken($impersonateEmail)
|
||||
{
|
||||
$creds = json_decode(file_get_contents($this->jsonPath), true);
|
||||
$client_email = $creds['client_email'];
|
||||
$private_key = $creds['private_key'];
|
||||
$now = time();
|
||||
|
||||
$header = ['alg'=>'RS256','typ'=>'JWT'];
|
||||
$claim = [
|
||||
'iss' => $client_email,
|
||||
'scope' => $this->scopes,
|
||||
'aud' => $this->tokenUri,
|
||||
'exp' => $now + 3600,
|
||||
'iat' => $now,
|
||||
'sub' => $impersonateEmail // Domain-Wide Delegation
|
||||
];
|
||||
|
||||
$jwt_unsigned = $this->b64url(json_encode($header)).'.'.$this->b64url(json_encode($claim));
|
||||
$signature = '';
|
||||
if (!openssl_sign($jwt_unsigned, $signature, $private_key, 'sha256WithRSAEncryption')) {
|
||||
throw new Exception("openssl_sign failed (clé privée invalide?)");
|
||||
}
|
||||
$jwt = $jwt_unsigned.'.'.$this->b64url($signature);
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'POST',
|
||||
$this->tokenUri,
|
||||
['Content-Type: application/x-www-form-urlencoded'],
|
||||
http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt
|
||||
])
|
||||
);
|
||||
if ($code !== 200) {
|
||||
throw new Exception("Token error ($code): ".$body);
|
||||
}
|
||||
$tok = json_decode($body, true);
|
||||
return $tok['access_token'];
|
||||
}
|
||||
|
||||
// ----- Public API
|
||||
|
||||
/**
|
||||
* Crée un événement et retourne ['eventId'=>..., 'calendarId'=>...]
|
||||
* $payload = [
|
||||
* 'summary','description','start','end','timezone','attendees'=>[['email'=>'...'],...]
|
||||
* ]
|
||||
*/
|
||||
public function createEvent($impersonateEmail, $calendarId, array $payload)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
|
||||
// Normalisation
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
$tz = $payload['timezone'] ?? date_default_timezone_get();
|
||||
|
||||
$event = [
|
||||
'summary' => $payload['summary'] ?? '(Sans titre)',
|
||||
'description' => $payload['description'] ?? '',
|
||||
'start' => isset($payload['start']) ? ['dateTime'=>$payload['start'], 'timeZone'=>$tz] : null,
|
||||
'end' => isset($payload['end']) ? ['dateTime'=>$payload['end'], 'timeZone'=>$tz] : null,
|
||||
];
|
||||
if (!empty($payload['attendees'])) $event['attendees'] = $payload['attendees'];
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'POST',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events",
|
||||
['Authorization: Bearer '.$access, 'Content-Type: application/json'],
|
||||
json_encode($event)
|
||||
);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
throw new Exception("Create event error ($code): $body");
|
||||
}
|
||||
$resp = json_decode($body, true);
|
||||
return ['eventId'=>$resp['id'], 'calendarId'=>$calendarId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un événement existant.
|
||||
*/
|
||||
public function updateEvent($impersonateEmail, $calendarId, $eventId, array $payload)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
$tz = $payload['timezone'] ?? date_default_timezone_get();
|
||||
|
||||
$eventPatch = [];
|
||||
foreach (['summary','description'] as $k) {
|
||||
if (array_key_exists($k, $payload)) $eventPatch[$k] = $payload[$k];
|
||||
}
|
||||
if (isset($payload['start'])) $eventPatch['start'] = ['dateTime'=>$payload['start'], 'timeZone'=>$tz];
|
||||
if (isset($payload['end'])) $eventPatch['end'] = ['dateTime'=>$payload['end'], 'timeZone'=>$tz];
|
||||
if (isset($payload['attendees'])) $eventPatch['attendees'] = $payload['attendees'];
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'PATCH',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events/".rawurlencode($eventId),
|
||||
['Authorization: Bearer '.$access, 'Content-Type: application/json'],
|
||||
json_encode($eventPatch)
|
||||
);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
throw new Exception("Update event error ($code): $body");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optionnel) Suppression
|
||||
*/
|
||||
public function deleteEvent($impersonateEmail, $calendarId, $eventId)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
list($code, $body) = $this->http(
|
||||
'DELETE',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events/".rawurlencode($eventId),
|
||||
['Authorization: Bearer '.$access]
|
||||
);
|
||||
if ($code !== 204) { // 204 = No Content (OK)
|
||||
throw new Exception("Delete event error ($code): $body");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// application/libraries/GCalService.php
|
||||
|
||||
public function listEvents(string $userEmail, string $calendarId, string $timeMinIso, string $timeMaxIso): array
|
||||
{
|
||||
// ===== Résolution ROBUSTE du service Calendar sans getServiceFor() =====
|
||||
// 1) Si ta classe a déjà $this->service (comme tes create/update/delete), on l'utilise.
|
||||
if (isset($this->service)) {
|
||||
$service = $this->service;
|
||||
|
||||
// 2) Si tu as une méthode getClient() (souvent le cas dans tes libs Google),
|
||||
// on l'emploie pour construire le service.
|
||||
} elseif (method_exists($this, 'getClient')) {
|
||||
$client = $this->getClient($userEmail); // si getClient n'accepte pas d'email, appelle-la sans argument
|
||||
$service = new Google_Service_Calendar($client);
|
||||
|
||||
// 3) Si tu exposes un client brut ($this->client), on crée le service à partir de là.
|
||||
} elseif (isset($this->client)) {
|
||||
$service = new Google_Service_Calendar($this->client);
|
||||
|
||||
// 4) Si tu as une propriété $this->calendar ou $this->calendarService déjà prête.
|
||||
} elseif (isset($this->calendar)) {
|
||||
$service = $this->calendar;
|
||||
} elseif (isset($this->calendarService)) {
|
||||
$service = $this->calendarService;
|
||||
|
||||
} else {
|
||||
// Dernier recours : on tente une méthode "buildService" si elle existe,
|
||||
// sinon on lève une exception claire pour que le log te guide.
|
||||
if (method_exists($this, 'buildService')) {
|
||||
$service = $this->buildService($userEmail);
|
||||
} else {
|
||||
throw new Exception('GCalService: aucun service Calendar initialisé (ni $this->service, ni getClient(), ni $this->client).');
|
||||
}
|
||||
}
|
||||
// ======================================================================
|
||||
|
||||
$opt = [
|
||||
'timeMin' => $timeMinIso, // RFC3339
|
||||
'timeMax' => $timeMaxIso, // RFC3339
|
||||
'singleEvents' => true, // déroule les occurrences
|
||||
'orderBy' => 'startTime',
|
||||
'maxResults' => 2500,
|
||||
];
|
||||
|
||||
$items = [];
|
||||
do {
|
||||
$resp = $service->events->listEvents($calendarId, $opt);
|
||||
|
||||
// Google renvoie des objets; on normalise pour le helper gcal_find_slot_minute()
|
||||
foreach ($resp->getItems() as $ev) {
|
||||
$start = $ev->getStart();
|
||||
$end = $ev->getEnd();
|
||||
$items[] = [
|
||||
'start' => [
|
||||
'dateTime' => $start ? $start->getDateTime() : null,
|
||||
'date' => $start ? $start->getDate() : null,
|
||||
],
|
||||
'end' => [
|
||||
'dateTime' => $end ? $end->getDateTime() : null,
|
||||
'date' => $end ? $end->getDate() : null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$opt['pageToken'] = $resp->getNextPageToken();
|
||||
} while (!empty($opt['pageToken']));
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
// Retourne les intervalles occupés [start,end) sous forme d'array de paires [DateTime $s, DateTime $e]
|
||||
public function freeBusy(string $userEmail, string $calendarId, string $timeMinIso, string $timeMaxIso, string $tz = 'America/Toronto'): array
|
||||
{
|
||||
// Initialisation du service identique à tes autres méthodes
|
||||
if (isset($this->service)) {
|
||||
$service = $this->service;
|
||||
} elseif (method_exists($this, 'getClient')) {
|
||||
$client = $this->getClient(); // adapte si ta getClient prend $userEmail
|
||||
$service = new Google_Service_Calendar($client);
|
||||
} elseif (isset($this->client)) {
|
||||
$service = new Google_Service_Calendar($this->client);
|
||||
} else {
|
||||
throw new Exception('GCalService: service Calendar non initialisé pour freeBusy().');
|
||||
}
|
||||
|
||||
// Requête FreeBusy
|
||||
$fbReq = new Google_Service_Calendar_FreeBusyRequest();
|
||||
$fbReq->setTimeMin($timeMinIso);
|
||||
$fbReq->setTimeMax($timeMaxIso);
|
||||
$fbReq->setTimeZone($tz);
|
||||
|
||||
$item = new Google_Service_Calendar_FreeBusyRequestItem();
|
||||
$item->setId($calendarId); // "primary" ou un ID spécifique
|
||||
$fbReq->setItems([$item]);
|
||||
|
||||
$resp = $service->freebusy->query($fbReq);
|
||||
$cals = $resp->getCalendars();
|
||||
|
||||
$busy = [];
|
||||
if (isset($cals[$calendarId])) {
|
||||
$blocks = $cals[$calendarId]['busy'] ?? [];
|
||||
foreach ($blocks as $b) {
|
||||
// $b['start'] et $b['end'] sont des RFC3339
|
||||
$s = new DateTime($b['start']);
|
||||
$e = new DateTime($b['end']);
|
||||
// On ne change pas le fuseau ici: Google renvoie des instants absolus.
|
||||
$busy[] = [$s, $e];
|
||||
}
|
||||
}
|
||||
|
||||
return $busy;
|
||||
}
|
||||
|
||||
}
|
||||
281
application/libraries/GCalService_avant.php
Normal file
281
application/libraries/GCalService_avant.php
Normal file
@ -0,0 +1,281 @@
|
||||
<?php
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
class GCalService_avant
|
||||
{
|
||||
private $jsonPath;
|
||||
private $tokenUri = 'https://oauth2.googleapis.com/token';
|
||||
private $scopes = 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events';
|
||||
|
||||
public function __construct($params = [])
|
||||
{
|
||||
$this->jsonPath = $params['jsonPath'] ?? APPPATH.'credentials/service_account.json';
|
||||
if (!file_exists($this->jsonPath)) {
|
||||
throw new Exception("Service account JSON introuvable: ".$this->jsonPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Utils
|
||||
private function b64url($data){ return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
|
||||
|
||||
private function http($method, $url, $headers = [], $body = null)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$opts = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => $headers
|
||||
];
|
||||
if ($body !== null) {
|
||||
$opts[CURLOPT_POSTFIELDS] = $body;
|
||||
}
|
||||
curl_setopt_array($ch, $opts);
|
||||
$res = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($res === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception("cURL error: $err");
|
||||
}
|
||||
curl_close($ch);
|
||||
return [$code, $res];
|
||||
}
|
||||
|
||||
private function getAccessToken($impersonateEmail)
|
||||
{
|
||||
$creds = json_decode(file_get_contents($this->jsonPath), true);
|
||||
$client_email = $creds['client_email'];
|
||||
$private_key = $creds['private_key'];
|
||||
$now = time();
|
||||
|
||||
$header = ['alg'=>'RS256','typ'=>'JWT'];
|
||||
$claim = [
|
||||
'iss' => $client_email,
|
||||
'scope' => $this->scopes,
|
||||
'aud' => $this->tokenUri,
|
||||
'exp' => $now + 3600,
|
||||
'iat' => $now,
|
||||
'sub' => $impersonateEmail // Domain-Wide Delegation
|
||||
];
|
||||
|
||||
$jwt_unsigned = $this->b64url(json_encode($header)).'.'.$this->b64url(json_encode($claim));
|
||||
$signature = '';
|
||||
if (!openssl_sign($jwt_unsigned, $signature, $private_key, 'sha256WithRSAEncryption')) {
|
||||
throw new Exception("openssl_sign failed (clé privée invalide?)");
|
||||
}
|
||||
$jwt = $jwt_unsigned.'.'.$this->b64url($signature);
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'POST',
|
||||
$this->tokenUri,
|
||||
['Content-Type: application/x-www-form-urlencoded'],
|
||||
http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt
|
||||
])
|
||||
);
|
||||
if ($code !== 200) {
|
||||
throw new Exception("Token error ($code): ".$body);
|
||||
}
|
||||
$tok = json_decode($body, true);
|
||||
return $tok['access_token'];
|
||||
}
|
||||
|
||||
// ----- Public API
|
||||
|
||||
/**
|
||||
* Crée un événement et retourne ['eventId'=>..., 'calendarId'=>...]
|
||||
* $payload = [
|
||||
* 'summary','description','start','end','timezone','attendees'=>[['email'=>'...'],...]
|
||||
* ]
|
||||
*/
|
||||
public function createEvent($impersonateEmail, $calendarId, array $payload)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
|
||||
// Normalisation
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
$tz = $payload['timezone'] ?? date_default_timezone_get();
|
||||
|
||||
$event = [
|
||||
'summary' => $payload['summary'] ?? '(Sans titre)',
|
||||
'description' => $payload['description'] ?? '',
|
||||
'start' => isset($payload['start']) ? ['dateTime'=>$payload['start'], 'timeZone'=>$tz] : null,
|
||||
'end' => isset($payload['end']) ? ['dateTime'=>$payload['end'], 'timeZone'=>$tz] : null,
|
||||
];
|
||||
if (!empty($payload['attendees'])) $event['attendees'] = $payload['attendees'];
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'POST',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events",
|
||||
['Authorization: Bearer '.$access, 'Content-Type: application/json'],
|
||||
json_encode($event)
|
||||
);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
throw new Exception("Create event error ($code): $body");
|
||||
}
|
||||
$resp = json_decode($body, true);
|
||||
return ['eventId'=>$resp['id'], 'calendarId'=>$calendarId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un événement existant.
|
||||
*/
|
||||
public function updateEvent($impersonateEmail, $calendarId, $eventId, array $payload)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
$tz = $payload['timezone'] ?? date_default_timezone_get();
|
||||
|
||||
$eventPatch = [];
|
||||
foreach (['summary','description'] as $k) {
|
||||
if (array_key_exists($k, $payload)) $eventPatch[$k] = $payload[$k];
|
||||
}
|
||||
if (isset($payload['start'])) $eventPatch['start'] = ['dateTime'=>$payload['start'], 'timeZone'=>$tz];
|
||||
if (isset($payload['end'])) $eventPatch['end'] = ['dateTime'=>$payload['end'], 'timeZone'=>$tz];
|
||||
if (isset($payload['attendees'])) $eventPatch['attendees'] = $payload['attendees'];
|
||||
|
||||
list($code, $body) = $this->http(
|
||||
'PATCH',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events/".rawurlencode($eventId),
|
||||
['Authorization: Bearer '.$access, 'Content-Type: application/json'],
|
||||
json_encode($eventPatch)
|
||||
);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
throw new Exception("Update event error ($code): $body");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optionnel) Suppression
|
||||
*/
|
||||
public function deleteEvent($impersonateEmail, $calendarId, $eventId)
|
||||
{
|
||||
$access = $this->getAccessToken($impersonateEmail);
|
||||
$calendarId = $calendarId ?: 'primary';
|
||||
list($code, $body) = $this->http(
|
||||
'DELETE',
|
||||
"https://www.googleapis.com/calendar/v3/calendars/".rawurlencode($calendarId)."/events/".rawurlencode($eventId),
|
||||
['Authorization: Bearer '.$access]
|
||||
);
|
||||
if ($code !== 204) { // 204 = No Content (OK)
|
||||
throw new Exception("Delete event error ($code): $body");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// application/libraries/GCalService.php
|
||||
|
||||
public function listEvents(string $userEmail, string $calendarId, string $timeMinIso, string $timeMaxIso): array
|
||||
{
|
||||
// ===== Résolution ROBUSTE du service Calendar sans getServiceFor() =====
|
||||
// 1) Si ta classe a déjà $this->service (comme tes create/update/delete), on l'utilise.
|
||||
if (isset($this->service)) {
|
||||
$service = $this->service;
|
||||
|
||||
// 2) Si tu as une méthode getClient() (souvent le cas dans tes libs Google),
|
||||
// on l'emploie pour construire le service.
|
||||
} elseif (method_exists($this, 'getClient')) {
|
||||
$client = $this->getClient($userEmail); // si getClient n'accepte pas d'email, appelle-la sans argument
|
||||
$service = new Google_Service_Calendar($client);
|
||||
|
||||
// 3) Si tu exposes un client brut ($this->client), on crée le service à partir de là.
|
||||
} elseif (isset($this->client)) {
|
||||
$service = new Google_Service_Calendar($this->client);
|
||||
|
||||
// 4) Si tu as une propriété $this->calendar ou $this->calendarService déjà prête.
|
||||
} elseif (isset($this->calendar)) {
|
||||
$service = $this->calendar;
|
||||
} elseif (isset($this->calendarService)) {
|
||||
$service = $this->calendarService;
|
||||
|
||||
} else {
|
||||
// Dernier recours : on tente une méthode "buildService" si elle existe,
|
||||
// sinon on lève une exception claire pour que le log te guide.
|
||||
if (method_exists($this, 'buildService')) {
|
||||
$service = $this->buildService($userEmail);
|
||||
} else {
|
||||
throw new Exception('GCalService: aucun service Calendar initialisé (ni $this->service, ni getClient(), ni $this->client).');
|
||||
}
|
||||
}
|
||||
// ======================================================================
|
||||
|
||||
$opt = [
|
||||
'timeMin' => $timeMinIso, // RFC3339
|
||||
'timeMax' => $timeMaxIso, // RFC3339
|
||||
'singleEvents' => true, // déroule les occurrences
|
||||
'orderBy' => 'startTime',
|
||||
'maxResults' => 2500,
|
||||
];
|
||||
|
||||
$items = [];
|
||||
do {
|
||||
$resp = $service->events->listEvents($calendarId, $opt);
|
||||
|
||||
// Google renvoie des objets; on normalise pour le helper gcal_find_slot_minute()
|
||||
foreach ($resp->getItems() as $ev) {
|
||||
$start = $ev->getStart();
|
||||
$end = $ev->getEnd();
|
||||
$items[] = [
|
||||
'start' => [
|
||||
'dateTime' => $start ? $start->getDateTime() : null,
|
||||
'date' => $start ? $start->getDate() : null,
|
||||
],
|
||||
'end' => [
|
||||
'dateTime' => $end ? $end->getDateTime() : null,
|
||||
'date' => $end ? $end->getDate() : null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$opt['pageToken'] = $resp->getNextPageToken();
|
||||
} while (!empty($opt['pageToken']));
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
// Retourne les intervalles occupés [start,end) sous forme d'array de paires [DateTime $s, DateTime $e]
|
||||
public function freeBusy(string $userEmail, string $calendarId, string $timeMinIso, string $timeMaxIso, string $tz = 'America/Toronto'): array
|
||||
{
|
||||
// Initialisation du service identique à tes autres méthodes
|
||||
if (isset($this->service)) {
|
||||
$service = $this->service;
|
||||
} elseif (method_exists($this, 'getClient')) {
|
||||
$client = $this->getClient(); // adapte si ta getClient prend $userEmail
|
||||
$service = new Google_Service_Calendar($client);
|
||||
} elseif (isset($this->client)) {
|
||||
$service = new Google_Service_Calendar($this->client);
|
||||
} else {
|
||||
throw new Exception('GCalService: service Calendar non initialisé pour freeBusy().');
|
||||
}
|
||||
|
||||
// Requête FreeBusy
|
||||
$fbReq = new Google_Service_Calendar_FreeBusyRequest();
|
||||
$fbReq->setTimeMin($timeMinIso);
|
||||
$fbReq->setTimeMax($timeMaxIso);
|
||||
$fbReq->setTimeZone($tz);
|
||||
|
||||
$item = new Google_Service_Calendar_FreeBusyRequestItem();
|
||||
$item->setId($calendarId); // "primary" ou un ID spécifique
|
||||
$fbReq->setItems([$item]);
|
||||
|
||||
$resp = $service->freebusy->query($fbReq);
|
||||
$cals = $resp->getCalendars();
|
||||
|
||||
$busy = [];
|
||||
if (isset($cals[$calendarId])) {
|
||||
$blocks = $cals[$calendarId]['busy'] ?? [];
|
||||
foreach ($blocks as $b) {
|
||||
// $b['start'] et $b['end'] sont des RFC3339
|
||||
$s = new DateTime($b['start']);
|
||||
$e = new DateTime($b['end']);
|
||||
// On ne change pas le fuseau ici: Google renvoie des instants absolus.
|
||||
$busy[] = [$s, $e];
|
||||
}
|
||||
}
|
||||
|
||||
return $busy;
|
||||
}
|
||||
|
||||
}
|
||||
224
application/libraries/GoogleDriveDocs.php
Normal file
224
application/libraries/GoogleDriveDocs.php
Normal file
@ -0,0 +1,224 @@
|
||||
<?php
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
class GoogleDriveDocs
|
||||
{
|
||||
protected $CI;
|
||||
|
||||
// Scopes
|
||||
const SCOPE_DRIVE = 'https://www.googleapis.com/auth/drive';
|
||||
const SCOPE_DOCS = 'https://www.googleapis.com/auth/documents';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI =& get_instance();
|
||||
// Doit être chargé dans le contrôleur ou le modèle avant usage,
|
||||
// mais on le fait “au cas où”
|
||||
if (!isset($this->CI->googleservicejwt)) {
|
||||
$this->CI->load->library('GoogleServiceJWT');
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
* HTTP helpers (Bearer token)
|
||||
* ----------------------------- */
|
||||
private function requestJson(string $method, string $url, string $token, ?string $body = null, array $extraHeaders = [])
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$headers = array_merge([
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Accept: application/json',
|
||||
], $extraHeaders);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 3,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CUSTOMREQUEST => strtoupper($method),
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
|
||||
$out = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($err) {
|
||||
log_message('error', '[GoogleDriveDocs] CURL error '.$err.' for '.$url);
|
||||
throw new Exception('HTTP error: ' . $err);
|
||||
}
|
||||
|
||||
$json = json_decode($out, true);
|
||||
if ($code >= 400) {
|
||||
$msg = is_array($json) ? json_encode($json) : $out;
|
||||
log_message('error', "[GoogleDriveDocs] HTTP $code: $msg");
|
||||
throw new Exception("HTTP $code: $msg");
|
||||
}
|
||||
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
public function getJson(string $url, string $token): array
|
||||
{
|
||||
return $this->requestJson('GET', $url, $token, null);
|
||||
}
|
||||
|
||||
public function postJson(string $url, string $token, string $jsonBody): array
|
||||
{
|
||||
return $this->requestJson('POST', $url, $token, $jsonBody, ['Content-Type: application/json']);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------
|
||||
* DRIVE (Shared drives OK) — Service Account JWT
|
||||
* ------------------------------------------------ */
|
||||
|
||||
/** Liste les sous-dossiers directs d’un parent (uniquement folders). */
|
||||
/** Liste les sous-dossiers directs d’un parent (uniquement folders), avec pagination complète. */
|
||||
public function listChildrenFolders(string $parentId, ?string $pageToken = null, int $pageSize = 1000): array
|
||||
{
|
||||
$token = $this->CI->googleservicejwt->getAccessToken(self::SCOPE_DRIVE);
|
||||
|
||||
$files = [];
|
||||
$q = sprintf(
|
||||
"'%s' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
|
||||
$parentId
|
||||
);
|
||||
|
||||
do {
|
||||
$params = [
|
||||
'supportsAllDrives' => 'true',
|
||||
'includeItemsFromAllDrives' => 'true',
|
||||
'corpora' => 'allDrives',
|
||||
'q' => $q,
|
||||
'orderBy' => 'name',
|
||||
'pageSize' => $pageSize, // 1000 max par appel
|
||||
'fields' => 'nextPageToken, files(id,name)'
|
||||
];
|
||||
if ($pageToken) {
|
||||
$params['pageToken'] = $pageToken;
|
||||
}
|
||||
|
||||
$url = 'https://www.googleapis.com/drive/v3/files?' . http_build_query($params);
|
||||
$resp = $this->getJson($url, $token);
|
||||
foreach (($resp['files'] ?? []) as $f) {
|
||||
if (!empty($f['id']) && !empty($f['name'])) {
|
||||
$files[] = $f; // on garde le format attendu par GoogleDrive::list_folders()
|
||||
}
|
||||
}
|
||||
$pageToken = $resp['nextPageToken'] ?? null;
|
||||
} while ($pageToken);
|
||||
|
||||
// IMPORTANT : GoogleDrive::list_folders() lit $res['files']
|
||||
return ['files' => $files];
|
||||
}
|
||||
|
||||
/** Métadonnées (id, name, parents, driveId, ownedByMe) d’un fichier/dossier. */
|
||||
public function getFileMeta(string $fileId): array
|
||||
{
|
||||
$token = $this->CI->googleservicejwt->getAccessToken(self::SCOPE_DRIVE);
|
||||
|
||||
$url = 'https://www.googleapis.com/drive/v3/files/' . rawurlencode($fileId) . '?' . http_build_query([
|
||||
'supportsAllDrives' => 'true',
|
||||
'fields' => 'id,name,parents,driveId,ownedByMe'
|
||||
]);
|
||||
|
||||
return $this->getJson($url, $token) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère (si existe) ou crée un Google Docs par NOM dans un dossier donné.
|
||||
* Retourne le fileId. Création directe dans le dossier => propriété = Drive/Shared drive, pas un utilisateur.
|
||||
*/
|
||||
public function ensureDocInFolderByName(string $folderId, string $name): string
|
||||
{
|
||||
$token = $this->CI->googleservicejwt->getAccessToken(self::SCOPE_DRIVE);
|
||||
|
||||
// 1) Cherche un doc (Google Docs) strictement par nom + parent
|
||||
$qName = addcslashes($name, "'");
|
||||
$listUrl = 'https://www.googleapis.com/drive/v3/files?' . http_build_query([
|
||||
'supportsAllDrives' => 'true',
|
||||
'includeItemsFromAllDrives' => 'true',
|
||||
'q' => "name='{$qName}' and '{$folderId}' in parents and mimeType='application/vnd.google-apps.document' and trashed=false",
|
||||
'fields' => 'files(id,name,driveId)'
|
||||
]);
|
||||
$found = $this->getJson($listUrl, $token);
|
||||
if (!empty($found['files'][0]['id'])) {
|
||||
return $found['files'][0]['id'];
|
||||
}
|
||||
|
||||
// 2) Crée le doc dans ce dossier (donc sous le drive partagé s’il s’agit d’un Shared drive)
|
||||
$createUrl = 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true';
|
||||
$body = json_encode([
|
||||
'name' => $name,
|
||||
'mimeType' => 'application/vnd.google-apps.document',
|
||||
'parents' => [$folderId],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$created = $this->postJson($createUrl, $token, $body);
|
||||
if (empty($created['id'])) {
|
||||
throw new Exception('Impossible de créer le document Google Docs.');
|
||||
}
|
||||
return $created['id'];
|
||||
}
|
||||
|
||||
/* -----------------------
|
||||
* DOCS helpers (JWT)
|
||||
* ----------------------- */
|
||||
|
||||
/** Lit un document Docs (pour récupérer endIndex, etc.). */
|
||||
public function getDocument(string $docId): array
|
||||
{
|
||||
$token = $this->CI->googleservicejwt->getAccessToken(self::SCOPE_DOCS);
|
||||
$url = 'https://docs.googleapis.com/v1/documents/' . rawurlencode($docId);
|
||||
return $this->getJson($url, $token) ?: [];
|
||||
}
|
||||
|
||||
/** Envoie un batchUpdate à Docs API. */
|
||||
public function batchUpdateDocument(string $docId, array $requests): array
|
||||
{
|
||||
$token = $this->CI->googleservicejwt->getAccessToken(self::SCOPE_DOCS);
|
||||
$url = 'https://docs.googleapis.com/v1/documents/' . rawurlencode($docId) . ':batchUpdate';
|
||||
$body = json_encode(['requests' => $requests], JSON_UNESCAPED_UNICODE);
|
||||
return $this->postJson($url, $token, $body);
|
||||
}
|
||||
|
||||
/** Efface le contenu (sauf le dernier caractère) pour “réécrire proprement”. */
|
||||
public function clearDocument(string $docId): void
|
||||
{
|
||||
// On lit le document pour connaître l’index de fin
|
||||
$doc = $this->getDocument($docId);
|
||||
$content = $doc['body']['content'] ?? [];
|
||||
if (!$content) {
|
||||
return; // rien à effacer
|
||||
}
|
||||
|
||||
// Google Docs garde un newline terminal non-supprimable
|
||||
$last = $content[count($content) - 1] ?? [];
|
||||
$end = isset($last['endIndex']) ? (int)$last['endIndex'] : null;
|
||||
if (!$end || $end <= 2) {
|
||||
// 1 = tout début, 2 = juste le newline terminal: rien à effacer
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ Ne jamais inclure le newline terminal dans la suppression
|
||||
$safeEnd = max(2, $end - 1);
|
||||
|
||||
$this->batchUpdateDocument($docId, [[
|
||||
'deleteContentRange' => [
|
||||
'range' => [
|
||||
'startIndex' => 1,
|
||||
'endIndex' => $safeEnd
|
||||
]
|
||||
]
|
||||
]]);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
0
application/libraries/GoogleJwtNoVendor.php
Normal file
0
application/libraries/GoogleJwtNoVendor.php
Normal file
114
application/libraries/GoogleServiceJWT.php
Normal file
114
application/libraries/GoogleServiceJWT.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
class GoogleServiceJWT
|
||||
{
|
||||
protected $CI;
|
||||
protected $creds;
|
||||
protected $cacheFile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI =& get_instance();
|
||||
$cfg = $this->CI->config->item('google');
|
||||
if (!$cfg || empty($cfg['credentials_path'])) {
|
||||
throw new Exception('Config google.credentials_path manquante');
|
||||
}
|
||||
$json = @file_get_contents($cfg['credentials_path']);
|
||||
if (!$json) {
|
||||
throw new Exception('Impossible de lire le service_account.json');
|
||||
}
|
||||
$this->creds = json_decode($json, true);
|
||||
if (empty($this->creds['client_email']) || empty($this->creds['private_key'])) {
|
||||
throw new Exception('JSON service account invalide (client_email/private_key).');
|
||||
}
|
||||
$cacheKey = md5(($cfg['credentials_path'] ?? '').'|'.php_uname(). '|' . phpversion());
|
||||
$this->cacheFileBase = sys_get_temp_dir().'/google_token_cache_'.$cacheKey.'_';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $scopes Scopes séparés par des espaces (ex: "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents")
|
||||
* @param ?string $impersonateEmail Email à impersonner (optionnel; utile pour Calendar si Domain-wide Delegation)
|
||||
*/
|
||||
public function getAccessToken(string $scopes, ?string $impersonateEmail = null): string
|
||||
{
|
||||
$cacheFile = $this->cacheFileBase . md5($scopes.'|'.($impersonateEmail ?: ''));
|
||||
|
||||
// Cache simple (~55 min)
|
||||
if (is_file($cacheFile)) {
|
||||
$cache = json_decode(@file_get_contents($cacheFile), true);
|
||||
if (!empty($cache['access_token']) && time() < ($cache['expires_at'] ?? 0)) {
|
||||
return $cache['access_token'];
|
||||
}
|
||||
}
|
||||
|
||||
$aud = $this->creds['token_uri'] ?? 'https://oauth2.googleapis.com/token';
|
||||
$now = time();
|
||||
|
||||
$header = ['alg'=>'RS256','typ'=>'JWT'];
|
||||
$payload = [
|
||||
'iss' => $this->creds['client_email'],
|
||||
'scope' => $scopes,
|
||||
'aud' => $aud,
|
||||
'exp' => $now + 3600,
|
||||
'iat' => $now,
|
||||
];
|
||||
if ($impersonateEmail) {
|
||||
$payload['sub'] = $impersonateEmail;
|
||||
}
|
||||
|
||||
$assertion = $this->b64($header).'.'.$this->b64($payload);
|
||||
|
||||
$pkey = openssl_pkey_get_private($this->creds['private_key']);
|
||||
if (!$pkey) {
|
||||
throw new Exception('Impossible de charger la clé privée du service account.');
|
||||
}
|
||||
if (!openssl_sign($assertion, $sig, $pkey, OPENSSL_ALGO_SHA256)) {
|
||||
throw new Exception('Échec openssl_sign()');
|
||||
}
|
||||
$jwt = $assertion.'.'.$this->b64($sig);
|
||||
|
||||
// Échange JWT -> access_token
|
||||
$post = http_build_query([
|
||||
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
'assertion' => $jwt,
|
||||
]);
|
||||
|
||||
$ch = curl_init($aud);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
|
||||
]);
|
||||
$raw = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($raw === false || $code !== 200) {
|
||||
throw new Exception("Token error ($code): $raw $err");
|
||||
}
|
||||
$data = json_decode($raw, true);
|
||||
if (empty($data['access_token'])) {
|
||||
throw new Exception('Réponse token invalide: '.$raw);
|
||||
}
|
||||
|
||||
// Cache ~55 min
|
||||
$ttl = max(300, (int)($data['expires_in'] ?? 3600) - 300);
|
||||
@file_put_contents($cacheFile, json_encode([
|
||||
'access_token' => $data['access_token'],
|
||||
'expires_at' => time() + $ttl,
|
||||
]));
|
||||
|
||||
return $data['access_token'];
|
||||
}
|
||||
|
||||
private function b64($arrOrBin): string
|
||||
{
|
||||
$bin = is_string($arrOrBin) ? $arrOrBin : json_encode($arrOrBin);
|
||||
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
45
application/libraries/PDF.php
Normal file
45
application/libraries/PDF.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
if(!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
require_once FCPATH.'fpdf/fpdf.php';
|
||||
|
||||
|
||||
class PDF extends FPDF {
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
|
||||
}
|
||||
|
||||
//Page header
|
||||
public function Header() {
|
||||
$mem_adress=utf8_decode("260-414, Boul. Sir Wilfrid-Laurier,\nMont Saint-Hilaire, Québec, J3H 3N9");
|
||||
$mem_adress2=utf8_decode("Tél.: 450.464.6711 - 800.461.0754\nwww.expresstours.ca - courriel: info@expresstours.ca");
|
||||
|
||||
$this->Image(FCPATH.'img/pdf/logoxpress.jpg',10,6,60);
|
||||
// Police Arial gras 15
|
||||
$this->SetFont('Arial','',8);
|
||||
$this->Ln(4);
|
||||
// Décalage à droite
|
||||
$this->Cell(120);
|
||||
// Titre
|
||||
$this->MultiCell(100,3,$mem_adress);
|
||||
$this->Cell(120);
|
||||
$this->MultiCell(100,3,$mem_adress2);
|
||||
|
||||
$this->Ln(10);
|
||||
}
|
||||
|
||||
// Page footer
|
||||
public function Footer() {
|
||||
// Position at 15 mm from bottom
|
||||
$this->SetY(-15);
|
||||
// Set font
|
||||
$this->SetFont('helvetica', 'I', 8);
|
||||
}
|
||||
}
|
||||
|
||||
/* End of file Pdf.php */
|
||||
/* Location: ./application/libraries/Pdf.php */
|
||||
20
application/libraries/PDFI.php
Normal file
20
application/libraries/PDFI.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
if(!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
require_once FCPATH.'fpdf/src/Fpdi.php';
|
||||
|
||||
|
||||
class xPDFI extends PDFI {
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* End of file Pdf.php */
|
||||
/* Location: ./application/libraries/Pdf.php */
|
||||
37
application/libraries/Pdf_reservation.php
Normal file
37
application/libraries/Pdf_reservation.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
if(!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
require_once FCPATH.'fpdf/fpdf.php';
|
||||
|
||||
class Pdf_reservation extends FPDF {
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
//Page header
|
||||
public function Header() {
|
||||
$this->Image('logo.png',10,6,30);
|
||||
// Police Arial gras 15
|
||||
$this->SetFont('Arial','B',15);
|
||||
// Décalage à droite
|
||||
$this->Cell(80);
|
||||
// Titre
|
||||
$this->Cell(30,10,'Titre',1,0,'C');
|
||||
// Saut de ligne
|
||||
$this->Ln(20);
|
||||
}
|
||||
|
||||
// Page footer
|
||||
public function Footer() {
|
||||
// Position at 15 mm from bottom
|
||||
$this->SetY(-15);
|
||||
// Set font
|
||||
$this->SetFont('helvetica', 'I', 8);
|
||||
// Page number
|
||||
$this->Cell(0, 10, 'Page '.$this->getAliasNumPage().'/'.$this->getAliasNbPages(), 0, false, 'C', 0, '', 0, false, 'T', 'M');
|
||||
}
|
||||
}
|
||||
|
||||
/* End of file Pdf.php */
|
||||
/* Location: ./application/libraries/Pdf.php */
|
||||
11
application/libraries/index.html
Normal file
11
application/libraries/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Directory access is forbidden.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user