Files
ladill-link/app/Services/Link/LinkPlatformProxy.php
T
isaaccladandCursor 01371663fb
Deploy Ladill Link / deploy (push) Successful in 43s
Proxy speaker portal paths on ladl.link to Events.
Route /{slug}/speaker/{token} through the Events app so speaker invite links resolve instead of hitting the account QR hub or a broken proxy patch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 17:47:27 +00:00

203 lines
6.6 KiB
PHP

<?php
namespace App\Services\Link;
use App\Support\LadillLink;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\Response;
/**
* Proxies ladl.link ecosystem paths to the platform QR resolver (/q/* on ladill.com)
* or directly to satellite apps (e.g. transfer.ladill.com). The browser stays on
* ladl.link; resolution happens server-side without redirect loops.
*/
class LinkPlatformProxy
{
public const INTERNAL_HEADER = 'X-Ladill-Internal';
public function forward(Request $request, string $slug, ?string $path = null): Response
{
$normalizedPath = $path !== null ? ltrim($path, '/') : '';
if ($normalizedPath === 'transfer' || str_starts_with($normalizedPath, 'transfer/')) {
return $this->fetch(
$request,
rtrim($this->transferBaseUrl(), '/').'/q/'.$slug.'/'.$normalizedPath,
);
}
if ($this->isEventsRegistrationPath($normalizedPath)) {
return $this->forwardToQrHub($request, $slug, $normalizedPath, $this->eventsBaseUrl());
}
$platformResponse = $this->forwardToQrHub($request, $slug, $normalizedPath, $this->platformBaseUrl());
if ($platformResponse->getStatusCode() !== 404) {
return $platformResponse;
}
return $this->forwardToQrHub($request, $slug, $normalizedPath, $this->eventsBaseUrl());
}
private function forwardToQrHub(Request $request, string $slug, string $normalizedPath, string $hubBase): Response
{
$target = rtrim($hubBase, '/').'/q/'.$slug;
if ($normalizedPath !== '') {
$target .= '/'.$normalizedPath;
}
$response = $this->fetch($request, $target);
if ($response->isRedirection()) {
$location = $response->headers->get('Location');
if (is_string($location)) {
if ($followUp = $this->followLadillLinkRedirect($request, $slug, $location)) {
return $followUp;
}
$response->headers->set('Location', $this->rewriteLocation($location, $slug));
}
}
return $response;
}
private function fetch(Request $request, string $target): Response
{
if ($qs = $request->getQueryString()) {
$target .= (str_contains($target, '?') ? '&' : '?').$qs;
}
$method = strtoupper($request->method());
$client = Http::withOptions(['allow_redirects' => false])
->timeout(15)
->withHeaders($this->forwardHeaders($request));
$pending = match ($method) {
'POST' => $this->forwardPost($client, $request, $target),
'PATCH' => $client->patch($target, $request->all()),
'DELETE' => $client->delete($target),
default => $client->get($target),
};
$upstream = $pending->toPsrResponse();
return new Response(
(string) $upstream->getBody(),
$upstream->getStatusCode(),
$this->sanitizeHeaders($upstream->getHeaders()),
);
}
private function followLadillLinkRedirect(Request $request, string $slug, string $location): ?Response
{
$parsed = parse_url($location);
if (! is_array($parsed)) {
return null;
}
$host = strtolower((string) ($parsed['host'] ?? ''));
$linkHost = strtolower((string) parse_url(LadillLink::baseUrl(), PHP_URL_HOST));
if ($host !== $linkHost) {
return null;
}
$path = ltrim((string) ($parsed['path'] ?? ''), '/');
$slugPrefix = $slug.'/';
if (! str_starts_with($path, $slugPrefix)) {
return null;
}
return $this->forward($request, $slug, substr($path, strlen($slugPrefix)));
}
private function isEventsRegistrationPath(string $path): bool
{
return $path === 'register'
|| str_starts_with($path, 'register/')
|| str_starts_with($path, 'registered/')
|| str_starts_with($path, 'speaker/');
}
private function platformBaseUrl(): string
{
return rtrim((string) config('app.platform_url', 'https://ladill.com'), '/');
}
private function eventsBaseUrl(): string
{
$domain = (string) config('app.events_domain', 'events.ladill.com');
return 'https://'.$domain;
}
private function transferBaseUrl(): string
{
$domain = (string) config('app.transfer_domain', 'transfer.ladill.com');
return 'https://'.$domain;
}
/** @return array<string, string> */
private function forwardHeaders(Request $request): array
{
return array_filter([
'Accept' => $request->header('Accept'),
'Accept-Language' => $request->header('Accept-Language'),
'Content-Type' => $request->header('Content-Type'),
'User-Agent' => $request->header('User-Agent'),
'Referer' => $request->header('Referer'),
self::INTERNAL_HEADER => '1',
]);
}
private function forwardPost(\Illuminate\Http\Client\PendingRequest $client, Request $request, string $target): \Illuminate\Http\Client\Response
{
if ($this->requestSendsJson($request)) {
return $client->asJson()->post($target, $request->all());
}
return $client->asForm()->post($target, $request->post());
}
private function requestSendsJson(Request $request): bool
{
return str_contains(strtolower((string) $request->header('Content-Type', '')), 'application/json');
}
/** @param array<string, array<int, string>> $headers */
private function sanitizeHeaders(array $headers): array
{
$flat = [];
foreach ($headers as $name => $values) {
if (strtolower($name) === 'transfer-encoding') {
continue;
}
$flat[$name] = $values[0] ?? '';
}
return $flat;
}
private function rewriteLocation(string $location, string $slug): string
{
foreach ([$this->platformBaseUrl(), $this->eventsBaseUrl()] as $base) {
$prefix = $base.'/q/'.$slug;
if (str_starts_with($location, $prefix)) {
$suffix = substr($location, strlen($prefix));
return LadillLink::url($slug).$suffix;
}
}
$transfer = rtrim($this->transferBaseUrl(), '/').'/q/'.$slug;
if (str_starts_with($location, $transfer)) {
$suffix = substr($location, strlen($transfer));
return LadillLink::url($slug).$suffix;
}
return $location;
}
}