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