diff --git a/README.md b/README.md index 641414d..b5544a6 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,18 @@ php artisan vendor:publish ## Usage +By default, Docify is only viewable when your Laravel application is running in the `local` environment. + +To allow additional environments, publish the config file and update `environments`: + +```php +'environments' => ['local', 'staging'], ``` -// Usage code and examples here + +Set the local editor used by the Edit link with `DOCIFY_EDITOR`. If it is not set, Docify will also check `DEBUGBAR_EDITOR` and `IGNITION_EDITOR` before defaulting to VS Code. + +```dotenv +DOCIFY_EDITOR=cursor ``` ## Testing diff --git a/config/docify.php b/config/docify.php index 5851c0d..5af761b 100644 --- a/config/docify.php +++ b/config/docify.php @@ -7,4 +7,6 @@ 'route_name' => 'docs', 'prefix' => 'docify', 'folder' => './docs', + 'environments' => ['local'], + 'editor' => env('DOCIFY_EDITOR') ?: env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'vscode'), ]; diff --git a/resources/views/livewire/docs.blade.php b/resources/views/livewire/docs.blade.php index fdc8512..0a05e1f 100644 --- a/resources/views/livewire/docs.blade.php +++ b/resources/views/livewire/docs.blade.php @@ -26,31 +26,38 @@ public function mount(?string $page = null): void { $this->page = trim($page ?: 'index', '/') ?: 'index'; - $docsPath = base_path(trim(config('docify.folder'), './')); - $resolvedPath = realpath(sprintf('%s/%s.md', $docsPath, $this->page)); - - abort_unless($resolvedPath && str_starts_with($resolvedPath, $docsPath), 404); + $docsPath = realpath(base_path(trim(config('docify.folder'), './'))); + $resolvedPath = $docsPath + ? realpath(sprintf('%s/%s.md', $docsPath, $this->page)) + : false; + + abort_unless( + $docsPath + && $resolvedPath + && str_starts_with($resolvedPath, $docsPath . DIRECTORY_SEPARATOR), + 404 + ); $this->path = $resolvedPath; } #[Computed] - public function editUrl(): string + public function editUrl(): ?string { - if (App::isLocal()) { - $editors = [ - 'vscode' => 'vscode://file/%s', - 'cursor' => 'cursor://file/%s', - 'phpstorm' => 'phpstorm://open?file=%s', - 'sublime' => 'subl://open?url=file://%s', - 'atom' => 'atom://open?url=file://%s', - 'zed' => 'zed://open?path=%s', - ]; + if (! App::isLocal()) { + return null; + } - $editor = config()->string('app.editor', 'vscode'); + $editor = config()->string('docify.editor', 'vscode'); - return sprintf($editors[$editor] ?? $editors['phpstorm'], $this->path); - } + return match ($editor) { + 'cursor' => 'cursor://file/' . $this->path, + 'phpstorm' => 'phpstorm://open?file=' . $this->path, + 'sublime' => 'subl://open?url=file://' . $this->path, + 'atom' => 'atom://open?url=file://' . $this->path, + 'zed' => 'zed://open?path=' . $this->path, + default => 'vscode://file/' . $this->path, + }; } /** @return array> */ @@ -152,7 +159,10 @@ public function content(): string
- Edit + @if ($this->editUrl) + Edit + @endif +
{!! $this->content !!}
diff --git a/routes/web.php b/routes/web.php index 7eff88f..e562200 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,7 +3,9 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; +use TechEnby\Docify\Http\Middleware\EnsureDocifyCanBeViewed; Route::livewire(rtrim(config('docify.route'), '/') . '/{page?}', config('docify.prefix') . '::docs') + ->middleware(EnsureDocifyCanBeViewed::class) ->where('page', '.*') ->name(config('docify.route_name')); diff --git a/src/Http/Middleware/EnsureDocifyCanBeViewed.php b/src/Http/Middleware/EnsureDocifyCanBeViewed.php new file mode 100644 index 0000000..6cb85c1 --- /dev/null +++ b/src/Http/Middleware/EnsureDocifyCanBeViewed.php @@ -0,0 +1,19 @@ +environment(config('docify.environments', ['local'])), 404); + + return $next($request); + } +} diff --git a/tests/Feature/DocsComponentTest.php b/tests/Feature/DocsComponentTest.php index 88ad5cf..3fff31b 100644 --- a/tests/Feature/DocsComponentTest.php +++ b/tests/Feature/DocsComponentTest.php @@ -3,11 +3,15 @@ declare(strict_types=1); use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Route; use Livewire\Livewire; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use TechEnby\Docify\Http\Middleware\EnsureDocifyCanBeViewed; beforeEach(function (): void { config()->set('app.key', 'base64:' . base64_encode(str_repeat('a', 32))); config()->set('docify.folder', './docs-test'); + config()->set('docify.environments', ['testing']); File::deleteDirectory(base_path('docs-test')); File::ensureDirectoryExists(base_path('docs-test/guides')); @@ -15,6 +19,7 @@ afterEach(function (): void { File::deleteDirectory(base_path('docs-test')); + File::deleteDirectory(base_path('docs-test-private')); }); test('renders the requested markdown page', function (): void { @@ -94,3 +99,72 @@ $this->get('/docs/missing')->assertNotFound(); }); + +test('renders docs in configured non local environments without an edit link', function (): void { + $this->app['env'] = 'staging'; + config()->set('docify.environments', ['local', 'staging']); + + File::put(base_path('docs-test/index.md'), '# Package Docs'); + + Livewire::test('docify::docs') + ->assertSee('Package Docs') + ->assertDontSee('Edit'); +}); + +test('uses the configured docify editor for local edit links', function (): void { + $this->app['env'] = 'local'; + config()->set('docify.editor', 'cursor'); + + File::put(base_path('docs-test/index.md'), '# Package Docs'); + + expect(Livewire::test('docify::docs')->get('editUrl')) + ->toBe('cursor://file/' . realpath(base_path('docs-test/index.md'))); +}); + +test('falls back to vscode for unknown local editors', function (): void { + $this->app['env'] = 'local'; + config()->set('docify.editor', 'unknown'); + + File::put(base_path('docs-test/index.md'), '# Package Docs'); + + expect(Livewire::test('docify::docs')->get('editUrl')) + ->toBe('vscode://file/' . realpath(base_path('docs-test/index.md'))); +}); + +test('does not allow traversing outside the configured docs folder', function (): void { + File::ensureDirectoryExists(base_path('docs-test-private')); + File::put(base_path('docs-test/index.md'), '# Public Docs'); + File::put(base_path('docs-test-private/secret.md'), '# Secret Docs'); + + $this->get('/docs/%2E%2E%2Fdocs-test-private%2Fsecret') + ->assertNotFound(); +}); + +test('registers the environment guard on the docs route', function (): void { + expect(Route::getRoutes()->getByName('docs')->gatherMiddleware()) + ->toContain(EnsureDocifyCanBeViewed::class); +}); + +test('only allows configured environments to view docs', function (): void { + $middleware = new EnsureDocifyCanBeViewed; + $request = request(); + $next = fn () => response('allowed'); + + $this->app['env'] = 'local'; + config()->set('docify.environments', ['local']); + + expect($middleware->handle($request, $next)->getContent())->toBe('allowed'); + + $this->app['env'] = 'production'; + + $middleware->handle($request, $next); +})->throws(NotFoundHttpException::class); + +test('allows users to configure additional docs environments', function (): void { + $middleware = new EnsureDocifyCanBeViewed; + + $this->app['env'] = 'staging'; + config()->set('docify.environments', ['local', 'staging']); + + expect($middleware->handle(request(), fn () => response('allowed'))->getContent())->toBe('allowed'); +});