Files
crm-ms1/application/libraries/GCalService.php
2026-05-27 11:44:10 -04:00

282 lines
11 KiB
PHP

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