If you’re like many developers working with Laravel, you’ve probably encountered the challenge of creating a secure file explorer for your application. You want to give users the ability to upload and manage files, but you also need to ensure that sensitive data is protected from unauthorized access. Perhaps you’ve tried using built-in file management tools or third-party libraries, only to find that they lack the flexibility and security features you need.
You’ll build a robust and secure file explorer with TinyMCE for rich text editing and Sortable.js for drag-and-drop functionality. By the end of this tutorial, you’ll have created a Livewire component that integrates both libraries seamlessly, allowing users to upload, organize, and view files in a user-friendly interface. You’ll also learn how to configure file storage in Laravel and secure the explorer with authentication and authorization.
Step 1: Installing Required Packages and Dependencies
To begin building our secure file explorer, we need to install the required packages and dependencies. We’ll be using TinyMCE for rich text editing, Sortable.js for drag-and-drop functionality, and Livewire for creating a secure file explorer component.
First, let’s create a new Laravel project or navigate to an existing one in the terminal:
composer create-project --prefer-dist laravel/laravel secure-file-explorer
Navigate into your project directory:
cd secure-file-explorer
Next, we’ll install the required packages. We need TinyMCE and Sortable.js, which are not included in Laravel by default. TinyMCE is available as a Composer package, while Sortable.js is installed with npm:
composer require tinymce/tinymce
npm install sortablejs
This will install both TinyMCE and Sortable.js along with their dependencies.
Neither library ships a Laravel service provider, so there is nothing to register in config/app.php. TinyMCE does, however, need its files to be publicly accessible, so copy them from vendor into the public directory:
mkdir -p public/js && cp -r vendor/tinymce/tinymce public/js/
Finally, keep that public copy in sync whenever Composer updates TinyMCE by adding the same command to the post-update-cmd scripts in your composer.json:
"scripts": {
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"mkdir -p public/js && cp -r vendor/tinymce/tinymce public/js/"
]
}
This makes TinyMCE available from public/js/tinymce and leaves Sortable.js ready to be imported into our JavaScript. In the next section, we’ll set up TinyMCE for rich text editing.
Setting Up TinyMCE for Rich Text Editing
To enable rich text editing in our file explorer, we’ll use the popular TinyMCE library. First, install the package using Composer:
composer require tinymce/tinymce
Next, configure the library by creating a Blade component that will hold its configuration, and update it to match your needs:
php artisan make:component Head/TinymceConfig
Then, edit the resources/views/components/head/tinymce-config.blade.php file to set up your TinyMCE settings. For example, you can enable or disable plugins like advlist, autosave, and fullscreen. Here’s an updated excerpt:
<script src="{{ asset('js/tinymce/tinymce.min.js') }}" referrerpolicy="origin"></script>
<script>
tinymce.init({
selector: '#editor',
license_key: 'gpl',
plugins: 'advlist autolink autosave link image lists charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media table help',
toolbar: 'undo redo | blocks | bold italic | bullist numlist | link image | code fullscreen',
});
</script>
In your Blade template, you can now create a TinyMCE editor instance:
<textarea id="editor" name="content"></textarea>
Give the element the same ID that the selector option targets (#editor) so TinyMCE knows where the editor should be rendered.
Finally, include the configuration component in the <head> of your layout so the TinyMCE editor is rendered on the page:
<head>
{{-- ... --}}
<x-head.tinymce-config />
</head>
With TinyMCE up and running, we can now focus on implementing drag-and-drop functionality with Sortable.js.
Implementing Sortable.js for Drag-and-Drop Functionality
To enable drag-and-drop functionality in our file explorer, we’ll be using Sortable.js. This library provides a simple and lightweight way to reorder items on a webpage.
First, install Sortable.js via npm by running the following command in your terminal:
npm install sortablejs
Next, update your resources/js/app.js file to import Sortable.js and make it available globally, then rebuild your assets with npm run build:
// resources/js/app.js
import Sortable from 'sortablejs';
window.Sortable = Sortable;
Now, let’s implement the drag-and-drop functionality in the view of our FileExplorer Livewire component, which we’ll build out in the next section. We’ll attach Sortable.js to the file list and send the new order back to the component’s reorderFiles method:
{{-- In resources/views/livewire/file-explorer.blade.php, add the following inside the component's root element: --}}
<ul id="file-list">
@foreach($files as $path => $name)
<li wire:key="{{ $path }}" data-path="{{ $path }}">
<span class="handle">::</span> {{ $name }}
</li>
@endforeach
</ul>
@script
<script>
Sortable.create(document.getElementById('file-list'), {
handle: '.handle',
onEnd: () => {
const order = [...document.querySelectorAll('#file-list li')].map((li) => li.dataset.path);
$wire.reorderFiles(order);
},
});
</script>
@endscript
This code enables drag-and-drop functionality for files in our file explorer, allowing users to reorder them as needed. With this feature implemented, we’re one step closer to building a comprehensive and user-friendly file explorer.
Creating a Secure File Explorer Component with Livewire
In this step, we’ll create a secure file explorer component using Laravel’s Livewire package. We’ll use it to display and manage files in our application.
First, let’s install the necessary packages by running:
composer require livewire/livewire
Next, create a new Livewire component named FileExplorer inside the app/Livewire directory (running php artisan make:livewire FileExplorer generates both the class and its view):
// app/Livewire/FileExplorer.php
namespace App\Livewire;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class FileExplorer extends Component
{
public $files = [];
protected $listeners = ['refreshFiles'];
public function render()
{
return view('livewire.file-explorer');
}
public function refreshFiles($directory)
{
$this->files = collect(Storage::disk('private')->files($directory))->mapWithKeys(function ($file) {
return [$file => basename($file)];
})->toArray();
}
public function reorderFiles(array $order)
{
$this->files = collect($order)
->filter(fn ($path) => array_key_exists($path, $this->files))
->mapWithKeys(fn ($path) => [$path => $this->files[$path]])
->toArray();
}
}
We’ll also create a new Blade view for our component:
{{-- resources/views/livewire/file-explorer.blade.php --}}
<div>
<ul>
@foreach($files as $path => $name)
<li>{{ $name }} ({{ Storage::disk('private')->size($path) }} bytes)</li>
@endforeach
</ul>
<button wire:click="refreshFiles('uploads')">Refresh Files</button>
</div>
Make sure to update your web.php route file to include the Livewire component:
// routes/web.php
use App\Livewire\FileExplorer;
Route::get('/file-explorer', FileExplorer::class);
This is a basic implementation of our secure file explorer component using Livewire. We’ll continue building upon this in the next sections to add more features and functionality.
The FileExplorer component now displays a list of files stored on the private disk (which we’ll configure shortly, and where uploads will be saved), with a button to refresh the list. The $files property is updated whenever the refreshFiles method is triggered, which uses Laravel’s Storage facade to fetch the latest file listing from the specified directory.
Integrating Cloud-Based Malware Detection (Optional)
To further enhance security in our file explorer, we can integrate a cloud-based malware detection service. This optional step requires an API key for a service like VirusTotal (owned by Google), which offers a rate-limited free public API as well as premium plans.
For this example, let’s assume you have already obtained an API key for VirusTotal and stored it in config/services.php under virus-total.api_key. Create a service provider and a service class to handle the integration:
// app/Providers/VirusTotalServiceServiceProvider.php
namespace App\Providers;
use App\Services\VirusTotalService;
use Illuminate\Support\ServiceProvider;
class VirusTotalServiceServiceProvider extends ServiceProvider
{
public function register()
{
// Register the service instance in the IoC container
$this->app->singleton(VirusTotalService::class, function ($app) {
return new VirusTotalService($app['config']->get('services.virus-total.api_key'));
});
}
}
// app/Services/VirusTotalService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class VirusTotalService
{
private $apiKey;
public function __construct($apiKey)
{
$this->apiKey = $apiKey;
}
public function getFileReport(string $fileHash): array
{
$response = Http::withHeaders(['x-apikey' => $this->apiKey])
->get('https://www.virustotal.com/api/v3/files/' . $fileHash);
return $response->json() ?? [];
}
}
Register the provider in bootstrap/providers.php (Laravel 11 and later) or in the providers array of config/app.php (Laravel 10 and earlier). To use the service in our file explorer component, inject it into a component action. Livewire resolves type-hinted dependencies in action methods from the service container:
// app/Livewire/FileExplorer.php
namespace App\Livewire;
use App\Services\VirusTotalService;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class FileExplorer extends Component
{
// ...
public function checkFile(VirusTotalService $virusTotal, string $path)
{
$hash = hash_file('sha256', Storage::disk('private')->path($path));
return $virusTotal->getFileReport($hash);
}
}
This integration adds an extra layer of protection against malware, but keep in mind that it’s not a replacement for traditional security measures.
With this step complete, our file explorer is now even more secure and robust. Next up, we’ll configure file upload and storage using Laravel.
Configuring File Upload and Storage in Laravel
To store and manage uploaded files securely, we’ll configure Laravel’s built-in file system abstraction layer. Open the existing config/filesystems.php file and update the default disk and the disks array:
// config/filesystems.php
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
// ... keep the existing 'local' and 's3' disks
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
'private' => [
'driver' => 'local',
'root' => storage_path('app/files'),
'visibility' => 'private',
],
],
Next, add an upload form to the component’s view in resources/views/livewire/file-explorer.blade.php. Binding a file input with wire:model makes Livewire upload the file to a temporary location as soon as it’s selected:
<form wire:submit="uploadFile">
<input type="file" wire:model="file">
@error('file') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Upload</button>
</form>
Finally, in the FileExplorer component, add Livewire’s WithFileUploads trait and use it to validate and store uploads on the private disk:
// app/Livewire/FileExplorer.php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class FileExplorer extends Component
{
use WithFileUploads;
public $file;
// ... existing $files property and methods
public function uploadFile()
{
$this->validate([
'file' => 'required|file|max:10240|mimes:pdf,jpg,png,txt,docx',
]);
$this->file->storeAs(
'uploads',
time() . '.' . $this->file->extension(),
'private'
);
// Process the uploaded file...
}
}
This sets up a basic file upload and storage system using Laravel’s filesystem abstraction and Livewire’s file uploads.
Securing the File Explorer with Authentication and Authorization
Now that our file explorer component is up and running, it’s essential to ensure that only authorized users can access and interact with files. We’ll achieve this by integrating Laravel’s built-in authentication and authorization features.
First, let’s add the auth middleware to our web routes in routes/web.php. This will ensure only authenticated users can reach these routes:
Route::middleware('auth')->group(function () {
// File explorer routes go here...
});
Next, we need to define permissions for our file explorer component. We’ll create a new policy called FilePolicy for a File Eloquent model (create one with php artisan make:model File if you don’t have it yet) in the app/Policies directory, so Laravel discovers it automatically. The hasPermissionTo() method used below comes from the HasRoles trait of the spatie/laravel-permission package, added to your User model:
// app/Policies/FilePolicy.php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class FilePolicy
{
use HandlesAuthorization;
public function viewAny(User $user)
{
// Allow users to view files if they're authenticated and have the 'file.view' permission
return $user->hasPermissionTo('file.view');
}
public function upload(User $user)
{
// Allow users to upload files if they're authenticated and have the 'file.upload' permission
return $user->hasPermissionTo('file.upload');
}
}
Finally, we’ll update our FileExplorer Livewire component to use these permissions:
{{-- resources/views/livewire/file-explorer.blade.php --}}
@can('viewAny', \App\Models\File::class)
{{-- Display file list... --}}
@endcan
@can('upload', \App\Models\File::class)
{{-- Render upload form... --}}
@endcan
With these changes in place, only authorized users will be able to access and interact with files in our secure file explorer. This ensures that sensitive data remains protected at all times.
This concludes our comprehensive tutorial on building a secure file explorer with TinyMCE and Sortable.js. With this final piece of the puzzle in place, you’re now ready to deploy your own production-ready file explorer application!
Testing and Deploying the Secure File Explorer
Now that our secure file explorer is built and secured with authentication and authorization, it’s time to put it through its paces.
First, let’s test our file explorer component locally using the following command in your terminal:
php artisan serve
Log in and navigate to http://localhost:8000/file-explorer in your web browser. You should see the file explorer component displayed with your files listed.
To test drag-and-drop functionality, drag files by their handle to sort the file order, and use the upload form to upload new files.
For more comprehensive testing, I recommend using Laravel’s built-in testing features. Create a new test class in tests/Feature directory, for example FileExplorerTest.php. You can then write feature tests to verify that each functionality works as expected:
namespace Tests\Feature;
use App\Livewire\FileExplorer;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Tests\TestCase;
class FileExplorerTest extends TestCase
{
use RefreshDatabase;
public function test_file_list_loads_properly()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/file-explorer');
$response->assertStatus(200);
}
public function test_drag_and_drop_functionality()
{
// Fake the private disk so the view can read file sizes
Storage::fake('private');
Storage::disk('private')->put('a.txt', 'A');
Storage::disk('private')->put('b.txt', 'B');
// Set up the component with a file list
$component = Livewire::test(FileExplorer::class)
->set('files', ['a.txt' => 'a.txt', 'b.txt' => 'b.txt']);
// Simulate the order Sortable.js sends after a drag-and-drop event
$component->call('reorderFiles', ['b.txt', 'a.txt']);
// Assert that the files are in the new order
$this->assertSame(['b.txt', 'a.txt'], array_keys($component->get('files')));
}
}
Once you’re satisfied with your testing results, it’s time to deploy your secure file explorer to a production environment. Follow standard Laravel deployment procedures using tools like composer and php artisan.
Frequently Asked Questions
What is the best alternative to TinyMCE for rich text editing in a Laravel application?
Other popular alternatives to TinyMCE include CKEditor and Quill, which offer similar features and flexibility. However, TinyMCE’s extensive plugin library and customization options make it a strong choice for many developers.
Why do I need to copy TinyMCE into the public directory?
Composer installs TinyMCE into the vendor directory, which the browser can’t access. Copying it to public/js/tinymce ensures that its scripts, skins, and plugins are available for use, while Sortable.js is bundled into your compiled JavaScript by Vite.
How do I prevent unauthorized access to sensitive data in my file explorer?
To secure your file explorer, you should implement authentication and authorization using Laravel’s built-in features. This includes setting up user roles and permissions, as well as configuring file storage and access controls.
What is the purpose of installing Sortable.js separately from TinyMCE?
Sortable.js provides drag-and-drop functionality for your file explorer, allowing users to reorder files without requiring manual intervention. Installing it separately gives you more control over its configuration and customization.
