Warenwirtschaft: Review-Fix — AP-32-Label als Base64 übertragen (Connector 1.5.0)

Die Brücke hätte live nicht funktioniert: Connector 1.4.0 sendete die
label_url aus dem per 'deny from all' geschützten DHL-Ordner (403), dazu
SSRF-Risiko durch serverseitigen Abruf einer Client-URL und Timeout-
Mismatch (20 s Download vs. 5 s WordPress-Default). Der Connector liest
die PDF jetzt vom Shop-Dateisystem (realpath-Pinning auf wp-uploads) und
sendet sie Base64; storeWpLabel() validiert und speichert ohne Http::get;
tote Shop-Link-Fallbacks aus den Views entfernt; Connector-Timeout 30 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-20 10:06:39 +00:00
parent ddd6dde593
commit 1b9b6adc44
11 changed files with 173 additions and 83 deletions

View file

@ -16,7 +16,6 @@ use App\Services\Shop;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
@ -32,6 +31,9 @@ class ShoppingUserController extends Controller
protected $member_id = 2; // thomas.krummel@gmail.com
/** AP-32: Obergrenze für per Base64 übertragene DHL-Label-PDFs (dekodiert). */
private const WP_LABEL_MAX_BYTES = 10 * 1024 * 1024;
/**
* @param Request $request
* wp_order_numbers[1234, 1234]
@ -368,6 +370,7 @@ class ShoppingUserController extends Controller
}
$wp_invoice_path = isset($request->wp_invoice_path) ? $request->wp_invoice_path : null;
$wp_label_pdf = isset($request->wp_label_pdf) ? $request->wp_label_pdf : null;
$wp_label_url = isset($request->wp_label_url) ? $request->wp_label_url : null;
$wp_tracking_number = isset($request->wp_tracking_number) ? $request->wp_tracking_number : null;
$wp_advertising = isset($request->wp_advertising) ? $request->wp_advertising : '';
@ -382,7 +385,7 @@ class ShoppingUserController extends Controller
if ($wp_order) {
// AP-32 Brückenlösung: DHL-Label aus dem Shop übernehmen (Kopie lokal speichern)
$this->storeWpLabel($shopping_user->shopping_order, $wp_label_url, $wp_tracking_number);
$this->storeWpLabel($shopping_user->shopping_order, $wp_label_pdf, $wp_label_url, $wp_tracking_number);
// AP-13: Übertragung inkl. SKU-Zuordnungsergebnis je Position protokollieren
ApiOrderLog::recordTransfer($request->wp_order_number, $shopping_user, $shopping_user->shopping_order, $wp_order);
@ -427,6 +430,7 @@ class ShoppingUserController extends Controller
{
$request->validate([
'wp_order_number' => 'required|int',
'wp_label_pdf' => 'nullable|string',
'wp_label_url' => 'nullable|url',
'wp_tracking_number' => 'nullable',
]);
@ -441,7 +445,7 @@ class ShoppingUserController extends Controller
}
$shopping_order = $shopping_user->shopping_order;
$this->storeWpLabel($shopping_order, $request->wp_label_url, $request->wp_tracking_number);
$this->storeWpLabel($shopping_order, $request->wp_label_pdf, $request->wp_label_url, $request->wp_tracking_number);
return response()->json([
'success' => true,
@ -456,15 +460,18 @@ class ShoppingUserController extends Controller
}
/**
* Label-URL + Sendungsnummer aus dem WooCommerce-Shop an der Bestellung speichern.
* Das PDF wird als lokale Kopie in den privaten Storage geladen (Auslieferung nur
* über die Admin-Route) — so bleibt es verfügbar, auch wenn es im Shop gelöscht
* wird, und die öffentliche Upload-URL muss nicht verlinkt werden. Ein
* fehlgeschlagener Download darf die Übertragung nie scheitern lassen.
* Label + Sendungsnummer aus dem WooCommerce-Shop an der Bestellung speichern.
* Das PDF kommt seit Connector 1.5.0 als Base64 direkt aus dem Shop-Dateisystem
* mit (Review 20.07.2026: der Label-Ordner im Shop ist per „deny from all"
* geschützt, ein URL-Abruf lief ins Leere — und der serverseitige Abruf einer
* Client-URL war ein SSRF-Risiko und blockierte den Request bis 20 s). Die Kopie
* liegt im privaten Storage (Auslieferung nur über die Admin-Route) und bleibt
* verfügbar, auch wenn das Label im Shop gelöscht wird. Ein ungültiges Label
* darf die Übertragung nie scheitern lassen.
*/
private function storeWpLabel($shopping_order, $wp_label_url, $wp_tracking_number): void
private function storeWpLabel($shopping_order, $wp_label_pdf, $wp_label_url, $wp_tracking_number): void
{
if (! $shopping_order || (! $wp_label_url && ! $wp_tracking_number)) {
if (! $shopping_order || (! $wp_label_pdf && ! $wp_label_url && ! $wp_tracking_number)) {
return;
}
@ -475,25 +482,21 @@ class ShoppingUserController extends Controller
$shopping_order->tracking = $wp_tracking_number;
}
// Reines Anzeige-Metadatum (Herkunft im Shop) — wird nie abgerufen oder verlinkt
if ($wp_label_url) {
$shopping_order->wp_label_url = $wp_label_url;
try {
$response = Http::timeout(20)->get($wp_label_url);
if ($response->successful() && str_starts_with($response->body(), '%PDF')) {
$path = 'wp-labels/label-'.$shopping_order->id.'.pdf';
Storage::disk('local')->put($path, $response->body());
$shopping_order->wp_label_path = $path;
} else {
Log::warning('WP-Label-Download fehlgeschlagen (kein PDF oder HTTP-Fehler)', [
'shopping_order_id' => $shopping_order->id,
'wp_label_url' => $wp_label_url,
'status' => $response->status(),
]);
}
} catch (\Throwable $e) {
Log::warning('WP-Label-Download fehlgeschlagen: '.$e->getMessage(), [
}
if ($wp_label_pdf) {
$content = base64_decode($wp_label_pdf, true);
if ($content !== false && str_starts_with($content, '%PDF') && strlen($content) <= self::WP_LABEL_MAX_BYTES) {
$path = 'wp-labels/label-'.$shopping_order->id.'.pdf';
Storage::disk('local')->put($path, $content);
$shopping_order->wp_label_path = $path;
} else {
Log::warning('WP-Label verworfen (kein gültiges Base64-PDF oder zu groß)', [
'shopping_order_id' => $shopping_order->id,
'wp_label_url' => $wp_label_url,
'bytes' => is_string($content) ? strlen($content) : null,
]);
}
}