282 lines
11 KiB
PHP
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;
|
|
}
|
|
|
|
}
|