File: /home/xedaptot/ai.naniguide.com/app/Http/Controllers/Refactor/QrCodeController.php
<?php
namespace App\Http\Controllers\Refactor;
use App\Http\Controllers\Controller;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Writer;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Validator;
/**
* SVG QR rendering — generic host endpoint reusable across the app.
*
* Primary consumer: mc-deeplink-button (C5, messenger W3) renders QR
* for Telegram bot deep-links + WhatsApp wa.me click-to-chat links.
*
* Returns SVG with theme-appropriate fg color hardcoded. Transparent
* background so the QR composes against any card surface.
*
* Rate-limited via the `throttle:qr` middleware (60/min per session)
* to prevent bulk-generation abuse. Behind auth — public-facing flows
* (W3 S3.5/S3.6 polling pages) will swap to a separate signed-route
* endpoint when those pages ship, since this one assumes authenticated
* customer/admin context.
*/
class QrCodeController extends Controller
{
private const MIN_SIZE = 80;
private const MAX_SIZE = 400;
private const MAX_URL_LENGTH = 2048;
public function render(Request $request): Response
{
$validated = Validator::make($request->all(), [
'url' => ['required', 'string', 'max:' . self::MAX_URL_LENGTH],
'size' => ['nullable', 'integer', 'between:' . self::MIN_SIZE . ',' . self::MAX_SIZE],
'theme' => ['nullable', 'in:light,dark'],
])->validate();
$url = $validated['url'];
$size = (int) ($validated['size'] ?? 160);
// Note: `theme` param is accepted for backwards-compat with existing
// callers but is no longer respected. QR codes ship pure-black-on-white
// universally (Google Auth / Stripe / banking-app convention). The
// historical theme-aware variant produced white-on-white render bugs
// when the OS's prefers-color-scheme disagreed with the app's
// <html data-theme="..."> attribute (trap L72).
$fg = new Rgb(0, 0, 0);
$bg = new Rgb(255, 255, 255);
$renderer = new ImageRenderer(
(new RendererStyle($size, 1, null, null, Fill::uniformColor($bg, $fg)))
->withSize($size)
->withMargin(1),
new SvgImageBackEnd()
);
$writer = new Writer($renderer);
$svg = $writer->writeString($url, 'utf-8');
// Keep bacon's white background rect intact — gives the QR its own
// white frame for max scannability against ANY surrounding surface
// (light or dark theme). Just inject a11y attributes.
$svg = $this->injectAccessibility($svg, $url);
return response($svg, 200, [
'Content-Type' => 'image/svg+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=86400, immutable',
// No CSP frame-ancestors etc.; the SVG is fully self-contained.
]);
}
/**
* Append `role="img"` + `aria-label="QR code linking to {url}"` to the
* root <svg> tag for screen-reader UX. bacon doesn't emit these by default.
*/
private function injectAccessibility(string $svg, string $url): string
{
$label = 'QR code linking to ' . $this->truncateUrlForLabel($url);
return preg_replace(
'/<svg\b([^>]*)>/i',
'<svg$1 role="img" aria-label="' . htmlspecialchars($label, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '">',
$svg,
1
) ?? $svg;
}
private function truncateUrlForLabel(string $url): string
{
if (strlen($url) <= 80) {
return $url;
}
return substr($url, 0, 77) . '...';
}
}