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..7219284 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())], ]); @@ -57,7 +58,7 @@ class CallForwardingController extends Controller ->ignore($callForwarding->id) ], 'forward_to' => 'required|in:sip_uri', - 'sip_uri' => 'required|starts_with:sip', + 'sip_uri' => ['required', new SipUri], 'enabled' => ['required', 'boolean', new CallForwardingEnable($request, $account)] ]); diff --git a/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php index 8e4047f..a768e54 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin\Account; use App\Account; use App\AccountFile; +use App\Rules\SipUri; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -24,7 +25,7 @@ class VoicemailController extends Controller } $request->validate([ - 'sip_from' => 'nullable|starts_with:sip', + 'sip_from' => ['nullable', new SipUri], 'content_type' => [ 'required', Rule::in(AccountFile::VOICEMAIL_CONTENTTYPES), diff --git a/flexiapi/app/Rules/SipUri.php b/flexiapi/app/Rules/SipUri.php new file mode 100644 index 0000000..467ca5e --- /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'; + } +}