This commit is contained in:
2026-05-27 11:44:10 -04:00
commit 414f85ad05
31452 changed files with 3580409 additions and 0 deletions

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

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

View 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 dun parent (uniquement folders). */
/** Liste les sous-dossiers directs dun 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) dun 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é sil sagit dun 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 lindex 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
]
]
]]);
}
}

View 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), '+/', '-_'), '=');
}
}

View 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 */

View 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 */

View 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 */

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>