From 1907d3b966d3eb043dc2424e8a7709021126d8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Mon, 9 Mar 2026 11:20:53 +0100 Subject: [PATCH] Add a SipUri validator with the Regex extracted from the RFC 3261 --- .../Account/CallForwardingController.php | 7 +- .../Account/CallForwardingController.php | 3 +- flexiapi/app/Rules/SipUri.php | 92 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 flexiapi/app/Rules/SipUri.php diff --git a/flexiapi/app/Http/Controllers/Admin/Account/CallForwardingController.php b/flexiapi/app/Http/Controllers/Admin/Account/CallForwardingController.php index d42e356..aa2b67e 100644 --- a/flexiapi/app/Http/Controllers/Admin/Account/CallForwardingController.php +++ b/flexiapi/app/Http/Controllers/Admin/Account/CallForwardingController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin\Account; use App\Account; use App\CallForwarding; use App\Http\Controllers\Controller; +use App\Rules\SipUri; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -20,19 +21,19 @@ class CallForwardingController extends Controller $request->validate([ 'always.forward_to' => $forwardTo, 'always.sip_uri' => array_key_exists('enabled', $request->get('always')) - ? 'nullable|starts_with:sip:|required_if:always.forward_to,sip_uri' + ? ['nullable', new SipUri, 'required_if:always.forward_to,sip_uri'] : 'nullable', 'always.contact_id' => ['required_if:always.forward_to,contact', Rule::in($contactsIds)], 'away.forward_to' => $forwardTo, 'away.sip_uri' => array_key_exists('enabled', $request->get('away')) - ? 'nullable|starts_with:sip:|required_if:away.forward_to,sip_uri' + ? ['nullable', new SipUri, 'required_if:away.forward_to,sip_uri'] : 'nullable', 'away.contact_id' => ['required_if:away.forward_to,contact', Rule::in($contactsIds)], 'busy.forward_to' => $forwardTo, 'busy.sip_uri' => array_key_exists('enabled', $request->get('busy')) - ? 'nullable|starts_with:sip:|required_if:busy.forward_to,sip_uri' + ? ['nullable', new SipUri, 'required_if:busy.forward_to,sip_uri'] : 'nullable', 'busy.contact_id' => ['required_if:busy.forward_to,contact', Rule::in($contactsIds)], ]); diff --git a/flexiapi/app/Http/Controllers/Api/Admin/Account/CallForwardingController.php b/flexiapi/app/Http/Controllers/Api/Admin/Account/CallForwardingController.php index 1a41b1c..e3cdc40 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/Account/CallForwardingController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/Account/CallForwardingController.php @@ -6,6 +6,7 @@ use App\Account; use App\CallForwarding; use App\Http\Controllers\Controller; use App\Rules\CallForwardingEnable; +use App\Rules\SipUri; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -26,7 +27,7 @@ class CallForwardingController extends Controller Rule::unique('call_forwardings', 'type')->where(fn($query) => $query->where('account_id', $accountId)) ], 'forward_to' => 'required|in:sip_uri,contact,voicemail', - 'sip_uri' => 'nullable|starts_with:sip:|required_if:forward_to,sip_uri', + 'sip_uri' => ['nullable', new SipUri, 'required_if:forward_to,sip_uri'], 'enabled' => ['required', 'boolean', new CallForwardingEnable($request, $account)], 'contact_id' => ['required_if:forward_to,contact', Rule::in(resolveUserContacts($account)->pluck('id')->toArray())], ]); diff --git a/flexiapi/app/Rules/SipUri.php b/flexiapi/app/Rules/SipUri.php new file mode 100644 index 0000000..b211477 --- /dev/null +++ b/flexiapi/app/Rules/SipUri.php @@ -0,0 +1,92 @@ +buildPattern(), $value)) { + $fail('The :attribute must be a valid SIP URI.'); + } + } + + /** + * Build the RFC 3261 §19.1 compliant regex pattern. + * Generated using Claude + */ + private function buildPattern(): string + { + // unreserved = alphanum / mark (mark = - _ . ! ~ * ' ( )) + $unreserved = '[A-Za-z0-9\-_.!~*\'()]'; + + // escaped = "%" HEXDIG HEXDIG + $escaped = '%[0-9A-Fa-f]{2}'; + + // user-unreserved = & = + $ , ; ? / + $userUnreserved = '[&=+$,;?\/]'; + + // user = 1*( unreserved / escaped / user-unreserved ) + $user = "(?:{$unreserved}|{$escaped}|{$userUnreserved})+"; + + // password = *( unreserved / escaped / & = + $ , ) + $password = "(?:{$unreserved}|{$escaped}|[&=+$,])*"; + + // userinfo = user [ ":" password ] "@" + $userinfo = "(?:{$user})(?::{$password})?@"; + + // domainlabel = alphanum / alphanum *( alphanum / "-" ) alphanum + $domainLabel = '[A-Za-z0-9](?:[A-Za-z0-9\-]*[A-Za-z0-9])?'; + + // toplabel = ALPHA / ALPHA *( alphanum / "-" ) alphanum + $topLabel = '[A-Za-z](?:[A-Za-z0-9\-]*[A-Za-z0-9])?'; + + // hostname = *( domainlabel "." ) toplabel [ "." ] + $hostname = "(?:{$domainLabel}\.)*{$topLabel}\.?"; + + // IPv4address = 1*3DIGIT "." x3 + $ipv4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'; + + // IPv6 building blocks (RFC 5954) + $h16 = '[0-9A-Fa-f]{1,4}'; + $ls32 = "(?:{$h16}:{$h16}|{$ipv4})"; + $ipv6 = "(?:" + . "(?:{$h16}:){6}{$ls32}" + . "|::(?:{$h16}:){5}{$ls32}" + . "|{$h16}?::(?:{$h16}:){4}{$ls32}" + . "|(?:{$h16}:)?{$h16}::(?:{$h16}:){3}{$ls32}" + . "|(?:{$h16}:){0,2}{$h16}::(?:{$h16}:){2}{$ls32}" + . "|(?:{$h16}:){0,3}{$h16}::{$h16}:{$ls32}" + . "|(?:{$h16}:){0,4}{$h16}::{$ls32}" + . "|(?:{$h16}:){0,5}{$h16}::{$h16}" + . "|(?:{$h16}:){0,6}{$h16}::" + . ")"; + + // IPv6reference = "[" IPv6address "]" + $ipv6ref = "\[{$ipv6}\]"; + + // host = IPv6reference / IPv4address / hostname (order matters) + $host = "(?:{$ipv6ref}|{$ipv4}|{$hostname})"; + + // hostport = host [ ":" port ] + $hostport = "{$host}(?::\d+)?"; + + // uri-parameters: *( ";" pname [ "=" pvalue ] ) + $paramChar = "(?:{$unreserved}|{$escaped}|[\\[\\]\/:&+$])"; + $uriParams = "(?:;{$paramChar}*(?:={$paramChar}*)?)*"; + + // headers: "?" hname "=" hvalue *( "&" hname "=" hvalue ) + $hnvUnreserved = '[\\[\\]\/?:+$]'; + $headerChar = "(?:{$unreserved}|{$escaped}|{$hnvUnreserved})"; + $headers = "(?:\\?{$headerChar}+={$headerChar}*(?:&{$headerChar}+={$headerChar}*)*)?"; + + return '/^sips?:(?:' . $userinfo . ')?' . $hostport . $uriParams . $headers . '$/i'; + } +} \ No newline at end of file