├── docs
├── customization.md
├── index.md
├── update.md
├── installation.md
├── configuration.md
├── acl.md
├── integration.md
└── events.md
├── .gitignore
├── CONTRIBUTING.md
├── src
├── Events
│ ├── BeforeInitialization.php
│ ├── DiskSelected.php
│ ├── Deleted.php
│ ├── Deleting.php
│ ├── Download.php
│ ├── Unzip.php
│ ├── FileCreated.php
│ ├── FileCreating.php
│ ├── Paste.php
│ ├── UnzipCreated.php
│ ├── UnzipFailed.php
│ ├── DirectoryCreated.php
│ ├── DirectoryCreating.php
│ ├── FileUpdate.php
│ ├── Zip.php
│ ├── ZipFailed.php
│ ├── ZipCreated.php
│ ├── Rename.php
│ ├── FilesUploaded.php
│ ├── FilesUploading.php
│ └── FilesUploadFailed.php
├── Services
│ ├── TransferService
│ │ ├── TransferFactory.php
│ │ ├── Transfer.php
│ │ ├── LocalTransfer.php
│ │ └── ExternalTransfer.php
│ ├── ACLService
│ │ ├── ConfigACLRepository.php
│ │ ├── DBACLRepository.php
│ │ ├── ACLRepository.php
│ │ └── ACL.php
│ ├── ConfigService
│ │ ├── ConfigRepository.php
│ │ └── DefaultConfigRepository.php
│ └── Zip.php
├── Requests
│ ├── CustomErrorMessage.php
│ └── RequestValidator.php
├── Traits
│ ├── CheckTrait.php
│ ├── PathTrait.php
│ └── ContentTrait.php
├── FileManagerServiceProvider.php
├── routes.php
├── Middleware
│ └── FileManagerACL.php
├── Controllers
│ └── FileManagerController.php
└── FileManager.php
├── examples
└── wysiwyg
│ ├── ckeditor.blade.php
│ ├── fm-button.blade.php
│ ├── tinymce.blade.php
│ └── summernote.blade.php
├── migrations
└── 2019_02_06_174631_make_acl_rules_table.php
├── composer.json
├── LICENSE.md
├── resources
├── views
│ ├── fmButton.blade.php
│ ├── summernote.blade.php
│ ├── tinymce.blade.php
│ ├── ckeditor.blade.php
│ └── tinymce5.blade.php
└── assets
│ └── css
│ └── file-manager.css
├── README.md
└── config
└── file-manager.php
/docs/customization.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | node_modules/
3 | npm-debug.log
4 | .idea
5 |
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | > Contributions are welcome, and are accepted via pull requests.
4 |
5 | ## Pull requests
6 |
7 | Please ensure all pull requests are made against the `develop` branch on GitHub.
8 |
--------------------------------------------------------------------------------
/src/Events/BeforeInitialization.php:
--------------------------------------------------------------------------------
1 | = 8.1
14 | * ext-zip - for zip and unzip functions
15 | * Laravel 9 or higher
16 | * GD Library or Imagick for [intervention/image-laravel](https://github.com/Intervention/image-laravel)
17 | * Bootstrap 5 and Bootstrap Icons v1.8.0 and higher
18 |
--------------------------------------------------------------------------------
/src/Events/DiskSelected.php:
--------------------------------------------------------------------------------
1 | disk = $disk;
21 | }
22 |
23 | /**
24 | * @return string
25 | */
26 | public function disk()
27 | {
28 | return $this->disk;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Services/TransferService/TransferFactory.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | File manager and CKeditor
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Requests/CustomErrorMessage.php:
--------------------------------------------------------------------------------
1 | container->call([$this, 'message'])
19 | : 'The given data was invalid.';
20 |
21 | throw new HttpResponseException(response()->json([
22 | 'errors' => $validator->errors(),
23 | 'message' => $message,
24 | ], 422));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Services/ACLService/ConfigACLRepository.php:
--------------------------------------------------------------------------------
1 | getUserID()] ?? [];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Events/Deleted.php:
--------------------------------------------------------------------------------
1 | disk = $disk;
27 | $this->items = $items;
28 | }
29 |
30 | /**
31 | * @return string
32 | */
33 | public function disk()
34 | {
35 | return $this->disk;
36 | }
37 |
38 | /**
39 | * @return array
40 | */
41 | public function items()
42 | {
43 | return $this->items;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Events/Deleting.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
27 | $this->items = $request->input('items');
28 | }
29 |
30 | /**
31 | * @return string
32 | */
33 | public function disk()
34 | {
35 | return $this->disk;
36 | }
37 |
38 | /**
39 | * @return array
40 | */
41 | public function items()
42 | {
43 | return $this->items;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Events/Download.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
27 | $this->path = $request->input('path');
28 | }
29 |
30 | /**
31 | * @return string
32 | */
33 | public function disk()
34 | {
35 | return $this->disk;
36 | }
37 |
38 | /**
39 | * @return string
40 | */
41 | public function path()
42 | {
43 | return $this->path;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Services/ACLService/DBACLRepository.php:
--------------------------------------------------------------------------------
1 | where('user_id', $this->getUserID())
31 | ->get(['disk', 'path', 'access'])
32 | ->map(function ($item) {
33 | return get_object_vars($item);
34 | })
35 | ->all();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Services/ACLService/ACLRepository.php:
--------------------------------------------------------------------------------
1 | [
25 | * "disk" => "public"
26 | * "path" => "music"
27 | * "access" => 0
28 | * ],
29 | * 1 => [
30 | * "disk" => "public"
31 | * "path" => "images"
32 | * "access" => 1
33 | * ]
34 | *
35 | * OR [] - if no results for selected user
36 | *
37 | * @return array
38 | */
39 | public function getRules(): array;
40 | }
41 |
--------------------------------------------------------------------------------
/migrations/2019_02_06_174631_make_acl_rules_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->unsignedBigInteger('user_id')->nullable();
19 | $table->string('disk');
20 | $table->string('path');
21 | $table->tinyInteger('access');
22 | $table->timestamps();
23 |
24 | $table->foreign('user_id')->references('id')->on('users');
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('acl_rules');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alexusmai/laravel-file-manager",
3 | "description": "File manager for Laravel",
4 | "keywords": [
5 | "laravel",
6 | "file",
7 | "manager"
8 | ],
9 | "authors": [
10 | {
11 | "name": "Aleksandr Manekin",
12 | "email": "alexusmai@gmail.com",
13 | "role": "Developer"
14 | }
15 | ],
16 | "homepage": "https://github.com/alexusami/laravel-file-manager",
17 | "license": "MIT",
18 | "minimum-stability": "dev",
19 | "require": {
20 | "php": "^8.1",
21 | "ext-zip": "*",
22 | "ext-json": "*",
23 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0",
24 | "league/flysystem": "^3.0",
25 | "intervention/image-laravel": "^1.2.0"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Alexusmai\\LaravelFileManager\\": "src"
30 | }
31 | },
32 | "extra": {
33 | "laravel": {
34 | "providers": [
35 | "Alexusmai\\LaravelFileManager\\FileManagerServiceProvider"
36 | ]
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Events/Unzip.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->folder = $request->input('folder');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function folder()
56 | {
57 | return $this->folder;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/FileCreated.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->name = $request->input('name');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function name()
56 | {
57 | return $this->name;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/FileCreating.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->name = $request->input('name');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function name()
56 | {
57 | return $this->name;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/Paste.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->clipboard = $request->input('clipboard');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return array
54 | */
55 | public function clipboard()
56 | {
57 | return $this->clipboard;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/UnzipCreated.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->folder = $request->input('folder');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function folder()
56 | {
57 | return $this->folder;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/UnzipFailed.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->folder = $request->input('folder');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function folder()
56 | {
57 | return $this->folder;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/DirectoryCreated.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->name = $request->input('name');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function name()
56 | {
57 | return $this->name;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Aleksandr Manekin alexusmai@gmail.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/Events/DirectoryCreating.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->name = $request->input('name');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | return $this->path;
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function name()
56 | {
57 | return $this->name;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/FileUpdate.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
32 | $this->path = $request->input('path');
33 | $this->file = $request->file('file');
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function disk()
40 | {
41 | return $this->disk;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function path()
48 | {
49 | if ($this->path) {
50 | return $this->path.'/'.$this->file->getClientOriginalName();
51 | }
52 |
53 | return $this->file->getClientOriginalName();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Traits/CheckTrait.php:
--------------------------------------------------------------------------------
1 | configRepository->getDiskList())
20 | && array_key_exists($name, config('filesystems.disks'));
21 | }
22 |
23 | /**
24 | * Check Disk and Path
25 | *
26 | * @param $disk
27 | * @param $path
28 | *
29 | * @return bool
30 | */
31 | public function checkPath($disk, $path): bool
32 | {
33 | // check disk name
34 | if (!$this->checkDisk($disk)) {
35 | return false;
36 | }
37 |
38 | // check path
39 | if ($path && !Storage::disk($disk)->exists($path)) {
40 | return false;
41 | }
42 |
43 | return true;
44 | }
45 |
46 | /**
47 | * Disk/path not found message
48 | *
49 | * @return array
50 | */
51 | public function notFoundMessage(): array
52 | {
53 | return [
54 | 'result' => [
55 | 'status' => 'danger',
56 | 'message' => 'notFound',
57 | ],
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Events/Zip.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->path = $request->input('path');
38 | $this->name = $request->input('name');
39 | $this->elements = $request->input('elements');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function path()
54 | {
55 | return $this->path;
56 | }
57 |
58 | /**
59 | * @return string
60 | */
61 | public function name()
62 | {
63 | return $this->name;
64 | }
65 |
66 | /**
67 | * @return array|string|null
68 | */
69 | public function elements()
70 | {
71 | return $this->elements;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Events/ZipFailed.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->path = $request->input('path');
38 | $this->name = $request->input('name');
39 | $this->elements = $request->input('elements');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function path()
54 | {
55 | return $this->path;
56 | }
57 |
58 | /**
59 | * @return string
60 | */
61 | public function name()
62 | {
63 | return $this->name;
64 | }
65 |
66 | /**
67 | * @return array|string|null
68 | */
69 | public function elements()
70 | {
71 | return $this->elements;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Events/ZipCreated.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->path = $request->input('path');
38 | $this->name = $request->input('name');
39 | $this->elements = $request->input('elements');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function path()
54 | {
55 | return $this->path;
56 | }
57 |
58 | /**
59 | * @return string
60 | */
61 | public function name()
62 | {
63 | return $this->name;
64 | }
65 |
66 | /**
67 | * @return array|string|null
68 | */
69 | public function elements()
70 | {
71 | return $this->elements;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Traits/PathTrait.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'File Manager') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/resources/views/summernote.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'File Manager') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/wysiwyg/fm-button.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | File Manager and standalone button
12 |
13 |
14 |
15 |
29 |
30 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/Services/TransferService/Transfer.php:
--------------------------------------------------------------------------------
1 | disk = $disk;
21 | $this->path = $path;
22 | $this->clipboard = $clipboard;
23 | }
24 |
25 | /**
26 | * Transfer files and folders
27 | *
28 | * @return array
29 | */
30 | public function filesTransfer(): array
31 | {
32 | try {
33 | // determine the type of operation
34 | if ($this->clipboard['type'] === 'copy') {
35 | $this->copy();
36 | } elseif ($this->clipboard['type'] === 'cut') {
37 | $this->cut();
38 | }
39 | } catch (\Exception $exception) {
40 | return [
41 | 'result' => [
42 | 'status' => 'error',
43 | 'message' => $exception->getMessage(),
44 | ],
45 | ];
46 | }
47 |
48 | return [
49 | 'result' => [
50 | 'status' => 'success',
51 | 'message' => 'copied',
52 | ],
53 | ];
54 | }
55 |
56 | abstract protected function copy();
57 |
58 | abstract protected function cut();
59 | }
60 |
--------------------------------------------------------------------------------
/src/Events/Rename.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->newName = $request->input('newName');
38 | $this->oldName = $request->input('oldName');
39 | $this->type = $request->input('type');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function newName()
54 | {
55 | return $this->newName;
56 | }
57 |
58 | /**
59 | * @return string
60 | */
61 | public function oldName()
62 | {
63 | return $this->oldName;
64 | }
65 |
66 | /**
67 | * @return string
68 | */
69 | public function type()
70 | {
71 | /*
72 | * $info = Storage::disk($this->disk)->getMetadata($this->oldName);
73 | * return $info['type'];
74 | */
75 |
76 | return $this->type;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Events/FilesUploaded.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->path = $request->input('path');
38 | $this->files = $request->file('files');
39 | $this->overwrite = $request->input('overwrite');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function path()
54 | {
55 | return $this->path;
56 | }
57 |
58 | /**
59 | * @return array
60 | */
61 | public function files()
62 | {
63 | return array_map(function ($file) {
64 | return [
65 | 'name' => $file->getClientOriginalName(),
66 | 'path' => $this->path.'/'.$file->getClientOriginalName(),
67 | 'extension' => $file->extension(),
68 | ];
69 | }, $this->files);
70 | }
71 |
72 | /**
73 | * @return bool
74 | */
75 | public function overwrite()
76 | {
77 | return !!$this->overwrite;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Events/FilesUploading.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
37 | $this->path = $request->input('path');
38 | $this->files = $request->file('files');
39 | $this->overwrite = $request->input('overwrite');
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function disk()
46 | {
47 | return $this->disk;
48 | }
49 |
50 | /**
51 | * @return string
52 | */
53 | public function path()
54 | {
55 | return $this->path;
56 | }
57 |
58 | /**
59 | * @return array
60 | */
61 | public function files()
62 | {
63 | return array_map(function ($file) {
64 | return [
65 | 'name' => $file->getClientOriginalName(),
66 | 'path' => $this->path.'/'.$file->getClientOriginalName(),
67 | 'extension' => $file->extension(),
68 | ];
69 | }, $this->files);
70 | }
71 |
72 | /**
73 | * @return bool
74 | */
75 | public function overwrite()
76 | {
77 | return !!$this->overwrite;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/resources/views/tinymce.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'File Manager') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/resources/views/ckeditor.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'File Manager') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/Requests/RequestValidator.php:
--------------------------------------------------------------------------------
1 | [
34 | 'sometimes',
35 | 'string',
36 | function ($attribute, $value, $fail) use($config) {
37 | if (!in_array($value, $config->getDiskList()) ||
38 | !array_key_exists($value, config('filesystems.disks'))
39 | ) {
40 | return $fail('diskNotFound');
41 | }
42 | },
43 | ],
44 | 'path' => [
45 | 'sometimes',
46 | 'string',
47 | 'nullable',
48 | function ($attribute, $value, $fail) {
49 | if ($value && !Storage::disk($this->input('disk'))->exists($value)
50 | ) {
51 | return $fail('pathNotFound');
52 | }
53 | },
54 | ],
55 | ];
56 | }
57 |
58 | /**
59 | * Not found message
60 | *
61 | * @return string
62 | */
63 | public function message()
64 | {
65 | return 'notFound';
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Events/FilesUploadFailed.php:
--------------------------------------------------------------------------------
1 | disk = $request->input('disk');
39 | $this->path = $request->input('path');
40 | $this->files = $request->file('files');
41 | $this->overwrite = $request->input('overwrite');
42 | $this->reason = $reason;
43 | }
44 |
45 | /**
46 | * @return string
47 | */
48 | public function disk()
49 | {
50 | return $this->disk;
51 | }
52 | public function reason()
53 | {
54 | return $this->reason;
55 | }
56 |
57 | /**
58 | * @return string
59 | */
60 | public function path()
61 | {
62 | return $this->path;
63 | }
64 |
65 | /**
66 | * @return array
67 | */
68 | public function files()
69 | {
70 | return array_map(function ($file) {
71 | return [
72 | 'name' => $file->getClientOriginalName(),
73 | 'path' => $this->path.'/'.$file->getClientOriginalName(),
74 | 'extension' => $file->extension(),
75 | ];
76 | }, $this->files);
77 | }
78 |
79 | /**
80 | * @return bool
81 | */
82 | public function overwrite()
83 | {
84 | return !!$this->overwrite;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/examples/wysiwyg/tinymce.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | File manager and TinyMCE
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/resources/views/tinymce5.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ config('app.name', 'File Manager') }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/examples/wysiwyg/summernote.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | File Manager and SummerNote
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/FileManagerServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadRoutesFrom(__DIR__.'/routes.php');
21 |
22 | // views
23 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'file-manager');
24 |
25 | // publish config
26 | $this->publishes([
27 | __DIR__
28 | .'/../config/file-manager.php' => config_path('file-manager.php'),
29 | ], 'fm-config');
30 |
31 | // publish views
32 | $this->publishes([
33 | __DIR__
34 | .'/../resources/views' => resource_path('views/vendor/file-manager'),
35 | ], 'fm-views');
36 |
37 | // publish js and css files - vue-file-manager module
38 | $this->publishes([
39 | __DIR__
40 | .'/../resources/assets' => public_path('vendor/file-manager'),
41 | ], 'fm-assets');
42 |
43 | // publish migrations
44 | $this->publishes([
45 | __DIR__
46 | .'/../migrations' => database_path('migrations'),
47 | ], 'fm-migrations');
48 | }
49 |
50 | /**
51 | * Register the application services.
52 | *
53 | * @return void
54 | */
55 | public function register()
56 | {
57 | $this->mergeConfigFrom(
58 | __DIR__.'/../config/file-manager.php',
59 | 'file-manager'
60 | );
61 |
62 | // Config Repository
63 | $this->app->bind(
64 | ConfigRepository::class,
65 | $this->app['config']['file-manager.configRepository']
66 | );
67 |
68 | // ACL Repository
69 | $this->app->bind(
70 | ACLRepository::class,
71 | $this->app->make(ConfigRepository::class)->getAclRepository()
72 | );
73 |
74 | // register ACL middleware
75 | $this->app['router']->aliasMiddleware('fm-acl', FileManagerACL::class);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Services/ACLService/ACL.php:
--------------------------------------------------------------------------------
1 | aclRepository = $aclRepository;
32 | $this->configRepository = $configRepository;
33 | }
34 |
35 | /**
36 | * Get access level for selected path
37 | *
38 | * @param $disk
39 | * @param string $path
40 | *
41 | * @return int
42 | */
43 | public function getAccessLevel($disk, $path = '/'): int
44 | {
45 | $rules = $this->rulesForDisk($disk);
46 |
47 | // find the first rule where the paths are equal
48 | $firstRule = Arr::first($rules, function ($value) use ($path) {
49 | return fnmatch($value['path'], $path);
50 | });
51 |
52 | if ($firstRule) {
53 | return $firstRule['access'];
54 | }
55 |
56 | // blacklist or whitelist (ACL strategy)
57 | return $this->configRepository->getAclStrategy() === 'blacklist' ? 2 : 0;
58 | }
59 |
60 | /**
61 | * Select rules for disk
62 | *
63 | * @param $disk
64 | *
65 | * @return array
66 | */
67 | protected function rulesForDisk($disk): array
68 | {
69 | return Arr::where($this->rulesList(),
70 | function ($value) use ($disk) {
71 | return $value['disk'] === $disk;
72 | });
73 | }
74 |
75 | /**
76 | * Get rules list from ACL Repository
77 | *
78 | * @return array|mixed
79 | */
80 | protected function rulesList(): mixed
81 | {
82 | // if cache on
83 | if ($minutes = $this->configRepository->getAclRulesCache()) {
84 | $cacheName = get_class($this->aclRepository) . '_' .$this->aclRepository->getUserID();
85 |
86 | return Cache::remember($cacheName, $minutes, function () {
87 | return $this->aclRepository->getRules();
88 | });
89 | }
90 |
91 | return $this->aclRepository->getRules();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | 1. Install package - using composer
4 |
5 | ```
6 | composer require alexusmai/laravel-file-manager
7 | ```
8 |
9 | For Laravel 5 - 8 use v2.5.4
10 |
11 | ```
12 | composer require alexusmai/laravel-file-manager "2.5.4"
13 | ```
14 |
15 | 2. If you use Laravel 5.4, then add service provider to config/app.php (for the Laravel 5.5 and higher skip this step):
16 |
17 | ```php
18 | Alexusmai\LaravelFileManager\FileManagerServiceProvider::class,
19 | ```
20 |
21 | 3. Publish configuration file
22 |
23 | ```bash
24 | php artisan vendor:publish --tag=fm-config
25 | ```
26 |
27 | 4. You can install npm package directly and use it in your vue application - more information about it -
28 | [vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
29 |
30 | > OR
31 |
32 | Publish compiled and minimized js and css files
33 |
34 | ```
35 | php artisan vendor:publish --tag=fm-assets
36 | ```
37 |
38 | Open the view file where you want to place the application block, and add:
39 |
40 | * add a csrf token to head block if you did not do it before
41 |
42 | ```
43 |
44 |
45 | ```
46 |
47 | * For version 3 and higher - the frontend package uses **Bootstrap 5** and **Bootstrap Icons** styles, if you already use it,
48 | then you do not need to connect any styles. Otherwise, add -
49 |
50 | ```
51 |
52 |
53 | ```
54 |
55 | * For old versions - the frontend package uses **Bootstrap 4** and **Font Awesome 5** styles, if you already use it,
56 | then you do not need to connect any styles. Otherwise, add -
57 |
58 | ```
59 |
60 |
61 | ```
62 |
63 | * add file manager styles
64 |
65 | ```
66 |
67 | ```
68 |
69 | * add file manager js
70 |
71 | ```
72 |
73 | ```
74 |
75 | * For version 3 and higher - add div for application (set application height!)
76 |
77 | ```
78 |
79 | ```
80 |
81 | * For old versions - add div for application (set application height!)
82 |
83 | ```
84 |
87 | ```
88 |
89 | ## What's next
90 |
91 | [Configuration](./configuration.md)
92 |
--------------------------------------------------------------------------------
/src/Services/TransferService/LocalTransfer.php:
--------------------------------------------------------------------------------
1 | clipboard['files'] as $file) {
31 | Storage::disk($this->disk)->copy(
32 | $file,
33 | $this->renamePath($file, $this->path)
34 | );
35 | }
36 |
37 | // directories
38 | foreach ($this->clipboard['directories'] as $directory) {
39 | $this->copyDirectory($directory);
40 | }
41 | }
42 |
43 | /**
44 | * Cut files and folders
45 | */
46 | protected function cut()
47 | {
48 | // files
49 | foreach ($this->clipboard['files'] as $file) {
50 | Storage::disk($this->disk)->move(
51 | $file,
52 | $this->renamePath($file, $this->path)
53 | );
54 | }
55 |
56 | // directories
57 | foreach ($this->clipboard['directories'] as $directory) {
58 | Storage::disk($this->disk)->move(
59 | $directory,
60 | $this->renamePath($directory, $this->path)
61 | );
62 | }
63 | }
64 |
65 | /**
66 | * Copy directory
67 | *
68 | * @param $directory
69 | */
70 | protected function copyDirectory($directory)
71 | {
72 | // get all directories in this directory
73 | $allDirectories = Storage::disk($this->disk)
74 | ->allDirectories($directory);
75 |
76 | $partsForRemove = count(explode('/', $directory)) - 1;
77 |
78 | // create this directories
79 | foreach ($allDirectories as $dir) {
80 | Storage::disk($this->disk)->makeDirectory(
81 | $this->transformPath(
82 | $dir,
83 | $this->path,
84 | $partsForRemove
85 | )
86 | );
87 | }
88 |
89 | // get all files
90 | $allFiles = Storage::disk($this->disk)->allFiles($directory);
91 |
92 | // copy files
93 | foreach ($allFiles as $file) {
94 | Storage::disk($this->disk)->copy(
95 | $file,
96 | $this->transformPath($file, $this->path, $partsForRemove)
97 | );
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/routes.php:
--------------------------------------------------------------------------------
1 | getMiddleware();
11 |
12 | /**
13 | * If ACL ON add "fm-acl" middleware to array
14 | */
15 | if ($config->getAcl()) {
16 | $middleware[] = 'fm-acl';
17 | }
18 |
19 | Route::group([
20 | 'middleware' => $middleware,
21 | 'prefix' => $config->getRoutePrefix(),
22 | 'namespace' => 'Alexusmai\LaravelFileManager\Controllers',
23 | ], function () {
24 |
25 | Route::get('initialize', [FileManagerController::class, 'initialize'])
26 | ->name('fm.initialize');
27 |
28 | Route::get('content', [FileManagerController::class, 'content'])
29 | ->name('fm.content');
30 |
31 | Route::get('tree', [FileManagerController::class, 'tree'])
32 | ->name('fm.tree');
33 |
34 | Route::get('select-disk', [FileManagerController::class, 'selectDisk'])
35 | ->name('fm.select-disk');
36 |
37 | Route::post('upload', [FileManagerController::class, 'upload'])
38 | ->name('fm.upload');
39 |
40 | Route::post('delete', [FileManagerController::class, 'delete'])
41 | ->name('fm.delete');
42 |
43 | Route::post('paste', [FileManagerController::class, 'paste'])
44 | ->name('fm.paste');
45 |
46 | Route::post('rename', [FileManagerController::class, 'rename'])
47 | ->name('fm.rename');
48 |
49 | Route::get('download', [FileManagerController::class, 'download'])
50 | ->name('fm.download');
51 |
52 | Route::get('thumbnails', [FileManagerController::class, 'thumbnails'])
53 | ->name('fm.thumbnails');
54 |
55 | Route::get('preview', [FileManagerController::class, 'preview'])
56 | ->name('fm.preview');
57 |
58 | Route::get('url', [FileManagerController::class, 'url'])
59 | ->name('fm.url');
60 |
61 | Route::post('create-directory', [FileManagerController::class, 'createDirectory'])
62 | ->name('fm.create-directory');
63 |
64 | Route::post('create-file', [FileManagerController::class, 'createFile'])
65 | ->name('fm.create-file');
66 |
67 | Route::post('update-file', [FileManagerController::class, 'updateFile'])
68 | ->name('fm.update-file');
69 |
70 | Route::get('stream-file', [FileManagerController::class, 'streamFile'])
71 | ->name('fm.stream-file');
72 |
73 | Route::post('zip', [FileManagerController::class, 'zip'])
74 | ->name('fm.zip');
75 |
76 | Route::post('unzip', [FileManagerController::class, 'unzip'])
77 | ->name('fm.unzip');
78 |
79 | // Route::get('properties', 'FileManagerController@properties');
80 |
81 | // Integration with editors
82 | Route::get('ckeditor', [FileManagerController::class, 'ckeditor'])
83 | ->name('fm.ckeditor');
84 |
85 | Route::get('tinymce', [FileManagerController::class, 'tinymce'])
86 | ->name('fm.tinymce');
87 |
88 | Route::get('tinymce5', [FileManagerController::class, 'tinymce5'])
89 | ->name('fm.tinymce5');
90 |
91 | Route::get('summernote', [FileManagerController::class, 'summernote'])
92 | ->name('fm.summernote');
93 |
94 | Route::get('fm-button', [FileManagerController::class, 'fmButton'])
95 | ->name('fm.fm-button');
96 | });
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel File Manager
2 |
3 | [](https://packagist.org/packages/alexusmai/laravel-file-manager)
4 | [](https://packagist.org/packages/alexusmai/laravel-file-manager)
5 | [](https://packagist.org/packages/alexusmai/laravel-file-manager)
6 | [](https://packagist.org/packages/alexusmai/laravel-file-manager)
7 | [](https://packagist.org/packages/alexusmai/laravel-file-manager)
8 |
9 | 
10 |
11 | **Vue.js Frontend:** [alexusmai/vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
12 |
13 | ## Documentation
14 |
15 | [Laravel File Manager Docs](./docs/index.md)
16 | * [Installation](./docs/installation.md)
17 | * [Configuration](./docs/configuration.md)
18 | * [Integration](./docs/integration.md)
19 | * [ACL](./docs/acl.md)
20 | * [Events](./docs/events.md)
21 | * [Update](./docs/update.md)
22 |
23 | ## Features
24 |
25 | * Frontend on Vue.js - [vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
26 | * Work with the file system is organized by the standard means Laravel Flysystem:
27 | * Local, (S)FTP, S3, Dropbox ...
28 | * The ability to work only with the selected disks
29 | * Several options for displaying the file manager:
30 | * One-panel view
31 | * One-panel + Directory tree
32 | * Two-panel
33 | * The minimum required set of operations:
34 | * Creating files
35 | * Creating folders
36 | * Copying / Cutting Folders and Files
37 | * Renaming
38 | * Uploading files (multi-upload)
39 | * Downloading files
40 | * Two modes of displaying elements - table and grid
41 | * Preview for images
42 | * Viewing images
43 | * Full screen mode
44 | * More operations (v.2):
45 | * Audio player (mp3, ogg, wav, aac), Video player (webm, mp4) - ([Plyr](https://github.com/sampotts/plyr))
46 | * Code editor - ([Code Mirror](https://github.com/codemirror/codemirror))
47 | * Image cropper - ([Cropper.js](https://github.com/fengyuanchen/cropperjs))
48 | * Zip / Unzip - only for local disks
49 | * Integration with WYSIWYG Editors:
50 | * CKEditor 4
51 | * TinyMCE 4
52 | * TinyMCE 5
53 | * SummerNote
54 | * Standalone button
55 | * ACL - access control list
56 | * delimiting access to files and folders
57 | * two work strategies:
58 | * blacklist - Allow everything that is not forbidden by the ACL rules list
59 | * whitelist - Deny everything, that not allowed by the ACL rules list
60 | * You can use different repositories for the rules - an array (configuration file), a database (there is an example implementation), or you can add your own.
61 | * You can hide files and folders that are not accessible.
62 | * Events (v2.2)
63 | * Thumbnails lazy load
64 | * Dynamic configuration (v2.4)
65 | * Supported locales : ru, en, ar, sr, cs, de, es, nl, zh-CN, fa, it, tr, fr, pt-BR, zh-TW, pl
66 |
67 | ## In a new version 3
68 |
69 | - **Version 3 only works with Laravel 9, 10 and 11!**
70 | - Vue.js 3
71 | - Bootstrap 5
72 | - Bootstrap Icons
73 |
--------------------------------------------------------------------------------
/src/Services/ConfigService/ConfigRepository.php:
--------------------------------------------------------------------------------
1 | ['web', 'auth', 'admin']
101 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!!
102 | *
103 | * @return array
104 | */
105 | public function getMiddleware(): array;
106 |
107 | /**
108 | * ACL mechanism ON/OFF
109 | *
110 | * default - false(OFF)
111 | *
112 | * @return bool
113 | */
114 | public function getAcl(): bool;
115 |
116 | /**
117 | * Hide files and folders from file-manager if user doesn't have access
118 | *
119 | * ACL access level = 0
120 | *
121 | * @return bool
122 | */
123 | public function getAclHideFromFM(): bool;
124 |
125 | /**
126 | * ACL strategy
127 | *
128 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
129 | *
130 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
131 | *
132 | * @return string
133 | */
134 | public function getAclStrategy(): string;
135 |
136 | /**
137 | * ACL rules repository
138 | *
139 | * default - config file(ConfigACLRepository)
140 | *
141 | * @return string
142 | */
143 | public function getAclRepository(): string;
144 |
145 | /**
146 | * ACL Rules cache
147 | *
148 | * null or value in minutes
149 | *
150 | * @return int|null
151 | */
152 | public function getAclRulesCache(): ?int;
153 |
154 | /**
155 | * Whether to slugify filenames
156 | *
157 | * boolean
158 | *
159 | * @return bool|null
160 | */
161 | public function getSlugifyNames(): ?bool;
162 | }
163 |
--------------------------------------------------------------------------------
/src/Services/TransferService/ExternalTransfer.php:
--------------------------------------------------------------------------------
1 | manager = new MountManager([
31 | 'from' => Storage::drive($clipboard['disk'])->getDriver(),
32 | 'to' => Storage::drive($disk)->getDriver(),
33 | ]);
34 | }
35 |
36 | /**
37 | * Copy files and folders
38 | *
39 | * @return void
40 | * @throws FilesystemException
41 | */
42 | protected function copy()
43 | {
44 | // files
45 | foreach ($this->clipboard['files'] as $file) {
46 | $this->copyToDisk(
47 | $file,
48 | $this->renamePath($file, $this->path)
49 | );
50 | }
51 |
52 | // directories
53 | foreach ($this->clipboard['directories'] as $directory) {
54 | $this->copyDirectoryToDisk($directory);
55 | }
56 | }
57 |
58 | /**
59 | * Cut files and folders
60 | *
61 | * @return void
62 | * @throws FilesystemException
63 | */
64 | protected function cut()
65 | {
66 | // files
67 | foreach ($this->clipboard['files'] as $file) {
68 | $this->moveToDisk(
69 | $file,
70 | $this->renamePath($file, $this->path)
71 | );
72 | }
73 |
74 | // directories
75 | foreach ($this->clipboard['directories'] as $directory) {
76 | $this->copyDirectoryToDisk($directory);
77 |
78 | // remove directory
79 | Storage::disk($this->clipboard['disk'])
80 | ->deleteDirectory($directory);
81 | }
82 | }
83 |
84 | /**
85 | * Copy directory to another disk
86 | *
87 | * @param $directory
88 | *
89 | * @return void
90 | * @throws FilesystemException
91 | */
92 | protected function copyDirectoryToDisk($directory)
93 | {
94 | // get all directories in this directory
95 | $allDirectories = Storage::disk($this->clipboard['disk'])
96 | ->allDirectories($directory);
97 |
98 | $partsForRemove = count(explode('/', $directory)) - 1;
99 |
100 | // create this directories
101 | foreach ($allDirectories as $dir) {
102 | Storage::disk($this->disk)->makeDirectory(
103 | $this->transformPath($dir, $this->path, $partsForRemove)
104 | );
105 | }
106 |
107 | // get all files
108 | $allFiles = Storage::disk($this->clipboard['disk'])
109 | ->allFiles($directory);
110 |
111 | // copy files
112 | foreach ($allFiles as $file) {
113 | $this->copyToDisk($file,
114 | $this->transformPath($file, $this->path, $partsForRemove));
115 | }
116 | }
117 |
118 | /**
119 | * Copy files to disk
120 | *
121 | * @param $filePath
122 | * @param $newPath
123 | *
124 | * @return void
125 | * @throws FilesystemException
126 | */
127 | protected function copyToDisk($filePath, $newPath)
128 | {
129 | $this->manager->copy(
130 | 'from://'.$filePath,
131 | 'to://'.$newPath
132 | );
133 | }
134 |
135 | /**
136 | * Move files to disk
137 | *
138 | * @param $filePath
139 | * @param $newPath
140 | *
141 | * @return void
142 | * @throws FilesystemException
143 | */
144 | protected function moveToDisk($filePath, $newPath)
145 | {
146 | $this->manager->move(
147 | 'from://'.$filePath,
148 | 'to://'.$newPath
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Open configuration file - config/file-manager.php
4 |
5 | - fill the disk list from config/filesystem.php (select the desired drive names)
6 | - set cache
7 | - select file manager windows configuration
8 |
9 | **!!! Be sure to add your middleware to restrict access to the application !!!**
10 |
11 | **Don't forget to configure your php and Nginx**
12 |
13 | ```
14 | // PHP
15 | upload_max_filesize,
16 | post_max_size
17 |
18 | // Nginx
19 | client_max_body_size
20 | ```
21 |
22 | ### You can set default disk and default path
23 |
24 | You have two variants for how to do it:
25 |
26 | 1. Add this params to the config file (config/file-manager.php)
27 |
28 | ```php
29 | /**
30 | * Default disk for left manager
31 | * null - auto select the first disk in the disk list
32 | */
33 | 'leftDisk' => 'public',
34 |
35 | /**
36 | * Default disk for right manager
37 | * null - auto select the first disk in the disk list
38 | */
39 | 'rightDisk' => null,
40 |
41 | /**
42 | * Default path for left manager
43 | * null - root directory
44 | */
45 | 'leftPath' => 'directory/sub-directory',
46 |
47 | /**
48 | * Default path for right manager
49 | * null - root directory
50 | */
51 | 'rightPath' => null,
52 | ```
53 |
54 | 2 Or you can add this params in URL
55 |
56 | ```
57 | http://site.name/?leftDisk=public
58 |
59 | http://site.name/?leftDisk=public&rightDisk=images
60 |
61 | http://site.name/?leftDisk=public&leftPath=directory/sub-directory
62 |
63 | http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars/vw/golf
64 | // %2F - /, %20 - space
65 | http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars%2Fvw%2Fgolf
66 | ```
67 |
68 | leftDisk and leftPath is default for the file manager windows configuration - 1,2
69 |
70 | **You can't add a disk that does not exist in the diskList array !**
71 |
72 | **! Params in URL have more weight than params in config file. It means that URL params can overwrite your config params. !**
73 |
74 | ## Disk settings example
75 |
76 | - config/filesystems.php
77 |
78 | ```php
79 | // Filesystem Disks
80 | 'disks' => [
81 | // images folder in public path
82 | 'images' => [
83 | 'driver' => 'local',
84 | 'root' => public_path('images'),
85 | 'url' => env('APP_URL').'/images',
86 | ],
87 |
88 | // public folder in storage/app/public
89 | 'public' => [
90 | 'driver' => 'local',
91 | 'root' => storage_path('app/public'),
92 | 'url' => env('APP_URL').'/storage', // https://laravel.com/docs/5.7/filesystem#file-urls
93 | 'visibility' => 'public',
94 | ],
95 |
96 | // ftp
97 | 'dd-wrt' => [
98 | 'driver' => 'ftp',
99 | 'host' => 'ftp.dd-wrt.com',
100 | 'username' => 'anonymous',
101 | 'passive' => true,
102 | 'timeout' => 30,
103 | ],
104 | ],
105 | ```
106 |
107 | - config/file-manager.php
108 |
109 | ```php
110 | // You need to enter the disks you want to use in the file manager
111 | 'diskList' => ['images', 'public'],
112 | ```
113 |
114 | ## Normalization of uploaded file names
115 |
116 | If you expect to work with files that may have filenames that are not considered *normal* `f.e.: "DCIM_2021 - čšč& (1).jpg.jpg"` so basically any time you give the option to upload files to users, you can set `slugifyNames` to `true` and have the names ran through `Str::slug()` before saving it so you file will look something like `dcim-2021-csc-1.jpg`
117 |
118 | ## Dynamic configuration
119 |
120 | You can create your own configuration, for example for different users or their roles.
121 |
122 | Create new class - example - TestConfigRepository
123 |
124 | ```php
125 | namespace App\Http;
126 |
127 | use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
128 |
129 | class TestConfigRepository implements ConfigRepository
130 | {
131 | // implement all methods from interface
132 |
133 | /**
134 | * Get disk list
135 | *
136 | * ['public', 'local', 's3']
137 | *
138 | * @return array
139 | */
140 | public function getDiskList(): array
141 | {
142 | if (\Auth::id() === 1) {
143 | return [
144 | ['public', 'local', 's3'],
145 | ];
146 | }
147 |
148 | return ['public'];
149 | }
150 |
151 | ...
152 | }
153 | ```
154 |
155 | For example see [src/Services/ConfigService/DefaultConfigRepository.php](https://github.com/alexusmai/laravel-file-manager/blob/master/src/Services/ConfigService/DefaultConfigRepository.php)
156 |
157 | ## What's next
158 |
159 | [ACL](./acl.md)
160 |
161 | [Integration](./integration.md)
162 |
--------------------------------------------------------------------------------
/config/file-manager.php:
--------------------------------------------------------------------------------
1 | DefaultConfigRepository::class,
14 |
15 | /**
16 | * ACL rules repository
17 | *
18 | * Default - ConfigACLRepository (see rules in - aclRules)
19 | */
20 | 'aclRepository' => ConfigACLRepository::class,
21 |
22 | //********* Default configuration for DefaultConfigRepository **************
23 |
24 | /**
25 | * LFM Route prefix
26 | * !!! WARNING - if you change it, you should compile frontend with new prefix(baseUrl) !!!
27 | */
28 | 'routePrefix' => 'file-manager',
29 |
30 | /**
31 | * List of disk names that you want to use
32 | * (from config/filesystems)
33 | */
34 | 'diskList' => ['public'],
35 |
36 | /**
37 | * Default disk for left manager
38 | *
39 | * null - auto select the first disk in the disk list
40 | */
41 | 'leftDisk' => null,
42 |
43 | /**
44 | * Default disk for right manager
45 | *
46 | * null - auto select the first disk in the disk list
47 | */
48 | 'rightDisk' => null,
49 |
50 | /**
51 | * Default path for left manager
52 | *
53 | * null - root directory
54 | */
55 | 'leftPath' => null,
56 |
57 | /**
58 | * Default path for right manager
59 | *
60 | * null - root directory
61 | */
62 | 'rightPath' => null,
63 |
64 | /**
65 | * File manager modules configuration
66 | *
67 | * 1 - only one file manager window
68 | * 2 - one file manager window with directories tree module
69 | * 3 - two file manager windows
70 | */
71 | 'windowsConfig' => 2,
72 |
73 | /**
74 | * File upload - Max file size in KB
75 | *
76 | * null - no restrictions
77 | */
78 | 'maxUploadFileSize' => null,
79 |
80 | /**
81 | * File upload - Allow these file types
82 | *
83 | * [] - no restrictions
84 | */
85 | 'allowFileTypes' => [],
86 |
87 | /**
88 | * Show / Hide system files and folders
89 | */
90 | 'hiddenFiles' => true,
91 |
92 | /***************************************************************************
93 | * Middleware
94 | *
95 | * Add your middleware name to array -> ['web', 'auth', 'admin']
96 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!!
97 | */
98 | 'middleware' => ['web'],
99 |
100 | /***************************************************************************
101 | * ACL mechanism ON/OFF
102 | *
103 | * default - false(OFF)
104 | */
105 | 'acl' => false,
106 |
107 | /**
108 | * Hide files and folders from file-manager if user doesn't have access
109 | *
110 | * ACL access level = 0
111 | */
112 | 'aclHideFromFM' => true,
113 |
114 | /**
115 | * ACL strategy
116 | *
117 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
118 | *
119 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
120 | */
121 | 'aclStrategy' => 'blacklist',
122 |
123 | /**
124 | * ACL Rules cache
125 | *
126 | * null or value in minutes
127 | */
128 | 'aclRulesCache' => null,
129 |
130 | //********* Default configuration for DefaultConfigRepository END **********
131 |
132 |
133 | /***************************************************************************
134 | * ACL rules list - used for default ACL repository (ConfigACLRepository)
135 | *
136 | * 1 it's user ID
137 | * null - for not authenticated user
138 | *
139 | * 'disk' => 'disk-name'
140 | *
141 | * 'path' => 'folder-name'
142 | * 'path' => 'folder1*' - select folder1, folder12, folder1/sub-folder, ...
143 | * 'path' => 'folder2/*' - select folder2/sub-folder,... but not select folder2 !!!
144 | * 'path' => 'folder-name/file-name.jpg'
145 | * 'path' => 'folder-name/*.jpg'
146 | *
147 | * * - wildcard
148 | *
149 | * access: 0 - deny, 1 - read, 2 - read/write
150 | */
151 | 'aclRules' => [
152 | null => [
153 | //['disk' => 'public', 'path' => '/', 'access' => 2],
154 | ],
155 | 1 => [
156 | //['disk' => 'public', 'path' => 'images/arch*.jpg', 'access' => 2],
157 | //['disk' => 'public', 'path' => 'files/*', 'access' => 1],
158 | ],
159 | ],
160 |
161 | /**
162 | * Enable slugification of filenames of uploaded files.
163 | *
164 | */
165 | 'slugifyNames' => false,
166 | ];
167 |
--------------------------------------------------------------------------------
/src/Services/ConfigService/DefaultConfigRepository.php:
--------------------------------------------------------------------------------
1 | ['web', 'auth', 'admin']
131 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!!
132 | *
133 | * @return array
134 | */
135 | final public function getMiddleware(): array
136 | {
137 | return config('file-manager.middleware');
138 | }
139 |
140 | /**
141 | * ACL mechanism ON/OFF
142 | *
143 | * default - false(OFF)
144 | *
145 | * @return bool
146 | */
147 | final public function getAcl(): bool
148 | {
149 | return config('file-manager.acl');
150 | }
151 |
152 | /**
153 | * Hide files and folders from file-manager if user doesn't have access
154 | *
155 | * ACL access level = 0
156 | *
157 | * @return bool
158 | */
159 | final public function getAclHideFromFM(): bool
160 | {
161 | return config('file-manager.aclHideFromFM');
162 | }
163 |
164 | /**
165 | * ACL strategy
166 | *
167 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
168 | *
169 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
170 | *
171 | * @return string
172 | */
173 | final public function getAclStrategy(): string
174 | {
175 | return config('file-manager.aclStrategy');
176 | }
177 |
178 | /**
179 | * ACL rules repository
180 | *
181 | * default - config file(ConfigACLRepository)
182 | *
183 | * @return string
184 | */
185 | final public function getAclRepository(): string
186 | {
187 | return config('file-manager.aclRepository');
188 | }
189 |
190 | /**
191 | * ACL Rules cache
192 | *
193 | * null or value in minutes
194 | *
195 | * @return int|null
196 | */
197 | final public function getAclRulesCache(): ?int
198 | {
199 | return config('file-manager.aclRulesCache');
200 | }
201 |
202 | /**
203 | * Whether to slugify filenames
204 | *
205 | * boolean
206 | *
207 | * @return bool|null
208 | */
209 | final public function getSlugifyNames(): ?bool
210 | {
211 | return config('file-manager.slugifyNames', false);
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/docs/acl.md:
--------------------------------------------------------------------------------
1 | # ACL
2 |
3 | You can use the access control system to differentiate access to files and folders for different users.
4 | For this you need to make the following settings.
5 | Open configuration file - config/file-manager.php
6 |
7 | 1. Turn ON ACL system (fm-acl middleware will turn ON automatically)
8 |
9 | ```php
10 | // set true
11 | 'acl' => true,
12 | ```
13 |
14 | 2. You can hide files and folders to which the user does not have access(access = 0).
15 |
16 | ```php
17 | 'aclHideFromFM' => true,
18 | ```
19 | 3. ACL system operation strategies:
20 |
21 | ```php
22 | /**
23 | * ACL strategy
24 | *
25 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
26 | *
27 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
28 | */
29 | 'aclStrategy' => 'blacklist',
30 | ```
31 |
32 | 4. Set the rule repository, the default is the configuration file.
33 |
34 | ```php
35 | /**
36 | * ACL rules repository
37 | *
38 | * default - config file(ConfigACLRepository)
39 | */
40 | 'aclRepository' => \Alexusmai\LaravelFileManager\Services\ACLService\ConfigACLRepository::class,
41 | ```
42 |
43 | Now you can add your rules in 'aclRules' array. But if you want to store your rules in another place, such as a database, you need to create your own class, and implements two functions from ACLRepository.
44 |
45 | I have already made a similar class for an example, and if it suits you, you can use it. You only need to replace the repository name in the configuration file. And add a new migration to the database.
46 |
47 | ```php
48 | php artisan vendor:publish --tag=fm-migrations
49 | ```
50 |
51 | See [/src/Services/ACLService/DBACLRepository.php](../src/Services/ACLService/DBACLRepository.php) and [/migrations/2019_02_06_174631_make_acl_rules_table.php](./../migrations/2019_02_06_174631_make_acl_rules_table.php)
52 |
53 | ## Example 1
54 |
55 | I have disk 'images' in /config/filesystems.php for folder /public/images
56 |
57 | ```php
58 | 'disks' => [
59 |
60 | 'images' => [
61 | 'driver' => 'local',
62 | 'root' => public_path('images'),
63 | 'url' => env('APP_URL').'/images/',
64 | ],
65 | ]
66 | ```
67 |
68 | This disk contain:
69 |
70 | ```php
71 | / // disk root folder
72 | |-- nature // folder
73 | |-- cars // folder
74 | |-- icons
75 | |-- image1.jpg // file
76 | |-- image2.jpg
77 | |-- avatar.png
78 | ```
79 |
80 | I add this disk to file-manager config file
81 |
82 | ```php
83 | 'diskList' => ['images'],
84 |
85 | 'aclStrategy' => 'blacklist',
86 |
87 | // now it's a black list
88 | 'aclRules' => [
89 | // null - for not authenticated users
90 | null => [
91 | ['disk' => 'images', 'path' => 'nature', 'access' => 0], // guest don't have access for this folder
92 | ['disk' => 'images', 'path' => 'icons', 'access' => 1], // only read - guest can't change folder - rename, delete
93 | ['disk' => 'images', 'path' => 'icons/*', 'access' => 1], // only read all files and foders in this folder
94 | ['disk' => 'images', 'path' => 'image*.jpg', 'access' => 0], // can't read and write (preview, rename, delete..)
95 | ['disk' => 'images', 'path' => 'avatar.png', 'access' => 1], // only read (view)
96 |
97 | ],
98 | // for user with ID = 1
99 | 1 => [
100 | ['disk' => 'images', 'path' => 'cars', 'access' => 0], // don't have access
101 | ['disk' => 'public', 'path' => 'image*.jpg', 'access' => 1], // only read (view)
102 | ],
103 | ],
104 | ```
105 |
106 | ## Example 2
107 |
108 | > Task: For each registered user, a new folder is created with his name(in folder /users). You want to allow users access only to their folders. But for an administrator with ID = 1, allow access to all folders.
109 |
110 | - You need to create a new repository for ACL rules, for example, in the / app / Http folder
111 |
112 | ```php
113 | 'disk-name', 'path' => '*', 'access' => 2],
141 | ];
142 | }
143 |
144 | return [
145 | ['disk' => 'disk-name', 'path' => '/', 'access' => 1], // main folder - read
146 | ['disk' => 'disk-name', 'path' => 'users', 'access' => 1], // only read
147 | ['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name, 'access' => 1], // only read
148 | ['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name .'/*', 'access' => 2], // read and write
149 | ];
150 | }
151 | }
152 | ```
153 |
154 | - disk-name - you need to replace for your disk name
155 |
156 | - now in the config file we will change the repository to a new one, and set aclStrategy in whitelist - we will deny everything that is not allowed by the rules. You can also hide folders and files that are not available.
157 |
158 | ```php
159 | /**
160 | * Hide files and folders from file-manager if user doesn't have access
161 | * ACL access level = 0
162 | */
163 | 'aclHideFromFM' => true,
164 |
165 | /**
166 | * ACL strategy
167 | *
168 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
169 | *
170 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
171 | */
172 | 'aclStrategy' => 'whitelist',
173 |
174 | /**
175 | * ACL rules repository
176 | *
177 | * default - config file(ConfigACLRepository)
178 | */
179 | 'aclRepository' => \App\Http\UsersACLRepository::class,
180 | ```
181 |
182 |
183 | ## What's next
184 |
185 | [Events](./events.md)
186 |
--------------------------------------------------------------------------------
/docs/integration.md:
--------------------------------------------------------------------------------
1 | # Integration
2 |
3 | > See examples in [examples](./../examples) folder
4 |
5 | ### CKEditor 4
6 |
7 | Add to CKEditor config
8 |
9 | ```js
10 | CKEDITOR.replace('editor-id', {filebrowserImageBrowseUrl: '/file-manager/ckeditor'});
11 | ```
12 |
13 | OR in to the config.js
14 |
15 | ```js
16 | CKEDITOR.editorConfig = function( config ) {
17 |
18 | //...
19 |
20 | // Upload image
21 | config.filebrowserImageBrowseUrl = '/file-manager/ckeditor';
22 | };
23 | ```
24 |
25 | After these actions, you will be able to call the file manager from the CKEditor editor menu (Image -> Selection on the server).
26 | The file manager will appear in a new window.
27 |
28 | ### TinyMCE 4
29 |
30 | Add to TinyMCE configuration
31 |
32 | ```js
33 | tinymce.init({
34 | selector: '#my-textarea',
35 | // ...
36 | file_browser_callback: function(field_name, url, type, win) {
37 | tinyMCE.activeEditor.windowManager.open({
38 | file: '/file-manager/tinymce',
39 | title: 'Laravel File Manager',
40 | width: window.innerWidth * 0.8,
41 | height: window.innerHeight * 0.8,
42 | resizable: 'yes',
43 | close_previous: 'no',
44 | }, {
45 | setUrl: function(url) {
46 | win.document.getElementById(field_name).value = url;
47 | },
48 | });
49 | },
50 | });
51 | ```
52 |
53 | ### TinyMCE 5
54 |
55 | Add to TinyMCE 5 configuration
56 |
57 | ```js
58 | tinymce.init({
59 | selector: '#my-textarea',
60 | // ...
61 | file_picker_callback (callback, value, meta) {
62 | let x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth
63 | let y = window.innerHeight|| document.documentElement.clientHeight|| document.getElementsByTagName('body')[0].clientHeight
64 |
65 | tinymce.activeEditor.windowManager.openUrl({
66 | url : '/file-manager/tinymce5',
67 | title : 'Laravel File manager',
68 | width : x * 0.8,
69 | height : y * 0.8,
70 | onMessage: (api, message) => {
71 | callback(message.content, { text: message.text })
72 | }
73 | })
74 | }
75 | });
76 | ```
77 |
78 | ### SummerNote
79 |
80 | Create and add new button
81 |
82 | ```js
83 | // File manager button (image icon)
84 | const FMButton = function(context) {
85 | const ui = $.summernote.ui;
86 | const button = ui.button({
87 | contents: ' ',
88 | tooltip: 'File Manager',
89 | click: function() {
90 | window.open('/file-manager/summernote', 'fm', 'width=1400,height=800');
91 | }
92 | });
93 | return button.render();
94 | };
95 |
96 | $('#summernote').summernote({
97 | toolbar: [
98 | // [groupName, [list of button]]
99 | // your settings
100 | ['fm-button', ['fm']],
101 | ],
102 | buttons: {
103 | fm: FMButton
104 | }
105 | });
106 | ```
107 |
108 | And add this function
109 |
110 | ```js
111 | // set file link
112 | function fmSetLink(url) {
113 | $('#summernote').summernote('insertImage', url);
114 | }
115 | ```
116 |
117 | See [example](./../examples/wysiwyg/summernote.blade.php)
118 |
119 | ### Standalone button
120 |
121 | Add button
122 |
123 | ```html
124 |
131 | ```
132 |
133 | and js script
134 |
135 | ```js
136 | document.addEventListener("DOMContentLoaded", function() {
137 |
138 | document.getElementById('button-image').addEventListener('click', (event) => {
139 | event.preventDefault();
140 |
141 | window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800');
142 | });
143 | });
144 |
145 | // set file link
146 | function fmSetLink($url) {
147 | document.getElementById('image_label').value = $url;
148 | }
149 | ```
150 |
151 | ### Multiple standalone buttons
152 |
153 | ```html
154 |
155 |
179 |
180 |
181 |
210 | ```
211 |
212 | ### Modifications
213 |
214 | To change standard views(with file manager), publish them.
215 |
216 | ```bash
217 | php artisan vendor:publish --tag=fm-views
218 | ```
219 |
220 | You will get:
221 |
222 | ```
223 | resources/views/vendor/file-manager/ckeditor.blade.php
224 | resources/views/vendor/file-manager/tinymce.blade.php
225 | resources/views/vendor/file-manager/summernote.blade.php
226 | resources/views/vendor/file-manager/fmButton.blade.php
227 | ```
228 |
229 | Now you can change styles and any more..
230 |
--------------------------------------------------------------------------------
/src/Services/Zip.php:
--------------------------------------------------------------------------------
1 | zip = $zip;
30 | $this->request = $request;
31 | //$this->pathPrefix = Storage::disk($request->input('disk'))->path();
32 | //->getDriver()
33 | //->getAdapter()
34 | //->getPathPrefix();
35 | }
36 |
37 | /**
38 | * Create new zip archive
39 | *
40 | * @return array
41 | */
42 | public function create(): array
43 | {
44 | if ($this->createArchive()) {
45 | return [
46 | 'result' => [
47 | 'status' => 'success',
48 | 'message' => null,
49 | ],
50 | ];
51 | }
52 |
53 | return [
54 | 'result' => [
55 | 'status' => 'warning',
56 | 'message' => 'zipError',
57 | ],
58 | ];
59 | }
60 |
61 | /**
62 | * Extract
63 | *
64 | * @return array
65 | */
66 | public function extract(): array
67 | {
68 | if ($this->extractArchive()) {
69 | return [
70 | 'result' => [
71 | 'status' => 'success',
72 | 'message' => null,
73 | ],
74 | ];
75 | }
76 |
77 | return [
78 | 'result' => [
79 | 'status' => 'warning',
80 | 'message' => 'zipError',
81 | ],
82 | ];
83 | }
84 |
85 | protected function prefixer($path): string
86 | {
87 | return Storage::disk($this->request->input('disk'))->path($path);
88 | }
89 |
90 | /**
91 | * Create zip archive
92 | *
93 | * @return bool
94 | */
95 | protected function createArchive(): bool
96 | {
97 | // elements list
98 | $elements = $this->request->input('elements');
99 |
100 | // Check files for traversal
101 | if (isset($elements['files']) && is_array($elements['files'])) {
102 | foreach ($elements['files'] as $file) {
103 | if (strpos($file, '..') !== false) {
104 | event(new ZipFailed($this->request));
105 | return false;
106 | }
107 | }
108 | }
109 |
110 | // Check directories for traversal
111 | if (isset($elements['directories']) && is_array($elements['directories'])) {
112 | foreach ($elements['directories'] as $directory) {
113 | if (strpos($directory, '..') !== false) {
114 | event(new ZipFailed($this->request));
115 | return false;
116 | }
117 | }
118 | }
119 |
120 | // create or overwrite archive
121 | if ($this->zip->open(
122 | $this->createName(),
123 | ZIPARCHIVE::OVERWRITE | ZIPARCHIVE::CREATE
124 | ) === true
125 | ) {
126 | if (isset($elements['files']) && $elements['files']) {
127 | foreach ($elements['files'] as $file) {
128 | $this->zip->addFile(
129 | $this->prefixer($file),
130 | basename($file)
131 | );
132 | }
133 | }
134 |
135 | if (isset($elements['directories']) && $elements['directories']) {
136 | $this->addDirs($elements['directories']);
137 | }
138 |
139 | $this->zip->close();
140 |
141 | event(new ZipCreated($this->request));
142 |
143 | return true;
144 | }
145 |
146 | event(new ZipFailed($this->request));
147 |
148 | return false;
149 | }
150 |
151 | /**
152 | * Archive extract
153 | *
154 | * @return bool
155 | */
156 | protected function extractArchive(): bool
157 | {
158 | $zipPath = $this->prefixer($this->request->input('path'));
159 | $rootPath = dirname($zipPath);
160 |
161 | // extract to new folder
162 | $folder = $this->request->input('folder');
163 |
164 | if ($folder && (strpos($folder, '..') !== false || strpos($folder, '://') !== false)) {
165 | event(new UnzipFailed($this->request));
166 | return false;
167 | }
168 |
169 | if ($this->zip->open($zipPath) === true) {
170 | $this->zip->extractTo($folder ? $rootPath.'/'.$folder : $rootPath);
171 | $this->zip->close();
172 |
173 | event(new UnzipCreated($this->request));
174 |
175 | return true;
176 | }
177 |
178 | event(new UnzipFailed($this->request));
179 |
180 | return false;
181 | }
182 |
183 | /**
184 | * Add directories - recursive
185 | *
186 | * @param array $directories
187 | */
188 | protected function addDirs(array $directories)
189 | {
190 | foreach ($directories as $directory) {
191 |
192 | // Create recursive directory iterator
193 | $files = new RecursiveIteratorIterator(
194 | new RecursiveDirectoryIterator($this->prefixer($directory)),
195 | RecursiveIteratorIterator::LEAVES_ONLY
196 | );
197 |
198 | foreach ($files as $name => $file) {
199 | // Get real and relative path for current item
200 | $filePath = $file->getRealPath();
201 | $relativePath = substr(
202 | $filePath,
203 | strlen($this->fullPath($this->request->input('path')))
204 | );
205 |
206 | if (!$file->isDir()) {
207 | // Add current file to archive
208 | $this->zip->addFile($filePath, $relativePath);
209 | } else {
210 | // add empty folders
211 | if (!glob($filePath.'/*')) {
212 | $this->zip->addEmptyDir($relativePath);
213 | }
214 | }
215 | }
216 | }
217 | }
218 |
219 | /**
220 | * Create archive name with full path
221 | *
222 | * @return string
223 | */
224 | protected function createName(): string
225 | {
226 | return $this->fullPath($this->request->input('path'))
227 | .$this->request->input('name');
228 | }
229 |
230 | /**
231 | * Generate full path
232 | *
233 | * @param $path
234 | *
235 | * @return string
236 | */
237 | protected function fullPath($path): string
238 | {
239 | return $path ? $this->prefixer($path).'/' : $this->prefixer('');
240 | }
241 | }
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | # Events
2 |
3 | ### BeforeInitialization
4 |
5 | > Alexusmai\LaravelFileManager\Events\BeforeInitialization
6 |
7 | Example:
8 |
9 | ```php
10 | \Event::listen('Alexusmai\LaravelFileManager\Events\BeforeInitialization',
11 | function ($event) {
12 |
13 | }
14 | );
15 | ```
16 |
17 | ### DiskSelected
18 |
19 | > Alexusmai\LaravelFileManager\Events\DiskSelected
20 |
21 | Example:
22 |
23 | ```php
24 | \Event::listen('Alexusmai\LaravelFileManager\Events\DiskSelected',
25 | function ($event) {
26 | \Log::info('DiskSelected:', [$event->disk()]);
27 | }
28 | );
29 | ```
30 |
31 | ### FilesUploading
32 |
33 | > Alexusmai\LaravelFileManager\Events\FilesUploading
34 |
35 | ```php
36 | \Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploading',
37 | function ($event) {
38 | \Log::info('FilesUploading:', [
39 | $event->disk(),
40 | $event->path(),
41 | $event->files(),
42 | $event->overwrite(),
43 | ]);
44 | }
45 | );
46 | ```
47 |
48 | ### FilesUploaded
49 |
50 | > Alexusmai\LaravelFileManager\Events\FilesUploaded
51 |
52 | ```php
53 | \Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploaded',
54 | function ($event) {
55 | \Log::info('FilesUploaded:', [
56 | $event->disk(),
57 | $event->path(),
58 | $event->files(),
59 | $event->overwrite(),
60 | ]);
61 | }
62 | );
63 | ```
64 |
65 | ### Deleting
66 |
67 | > Alexusmai\LaravelFileManager\Events\Deleting
68 |
69 | ```php
70 | \Event::listen('Alexusmai\LaravelFileManager\Events\Deleting',
71 | function ($event) {
72 | \Log::info('Deleting:', [
73 | $event->disk(),
74 | $event->items(),
75 | ]);
76 | }
77 | );
78 | ```
79 |
80 | ### Deleted
81 |
82 | > Alexusmai\LaravelFileManager\Events\Deleted
83 |
84 | ```php
85 | \Event::listen('Alexusmai\LaravelFileManager\Events\Deleted',
86 | function ($event) {
87 | \Log::info('Deleted:', [
88 | $event->disk(),
89 | $event->items(),
90 | ]);
91 | }
92 | );
93 | ```
94 |
95 | ### Paste
96 |
97 | > Alexusmai\LaravelFileManager\Events\Paste
98 |
99 | ```php
100 | \Event::listen('Alexusmai\LaravelFileManager\Events\Paste',
101 | function ($event) {
102 | \Log::info('Paste:', [
103 | $event->disk(),
104 | $event->path(),
105 | $event->clipboard(),
106 | ]);
107 | }
108 | );
109 | ```
110 |
111 | ### Rename
112 |
113 | > Alexusmai\LaravelFileManager\Events\Rename
114 |
115 | ```php
116 | \Event::listen('Alexusmai\LaravelFileManager\Events\Rename',
117 | function ($event) {
118 | \Log::info('Rename:', [
119 | $event->disk(),
120 | $event->newName(),
121 | $event->oldName(),
122 | $event->type(), // 'file' or 'dir'
123 | ]);
124 | }
125 | );
126 | ```
127 |
128 | ### Download
129 |
130 | > Alexusmai\LaravelFileManager\Events\Download
131 |
132 | ```php
133 | \Event::listen('Alexusmai\LaravelFileManager\Events\Download',
134 | function ($event) {
135 | \Log::info('Download:', [
136 | $event->disk(),
137 | $event->path(),
138 | ]);
139 | }
140 | );
141 | ```
142 |
143 | *When using a text editor, the file you are editing is also downloaded! And this event is triggered!*
144 |
145 | ### DirectoryCreating
146 |
147 | > Alexusmai\LaravelFileManager\Events\DirectoryCreating
148 |
149 | ```php
150 | \Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreating',
151 | function ($event) {
152 | \Log::info('DirectoryCreating:', [
153 | $event->disk(),
154 | $event->path(),
155 | $event->name(),
156 | ]);
157 | }
158 | );
159 | ```
160 |
161 | ### DirectoryCreated
162 |
163 | > Alexusmai\LaravelFileManager\Events\DirectoryCreated
164 |
165 | ```php
166 | \Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreated',
167 | function ($event) {
168 | \Log::info('DirectoryCreated:', [
169 | $event->disk(),
170 | $event->path(),
171 | $event->name(),
172 | ]);
173 | }
174 | );
175 | ```
176 |
177 | ### FileCreating
178 |
179 | > Alexusmai\LaravelFileManager\Events\FileCreating
180 |
181 | ```php
182 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileCreating',
183 | function ($event) {
184 | \Log::info('FileCreating:', [
185 | $event->disk(),
186 | $event->path(),
187 | $event->name(),
188 | ]);
189 | }
190 | );
191 | ```
192 |
193 | ### FileCreated
194 |
195 | > Alexusmai\LaravelFileManager\Events\FileCreated
196 |
197 | ```php
198 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileCreated',
199 | function ($event) {
200 | \Log::info('FileCreated:', [
201 | $event->disk(),
202 | $event->path(),
203 | $event->name(),
204 | ]);
205 | }
206 | );
207 | ```
208 |
209 | ### FileUpdate
210 |
211 | > Alexusmai\LaravelFileManager\Events\FileUpdate
212 |
213 | ```php
214 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileUpdate',
215 | function ($event) {
216 | \Log::info('FileUpdate:', [
217 | $event->disk(),
218 | $event->path(),
219 | ]);
220 | }
221 | );
222 | ```
223 |
224 | ### Zip
225 |
226 | > Alexusmai\LaravelFileManager\Events\Zip
227 |
228 | ```php
229 | \Event::listen('Alexusmai\LaravelFileManager\Events\Zip',
230 | function ($event) {
231 | \Log::info('Zip:', [
232 | $event->disk(),
233 | $event->path(),
234 | $event->name(),
235 | $event->elements(),
236 | ]);
237 | }
238 | );
239 | ```
240 |
241 | ### ZipCreated
242 |
243 | > Alexusmai\LaravelFileManager\Events\ZipCreated
244 |
245 | ```php
246 | \Event::listen('Alexusmai\LaravelFileManager\Events\ZipCreated',
247 | function ($event) {
248 | \Log::info('ZipCreated:', [
249 | $event->disk(),
250 | $event->path(),
251 | $event->name(),
252 | $event->elements(),
253 | ]);
254 | }
255 | );
256 | ```
257 |
258 | ### ZipFailed
259 |
260 | > Alexusmai\LaravelFileManager\Events\ZipCreated
261 |
262 | ```php
263 | \Event::listen('Alexusmai\LaravelFileManager\Events\ZipFailed',
264 | function ($event) {
265 | \Log::info('ZipFailed:', [
266 | $event->disk(),
267 | $event->path(),
268 | $event->name(),
269 | $event->elements(),
270 | ]);
271 | }
272 | );
273 | ```
274 |
275 | ### Unzip
276 |
277 | > Alexusmai\LaravelFileManager\Events\Unzip
278 |
279 | ```php
280 | \Event::listen('Alexusmai\LaravelFileManager\Events\Unzip',
281 | function ($event) {
282 | \Log::info('Unzip:', [
283 | $event->disk(),
284 | $event->path(),
285 | $event->folder(),
286 | ]);
287 | }
288 | );
289 | ```
290 |
291 | ### UnzipCreated
292 |
293 | > Alexusmai\LaravelFileManager\Events\UnzipCreated
294 |
295 | ```php
296 | \Event::listen('Alexusmai\LaravelFileManager\Events\UnzipCreated',
297 | function ($event) {
298 | \Log::info('UnzipCreated:', [
299 | $event->disk(),
300 | $event->path(),
301 | $event->folder(),
302 | ]);
303 | }
304 | );
305 | ```
306 |
307 | ### UnzipFailed
308 |
309 | > Alexusmai\LaravelFileManager\Events\UnzipFailed
310 |
311 | ```php
312 | \Event::listen('Alexusmai\LaravelFileManager\Events\UnzipFailed',
313 | function ($event) {
314 | \Log::info('UnzipFailed:', [
315 | $event->disk(),
316 | $event->path(),
317 | $event->folder(),
318 | ]);
319 | }
320 | );
321 | ```
322 |
--------------------------------------------------------------------------------
/src/Traits/ContentTrait.php:
--------------------------------------------------------------------------------
1 | listContents($path ?: '')->toArray();
27 |
28 | $directories = $this->filterDir($disk, $content);
29 | $files = $this->filterFile($disk, $content);
30 |
31 | return compact('directories', 'files');
32 | }
33 |
34 | /**
35 | * Get directories with properties
36 | *
37 | * @param $disk
38 | * @param null $path
39 | *
40 | * @return array
41 | * @throws FilesystemException
42 | */
43 | public function directoriesWithProperties($disk, $path = null): array
44 | {
45 | $content = Storage::disk($disk)->listContents($path ?: '')->toArray();
46 |
47 | return $this->filterDir($disk, $content);
48 | }
49 |
50 | /**
51 | * Get files with properties
52 | *
53 | * @param $disk
54 | * @param null $path
55 | *
56 | * @return array
57 | * @throws FilesystemException
58 | */
59 | public function filesWithProperties($disk, $path = null): array
60 | {
61 | $content = Storage::disk($disk)->listContents($path ?: '');
62 |
63 | return $this->filterFile($disk, $content);
64 | }
65 |
66 | /**
67 | * Get directories for tree module
68 | *
69 | * @param $disk
70 | * @param null $path
71 | *
72 | * @return array
73 | * @throws FilesystemException
74 | */
75 | public function getDirectoriesTree($disk, $path = null): array
76 | {
77 | $directories = $this->directoriesWithProperties($disk, $path);
78 |
79 | foreach ($directories as $index => $dir) {
80 | $directories[$index]['props'] = [
81 | 'hasSubdirectories' => (bool) Storage::disk($disk)->directories($dir['path']),
82 | ];
83 | }
84 |
85 | return $directories;
86 | }
87 |
88 | /**
89 | * File properties
90 | *
91 | * @param $disk
92 | * @param $path
93 | *
94 | * @return mixed
95 | */
96 | public function fileProperties($disk, $path = null): mixed
97 | {
98 | $pathInfo = pathinfo($path);
99 |
100 | $properties = [
101 | 'type' => 'file',
102 | 'path' => $path,
103 | 'basename' => $pathInfo['basename'],
104 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'],
105 | 'extension' => $pathInfo['extension'] ?? '',
106 | 'filename' => $pathInfo['filename'],
107 | 'size' => Storage::disk($disk)->size($path),
108 | 'timestamp' => Storage::disk($disk)->lastModified($path),
109 | 'visibility' => Storage::disk($disk)->getVisibility($path),
110 | ];
111 |
112 | // if ACL ON
113 | if ($this->configRepository->getAcl()) {
114 | return $this->aclFilter($disk, [$properties])[0];
115 | }
116 |
117 | return $properties;
118 | }
119 |
120 | /**
121 | * Get properties for the selected directory
122 | *
123 | * @param $disk
124 | * @param null $path
125 | *
126 | * @return array|false
127 | */
128 | public function directoryProperties($disk, $path = null): bool|array
129 | {
130 | $adapter = Storage::drive($disk)->getAdapter();
131 |
132 | $pathInfo = pathinfo($path);
133 |
134 | if (
135 | $adapter instanceof AwsS3V3Adapter ||
136 | $adapter instanceof FtpAdapter ||
137 | $adapter instanceof SftpAdapter
138 | ){
139 | $timestamp = null;
140 | $visibility = null;
141 | } else {
142 | $timestamp = Storage::disk($disk)->lastModified($path);
143 | $visibility = Storage::disk($disk)->getVisibility($path);
144 | }
145 |
146 | $properties = [
147 | 'type' => 'dir',
148 | 'path' => $path,
149 | 'basename' => $pathInfo['basename'],
150 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'],
151 | 'timestamp' => $timestamp,
152 | 'visibility' => $visibility,
153 | ];
154 |
155 | // if ACL ON
156 | if ($this->configRepository->getAcl()) {
157 | return $this->aclFilter($disk, [$properties])[0];
158 | }
159 |
160 | return $properties;
161 | }
162 |
163 | /**
164 | * Get only directories
165 | *
166 | * @param $disk
167 | * @param $content
168 | *
169 | * @return array
170 | */
171 | protected function filterDir($disk, $content): array
172 | {
173 | // select only dir
174 | $dirsList = array_filter($content, fn($item) => $item['type'] === 'dir');
175 |
176 | $dirs = array_map(function ($item) {
177 | $pathInfo = pathinfo($item['path']);
178 |
179 | return [
180 | 'type' => $item['type'],
181 | 'path' => $item['path'],
182 | 'basename' => $pathInfo['basename'],
183 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'],
184 | 'timestamp' => $item['lastModified'],
185 | 'visibility' => $item['visibility'],
186 | ];
187 | }, $dirsList);
188 |
189 | // if ACL ON
190 | if ($this->configRepository->getAcl()) {
191 | return array_values($this->aclFilter($disk, $dirs));
192 | }
193 |
194 | return array_values($dirs);
195 | }
196 |
197 | /**
198 | * Get only files
199 | *
200 | * @param $disk
201 | * @param $content
202 | *
203 | * @return array
204 | */
205 | protected function filterFile($disk, $content): array
206 | {
207 | // select only dir
208 | $filesList = array_filter($content, fn($item) => $item['type'] === 'file');
209 |
210 | $files = array_map(function ($item) {
211 | $pathInfo = pathinfo($item['path']);
212 |
213 | return [
214 | 'type' => $item['type'],
215 | 'path' => $item['path'],
216 | 'basename' => $pathInfo['basename'],
217 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'],
218 | 'extension' => $pathInfo['extension'] ?? '',
219 | 'filename' => $pathInfo['filename'],
220 | 'size' => $item['fileSize'],
221 | 'timestamp' => $item['lastModified'],
222 | 'visibility' => $item['visibility'],
223 | ];
224 | }, $filesList);
225 |
226 | // if ACL ON
227 | if ($this->configRepository->getAcl()) {
228 | return array_values($this->aclFilter($disk, $files));
229 | }
230 |
231 | return array_values($files);
232 | }
233 |
234 | /**
235 | * ACL filter
236 | *
237 | * @param $disk
238 | * @param $content
239 | *
240 | * @return mixed
241 | */
242 | protected function aclFilter($disk, $content): mixed
243 | {
244 | $acl = resolve(ACL::class);
245 |
246 | $withAccess = array_map(function ($item) use ($acl, $disk) {
247 | // add acl access level
248 | $item['acl'] = $acl->getAccessLevel($disk, $item['path']);
249 |
250 | return $item;
251 | }, $content);
252 |
253 | // filter files and folders
254 | if ($this->configRepository->getAclHideFromFM()) {
255 | return array_filter($withAccess, function ($item) {
256 | return $item['acl'] !== 0;
257 | });
258 | }
259 |
260 | return $withAccess;
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/src/Middleware/FileManagerACL.php:
--------------------------------------------------------------------------------
1 | 'checkContent',
21 | 'fm.content' => 'checkContent',
22 | 'fm.preview' => 'checkContent',
23 | 'fm.thumbnails' => 'checkContent',
24 | 'fm.url' => 'checkContent',
25 | 'fm.stream-file' => 'checkContent',
26 | 'fm.download' => 'checkDownload',
27 | 'fm.create-file' => 'checkCreate',
28 | 'fm.create-directory' => 'checkCreate',
29 | 'fm.update-file' => 'checkUpdate',
30 | 'fm.upload' => 'checkUpload',
31 | 'fm.delete' => 'checkDelete',
32 | 'fm.paste' => 'checkPaste',
33 | 'fm.rename' => 'checkRename',
34 | 'fm.zip' => 'checkZip',
35 | 'fm.unzip' => 'checkUnzip',
36 | ];
37 |
38 | /**
39 | * @var string|null
40 | */
41 | protected $disk;
42 |
43 | /**
44 | * @var string|null
45 | */
46 | protected $path;
47 |
48 | /**
49 | * @var ACL|mixed
50 | */
51 | protected $acl;
52 |
53 | /**
54 | * @var Request
55 | */
56 | protected $request;
57 |
58 | /**
59 | * FileManagerACL constructor.
60 | *
61 | * @param Request $request
62 | * @param ACL $acl
63 | */
64 | public function __construct(Request $request, ACL $acl)
65 | {
66 | $this->disk = $request->has('disk') ? $request->input('disk') : null;
67 | $this->path = $request->has('path') ? $request->input('path') : '/';
68 |
69 | $this->acl = $acl;
70 |
71 | $this->request = $request;
72 | }
73 |
74 | /**
75 | * Handle an incoming request.
76 | *
77 | * @param \Illuminate\Http\Request $request
78 | * @param \Closure $next
79 | *
80 | * @return mixed
81 | */
82 | public function handle($request, Closure $next)
83 | {
84 | $routeName = $request->route()->getName();
85 |
86 | // if ACL is OFF or route name wasn't found
87 | if ( ! resolve(ConfigRepository::class)->getAcl()
88 | || ! array_key_exists($routeName, self::CHECKERS)
89 | ) {
90 | return $next($request);
91 | }
92 |
93 | if ( ! call_user_func([$this, self::CHECKERS[$routeName]])) {
94 | return $this->errorMessage();
95 | }
96 |
97 | // return request
98 | return $next($request);
99 | }
100 |
101 | /**
102 | * ACL Error message
103 | *
104 | * @return \Illuminate\Http\JsonResponse
105 | */
106 | protected function errorMessage()
107 | {
108 | return response()->json([
109 | 'result' => [
110 | 'status' => 'error',
111 | 'message' => 'aclError',
112 | ],
113 | ]);
114 | }
115 |
116 | /**
117 | * Check content actions
118 | *
119 | * @return bool
120 | */
121 | protected function checkContent()
122 | {
123 | // need r access
124 | return ! ($this->acl->getAccessLevel($this->disk, $this->path) === 0);
125 | }
126 |
127 | /**
128 | * Check download actions
129 | */
130 | protected function checkDownload()
131 | {
132 | // need r access
133 | abort_if(
134 | $this->acl->getAccessLevel($this->disk, $this->path) === 0,
135 | 403
136 | );
137 |
138 | return true;
139 | }
140 |
141 | /**
142 | * Check create actions
143 | *
144 | * @return bool
145 | */
146 | protected function checkCreate()
147 | {
148 | $name = $this->request->input('name');
149 | $pathToWrite = $this->request->input('path')
150 | ? $this->request->input('path').'/' : '';
151 |
152 | // need r/w access
153 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2);
154 | }
155 |
156 | /**
157 | * Check update actions
158 | *
159 | * @return bool
160 | */
161 | protected function checkUpdate()
162 | {
163 | $pathToWrite = $this->request->input('path')
164 | ? $this->request->input('path').'/' : '';
165 |
166 | $name = $this->request->file('file')->getClientOriginalName();
167 |
168 | // need r/w access
169 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2);
170 | }
171 |
172 | /**
173 | * Check upload actions
174 | *
175 | * @return bool
176 | */
177 | protected function checkUpload()
178 | {
179 | $pathToWrite = $this->request->input('path')
180 | ? $this->request->input('path').'/' : '';
181 |
182 | // filter
183 | $firstFall = Arr::first($this->request->file('files'),
184 | function ($value) use ($pathToWrite) {
185 | // need r/w access
186 | return $this->acl->getAccessLevel(
187 | $this->disk,
188 | $pathToWrite.$value->getClientOriginalName()
189 | ) !== 2;
190 | }, null);
191 |
192 | // if founded one access error
193 | if ($firstFall) {
194 | return false;
195 | }
196 |
197 | return true;
198 | }
199 |
200 | /**
201 | * Check delete actions
202 | *
203 | * @return bool
204 | */
205 | protected function checkDelete()
206 | {
207 | $firstFall = Arr::first($this->request->input('items'),
208 | function ($value) {
209 | // need r/w access
210 | return $this->acl->getAccessLevel($this->disk, $value['path']) !== 2;
211 | }, null);
212 |
213 | if ($firstFall) {
214 | return false;
215 | }
216 |
217 | return true;
218 | }
219 |
220 | /**
221 | * Check paste action
222 | *
223 | * @return bool
224 | */
225 | protected function checkPaste()
226 | {
227 | // get clipboard data
228 | $clipboard = $this->request->input('clipboard');
229 |
230 | // copy - r, cut - rw
231 | $getLevel = $clipboard['type'] === 'copy' ? 1 : 2;
232 |
233 | // can user copy or cut selected files and folders
234 | $checkDirs = Arr::first($clipboard['directories'],
235 | function ($value) use ($clipboard, $getLevel) {
236 | return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel;
237 | }, null);
238 |
239 | $checkFiles = Arr::first($clipboard['files'],
240 | function ($value) use ($clipboard, $getLevel) {
241 | return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel;
242 | }, null);
243 |
244 | // can user write to selected folder?
245 | $writeToFolder = $this->acl->getAccessLevel($this->disk, $this->path);
246 |
247 | return ! ($checkDirs || $checkFiles || $writeToFolder !== 2);
248 | }
249 |
250 | /**
251 | * Check rename actions
252 | *
253 | * @return bool
254 | */
255 | protected function checkRename()
256 | {
257 | // old path
258 | $oldPath = $this->request->input('oldName');
259 |
260 | // new path
261 | $newPath = $this->request->input('newName');
262 |
263 | // need r/w access
264 | return ! ($this->acl->getAccessLevel($this->disk, $oldPath) !== 2
265 | || $this->acl->getAccessLevel($this->disk, $newPath) !== 2);
266 | }
267 |
268 | /**
269 | * Check zip actions
270 | *
271 | * @return bool
272 | */
273 | protected function checkZip()
274 | {
275 | // can user write to selected folder?
276 | $writeToFolder = $this->acl->getAccessLevel(
277 | $this->disk,
278 | $this->newPath(
279 | $this->request->input('path'),
280 | $this->request->input('name')
281 | )
282 | );
283 |
284 | // need r/w access
285 | if ($writeToFolder !== 2) {
286 | return false;
287 | }
288 |
289 | // data to zip
290 | $elements = $this->request->input('elements');
291 |
292 | // can user read selected files and folders?
293 | $checkDirs = Arr::first($elements['directories'],
294 | function ($value) {
295 | // need r access
296 | return $this->acl->getAccessLevel($this->disk, $value) === 0;
297 | }, null);
298 |
299 |
300 | $checkFiles = Arr::first($elements['files'],
301 | function ($value) {
302 | // need r access
303 | return $this->acl->getAccessLevel($this->disk, $value) === 0;
304 | }, null);
305 |
306 | return ! ($checkDirs || $checkFiles);
307 | }
308 |
309 | /**
310 | * Check unzip actions
311 | *
312 | * @return bool
313 | */
314 | protected function checkUnzip()
315 | {
316 | if ($this->request->input('folder')) {
317 | $dirname = dirname($this->path) === '.' ? ''
318 | : dirname($this->path).'/';
319 | $pathToWrite = $dirname.$this->request->input('folder');
320 | } else {
321 | $pathToWrite = dirname($this->path) === '.' ? '/'
322 | : dirname($this->path);
323 | }
324 |
325 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite) !== 2
326 | || $this->acl->getAccessLevel($this->disk, $this->path) === 0);
327 | }
328 | }
329 |
--------------------------------------------------------------------------------
/src/Controllers/FileManagerController.php:
--------------------------------------------------------------------------------
1 | fm = $fm;
48 | }
49 |
50 | /**
51 | * Initialize file manager
52 | *
53 | * @return JsonResponse
54 | */
55 | public function initialize(): JsonResponse
56 | {
57 | event(new BeforeInitialization());
58 |
59 | return response()->json(
60 | $this->fm->initialize()
61 | );
62 | }
63 |
64 | /**
65 | * Get files and directories for the selected path and disk
66 | *
67 | * @param RequestValidator $request
68 | *
69 | * @return JsonResponse
70 | * @throws FilesystemException
71 | */
72 | public function content(RequestValidator $request): JsonResponse
73 | {
74 | return response()->json(
75 | $this->fm->content(
76 | $request->input('disk'),
77 | $request->input('path')
78 | )
79 | );
80 | }
81 |
82 | /**
83 | * Directory tree
84 | *
85 | * @param RequestValidator $request
86 | *
87 | * @return JsonResponse
88 | * @throws FilesystemException
89 | */
90 | public function tree(RequestValidator $request): JsonResponse
91 | {
92 | return response()->json(
93 | $this->fm->tree(
94 | $request->input('disk'),
95 | $request->input('path')
96 | )
97 | );
98 | }
99 |
100 | /**
101 | * Check the selected disk
102 | *
103 | * @param RequestValidator $request
104 | *
105 | * @return JsonResponse
106 | */
107 | public function selectDisk(RequestValidator $request): JsonResponse
108 | {
109 | event(new DiskSelected($request->input('disk')));
110 |
111 | return response()->json([
112 | 'result' => [
113 | 'status' => 'success',
114 | 'message' => 'diskSelected',
115 | ],
116 | ]);
117 | }
118 |
119 | /**
120 | * Upload files
121 | *
122 | * @param RequestValidator $request
123 | *
124 | * @return JsonResponse
125 | */
126 | public function upload(RequestValidator $request): JsonResponse
127 | {
128 | event(new FilesUploading($request));
129 |
130 | $uploadResponse = $this->fm->upload(
131 | $request->input('disk'),
132 | $request->input('path'),
133 | $request->file('files'),
134 | $request->input('overwrite')
135 | );
136 | $status = $uploadResponse['result']['status'];
137 |
138 | if ($status === "success")
139 | event(new FilesUploaded($request));
140 | else
141 | event(new FilesUploadFailed($request,$uploadResponse['result']['message']));
142 |
143 |
144 | return response()->json($uploadResponse);
145 | }
146 |
147 | /**
148 | * Delete files and folders
149 | *
150 | * @param RequestValidator $request
151 | *
152 | * @return JsonResponse
153 | */
154 | public function delete(RequestValidator $request): JsonResponse
155 | {
156 | event(new Deleting($request));
157 |
158 | $deleteResponse = $this->fm->delete(
159 | $request->input('disk'),
160 | $request->input('items')
161 | );
162 |
163 | return response()->json($deleteResponse);
164 | }
165 |
166 | /**
167 | * Copy / Cut files and folders
168 | *
169 | * @param RequestValidator $request
170 | *
171 | * @return JsonResponse
172 | */
173 | public function paste(RequestValidator $request): JsonResponse
174 | {
175 | event(new Paste($request));
176 |
177 | return response()->json(
178 | $this->fm->paste(
179 | $request->input('disk'),
180 | $request->input('path'),
181 | $request->input('clipboard')
182 | )
183 | );
184 | }
185 |
186 | /**
187 | * Rename
188 | *
189 | * @param RequestValidator $request
190 | *
191 | * @return JsonResponse
192 | */
193 | public function rename(RequestValidator $request): JsonResponse
194 | {
195 | event(new Rename($request));
196 |
197 | return response()->json(
198 | $this->fm->rename(
199 | $request->input('disk'),
200 | $request->input('newName'),
201 | $request->input('oldName')
202 | )
203 | );
204 | }
205 |
206 | /**
207 | * Download file
208 | *
209 | * @param RequestValidator $request
210 | *
211 | * @return StreamedResponse
212 | */
213 | public function download(RequestValidator $request): StreamedResponse
214 | {
215 | event(new Download($request));
216 |
217 | return $this->fm->download(
218 | $request->input('disk'),
219 | $request->input('path')
220 | );
221 | }
222 |
223 | /**
224 | * Create thumbnails
225 | *
226 | * @param RequestValidator $request
227 | *
228 | * @return Response|mixed
229 | * @throws BindingResolutionException
230 | */
231 | public function thumbnails(RequestValidator $request): mixed
232 | {
233 | return $this->fm->thumbnails(
234 | $request->input('disk'),
235 | $request->input('path')
236 | );
237 | }
238 |
239 | /**
240 | * Image preview
241 | *
242 | * @param RequestValidator $request
243 | *
244 | * @return mixed
245 | */
246 | public function preview(RequestValidator $request): mixed
247 | {
248 | return $this->fm->preview(
249 | $request->input('disk'),
250 | $request->input('path')
251 | );
252 | }
253 |
254 | /**
255 | * File url
256 | *
257 | * @param RequestValidator $request
258 | *
259 | * @return JsonResponse
260 | */
261 | public function url(RequestValidator $request): JsonResponse
262 | {
263 | return response()->json(
264 | $this->fm->url(
265 | $request->input('disk'),
266 | $request->input('path')
267 | )
268 | );
269 | }
270 |
271 | /**
272 | * Create new directory
273 | *
274 | * @param RequestValidator $request
275 | *
276 | * @return JsonResponse
277 | */
278 | public function createDirectory(RequestValidator $request): JsonResponse
279 | {
280 | event(new DirectoryCreating($request));
281 |
282 | $createDirectoryResponse = $this->fm->createDirectory(
283 | $request->input('disk'),
284 | $request->input('path'),
285 | $request->input('name')
286 | );
287 |
288 | if ($createDirectoryResponse['result']['status'] === 'success') {
289 | event(new DirectoryCreated($request));
290 | }
291 |
292 | return response()->json($createDirectoryResponse);
293 | }
294 |
295 | /**
296 | * Create new file
297 | *
298 | * @param RequestValidator $request
299 | *
300 | * @return JsonResponse
301 | */
302 | public function createFile(RequestValidator $request): JsonResponse
303 | {
304 | event(new FileCreating($request));
305 |
306 | $createFileResponse = $this->fm->createFile(
307 | $request->input('disk'),
308 | $request->input('path'),
309 | $request->input('name')
310 | );
311 |
312 | if ($createFileResponse['result']['status'] === 'success') {
313 | event(new FileCreated($request));
314 | }
315 |
316 | return response()->json($createFileResponse);
317 | }
318 |
319 | /**
320 | * Update file
321 | *
322 | * @param RequestValidator $request
323 | *
324 | * @return JsonResponse
325 | */
326 | public function updateFile(RequestValidator $request): JsonResponse
327 | {
328 | event(new FileUpdate($request));
329 |
330 | return response()->json(
331 | $this->fm->updateFile(
332 | $request->input('disk'),
333 | $request->input('path'),
334 | $request->file('file')
335 | )
336 | );
337 | }
338 |
339 | /**
340 | * Stream file
341 | *
342 | * @param RequestValidator $request
343 | *
344 | * @return mixed
345 | */
346 | public function streamFile(RequestValidator $request): mixed
347 | {
348 | return $this->fm->streamFile(
349 | $request->input('disk'),
350 | $request->input('path')
351 | );
352 | }
353 |
354 | /**
355 | * Create zip archive
356 | *
357 | * @param RequestValidator $request
358 | * @param Zip $zip
359 | *
360 | * @return array
361 | */
362 | public function zip(RequestValidator $request, Zip $zip)
363 | {
364 | event(new ZipEvent($request));
365 |
366 | return $zip->create();
367 | }
368 |
369 | /**
370 | * Extract zip archive
371 | *
372 | * @param RequestValidator $request
373 | * @param Zip $zip
374 | *
375 | * @return array
376 | */
377 | public function unzip(RequestValidator $request, Zip $zip)
378 | {
379 | event(new UnzipEvent($request));
380 |
381 | return $zip->extract();
382 | }
383 |
384 | /**
385 | * Integration with ckeditor 4
386 | *
387 | * @return Factory|View
388 | */
389 | public function ckeditor(): Factory|View
390 | {
391 | return view('file-manager::ckeditor');
392 | }
393 |
394 | /**
395 | * Integration with TinyMCE v4
396 | *
397 | * @return Factory|View
398 | */
399 | public function tinymce(): Factory|View
400 | {
401 | return view('file-manager::tinymce');
402 | }
403 |
404 | /**
405 | * Integration with TinyMCE v5
406 | *
407 | * @return Factory|View
408 | */
409 | public function tinymce5(): Factory|View
410 | {
411 | return view('file-manager::tinymce5');
412 | }
413 |
414 | /**
415 | * Integration with SummerNote
416 | *
417 | * @return Factory|View
418 | */
419 | public function summernote(): Factory|View
420 | {
421 | return view('file-manager::summernote');
422 | }
423 |
424 | /**
425 | * Simple integration with input field
426 | *
427 | * @return Factory|View
428 | */
429 | public function fmButton(): Factory|View
430 | {
431 | return view('file-manager::fmButton');
432 | }
433 | }
434 |
--------------------------------------------------------------------------------
/src/FileManager.php:
--------------------------------------------------------------------------------
1 | configRepository = $configRepository;
37 | }
38 |
39 | /**
40 | * Initialize App
41 | *
42 | * @return array
43 | */
44 | public function initialize(): array
45 | {
46 | if (!config()->has('file-manager')) {
47 | return [
48 | 'result' => [
49 | 'status' => 'danger',
50 | 'message' => 'noConfig',
51 | ],
52 | ];
53 | }
54 |
55 | $config = [
56 | 'acl' => $this->configRepository->getAcl(),
57 | 'leftDisk' => $this->configRepository->getLeftDisk(),
58 | 'rightDisk' => $this->configRepository->getRightDisk(),
59 | 'leftPath' => $this->configRepository->getLeftPath(),
60 | 'rightPath' => $this->configRepository->getRightPath(),
61 | 'windowsConfig' => $this->configRepository->getWindowsConfig(),
62 | 'hiddenFiles' => $this->configRepository->getHiddenFiles(),
63 | ];
64 |
65 | // disk list
66 | foreach ($this->configRepository->getDiskList() as $disk) {
67 | if (array_key_exists($disk, config('filesystems.disks'))) {
68 | $config['disks'][$disk] = Arr::only(
69 | config('filesystems.disks')[$disk], ['driver']
70 | );
71 | }
72 | }
73 |
74 | // get language
75 | $config['lang'] = app()->getLocale();
76 |
77 | return [
78 | 'result' => [
79 | 'status' => 'success',
80 | 'message' => null,
81 | ],
82 | 'config' => $config,
83 | ];
84 | }
85 |
86 | /**
87 | * Get files and directories for the selected path and disk
88 | *
89 | * @param $disk
90 | * @param $path
91 | *
92 | * @return array
93 | * @throws FilesystemException
94 | */
95 | public function content($disk, $path): array
96 | {
97 | $content = $this->getContent($disk, $path);
98 |
99 | return [
100 | 'result' => [
101 | 'status' => 'success',
102 | 'message' => null,
103 | ],
104 | 'directories' => $content['directories'],
105 | 'files' => $content['files'],
106 | ];
107 | }
108 |
109 | /**
110 | * Get part of the directory tree
111 | *
112 | * @param $disk
113 | * @param $path
114 | *
115 | * @return array
116 | * @throws FilesystemException
117 | */
118 | public function tree($disk, $path): array
119 | {
120 | $directories = $this->getDirectoriesTree($disk, $path);
121 |
122 | return [
123 | 'result' => [
124 | 'status' => 'success',
125 | 'message' => null,
126 | ],
127 | 'directories' => $directories,
128 | ];
129 | }
130 |
131 | /**
132 | * Upload files
133 | *
134 | * @param string|null $disk
135 | * @param string|null $path
136 | * @param array|null $files
137 | * @param bool $overwrite
138 | *
139 | * @return array
140 | */
141 | public function upload($disk, $path, $files, $overwrite): array
142 | {
143 | $fileNotUploaded = false;
144 |
145 | foreach ($files as $file) {
146 | // skip or overwrite files
147 | if (!$overwrite && Storage::disk($disk)->exists($path . '/' . $file->getClientOriginalName())) {
148 | continue;
149 | }
150 |
151 | // check file size
152 | if ($this->configRepository->getMaxUploadFileSize()
153 | && $file->getSize() / 1024 > $this->configRepository->getMaxUploadFileSize()
154 | ) {
155 | $fileNotUploaded = true;
156 | continue;
157 | }
158 |
159 | // check file type
160 | if ($this->configRepository->getAllowFileTypes()
161 | && !in_array(
162 | $file->getClientOriginalExtension(),
163 | $this->configRepository->getAllowFileTypes()
164 | )
165 | ) {
166 | $fileNotUploaded = true;
167 | continue;
168 | }
169 |
170 | $name = $file->getClientOriginalName();
171 | if ($this->configRepository->getSlugifyNames()) {
172 | $name = Str::slug(
173 | Str::replace(
174 | '.' . $file->getClientOriginalExtension(),
175 | '',
176 | $name
177 | )
178 | ) . '.' . $file->getClientOriginalExtension();
179 | }
180 | // overwrite or save file
181 | Storage::disk($disk)->putFileAs(
182 | $path,
183 | $file,
184 | $name
185 | );
186 | }
187 |
188 | if ($fileNotUploaded) {
189 | return [
190 | 'result' => [
191 | 'status' => 'warning',
192 | 'message' => 'notAllUploaded',
193 | ],
194 | ];
195 | }
196 |
197 | return [
198 | 'result' => [
199 | 'status' => 'success',
200 | 'message' => 'uploaded',
201 | ],
202 | ];
203 | }
204 |
205 | /**
206 | * Delete files and folders
207 | *
208 | * @param $disk
209 | * @param $items
210 | *
211 | * @return array
212 | */
213 | public function delete($disk, $items): array
214 | {
215 | $deletedItems = [];
216 |
217 | foreach ($items as $item) {
218 | if (!Storage::disk($disk)->exists($item['path'])) {
219 | continue;
220 | } else {
221 | if ($item['type'] === 'dir') {
222 | Storage::disk($disk)->deleteDirectory($item['path']);
223 | } else {
224 | Storage::disk($disk)->delete($item['path']);
225 | }
226 | }
227 |
228 | $deletedItems[] = $item;
229 | }
230 |
231 | event(new Deleted($disk, $deletedItems));
232 |
233 | return [
234 | 'result' => [
235 | 'status' => 'success',
236 | 'message' => 'deleted',
237 | ],
238 | ];
239 | }
240 |
241 | /**
242 | * Copy / Cut - Files and Directories
243 | *
244 | * @param $disk
245 | * @param $path
246 | * @param $clipboard
247 | *
248 | * @return array
249 | */
250 | public function paste($disk, $path, $clipboard): array
251 | {
252 | // compare disk names
253 | if ($disk !== $clipboard['disk']) {
254 |
255 | if (!$this->checkDisk($clipboard['disk'])) {
256 | return $this->notFoundMessage();
257 | }
258 | }
259 |
260 | $transferService = TransferFactory::build($disk, $path, $clipboard);
261 |
262 | return $transferService->filesTransfer();
263 | }
264 |
265 | /**
266 | * Rename file or folder
267 | *
268 | * @param $disk
269 | * @param $newName
270 | * @param $oldName
271 | *
272 | * @return array
273 | */
274 | public function rename($disk, $newName, $oldName): array
275 | {
276 | Storage::disk($disk)->move($oldName, $newName);
277 |
278 | return [
279 | 'result' => [
280 | 'status' => 'success',
281 | 'message' => 'renamed',
282 | ],
283 | ];
284 | }
285 |
286 | /**
287 | * Download selected file
288 | *
289 | * @param $disk
290 | * @param $path
291 | *
292 | * @return StreamedResponse
293 | */
294 | public function download($disk, $path): StreamedResponse
295 | {
296 | // if file name not in ASCII format
297 | if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) {
298 | $filename = Str::ascii(basename($path));
299 | } else {
300 | $filename = basename($path);
301 | }
302 |
303 | return Storage::disk($disk)->download($path, $filename);
304 | }
305 |
306 | /**
307 | * Create thumbnails
308 | *
309 | * @param $disk
310 | * @param $path
311 | *
312 | * @return Response|mixed
313 | * @throws BindingResolutionException
314 | */
315 | public function thumbnails($disk, $path): mixed
316 | {
317 | return response()->make(
318 | Image::read(
319 | Storage::disk($disk)->get($path))
320 | ->coverDown(80, 80)
321 | ->encode(),
322 | 200,
323 | ['Content-Type' => Storage::disk($disk)->mimeType($path)]
324 | );
325 | }
326 |
327 | /**
328 | * Image preview
329 | *
330 | * @param $disk
331 | * @param $path
332 | *
333 | * @return mixed
334 | * @throws BindingResolutionException
335 | */
336 | public function preview($disk, $path): mixed
337 | {
338 | return response()->make(
339 | Image::read(Storage::disk($disk)->get($path))->encode(),
340 | 200,
341 | ['Content-Type' => Storage::disk($disk)->mimeType($path)]
342 | );
343 | }
344 |
345 | /**
346 | * Get file URL
347 | *
348 | * @param $disk
349 | * @param $path
350 | *
351 | * @return array
352 | */
353 | public function url($disk, $path): array
354 | {
355 | return [
356 | 'result' => [
357 | 'status' => 'success',
358 | 'message' => null,
359 | ],
360 | 'url' => Storage::disk($disk)->url($path),
361 | ];
362 | }
363 |
364 | /**
365 | * Create new directory
366 | *
367 | * @param $disk
368 | * @param $path
369 | * @param $name
370 | *
371 | * @return array
372 | */
373 | public function createDirectory($disk, $path, $name)
374 | {
375 | $directoryName = $this->newPath($path, $name);
376 |
377 | if (Storage::disk($disk)->exists($directoryName)) {
378 | return [
379 | 'result' => [
380 | 'status' => 'warning',
381 | 'message' => 'dirExist',
382 | ],
383 | ];
384 | }
385 |
386 | Storage::disk($disk)->makeDirectory($directoryName);
387 | $directoryProperties = $this->directoryProperties(
388 | $disk,
389 | $directoryName
390 | );
391 |
392 | // add directory properties for the tree module
393 | $tree = $directoryProperties;
394 | $tree['props'] = ['hasSubdirectories' => false];
395 |
396 | return [
397 | 'result' => [
398 | 'status' => 'success',
399 | 'message' => 'dirCreated',
400 | ],
401 | 'directory' => $directoryProperties,
402 | 'tree' => [$tree],
403 | ];
404 | }
405 |
406 | /**
407 | * Create new file
408 | *
409 | * @param $disk
410 | * @param $path
411 | * @param $name
412 | *
413 | * @return array
414 | */
415 | public function createFile($disk, $path, $name): array
416 | {
417 | $path = $this->newPath($path, $name);
418 |
419 | if (Storage::disk($disk)->exists($path)) {
420 | return [
421 | 'result' => [
422 | 'status' => 'warning',
423 | 'message' => 'fileExist',
424 | ],
425 | ];
426 | }
427 |
428 | Storage::disk($disk)->put($path, '');
429 | $fileProperties = $this->fileProperties($disk, $path);
430 |
431 | return [
432 | 'result' => [
433 | 'status' => 'success',
434 | 'message' => 'fileCreated',
435 | ],
436 | 'file' => $fileProperties,
437 | ];
438 | }
439 |
440 | /**
441 | * Update file
442 | *
443 | * @param $disk
444 | * @param $path
445 | * @param $file
446 | *
447 | * @return array
448 | */
449 | public function updateFile($disk, $path, $file): array
450 | {
451 | Storage::disk($disk)->putFileAs(
452 | $path,
453 | $file,
454 | $file->getClientOriginalName()
455 | );
456 |
457 | $filePath = $this->newPath($path, $file->getClientOriginalName());
458 | $fileProperties = $this->fileProperties($disk, $filePath);
459 |
460 | return [
461 | 'result' => [
462 | 'status' => 'success',
463 | 'message' => 'fileUpdated',
464 | ],
465 | 'file' => $fileProperties,
466 | ];
467 | }
468 |
469 | /**
470 | * Stream file - for audio and video
471 | *
472 | * @param $disk
473 | * @param $path
474 | *
475 | * @return StreamedResponse
476 | */
477 | public function streamFile($disk, $path): StreamedResponse
478 | {
479 | // if file name not in ASCII format
480 | if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) {
481 | $filename = Str::ascii(basename($path));
482 | } else {
483 | $filename = basename($path);
484 | }
485 |
486 | return Storage::disk($disk)->response($path, $filename, ['Accept-Ranges' => 'bytes']);
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/resources/assets/css/file-manager.css:
--------------------------------------------------------------------------------
1 | .fm-navbar{flex:0 0 auto}.fm-navbar .col-auto>.btn-group:not(:last-child){margin-right:.4rem}.fm-tree-branch{display:table;width:100%;padding-left:1rem}.fm-tree-branch li>p{margin-bottom:0;padding:.4rem;white-space:nowrap;cursor:pointer}.fm-tree-branch li>p:hover,.fm-tree-branch li>p.selected{background-color:#f8f9fa}.fm-tree-branch .bi.bi-dash,.fm-tree-branch .bi.bi-dash-square,.fm-tree-branch .bi.bi-plus-square{font-size:.9rem;padding-right:.4rem}.fade-tree-enter-active,.fade-tree-leave-active{transition:all .3s ease}.fade-tree-enter,.fade-tree-leave-to{transform:translate(20px);opacity:0}.fm-tree{overflow:auto;border-right:1px solid #6c757d}.fm-tree>.fm-tree-branch{padding-left:0}.fm-tree .fm-tree-disk{padding:.2rem .3rem;background-color:#cff4fc}.fm-tree .fm-tree-disk>i{padding-right:.4rem}.fm-disk-list ul.list-inline{margin-bottom:.5rem}.fm-disk-list .badge.bg-light{cursor:pointer}.fm-breadcrumb .breadcrumb{flex-wrap:nowrap;padding:.2rem .3rem;margin-bottom:.5rem}.fm-breadcrumb .breadcrumb.active-manager{background-color:#cff4fc}.fm-breadcrumb .breadcrumb .breadcrumb-item:not(.active):hover{cursor:pointer;font-weight:400;color:#6c757d}.fm-table thead th{background:white;position:sticky;top:0;z-index:10;cursor:pointer;border-top:none}.fm-table thead th:hover{background-color:#f8f9fa}.fm-table thead th>i{padding-left:.5rem}.fm-table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fm-table tr:hover{background-color:#f8f9fa}.fm-table .w-10{width:10%}.fm-table .w-65{width:65%}.fm-table .fm-content-item{cursor:pointer;max-width:1px}.fm-table .text-hidden{color:#cdcdcd}.fm-thumbnail{display:flex;justify-content:center;align-items:center}.fm-thumbnail .img-thumbnail{width:88px;height:88px}.fm-thumbnail .fade-enter-active,.fm-thumbnail .fade-leave-active{transition:opacity .3s}.fm-thumbnail .fade-enter,.fm-thumbnail .fade-leave-to{opacity:0}.fm-grid{padding-top:1rem}.fm-grid .fm-grid-item{position:relative;width:125px;padding:.4rem;margin-bottom:1rem;margin-right:1rem;border-radius:5px}.fm-grid .fm-grid-item.active{background-color:#cff4fc;box-shadow:3px 2px 5px gray}.fm-grid .fm-grid-item:not(.active):hover{background-color:#f8f9fa;box-shadow:3px 2px 5px gray}.fm-grid .fm-grid-item .fm-item-icon{font-size:5rem;cursor:pointer}.fm-grid .fm-grid-item .fm-item-icon>i,.fm-grid .fm-grid-item .fm-item-icon>figure>i{color:#6c757d}.fm-grid .fm-grid-item .fm-item-info{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fm-content{padding-left:1rem}.fm-content .fm-content-body{overflow:auto}.fm-modal-upload .fm-btn-wrapper{position:relative;overflow:hidden;padding-bottom:6px;margin-bottom:.6rem}.fm-modal-upload .fm-btn-wrapper input[type=file]{font-size:100px;position:absolute;left:0;top:0;opacity:0;cursor:pointer}.fm-modal-upload .fm-upload-list .bi{padding-right:.5rem}.fm-modal-upload .fm-upload-list .form-check-inline{margin-right:0}.fm-modal-upload .fm-upload-info>.progress{margin-bottom:1rem}.fm-additions-file-list .bi,.fm-modal-clipboard .modal-body .far{padding-right:.5rem}.fm-modal-properties .modal-body .row{margin-bottom:.3rem;padding-top:.3rem;padding-bottom:.3rem}.fm-modal-properties .modal-body .row .bi-files{display:none;cursor:pointer}.fm-modal-properties .modal-body .row:hover{background-color:#f8f9fa}.fm-modal-properties .modal-body .row:hover .bi-files{display:block}.fm-modal-properties .modal-body .col-2{font-weight:700}.fm-modal-properties .modal-body .col-9{word-wrap:break-word}/*!
2 | * Cropper.js v1.6.1
3 | * https://fengyuanchen.github.io/cropperjs
4 | *
5 | * Copyright 2015-present Chen Fengyuan
6 | * Released under the MIT license
7 | *
8 | * Date: 2023-09-17T03:44:17.565Z
9 | */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-wrap-box,.cropper-canvas,.cropper-drag-box,.cropper-crop-box,.cropper-modal{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-wrap-box,.cropper-canvas{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:calc(100% / 3);left:0;top:calc(100% / 3);width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:calc(100% / 3);top:0;width:calc(100% / 3)}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:before,.cropper-center:after{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width: 768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width: 992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width: 1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}.fm-additions-cropper{overflow:hidden}.fm-additions-cropper button>i{color:#fff;font-weight:700}.fm-additions-cropper>.row{flex-wrap:nowrap}.fm-additions-cropper .cropper-block{overflow:hidden}.fm-additions-cropper .cropper-block img{max-width:100%}.fm-additions-cropper .col-sm-3{overflow:auto}.fm-additions-cropper .col-sm-3::-webkit-scrollbar{display:none}.fm-additions-cropper .cropper-preview{margin-bottom:1rem;overflow:hidden;height:200px}.fm-additions-cropper .cropper-preview img{max-width:100%}.fm-additions-cropper .cropper-data{padding-left:1rem;padding-right:1rem}.fm-additions-cropper .cropper-data>.input-group{margin-bottom:.5rem}.fm-additions-cropper .cropper-data .input-group>.input-group-text:first-child{min-width:4rem}.fm-additions-cropper .cropper-data .input-group>.input-group-text:last-child{min-width:3rem}.fm-additions-cropper>.d-flex{padding:1rem;border-top:1px solid #e9ecef}.fm-modal-preview .modal-body{padding:0}.fm-modal-preview .modal-body img{max-width:100%}.fm-modal-preview>.d-flex{padding:1rem;border-top:1px solid #e9ecef}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:transparent}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:transparent}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:white}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:none;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none;outline:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors,.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:#ff06}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}.CodeMirror-merge{position:relative;border:1px solid #ddd;white-space:pre}.CodeMirror-merge,.CodeMirror-merge .CodeMirror{height:350px}.CodeMirror-merge-2pane .CodeMirror-merge-pane{width:47%}.CodeMirror-merge-2pane .CodeMirror-merge-gap{width:6%}.CodeMirror-merge-3pane .CodeMirror-merge-pane{width:31%}.CodeMirror-merge-3pane .CodeMirror-merge-gap{width:3.5%}.CodeMirror-merge-pane{display:inline-block;white-space:normal;vertical-align:top}.CodeMirror-merge-pane-rightmost{position:absolute;right:0px;z-index:1}.CodeMirror-merge-gap{z-index:2;display:inline-block;height:100%;-moz-box-sizing:border-box;box-sizing:border-box;overflow:hidden;border-left:1px solid #ddd;border-right:1px solid #ddd;position:relative;background:#f8f8f8}.CodeMirror-merge-scrolllock-wrap{position:absolute;bottom:0;left:50%}.CodeMirror-merge-scrolllock{position:relative;left:-50%;cursor:pointer;color:#555;line-height:1}.CodeMirror-merge-scrolllock:after{content:"\21db\a0\a0\21da"}.CodeMirror-merge-scrolllock.CodeMirror-merge-scrolllock-enabled:after{content:"\21db\21da"}.CodeMirror-merge-copybuttons-left,.CodeMirror-merge-copybuttons-right{position:absolute;left:0;top:0;right:0;bottom:0;line-height:1}.CodeMirror-merge-copy{position:absolute;cursor:pointer;color:#44c;z-index:3}.CodeMirror-merge-copy-reverse{position:absolute;cursor:pointer;color:#44c}.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy{left:2px}.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy{right:2px}.CodeMirror-merge-r-inserted,.CodeMirror-merge-l-inserted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-r-deleted,.CodeMirror-merge-l-deleted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-r-chunk{background:#ffffe0}.CodeMirror-merge-r-chunk-start{border-top:1px solid #ee8}.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #ee8}.CodeMirror-merge-r-connect{fill:#ffffe0;stroke:#ee8;stroke-width:1px}.CodeMirror-merge-l-chunk{background:#eef}.CodeMirror-merge-l-chunk-start{border-top:1px solid #88e}.CodeMirror-merge-l-chunk-end{border-bottom:1px solid #88e}.CodeMirror-merge-l-connect{fill:#eef;stroke:#88e;stroke-width:1px}.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk{background:#dfd}.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start{border-top:1px solid #4e4}.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #4e4}.CodeMirror-merge-collapsed-widget:before{content:"(...)"}.CodeMirror-merge-collapsed-widget{cursor:pointer;color:#88b;background:#eef;border:1px solid #ddf;font-size:90%;padding:0 3px;border-radius:4px}.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt{display:none}.cm-s-blackboard.CodeMirror{background:#0C1021;color:#f8f8f8}.cm-s-blackboard div.CodeMirror-selected{background:#253B76}.cm-s-blackboard .CodeMirror-line::selection,.cm-s-blackboard .CodeMirror-line>span::selection,.cm-s-blackboard .CodeMirror-line>span>span::selection{background:rgba(37,59,118,.99)}.cm-s-blackboard .CodeMirror-line::-moz-selection,.cm-s-blackboard .CodeMirror-line>span::-moz-selection,.cm-s-blackboard .CodeMirror-line>span>span::-moz-selection{background:rgba(37,59,118,.99)}.cm-s-blackboard .CodeMirror-gutters{background:#0C1021;border-right:0}.cm-s-blackboard .CodeMirror-guttermarker{color:#fbde2d}.cm-s-blackboard .CodeMirror-guttermarker-subtle,.cm-s-blackboard .CodeMirror-linenumber{color:#888}.cm-s-blackboard .CodeMirror-cursor{border-left:1px solid #A7A7A7}.cm-s-blackboard .cm-keyword{color:#fbde2d}.cm-s-blackboard .cm-atom,.cm-s-blackboard .cm-number{color:#d8fa3c}.cm-s-blackboard .cm-def{color:#8da6ce}.cm-s-blackboard .cm-variable{color:#ff6400}.cm-s-blackboard .cm-operator{color:#fbde2d}.cm-s-blackboard .cm-comment{color:#aeaeae}.cm-s-blackboard .cm-string,.cm-s-blackboard .cm-string-2{color:#61ce3c}.cm-s-blackboard .cm-meta{color:#d8fa3c}.cm-s-blackboard .cm-builtin,.cm-s-blackboard .cm-tag,.cm-s-blackboard .cm-attribute{color:#8da6ce}.cm-s-blackboard .cm-header{color:#ff6400}.cm-s-blackboard .cm-hr{color:#aeaeae}.cm-s-blackboard .cm-link{color:#8da6ce}.cm-s-blackboard .cm-error{background:#9D1E15;color:#f8f8f8}.cm-s-blackboard .CodeMirror-activeline-background{background:#3C3636}.cm-s-blackboard .CodeMirror-matchingbracket{outline:1px solid grey;color:#fff!important}.fm-modal-text-edit .modal-body{padding:0}.fm-modal-audio-player .bi.bi-play-fill{color:gray;opacity:.1;cursor:pointer}.fm-modal-audio-player .bi.bi-play-fill:hover{opacity:.5}.fm-modal-audio-player .bi.bi-play-fill.active{opacity:1;color:#00bfff}.fm-modal-audio-player .bi.bi-pause-fill{color:gray;opacity:.5;cursor:pointer}@keyframes plyr-progress{to{background-position:var(--plyr-progress-loading-size, 25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:var(--plyr-font-family, inherit);font-variant-numeric:tabular-nums;font-weight:var(--plyr-font-weight-regular, 400);line-height:var(--plyr-line-height, 1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr video,.plyr audio,.plyr iframe{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui *:after,.plyr--full-ui *:before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:var(--plyr-badge-background, hsl(216, 15%, 34%));border-radius:var(--plyr-badge-border-radius, 2px);color:var(--plyr-badge-text-color, #fff);font-size:var(--plyr-font-size-badge, 9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:var(--plyr-font-size-small, 13px);left:0;padding:var(--plyr-control-spacing, 10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width: 480px){.plyr__captions{font-size:var(--plyr-font-size-base, 15px);padding:calc(var(--plyr-control-spacing, 10px) * 2)}}@media (min-width: 768px){.plyr__captions{font-size:var(--plyr-font-size-large, 18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(calc(var(--plyr-control-spacing, 10px) * -4))}.plyr__caption{background:var(--plyr-captions-background, rgba(0, 0, 0, .8));border-radius:2px;box-decoration-break:clone;color:var(--plyr-captions-text-color, #fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:transparent;border:0;border-radius:var(--plyr-control-radius, 4px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:calc(var(--plyr-control-spacing, 10px) * .7);position:relative;transition:all .3s ease}.plyr__control svg{display:block;fill:currentColor;height:var(--plyr-control-icon-size, 18px);pointer-events:none;width:var(--plyr-control-icon-size, 18px)}.plyr__control:focus{outline:0}.plyr__control:focus-visible{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}a.plyr__control{text-decoration:none}a.plyr__control:after,a.plyr__control:before{display:none}.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,.plyr__control.plyr__control--pressed .label--not-pressed{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:calc(var(--plyr-control-spacing, 10px) / 4)}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:calc(var(--plyr-control-spacing, 10px) / 4)}.plyr__controls .plyr__controls__item.plyr__time{padding:0 calc(var(--plyr-control-spacing, 10px) / 2)}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time{padding-left:0}.plyr__controls:empty{display:none}.plyr [data-plyr=captions],.plyr [data-plyr=pip],.plyr [data-plyr=airplay],.plyr [data-plyr=fullscreen]{display:none}.plyr--captions-enabled [data-plyr=captions],.plyr--pip-supported [data-plyr=pip],.plyr--airplay-supported [data-plyr=airplay],.plyr--fullscreen-enabled [data-plyr=fullscreen]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:var(--plyr-menu-background, rgba(255, 255, 255, .9));border-radius:var(--plyr-menu-radius, 8px);bottom:100%;box-shadow:var(--plyr-menu-shadow, 0 1px 2px rgba(0, 0, 0, .15));color:var(--plyr-menu-color, hsl(216, 15%, 34%));font-size:var(--plyr-font-size-base, 15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:var(--plyr-menu-arrow-size, 4px) solid transparent;border-top-color:var(--plyr-menu-background, rgba(255, 255, 255, .9));content:"";height:0;position:absolute;right:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7) - var(--plyr-menu-arrow-size, 4px) / 2);top:100%;width:0}.plyr__menu__container [role=menu]{padding:calc(var(--plyr-control-spacing, 10px) * .7)}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:var(--plyr-menu-color, hsl(216, 15%, 34%));display:flex;font-size:var(--plyr-font-size-menu, var(--plyr-font-size-small, 13px));padding:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 1.5) calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5);user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:var(--plyr-menu-item-arrow-size, 4px) solid transparent;content:"";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 4)}.plyr__menu__container .plyr__control--forward:after{border-left-color:var(--plyr-menu-arrow-color, hsl(216, 15%, 52%));right:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--forward:focus-visible:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:currentColor}.plyr__menu__container .plyr__control--back{font-weight:var(--plyr-font-weight-regular, 400);margin:calc(var(--plyr-control-spacing, 10px) * .7);margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 2);padding-left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 4);position:relative;width:calc(100% - calc(var(--plyr-control-spacing, 10px) * .7) * 2)}.plyr__menu__container .plyr__control--back:after{border-right-color:var(--plyr-menu-arrow-color, hsl(216, 15%, 52%));left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--back:before{background:var(--plyr-menu-back-border-color, hsl(216, 15%, 88%));box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color, #fff);content:"";height:1px;left:0;margin-top:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 2);overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back:focus-visible:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:currentColor}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:calc(var(--plyr-control-spacing, 10px) * .7)}.plyr__menu__container .plyr__control[role=menuitemradio]:before,.plyr__menu__container .plyr__control[role=menuitemradio]:after{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:rgba(0,0,0,.1);content:"";display:block;flex-shrink:0;height:16px;margin-right:var(--plyr-control-spacing, 10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:var(--plyr-control-toggle-checked-background, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio]:focus-visible:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:rgba(35,40,47,.1)}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:calc((calc(var(--plyr-control-spacing, 10px) * .7) - 2px) * -1);overflow:hidden;padding-left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 3.5);pointer-events:none}.plyr--full-ui input[type=range]{appearance:none;background:transparent;border:0;border-radius:calc(var(--plyr-range-thumb-height, 13px) * 2);color:var(--plyr-range-fill-background, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));display:block;height:calc(var(--plyr-range-thumb-active-shadow-width, 3px) * 2 + var(--plyr-range-thumb-height, 13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;background-image:linear-gradient(to right,currentColor var(--value, 0%),transparent var(--value, 0%))}.plyr--full-ui input[type=range]::-webkit-slider-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px);appearance:none;margin-top:calc((var(--plyr-range-thumb-height, 13px) - var(--plyr-range-track-height, 5px)) / 2 * -1)}.plyr--full-ui input[type=range]::-moz-range-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px)}.plyr--full-ui input[type=range]::-ms-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;color:transparent}.plyr--full-ui input[type=range]::-ms-fill-upper{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;background:currentColor}.plyr--full-ui input[type=range]::-ms-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px);margin-top:0}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range]:focus-visible::-webkit-slider-runnable-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-moz-range-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-ms-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr__poster{background-color:var(--plyr-video-background, var(--plyr-video-background, rgb(0, 0, 0)));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:var(--plyr-font-size-time, var(--plyr-font-size-small, 13px))}.plyr__time+.plyr__time:before{content:"\2044";margin-right:var(--plyr-control-spacing, 10px)}@media (max-width: 767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:var(--plyr-tooltip-background, #fff);border-radius:var(--plyr-tooltip-radius, 5px);bottom:100%;box-shadow:var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, .15));color:var(--plyr-tooltip-color, hsl(216, 15%, 34%));font-size:var(--plyr-font-size-small, 13px);font-weight:var(--plyr-font-weight-regular, 400);left:50%;line-height:1.3;margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);opacity:0;padding:calc(var(--plyr-control-spacing, 10px) / 2) calc(calc(var(--plyr-control-spacing, 10px) / 2) * 1.5);pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s .1s ease,opacity .2s .1s ease;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-right:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-top:var(--plyr-tooltip-arrow-size, 4px) solid var(--plyr-tooltip-background, #fff);bottom:calc(var(--plyr-tooltip-arrow-size, 4px) * -1);content:"";height:0;left:50%;position:absolute;transform:translate(-50%);width:0;z-index:2}.plyr .plyr__control:hover .plyr__tooltip,.plyr .plyr__control:focus-visible .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7))}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7));transform:translate(50%)}.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible{transform:translate(0) scale(1)}.plyr__progress{left:calc(var(--plyr-range-thumb-height, 13px) * .5);margin-right:var(--plyr-range-thumb-height, 13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:calc(var(--plyr-range-thumb-height, 13px) * -.5);margin-right:calc(var(--plyr-range-thumb-height, 13px) * -.5);width:calc(100% + var(--plyr-range-thumb-height, 13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:transparent;border:0;border-radius:100px;height:var(--plyr-range-track-height, 5px);left:0;margin-top:calc(var(--plyr-range-track-height, 5px) / 2 * -1);padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:transparent}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:var(--plyr-range-track-height, 5px);transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:var(--plyr-range-track-height, 5px);transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 25%,transparent 25%,transparent 50%,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 50%,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 75%,transparent 75%,transparent);background-repeat:repeat-x;background-size:var(--plyr-progress-loading-size, 25px) var(--plyr-progress-loading-size, 25px);color:transparent}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25))}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6))}.plyr__progress__marker{background-color:var(--plyr-progress-marker-background, #fff);border-radius:1px;height:var(--plyr-range-track-height, 5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--plyr-progress-marker-width, 3px);z-index:3}.plyr__volume{align-items:center;display:flex;position:relative}.plyr__volume input[type=range]{margin-left:calc(var(--plyr-control-spacing, 10px) / 2);margin-right:calc(var(--plyr-control-spacing, 10px) / 2);max-width:90px;min-width:60px;position:relative;z-index:2}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:var(--plyr-audio-controls-background, #fff);border-radius:inherit;color:var(--plyr-audio-control-color, hsl(216, 15%, 34%));padding:var(--plyr-control-spacing, 10px)}.plyr--audio .plyr__control:focus-visible,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:var(--plyr-audio-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));color:var(--plyr-audio-control-color-hover, #fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--audio .plyr__progress__buffer{color:var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6))}.plyr--video{overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:var(--plyr-video-background, var(--plyr-video-background, rgb(0, 0, 0)));border-radius:inherit;height:100%;margin:auto;overflow:hidden;position:relative;width:100%}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}@supports not (aspect-ratio: 16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:var(--plyr-video-controls-background, linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .75)));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:var(--plyr-video-control-color, #fff);left:0;padding:calc(var(--plyr-control-spacing, 10px) / 2);padding-top:calc(var(--plyr-control-spacing, 10px) * 2);position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width: 480px){.plyr--video .plyr__controls{padding:var(--plyr-control-spacing, 10px);padding-top:calc(var(--plyr-control-spacing, 10px) * 3.5)}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control:focus-visible,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));color:var(--plyr-video-control-color-hover, #fff)}.plyr__control--overlaid{background:var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));border:0;border-radius:100%;color:var(--plyr-video-control-color, #fff);display:none;left:50%;opacity:.9;padding:calc(var(--plyr-control-spacing, 10px) * 1.5);position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:hover,.plyr__control--overlaid:focus{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--video .plyr__progress__buffer{color:var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25))}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen video{height:100%}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width: 1024px){.plyr:fullscreen .plyr__captions{font-size:var(--plyr-font-size-xlarge, 21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;height:100%;margin:0;width:100%;bottom:0;left:0;position:fixed;right:0;top:0;z-index:10000000}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width: 1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:var(--plyr-font-size-xlarge, 21px)}}.plyr__ads{border-radius:inherit;bottom:0;cursor:pointer;left:0;overflow:hidden;position:absolute;right:0;top:0;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:hsl(216,15%,16%);border-radius:2px;bottom:var(--plyr-control-spacing, 10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:var(--plyr-control-spacing, 10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:var(--plyr-range-track-height, 5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:var(--plyr-tooltip-background, #fff);border-radius:var(--plyr-menu-radius, 8px);bottom:100%;box-shadow:var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, .15));margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s .1s ease,opacity .2s .1s ease;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-right:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-top:var(--plyr-tooltip-arrow-size, 4px) solid var(--plyr-tooltip-background, #fff);bottom:calc(var(--plyr-tooltip-arrow-size, 4px) * -1);content:"";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translate(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:hsl(216,15%,79%);border-radius:calc(var(--plyr-menu-radius, 8px) - 1px);overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px #00000026;content:"";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:var(--plyr-video-controls-background, linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .75)));border-bottom-left-radius:calc(var(--plyr-menu-radius, 8px) - 1px);border-bottom-right-radius:calc(var(--plyr-menu-radius, 8px) - 1px);bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:var(--plyr-font-size-time, var(--plyr-font-size-small, 13px))}.plyr__preview-scrubbing{bottom:0;filter:blur(1px);height:100%;left:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;right:0;top:0;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);overflow:hidden;border:0!important;height:1px!important;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important}.fm-modal{position:absolute;z-index:9998;top:0;bottom:0;left:0;right:0;width:100%;height:100%;background-color:#00000059;display:block;transition:opacity .4s ease;overflow:auto}.fm-modal .modal-xl{max-width:96%}.fm-modal-enter-active,.fm-modal-leave-active{transition:opacity .5s}.fm-modal-enter,.fm-modal-leave-to{opacity:0}.fm-info-block{flex:0 0 auto;padding-top:.2rem;padding-bottom:.4rem;border-bottom:1px solid #6c757d}.fm-info-block .progress{margin-top:.3rem}.fm-info-block .text-right>span{padding-left:.5rem;cursor:pointer}.fm-context-menu{position:absolute;z-index:9997;background-color:#fff;box-shadow:3px 2px 5px gray;border-radius:5px}.fm-context-menu:focus{outline:none}.fm-context-menu .list-unstyled{margin-bottom:0;border-bottom:1px solid rgba(0,0,0,.125)}.fm-context-menu ul>li{padding:.4rem 1rem}.fm-context-menu ul>li:not(.disabled){cursor:pointer}.fm-context-menu ul>li:not(.disabled):hover{background-color:#f8f9fa}.fm-context-menu ul>li:not(.disabled) i{padding-right:1.5rem}.fm-notification{position:absolute;right:1rem;bottom:0;z-index:9999;width:350px;display:block;transition:opacity .4s ease;overflow:auto}.fm-notification .fm-notification-item{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid;border-radius:.25rem}.fm-notification .notify-enter-active{transition:all .3s ease}.fm-notification .notify-leave-active{transition:all .8s ease}.fm-notification .notify-enter,.fm-notification .notify-leave-to{opacity:0}.fm{position:relative;height:100%;padding:1rem;background-color:#fff}.fm:-moz-full-screen{background-color:#fff}.fm:-webkit-full-screen{background-color:#fff}.fm:fullscreen{background-color:#fff}.fm .fm-body{flex:1 1 auto;overflow:hidden;position:relative;padding-top:1rem;padding-bottom:1rem;border-top:1px solid #6c757d;border-bottom:1px solid #6c757d}.fm .unselectable{user-select:none}.fm-error{color:#fff;background-color:#dc3545;border-color:#dc3545}.fm-danger{color:#dc3545;background-color:#fff;border-color:#dc3545}.fm-warning{color:#ffc107;background-color:#fff;border-color:#ffc107}.fm-success{color:#198754;background-color:#fff;border-color:#198754}.fm-info{color:#0dcaf0;background-color:#fff;border-color:#0dcaf0}.fm.fm-full-screen{width:100%;height:100%;padding-bottom:0}
10 |
--------------------------------------------------------------------------------