diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9b6a0f8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.github +.env +.env.backup +.env.production +storage/app/* +storage/logs/* +storage/framework/cache/data/* +storage/framework/sessions/* +storage/framework/views/* +node_modules diff --git a/.env.example b/.env.example index 00b5aa5..e0c4ac8 100644 --- a/.env.example +++ b/.env.example @@ -6,14 +6,10 @@ APP_DEBUG=false APP_URL=http://localhost APP_LOCALE=pt-br -DNS_SERVERS=1.1.1.1,8.8.8.8 - BROWSER_WS_ENDPOINT=ws://marreta_browser:9222 BROWSER_TIMEOUT_MS=30000 DISABLE_CACHE=false -CLEANUP_DAYS=7 -LOG_DAYS_TO_KEEP=7 # Proxy (optional) # PROXY_URL= diff --git a/.gitignore b/.gitignore index bac5d2c..b71b1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,3 @@ Homestead.json Homestead.yaml Thumbs.db -/cache -/database diff --git a/Dockerfile b/Dockerfile index 9080531..439f98e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ WORKDIR /var/www/html COPY . . RUN composer install --no-interaction --optimize-autoloader --no-dev \ - && php artisan config:cache \ && php artisan route:cache \ && php artisan view:cache diff --git a/README.md b/README.md index 5cf4587..9788f31 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ Agora modifique com suas preferencias: - `APP_NAME`: Nome do seu Marreta - `APP_DESCRIPTION`: Conta pra que serve -- `APP_URL`: Onde vai rodar, endereço completo com `https://`. Se você alterar a porta no docker-compose (ex: 8080:80), você também deve incluir a porta no SITE_URL (ex: https://seusite:8080) -- `APP_LANGUAGE`: pt-br (Português Brasil), en (Inglês), es (Espanhol) ou de-de (Alemão), ru-ru (Russo) +- `APP_URL`: Onde vai rodar, endereço completo com `https://`. Se você alterar a porta no docker-compose (ex: 8080:80), você também deve incluir a porta no APP_URL (ex: https://seusite:8080) +- `APP_LOCALE`: pt-br (Português Brasil), en (Inglês), es (Espanhol) ou de-de (Alemão), ru-ru (Russo) - `ADMIN_EMAIL`: admin@marreta.local - `ADMIN_PASSWORD`: password diff --git a/app/Http/Controllers/Api/ProxyController.php b/app/Http/Controllers/Api/ProxyController.php index aa8c6c4..294afbf 100644 --- a/app/Http/Controllers/Api/ProxyController.php +++ b/app/Http/Controllers/Api/ProxyController.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace App\Http\Controllers\Api; +use App\Enums\MarretaError; use App\Exceptions\MarretaException; use App\Http\Controllers\Controller; use App\Services\ProxyService; use App\Services\UrlNormalizer; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; final class ProxyController extends Controller @@ -19,19 +19,17 @@ final class ProxyController extends Controller private readonly UrlNormalizer $urlNormalizer, ) {} - public function __invoke(Request $request, string $url): JsonResponse|RedirectResponse + public function __invoke(Request $request, string $url): JsonResponse { - $processed = $this->urlNormalizer->processRouteUrl($url, $request->query->all()); - - if (! $processed['valid']) { - return redirect('/?message=INVALID_URL'); - } - - if ($processed['needs_redirect']) { - return redirect(config('marreta.site_url').'/api/'.$processed['url']); - } + $headers = ['Access-Control-Allow-Origin' => '*']; try { + $processed = $this->urlNormalizer->processRouteUrl($url, $request->query->all()); + + if (! $processed['valid']) { + throw new MarretaException(MarretaError::InvalidUrl); + } + $this->proxyService->analyze($processed['url']); $displayUrl = preg_replace('#^https?://#', '', $processed['url']); @@ -39,15 +37,9 @@ final class ProxyController extends Controller return response()->json([ 'status' => 200, 'url' => config('marreta.site_url').'/p/'.$displayUrl, - ], 200, [ - 'Access-Control-Allow-Origin' => '*', - 'Access-Control-Allow-Methods' => 'GET', - ]); + ], 200, $headers + ['Access-Control-Allow-Methods' => 'GET']); } catch (MarretaException $e) { - $headers = [ - 'Access-Control-Allow-Origin' => '*', - 'X-Error-Type' => $e->errorType(), - ]; + $headers['X-Error-Type'] = $e->errorType(); if ($e->additionalInfo()) { $headers['X-Error-Info'] = $e->additionalInfo(); @@ -56,7 +48,7 @@ final class ProxyController extends Controller return response()->json([ 'status' => $e->error()->httpCode(), 'error' => [ - 'type' => $e->errorType(), + 'code' => $e->errorType(), 'message' => $e->getMessage(), 'details' => $e->additionalInfo() ?: null, ], diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 3d623b4..af08eb5 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -4,14 +4,13 @@ declare(strict_types=1); namespace App\Http\Controllers; -use App\Services\MarretaCacheService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; final class HomeController extends Controller { - public function __invoke(Request $request, MarretaCacheService $cache): View|RedirectResponse + public function __invoke(Request $request): View|RedirectResponse { app()->setLocale(config('marreta.language')); @@ -47,7 +46,6 @@ final class HomeController extends Controller 'message' => $message, 'message_type' => $messageType, 'url' => $url, - 'cache_count' => $cache->getCacheFileCount(), 'site_name' => config('marreta.site_name'), 'site_description' => config('marreta.site_description'), 'site_url' => config('marreta.site_url'), diff --git a/app/Services/ProxyService.php b/app/Services/ProxyService.php index f0e5bae..8c70bfa 100644 --- a/app/Services/ProxyService.php +++ b/app/Services/ProxyService.php @@ -35,6 +35,10 @@ final class ProxyService */ public function analyze(string $url): string { + if (! preg_match('#^https?://#', $url)) { + $url = 'https://'.$url; + } + $host = parse_url($url, PHP_URL_HOST); if (! $host) { throw new MarretaException(MarretaError::InvalidUrl); diff --git a/app/Services/UrlNormalizer.php b/app/Services/UrlNormalizer.php index fb3dd3e..d792162 100644 --- a/app/Services/UrlNormalizer.php +++ b/app/Services/UrlNormalizer.php @@ -41,7 +41,7 @@ final class UrlNormalizer $url = preg_replace('#/+#', '/', $url); $url = 'https://'.$url; - if (filter_var($url, FILTER_VALIDATE_URL) === false) { + if (! $this->isValidUrl($url)) { return ['valid' => false, 'url' => '', 'needs_redirect' => false]; } @@ -66,7 +66,7 @@ final class UrlNormalizer { $url = trim($url); - if (filter_var($url, FILTER_VALIDATE_URL) === false) { + if (! $this->isValidUrl($url)) { return ''; } @@ -85,8 +85,21 @@ final class UrlNormalizer $cleaned .= isset($parts['query']) ? '?'.$parts['query'] : ''; $cleaned .= isset($parts['fragment']) ? '#'.$parts['fragment'] : ''; - $cleaned = preg_replace('/[\x00-\x1F\x7F]/', '', $cleaned); + return preg_replace('/[\x00-\x1F\x7F]/', '', $cleaned); + } - return (string) filter_var($cleaned, FILTER_SANITIZE_URL); + /** + * Validate a URL, tolerating non-ASCII characters that FILTER_VALIDATE_URL rejects + * (e.g. accented letters in Wikipedia-style paths) by percent-encoding them first. + */ + private function isValidUrl(string $url): bool + { + $ascii = preg_replace_callback( + '/[^\x21-\x7E]/', + fn (array $m): string => rawurlencode($m[0]), + $url, + ); + + return filter_var($ascii, FILTER_VALIDATE_URL) !== false; } } diff --git a/composer.json b/composer.json index 8cb474b..b332022 100644 --- a/composer.json +++ b/composer.json @@ -41,13 +41,11 @@ "composer install", "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", "@php artisan key:generate", - "@php artisan migrate --force", - "npm install", - "npm run build" + "@php artisan migrate --force" ], "dev": [ "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" --names=server,queue,logs --kill-others" ], "test": [ "@php artisan config:clear --ansi", diff --git a/composer.lock b/composer.lock index 9414879..ee0882d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "43bc61ffbf39fedb6f934290bc5fadf6", + "content-hash": "4b8ce4c8ae1ef4c82a90d5d4ea094c21", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -2211,16 +2211,16 @@ }, { "name": "kirschbaum-development/eloquent-power-joins", - "version": "4.3.2", + "version": "4.3.3", "source": { "type": "git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "33c189bd51a510c1ceba67222395ead08a29863a" + "reference": "c609dbbe4ad2051b667e937f1ab554067519d64b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/33c189bd51a510c1ceba67222395ead08a29863a", - "reference": "33c189bd51a510c1ceba67222395ead08a29863a", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/c609dbbe4ad2051b667e937f1ab554067519d64b", + "reference": "c609dbbe4ad2051b667e937f1ab554067519d64b", "shasum": "" }, "require": { @@ -2268,22 +2268,22 @@ ], "support": { "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.2" + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.3" }, - "time": "2026-05-28T20:35:55+00:00" + "time": "2026-07-23T11:41:37+00:00" }, { "name": "laravel/framework", - "version": "v13.21.1", + "version": "v13.22.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "303b5f8dc899f89e8c38c350b639b8fbd193ed16" + "reference": "d354afa45334cdac523fbc015f203a78d7e2f5c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/303b5f8dc899f89e8c38c350b639b8fbd193ed16", - "reference": "303b5f8dc899f89e8c38c350b639b8fbd193ed16", + "url": "https://api.github.com/repos/laravel/framework/zipball/d354afa45334cdac523fbc015f203a78d7e2f5c6", + "reference": "d354afa45334cdac523fbc015f203a78d7e2f5c6", "shasum": "" }, "require": { @@ -2497,7 +2497,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-07-21T14:27:35+00:00" + "time": "2026-07-24T20:38:48+00:00" }, { "name": "laravel/prompts", @@ -10518,7 +10518,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/config/database.php b/config/database.php index 64709ce..2cd4488 100644 --- a/config/database.php +++ b/config/database.php @@ -35,7 +35,7 @@ return [ 'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DB_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'database' => env('DB_DATABASE', storage_path('app/database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 'busy_timeout' => null, diff --git a/config/marreta.php b/config/marreta.php index b3569ea..847de3b 100644 --- a/config/marreta.php +++ b/config/marreta.php @@ -28,21 +28,18 @@ return [ |-------------------------------------------------------------------------- */ - 'dns_servers' => array_values(array_filter(explode(',', (string) env('DNS_SERVERS', '1.1.1.1,8.8.8.8')))), - 'proxy_url' => env('PROXY_URL'), /* |-------------------------------------------------------------------------- | Browser engine (Lightpanda via CDP) - | BROWSER_WS_ENDPOINT takes precedence; legacy SELENIUM_HOST is honoured - | as a deprecated fallback (translated into a ws:// endpoint). |-------------------------------------------------------------------------- */ 'browser' => [ - 'ws_endpoint' => env('BROWSER_WS_ENDPOINT') - ?: (env('SELENIUM_HOST') ? 'ws://'.preg_replace('#^https?://#', '', (string) env('SELENIUM_HOST')) : 'ws://localhost:9222'), + // A trailing path is required: the underlying WebSocket client rejects a URI with no path. + // Left null when unset — the browser fetch strategy is skipped in that case. + 'ws_endpoint' => env('BROWSER_WS_ENDPOINT') ? rtrim((string) env('BROWSER_WS_ENDPOINT'), '/').'/' : null, 'timeout_ms' => (int) env('BROWSER_TIMEOUT_MS', 30000), ], @@ -54,18 +51,10 @@ return [ 'cache' => [ 'disabled' => (bool) env('DISABLE_CACHE', false), - 'directory' => env('CACHE_DIR', base_path('cache')), + 'directory' => storage_path('app/marreta-cache'), 'compress_level' => 3, ], - /* - |-------------------------------------------------------------------------- - | Maintenance - |-------------------------------------------------------------------------- - */ - - 'cleanup_days' => (int) env('CLEANUP_DAYS', 0), - /* |-------------------------------------------------------------------------- | Admin (Filament) diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..5b09ead 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,9 +5,6 @@ colors="true" > - - tests/Unit - tests/Feature diff --git a/resources/views/filament/pages/global-rules.blade.php b/resources/views/filament/pages/global-rules.blade.php new file mode 100644 index 0000000..bd0dd5f --- /dev/null +++ b/resources/views/filament/pages/global-rules.blade.php @@ -0,0 +1,11 @@ + +
+ {{ $this->form }} + +
+ + {{ __('admin.save') }} + +
+
+