diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 3c94ee57f..9a7ae362b 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1091,7 +1091,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -1332,7 +1332,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -1545,7 +1545,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -2551,7 +2551,7 @@ public function update_by_uuid(Request $request) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php index f88f78c34..af75098b5 100644 --- a/app/Support/ValidationPatterns.php +++ b/app/Support/ValidationPatterns.php @@ -557,7 +557,7 @@ public static function validateApplicationDomains(mixed $value): array continue; } - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; continue; diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index ff77a78e2..f3c5359f7 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -4,6 +4,54 @@ use App\Models\ServiceApplication; use Illuminate\Support\Collection; +function isValidDomainUrl(string $url): bool +{ + $components = parse_url($url); + + if ($components === false) { + return false; + } + + $scheme = $components['scheme'] ?? ''; + $host = $components['host'] ?? ''; + + if (! in_array(strtolower($scheme), ['http', 'https'], true) || $host === '') { + return false; + } + + $urlToValidate = $scheme.'://'; + + if (isset($components['user'])) { + $urlToValidate .= $components['user']; + + if (isset($components['pass'])) { + $urlToValidate .= ':'.$components['pass']; + } + + $urlToValidate .= '@'; + } + + $urlToValidate .= str_replace('_', '-', $host); + + if (isset($components['port'])) { + $urlToValidate .= ':'.$components['port']; + } + + if (isset($components['path'])) { + $urlToValidate .= $components['path']; + } + + if (isset($components['query'])) { + $urlToValidate .= '?'.$components['query']; + } + + if (isset($components['fragment'])) { + $urlToValidate .= '#'.$components['fragment']; + } + + return filter_var($urlToValidate, FILTER_VALIDATE_URL) !== false; +} + function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { $conflicts = []; diff --git a/openapi.json b/openapi.json index 816cdb535..cdb7e736e 100644 --- a/openapi.json +++ b/openapi.json @@ -2553,6 +2553,10 @@ "is_preserve_repository_enabled": { "type": "boolean", "description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false." + }, + "include_source_commit_in_build": { + "type": "boolean", + "description": "Include source commit information in the build. Default is false." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index dacacece1..f2775c8d7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1663,6 +1663,9 @@ paths: is_preserve_repository_enabled: type: boolean description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.' + include_source_commit_in_build: + type: boolean + description: 'Include source commit information in the build. Default is false.' type: object responses: '200': diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 0a2dfda9d..8d1a3369b 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -803,7 +803,7 @@ "category": "backend", "logo": "svgs/convex.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-06-12T10:45:52+02:00", + "template_last_updated_at": "2026-05-09T19:26:30+05:30", "port": "6791" }, "cryptgeon": { @@ -1779,7 +1779,7 @@ "category": "devtools", "logo": "svgs/gitea.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-06-06T00:11:24+02:00" + "template_last_updated_at": "2026-06-01T07:54:27-05:00" }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", @@ -2361,7 +2361,7 @@ "category": "automation", "logo": "svgs/inngest.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-07-02T13:25:47+02:00", + "template_last_updated_at": null, "port": "8288" }, "invoice-ninja": { diff --git a/templates/service-templates.json b/templates/service-templates.json index 4d97d8d5e..2ad3008f5 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -803,7 +803,7 @@ "category": "backend", "logo": "svgs/convex.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-06-12T10:45:52+02:00", + "template_last_updated_at": "2026-05-09T19:26:30+05:30", "port": "6791" }, "cryptgeon": { @@ -1779,7 +1779,7 @@ "category": "devtools", "logo": "svgs/gitea.svg", "minversion": "0.0.0", - "template_last_updated_at": "2026-06-06T00:11:24+02:00" + "template_last_updated_at": "2026-06-01T07:54:27-05:00" }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", @@ -2361,7 +2361,7 @@ "category": "automation", "logo": "svgs/inngest.png", "minversion": "0.0.0", - "template_last_updated_at": "2026-07-02T13:25:47+02:00", + "template_last_updated_at": null, "port": "8288" }, "invoice-ninja": { diff --git a/tests/Unit/IsValidDomainUrlTest.php b/tests/Unit/IsValidDomainUrlTest.php new file mode 100644 index 000000000..3f40646f7 --- /dev/null +++ b/tests/Unit/IsValidDomainUrlTest.php @@ -0,0 +1,29 @@ +toBeTrue(); + expect(isValidDomainUrl('http://my_app.example.com'))->toBeTrue(); + expect(isValidDomainUrl('https://a_b_c.example.com/path'))->toBeTrue(); +}); + +it('accepts ordinary domains and URLs', function () { + expect(isValidDomainUrl('https://example.com'))->toBeTrue(); + expect(isValidDomainUrl('http://sub.example.com:8080/path?q=1'))->toBeTrue(); + expect(isValidDomainUrl('https://example.com/a_b'))->toBeTrue(); +}); + +it('rejects strings that are not valid URLs', function () { + expect(isValidDomainUrl('not a url'))->toBeFalse(); + expect(isValidDomainUrl('example.com'))->toBeFalse(); + expect(isValidDomainUrl('ht_tp://example.com'))->toBeFalse(); + expect(isValidDomainUrl(''))->toBeFalse(); +}); + +it('rejects URLs that do not use HTTP or HTTPS schemes', function () { + expect(isValidDomainUrl('ftp://example.com'))->toBeFalse(); + expect(isValidDomainUrl('javascript://example.com'))->toBeFalse(); + expect(isValidDomainUrl('data://example.com'))->toBeFalse(); +}); diff --git a/tests/Unit/ValidationPatternsTest.php b/tests/Unit/ValidationPatternsTest.php index cb9d0144d..cfcceac38 100644 --- a/tests/Unit/ValidationPatternsTest.php +++ b/tests/Unit/ValidationPatternsTest.php @@ -187,3 +187,7 @@ expect(ValidationPatterns::normalizeApplicationDomains($domains)) ->toBe('https://example.com/MixedCase/Path?Token=ABC#Fragment,http://sub.example.com/Api/V1'); }); + +it('validates application domains with underscores in the hostname', function () { + expect(ValidationPatterns::validateApplicationDomains('https://myapp_service.example.com'))->toBeEmpty(); +});