225 lines
8.4 KiB
PHP
225 lines
8.4 KiB
PHP
<?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 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
|
||
]
|
||
]
|
||
]]);
|
||
|
||
|
||
|
||
}
|
||
|
||
}
|