| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <?php
- declare(strict_types=1);
- header('Content-Type: application/json');
- ini_set('display_errors','0');
- error_reporting(E_ALL);
- ob_start(); // capture any stray output
- require __DIR__ . '/vendor/autoload.php';
- require_once __DIR__ . '/gapi_bootstrap.php';
- use Google\Service\Drive;
- use Google\Service\Drive\DriveFile;
- use Google\Service\Drive\Permission;
- use Google\Service\Docs;
- use Google\Service\Docs\BatchUpdateDocumentRequest;
- /* ---------- helpers ---------- */
- function md_to_html(string $md): string {
- if (class_exists('Parsedown')) {
- $p = new \Parsedown(); $p->setBreaksEnabled(true);
- return $p->text($md);
- }
- // tiny fallback (headings + lists + paragraphs)
- $h = htmlspecialchars($md, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
- $h = preg_replace('/^######\s*(.+)$/m','<h6>$1</h6>',$h);
- $h = preg_replace('/^#####\s*(.+)$/m','<h5>$1</h5>',$h);
- $h = preg_replace('/^####\s*(.+)$/m','<h4>$1</h4>',$h);
- $h = preg_replace('/^###\s*(.+)$/m','<h3>$1</h3>',$h);
- $h = preg_replace('/^##\s*(.+)$/m','<h2>$1</h2>',$h);
- $h = preg_replace('/^#\s*(.+)$/m','<h1>$1</h1>',$h);
- $h = preg_replace('/^\s*[-*]\s+(.+)$/m','<li>$1</li>',$h);
- if (preg_match_all('/(<li>.*<\/li>)/sU',$h,$m)) {
- foreach ($m[0] as $block) $h = str_replace($block,"<ul>$block</ul>",$h);
- }
- $h = preg_replace('/(?:\r?\n){2,}/', "</p>\n<p>", $h);
- return "<p>{$h}</p>";
- }
- function build_cover_and_body_html(array $ctx, string $bodyHtml): string {
- $address = htmlspecialchars($ctx['address'] ?? 'Planning Report', ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
- $preparedFor = htmlspecialchars($ctx['prepared_for'] ?? '—', ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
- $preparedBy = htmlspecialchars($ctx['prepared_by'] ?? 'Modulos Design', ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
- $when = date('j F Y');
- $intent = empty($ctx['project_intent']) ? '' :
- '<p><strong>Project intent:</strong> '.htmlspecialchars($ctx['project_intent'], ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8').'</p>';
- return <<<HTML
- <html><head><meta charset="utf-8"></head>
- <body style="font-family: Arial, sans-serif;">
- <div style="page-break-after:always">
- <h1>Supporting Planning Report</h1>
- <div><strong>Address:</strong> {$address}</div>
- <div>Prepared for: <strong>{$preparedFor}</strong></div>
- <div>Prepared by: <strong>{$preparedBy}</strong></div>
- <div>Date: <strong>{$when}</strong></div>
- {$intent}
- </div>
- {$bodyHtml}
- </body></html>
- HTML;
- }
- function plain_from_html(string $html): string {
- $t = html_entity_decode(strip_tags($html), ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
- $t = preg_replace("/\r\n|\r/","\n",$t);
- $t = preg_replace("/\n{3,}/","\n\n",$t);
- return trim($t)."\n";
- }
- function json_fail(int $code, string $msg, array $extra = []): void {
- http_response_code($code);
- $noise = trim(ob_get_clean());
- $out = ['ok'=>false,'error'=>$msg] + $extra;
- if ($noise) $out['_noise'] = $noise;
- echo json_encode($out); exit;
- }
- /* ---------- main ---------- */
- try {
- $in = json_decode(file_get_contents('php://input'), true) ?: [];
- $markdown = (string)($in['markdown'] ?? '');
- $ctx = $in['context'] ?? [];
- $useTpl = !empty($in['use_template']);
- if ($markdown === '') json_fail(400, 'Missing markdown');
- // Auth (your bootstrap decides OAuth vs Service Account)
- [$client, $who] = google_client();
- $isSA = stripos($who, 'iam.gserviceaccount.com') !== false;
- $drive = new Drive($client);
- $docs = new Docs($client);
- $parentId = getenv('GDOC_PARENT_ID') ?: null;
- $parents = $parentId ? [$parentId] : [];
- $title = 'Supporting Planning Report';
- if (!empty($ctx['address'])) $title .= ' — '.$ctx['address'];
- $templateId = getenv('GDOC_TEMPLATE_ID') ?: null;
- // If using template, verify we can read it before doing anything else
- if ($useTpl) {
- if (!$templateId) {
- json_fail(400, 'Template requested but GDOC_TEMPLATE_ID is not set', [
- 'owner'=>$who,'hint'=>'Set GDOC_TEMPLATE_ID env var or uncheck “Use template”.'
- ]);
- }
- try {
- // quick permission probe
- $tplMeta = $drive->files->get($templateId, ['fields'=>'id,name,owners,driveId,parents']);
- } catch (\Google\Service\Exception $e) {
- json_fail(500, 'Cannot access template (check GDOC_TEMPLATE_ID and sharing)', [
- 'owner'=>$who,
- 'google_error'=>$e->getMessage(),
- 'template_id'=>$templateId,
- 'is_service_account'=>$isSA,
- ]);
- }
- }
- /* ---------- TEMPLATE FLOW ---------- */
- if ($useTpl && $templateId) {
- // Copy template to target folder
- $copy = $drive->files->copy(
- $templateId,
- new DriveFile(['name'=>$title, 'parents'=>$parents]),
- ['supportsAllDrives'=>true, 'fields'=>'id,webViewLink']
- );
- $docId = $copy->id;
- // Build content and replace placeholders; if {{BODY}} not found -> append
- $htmlBody = build_cover_and_body_html($ctx, md_to_html($markdown));
- $plain = plain_from_html($htmlBody);
- $resp = $docs->documents->batchUpdate($docId, new BatchUpdateDocumentRequest([
- 'requests' => [
- ['replaceAllText' => ['containsText'=>['text'=>'{{BODY}}','matchCase'=>true], 'replaceText'=>$plain]],
- ['replaceAllText' => ['containsText'=>['text'=>'{{ADDRESS}}','matchCase'=>true], 'replaceText'=>(string)($ctx['address'] ?? '')]],
- ['replaceAllText' => ['containsText'=>['text'=>'{{DATE}}','matchCase'=>true], 'replaceText'=>date('j F Y')]],
- ['replaceAllText' => ['containsText'=>['text'=>'{{PREPARED_FOR}}','matchCase'=>true], 'replaceText'=>(string)($ctx['prepared_for'] ?? '—')]],
- ['replaceAllText' => ['containsText'=>['text'=>'{{PREPARED_BY}}','matchCase'=>true], 'replaceText'=>(string)($ctx['prepared_by'] ?? 'Modulos Design')]],
- ['replaceAllText' => ['containsText'=>['text'=>'{{INTENT}}','matchCase'=>true], 'replaceText'=>(string)($ctx['project_intent'] ?? '')]],
- ]
- ]));
- // Did {{BODY}} exist?
- $occ = 0;
- $replies = $resp->getReplies();
- if ($replies && isset($replies[0]) && method_exists($replies[0],'getReplaceAllText') && $replies[0]->getReplaceAllText()) {
- $occ = (int)$replies[0]->getReplaceAllText()->getOccurrencesChanged();
- }
- if ($occ === 0) {
- $meta = $docs->documents->get($docId, ['fields'=>'body/content']);
- $content = $meta->getBody()->getContent();
- $endIndex = $content ? end($content)->getEndIndex() : 1;
- $docs->documents->batchUpdate($docId, new BatchUpdateDocumentRequest([
- 'requests' => [[ 'insertText' => ['location'=>['index'=>max(1, $endIndex-1)], 'text'=>$plain ] ]]
- ]));
- }
- // Optional: share to a user when SA is used (or always if you prefer)
- if ($email = getenv('GDOC_SHARE_EMAIL')) {
- try {
- $perm = new Permission(['type'=>'user','role'=>'writer','emailAddress'=>$email]);
- $drive->permissions->create($docId, $perm, [
- 'sendNotificationEmail'=>false, 'supportsAllDrives'=>true
- ]);
- } catch (\Google\Service\Exception $e) {
- // don’t fail the whole op for a sharing hiccup—just include detail
- $shareErr = $e->getMessage();
- }
- }
- $noise = trim(ob_get_clean());
- echo json_encode([
- 'ok'=>true, 'id'=>$docId, 'url'=>$copy->webViewLink,
- 'template'=>true, 'owner'=>$who, '_noise'=>$noise ?: null,
- 'share_error'=>$shareErr ?? null
- ]);
- exit;
- }
- /* ---------- BLANK (HTML IMPORT) FLOW ---------- */
- $fullHtml = build_cover_and_body_html($ctx, md_to_html($markdown));
- $fileMeta = new DriveFile([
- 'name'=>$title, 'mimeType'=>'application/vnd.google-apps.document', 'parents'=>$parents
- ]);
- try {
- // Attempt HTML import (best formatting)
- $created = $drive->files->create(
- $fileMeta,
- [
- 'data' => $fullHtml,
- 'mimeType' => 'text/html',
- 'uploadType' => 'multipart',
- 'supportsAllDrives' => true,
- 'fields' => 'id,webViewLink'
- ]
- );
- } catch (\Google\Service\Exception $importErr) {
- // Fallback: create blank doc then insert plain text via Docs API
- $created = $drive->files->create(
- new DriveFile(['name'=>$title,'mimeType'=>'application/vnd.google-apps.document','parents'=>$parents]),
- ['supportsAllDrives'=>true, 'fields'=>'id,webViewLink']
- );
- $plain = plain_from_html($fullHtml);
- $docs->documents->batchUpdate($created->id, new BatchUpdateDocumentRequest([
- 'requests' => [[ 'insertText' => ['location'=>['index'=>1], 'text'=>$plain ] ]]
- ]));
- $importErrorMessage = $importErr->getMessage();
- }
- if ($email = getenv('GDOC_SHARE_EMAIL')) {
- try {
- $perm = new Permission(['type'=>'user','role'=>'writer','emailAddress'=>$email]);
- $drive->permissions->create($created->id, $perm, [
- 'sendNotificationEmail'=>false, 'supportsAllDrives'=>true
- ]);
- } catch (\Google\Service\Exception $e) {
- $shareErr = $e->getMessage();
- }
- }
- $noise = trim(ob_get_clean());
- echo json_encode([
- 'ok'=>true, 'id'=>$created->id, 'url'=>$created->webViewLink,
- 'template'=>false, 'owner'=>$who, '_noise'=>$noise ?: null,
- 'import_fallback_used'=> isset($importErrorMessage),
- 'import_error'=> $importErrorMessage ?? null,
- 'share_error'=> $shareErr ?? null
- ]);
- exit;
- } catch (\Google\Service\Exception $e) {
- $noise = trim(ob_get_clean());
- $out = ['ok'=>false,'error'=>$e->getMessage(),'_noise'=>$noise ?: null];
- if (method_exists($e,'getErrors')) $out['errors'] = $e->getErrors();
- http_response_code(500); echo json_encode($out); exit;
- } catch (\Throwable $e) {
- $noise = trim(ob_get_clean());
- http_response_code(500);
- echo json_encode(['ok'=>false,'error'=>$e->getMessage(),'_noise'=>$noise ?: null]); exit;
- }
|