* @copyright Copyright (c) 2026 OpenCloud GmbH * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, * as published by the Free Software Foundation; * either version 3 of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see * */ namespace TestHelpers; use GuzzleHttp\Exception\GuzzleException; use Psr\Http\Message\ResponseInterface; /** * Helper for the unauthenticated guest links endpoints of the auth-guest service */ class GuestLinkHelper { public const SESSION_COOKIE_NAME = '__Host-oc_guest_session'; /** * the nonce cookie that binds a PIN to the browser is named "_" */ public const NONCE_COOKIE_BASE_NAME = '__Host-oc_guest_nonce'; /** * the guestlinks auth manager of reva still accepts a guest session for this long after it expired */ public const SESSION_EXPIRY_LEEWAY_SECONDS = 30; /** * matches the guest link token in a guest link email, e.g. "/g/v1.." */ public const TOKEN_IN_EMAIL_REGEX = '/\/g\/(v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/'; /** * matches the PIN in a renewed guest link email */ public const PIN_IN_EMAIL_REGEX = '/Your PIN: (\d{6})/'; /** * @param string $baseUrl * @param string $action verify/token|verify/pin|renew * * @return string */ public static function getUrl(string $baseUrl, string $action): string { return GraphHelper::getBetaFullUrl($baseUrl, "extensions/org.libregraph/guestLinks/$action"); } /** * @param array $cookies cookie name => value * * @return array */ public static function getHeaders(array $cookies = []): array { $headers = ['Content-Type' => 'application/json']; if ($cookies !== []) { $headers['Cookie'] = \implode( '; ', \array_map(fn ($name, $value) => "$name=$value", \array_keys($cookies), $cookies) ); } return $headers; } /** * @param string $baseUrl * @param string $xRequestId * @param string $token * * @return ResponseInterface * @throws GuzzleException */ public static function verifyToken(string $baseUrl, string $xRequestId, string $token): ResponseInterface { return HttpRequestHelper::post( self::getUrl($baseUrl, 'verify/token'), $xRequestId, null, null, self::getHeaders(), \json_encode(['token' => $token]) ); } /** * @param string $baseUrl * @param string $xRequestId * @param string $permissionId * @param string $pin * @param array $cookies the nonce cookie set by renew * * @return ResponseInterface * @throws GuzzleException */ public static function verifyPin( string $baseUrl, string $xRequestId, string $permissionId, string $pin, array $cookies = [] ): ResponseInterface { return HttpRequestHelper::post( self::getUrl($baseUrl, 'verify/pin'), $xRequestId, null, null, self::getHeaders($cookies), \json_encode(['pin' => $pin, 'permissionId' => $permissionId]) ); } /** * @param string $baseUrl * @param string $xRequestId * @param string $permissionId * @param array $cookies the guest session cookie, if any * * @return ResponseInterface * @throws GuzzleException */ public static function renew( string $baseUrl, string $xRequestId, string $permissionId, array $cookies = [] ): ResponseInterface { return HttpRequestHelper::post( self::getUrl($baseUrl, 'renew'), $xRequestId, null, null, self::getHeaders($cookies), \json_encode(['permissionId' => $permissionId]) ); } /** * Returns the Set-Cookie headers of the response, keyed by the cookie name * * @param ResponseInterface $response * * @return array */ public static function getSetCookieHeaders(ResponseInterface $response): array { $setCookies = []; foreach ($response->getHeader('Set-Cookie') as $setCookie) { $name = \trim(\explode('=', \explode(';', $setCookie, 2)[0], 2)[0]); $setCookies[$name] = $setCookie; } return $setCookies; } /** * Returns the value of a cookie set by the response, or null when it is not set or is cleared * * @param string $setCookie * * @return string|null */ public static function getCookieValue(string $setCookie): ?string { $value = \explode('=', \explode(';', $setCookie, 2)[0], 2)[1] ?? ''; $attributes = \strtolower($setCookie); if ($value === '' || \str_contains($attributes, 'max-age=0') || \preg_match('/max-age=-\d/', $attributes)) { return null; } return $value; } /** * Returns the raw Set-Cookie header of the guest session cookie, or null * * @param ResponseInterface $response * * @return string|null */ public static function getSessionSetCookieHeader(ResponseInterface $response): ?string { return self::getSetCookieHeaders($response)[self::SESSION_COOKIE_NAME] ?? null; } /** * Returns the raw Set-Cookie header of the nonce cookie, or null * * @param ResponseInterface $response * * @return string|null */ public static function getNonceSetCookieHeader(ResponseInterface $response): ?string { foreach (self::getSetCookieHeaders($response) as $name => $setCookie) { if (\str_starts_with($name, self::NONCE_COOKIE_BASE_NAME)) { return $setCookie; } } return null; } }