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