bericht/api/photo.php
Eduard Wisch a8d8291c3a API 1.5.1: Thumbnails, Aufnahmezeit, Seitenfelder, Textnotizen
Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat.

photo.php kann jetzt size=thumb&w=<px> und rendert ueber bericht_attachment_thumb()
(GD, Cache, EXIF-Rotation) statt die Originaldatei auszuliefern. size=small konnte nur
ein von Dolibarr vorgefertigtes thumbs/<name>_small.<ext> liefern - fuer Uploads ueber
orders.php?action=upload_photo gibt es das nicht, dort laeuft kein vignette(). Die PWA
lud dadurch fuer jede Kachel das komplette Foto: gemessen 9,8 KB statt 235 KB je Kachel.
Antwort mit ETag aus der mtime, Folgeaufrufe enden mit 304.

orders.php?action=photos liefert taken_at (EXIF-Aufnahmezeit ueber bericht_file_taken_at,
sonst Dateidatum) und sortiert danach statt nach filemtime.

reports.php gibt je Seite title und composite_path aus. title konnte ueber pages.php
gesetzt werden, kam aber nie zurueck. composite_path ist das im Editor gebaute
Seitenbild - ohne das zeigen Clients nur das Rohbild ohne Anmerkungen, bei
Raster-Layouts eines von bis zu sechs Bildern und bei title_only gar keins.

Neu: api/note.php - Textnotizen zum Auftrag, aufgebaut wie die Sprachnotiz. Die Notiz
liegt als notiz_<betreff>_<datum>.txt im Auftragsverzeichnis und ist damit auch im
Dolibarr-Auftrag und in der Anhaenge-Spalte des Editors sichtbar. Liste, Lesen, Anlegen,
Aendern, Loeschen. Der file-Parameter wird gegen ^notiz_[A-Za-z0-9_.-]*\.txt$ geprueft;
gegen Pfad-Ausbrueche getestet.

ROADMAP.md wiederhergestellt - sie war mit eb37a4b geloescht worden, weil alle Punkte
abgehakt waren. Sie bleibt ab jetzt liegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:11:42 +02:00

132 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* GET /api/photo.php?relpath=<path>
* Liefert eine Datei aus DOL_DATA_ROOT aus, authentifiziert per JWT.
* Whitelist: nur facture/, commande/, propal/, bericht/
*
* Query:
* relpath — relativer Pfad unter DOL_DATA_ROOT
* size=thumb&w=<px> — serverseitig erzeugtes Thumbnail (GD, gecacht, mit ETag).
* Das ist der Weg für Kachel-Ansichten. w: 40800, Default 320.
* size=small|mini — altes Verhalten: nutzt ein von Dolibarr vorgefertigtes
* thumbs/<name>_small.<ext>, falls vorhanden. Für Dateien aus
* der PWA existiert so eines nicht (kein vignette() beim Upload).
* download=1 — als Anhang ausliefern statt inline
*/
// Dieser Endpoint liefert Binärdaten aus — KEIN JSON Content-Type!
// Deshalb nicht _inc.php nutzen, sondern awlauth + Dolibarr manuell laden.
if (!defined('NOLOGIN')) define('NOLOGIN', '1');
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; }
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once __DIR__.'/../lib/bericht.lib.php';
// Auth über das zentrale SSO-Cookie awl_sso. Bewusst awlauth_verify (OHNE CSRF),
// weil dieser reine GET-Read auch per window.location/<a href> geladen wird
// (Download-Link, Audio-Element) und dort kein X-Requested-With setzbar ist.
if (!dol_include_once('/awlauth/lib/awlauth.lib.php') || !function_exists('awlauth_verify')) {
http_response_code(500);
header('Content-Type: text/plain');
echo 'SSO-Modul (awlauth) nicht verfügbar';
exit;
}
$user = awlauth_verify();
if (!$user || !$user->hasRight('bericht', 'read')) {
http_response_code(401);
header('Content-Type: text/plain');
echo 'Nicht angemeldet';
exit;
}
$relpath = (string) ($_GET['relpath'] ?? '');
$size = (string) ($_GET['size'] ?? '');
if (empty($relpath)) {
http_response_code(400);
header('Content-Type: text/plain');
echo 'relpath fehlt';
exit;
}
// Whitelist
if (!preg_match('#^(facture|commande|propal|bericht)/#', $relpath)) {
http_response_code(403);
header('Content-Type: text/plain');
echo 'Pfad nicht erlaubt: '.$relpath;
exit;
}
$full = bericht_resolve_data_path($relpath);
if (!$full || !file_exists($full)) {
http_response_code(404);
header('Content-Type: text/plain');
echo 'Datei nicht gefunden: '.$relpath;
exit;
}
// Serverseitig erzeugtes Thumbnail (GD, gecacht) — der Weg für die Kachel-Ansicht der PWA.
//
// Die alte Variante size=small/mini konnte nur ein Thumbnail ausliefern, das Dolibarr selbst
// unter thumbs/<name>_small.<ext> abgelegt hatte. Fotos aus der PWA kommen aber über
// orders.php?action=upload_photo herein, und dort läuft kein vignette() — es gab also nie
// eines, und jede Kachel lud das komplette Handyfoto. bericht_attachment_thumb() (seit 1.5.0,
// auch vom Editor genutzt) rendert bei Bedarf und legt das Ergebnis unter bericht/thumbs/ ab.
if ($size === 'thumb') {
$w = (int) ($_GET['w'] ?? 320);
if ($w < 40) $w = 40;
if ($w > 800) $w = 800;
// ETag über mtime: ein ersetztes Original liefert einen neuen Wert, sonst antwortet
// der Server mit 304 und überträgt gar nichts.
$etag = '"'.sha1($relpath.'|'.@filemtime($full).'|'.$w).'"';
header('ETag: '.$etag);
header('Cache-Control: private, max-age=2592000');
if (trim((string) ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '')) === $etag) {
http_response_code(304);
exit;
}
$thumb_data = bericht_attachment_thumb($full, $w);
if ($thumb_data !== null) {
header('Content-Type: image/jpeg');
header('Content-Length: '.strlen($thumb_data));
echo $thumb_data;
exit;
}
// Kein unterstütztes Bild (PDF/Video) oder zu groß für den Speicher:
// unten regulär das Original ausliefern.
}
// Alt-Variante: von Dolibarr vorgefertigtes Thumbnail, falls vorhanden
if ($size === 'small' || $size === 'mini') {
$dir = dirname($full);
$base = pathinfo($full, PATHINFO_FILENAME);
$ext = pathinfo($full, PATHINFO_EXTENSION);
$thumb = $dir.'/thumbs/'.$base.'_'.$size.'.'.$ext;
if (file_exists($thumb)) $full = $thumb;
}
$mime = function_exists('dol_mimetype') ? dol_mimetype($full) : 'application/octet-stream';
$download = !empty($_GET['download']);
$filename = $download ? basename($full) : '';
header('Content-Type: '.$mime);
header('Content-Length: '.filesize($full));
header('Cache-Control: private, max-age=3600');
if ($download) {
header('Content-Disposition: attachment; filename="'.addslashes($filename).'"');
}
readfile($full);
exit;