├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── TODO.md
├── assets
├── js
│ ├── actions
│ │ ├── addFile.js
│ │ ├── addFolder.js
│ │ ├── download.js
│ │ ├── edit.js
│ │ ├── getDirectoryTree.js
│ │ ├── getFileContent.js
│ │ ├── listing.js
│ │ ├── move.js
│ │ ├── permissions.js
│ │ ├── refresh.js
│ │ ├── remove.js
│ │ ├── rename.js
│ │ └── upload.js
│ ├── app.js
│ ├── config
│ │ └── app.js
│ ├── entities
│ │ └── File.js
│ ├── helpers
│ │ ├── DOMRender.js
│ │ ├── fetch.js
│ │ ├── functions.js
│ │ ├── loading.js
│ │ ├── modal.js
│ │ ├── treeViewer.js
│ │ └── uploadRequest.js
│ ├── main.js
│ ├── state.js
│ └── templates
│ │ ├── includes
│ │ ├── fileIcon.js
│ │ ├── sidebarDirIcon.js
│ │ └── tableDirIcon.js
│ │ ├── moveModalDirItem.js
│ │ ├── sidebarFileItem.js
│ │ ├── tableFileItem.js
│ │ └── uploadModalFileItem.js
└── scss
│ ├── _global.scss
│ ├── base
│ ├── _reset.scss
│ └── _utilities.scss
│ ├── components
│ ├── _button.scss
│ ├── _checkbox.scss
│ └── _input.scss
│ ├── pages
│ ├── _error.scss
│ ├── _homepage.scss
│ └── _login.scss
│ ├── style.scss
│ └── variables
│ ├── _mixins.scss
│ └── _variables.scss
├── bootstrap
└── bootstrap.php
├── composer.json
├── composer.lock
├── config
├── app.php
├── routes.php
├── services.php
└── session.php
├── gulpfile.js
├── lib
├── DIC
│ └── DIC.php
├── ErrorHandling
│ └── ErrorHandler.php
├── Http
│ ├── CookiesFactory.php
│ ├── Exception
│ │ ├── HttpInvalidArgumentException.php
│ │ └── HttpRuntimeException.php
│ ├── HttpCookie.php
│ ├── HttpRedirect.php
│ ├── HttpRequest.php
│ ├── HttpResponse.php
│ └── JsonResponse.php
├── Renderer
│ ├── Renderer.php
│ └── RendererException.php
├── Routing
│ ├── Exception
│ │ ├── RouteInvalidArgumentException.php
│ │ ├── RouteLogicException.php
│ │ └── RouteMatchingException.php
│ ├── Route.php
│ ├── RouteCollection.php
│ ├── RouteDispatcher.php
│ └── RouteUrlGenerator.php
└── Session
│ ├── Exception
│ └── SessionRuntimeException.php
│ ├── Session.php
│ └── SessionStorage.php
├── package-lock.json
├── package.json
├── public
├── .htaccess
├── dist
│ ├── app.min.js
│ └── style.min.css
├── index.php
└── vendor
│ ├── fetch-umd-polyfill.min.js
│ ├── filemanager-template.min.css
│ ├── filemanager-template.min.js
│ └── promises-polyfill.min.js
├── src
├── AppHandler.php
├── Controllers
│ ├── Controller.php
│ ├── Error
│ │ └── ErrorController.php
│ ├── Filemanager
│ │ └── FilemanagerController.php
│ ├── Home
│ │ └── HomeController.php
│ └── Login
│ │ └── LoginController.php
├── Modules
│ └── FtpAdapter
│ │ ├── FtpAdapter.php
│ │ ├── FtpAdapterException.php
│ │ └── FtpClient
│ │ ├── FtpClientAdapter.php
│ │ └── FtpClientAdapterException.php
└── views
│ ├── error.php
│ ├── filemanager.php
│ ├── homepage.php
│ ├── includes
│ ├── meta.html
│ └── styles.php
│ └── login.php
└── storage
├── logs
└── .gitignore
└── sessions
└── .gitignore
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-detectable=false
2 | *.css linguist-detectable=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /node_modules/
3 | /vendor/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 EL AMRANI CHAKIR
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ftp-filemanager
2 |
3 | A web based FTP client application to manage your FTP files, built with a simple MVC architecture, no frameworks or libraries are used (except my owns).
4 |
5 | 
6 |
7 | ## Features
8 | * Download & upload.
9 | * CRUD operations.
10 | * Move file & folder.
11 | * Rename file & folder.
12 | * Search for files.
13 | * Change file permissions.
14 | * View file details.
15 |
16 | ## Requirements
17 |
18 | * php >= 5.5.0
19 | * Apache rewrite module enabled.
20 |
21 | ## Dependencies
22 |
23 | This application uses :
24 | * [php-ftp-client](https://github.com/lazzard/php-ftp-client) : A library that's wraps the PHP FTP extension functions in an oriented objet way.
25 | * [filemanager-template](https://github.com/ELAMRANI744/filemanager-template) : A filemanager template that's offer a clean interface, and some other important features.
26 |
27 | ## How to setup this project
28 |
29 | Download the repo or clone it using git:
30 |
31 | ```
32 | git clone https://github.com/ELAMRANI744/ftp-filemanager
33 | ```
34 |
35 | Then install composer dependencies :
36 |
37 | ```
38 | composer install
39 | ```
40 |
41 | ## Deployment
42 |
43 | 1. Move project files to the production server folder (tipically `public_html` folder).
44 | 2. Install the application dependencies (install composer dependencies).
45 | 3. Disable the debugging mode in `config/app.php`, and you ready to start.
46 |
47 | ## Development
48 |
49 | For development environment, you need to install npm dependencies (You need also install composer dependencies):
50 |
51 | ```
52 | npm install
53 | ```
54 |
55 | ## Worth knowing about this project MVC architecture
56 |
57 | Before the development process, one of the requirements was building an application that's based on MVC pattern without using any of the existing frameworks (Laravel, Symfony ...), for that I have started with this [tutorial](https://github.com/PatrickLouys/no-framework-tutorial) (Thanks for the author), this tutorial was a great place to understand the MVC pattern and know how the biggest frameworks actually works, however this Tuto uses some of others components that's necessary for every MVC application, and in this point i've decided to not use any of them, but instead trying to understand the basic concepts for each of them, and attempt to build my own components (light and simple ones for this time) - you can find them in the `lib` folder.
58 |
59 | ## Concepts
60 |
61 | This is a full stack project, a lot of things covered here either in front end or backend part, however the project covered this web programming techniques :
62 |
63 | * Design UI/UX
64 | * This project is designed taking into consideration the UI/UX approach, you can check the design muckop in [behance](https://www.behance.net/gallery/104400253/FTP-Client-web-application).
65 |
66 | * Web integration :
67 | * Using css preprocessors (SASS).
68 | * Vanilla javascript (trying to make a clean code).
69 | * Using AJAX (Fetch API & XMLHttpRequest).
70 | * Using some of ES6 features.
71 | * Using Gulp Task runner.
72 |
73 | * Server side development :
74 | * Dependency injection container (The base Controller injection).
75 | * Basic Http component to simulate Http requests and responses.
76 | * A very simple template renderer (To separate Php code from Html - Using PHP Raw Templating).
77 | * A basic routing component (The hard part for me).
78 | * PHP session management & some of security concerns.
79 | * Handling PHP errors & exceptions.
80 | * SOLID principles.
81 | * Dependency injection.
82 | * The Front controller pattern.
83 |
84 | ## Contribution
85 |
86 | All contributions are welcome, for a features ideas check the `TODO.md` file. Thank you.
87 |
88 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | ### Features to add.
4 | - [ ] Add compress ftp files action.
5 | - [ ] Add extract compressed files option while uploading.
6 | - [ ] IE supporting (Using babel polyfill for Gulp).
7 |
8 | ### Others.
9 | - [ ] Makes the home page features (I have no time for that now xD).
--------------------------------------------------------------------------------
/assets/js/actions/addFile.js:
--------------------------------------------------------------------------------
1 | import {fetchUpdate} from "../helpers/fetch";
2 | import refresh from "./refresh";
3 | import modal from "../helpers/modal";
4 | import {hideLoading, showLoading} from "../helpers/loading";
5 |
6 | function addFile(filename, path) {
7 | modal('#addFileModal').close();
8 | showLoading();
9 | fetchUpdate('POST', 'api?action=addFile', {
10 | name: filename,
11 | path: path
12 | }, function (res) {
13 | if (res.result) {
14 | refresh(path);
15 | }
16 | }, function (error) {
17 | modal('#addFileModal').show().showError(error);
18 | hideLoading();
19 | });
20 | }
21 |
22 | export default addFile;
--------------------------------------------------------------------------------
/assets/js/actions/addFolder.js:
--------------------------------------------------------------------------------
1 | import modal from "../helpers/modal";
2 | import {hideLoading, showLoading} from "../helpers/loading";
3 | import {fetchUpdate} from "../helpers/fetch";
4 | import refresh from "./refresh";
5 |
6 | function addFolder(folderName, path) {
7 | modal('#addFolderModal').close();
8 | showLoading();
9 | fetchUpdate('POST', 'api?action=addFolder', {
10 | name: folderName,
11 | path: path
12 | }, function (res) {
13 | if (res.result) {
14 | refresh(path);
15 | }
16 | }, function (error) {
17 | modal('#addFolderModal').show().showError(error);
18 | hideLoading();
19 | });
20 | }
21 |
22 | export default addFolder;
--------------------------------------------------------------------------------
/assets/js/actions/download.js:
--------------------------------------------------------------------------------
1 | function download(path, files) {
2 | if (Array.isArray(files)) {
3 | files.forEach(function (file) {
4 | window.open('api?action=download&file=' + encodeURIComponent(path + file), '_blank');
5 | });
6 | }
7 | }
8 |
9 | export default download;
--------------------------------------------------------------------------------
/assets/js/actions/edit.js:
--------------------------------------------------------------------------------
1 | import {fetchUpdate} from "../helpers/fetch";
2 | import modal from "../helpers/modal";
3 | import {hideLoading, showLoading} from "../helpers/loading";
4 | import refresh from "./refresh";
5 | import state from "../state";
6 |
7 | function edit(file, content) {
8 | modal('#editorModal').close();
9 | showLoading();
10 | fetchUpdate('PUT', 'api?action=updateFileContent', {
11 | file: file,
12 | content: content
13 | }, function (res) {
14 | if (res.result) {
15 | refresh(state.path);
16 | }
17 | }, function (error) {
18 | modal('#editorModal').show().showError(error);
19 | hideLoading();
20 | });
21 | }
22 |
23 | export default edit;
--------------------------------------------------------------------------------
/assets/js/actions/getDirectoryTree.js:
--------------------------------------------------------------------------------
1 | import {fetchGet} from "../helpers/fetch";
2 | import DOMRender from "../helpers/DOMRender";
3 | import moveModalDirItem from "../templates/moveModalDirItem";
4 | import {getElement} from "../helpers/functions";
5 |
6 | function getDirectoryTree() {
7 | fetchGet('api?action=getDirectoryTree', function (res) {
8 | if (res.result) {
9 | getElement('.move-file-modal .files-list').textContent = '';
10 |
11 | res.result.forEach(function (dir) {
12 | // If the path starts with a slash remove it
13 | dir.path = dir.path.replace(/^\//, '');
14 |
15 | if (dir.path.indexOf('/') !== -1) { // Is a 'deep' path
16 | const path = dir.path.slice(0, dir.path.lastIndexOf('/'));
17 | // Append the content to the right dir item using the path
18 | DOMRender(moveModalDirItem(dir), `.move-file-modal .files-list .dir-item[data-path="${encodeURI(path)}"] .sub-files`);
19 | } else {
20 | DOMRender(moveModalDirItem(dir), '.move-file-modal .files-list');
21 | }
22 | });
23 | }
24 | });
25 | }
26 |
27 | export default getDirectoryTree;
--------------------------------------------------------------------------------
/assets/js/actions/getFileContent.js:
--------------------------------------------------------------------------------
1 | import {fetchGet} from "../helpers/fetch";
2 | import modal from "../helpers/modal";
3 |
4 | function getFileContent(file) {
5 | fmEditor.clear().showLoading();
6 | fetchGet('api?action=getFileContent&file=' + encodeURIComponent(file), function (res) {
7 | if (res.result) {
8 | fmEditor.set(res.result);
9 | }
10 | }, function (err) {
11 | modal('#editorModal').showError(err);
12 | }, function () {
13 | fmEditor.hideLoading();
14 | });
15 | }
16 |
17 | export default getFileContent;
--------------------------------------------------------------------------------
/assets/js/actions/listing.js:
--------------------------------------------------------------------------------
1 | import {hideLoading, showLoading} from "../helpers/loading";
2 | import {getAppendToSelector} from "../helpers/treeViewer";
3 | import {fetchGet} from "../helpers/fetch";
4 | import DOMRender from "../helpers/DOMRender";
5 | import sidebarFileItem from "../templates/sidebarFileItem";
6 | import File from "../entities/File";
7 | import tableFileItem from "../templates/tableFileItem";
8 | import state from "../state";
9 | import {getElement} from "../helpers/functions";
10 |
11 | function browse(path) {
12 | const appendTo = getAppendToSelector(path);
13 | showLoading();
14 | fetchGet('api?action=browse&path=' + encodeURIComponent(path), function (data) {
15 | if (Array.isArray(data.result)) {
16 | // Clear table content
17 | getElement('.files-table tbody').textContent = '';
18 |
19 | data.result.forEach(function (item) {
20 | DOMRender(sidebarFileItem(new File(item)), appendTo);
21 | DOMRender(tableFileItem(new File(item)), '.files-table tbody');
22 | });
23 |
24 | // Refresh files state
25 | state.files = data.result;
26 | }
27 | }, function (err) {
28 | // in case of request fail close the last sidebar clicked dir item
29 | getElement(appendTo.split(' ').slice(0, -1).join(' ')).dataset.open = 'false';
30 | // back path
31 | state.path = state.path.substring(0, state.path.lastIndexOf('/'));
32 | // show the error message
33 | if (typeof err !== 'undefined') {
34 | alert('Error : ' + err);
35 | }
36 | }, function () {
37 | hideLoading();
38 | });
39 | }
40 |
41 | /**
42 | * Backs to the previous directory.
43 | */
44 | function back() {
45 | /**
46 | * / => /
47 | * /public_html/ => /
48 | * /public_html/css => public_html
49 | */
50 | var backPath = state.path
51 | .replace(/(^\/|\/$)/g, '') // clear the slashes from start&end
52 | .split('/')
53 | .slice(0, -1)
54 | .join('/');
55 |
56 | if (backPath === '') {
57 | backPath = '/';
58 | }
59 |
60 | getElement(`.sidebar .dir-item[data-name="${encodeURI(backPath)}"`).click();
61 | }
62 |
63 | function forward() {
64 | const selectedDir = getElement('.files-table .file-item.selected[data-type="dir"]');
65 | if (typeof selectedDir === 'object') {
66 | // Simulate the double click
67 | selectedDir.dispatchEvent(new MouseEvent('dblclick', {
68 | bubbles: true, // Important! Enable event bubbling
69 | cancelable: true,
70 | }));
71 | }
72 | }
73 |
74 | function home() {
75 | const root = getElement('.sidebar .files-list .dir-item[data-name="/"]');
76 | if (typeof root === 'object') {
77 | root.dispatchEvent(new MouseEvent('click', {
78 | bubbles: true,
79 | cancelable: true,
80 | }));
81 | }
82 | }
83 |
84 | export {
85 | browse,
86 | back,
87 | forward,
88 | home,
89 | };
--------------------------------------------------------------------------------
/assets/js/actions/move.js:
--------------------------------------------------------------------------------
1 | import {fetchUpdate} from "../helpers/fetch";
2 | import {hideLoading, showLoading} from "../helpers/loading";
3 | import modal from "../helpers/modal";
4 | import refresh from "./refresh";
5 | import state from "../state";
6 |
7 | function move(path, file, newPath) {
8 | modal("#moveFileModal").close();
9 | showLoading();
10 | fetchUpdate('PUT', 'api?action=move', {
11 | path: path,
12 | file: file,
13 | newPath: newPath
14 | }, function (res) {
15 | if (res.result) {
16 | refresh(state.path);
17 | }
18 | }, function (err) {
19 | modal("#moveFileModal").show().showError(err);
20 | hideLoading();
21 | });
22 | }
23 |
24 | export default move;
--------------------------------------------------------------------------------
/assets/js/actions/permissions.js:
--------------------------------------------------------------------------------
1 | import {hideLoading, showLoading} from "../helpers/loading";
2 | import {fetchUpdate} from "../helpers/fetch";
3 | import modal from "../helpers/modal";
4 | import refresh from "./refresh";
5 | import state from "../state";
6 |
7 | function permissions(path, file, chmod) {
8 | modal('#permissionsModal').close();
9 | showLoading();
10 | fetchUpdate('PUT', 'api?action=permissions', {
11 | path: path,
12 | file: file,
13 | permissions: chmod
14 | }, function (res) {
15 | if (res.result) {
16 | refresh(state.path);
17 | }
18 | }, function (err) {
19 | modal('#permissionsModal').show().showError(err);
20 | hideLoading();
21 | })
22 | }
23 |
24 | export default permissions;
--------------------------------------------------------------------------------
/assets/js/actions/refresh.js:
--------------------------------------------------------------------------------
1 | import {getElement} from "../helpers/functions";
2 | import {getAppendToSelector} from "../helpers/treeViewer";
3 | import {browse} from "./listing";
4 |
5 | function refresh(path) {
6 | getElement(getAppendToSelector(path)).textContent = '';
7 | browse(path);
8 | }
9 |
10 | export default refresh;
--------------------------------------------------------------------------------
/assets/js/actions/remove.js:
--------------------------------------------------------------------------------
1 | import {hideLoading, showLoading} from "../helpers/loading";
2 | import {fetchDelete} from "../helpers/fetch";
3 | import refresh from "./refresh";
4 | import state from "../state";
5 | import modal from "../helpers/modal";
6 |
7 | function remove(files) {
8 | modal('#removeFileModal').close();
9 | showLoading();
10 | fetchDelete('api?action=remove', {
11 | files: files
12 | }, function (res) {
13 | if (res.result) {
14 | refresh(state.path);
15 | }
16 | }, function (error) {
17 | modal('#removeFileModal').show().showError(error);
18 | hideLoading();
19 | })
20 | }
21 |
22 | export default remove;
--------------------------------------------------------------------------------
/assets/js/actions/rename.js:
--------------------------------------------------------------------------------
1 | import {fetchUpdate} from "../helpers/fetch";
2 | import modal from "../helpers/modal";
3 | import {hideLoading, showLoading} from "../helpers/loading";
4 | import refresh from "./refresh";
5 | import state from "../state";
6 |
7 | function rename(path, file, newName) {
8 | modal('#renameFileModal').close();
9 | showLoading();
10 | fetchUpdate('PUT', 'api?action=rename', {
11 | path: path,
12 | file: file,
13 | newName: newName,
14 | }, function (res) {
15 | if (res.result) {
16 | refresh(state.path);
17 | }
18 | }, function (err) {
19 | modal('#renameFileModal').show().showError(err);
20 | hideLoading();
21 | });
22 | }
23 |
24 | export default rename;
--------------------------------------------------------------------------------
/assets/js/actions/upload.js:
--------------------------------------------------------------------------------
1 | import uploadRequest from "../helpers/uploadRequest";
2 |
3 | var Upload = function () {
4 | /**
5 | * Store an array of promises results.
6 | *
7 | * @type {Array}
8 | */
9 | this.stack = [];
10 |
11 | /**
12 | * Register a promise in the stack.
13 | *
14 | * @param {string} path
15 | * @param {string} data
16 | * @param {function} onprogress
17 | * @param {function} onerror
18 | */
19 | this.push = function (path, data, onprogress, onerror) {
20 | this.stack.push(uploadRequest({
21 | method: 'POST',
22 | url: 'api?action=upload',
23 | data: data,
24 | success: function (res) {
25 | if (res.error) {
26 | onerror(res.error);
27 | }
28 | },
29 | progress: function (info) {
30 | const percentage = (info.loaded / info.total) * 100;
31 | onprogress(parseInt(percentage.toFixed()));
32 | }
33 | }));
34 | };
35 |
36 | /***
37 | * Resolves the promises stack.
38 | *
39 | * @param {function} resolver
40 | */
41 | this.resolveStack = function (resolver) {
42 | Promise.all(this.stack).then(function (responses) {
43 | resolver(responses);
44 | });
45 | };
46 | };
47 |
48 | export default Upload;
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | import { bindEvent, getElement, getElements, on } from "./helpers/functions";
2 | import { closeSiblingTreeOf, getSelectedPath } from "./helpers/treeViewer";
3 | import refresh from "./actions/refresh";
4 | import state from "./state";
5 | import addFile from "./actions/addFile";
6 | import addFolder from "./actions/addFolder";
7 | import modal from "./helpers/modal";
8 | import getFileContent from "./actions/getFileContent";
9 | import { browse, back, forward, home } from "./actions/listing";
10 | import edit from "./actions/edit";
11 | import remove from "./actions/remove";
12 | import rename from "./actions/rename";
13 | import getDirectoryTree from "./actions/getDirectoryTree";
14 | import move from "./actions/move";
15 | import permissions from "./actions/permissions";
16 | import File from "./entities/File";
17 | import download from "./actions/download";
18 | import DOMRender from "./helpers/DOMRender";
19 | import uploadModalFileItem from "./templates/uploadModalFileItem";
20 | import Upload from "./actions/upload";
21 | import config from "./config/app";
22 |
23 | const App = function () {
24 | /**
25 | * A registry for events methods.
26 | */
27 | var registry = [];
28 |
29 | /**
30 | * Registers events methods.
31 | */
32 | this.registerEvents = function () {
33 | registry.push(load);
34 |
35 | /**
36 | * Directory listing actions.
37 | */
38 | registry.push(sidebarDirectoryListing);
39 | registry.push(tableDirectoryListing);
40 | registry.push(toolbarActions);
41 |
42 | /**
43 | * Actions.
44 | */
45 | registry.push(refreshAction);
46 | registry.push(addFileAction);
47 | registry.push(addFolderAction);
48 | registry.push(removeFilesAction);
49 | registry.push(renameAction);
50 | registry.push(moveFileAction);
51 | registry.push(editFileAction);
52 | registry.push(changePermissionsAction);
53 | registry.push(downloadAction);
54 | registry.push(uploadAction);
55 |
56 | /**
57 | * Modal components events.
58 | */
59 | registry.push(updateEditorModal);
60 | registry.push(updateMoveModal);
61 | registry.push(updatePermissionsModal);
62 | registry.push(updateInfoModal);
63 | registry.push(updateUploadModal);
64 | };
65 |
66 | /**
67 | * Call and bind the events listeners.
68 | */
69 | this.init = function () {
70 | registry.forEach(function (fn) {
71 | fn();
72 | });
73 | };
74 |
75 | var load = function () {
76 | bindEvent("DOMContentLoaded", document, browse(state.path));
77 | };
78 |
79 | var sidebarDirectoryListing = function () {
80 | on("click", ".sidebar .dir-item", function (e) {
81 | if (!e.target.closest(".file-item")) {
82 | // ignore file items click
83 | const item = e.target.closest(".dir-item");
84 |
85 | item.dataset.open = "true";
86 | item.querySelector(".sub-files").textContent = "";
87 |
88 | closeSiblingTreeOf(item);
89 | browse((state.path = getSelectedPath()));
90 | }
91 | });
92 | };
93 |
94 | var tableDirectoryListing = function () {
95 | on("dblclick", '.files-table .file-item[data-type="dir"]', function (e) {
96 | const item = e.target.closest('.file-item[data-type="dir"]'),
97 | fileName = getElement(".file-name", item).textContent.trim();
98 |
99 | const path = getSelectedPath() + fileName + "/";
100 |
101 | // find the sidebar alternative file and make it open
102 | if (path !== "/") {
103 | getElement(
104 | '.sidebar .dir-item[data-name="' + encodeURI(fileName) + '"]'
105 | ).dataset.open = "true";
106 | }
107 |
108 | browse((state.path = path));
109 |
110 | // Disable footer right buttons
111 | getElements(".right-buttons *[data-action]").forEach(function (button) {
112 | button.disabled = true;
113 | });
114 | });
115 | };
116 |
117 | var refreshAction = function () {
118 | bindEvent("click", 'button[data-action="refresh"]', function () {
119 | refresh(state.path);
120 | });
121 | };
122 |
123 | var addFileAction = function () {
124 | bindEvent("click", "#addFileBtn", function () {
125 | addFile(getElement("#addFileModal #fileName").value, state.path);
126 | });
127 | };
128 |
129 | var addFolderAction = function () {
130 | bindEvent("click", "#addFolderBtn", function () {
131 | addFolder(getElement("#addFolderModal #folderName").value, state.path);
132 | });
133 | };
134 |
135 | var updateEditorModal = function () {
136 | // Get editor file content when double clicking in a table file item
137 | on("dblclick", '.files-table .file-item[data-type="file"]', function (e) {
138 | modal("#editorModal").show();
139 | const clickedFileName = getElement(
140 | ".file-name",
141 | e.target.closest(".file-item")
142 | ).textContent;
143 | state.editableFile = state.path + clickedFileName;
144 | getFileContent(state.editableFile);
145 | });
146 |
147 | // Update editor content when clicking in the footer edit button
148 | bindEvent("click", 'button[data-action="edit"]', function () {
149 | alert();
150 | const selectedFile = getElement(
151 | '.files-table .file-item.selected[data-type="file"] .file-name'
152 | );
153 | if (typeof selectedFile === "object") {
154 | state.editableFile = state.path + selectedFile.textContent;
155 | getFileContent(state.editableFile);
156 | }
157 | });
158 |
159 | // Get editor file content when clicking in a sidebar file item
160 | on("click", ".sidebar .file-item", function (e) {
161 | const file = e.target.closest(".file-item"),
162 | fileName = decodeURI(file.dataset.name);
163 |
164 | // Close siblings opened directories
165 | closeSiblingTreeOf(file);
166 | // Update path
167 | state.path = getSelectedPath();
168 | // Show editor modal
169 | modal("#editorModal").show();
170 | // Get file content
171 | state.editableFile = state.path + fileName;
172 | getFileContent(state.editableFile);
173 | });
174 | };
175 |
176 | var editFileAction = function () {
177 | bindEvent("click", "#updateFileBtn", function () {
178 | edit(state.editableFile, fmEditor.get());
179 | });
180 | };
181 |
182 | var removeFilesAction = function () {
183 | bindEvent("click", "#removeFileBtn", function () {
184 | const selectedItems = getElements(".files-table .file-item.selected"),
185 | files = [];
186 |
187 | selectedItems.forEach(function (item) {
188 | const name = getElement(".file-name", item).textContent;
189 | files.push(state.path + name);
190 | });
191 |
192 | remove(files);
193 | });
194 | };
195 |
196 | var renameAction = function () {
197 | bindEvent("click", "#renameFileBtn", function () {
198 | const file = getElement("#renameFileModal .name-for").textContent,
199 | newName = getElement("#newFileName").value;
200 |
201 | rename(state.path, file, newName);
202 | });
203 | };
204 |
205 | var updateMoveModal = function () {
206 | bindEvent("click", 'button[data-action="move"]', function () {
207 | getDirectoryTree();
208 | });
209 | };
210 |
211 | var moveFileAction = function () {
212 | bindEvent("click", "#moveFileBtn", function () {
213 | const file = getElement("#moveFileModal .source").textContent,
214 | newPath = getElement("#moveFileModal .destination").textContent;
215 |
216 | move(state.path, file, newPath);
217 | });
218 | };
219 |
220 | var changePermissionsAction = function () {
221 | bindEvent("click", "#changePermBtn", function () {
222 | const file = getElement("#permissionsModal .filename").textContent,
223 | chmod = getElement("#permissionsModal .numeric-chmod").textContent;
224 |
225 | permissions(state.path, file, chmod);
226 | });
227 | };
228 |
229 | var updatePermissionsModal = function () {
230 | bindEvent("click", 'button[data-action="permissions"]', function () {
231 | const file = state.getFileByName(
232 | getElement("#permissionsModal .filename").textContent
233 | ),
234 | permissions = file.permissions,
235 | chunks = permissions.slice(1).split(""),
236 | perms = {
237 | owner: chunks.slice(0, 3),
238 | group: chunks.slice(3, 6),
239 | others: chunks.slice(6),
240 | },
241 | translate = {
242 | r: "read",
243 | w: "write",
244 | x: "execute",
245 | },
246 | rules = {
247 | r: 4,
248 | w: 2,
249 | x: 1,
250 | };
251 |
252 | var chmod = [0, 0, 0];
253 | for (var prop in perms) {
254 | if (perms.hasOwnProperty(prop)) {
255 | perms[prop].forEach(function (perm) {
256 | if (perm !== "-") {
257 | getElement(
258 | `#permissionsModal .checkbox[data-action="${translate[perm]}"][data-group="${prop}"] input[type=checkbox]`
259 | ).checked = true;
260 |
261 | switch (prop) {
262 | case "owner":
263 | chmod[0] += rules[perm];
264 | break;
265 | case "group":
266 | chmod[1] += rules[perm];
267 | break;
268 | case "others":
269 | chmod[2] += rules[perm];
270 | break;
271 | }
272 | }
273 | });
274 | }
275 | }
276 |
277 | getElement(
278 | "#permissionsModal .numeric-chmod"
279 | ).textContent = `0${chmod.join("")}`;
280 | });
281 | };
282 |
283 | var updateInfoModal = function () {
284 | bindEvent("click", 'button[data-action="info"]', function () {
285 | const selectedFilename = getElement(
286 | ".files-table .file-item.selected .file-name"
287 | ).textContent,
288 | file = new File(state.getFileByName(selectedFilename));
289 |
290 | for (var prop in file) {
291 | if (file.hasOwnProperty(prop)) {
292 | const infoItem = getElement(`.info-modal .info-item.${prop}`);
293 | if (typeof infoItem !== "undefined") {
294 | var info = file[prop];
295 | if (prop === "size") {
296 | info = file.bytesToSize();
297 | }
298 | getElement(".info-text", infoItem).textContent = info;
299 | }
300 | }
301 | }
302 | });
303 | };
304 |
305 | var toolbarActions = function () {
306 | bindEvent("click", '.toolbar button[data-action="back"]', back);
307 | bindEvent("click", '.toolbar button[data-action="forward"]', forward);
308 | bindEvent("click", '.toolbar button[data-action="home"]', home);
309 | };
310 |
311 | var downloadAction = function () {
312 | bindEvent("click", 'button[data-action="download"]', function () {
313 | const selectedFiles = Array.from(
314 | getElements(".files-table .file-item.selected .file-name")
315 | );
316 |
317 | var files = selectedFiles.map(function (item) {
318 | return item.textContent;
319 | });
320 |
321 | download(state.path, files);
322 | });
323 | };
324 |
325 | var updateUploadModal = function () {
326 | // Update the uploading path and remove existing state files
327 | bindEvent("click", 'button[data-action="upload"]', function () {
328 | const uploadingFolder = getElement("#uploadModal .uploading-folder");
329 | uploadingFolder.textContent = state.path;
330 | state.uploadedFiles = [];
331 | getElement(".files-to-upload").textContent = "";
332 | });
333 |
334 | // Append files
335 | bindEvent("change", "#uploadFilesBtn", function () {
336 | const files = Array.from(getElement("#uploadFilesBtn").files);
337 | if (files.length === 0) return;
338 |
339 | if (state.uploadedFiles.length > config.maximumFilesUpload) {
340 | modal("#uploadModal").showError(
341 | `Cannot upload more than ${config.maximumFilesUpload} files at time.`
342 | );
343 | return false;
344 | }
345 |
346 | files.forEach(function (file) {
347 | DOMRender(uploadModalFileItem(new File(file)), ".files-to-upload");
348 | // Check for the max file size
349 | if (
350 | new File(file).size <=
351 | config.maximumUploadSize * Math.pow(1024, 2)
352 | ) {
353 | // Storing the files
354 | state.uploadedFiles.push(file);
355 | } else {
356 | const fileItem = getElement(
357 | `#uploadModal .file-item[data-name="${encodeURI(file.name)}"]`
358 | );
359 | getElement(
360 | ".file-error .text",
361 | fileItem
362 | ).textContent = `Maximum upload size ${config.maximumUploadSize} MG exceeded, this file will be skipped.`;
363 | }
364 | });
365 |
366 | // Reset the form
367 | getElement("#uploadFilesForm").reset();
368 | });
369 |
370 | // Remove the files
371 | on("click", ".remove-upload-file", function (e) {
372 | const removedFile = getElement(
373 | ".name",
374 | e.target.closest(".file-item")
375 | ).textContent;
376 | state.uploadedFiles.forEach(function (file, i) {
377 | if (file.name === removedFile) {
378 | delete state.uploadedFiles[i];
379 | }
380 | });
381 | });
382 | };
383 |
384 | var uploadAction = function () {
385 | bindEvent("click", "#uploadBtn", function () {
386 | // reset all progress info
387 | getElements("#uploadModal .file-item").forEach(function (item) {
388 | getElement(".progress-bar", item).style.width = "0";
389 | getElement(".percentage", item).textContent = "0%";
390 | getElement(".upload-state", item).textContent = "Uploading ...";
391 | getElement(".file-error .text", item).textContent = "";
392 | });
393 |
394 | const formData = new FormData();
395 | const upload = new Upload();
396 | state.uploadedFiles.forEach(function (file) {
397 | const fileItem = getElement(
398 | `#uploadModal .file-item[data-name="${encodeURI(file.name)}"]`
399 | ),
400 | errorEle = getElement(".file-error .text", fileItem),
401 | percentageEle = getElement(".percentage", fileItem),
402 | progressEle = getElement(".progress-bar", fileItem),
403 | uploadStateEle = getElement(".upload-state", fileItem);
404 |
405 | formData.append("file", file);
406 | formData.append("path", state.path);
407 | upload.push(
408 | state.path,
409 | formData,
410 | function (progress) {
411 | percentageEle.textContent = progress + "%";
412 | progressEle.style.width = progress + "%";
413 |
414 | if (progress === 100) {
415 | uploadStateEle.textContent = "Wait!! Uploading to the server ...";
416 | percentageEle.textContent = "";
417 | }
418 | },
419 | function (err) {
420 | errorEle.textContent = err;
421 | }
422 | );
423 | });
424 |
425 | upload.resolveStack(function () {
426 | modal("#uploadModal").close();
427 | refresh(state.path);
428 | });
429 | });
430 | };
431 | };
432 |
433 | export default App;
434 |
--------------------------------------------------------------------------------
/assets/js/config/app.js:
--------------------------------------------------------------------------------
1 | const config = {
2 |
3 | /**
4 | * Specify the maximum file size in MG allowed for upload transfers.
5 | */
6 | maximumUploadSize: 2,
7 |
8 | /**
9 | * The maximum files allowed to be uploaded for one upload operation.
10 | */
11 | maximumFilesUpload : 10,
12 |
13 | /**
14 | * Fetch configs.
15 | */
16 | fetch: {
17 | get: {
18 | headers: {
19 | 'Accept': 'application/json',
20 | 'X-Requested-With': 'XMLHttpRequest',
21 | }
22 | },
23 |
24 | post: {
25 | method: 'POST',
26 | headers: {
27 | 'Content-type': 'application/json',
28 | 'X-Requested-With': 'XMLHttpRequest',
29 | }
30 | },
31 |
32 | put: {
33 | method: 'PUT',
34 | headers: {
35 | 'Content-type': 'application/json',
36 | 'X-Requested-With': 'XMLHttpRequest',
37 | }
38 | },
39 |
40 | delete: {
41 | method: 'DELETE',
42 | headers: {
43 | 'Content-type': 'application/json',
44 | 'X-Requested-With': 'XMLHttpRequest',
45 | }
46 | },
47 |
48 | errorHandler: function (res) {
49 | if (res.location) {
50 | window.location = res.location;
51 | }
52 | },
53 | }
54 | };
55 |
56 | export default config;
--------------------------------------------------------------------------------
/assets/js/entities/File.js:
--------------------------------------------------------------------------------
1 | var File = function (data) {
2 | if (typeof data !== 'object') return;
3 |
4 | this.name = data.name || '';
5 | this.type = data.type || 'file';
6 | this.size = parseInt(data.size) || 0;
7 | this.path = data.path || '';
8 | this.owner = parseInt(data.owner) || '';
9 | this.group = parseInt(data.group) || '';
10 | this.modifiedTime = data.modifiedTime || '';
11 | this.permissions = data.permissions || '';
12 |
13 | File.prototype.bytesToSize = function () {
14 | if (this.size === 0) return '0 Byte';
15 |
16 | const sizes = {
17 | Bytes: 1,
18 | KB: Math.pow(1024, 1),
19 | MB: Math.pow(1024, 2),
20 | GB: Math.pow(1024, 3),
21 | TB: Math.pow(1024, 4),
22 | };
23 |
24 | var unit = null;
25 | for (var prop in sizes) {
26 | if (this.size >= sizes[prop]) {
27 | unit = prop;
28 | }
29 | }
30 |
31 | return (this.size / sizes[unit]).toFixed(0) + ' ' + unit;
32 | };
33 |
34 | File.prototype.isFile = function () {
35 | return this.type === 'file';
36 | };
37 |
38 | File.prototype.isDir = function () {
39 | return this.type === 'dir';
40 | };
41 | };
42 |
43 | export default File;
--------------------------------------------------------------------------------
/assets/js/helpers/DOMRender.js:
--------------------------------------------------------------------------------
1 | function DOMRender(template, container) {
2 | (document.querySelector(container) || document.body)
3 | .insertAdjacentHTML('beforeend', template);
4 | }
5 |
6 | export default DOMRender;
--------------------------------------------------------------------------------
/assets/js/helpers/fetch.js:
--------------------------------------------------------------------------------
1 | import config from "../config/app";
2 |
3 | const fetchConfig = config.fetch;
4 |
5 | const doFetch = function (uri, options, successHandler, errorHandler = null, completeHandler = null) {
6 | fetch(uri, options)
7 | .then(response => {
8 | const contentType = response.headers.get('Content-type');
9 | if (contentType && contentType.indexOf('application/json') !== -1) {
10 | return response.json().then((json) => {
11 | if (response.ok) {
12 | callHandler(successHandler, json);
13 | } else {
14 | callHandler(completeHandler, json);
15 | return Promise.reject(json);
16 | }
17 | callHandler(completeHandler, json);
18 | return json;
19 | });
20 | }
21 | })
22 | .catch((json) => {
23 | fetchConfig.errorHandler(json);
24 | callHandler(errorHandler, json.error);
25 | });
26 |
27 | var callHandler = function (handler) {
28 | if (typeof handler === 'function') {
29 | handler.apply(this, Array.prototype.slice.call(arguments, 1));
30 | }
31 | };
32 | };
33 |
34 | const fetchGet = function (uri, success, error, complete) {
35 | doFetch(uri, fetchConfig.get, success, error, complete);
36 | };
37 |
38 | const fetchUpdate = function (method, uri, data, success, error, complete) {
39 | const allowedMethods = ['POST', 'PUT', 'PATCH'];
40 |
41 | if (!allowedMethods.includes(method)) {
42 | throw `fetchUpdate accept only ${allowedMethods.join(', ')}, ${method} giving.`;
43 | }
44 |
45 | doFetch(uri, Object.assign(fetchConfig[method.toLowerCase()], {
46 | body: JSON.stringify(data),
47 | }), success, error, complete);
48 | };
49 |
50 | const fetchDelete = function (uri, data, success, error, complete) {
51 | doFetch(uri, Object.assign(fetchConfig.delete, {
52 | body: JSON.stringify(data),
53 | }), success, error, complete);
54 | };
55 |
56 | export {
57 | fetchGet,
58 | fetchUpdate,
59 | fetchDelete,
60 | };
--------------------------------------------------------------------------------
/assets/js/helpers/functions.js:
--------------------------------------------------------------------------------
1 | function bindEvent(event, elements, fn) {
2 | // checks if 'elements' is a selector
3 | if (typeof elements === 'string') {
4 | elements = document.querySelectorAll(elements);
5 | }
6 |
7 | if (Node.prototype.isPrototypeOf(elements)) {
8 | elements.addEventListener(event, fn);
9 | }
10 |
11 | if (HTMLCollection.prototype.isPrototypeOf(elements)) {
12 | Array.from(elements).forEach(function (ele) {
13 | ele.addEventListener(event, fn);
14 | });
15 | }
16 |
17 | if (NodeList.prototype.isPrototypeOf(elements)) {
18 | elements.forEach(function (ele) {
19 | ele.addEventListener(event, fn);
20 | });
21 | }
22 | }
23 |
24 | function on(event, selector, fn) {
25 | if (typeof selector !== 'string') return;
26 | document.addEventListener(event, function (e) {
27 | const find = e.target.closest(selector);
28 | if (find && find.matches(selector)) {
29 | fn(e);
30 | }
31 | });
32 | }
33 |
34 | function getElement(selector, findIn) {
35 | return getElements(selector, findIn)[0];
36 | }
37 |
38 | function getElements(selector, findIn) {
39 | if (typeof selector !== 'string') return;
40 | return (findIn || document).querySelectorAll(selector) || false;
41 | }
42 |
43 | export {
44 | bindEvent,
45 | on,
46 | getElement,
47 | getElements,
48 | };
--------------------------------------------------------------------------------
/assets/js/helpers/loading.js:
--------------------------------------------------------------------------------
1 | const
2 | sidebarLoader = fmWrapper.querySelector('.sidebar .loader'),
3 | tableLoader = fmWrapper.querySelector('.table-section .loader');
4 |
5 | function showLoading() {
6 | fmWrapper.style.pointerEvents = 'none';
7 | sidebarLoader.classList.add('show');
8 | tableLoader.classList.add('show');
9 | }
10 |
11 | function hideLoading() {
12 | fmWrapper.style.pointerEvents = 'auto';
13 | sidebarLoader.classList.remove('show');
14 | tableLoader.classList.remove('show');
15 | }
16 |
17 | export {
18 | showLoading,
19 | hideLoading
20 | };
--------------------------------------------------------------------------------
/assets/js/helpers/modal.js:
--------------------------------------------------------------------------------
1 | import {getElement} from "./functions";
2 |
3 | var modal = function (modalId) {
4 | const
5 | ele = getElement(modalId),
6 | errorDiv = ele.querySelector('.alert.error');
7 |
8 | return {
9 |
10 | show: function () {
11 | errorDiv.classList.remove('show');
12 | ele.classList.add('show');
13 | getElement('.modal-overlay').classList.add('show');
14 |
15 | return this;
16 | },
17 |
18 | showError: function (err) {
19 | errorDiv.classList.add('show');
20 | errorDiv.textContent = err;
21 |
22 | // hide the error after 5 secs
23 | setTimeout(function () {
24 | errorDiv.classList.remove('show');
25 | }, 5000);
26 |
27 | return this;
28 | },
29 |
30 | close: function () {
31 | ele.classList.remove('show');
32 | getElement('.modal-overlay').classList.remove('show');
33 |
34 | return this;
35 | },
36 | };
37 | };
38 |
39 | export default modal;
--------------------------------------------------------------------------------
/assets/js/helpers/treeViewer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets sidebar selected path tree.
3 | * @return {string}
4 | */
5 | import {getElement} from "./functions";
6 |
7 | function getSelectedPath() {
8 | const element = document.querySelector('.sidebar .dir-item[data-open="true"]');
9 |
10 | if (!element) {
11 | return '/';
12 | }
13 |
14 | var
15 | child = element.querySelector('.dir-item[data-open="true"]'),
16 | path = decodeURI(element.dataset.name).slice(1);
17 |
18 | while (child) {
19 | path += '/' + decodeURI(child.dataset.name);
20 | child = child.querySelector('.dir-item[data-open="true"]');
21 | }
22 |
23 | return path + '/';
24 | }
25 | /**
26 | * Closes the sibling tree of the passed element
27 | */
28 | function closeSiblingTreeOf(element) {
29 | var
30 | result = [],
31 | node = element.parentNode.firstChild;
32 |
33 | while (node) {
34 | if (node !== element && node.nodeType === Node.ELEMENT_NODE) {
35 | result.push(node);
36 | }
37 |
38 | node = node.nextElementSibling || node.nextSibling;
39 | }
40 |
41 | result.forEach(function (item) {
42 | if (item.classList.contains('dir-item') && item.dataset.open === 'true') {
43 | item.dataset.open = 'false';
44 | getElement('.files-table tbody').textContent = '';
45 | item.querySelector('.sub-files').textContent = '';
46 |
47 | var child = item.querySelector('.dir-item[data-open="true"]');
48 |
49 | while (child) {
50 | child.dataset.open = 'false';
51 | child = item.querySelector('.dir-item[data-open="true"]');
52 | }
53 | }
54 | });
55 | }
56 |
57 | /**
58 | * Gets the element that will receive the next request files content
59 | * @param {string} path
60 | * @return {string}
61 | */
62 | function getAppendToSelector(path)
63 | {
64 | path = path.replace(/^\/|\/$/g, '');
65 |
66 | if (path !== '') {
67 | const name = path.split('/').pop(),
68 | chunks = path.split('/'),
69 | dir = chunks[chunks.length - 2];
70 |
71 | if (dir) {
72 | return `.sidebar .dir-item[data-name="${encodeURI(dir)}"] .dir-item[data-name="${encodeURI(name)}"] .sub-files`;
73 | }
74 |
75 | return `.sidebar .dir-item[data-name="${encodeURI(name)}"] .sub-files`;
76 | }
77 |
78 | getElement('.sidebar .files-list .dir-item[data-name="/"]').dataset.open = 'true';
79 | return '.sidebar .files-list .dir-item[data-name="/"] .sub-files';
80 | }
81 |
82 | export {
83 | getSelectedPath,
84 | closeSiblingTreeOf,
85 | getAppendToSelector
86 | };
--------------------------------------------------------------------------------
/assets/js/helpers/uploadRequest.js:
--------------------------------------------------------------------------------
1 | var uploadRequest = function (data) {
2 | return new Promise(function (resolve, reject) {
3 | const allowedMethods = ['POST', 'PUT'];
4 |
5 | if (allowedMethods.indexOf(data.method) === -1) {
6 | throw `${data.method} not allowed for an upload transfer.`;
7 | }
8 |
9 | var xhr = new XMLHttpRequest();
10 | xhr.open(data.method, data.url, true);
11 |
12 | xhr.setRequestHeader('Accept', 'application/json');
13 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
14 |
15 | // Success&complete
16 | xhr.addEventListener('load', function () {
17 | if (typeof data.complete === 'function') {
18 | data.complete(JSONResponse());
19 | }
20 | if (typeof data.success === 'function') {
21 | if (this.readyState === 4) {
22 | if (JSONResponse().location) {
23 | window.location = JSONResponse().location;
24 | }
25 | data.success(JSONResponse());
26 | resolve(JSONResponse());
27 | }
28 | }
29 | });
30 |
31 | // Error
32 | xhr.addEventListener('error', function () {
33 | if (typeof data.success === 'function') {
34 | data.failure();
35 | reject(this.response);
36 | }
37 | });
38 |
39 | // Progress
40 | xhr.upload.addEventListener('progress', function (e) {
41 | if (typeof data.progress === 'function') {
42 | data.progress(e);
43 | }
44 | });
45 |
46 | xhr.send(data.data);
47 |
48 | var JSONResponse = function () {
49 | return JSON.parse(xhr.response);
50 | };
51 | });
52 | };
53 |
54 | export default uploadRequest;
--------------------------------------------------------------------------------
/assets/js/main.js:
--------------------------------------------------------------------------------
1 | import App from "./app";
2 |
3 | const app = new App();
4 | app.registerEvents();
5 | app.init();
6 |
--------------------------------------------------------------------------------
/assets/js/state.js:
--------------------------------------------------------------------------------
1 | // Storing state variables
2 | var state = {
3 | path: ".",
4 | editableFile: null,
5 | files: [],
6 | uploadedFiles: [],
7 |
8 | getFileByName: function (name) {
9 | for (var prop in this.files) {
10 | if (this.files[prop].name === name) {
11 | return this.files[prop];
12 | }
13 | }
14 |
15 | return false;
16 | },
17 | };
18 |
19 | export default state;
20 |
--------------------------------------------------------------------------------
/assets/js/templates/includes/fileIcon.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return `
3 |
10 | `;
11 | };
--------------------------------------------------------------------------------
/assets/js/templates/includes/sidebarDirIcon.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return `
3 |
7 |
13 | `;
14 | };
--------------------------------------------------------------------------------
/assets/js/templates/includes/tableDirIcon.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return `
3 |
7 | `;
8 | };
--------------------------------------------------------------------------------
/assets/js/templates/moveModalDirItem.js:
--------------------------------------------------------------------------------
1 | import dirIcon from '../templates/includes/sidebarDirIcon';
2 |
3 | function moveModalDirItem(dir) {
4 | return `
5 |
6 | ${dirIcon()}
7 | ${dir.name}
8 |
9 |
10 | `;
11 | }
12 |
13 | export default moveModalDirItem;
--------------------------------------------------------------------------------
/assets/js/templates/sidebarFileItem.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {File} fileItem
3 | * @returns {string}
4 | */
5 | import dirIcon from "./includes/sidebarDirIcon";
6 | import fileIcon from "./includes/fileIcon";
7 |
8 | export default function (fileItem) {
9 | if (typeof fileItem !== 'object') return;
10 |
11 | return `
12 |
13 | ${fileItem.isDir() ? dirIcon() : fileIcon()}
14 | ${fileItem.name}
15 | ${fileItem.isDir() ? '' : ''}
16 | `;
17 | }
--------------------------------------------------------------------------------
/assets/js/templates/tableFileItem.js:
--------------------------------------------------------------------------------
1 | import fileIcon from "./includes/fileIcon";
2 | import dirIcon from "./includes/tableDirIcon";
3 |
4 | /**
5 | * @param {File} fileItem
6 | * @return {string}
7 | */
8 | export default function (fileItem) {
9 | if (typeof fileItem !== 'object') return;
10 |
11 | return `
12 |
13 |
14 |
18 | |
19 |
20 | ${fileItem.isFile() ? fileIcon() : dirIcon()}
21 | ${fileItem.name}
22 | |
23 | ${fileItem.isFile() ? fileItem.bytesToSize() : ''} |
24 | ${fileItem.modifiedTime} |
25 | ${fileItem.permissions} |
26 |
`;
27 | };
--------------------------------------------------------------------------------
/assets/js/templates/uploadModalFileItem.js:
--------------------------------------------------------------------------------
1 | function uploadModalFileItem(fileItem) {
2 | return `
3 |
4 | ${fileItem.name}
5 | ${fileItem.bytesToSize()}
6 |
9 |
10 |
Uploading ...
11 |
0%
12 |
16 |
17 |
18 |
19 |
20 |
21 | `;
22 | }
23 |
24 | export default uploadModalFileItem;
--------------------------------------------------------------------------------
/assets/scss/_global.scss:
--------------------------------------------------------------------------------
1 | *, *:before, *:after {
2 | font-family: Arvo, Helvetica, sans-serif;
3 | }
4 |
5 | button {
6 | text-transform: capitalize;
7 | }
8 |
9 | .homepage-wrapper, .login-wrapper, .error-container {
10 | position: fixed;
11 | width: 100%;
12 | height: 100%;
13 | }
14 |
15 | .homepage-wrapper, .login-wrapper {
16 | svg.shape-right,
17 | svg.shape-left {
18 | position: absolute;
19 | bottom: 0;
20 | z-index: -1;
21 | }
22 |
23 | svg.shape-left {
24 | left: 0;
25 | }
26 | svg.shape-right {
27 | right: 0;
28 | }
29 | }
--------------------------------------------------------------------------------
/assets/scss/base/_reset.scss:
--------------------------------------------------------------------------------
1 | *, *:after, *:before {
2 | box-sizing: border-box;
3 | }
4 |
5 | body, h1, h2, h3, h4, h5, h6, p, ol, ul {
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | ol, ul {
11 | list-style: none;
12 | }
13 |
14 | a {
15 | text-decoration: none;
16 | }
17 |
18 | button, input {
19 | &:focus {
20 | outline: none;
21 | }
22 | }
23 |
24 | input::placeholder {
25 | color: inherit;
26 | }
27 |
28 | table {
29 | border-collapse: collapse;
30 | }
31 |
--------------------------------------------------------------------------------
/assets/scss/base/_utilities.scss:
--------------------------------------------------------------------------------
1 | .list-inline li {
2 | display: inline-block;
3 | }
4 |
5 | .uppercase {
6 | text-transform: uppercase;
7 | }
--------------------------------------------------------------------------------
/assets/scss/components/_button.scss:
--------------------------------------------------------------------------------
1 | %btn {
2 | cursor: pointer;
3 | background-color: transparent;
4 | padding: 10px 24px;
5 | border-radius: 5px;
6 | border: 2px solid transparent;
7 | box-shadow: 0 2px 4px -2px #999;
8 |
9 | &:focus {
10 | border: 2px solid #23e8e8;
11 | }
12 | }
13 |
14 | .btn-primary {
15 | @extend %btn;
16 | background-color: $secondaryBlue;
17 | color: #FFF;
18 | @include btn-hover-color($secondaryBlue);
19 | }
20 |
21 | .btn-secondary {
22 | @extend %btn;
23 | background-color: $thirdLightBrown;
24 | color: $secondaryBlue;
25 | @include btn-hover-color($thirdLightBrown);
26 | }
27 |
28 | .btn-icon-svg {
29 | position: relative;
30 | padding-top: 10px;
31 | padding-bottom: 10px;
32 | padding-right: 24px;
33 | padding-left: 36px;
34 |
35 | svg {
36 | @include center-vertically('absolute');
37 | vertical-align: middle;
38 | left: 10px;
39 | }
40 | }
--------------------------------------------------------------------------------
/assets/scss/components/_checkbox.scss:
--------------------------------------------------------------------------------
1 | .checkbox {
2 | position: relative;
3 | vertical-align: middle;
4 | cursor: pointer;
5 |
6 | input[type='checkbox'], .checkbox-text {
7 | vertical-align: middle;
8 | }
9 |
10 | input[type='checkbox'] {
11 | display: none;
12 |
13 | &:checked + .checkbox-text:after {
14 | content: '\002714';
15 | color: #FFF;
16 | position: absolute;
17 | left: 2px;
18 | top: 0;
19 | }
20 |
21 | &:checked + .checkbox-text:before {
22 | background-color: $primaryRed;
23 | border: .05em solid $primaryRed;
24 | transition: .3s;
25 | }
26 | }
27 |
28 | .checkbox-text {
29 | color: $textBlack;
30 |
31 | &:before {
32 | content: '';
33 | position: relative;
34 | display: inline-block;
35 | top: 2px;
36 | width: 1em;
37 | height: 1em;
38 | margin-right: .5em;
39 | border: .05em solid #ddd;
40 | border-radius: .2em;
41 | box-shadow: 0 0 2px 0 #EEE;
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/assets/scss/components/_input.scss:
--------------------------------------------------------------------------------
1 | $inputBackground: #FFF5E7;
2 |
3 | .input-area {
4 | input {
5 | width: 100%;
6 | padding: 12px 8px;
7 | border-radius: 6px;
8 | border: 2px solid $secondaryBlue;
9 | background-color: $inputBackground;
10 | color: $textBlack;
11 | }
12 | }
--------------------------------------------------------------------------------
/assets/scss/pages/_error.scss:
--------------------------------------------------------------------------------
1 | .error-container {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | background-color: $primaryRed;
6 | color: #FFF;
7 | font-size: 10rem;
8 | text-transform: uppercase;
9 | user-select: none;
10 |
11 | .error-code {
12 | margin-left: 3rem;
13 | }
14 | }
--------------------------------------------------------------------------------
/assets/scss/pages/_homepage.scss:
--------------------------------------------------------------------------------
1 | .homepage-wrapper {
2 | .overlay {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | bottom: 0;
7 | right: 0;
8 | z-index: 999;
9 | background-color: rgba(#FFF, .95);
10 | transition: 1.5s;
11 | visibility: visible;
12 | opacity: 1;
13 |
14 | &.hide {
15 | visibility: hidden;
16 | opacity: 0;
17 | }
18 | }
19 |
20 | header.header {
21 | background-color: $primaryRed;
22 | color: #FFF;
23 | height: 70px;
24 | display: flex;
25 | justify-content: space-between;
26 | align-items: center;
27 | box-shadow: 0 4px 2px -2px #999;
28 |
29 | .brand {
30 | background-color: $secondaryBlue;
31 | height: 100%;
32 | padding: 0 25px;
33 |
34 | h3 {
35 | @include center-vertically();
36 | font-size: 2rem;
37 | }
38 | }
39 |
40 | nav.actions {
41 | padding-right: 30px;
42 |
43 | ul {
44 | li:not(:last-of-type) {
45 | margin-right: 15px;
46 | }
47 | }
48 | }
49 | }
50 |
51 | section.intro {
52 | position: relative;
53 | height: calc(100% - 70px);
54 |
55 | .row {
56 | position: absolute;
57 | display: flex;
58 | justify-content: space-around;
59 | width: 85%;
60 | top: 50%;
61 | left: 50%;
62 | transform: translate(-50%, -45%);
63 |
64 | .app-desc {
65 | color: $textBlack;
66 | padding-top: 25px;
67 |
68 | .title {
69 | text-transform: uppercase;
70 | font-size: 2.8rem;
71 | margin-bottom: 50px;
72 | }
73 |
74 | .desc {
75 | font-size: 1.4rem;
76 | line-height: 1.6;
77 | }
78 |
79 | ul.actions {
80 | margin-top: 50px;
81 |
82 | li:not(:last-of-type) {
83 | margin-right: 15px;
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/assets/scss/pages/_login.scss:
--------------------------------------------------------------------------------
1 | .login-wrapper {
2 | background-color: $primaryRed;
3 |
4 | a.shape-arrow {
5 | position: absolute;
6 | left: 25px;
7 | bottom: 25px;
8 | }
9 |
10 | header.header {
11 | padding-top: 30px;
12 | padding-left: 30px;
13 | user-select: none;
14 |
15 | .brand {
16 | .name {
17 | font-size: 2.5rem;
18 | color: #FFF;
19 | }
20 | }
21 | }
22 |
23 | form.login-form {
24 | position: absolute;
25 | top: 50%;
26 | left: 50%;
27 | transform: translate(-50%, -50%);
28 | width: 400px;
29 | padding: 40px 15px 15px 15px;
30 | border-radius: 15px;
31 | box-shadow: 0 0 10px 0 #333;
32 | background-color: #FFF;
33 |
34 | .input-area {
35 | margin-bottom: 25px;
36 | }
37 |
38 | .checkbox {
39 | display: block;
40 | margin-bottom: 10px;
41 | }
42 |
43 | .server-error {
44 | color: $primaryRed;
45 | font-size: .8rem;
46 | }
47 |
48 | .form-actions {
49 | margin-top: 25px;
50 |
51 | input[type="submit"] {
52 | display: block;
53 | margin: 0 auto;
54 | }
55 | }
56 | }
57 |
58 | .github-btn {
59 | position: absolute;
60 | bottom: 15px;
61 | left: 50%;
62 | transform: translateX(-50%);
63 | padding: 0;
64 | height: 40px;
65 | width: 40px;
66 | border-radius: 50%;
67 |
68 | svg {
69 | vertical-align: middle;
70 | }
71 |
72 | &:hover {
73 | background-color: #FFF;
74 |
75 | svg path {
76 | fill: $primaryRed;
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/assets/scss/style.scss:
--------------------------------------------------------------------------------
1 | // Base
2 | @import "base/reset";
3 | @import "base/utilities";
4 |
5 | // Variables
6 | @import "variables/variables";
7 | @import "variables/mixins";
8 |
9 | // Components
10 | @import "components/button";
11 | @import "components/checkbox";
12 | @import "components/input";
13 |
14 | // Pages
15 | @import "pages/error";
16 | @import "pages/homepage";
17 | @import "pages/login";
18 |
19 | // Global
20 | @import "global";
21 |
--------------------------------------------------------------------------------
/assets/scss/variables/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin center-vertically($position: 'relative') {
2 | position: #{$position};
3 | top: 50%;
4 | transform: translateY(-50%);
5 | }
6 |
7 | @mixin set-font($font) {
8 | font-family: #{$font}, Helvetica, sans-serif;
9 | }
10 |
11 | @mixin btn-hover-color($color, $amount: 7%) {
12 | &:hover {
13 | background-color: lighten($color, $amount);
14 | }
15 | }
--------------------------------------------------------------------------------
/assets/scss/variables/_variables.scss:
--------------------------------------------------------------------------------
1 | // Fonts
2 | @import url('https://fonts.googleapis.com/css2?family=Arvo:wght@400;700&display=swap');
3 |
4 | // Colors
5 | $primaryRed: #EE3B5C;
6 | $secondaryBlue: #364F6B;
7 | $thirdLightBrown: #F3DCB7;
8 |
9 | $textBlack: #58595B;
--------------------------------------------------------------------------------
/bootstrap/bootstrap.php:
--------------------------------------------------------------------------------
1 | pushHandler(new PrettyPageHandler);
22 |
23 | $configFile = dirname(__DIR__) . '/config/app.php';
24 | if (!file_exists($configFile) || !is_readable($configFile)) {
25 | $whoops->handleException(new \RuntimeException("The application config file is missing or isn't readable."));
26 | }
27 |
28 | $config = include($configFile);
29 |
30 | if ($config['debug']) {
31 | $handler = new ErrorHandler(function ($ex) use($whoops) {
32 | $whoops->handleException($ex);
33 | });
34 | } elseif (!$config['debug']) {
35 | $handler = new ErrorHandler(function ($ex) use($whoops) {
36 | // Build the message string
37 | $message = sprintf(
38 | "[%s] [%s] [%s] [Line : %s]\n%s\n",
39 | date('Y:m:d h:m:s'),
40 | $ex->getMessage(),
41 | $ex->getFile(),
42 | $ex->getLine(),
43 | $ex->getTraceAsString()
44 | );
45 | // logs the generated error info to a custom file
46 | error_log($message, 3, dirname(__DIR__) . '/storage/logs/'.date('Y-m-d').'.log');
47 | // send a friendly message to the user
48 | $whoops->handleException(new Exception("Oops! Something goes wrong."));
49 | });
50 | }
51 |
52 | $handler->start();
53 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ftp-filemanager/ftp-filemanager",
3 | "description": "A web-based FTP client application to manage your FTP files, built with a simple MVC architecture, no frameworks or libraries are used (except my owns)",
4 | "type": "project",
5 | "license": "MIT",
6 | "keywords": ["ftp-filemanage", "no-framework", "mvc-pattern"],
7 | "authors": [
8 | {
9 | "name": "El Amrani Shakir",
10 | "email": "elamrani.sv.laza@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "php": ">=5.6.0",
15 | "ext-ftp": "*",
16 | "lazzard/php-ftp-client": "^1.3",
17 | "filp/whoops": "^2.12"
18 | },
19 | "autoload": {
20 | "psr-4": {
21 | "FTPApp\\": ["src/", "lib/"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "1e86ee1c1fb3836e8c07a72bafb40a59",
8 | "packages": [
9 | {
10 | "name": "filp/whoops",
11 | "version": "2.12.1",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/filp/whoops.git",
15 | "reference": "c13c0be93cff50f88bbd70827d993026821914dd"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/filp/whoops/zipball/c13c0be93cff50f88bbd70827d993026821914dd",
20 | "reference": "c13c0be93cff50f88bbd70827d993026821914dd",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": "^5.5.9 || ^7.0 || ^8.0",
25 | "psr/log": "^1.0.1"
26 | },
27 | "require-dev": {
28 | "mockery/mockery": "^0.9 || ^1.0",
29 | "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
30 | "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
31 | },
32 | "suggest": {
33 | "symfony/var-dumper": "Pretty print complex values better with var-dumper available",
34 | "whoops/soap": "Formats errors as SOAP responses"
35 | },
36 | "type": "library",
37 | "extra": {
38 | "branch-alias": {
39 | "dev-master": "2.7-dev"
40 | }
41 | },
42 | "autoload": {
43 | "psr-4": {
44 | "Whoops\\": "src/Whoops/"
45 | }
46 | },
47 | "notification-url": "https://packagist.org/downloads/",
48 | "license": [
49 | "MIT"
50 | ],
51 | "authors": [
52 | {
53 | "name": "Filipe Dobreira",
54 | "homepage": "https://github.com/filp",
55 | "role": "Developer"
56 | }
57 | ],
58 | "description": "php error handling for cool kids",
59 | "homepage": "https://filp.github.io/whoops/",
60 | "keywords": [
61 | "error",
62 | "exception",
63 | "handling",
64 | "library",
65 | "throwable",
66 | "whoops"
67 | ],
68 | "support": {
69 | "issues": "https://github.com/filp/whoops/issues",
70 | "source": "https://github.com/filp/whoops/tree/2.12.1"
71 | },
72 | "funding": [
73 | {
74 | "url": "https://github.com/denis-sokolov",
75 | "type": "github"
76 | }
77 | ],
78 | "time": "2021-04-25T12:00:00+00:00"
79 | },
80 | {
81 | "name": "lazzard/php-ftp-client",
82 | "version": "v1.3.3",
83 | "source": {
84 | "type": "git",
85 | "url": "https://github.com/lazzard/php-ftp-client.git",
86 | "reference": "37ba41d40fd5019bc2bb3cc08ce4cb88f4cbdb35"
87 | },
88 | "dist": {
89 | "type": "zip",
90 | "url": "https://api.github.com/repos/lazzard/php-ftp-client/zipball/37ba41d40fd5019bc2bb3cc08ce4cb88f4cbdb35",
91 | "reference": "37ba41d40fd5019bc2bb3cc08ce4cb88f4cbdb35",
92 | "shasum": ""
93 | },
94 | "require": {
95 | "ext-ftp": "*",
96 | "php": ">=5.6.0"
97 | },
98 | "require-dev": {
99 | "phpunit/phpunit": "^5"
100 | },
101 | "type": "library",
102 | "autoload": {
103 | "psr-4": {
104 | "Lazzard\\FtpClient\\": "src/"
105 | }
106 | },
107 | "notification-url": "https://packagist.org/downloads/",
108 | "license": [
109 | "MIT"
110 | ],
111 | "authors": [
112 | {
113 | "name": "El Amrani Shaker",
114 | "email": "elamrani.sv.laza@gmail.com",
115 | "homepage": "https://github.com/AmraniCh"
116 | }
117 | ],
118 | "description": "A library that wraps the PHP FTP functions in an OOP way.",
119 | "keywords": [
120 | "ftp-client",
121 | "ftp-library",
122 | "php",
123 | "php-ftp-client"
124 | ],
125 | "support": {
126 | "issues": "https://github.com/lazzard/php-ftp-client/issues",
127 | "source": "https://github.com/lazzard/php-ftp-client/tree/v1.3.3"
128 | },
129 | "time": "2021-05-02T23:18:19+00:00"
130 | },
131 | {
132 | "name": "psr/log",
133 | "version": "1.1.4",
134 | "source": {
135 | "type": "git",
136 | "url": "https://github.com/php-fig/log.git",
137 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
138 | },
139 | "dist": {
140 | "type": "zip",
141 | "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
142 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
143 | "shasum": ""
144 | },
145 | "require": {
146 | "php": ">=5.3.0"
147 | },
148 | "type": "library",
149 | "extra": {
150 | "branch-alias": {
151 | "dev-master": "1.1.x-dev"
152 | }
153 | },
154 | "autoload": {
155 | "psr-4": {
156 | "Psr\\Log\\": "Psr/Log/"
157 | }
158 | },
159 | "notification-url": "https://packagist.org/downloads/",
160 | "license": [
161 | "MIT"
162 | ],
163 | "authors": [
164 | {
165 | "name": "PHP-FIG",
166 | "homepage": "https://www.php-fig.org/"
167 | }
168 | ],
169 | "description": "Common interface for logging libraries",
170 | "homepage": "https://github.com/php-fig/log",
171 | "keywords": [
172 | "log",
173 | "psr",
174 | "psr-3"
175 | ],
176 | "support": {
177 | "source": "https://github.com/php-fig/log/tree/1.1.4"
178 | },
179 | "time": "2021-05-03T11:20:27+00:00"
180 | }
181 | ],
182 | "packages-dev": [],
183 | "aliases": [],
184 | "minimum-stability": "stable",
185 | "stability-flags": [],
186 | "prefer-stable": false,
187 | "prefer-lowest": false,
188 | "platform": {
189 | "php": ">=5.6.0",
190 | "ext-ftp": "*"
191 | },
192 | "platform-dev": [],
193 | "plugin-api-version": "2.0.0"
194 | }
195 |
--------------------------------------------------------------------------------
/config/app.php:
--------------------------------------------------------------------------------
1 | 'FTP FileManager',
6 |
7 | /**
8 | * If the debug mode enabled all exception and errors will be shows to
9 | * the end user, if disabled the errors will be logged to the logs
10 | * file.
11 | */
12 | 'debug' => true,
13 |
14 | /**
15 | * The inactivity timeout in minutes.
16 | */
17 | 'inactivityTimeout' => 100,
18 |
19 | /**
20 | * Ftp configs.
21 | */
22 | 'ftp' => [
23 |
24 | /**
25 | * Specify the FTP connection timeout in seconds.
26 | */
27 | 'timeout' => 90,
28 |
29 | /**
30 | * Enable auto seeking for transfer resuming operations.
31 | */
32 | 'autoSeek' => true,
33 |
34 | /**
35 | * Specifies whether to resume the upload operations in the server or start
36 | * the uploading from the beginning.
37 | *
38 | * note: in order for this option to work properly the autoSeek option must be enabled.
39 | */
40 | 'resumeUpload' => true,
41 | ],
42 | ];
--------------------------------------------------------------------------------
/config/routes.php:
--------------------------------------------------------------------------------
1 | new RouteUrlGenerator(new RouteCollection(include(dirname(__FILE__) . '/routes.php'))),
13 | 'Renderer' => new Renderer(dirname(__DIR__) . '/src/views'),
14 | 'Session' => new Session(include(dirname(__FILE__) . '/session.php')),
15 | 'SessionStorage' => new SessionStorage(),
16 | 'FtpAdapter' => new FtpClientAdapter(),
17 |
18 | ];
19 |
--------------------------------------------------------------------------------
/config/session.php:
--------------------------------------------------------------------------------
1 | 'FTPAPPSESSID',
9 |
10 | /**
11 | * If enabled the session cookie wil be accessible only
12 | * via the Http protocol, this will prevent javascript
13 | * from accessing the session cookie.
14 | */
15 | 'cookie_httponly' => true,
16 |
17 | /**
18 | * Specifies the session cookie life time in seconds.
19 | */
20 | 'cookie_lifetime' => 60 * 15, // 15 minutes
21 |
22 | /**
23 | * The session cookie path.
24 | */
25 | 'cookie_path' => '/',
26 |
27 | /**
28 | * Specifies whether to use cookies in order to store session id
29 | * in the browser.
30 | */
31 | 'use_cookies' => true,
32 |
33 | /**
34 | * If enabled the session sent with URLs will not accepted.
35 | */
36 | 'use_only_cookies' => true,
37 |
38 | /**
39 | * Disable or enable the URL based session management.
40 | */
41 | 'use_trans_sid' => false,
42 |
43 | /**
44 | * Specifies where the session files will be stored.
45 | */
46 | 'save_path' => dirname(__DIR__) . '/storage/sessions',
47 |
48 | /**
49 | * Whether to send the session cookie only via a secure connection
50 | * like when using HTTPS protocol.
51 | */
52 | 'cookie_secure' => false,
53 |
54 | /**
55 | * If enabled the uninitialized session ids by the session module will be discarded.
56 | */
57 | 'use_strict_mode' => true,
58 | ];
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var
2 | gulp = require('gulp'),
3 | rename = require('gulp-rename'),
4 | prefixer = require('gulp-autoprefixer'),
5 | sass = require('gulp-sass'),
6 | sourcemaps = require('gulp-sourcemaps'),
7 | livereload = require('gulp-livereload'),
8 | uglify = require('gulp-uglify'),
9 | notify = require('gulp-notify'),
10 | plumber = require('gulp-plumber'),
11 | browserify = require('browserify'),
12 | babelify = require('babelify'),
13 | source = require('vinyl-source-stream'),
14 | buffer = require('vinyl-buffer');
15 |
16 | var onError = function (err) {
17 | notify.onError({
18 | title: "Gulp",
19 | subtitle: "Failure!",
20 | message: "Error: <%= error.message %>",
21 | sound: "Beep"
22 | })(err);
23 | this.emit('end');
24 | };
25 |
26 | gulp.task('bundle', function () {
27 | return browserify({
28 | entries: ['./assets/js/main.js']
29 | })
30 | .transform(babelify, { 'presets': ['env'] })
31 | .bundle()
32 | .pipe(plumber({errorHandler: onError}))
33 | .pipe(source('app.js'))
34 | .pipe(rename({ suffix: '.min' }))
35 | .pipe(buffer())
36 | .pipe(uglify())
37 | .pipe(gulp.dest('./public/dist/'))
38 | });
39 |
40 | gulp.task('styles', function () {
41 | return gulp.src('assets/scss/style.scss')
42 | .pipe(plumber({errorHandler: onError}))
43 | .pipe(sourcemaps.init())
44 | .pipe(sass({
45 | outputStyle: 'compressed'
46 | }))
47 | .pipe(prefixer([
48 | 'last 2 versions',
49 | '> 0.2%',
50 | 'ie >= 9'
51 | ]))
52 | .pipe(sourcemaps.write())
53 | .pipe(rename({suffix: '.min'}))
54 | .pipe(gulp.dest('public/dist'))
55 | .pipe(livereload())
56 | });
57 |
58 | gulp.task('watch', function () {
59 | livereload.listen();
60 | gulp.watch('assets/js/**/*.js', ['bundle']);
61 | gulp.watch('assets/scss/**/*.scss', ['styles']);
62 | });
63 |
64 | gulp.task('default', ['watch']);
--------------------------------------------------------------------------------
/lib/DIC/DIC.php:
--------------------------------------------------------------------------------
1 | definitions = $definitions;
20 | }
21 |
22 | /**
23 | * @return array
24 | */
25 | public function getDefinitions()
26 | {
27 | return $this->definitions;
28 | }
29 |
30 | /**
31 | * @param array $definitions
32 | */
33 | public function setDefinitions($definitions)
34 | {
35 | $this->definitions = $definitions;
36 | }
37 |
38 | /**
39 | * @param string $name
40 | * @param string $callback
41 | *
42 | * @return void
43 | */
44 | public function set($name, $callback)
45 | {
46 | $this->definitions[$name] = $callback();
47 | }
48 |
49 | /**
50 | * @param string $name
51 | * @param string $callback
52 | *
53 | * @return void
54 | */
55 | public function factory($name, $callback)
56 | {
57 | $this->definitions[$name] = $callback;
58 | }
59 |
60 | /**
61 | * @param string $name
62 | *
63 | * @return object
64 | */
65 | public function get($name)
66 | {
67 | if (is_callable($this->definitions[$name])) {
68 | return $this->definitions[$name]();
69 | }
70 |
71 | return $this->definitions[$name];
72 | }
73 |
74 | /**
75 | * @param string $name
76 | *
77 | * @return bool
78 | */
79 | public function has($name)
80 | {
81 | return array_key_exists($name, $this->definitions);
82 | }
83 | }
--------------------------------------------------------------------------------
/lib/ErrorHandling/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | handler = $handler;
22 | }
23 | }
24 |
25 | public function start()
26 | {
27 | $this->setExceptionHandler($this->handler);
28 | $this->setErrorHandler();
29 | }
30 |
31 | public function restore()
32 | {
33 | restore_exception_handler();
34 | restore_error_handler();
35 | }
36 |
37 | protected function setExceptionHandler()
38 | {
39 | set_exception_handler(function ($ex) {
40 | http_response_code(500);
41 | call_user_func_array($this->handler, [$ex]);
42 | });
43 | }
44 |
45 | protected function setErrorHandler()
46 | {
47 | set_error_handler(function ($errno, $errstr, $errfile, $errline) {
48 | if (ini_get('error_reporting') == 0) {
49 | trigger_error($errstr);
50 | return;
51 | }
52 |
53 | throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/Http/CookiesFactory.php:
--------------------------------------------------------------------------------
1 | registry = func_get_args();
17 | }
18 |
19 | public function factory()
20 | {
21 | $cookies = [];
22 |
23 | if (!empty($this->registry)) {
24 | foreach ($this->registry as $cookiesArray) {
25 | $cookies[] = new HttpCookie(
26 | $cookiesArray['name'],
27 | $cookiesArray['value'],
28 | isset($cookiesArray['expire']) ? $cookiesArray['expire'] : HttpCookie::EXPIRE_SESSION,
29 | isset($cookiesArray['path']) ? $cookiesArray['path'] : HttpCookie::CURRENT_PATH,
30 | isset($cookiesArray['domain']) ? $cookiesArray['domain'] : HttpCookie::WHOLE_DOMAIN,
31 | isset($cookiesArray['secure']) ? $cookiesArray['secure'] : false,
32 | isset($cookiesArray['httpOnly']) ? $cookiesArray['httpOnly'] : false,
33 | isset($cookiesArray['sameSite']) ? $cookiesArray['sameSite'] : HttpCookie::SAMESITE_NONE
34 | );
35 | }
36 | }
37 |
38 | return $cookies;
39 | }
40 | }
--------------------------------------------------------------------------------
/lib/Http/Exception/HttpInvalidArgumentException.php:
--------------------------------------------------------------------------------
1 | name = $name;
65 | $this->value = $value;
66 | $this->expire = $expire;
67 | $this->path = $path;
68 | $this->domain = $domain;
69 | $this->secure = $secure;
70 | $this->httpOnly = $httpOnly;
71 | $this->sameSite = $sameSite;
72 |
73 | if (!setcookie($name, $value, $expire, $path, $domain, $secure, $sameSite)) {
74 | throw new HttpRuntimeException("Cannot create cookie with name $name");
75 | }
76 | }
77 |
78 | /**
79 | * @param string $name
80 | */
81 | public function setName($name)
82 | {
83 | $this->name = $name;
84 | }
85 |
86 | /**
87 | * @param string $value
88 | */
89 | public function setValue($value)
90 | {
91 | $this->value = $value;
92 | }
93 |
94 | /**
95 | * @param int $expire
96 | */
97 | public function setExpire($expire)
98 | {
99 | $this->expire = $expire;
100 | }
101 |
102 | /**
103 | * @param string $path
104 | */
105 | public function setPath($path)
106 | {
107 | $this->path = $path;
108 | }
109 |
110 | /**
111 | * @param string $domain
112 | */
113 | public function setDomain($domain)
114 | {
115 | $this->domain = $domain;
116 | }
117 |
118 | /**
119 | * @param bool $secure
120 | */
121 | public function setSecure($secure)
122 | {
123 | $this->secure = $secure;
124 | }
125 |
126 | /**
127 | * @param bool $httpOnly
128 | */
129 | public function setHttpOnly($httpOnly)
130 | {
131 | $this->httpOnly = $httpOnly;
132 | }
133 |
134 | /**
135 | * @param string $sameSite
136 | */
137 | public function setSameSite($sameSite)
138 | {
139 | $this->sameSite = $sameSite;
140 | }
141 |
142 | /**
143 | * @return string
144 | */
145 | public function getName()
146 | {
147 | return $this->name;
148 | }
149 |
150 | /**
151 | * @return string
152 | */
153 | public function getValue()
154 | {
155 | return $this->value;
156 | }
157 |
158 | /**
159 | * @return int
160 | */
161 | public function getExpire()
162 | {
163 | return $this->expire;
164 | }
165 |
166 | /**
167 | * @return string
168 | */
169 | public function getPath()
170 | {
171 | return $this->path;
172 | }
173 |
174 | /**
175 | * @return string
176 | */
177 | public function getDomain()
178 | {
179 | return $this->domain;
180 | }
181 |
182 | /**
183 | * @return bool
184 | */
185 | public function isSecure()
186 | {
187 | return $this->secure;
188 | }
189 |
190 | /**
191 | * @return bool
192 | */
193 | public function isHttpOnly()
194 | {
195 | return $this->httpOnly;
196 | }
197 |
198 | /**
199 | * @return string
200 | */
201 | public function getSameSite()
202 | {
203 | return $this->sameSite;
204 | }
205 | }
--------------------------------------------------------------------------------
/lib/Http/HttpRedirect.php:
--------------------------------------------------------------------------------
1 | addHeader('Location', $uri);
21 | }
22 | }
--------------------------------------------------------------------------------
/lib/Http/HttpRequest.php:
--------------------------------------------------------------------------------
1 | get = $_GET;
37 | $this->post = $_POST;
38 | $this->files = $_FILES;
39 | $this->cookies = $_COOKIE;
40 | $this->headers = getallheaders() ?: [];
41 | $this->server = $_SERVER;
42 | }
43 |
44 | /**
45 | * @return array
46 | */
47 | public function getQueryParameters()
48 | {
49 | return $this->get;
50 | }
51 |
52 | /**
53 | * @return array
54 | */
55 | public function getBodyParameters()
56 | {
57 | return $this->post;
58 | }
59 |
60 | /**
61 | * @return array
62 | */
63 | public function getFiles()
64 | {
65 | return $this->files;
66 | }
67 |
68 | /**
69 | * @return string
70 | */
71 | public function getMethod()
72 | {
73 | return $this->server['REQUEST_METHOD'];
74 | }
75 |
76 | /**
77 | * @return string|null
78 | */
79 | public function getQueryString()
80 | {
81 | if (isset($this->server['QUERY_STRING'])) {
82 | return $this->server['QUERY_STRING'];
83 | }
84 |
85 | return null;
86 | }
87 |
88 | /**
89 | * @return array
90 | */
91 | public function getParameters()
92 | {
93 | return array_merge($this->get, $this->post);
94 | }
95 |
96 | /**
97 | * @param string $name
98 | *
99 | * @return string|false
100 | */
101 | public function getParameter($name)
102 | {
103 | if (array_key_exists($name, $this->getParameters())) {
104 | return $this->getParameters()[$name];
105 | }
106 |
107 | return false;
108 | }
109 |
110 | public function getJSONBodyParameters()
111 | {
112 | return json_decode(file_get_contents('php://input'), true);
113 | }
114 |
115 | /**
116 | * @return array
117 | */
118 | public function getCookies()
119 | {
120 | return $this->cookies;
121 | }
122 |
123 | /**
124 | * @return array
125 | */
126 | public function getHeaders()
127 | {
128 | return $this->headers;
129 | }
130 |
131 | /**
132 | * @return array
133 | */
134 | public function getServer()
135 | {
136 | return $this->server;
137 | }
138 |
139 | /**
140 | * @param string $header
141 | *
142 | * @return bool
143 | */
144 | public function hasHeader($header)
145 | {
146 | return array_key_exists(ucfirst(strtolower($header)), $this->headers);
147 | }
148 |
149 | /**
150 | * @return bool
151 | */
152 | public function isAjaxRequest()
153 | {
154 | if (array_key_exists('X-Requested-With', $this->headers)) {
155 | return $this->headers['X-Requested-With'] === 'XMLHttpRequest';
156 | }
157 |
158 | return false;
159 | }
160 |
161 | public function getUri()
162 | {
163 | return $this->server['REQUEST_URI'];
164 | }
165 |
166 |
167 | }
168 |
--------------------------------------------------------------------------------
/lib/Http/HttpResponse.php:
--------------------------------------------------------------------------------
1 | statusCode = $statusCode;
34 | $this->content = $content;
35 | $this->headers = $headers;
36 | }
37 |
38 | /**
39 | * @return int
40 | */
41 | public function getStatusCode()
42 | {
43 | return $this->statusCode;
44 | }
45 |
46 | /**
47 | * @param int $statusCode
48 | *
49 | * @return $this
50 | */
51 | public function setStatusCode($statusCode)
52 | {
53 | $this->statusCode = $statusCode;
54 |
55 | return $this;
56 | }
57 |
58 | /**
59 | * @return mixed
60 | */
61 | public function getContent()
62 | {
63 | return $this->content;
64 | }
65 |
66 | /**
67 | * @param mixed $content
68 | *
69 | * @return $this
70 | */
71 | public function setContent($content)
72 | {
73 | $this->content = $content;
74 |
75 | return $this;
76 | }
77 |
78 | /**
79 | * @param string $name
80 | * @param string $value
81 | *
82 | * @return $this
83 | * @throws HttpInvalidArgumentException
84 | *
85 | */
86 | public function addHeader($name, $value)
87 | {
88 | if (array_key_exists($name, $this->headers)) {
89 | throw new HttpInvalidArgumentException("Cannot add Http header [$name], it already exists.");
90 | }
91 |
92 | $this->headers[$name] = $value;
93 |
94 | return $this;
95 | }
96 |
97 | /**
98 | * @param string $name
99 | * @param string $value
100 | *
101 | * @return $this
102 | * @throws HttpInvalidArgumentException
103 | *
104 | */
105 | public function setHeader($name, $value)
106 | {
107 | if (!array_key_exists($name, $this->headers)) {
108 | throw new HttpInvalidArgumentException("Http header [$name] doesn't exists to overwrite their value.");
109 | }
110 |
111 | $this->headers[$name] = $value;
112 |
113 | return $this;
114 | }
115 |
116 | /**
117 | * @return $this
118 | */
119 | public function send()
120 | {
121 | $this->sendContent($this->content);
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * @return $this
128 | */
129 | public function sendJSON()
130 | {
131 | $this->sendContent(json_encode($this->content));
132 |
133 | return $this;
134 | }
135 |
136 | /**
137 | * @return $this
138 | */
139 | public function removeXPoweredByHeader()
140 | {
141 | if (!headers_sent() && $this->hasHeader('X-Powered-By')) {
142 | header_remove('X-Powered-By');
143 | }
144 |
145 | return $this;
146 | }
147 |
148 | /**
149 | * @param string $name
150 | *
151 | * @return $this
152 | */
153 | public function removeHeader($name)
154 | {
155 | if (!array_key_exists($name, $this->getResponseHeaders())) {
156 | throw new HttpInvalidArgumentException("Http header [$name] doesn't exists to remove.");
157 | }
158 |
159 | header_remove($name);
160 |
161 | return $this;
162 | }
163 |
164 | /**
165 | * Gets all headers that's will be send with the response.
166 | *
167 | * @return array
168 | */
169 | public function getResponseHeaders()
170 | {
171 | return array_merge($this->headers, $this->getReadyHeaders());
172 | }
173 |
174 | /**
175 | * @param string $name
176 | *
177 | * @return bool
178 | */
179 | public function hasHeader($name)
180 | {
181 | return array_key_exists($name, $this->getResponseHeaders());
182 | }
183 |
184 | /**
185 | * @return $this
186 | */
187 | public function cleanContent()
188 | {
189 | if (ob_get_contents()) {
190 | ob_clean();
191 | }
192 |
193 | return $this;
194 | }
195 |
196 | /**
197 | * Clears all ready headers.
198 | *
199 | * @return $this
200 | */
201 | public function clearReadyHeaders()
202 | {
203 | if (headers_sent() || empty(headers_list())) {
204 | return $this;
205 | }
206 |
207 | foreach ($this->getReadyHeaders() as $name => $value) {
208 | header_remove($name);
209 | }
210 |
211 | return $this;
212 | }
213 |
214 | /**
215 | * Clear all headers that's not sent yet.
216 | */
217 | public function clearAllHeaders()
218 | {
219 | $this->headers = [];
220 | }
221 |
222 | /**
223 | * @param array $cookies
224 | *
225 | * @return $this
226 | */
227 | public function withCookies($cookies)
228 | {
229 | if (!is_array($cookies)) {
230 | throw new HttpInvalidArgumentException(
231 | sprintf("An array must be passed to %s, %s given.",
232 | __METHOD__,
233 | gettype($cookies)
234 | ));
235 | }
236 |
237 | foreach ($cookies as $cookie) {
238 | if ($cookie instanceof HttpCookie) {
239 | $this->cookies[] = $cookie;
240 | }
241 | }
242 |
243 | return $this;
244 | }
245 |
246 | protected function sendCookies()
247 | {
248 | if (empty($this->cookies)) {
249 | return;
250 | }
251 |
252 | /** @var HttpCookie $cookie */
253 | foreach ($this->cookies as $cookie) {
254 | setcookie(
255 | $cookie->getName(),
256 | $cookie->getValue(),
257 | $cookie->getExpire(),
258 | $cookie->getPath(),
259 | $cookie->getDomain(),
260 | $cookie->isSecure(),
261 | $cookie->isHttpOnly()
262 | );
263 | // Same site
264 | if ($cookie->getSameSite() !== HttpCookie::SAMESITE_NONE) {
265 | header(
266 | sprintf(
267 | 'Set-cookie: %s=%s;samesite=%s',
268 | $cookie->getName(),
269 | $cookie->getValue(),
270 | $cookie->getSameSite()
271 | ),
272 | false, // Disable replacing
273 | null
274 | );
275 | }
276 | }
277 | }
278 |
279 | /**
280 | * Sends all Http headers if not already sent.
281 | *
282 | * @return void
283 | */
284 | protected function sendRawHeaders()
285 | {
286 | if (headers_sent() && empty($this->headers)) {
287 | return;
288 | }
289 |
290 | // TODO Should send the content-type header ??
291 | /*
292 | if ($this->content) {
293 | $this->addHeader('Content-type', 'text/plain');
294 | }*/
295 |
296 | foreach ($this->headers as $name => $value) {
297 | header(sprintf("%s: %s", ucfirst(strtolower($name)), $value), false);
298 | }
299 | }
300 |
301 | /**
302 | * Sets the Http status code.
303 | */
304 | protected function setResponseCode()
305 | {
306 | http_response_code($this->statusCode);
307 | }
308 |
309 | /**
310 | * @return array
311 | */
312 | protected function getReadyHeaders()
313 | {
314 | $headers = [];
315 |
316 | foreach (headers_list() as $header) {
317 | $parts = explode(' ', $header);
318 | $headers[substr($parts[0], 0, -1)] = $parts[1];
319 | }
320 |
321 | return $headers;
322 | }
323 |
324 | /**
325 | * @param mixed $content
326 | */
327 | protected function sendContent($content)
328 | {
329 | $this->sendRawHeaders();
330 | $this->sendCookies();
331 | $this->setResponseCode();
332 | echo $content;
333 | exit();
334 | }
335 | }
--------------------------------------------------------------------------------
/lib/Http/JsonResponse.php:
--------------------------------------------------------------------------------
1 | addHeader('Content-type', 'application/json');
24 | }
25 | }
--------------------------------------------------------------------------------
/lib/Renderer/Renderer.php:
--------------------------------------------------------------------------------
1 | path = $path;
21 | }
22 |
23 | /**
24 | * @return mixed
25 | */
26 | public function getPath()
27 | {
28 | return $this->path;
29 | }
30 |
31 | /**
32 | * @param string $path
33 | */
34 | public function setPath($path)
35 | {
36 | $this->path = $path;
37 | }
38 |
39 | /**
40 | * Renders a view and returns the gathered content as a string.
41 | *
42 | * @param string $view
43 | * @param array $params
44 | *
45 | * @return string
46 | */
47 | public function render($view, $params = [])
48 | {
49 | ob_start();
50 |
51 | // Extract the passed parameters as variables for the view
52 | extract($params);
53 |
54 | // Declaring an instance of this object to be available in the view
55 | $renderer = $this;
56 |
57 | // Include the view
58 | if (strpos($view, '.') !== false) {
59 | $path = $this->path . DIRECTORY_SEPARATOR . $view;
60 | } else {
61 | $path = $this->path . DIRECTORY_SEPARATOR . $view . self::DEFAULT_EXTENSION;
62 | }
63 |
64 | // Checks if the file exists before include it
65 | if (file_exists($path)) {
66 | include($path);
67 | } else {
68 | throw new RendererException("View [$path] not exists.");
69 | }
70 |
71 | // Gets the output buffer content
72 | $content = ob_get_contents();
73 |
74 | // Clean the output buffer
75 | ob_end_clean();
76 |
77 | return $content;
78 | }
79 |
80 | /**
81 | * Generates a relative path for the giving path parameter
82 | * based on the current request path.
83 | *
84 | * @param string $path
85 | *
86 | * @return string
87 | */
88 | public function path($path)
89 | {
90 | $query = str_replace('/', '\\/', $_SERVER['QUERY_STRING']);
91 | $uri = preg_replace('/\?/', '&', $_SERVER['REQUEST_URI'], 1);
92 | $result = preg_replace('/'.$query.'$|index.php$|&$/', '', $uri);
93 | return $result . preg_replace('/^\//', '', $path);
94 | }
95 | }
--------------------------------------------------------------------------------
/lib/Renderer/RendererException.php:
--------------------------------------------------------------------------------
1 | setMethods($methods);
51 | $this->setPath($path);
52 | $this->setHandler($handler);
53 | $this->setName($name);
54 | $this->setMatches([]);
55 | }
56 |
57 | /**
58 | * Handles the static calls of route methods like Route::get().
59 | *
60 | * @param string $name
61 | * @param array $arguments
62 | *
63 | * @return static Return new route instance.
64 | */
65 | public static function __callStatic($name, $arguments)
66 | {
67 | array_unshift($arguments, [strtoupper($name)]);
68 | return forward_static_call_array([self::class, 'instanceFactory'], $arguments);
69 | }
70 |
71 | /**
72 | * @return array
73 | */
74 | public function getMethods()
75 | {
76 | return $this->methods;
77 | }
78 |
79 | /**
80 | * @return mixed
81 | */
82 | public function getPath()
83 | {
84 | return $this->path;
85 | }
86 |
87 | /**
88 | * @return array|callable
89 | */
90 | public function getHandler()
91 | {
92 | return $this->handler;
93 | }
94 |
95 | /**
96 | * @return array
97 | */
98 | public function getMatches()
99 | {
100 | return $this->matches;
101 | }
102 |
103 | /**
104 | * @return string
105 | */
106 | public function getName()
107 | {
108 | return $this->name;
109 | }
110 |
111 | /**
112 | * @param array $methods
113 | */
114 | public function setMethods($methods)
115 | {
116 | foreach ($methods as $method) {
117 | if (!in_array($method, self::$allowedMethods, true)) {
118 | throw new RouteInvalidArgumentException("$method is unknown http method");
119 | }
120 | }
121 |
122 | $this->methods = $methods;
123 | }
124 |
125 | /**
126 | * @param string $path
127 | */
128 | public function setPath($path)
129 | {
130 | $this->path = $path;
131 | }
132 |
133 | /**
134 | * @param array|callable $handler
135 | */
136 | public function setHandler($handler)
137 | {
138 | if (!is_callable($handler) && !is_array($handler)) {
139 | throw new RouteInvalidArgumentException(
140 | "Route handler must be either an array or a function callback, " . gettype($handler) . "giving.");
141 | }
142 |
143 | $this->handler = $handler;
144 | }
145 |
146 | /**
147 | * @param array $matches
148 | */
149 | public function setMatches($matches)
150 | {
151 | $this->matches = $matches;
152 | }
153 |
154 | /**
155 | * @param string $name
156 | */
157 | public function setName($name)
158 | {
159 | $this->name = $name;
160 | }
161 |
162 | protected static function instanceFactory($methods, $path, $handler, $name = '')
163 | {
164 | return new static($methods, $path, $handler, $name);
165 | }
166 | }
--------------------------------------------------------------------------------
/lib/Routing/RouteCollection.php:
--------------------------------------------------------------------------------
1 | routes = $routes;
17 | }
18 |
19 | /**
20 | * @return array
21 | */
22 | public function getRoutes()
23 | {
24 | return $this->routes;
25 | }
26 |
27 | /**
28 | * @param array $routes
29 | */
30 | public function setRoutes($routes)
31 | {
32 | $this->routes = $routes;
33 | }
34 |
35 | /**
36 | * @param Route $route
37 | *
38 | * @return void
39 | */
40 | public function addRoute(Route $route)
41 | {
42 | $this->routes[] = $route;
43 | }
44 | }
--------------------------------------------------------------------------------
/lib/Routing/RouteDispatcher.php:
--------------------------------------------------------------------------------
1 | '[0-9]+',
16 | 's' => '[a-zA-z]+',
17 | 'encoded' => '[^;,/?:@&=+$]+',
18 | 'any' => '.*',
19 | ];
20 |
21 | /** @var RouteCollection */
22 | protected $routes;
23 |
24 | /** @var string */
25 | protected $uri;
26 |
27 | /** @var string */
28 | protected $method;
29 |
30 | /** @var callable */
31 | protected $notFoundedHandler;
32 |
33 | /** @var callable */
34 | protected $methodNotAllowedHandler;
35 |
36 | /** @var callable */
37 | protected $foundedHandler;
38 |
39 | /**
40 | * RouteDispatcher constructor.
41 | *
42 | * @param RouteCollection $routes
43 | * @param string $uri
44 | * @param string $method
45 | */
46 | public function __construct(RouteCollection $routes, $uri, $method)
47 | {
48 | $this->routes = $routes;
49 | $this->uri = $this->getRequestUri($uri);
50 | $this->method = $method;
51 | }
52 |
53 | /**
54 | * Handles the routes collection and returns the result of the defined handler.
55 | *
56 | * @param callable $dispatchCallback If not callback was provided the dispatch foundCallback will be used.
57 | *
58 | * @return mixed
59 | */
60 | public function dispatch($dispatchCallback = null)
61 | {
62 | /** @var Route $route */
63 | foreach ($this->routes->getRoutes() as $route) {
64 | if (($matches = $this->matchUri($route->getPath()))) {
65 | $route->setMatches($matches);
66 | $routeInfo = [
67 | $route->getMethods(),
68 | $route->getPath(),
69 | $route->getHandler(),
70 | $route->getMatches(),
71 | ];
72 |
73 | if ($this->isMethodNotAllowed($route)) {
74 | return call_user_func_array($this->methodNotAllowedHandler, [$routeInfo]);
75 | }
76 |
77 | if ($this->matchMethod($route->getMethods())) {
78 | return call_user_func_array($dispatchCallback ?: $this->foundedHandler, [$routeInfo]);
79 | }
80 | }
81 | }
82 |
83 | // If no callback was returned that's means no matching was founded for the request uri
84 | return call_user_func_array($this->notFoundedHandler, [$this->uri]);
85 | }
86 |
87 | public function methodNotAllowedHandler($handler)
88 | {
89 | $this->methodNotAllowedHandler = $handler;
90 | }
91 |
92 | public function foundedHandler($handler)
93 | {
94 | $this->foundedHandler = $handler;
95 | }
96 |
97 | public function notFoundedHandler($handler)
98 | {
99 | $this->notFoundedHandler = $handler;
100 | }
101 |
102 | protected function matchUri($routeUri)
103 | {
104 | // If the route uri matches the requested uri then returns the match
105 | if (strcmp($this->uri, $routeUri) === 0) {
106 | return [$routeUri];
107 | }
108 |
109 | // Remove slashes from the route uri => avoiding troubles after
110 | $routeUri = trim($routeUri, '/');
111 |
112 | // Get the match types from the routes
113 | if (preg_match_all('/:(\w+)/i', $routeUri, $matches)) {
114 | $matchTypes = $this->extractMatches($matches);
115 |
116 | // Replace each match type in the route uri with the appropriate match type regex
117 | $subject = $routeUri;
118 |
119 | $replace = '';
120 | foreach ($matchTypes as $param) {
121 | // If the match type is not registered throws an exception
122 | if (!array_key_exists($param, self::MATCH_TYPES)) {
123 | throw new RouteMatchingException("[$param] is unknown match type.");
124 | }
125 | $regex = sprintf('/:(%s)/i', $param);
126 | $replace = preg_replace($regex, '(' . self::MATCH_TYPES[$param] . ')', $subject);
127 | $subject = $replace;
128 | }
129 |
130 | // Escape the special chars
131 | $replace = $this->escapedSpecialChars($replace);
132 |
133 | // Build a new regex
134 | $regex = "/^$replace$/i";
135 |
136 | // Matches the request uri using the regex '$regex'
137 | if (preg_match_all($regex, trim($this->uri, '/'), $matches)) {
138 | return $this->extractMatches($matches);
139 | }
140 | }
141 |
142 | return false;
143 | }
144 |
145 | protected function matchMethod($routeMethods)
146 | {
147 | return in_array($this->method, $routeMethods, true);
148 | }
149 |
150 | protected function escapedSpecialChars($uri)
151 | {
152 | $specialChars = ['/', '?'];
153 |
154 | $subject = $uri;
155 | $replace = '';
156 | foreach ($specialChars as $char) {
157 | $replace = str_replace($char, '\\' . $char, $subject);
158 | $subject = $replace;
159 | }
160 |
161 | return $replace;
162 | }
163 |
164 | protected function extractMatches($matches)
165 | {
166 | array_shift($matches);
167 |
168 | $result = [];
169 | foreach ($matches as $array) {
170 | foreach ($array as $val) {
171 | $result[] = $val;
172 | }
173 | }
174 | return $result;
175 | }
176 |
177 | protected function isMethodNotAllowed($route)
178 | {
179 | $methods = [];
180 |
181 | // Loop through the routes collection
182 | /** @var Route $r */
183 | foreach ($this->routes->getRoutes() as $r) {
184 | if ($route->getPath() !== $r->getPath()) {
185 | continue;
186 | }
187 |
188 | foreach ($r->getMethods() as $method) {
189 | $methods[] = $method;
190 | }
191 | }
192 |
193 | return !in_array($this->method, $methods);
194 | }
195 |
196 | /**
197 | * Gets the actual requested uri.
198 | *
199 | * @param string $uri
200 | *
201 | * @return string
202 | */
203 | protected function getRequestUri($uri)
204 | {
205 | if ($uri == '') {
206 | return '/';
207 | } else {
208 | /**
209 | * Fix the [QSA] flag which replace the question
210 | * mark for the query sting with an '&' char.
211 | */
212 | return '/' . preg_replace('/&/', '?', $uri, 1);
213 | }
214 | }
215 | }
--------------------------------------------------------------------------------
/lib/Routing/RouteUrlGenerator.php:
--------------------------------------------------------------------------------
1 | routes = $routes;
23 | }
24 |
25 | /**
26 | * Generates a path for the giving route based on current request path.
27 | *
28 | * @param string $routeName
29 | * @param array $params
30 | *
31 | * @return string
32 | */
33 | public function generate($routeName, $params = [])
34 | {
35 | if (!($route = $this->getRoute($routeName))) {
36 | throw new RouteInvalidArgumentException("Route [$routeName] not registered.");
37 | }
38 |
39 | if (!$this->hasMatchType($route->getPath())) {
40 | return $this->getRoutingPath() . $route->getPath();
41 | }
42 |
43 | $subject = $route->getPath();
44 | $replace = '';
45 | foreach ($params as $param) {
46 | $replace = preg_replace('/:(\w+)/i', $param, $subject, 1);
47 | $subject = $replace;
48 | }
49 |
50 | return $this->getRoutingPath() . $replace;
51 | }
52 |
53 | /**
54 | * @param string $name
55 | *
56 | * @return Route|false
57 | */
58 | protected function getRoute($name)
59 | {
60 | /** @var Route $route */
61 | foreach ($this->routes->getRoutes() as $route) {
62 | if ($route->getName() === $name) {
63 | return $route;
64 | }
65 | }
66 |
67 | return false;
68 | }
69 |
70 | /**
71 | * @param string $routeName
72 | *
73 | * @return bool
74 | */
75 | protected function hasMatchType($routeName)
76 | {
77 | return preg_match('/:(\w+)/i', $routeName) === 1;
78 | }
79 |
80 | /**
81 | * Gets the actual routing path.
82 | *
83 | * @return string
84 | */
85 | protected function getRoutingPath()
86 | {
87 | $query = str_replace('/', '\\/', $_SERVER['QUERY_STRING']);
88 | $uri = preg_replace('/\?/', '&', $_SERVER['REQUEST_URI'], 1);
89 | $result = preg_replace('/'.$query.'$|index.php$|&$/', '', $uri);
90 | return preg_replace('/\/$/', '', $result);
91 | }
92 | }
--------------------------------------------------------------------------------
/lib/Session/Exception/SessionRuntimeException.php:
--------------------------------------------------------------------------------
1 | options = $options;
19 | $this->setDirectivesConfiguration();
20 | }
21 |
22 | public function setID($id)
23 | {
24 | session_id($id);
25 |
26 | if ($this->getID() !== $id) {
27 | throw new SessionRuntimeException("Failed to set session id to [$id].");
28 | }
29 |
30 | return true;
31 | }
32 |
33 | public function setName($name)
34 | {
35 | session_name($name);
36 |
37 | if ($this->getName() !== $name) {
38 | throw new SessionRuntimeException("Failed to set session name to [$name].");
39 | }
40 |
41 | return true;
42 | }
43 |
44 | public function getID()
45 | {
46 | return session_id();
47 | }
48 |
49 | public function getName()
50 | {
51 | return session_name();
52 | }
53 |
54 | public function start()
55 | {
56 | if ($this->isNone()) {
57 | session_name(ini_get('session.name'));
58 | return session_start();
59 | }
60 |
61 | return false;
62 | }
63 |
64 | public function regenerateID($deleteOldSession = true)
65 | {
66 | if ($this->isActive()) {
67 | if (session_regenerate_id()) {
68 | return session_regenerate_id($deleteOldSession);
69 | }
70 | }
71 |
72 | return false;
73 | }
74 |
75 | public function setCookieParameters($lifetime, $path, $domain, $secure = false, $httpOnly = false)
76 | {
77 | if (!session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly)) {
78 | throw new SessionRuntimeException("Unable to set session cookie parameters.");
79 | }
80 |
81 | return true;
82 | }
83 |
84 | public function getCookieParameters()
85 | {
86 | return session_get_cookie_params();
87 | }
88 |
89 | public function isActive()
90 | {
91 | return session_status() === PHP_SESSION_ACTIVE;
92 | }
93 |
94 | public function isDisabled()
95 | {
96 | return session_status() === PHP_SESSION_DISABLED;
97 | }
98 |
99 | public function isNone()
100 | {
101 | return session_status() === PHP_SESSION_NONE;
102 | }
103 |
104 | public function destroy()
105 | {
106 | if (Session::isActive()) {
107 | return session_destroy();
108 | }
109 |
110 | return false;
111 | }
112 |
113 | public function deleteCookie()
114 | {
115 | $cookieName = $this->getName();
116 | if (isset($_COOKIE[$cookieName])) {
117 | unset($_COOKIE[$cookieName]);
118 | setcookie($cookieName, '', time() - 3600);
119 | }
120 | }
121 |
122 | public function cookieExists()
123 | {
124 | return isset($_COOKIE[$this->getName()]);
125 | }
126 |
127 | protected function setDirectivesConfiguration()
128 | {
129 | if (!empty($this->options)) {
130 | foreach ($this->options as $key => $value) {
131 | if (ini_set('session.' . $key, $value) === false) {
132 | throw new SessionRuntimeException(sprintf(
133 | "Cannot set directive [%s] to [%s]",
134 | $key,
135 | gettype($value) === 'boolean' ? $value ? 'true' : 'false' : $value
136 | ));
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/lib/Session/SessionStorage.php:
--------------------------------------------------------------------------------
1 | ';
24 | print_r($_SESSION);
25 | echo '';
26 | }
27 |
28 | public function unsetVars()
29 | {
30 | if (isset($_SESSION)) {
31 | foreach ($_SESSION as $key => $value) {
32 | unset($_SESSION[$key]);
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ftp-filemanager",
3 | "version": "1.0.0",
4 | "description": "An open source FTP filemanager website with modern interface without any framework",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/ELAMRANI744/ftp-filemanager.git"
11 | },
12 | "keywords": [
13 | "ftp",
14 | "filemanager",
15 | "website"
16 | ],
17 | "authors": [
18 | {
19 | "name": "El Amrani Chakir",
20 | "email": "elamrani.sv.laza@gmail.com"
21 | }
22 | ],
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/ELAMRANI744/ftp-filemanager/issues"
26 | },
27 | "homepage": "https://github.com/ELAMRANI744/ftp-filemanager#readme",
28 | "devDependencies": {
29 | "babel-core": "^6.26.3",
30 | "babel-preset-env": "^1.7.0",
31 | "babelify": "^8.0.0",
32 | "browserify": "^16.5.2",
33 | "gulp": "^3.9.0",
34 | "gulp-autoprefixer": "^7.0.1",
35 | "gulp-livereload": "^4.0.2",
36 | "gulp-notify": "^3.2.0",
37 | "gulp-plumber": "^1.2.1",
38 | "gulp-rename": "^2.0.0",
39 | "gulp-sass": "^4.1.0",
40 | "gulp-sourcemaps": "^2.6.5",
41 | "gulp-uglify": "^3.0.2",
42 | "vinyl-buffer": "^1.0.1",
43 | "vinyl-source-stream": "^2.0.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Options -MultiViews
3 |
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Ignore routing the static files & directories
9 | RewriteCond %{REQUEST_FILENAME} !-f
10 | RewriteCond %{REQUEST_FILENAME} !-d
11 | RewriteCond %{REQUEST_FILENAME} !-l
12 |
13 | # Rewrite every thnig else to the app hanlder
14 | RewriteRule ^(.*)$ index.php?$1 [L,QSA]
15 |
--------------------------------------------------------------------------------
/public/dist/style.min.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Arvo:wght@400;700&display=swap");*,*:after,*:before{-webkit-box-sizing:border-box;box-sizing:border-box}body,h1,h2,h3,h4,h5,h6,p,ol,ul{margin:0;padding:0}ol,ul{list-style:none}a{text-decoration:none}button:focus,input:focus{outline:none}input::-webkit-input-placeholder{color:inherit}input::-moz-placeholder{color:inherit}input:-ms-input-placeholder{color:inherit}input::-ms-input-placeholder{color:inherit}input::placeholder{color:inherit}table{border-collapse:collapse}.list-inline li{display:inline-block}.uppercase{text-transform:uppercase}.btn-primary,.btn-secondary{cursor:pointer;background-color:transparent;padding:10px 24px;border-radius:5px;border:2px solid transparent;-webkit-box-shadow:0 2px 4px -2px #999;box-shadow:0 2px 4px -2px #999}.btn-primary:focus,.btn-secondary:focus{border:2px solid #23e8e8}.btn-primary{background-color:#364F6B;color:#FFF}.btn-primary:hover{background-color:#426183}.btn-secondary{background-color:#F3DCB7;color:#364F6B}.btn-secondary:hover{background-color:#f8ebd6}.btn-icon-svg{position:relative;padding-top:10px;padding-bottom:10px;padding-right:24px;padding-left:36px}.btn-icon-svg svg{position:absolute;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);vertical-align:middle;left:10px}.checkbox{position:relative;vertical-align:middle;cursor:pointer}.checkbox input[type='checkbox'],.checkbox .checkbox-text{vertical-align:middle}.checkbox input[type='checkbox']{display:none}.checkbox input[type='checkbox']:checked+.checkbox-text:after{content:'\002714';color:#FFF;position:absolute;left:2px;top:0}.checkbox input[type='checkbox']:checked+.checkbox-text:before{background-color:#EE3B5C;border:0.05em solid #EE3B5C;-webkit-transition:.3s;transition:.3s}.checkbox .checkbox-text{color:#58595B}.checkbox .checkbox-text:before{content:'';position:relative;display:inline-block;top:2px;width:1em;height:1em;margin-right:.5em;border:.05em solid #ddd;border-radius:.2em;-webkit-box-shadow:0 0 2px 0 #EEE;box-shadow:0 0 2px 0 #EEE}.input-area input{width:100%;padding:12px 8px;border-radius:6px;border:2px solid #364F6B;background-color:#FFF5E7;color:#58595B}.error-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:#EE3B5C;color:#FFF;font-size:10rem;text-transform:uppercase;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.error-container .error-code{margin-left:3rem}.homepage-wrapper .overlay{position:absolute;top:0;left:0;bottom:0;right:0;z-index:999;background-color:rgba(255,255,255,0.95);-webkit-transition:1.5s;transition:1.5s;visibility:visible;opacity:1}.homepage-wrapper .overlay.hide{visibility:hidden;opacity:0}.homepage-wrapper header.header{background-color:#EE3B5C;color:#FFF;height:70px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-shadow:0 4px 2px -2px #999;box-shadow:0 4px 2px -2px #999}.homepage-wrapper header.header .brand{background-color:#364F6B;height:100%;padding:0 25px}.homepage-wrapper header.header .brand h3{position:relative;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);font-size:2rem}.homepage-wrapper header.header nav.actions{padding-right:30px}.homepage-wrapper header.header nav.actions ul li:not(:last-of-type){margin-right:15px}.homepage-wrapper section.intro{position:relative;height:calc(100% - 70px)}.homepage-wrapper section.intro .row{position:absolute;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;width:85%;top:50%;left:50%;-webkit-transform:translate(-50%, -45%);-ms-transform:translate(-50%, -45%);transform:translate(-50%, -45%)}.homepage-wrapper section.intro .row .app-desc{color:#58595B;padding-top:25px}.homepage-wrapper section.intro .row .app-desc .title{text-transform:uppercase;font-size:2.8rem;margin-bottom:50px}.homepage-wrapper section.intro .row .app-desc .desc{font-size:1.4rem;line-height:1.6}.homepage-wrapper section.intro .row .app-desc ul.actions{margin-top:50px}.homepage-wrapper section.intro .row .app-desc ul.actions li:not(:last-of-type){margin-right:15px}.login-wrapper{background-color:#EE3B5C}.login-wrapper a.shape-arrow{position:absolute;left:25px;bottom:25px}.login-wrapper header.header{padding-top:30px;padding-left:30px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-wrapper header.header .brand .name{font-size:2.5rem;color:#FFF}.login-wrapper form.login-form{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);-ms-transform:translate(-50%, -50%);transform:translate(-50%, -50%);width:400px;padding:40px 15px 15px 15px;border-radius:15px;-webkit-box-shadow:0 0 10px 0 #333;box-shadow:0 0 10px 0 #333;background-color:#FFF}.login-wrapper form.login-form .input-area{margin-bottom:25px}.login-wrapper form.login-form .checkbox{display:block;margin-bottom:10px}.login-wrapper form.login-form .server-error{color:#EE3B5C;font-size:.8rem}.login-wrapper form.login-form .form-actions{margin-top:25px}.login-wrapper form.login-form .form-actions input[type="submit"]{display:block;margin:0 auto}.login-wrapper .github-btn{position:absolute;bottom:15px;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);padding:0;height:40px;width:40px;border-radius:50%}.login-wrapper .github-btn svg{vertical-align:middle}.login-wrapper .github-btn:hover{background-color:#FFF}.login-wrapper .github-btn:hover svg path{fill:#EE3B5C}*,*:before,*:after{font-family:Arvo, Helvetica, sans-serif}button{text-transform:capitalize}.homepage-wrapper,.login-wrapper,.error-container{position:fixed;width:100%;height:100%}.homepage-wrapper svg.shape-right,.homepage-wrapper svg.shape-left,.login-wrapper svg.shape-right,.login-wrapper svg.shape-left{position:absolute;bottom:0;z-index:-1}.homepage-wrapper svg.shape-left,.login-wrapper svg.shape-left{left:0}.homepage-wrapper svg.shape-right,.login-wrapper svg.shape-right{right:0}
2 |
3 | /*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInZhcmlhYmxlcy9fdmFyaWFibGVzLnNjc3MiLCJiYXNlL19yZXNldC5zY3NzIiwiYmFzZS9fdXRpbGl0aWVzLnNjc3MiLCJjb21wb25lbnRzL19idXR0b24uc2NzcyIsInZhcmlhYmxlcy9fbWl4aW5zLnNjc3MiLCJjb21wb25lbnRzL19jaGVja2JveC5zY3NzIiwiY29tcG9uZW50cy9faW5wdXQuc2NzcyIsInBhZ2VzL19lcnJvci5zY3NzIiwicGFnZXMvX2hvbWVwYWdlLnNjc3MiLCJwYWdlcy9fbG9naW4uc2NzcyIsIl9nbG9iYWwuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxzRkFBWSxDQ0RaLG1CQUNJLDZCQUFZLENBQVoscUJBQXNCLENBQ3pCLCtCQUdHLFFBQVMsQ0FDVCxTQUFVLENBQ2IsTUFHRyxlQUFnQixDQUNuQixFQUdHLG9CQUFxQixDQUN4Qix5QkFJTyxZQUFhLENBQ2hCLGlDQUlELGFBQWMsQ0FKYix3QkFJRCxhQUFjLENBSmIsNEJBSUQsYUFBYyxDQUpiLDZCQUlELGFBQWMsQ0FKYixtQkFJRCxhQUFjLENBQ2pCLE1BR0csd0JBQXlCLENBQzVCLGdCQzVCRyxvQkFBcUIsQ0FDeEIsV0FHRyx3QkFBeUIsQ0FDNUIsNEJDTEcsY0FBZSxDQUNmLDRCQUE2QixDQUM3QixpQkFBa0IsQ0FDbEIsaUJBQWtCLENBQ2xCLDRCQUE2QixDQUM3QixzQ0FBWSxDQUFaLDhCQUErQixDQUUvQix3Q0FDSSx3QkFBeUIsQ0FDNUIsYUFLRCx3QkhWbUIsQ0dXbkIsVUFBVyxDQ0xYLG1CQUNJLHdCQUEwQyxDQUM3QyxlRFNELHdCSGhCcUIsQ0dpQnJCLGFIbEJtQixDSU1uQixxQkFDSSx3QkFBMEMsQ0FDN0MsY0RlRCxpQkFBa0IsQ0FDbEIsZ0JBQWlCLENBQ2pCLG1CQUFvQixDQUNwQixrQkFBbUIsQ0FDbkIsaUJBQWtCLENBTHRCLGtCQzFCSSxpQkFBVSxDQUNWLE9BQVEsQ0FDUixrQ0FBMkIsQ0FBM0IsOEJBQTJCLENBQTNCLDBCQUEyQixDRGlDdkIscUJBQXNCLENBQ3RCLFNBQVUsQ0FDYixVRXJDRCxpQkFBa0IsQ0FDbEIscUJBQXNCLENBQ3RCLGNBQWUsQ0FIbkIsMERBTVEscUJBQXNCLENBTjlCLGlDQVVRLFlBQWEsQ0FWckIsOERBYVksaUJBQWtCLENBQ2xCLFVBQVcsQ0FDWCxpQkFBa0IsQ0FDbEIsUUFBUyxDQUNULEtBQU0sQ0FqQmxCLCtEQXFCWSx3QkxqQlEsQ0trQlIsMkJMbEJRLENLbUJSLHNCQUFZLENBQVosY0FBZSxDQXZCM0IseUJBNEJRLGFMcEJXLENLUm5CLGdDQStCWSxVQUFXLENBQ1gsaUJBQWtCLENBQ2xCLG9CQUFxQixDQUNyQixPQUFRLENBQ1IsU0FBVSxDQUNWLFVBQVcsQ0FDWCxpQkFBa0IsQ0FDbEIsdUJBQXdCLENBQ3hCLGtCQUFtQixDQUNuQixpQ0FBWSxDQUFaLHlCQUEwQixDQUM3QixrQkNyQ0QsVUFBVyxDQUNYLGdCQUFpQixDQUNqQixpQkFBa0IsQ0FDbEIsd0JORmUsQ01HZix3QkFSaUIsQ0FTakIsYU5EVyxDTUVkLGlCQ1RELG1CQUFhLENBQWIsbUJBQWEsQ0FBYixZQUFhLENBQ2IsdUJBQXVCLENBQXZCLG9CQUF1QixDQUF2QixzQkFBdUIsQ0FDdkIsd0JBQW1CLENBQW5CLHFCQUFtQixDQUFuQixrQkFBbUIsQ0FDbkIsd0JQQWdCLENPQ2hCLFVBQVcsQ0FDWCxlQUFnQixDQUNoQix3QkFBeUIsQ0FDekIsd0JBQWEsQ0FBYixxQkFBYSxDQUFiLG9CQUFhLENBQWIsZ0JBQWlCLENBUnJCLDZCQVdRLGdCQUFpQixDQUNwQiwyQkNWRyxpQkFBa0IsQ0FDbEIsS0FBTSxDQUNOLE1BQU8sQ0FDUCxRQUFTLENBQ1QsT0FBUSxDQUNSLFdBQVksQ0FDWix1Q0FBMkIsQ0FDM0IsdUJBQWdCLENBQWhCLGVBQWdCLENBQ2hCLGtCQUFtQixDQUNuQixTQUFVLENBWGxCLGdDQWNZLGlCQUFrQixDQUNsQixTQUFVLENBZnRCLGdDQW9CUSx3QlJoQlksQ1FpQlosVUFBVyxDQUNYLFdBQVksQ0FDWixtQkFBYSxDQUFiLG1CQUFhLENBQWIsWUFBYSxDQUNiLHdCQUE4QixDQUE5QixxQkFBOEIsQ0FBOUIsNkJBQThCLENBQzlCLHdCQUFtQixDQUFuQixxQkFBbUIsQ0FBbkIsa0JBQW1CLENBQ25CLHNDQUFZLENBQVosOEJBQStCLENBMUJ2Qyx1Q0E2Qlksd0JSeEJXLENReUJYLFdBQVksQ0FDWixjQUFlLENBL0IzQiwwQ0pDSSxpQkFBVSxDQUNWLE9BQVEsQ0FDUixrQ0FBMkIsQ0FBM0IsOEJBQTJCLENBQTNCLDBCQUEyQixDSWdDZixjQUFlLENBbkMvQiw0Q0F3Q1ksa0JBQW1CLENBeEMvQixxRUE0Q29CLGlCQUFrQixDQTVDdEMsZ0NBbURRLGlCQUFrQixDQUNsQix3QkFBeUIsQ0FwRGpDLHFDQXVEWSxpQkFBa0IsQ0FDbEIsbUJBQWEsQ0FBYixtQkFBYSxDQUFiLFlBQWEsQ0FDYix3QkFBNkIsQ0FBN0IsNEJBQTZCLENBQzdCLFNBQVUsQ0FDVixPQUFRLENBQ1IsUUFBUyxDQUNULHVDQUFXLENBQVgsbUNBQVcsQ0FBWCwrQkFBZ0MsQ0E3RDVDLCtDQWdFZ0IsYVJ4REcsQ1F5REgsZ0JBQWlCLENBakVqQyxzREFvRW9CLHdCQUF5QixDQUN6QixnQkFBaUIsQ0FDakIsa0JBQW1CLENBdEV2QyxxREEwRW9CLGdCQUFpQixDQUNqQixlQUFnQixDQTNFcEMsMERBK0VvQixlQUFnQixDQS9FcEMsZ0ZBa0Z3QixpQkFBa0IsQ0FDckIsZUNsRmpCLHdCVEdnQixDU0pwQiw2QkFJUSxpQkFBa0IsQ0FDbEIsU0FBVSxDQUNWLFdBQVksQ0FOcEIsNkJBVVEsZ0JBQWlCLENBQ2pCLGlCQUFrQixDQUNsQix3QkFBYSxDQUFiLHFCQUFhLENBQWIsb0JBQWEsQ0FBYixnQkFBaUIsQ0FaekIsMENBZ0JnQixnQkFBaUIsQ0FDakIsVUFBVyxDQWpCM0IsK0JBdUJRLGlCQUFrQixDQUNsQixPQUFRLENBQ1IsUUFBUyxDQUNULHVDQUFnQyxDQUFoQyxtQ0FBZ0MsQ0FBaEMsK0JBQWdDLENBQ2hDLFdBQVksQ0FDWiwyQkFBNEIsQ0FDNUIsa0JBQW1CLENBQ25CLGtDQUEyQixDQUEzQiwwQkFBMkIsQ0FDM0IscUJBQXNCLENBL0I5QiwyQ0FrQ1ksa0JBQW1CLENBbEMvQix5Q0FzQ1ksYUFBYyxDQUNkLGtCQUFtQixDQXZDL0IsNkNBMkNZLGFUdkNRLENTd0NSLGVBQWdCLENBNUM1Qiw2Q0FnRFksZUFBZ0IsQ0FoRDVCLGtFQW1EZ0IsYUFBYyxDQUNkLGFBQWMsQ0FwRDlCLDJCQTBEUSxpQkFBa0IsQ0FDbEIsV0FBWSxDQUNaLFFBQVMsQ0FDVCxrQ0FBMkIsQ0FBM0IsOEJBQTJCLENBQTNCLDBCQUEyQixDQUMzQixTQUFVLENBQ1YsV0FBWSxDQUNaLFVBQVcsQ0FDWCxpQkFBa0IsQ0FqRTFCLCtCQW9FWSxxQkFBc0IsQ0FwRWxDLGlDQXdFWSxxQkFBc0IsQ0F4RWxDLDBDQTJFZ0IsWVR2RUksQ1N3RVAsbUJDM0VULHVDQUF3QyxDQUMzQyxPQUdHLHlCQUEwQixDQUM3QixrREFHRyxjQUFlLENBQ2YsVUFBVyxDQUNYLFdBQVksQ0FDZixnSUFLTyxpQkFBa0IsQ0FDbEIsUUFBUyxDQUNULFVBQVcsQ0FMbkIsK0RBU1EsTUFBTyxDQVRmLGlFQVlRLE9BQVEiLCJmaWxlIjoic3R5bGUuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gRm9udHNcclxuQGltcG9ydCB1cmwoJ2h0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzMj9mYW1pbHk9QXJ2bzp3Z2h0QDQwMDs3MDAmZGlzcGxheT1zd2FwJyk7XHJcblxyXG4vLyBDb2xvcnNcclxuJHByaW1hcnlSZWQ6ICNFRTNCNUM7XHJcbiRzZWNvbmRhcnlCbHVlOiAjMzY0RjZCO1xyXG4kdGhpcmRMaWdodEJyb3duOiAjRjNEQ0I3O1xyXG5cclxuJHRleHRCbGFjazogIzU4NTk1QjsiLCIqLCAqOmFmdGVyLCAqOmJlZm9yZSB7XHJcbiAgICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xyXG59XHJcblxyXG5ib2R5LCBoMSwgaDIsIGgzLCBoNCwgaDUsIGg2LCBwLCBvbCwgdWwge1xyXG4gICAgbWFyZ2luOiAwO1xyXG4gICAgcGFkZGluZzogMDtcclxufVxyXG5cclxub2wsIHVsIHtcclxuICAgIGxpc3Qtc3R5bGU6IG5vbmU7XHJcbn1cclxuXHJcbmEge1xyXG4gICAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xyXG59XHJcblxyXG5idXR0b24sIGlucHV0IHtcclxuICAgICY6Zm9jdXMge1xyXG4gICAgICAgIG91dGxpbmU6IG5vbmU7XHJcbiAgICB9XHJcbn1cclxuXHJcbmlucHV0OjpwbGFjZWhvbGRlciB7XHJcbiAgICBjb2xvcjogaW5oZXJpdDtcclxufVxyXG5cclxudGFibGUge1xyXG4gICAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcclxufVxyXG4iLCIubGlzdC1pbmxpbmUgbGkge1xyXG4gICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xyXG59XHJcblxyXG4udXBwZXJjYXNlIHtcclxuICAgIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7XHJcbn0iLCIlYnRuIHtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxuICAgIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xyXG4gICAgcGFkZGluZzogMTBweCAyNHB4O1xyXG4gICAgYm9yZGVyLXJhZGl1czogNXB4O1xyXG4gICAgYm9yZGVyOiAycHggc29saWQgdHJhbnNwYXJlbnQ7XHJcbiAgICBib3gtc2hhZG93OiAwIDJweCA0cHggLTJweCAjOTk5O1xyXG5cclxuICAgICY6Zm9jdXMge1xyXG4gICAgICAgIGJvcmRlcjogMnB4IHNvbGlkICMyM2U4ZTg7XHJcbiAgICB9XHJcbn1cclxuXHJcbi5idG4tcHJpbWFyeSB7XHJcbiAgICBAZXh0ZW5kICVidG47XHJcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAkc2Vjb25kYXJ5Qmx1ZTtcclxuICAgIGNvbG9yOiAjRkZGO1xyXG4gICAgQGluY2x1ZGUgYnRuLWhvdmVyLWNvbG9yKCRzZWNvbmRhcnlCbHVlKTtcclxufVxyXG5cclxuLmJ0bi1zZWNvbmRhcnkge1xyXG4gICAgQGV4dGVuZCAlYnRuO1xyXG4gICAgYmFja2dyb3VuZC1jb2xvcjogJHRoaXJkTGlnaHRCcm93bjtcclxuICAgIGNvbG9yOiAkc2Vjb25kYXJ5Qmx1ZTtcclxuICAgIEBpbmNsdWRlIGJ0bi1ob3Zlci1jb2xvcigkdGhpcmRMaWdodEJyb3duKTtcclxufVxyXG5cclxuLmJ0bi1pY29uLXN2ZyB7XHJcbiAgICBwb3NpdGlvbjogcmVsYXRpdmU7XHJcbiAgICBwYWRkaW5nLXRvcDogMTBweDtcclxuICAgIHBhZGRpbmctYm90dG9tOiAxMHB4O1xyXG4gICAgcGFkZGluZy1yaWdodDogMjRweDtcclxuICAgIHBhZGRpbmctbGVmdDogMzZweDtcclxuXHJcbiAgICBzdmcge1xyXG4gICAgICAgIEBpbmNsdWRlIGNlbnRlci12ZXJ0aWNhbGx5KCdhYnNvbHV0ZScpO1xyXG4gICAgICAgIHZlcnRpY2FsLWFsaWduOiBtaWRkbGU7XHJcbiAgICAgICAgbGVmdDogMTBweDtcclxuICAgIH1cclxufSIsIkBtaXhpbiBjZW50ZXItdmVydGljYWxseSgkcG9zaXRpb246ICdyZWxhdGl2ZScpIHtcclxuICAgIHBvc2l0aW9uOiAjeyRwb3NpdGlvbn07XHJcbiAgICB0b3A6IDUwJTtcclxuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgtNTAlKTtcclxufVxyXG5cclxuQG1peGluIHNldC1mb250KCRmb250KSB7XHJcbiAgICBmb250LWZhbWlseTogI3skZm9udH0sIEhlbHZldGljYSwgc2Fucy1zZXJpZjtcclxufVxyXG5cclxuQG1peGluIGJ0bi1ob3Zlci1jb2xvcigkY29sb3IsICRhbW91bnQ6IDclKSB7XHJcbiAgICAmOmhvdmVyIHtcclxuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiBsaWdodGVuKCRjb2xvciwgJGFtb3VudCk7XHJcbiAgICB9XHJcbn0iLCIuY2hlY2tib3gge1xyXG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xyXG4gICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxuXHJcbiAgICBpbnB1dFt0eXBlPSdjaGVja2JveCddLCAuY2hlY2tib3gtdGV4dCB7XHJcbiAgICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcclxuICAgIH1cclxuXHJcbiAgICBpbnB1dFt0eXBlPSdjaGVja2JveCddIHtcclxuICAgICAgICBkaXNwbGF5OiBub25lO1xyXG5cclxuICAgICAgICAmOmNoZWNrZWQgKyAuY2hlY2tib3gtdGV4dDphZnRlciB7XHJcbiAgICAgICAgICAgIGNvbnRlbnQ6ICdcXDAwMjcxNCc7XHJcbiAgICAgICAgICAgIGNvbG9yOiAjRkZGO1xyXG4gICAgICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgICAgICAgICAgIGxlZnQ6IDJweDtcclxuICAgICAgICAgICAgdG9wOiAwO1xyXG4gICAgICAgIH1cclxuXHJcbiAgICAgICAgJjpjaGVja2VkICsgLmNoZWNrYm94LXRleHQ6YmVmb3JlIHtcclxuICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogJHByaW1hcnlSZWQ7XHJcbiAgICAgICAgICAgIGJvcmRlcjogLjA1ZW0gc29saWQgJHByaW1hcnlSZWQ7XHJcbiAgICAgICAgICAgIHRyYW5zaXRpb246IC4zcztcclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgLmNoZWNrYm94LXRleHQge1xyXG4gICAgICAgIGNvbG9yOiAkdGV4dEJsYWNrO1xyXG5cclxuICAgICAgICAmOmJlZm9yZSB7XHJcbiAgICAgICAgICAgIGNvbnRlbnQ6ICcnO1xyXG4gICAgICAgICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XHJcbiAgICAgICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcclxuICAgICAgICAgICAgdG9wOiAycHg7XHJcbiAgICAgICAgICAgIHdpZHRoOiAxZW07XHJcbiAgICAgICAgICAgIGhlaWdodDogMWVtO1xyXG4gICAgICAgICAgICBtYXJnaW4tcmlnaHQ6IC41ZW07XHJcbiAgICAgICAgICAgIGJvcmRlcjogLjA1ZW0gc29saWQgI2RkZDtcclxuICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogLjJlbTtcclxuICAgICAgICAgICAgYm94LXNoYWRvdzogMCAwIDJweCAwICNFRUU7XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG59IiwiJGlucHV0QmFja2dyb3VuZDogI0ZGRjVFNztcclxuXHJcbi5pbnB1dC1hcmVhIHtcclxuICAgIGlucHV0IHtcclxuICAgICAgICB3aWR0aDogMTAwJTtcclxuICAgICAgICBwYWRkaW5nOiAxMnB4IDhweDtcclxuICAgICAgICBib3JkZXItcmFkaXVzOiA2cHg7XHJcbiAgICAgICAgYm9yZGVyOiAycHggc29saWQgJHNlY29uZGFyeUJsdWU7XHJcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogJGlucHV0QmFja2dyb3VuZDtcclxuICAgICAgICBjb2xvcjogJHRleHRCbGFjaztcclxuICAgIH1cclxufSIsIi5lcnJvci1jb250YWluZXIge1xyXG4gICAgZGlzcGxheTogZmxleDtcclxuICAgIGp1c3RpZnktY29udGVudDogY2VudGVyO1xyXG4gICAgYWxpZ24taXRlbXM6IGNlbnRlcjtcclxuICAgIGJhY2tncm91bmQtY29sb3I6ICRwcmltYXJ5UmVkO1xyXG4gICAgY29sb3I6ICNGRkY7XHJcbiAgICBmb250LXNpemU6IDEwcmVtO1xyXG4gICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcclxuICAgIHVzZXItc2VsZWN0OiBub25lO1xyXG5cclxuICAgIC5lcnJvci1jb2RlIHtcclxuICAgICAgICBtYXJnaW4tbGVmdDogM3JlbTtcclxuICAgIH1cclxufSIsIi5ob21lcGFnZS13cmFwcGVyIHtcclxuICAgIC5vdmVybGF5IHtcclxuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgICAgICAgdG9wOiAwO1xyXG4gICAgICAgIGxlZnQ6IDA7XHJcbiAgICAgICAgYm90dG9tOiAwO1xyXG4gICAgICAgIHJpZ2h0OiAwO1xyXG4gICAgICAgIHotaW5kZXg6IDk5OTtcclxuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKCNGRkYsIC45NSk7XHJcbiAgICAgICAgdHJhbnNpdGlvbjogMS41cztcclxuICAgICAgICB2aXNpYmlsaXR5OiB2aXNpYmxlO1xyXG4gICAgICAgIG9wYWNpdHk6IDE7XHJcblxyXG4gICAgICAgICYuaGlkZSB7XHJcbiAgICAgICAgICAgIHZpc2liaWxpdHk6IGhpZGRlbjtcclxuICAgICAgICAgICAgb3BhY2l0eTogMDtcclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgaGVhZGVyLmhlYWRlciB7XHJcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogJHByaW1hcnlSZWQ7XHJcbiAgICAgICAgY29sb3I6ICNGRkY7XHJcbiAgICAgICAgaGVpZ2h0OiA3MHB4O1xyXG4gICAgICAgIGRpc3BsYXk6IGZsZXg7XHJcbiAgICAgICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuO1xyXG4gICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XHJcbiAgICAgICAgYm94LXNoYWRvdzogMCA0cHggMnB4IC0ycHggIzk5OTtcclxuXHJcbiAgICAgICAgLmJyYW5kIHtcclxuICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogJHNlY29uZGFyeUJsdWU7XHJcbiAgICAgICAgICAgIGhlaWdodDogMTAwJTtcclxuICAgICAgICAgICAgcGFkZGluZzogMCAyNXB4O1xyXG5cclxuICAgICAgICAgICAgaDMge1xyXG4gICAgICAgICAgICAgICAgQGluY2x1ZGUgY2VudGVyLXZlcnRpY2FsbHkoKTtcclxuICAgICAgICAgICAgICAgIGZvbnQtc2l6ZTogMnJlbTtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuXHJcbiAgICAgICAgbmF2LmFjdGlvbnMge1xyXG4gICAgICAgICAgICBwYWRkaW5nLXJpZ2h0OiAzMHB4O1xyXG5cclxuICAgICAgICAgICAgdWwge1xyXG4gICAgICAgICAgICAgICAgbGk6bm90KDpsYXN0LW9mLXR5cGUpIHtcclxuICAgICAgICAgICAgICAgICAgICBtYXJnaW4tcmlnaHQ6IDE1cHg7XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICB9XHJcbiAgICB9XHJcblxyXG4gICAgc2VjdGlvbi5pbnRybyB7XHJcbiAgICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xyXG4gICAgICAgIGhlaWdodDogY2FsYygxMDAlIC0gNzBweCk7XHJcblxyXG4gICAgICAgIC5yb3cge1xyXG4gICAgICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7XHJcbiAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogc3BhY2UtYXJvdW5kO1xyXG4gICAgICAgICAgICB3aWR0aDogODUlO1xyXG4gICAgICAgICAgICB0b3A6IDUwJTtcclxuICAgICAgICAgICAgbGVmdDogNTAlO1xyXG4gICAgICAgICAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNDUlKTtcclxuXHJcbiAgICAgICAgICAgIC5hcHAtZGVzYyB7XHJcbiAgICAgICAgICAgICAgICBjb2xvcjogJHRleHRCbGFjaztcclxuICAgICAgICAgICAgICAgIHBhZGRpbmctdG9wOiAyNXB4O1xyXG5cclxuICAgICAgICAgICAgICAgIC50aXRsZSB7XHJcbiAgICAgICAgICAgICAgICAgICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcclxuICAgICAgICAgICAgICAgICAgICBmb250LXNpemU6IDIuOHJlbTtcclxuICAgICAgICAgICAgICAgICAgICBtYXJnaW4tYm90dG9tOiA1MHB4O1xyXG4gICAgICAgICAgICAgICAgfVxyXG5cclxuICAgICAgICAgICAgICAgIC5kZXNjIHtcclxuICAgICAgICAgICAgICAgICAgICBmb250LXNpemU6IDEuNHJlbTtcclxuICAgICAgICAgICAgICAgICAgICBsaW5lLWhlaWdodDogMS42O1xyXG4gICAgICAgICAgICAgICAgfVxyXG5cclxuICAgICAgICAgICAgICAgIHVsLmFjdGlvbnMge1xyXG4gICAgICAgICAgICAgICAgICAgIG1hcmdpbi10b3A6IDUwcHg7XHJcblxyXG4gICAgICAgICAgICAgICAgICAgIGxpOm5vdCg6bGFzdC1vZi10eXBlKSB7XHJcbiAgICAgICAgICAgICAgICAgICAgICAgIG1hcmdpbi1yaWdodDogMTVweDtcclxuICAgICAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgICAgICB9XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICB9XHJcbiAgICB9XHJcbn0iLCIubG9naW4td3JhcHBlciB7XHJcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAkcHJpbWFyeVJlZDtcclxuXHJcbiAgICBhLnNoYXBlLWFycm93IHtcclxuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgICAgICAgbGVmdDogMjVweDtcclxuICAgICAgICBib3R0b206IDI1cHg7XHJcbiAgICB9XHJcblxyXG4gICAgaGVhZGVyLmhlYWRlciB7XHJcbiAgICAgICAgcGFkZGluZy10b3A6IDMwcHg7XHJcbiAgICAgICAgcGFkZGluZy1sZWZ0OiAzMHB4O1xyXG4gICAgICAgIHVzZXItc2VsZWN0OiBub25lO1xyXG5cclxuICAgICAgICAuYnJhbmQge1xyXG4gICAgICAgICAgICAubmFtZSB7XHJcbiAgICAgICAgICAgICAgICBmb250LXNpemU6IDIuNXJlbTtcclxuICAgICAgICAgICAgICAgIGNvbG9yOiAjRkZGO1xyXG4gICAgICAgICAgICB9XHJcbiAgICAgICAgfVxyXG4gICAgfVxyXG5cclxuICAgIGZvcm0ubG9naW4tZm9ybSB7XHJcbiAgICAgICAgcG9zaXRpb246IGFic29sdXRlO1xyXG4gICAgICAgIHRvcDogNTAlO1xyXG4gICAgICAgIGxlZnQ6IDUwJTtcclxuICAgICAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcclxuICAgICAgICB3aWR0aDogNDAwcHg7XHJcbiAgICAgICAgcGFkZGluZzogNDBweCAxNXB4IDE1cHggMTVweDtcclxuICAgICAgICBib3JkZXItcmFkaXVzOiAxNXB4O1xyXG4gICAgICAgIGJveC1zaGFkb3c6IDAgMCAxMHB4IDAgIzMzMztcclxuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjRkZGO1xyXG5cclxuICAgICAgICAuaW5wdXQtYXJlYSB7XHJcbiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDI1cHg7XHJcbiAgICAgICAgfVxyXG5cclxuICAgICAgICAuY2hlY2tib3gge1xyXG4gICAgICAgICAgICBkaXNwbGF5OiBibG9jaztcclxuICAgICAgICAgICAgbWFyZ2luLWJvdHRvbTogMTBweDtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgIC5zZXJ2ZXItZXJyb3Ige1xyXG4gICAgICAgICAgICBjb2xvcjogJHByaW1hcnlSZWQ7XHJcbiAgICAgICAgICAgIGZvbnQtc2l6ZTogLjhyZW07XHJcbiAgICAgICAgfVxyXG5cclxuICAgICAgICAuZm9ybS1hY3Rpb25zIHtcclxuICAgICAgICAgICAgbWFyZ2luLXRvcDogMjVweDtcclxuXHJcbiAgICAgICAgICAgIGlucHV0W3R5cGU9XCJzdWJtaXRcIl0ge1xyXG4gICAgICAgICAgICAgICAgZGlzcGxheTogYmxvY2s7XHJcbiAgICAgICAgICAgICAgICBtYXJnaW46IDAgYXV0bztcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgIH1cclxuXHJcbiAgICAuZ2l0aHViLWJ0biB7XHJcbiAgICAgICAgcG9zaXRpb246IGFic29sdXRlO1xyXG4gICAgICAgIGJvdHRvbTogMTVweDtcclxuICAgICAgICBsZWZ0OiA1MCU7XHJcbiAgICAgICAgdHJhbnNmb3JtOiB0cmFuc2xhdGVYKC01MCUpO1xyXG4gICAgICAgIHBhZGRpbmc6IDA7XHJcbiAgICAgICAgaGVpZ2h0OiA0MHB4O1xyXG4gICAgICAgIHdpZHRoOiA0MHB4O1xyXG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDUwJTtcclxuXHJcbiAgICAgICAgc3ZnIHtcclxuICAgICAgICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcclxuICAgICAgICB9XHJcblxyXG4gICAgICAgICY6aG92ZXIge1xyXG4gICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjRkZGO1xyXG5cclxuICAgICAgICAgICAgc3ZnIHBhdGgge1xyXG4gICAgICAgICAgICAgICAgZmlsbDogJHByaW1hcnlSZWQ7XHJcbiAgICAgICAgICAgIH1cclxuICAgICAgICB9XHJcbiAgICB9XHJcbn0iLCIqLCAqOmJlZm9yZSwgKjphZnRlciB7XHJcbiAgICBmb250LWZhbWlseTogQXJ2bywgSGVsdmV0aWNhLCBzYW5zLXNlcmlmO1xyXG59XHJcblxyXG5idXR0b24ge1xyXG4gICAgdGV4dC10cmFuc2Zvcm06IGNhcGl0YWxpemU7XHJcbn1cclxuXHJcbi5ob21lcGFnZS13cmFwcGVyLCAubG9naW4td3JhcHBlciwgLmVycm9yLWNvbnRhaW5lciB7XHJcbiAgICBwb3NpdGlvbjogZml4ZWQ7XHJcbiAgICB3aWR0aDogMTAwJTtcclxuICAgIGhlaWdodDogMTAwJTtcclxufVxyXG5cclxuLmhvbWVwYWdlLXdyYXBwZXIsIC5sb2dpbi13cmFwcGVyIHtcclxuICAgIHN2Zy5zaGFwZS1yaWdodCxcclxuICAgIHN2Zy5zaGFwZS1sZWZ0IHtcclxuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XHJcbiAgICAgICAgYm90dG9tOiAwO1xyXG4gICAgICAgIHotaW5kZXg6IC0xO1xyXG4gICAgfVxyXG5cclxuICAgIHN2Zy5zaGFwZS1sZWZ0IHtcclxuICAgICAgICBsZWZ0OiAwO1xyXG4gICAgfVxyXG4gICAgc3ZnLnNoYXBlLXJpZ2h0IHtcclxuICAgICAgICByaWdodDogMDtcclxuICAgIH1cclxufSJdfQ== */
4 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | getQueryString(),
16 | $request->getMethod()
17 | );
18 | $container = new DIC(include(dirname(__DIR__) . '/config/services.php'));
19 |
20 | $app = new AppHandler($request, $dispatcher, $container);
21 | $app->init();
22 |
23 | $response = $app->handle();
24 | if ($response instanceof Response) {
25 | $response->send();
26 | }
27 |
--------------------------------------------------------------------------------
/public/vendor/fetch-umd-polyfill.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Minified by jsDelivr using Terser v3.14.1.
3 | * Original file: /npm/whatwg-fetch@3.0.1/dist/fetch.umd.js
4 | *
5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 | */
7 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.WHATWGFetch={})}(this,function(t){"use strict";var e={searchParams:"URLSearchParams"in self,iterable:"Symbol"in self&&"iterator"in Symbol,blob:"FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in self,arrayBuffer:"ArrayBuffer"in self};if(e.arrayBuffer)var r=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],o=ArrayBuffer.isView||function(t){return t&&r.indexOf(Object.prototype.toString.call(t))>-1};function n(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(t)||""===t)throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function i(t){return"string"!=typeof t&&(t=String(t)),t}function s(t){var r={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return e.iterable&&(r[Symbol.iterator]=function(){return r}),r}function a(t){this.map={},t instanceof a?t.forEach(function(t,e){this.append(e,t)},this):Array.isArray(t)?t.forEach(function(t){this.append(t[0],t[1])},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function f(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function h(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function u(t){var e=new FileReader,r=h(e);return e.readAsArrayBuffer(t),r}function d(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function c(){return this.bodyUsed=!1,this._initBody=function(t){var r;this.bodyUsed=this.bodyUsed,this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:e.blob&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:e.formData&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:e.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():e.arrayBuffer&&e.blob&&((r=t)&&DataView.prototype.isPrototypeOf(r))?(this._bodyArrayBuffer=d(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):e.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(t)||o(t))?this._bodyArrayBuffer=d(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):e.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},e.blob&&(this.blob=function(){var t=f(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?f(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(u)}),this.text=function(){var t,e,r,o=f(this);if(o)return o;if(this._bodyBlob)return t=this._bodyBlob,e=new FileReader,r=h(e),e.readAsText(t),r;if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o-1?o:r),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(n)}function p(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function b(t,e){e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"",this.headers=new a(e.headers),this.url=e.url||"",this._initBody(t)}l.prototype.clone=function(){return new l(this,{body:this._bodyInit})},c.call(l.prototype),c.call(b.prototype),b.prototype.clone=function(){return new b(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new a(this.headers),url:this.url})},b.error=function(){var t=new b(null,{status:0,statusText:""});return t.type="error",t};var m=[301,302,303,307,308];b.redirect=function(t,e){if(-1===m.indexOf(e))throw new RangeError("Invalid status code");return new b(null,{status:e,headers:{location:t}})},t.DOMException=self.DOMException;try{new t.DOMException}catch(e){t.DOMException=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function w(r,o){return new Promise(function(n,i){var s=new l(r,o);if(s.signal&&s.signal.aborted)return i(new t.DOMException("Aborted","AbortError"));var f=new XMLHttpRequest;function h(){f.abort()}f.onload=function(){var t,e,r={status:f.status,statusText:f.statusText,headers:(t=f.getAllResponseHeaders()||"",e=new a,t.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}}),e)};r.url="responseURL"in f?f.responseURL:r.headers.get("X-Request-URL");var o="response"in f?f.response:f.responseText;setTimeout(function(){n(new b(o,r))},0)},f.onerror=function(){setTimeout(function(){i(new TypeError("Network request failed"))},0)},f.ontimeout=function(){setTimeout(function(){i(new TypeError("Network request failed"))},0)},f.onabort=function(){setTimeout(function(){i(new t.DOMException("Aborted","AbortError"))},0)},f.open(s.method,function(t){try{return""===t&&self.location.href?self.location.href:t}catch(e){return t}}(s.url),!0),"include"===s.credentials?f.withCredentials=!0:"omit"===s.credentials&&(f.withCredentials=!1),"responseType"in f&&(e.blob?f.responseType="blob":e.arrayBuffer&&s.headers.get("Content-Type")&&-1!==s.headers.get("Content-Type").indexOf("application/octet-stream")&&(f.responseType="arraybuffer")),s.headers.forEach(function(t,e){f.setRequestHeader(e,t)}),s.signal&&(s.signal.addEventListener("abort",h),f.onreadystatechange=function(){4===f.readyState&&s.signal.removeEventListener("abort",h)}),f.send(void 0===s._bodyInit?null:s._bodyInit)})}w.polyfill=!0,self.fetch||(self.fetch=w,self.Headers=a,self.Request=l,self.Response=b),t.Headers=a,t.Request=l,t.Response=b,t.fetch=w,Object.defineProperty(t,"__esModule",{value:!0})});
8 | //# sourceMappingURL=/sm/a3f6f1c85ee4d5589ae21b827111ab8469dd6d2f7c74740e8fb47e05fd421627.map
--------------------------------------------------------------------------------
/public/vendor/filemanager-template.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";var e,t;function r(e){var t,r=fmWrapper.querySelector(".theme-option[data-theme="+e+"]");r&&(fmWrapper.querySelectorAll(".theme-option").forEach(function(e){e.classList.remove("selected")}),r.classList.add("selected"),t=[],fmWrapper.querySelectorAll(".theme-option").forEach(function(e){t.push(e.getAttribute("data-theme"))}),fmWrapper.classList.forEach(function(e){t.forEach(function(e){fmWrapper.classList.remove(e)})}),fmWrapper.classList.add(e),localStorage.setItem("fm-theme",e))}window.fmWrapper=document.querySelector(".fm-wrapper"),window.addEventListener("keyup",function(e){var t;"Enter"!==e.key||"object"==typeof(t=fmWrapper.querySelector('.modal:not(.editor-modal).show .modal-footer button:not([data-close="modal"])'))&&t.dispatchEvent(new MouseEvent("click",{bubbles:!0,cancelable:!0}))}),Element.prototype.closest||((e=Element.prototype).matches||(e.matches=e.msMatchesSelector||e.webkitMatchesSelector),e.closest=function(e){var t=this;if(t.matches(e))return t;for(;t.parentElement;){var r=t.parentElement;if(r.matches(e))return r;t=t.parentElement}}),NodeList.prototype.forEach||(NodeList.prototype.forEach=function(e,t){for(var r=0;rn;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o});
2 |
--------------------------------------------------------------------------------
/src/AppHandler.php:
--------------------------------------------------------------------------------
1 | request = $request;
35 | $this->dispatcher = $dispatcher;
36 | $this->container = $container;
37 | }
38 |
39 | /**
40 | * Initializes the application.
41 | *
42 | * @return void
43 | */
44 | public function init()
45 | {
46 | // Set the base controller container
47 | Controller::setContainer($this->container);
48 | // Make the configuration info available in the base controller
49 | Controller::setConfig(include(dirname(__DIR__).'/config/app.php'));
50 | }
51 |
52 | /**
53 | * Handles the request and returns the appropriate response.
54 | *
55 | * @return mixed
56 | */
57 | public function handle()
58 | {
59 | $this->dispatcher->notFoundedHandler(function () {
60 | return (new ErrorController($this->request))->index(404);
61 | });
62 |
63 | $this->dispatcher->methodNotAllowedHandler(function ($routeInfo) {
64 | return (new ErrorController($this->request))->index(405, [
65 | /**
66 | * Sending the 'Allow' header including the allowed methods
67 | * for the request as defined in RFC 7231.
68 | *
69 | * @link https://tools.ietf.org/html/rfc7231#section-6.5.5
70 | */
71 | 'Allow' => implode(', ', $routeInfo[0]),
72 | ]);
73 | });
74 |
75 | // Define the founded handler as a dispatcher callback
76 | return $this->dispatcher->dispatch(function ($routeInfo) {
77 | $handler = $routeInfo[2];
78 |
79 | // Callback
80 | if (!is_array($handler)) {
81 | return call_user_func_array($handler, $routeInfo[3]);
82 | }
83 |
84 | // Controller
85 | $class = $handler[0];
86 | $method = $handler[1];
87 |
88 | $controller = new $class($this->request);
89 | if (!method_exists($controller, $method)) {
90 | throw new RouteLogicException("Invoking a non existing controller method [$method].");
91 | }
92 |
93 | $before = call_user_func([$controller, 'before']);
94 | if (!is_null($before)) {
95 | return $before;
96 | }
97 |
98 | return call_user_func_array(
99 | [$controller, $method],
100 | isset($handler[2]) ? $handler[2] : []
101 | );
102 | });
103 | }
104 | }
--------------------------------------------------------------------------------
/src/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | request = $request;
32 | }
33 |
34 | /**
35 | * @param DIC $container
36 | */
37 | public static function setContainer($container)
38 | {
39 | self::$container = $container;
40 | }
41 |
42 | /**
43 | * @return DIC
44 | */
45 | public static function getContainer()
46 | {
47 | return self::$container;
48 | }
49 |
50 | /**
51 | * @return array
52 | */
53 | public static function getConfig()
54 | {
55 | return self::$config;
56 | }
57 |
58 | /**
59 | * @param array $config
60 | */
61 | public static function setConfig($config)
62 | {
63 | self::$config = $config;
64 | }
65 |
66 | /**
67 | * @param string $name
68 | * @param string $definition
69 | *
70 | * @return void
71 | */
72 | public static function set($name, $definition)
73 | {
74 | self::$container->set($name, $definition);
75 | }
76 |
77 | /**
78 | * @param string $name
79 | *
80 | * @return mixed
81 | */
82 | public static function get($name)
83 | {
84 | return self::$container->get($name);
85 | }
86 |
87 | /**
88 | * Renders a view.
89 | *
90 | * @param string $view
91 | * @param array $params
92 | *
93 | * @return string
94 | */
95 | public function render($view, $params = [])
96 | {
97 | if (!$this->has('Renderer')) {
98 | throw new \LogicException("The renderer service not registered for the base controller.");
99 | }
100 |
101 | return self::get('Renderer')->render($view, $params);
102 | }
103 |
104 | /**
105 | * @param string $name
106 | *
107 | * @return bool
108 | */
109 | public function has($name)
110 | {
111 | return self::$container->has($name);
112 | }
113 |
114 | /**
115 | * Renders a view and returns the result as an http response.
116 | *
117 | * @param string $view
118 | * @param array $params
119 | * @param int $statusCode
120 | * @param array $headers
121 | *
122 | * @return string
123 | */
124 | public function renderWithResponse($view, $params = [], $statusCode = 200, $headers = [])
125 | {
126 | return new HttpResponse($this->render($view, $params), $statusCode, $headers);
127 | }
128 |
129 | /**
130 | * Generates a dynamic url from a route path.
131 | *
132 | * @param string $name
133 | * @param array $params
134 | *
135 | * @return string
136 | */
137 | public function generateUrl($name, $params = [])
138 | {
139 | if (!$this->has('RouteUrlGenerator')) {
140 | throw new \LogicException("The RouteUrlGenerator service not registered for the base controller.");
141 | }
142 |
143 | return self::get('RouteUrlGenerator')->generate($name, $params);
144 | }
145 |
146 | /**
147 | * Makes a simple http redirection.
148 | *
149 | * @param string $uri
150 | * @param int $statusCode
151 | * @param array $headers
152 | *
153 | * @return HttpRedirect
154 | */
155 | public function redirect($uri, $statusCode = 301, $headers = [])
156 | {
157 | return new HttpRedirect($uri, $statusCode, $headers);
158 | }
159 |
160 | /**
161 | * Redirects to the giving route.
162 | *
163 | * @param string $route
164 | * @param int $statusCode
165 | * @param array $headers
166 | *
167 | * @return HttpRedirect
168 | */
169 | public function redirectToRoute($route, $statusCode = 301, $headers = [])
170 | {
171 | return new HttpRedirect($this->generateUrl($route), $statusCode, $headers);
172 | }
173 |
174 | /**
175 | * Gets the configured session service.
176 | *
177 | * @return Session
178 | */
179 | public function session()
180 | {
181 | if (!$this->has('Session')) {
182 | throw new \LogicException("Session service not registered for the base controller.");
183 | }
184 |
185 | return self::get('Session');
186 | }
187 |
188 | /**
189 | * Gets the session storage service.
190 | *
191 | * @return SessionStorage
192 | */
193 | public function sessionStorage()
194 | {
195 | if (!$this->has('Session')) {
196 | throw new \LogicException("SessionStorage service not available in the base controller.");
197 | }
198 |
199 | return self::get('SessionStorage');
200 | }
201 |
202 | /**
203 | * Gets the http request parameters.
204 | *
205 | * @return array
206 | */
207 | public function getParameters()
208 | {
209 | return $this->request->getParameters();
210 | }
211 |
212 | /**
213 | * @return FtpAdapter
214 | */
215 | public function ftpAdapter()
216 | {
217 | if (!$this->has('FtpAdapter')) {
218 | throw new \LogicException("FtpAdapter service not registered.");
219 | }
220 |
221 | return self::get('FtpAdapter');
222 | }
223 |
224 | /**
225 | * Executes some code before calling the controller method for the request.
226 | *
227 | * @return mixed
228 | */
229 | public function before()
230 | {
231 |
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/Controllers/Error/ErrorController.php:
--------------------------------------------------------------------------------
1 | renderWithResponse('error',
12 | [ 'errorCode' => $errorCode ],
13 | $errorCode,
14 | $headers
15 | );
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Controllers/Filemanager/FilemanagerController.php:
--------------------------------------------------------------------------------
1 | session()->start();
24 |
25 | $loggedIn = $this->sessionStorage()->get('loggedIn');
26 | $lastLoginTime = $this->sessionStorage()->get('lastLoginTime');
27 |
28 | if (is_bool($loggedIn) && $loggedIn) {
29 | // Check inactivity timeout
30 | if (time() - $lastLoginTime > self::getConfig()['inactivityTimeout'] * 60) {
31 | if ($this->request->isAjaxRequest()) {
32 | return new JsonResponse(['location' => $this->generateUrl('login')], 401);
33 | } else {
34 | return $this->redirectToRoute('login');
35 | }
36 | }
37 |
38 | // Restart inactivity timeout
39 | $this->sessionStorage()->set('lastLoginTime', time());
40 |
41 | $config = array_merge($this->sessionStorage()->get('config'), self::getConfig()['ftp']);
42 |
43 | if (is_array($config) && is_bool($loggedIn) && $loggedIn) {
44 | $this->ftpAdapter()->openConnection($config);
45 | }
46 | } else {
47 | if ($this->request->isAjaxRequest()) {
48 | return new JsonResponse(['location' => $this->generateUrl('login')], 401);
49 | } else {
50 | return $this->redirectToRoute('login');
51 | }
52 | }
53 | } catch (FtpAdapterException $ex) {
54 | if ($this->request->isAjaxRequest()) {
55 | return new JsonResponse(['error' => $ex->getMessage()], 500);
56 | } else {
57 | return $this->redirectToRoute('login');
58 | }
59 | }
60 | }
61 |
62 | public function index()
63 | {
64 | // If is not logged in make a redirection to login page.
65 | $loggedIn = $this->sessionStorage()->get('loggedIn');
66 | if (is_bool($loggedIn) && !$loggedIn) {
67 | return $this->redirectToRoute('login');
68 | }
69 |
70 | // Regenerate the session ID.
71 | $this->session()->regenerateID(true);
72 |
73 | return $this->renderWithResponse('filemanager', [
74 | 'homeUrl' => $this->generateUrl('home'),
75 | 'loginUrl' => $this->generateUrl('login'),
76 | 'appName' => self::getConfig()['appName']
77 | ]);
78 | }
79 |
80 | public function browse()
81 | {
82 | try {
83 | return new JsonResponse([
84 | 'result' => $this->ftpAdapter()->browse($this->getParameters()['path']),
85 | ]);
86 | } catch (FtpAdapterException $ex) {
87 | return new JsonResponse(['error' => $ex->getMessage()], 500);
88 | }
89 | }
90 |
91 | public function addFile()
92 | {
93 | try {
94 | $params = $this->request->getJSONBodyParameters();
95 | return new JsonResponse([
96 | 'result' => $this->ftpAdapter()->addFile($params['path'] . $params['name']),
97 | ], 201);
98 | } catch (FtpAdapterException $ex) {
99 | return new JsonResponse(['error' => $ex->getMessage()], 500);
100 | }
101 | }
102 |
103 | public function addFolder()
104 | {
105 | try {
106 | $params = $this->request->getJSONBodyParameters();
107 | return new JsonResponse([
108 | 'result' => $this->ftpAdapter()->addFolder($params['path'] . $params['name']),
109 | ], 201);
110 | } catch (FtpAdapterException $ex) {
111 | return new JsonResponse(['error' => $ex->getMessage()], 500);
112 | }
113 | }
114 |
115 | public function getFileContent()
116 | {
117 | try {
118 | return new JsonResponse([
119 | 'result' => utf8_encode($this->ftpAdapter()->getFileContent($this->request->getParameters()['file'])),
120 | ]);
121 | } catch (FtpAdapterException $ex) {
122 | return new JsonResponse(['error' => $ex->getMessage()], 500);
123 | }
124 | }
125 |
126 | public function updateFileContent()
127 | {
128 | try {
129 | return new JsonResponse([
130 | 'result' => $this->ftpAdapter()->updateFileContent(
131 | $this->request->getJSONBodyParameters()['file'],
132 | $this->request->getJSONBodyParameters()['content']
133 | ),
134 | ]);
135 | } catch (FtpAdapterException $ex) {
136 | return new JsonResponse(['error' => $ex->getMessage()], 500);
137 | }
138 | }
139 |
140 | public function remove()
141 | {
142 | try {
143 | return new JsonResponse([
144 | 'result' => $this->ftpAdapter()->remove($this->request->getJSONBodyParameters()['files']),
145 | ]);
146 | } catch (FtpAdapterException $ex) {
147 | return new JsonResponse(['error' => $ex->getMessage()], 500);
148 | }
149 | }
150 |
151 | public function rename()
152 | {
153 | try {
154 | $params = $this->request->getJSONBodyParameters();
155 | $path = $params['path'];
156 | return new JsonResponse([
157 | 'result' => $this->ftpAdapter()->rename(ltrim($path . $params['file'], '/'), $path . $params['newName']),
158 | ]);
159 | } catch (FtpAdapterException $ex) {
160 | return new JsonResponse(['error' => $ex->getMessage()], 500);
161 | }
162 | }
163 |
164 | public function getDirectoryTree()
165 | {
166 | try {
167 | return new JsonResponse([
168 | 'result' => $this->ftpAdapter()->getDirectoryTree(),
169 | ]);
170 | } catch (FtpAdapterException $ex) {
171 | return new JsonResponse(['error' => $ex->getMessage()], 500);
172 | }
173 | }
174 |
175 | public function move()
176 | {
177 | try {
178 | $params = $this->request->getJSONBodyParameters();
179 | return new JsonResponse([
180 | 'result' => $this->ftpAdapter()->move($params['path'] . $params['file'], $params['newPath']),
181 | ]);
182 | } catch (FtpAdapterException $ex) {
183 | return new JsonResponse(['error' => $ex->getMessage()], 500);
184 | }
185 | }
186 |
187 | public function permissions()
188 | {
189 | try {
190 | $params = $this->request->getJSONBodyParameters();
191 | return new JsonResponse([
192 | 'result' => $this->ftpAdapter()->permissions($params['path'] . $params['file'], $params['permissions']),
193 | ]);
194 | } catch (FtpAdapterException $ex) {
195 | return new JsonResponse(['error' => $ex->getMessage()], 500);
196 | }
197 | }
198 |
199 | public function download()
200 | {
201 | try {
202 | return $this->ftpAdapter()->download($this->getParameters()['file']);
203 | } catch (FtpAdapterException $ex) {
204 | return new JsonResponse(['error' => $ex->getMessage()], 500);
205 | }
206 | }
207 |
208 | public function upload()
209 | {
210 | try {
211 | $file = $this->request->getFiles()['file'];
212 | return new JsonResponse([
213 | 'result' => $this->ftpAdapter()->upload(
214 | $file['tmp_name'],
215 | $this->getParameters()['path'] . $file['name'],
216 | self::getConfig()['ftp']['resumeUpload']
217 | ),
218 | ]);
219 | } catch (FtpAdapterException $ex) {
220 | return new JsonResponse(['error' => $ex->getMessage()], 500);
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/Controllers/Home/HomeController.php:
--------------------------------------------------------------------------------
1 | session()->start();
12 | return $this->renderWithResponse('homepage', ['loginUrl' => $this->generateUrl('login')]);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Controllers/Login/LoginController.php:
--------------------------------------------------------------------------------
1 | session()->deleteCookie();
14 | // Destroy session data
15 | $this->session()->destroy();
16 | // Unset the session vars
17 | $this->sessionStorage()->unsetVars();
18 | // Start a new session
19 | $this->session()->start();
20 |
21 | return $this->renderWithResponse('login', ['homeUrl' => $this->generateUrl('home')]);
22 | }
23 |
24 | public function login()
25 | {
26 | try {
27 | $config = $this->request->getBodyParameters();
28 |
29 | // Try to open the a successful ftp connection
30 | $this->ftpAdapter()->openConnection(array_merge($config, self::getConfig()['ftp']));
31 |
32 | // Store the client connection configuration in the session
33 | $this->session()->start();
34 | $this->sessionStorage()->set('config', $config);
35 | $this->sessionStorage()->set('loggedIn', true);
36 | $this->sessionStorage()->set('lastLoginTime', time());
37 |
38 | } catch (FtpAdapterException $ex) {
39 | return $this->renderWithResponse('/login', [
40 | 'serverError' => $ex->getMessage(),
41 | 'homeUrl' => $this->generateUrl('home')
42 | ], 400);
43 | }
44 |
45 | return $this->redirectToRoute('filemanager');
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Modules/FtpAdapter/FtpAdapter.php:
--------------------------------------------------------------------------------
1 | open();
50 |
51 | $this->connection = $connection;
52 | $this->config = new FtpConfig($connection);
53 | $this->client = new FtpClient($connection);
54 |
55 | if (isset($config['usePassive']) && $config['usePassive']) {
56 | $this->config->setPassive($config['usePassive']);
57 | $this->config->setAutoSeek($config['autoSeek']);
58 | }
59 | } catch (FtpClientException $ex) {
60 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
61 | }
62 | }
63 |
64 | /**
65 | * @inheritDoc
66 | */
67 | public function browse($dir)
68 | {
69 | try {
70 | $list = $this->client->listDirDetails(trim($dir, '/'));
71 |
72 | $files = [];
73 | $dirs = [];
74 | foreach ($list as $file) {
75 | $info = [
76 | 'name' => $file['name'],
77 | 'type' => $file['type'],
78 | 'size' => $file['size'],
79 | 'modifiedTime' => sprintf("%s %s %s", $file['day'], $file['month'], $file['time']),
80 | 'permissions' => $file['chmod'],
81 | 'path' => $file['path'],
82 | 'owner' => $file['owner'],
83 | 'group' => $file['group'],
84 | ];
85 |
86 | if ($file['type'] === 'dir') {
87 | $dirs[] = $info;
88 | } else {
89 | $files[] = $info;
90 | }
91 | }
92 |
93 | return array_merge($dirs, $files);
94 | } catch (FtpClientException $ex) {
95 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
96 | }
97 | }
98 |
99 | public function addFile($file)
100 | {
101 | try {
102 | return $this->client->createFile(urldecode(ltrim($file, '/')));
103 | } catch (FtpClientException $ex) {
104 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
105 | }
106 | }
107 |
108 | public function addFolder($dir)
109 | {
110 | try {
111 | return $this->client->createDir(urldecode(ltrim($dir, '/')));
112 | } catch (FtpClientException $ex) {
113 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
114 | }
115 | }
116 |
117 | public function getFileContent($file)
118 | {
119 | try {
120 | return $this->client->getFileContent($file);
121 | } catch (FtpClientException $ex) {
122 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
123 | }
124 | }
125 |
126 | public function updateFileContent($file, $content)
127 | {
128 | try {
129 | $this->client->removeFile($file);
130 | return $this->client->createFile($file, $content);
131 | } catch (FtpClientException $ex) {
132 | throw new FtpClientAdapterException("Unable to edit file [$file].");
133 | }
134 | }
135 |
136 | public function remove($files)
137 | {
138 | try {
139 | foreach ($files as $file) {
140 | if ($this->client->isDir($file)) {
141 | $this->client->removeDir($file);
142 | } else {
143 | $this->client->removeFile($file);
144 | }
145 | }
146 | return true;
147 | } catch (FtpClientException $ex) {
148 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
149 | }
150 | }
151 |
152 | public function rename($file, $newName)
153 | {
154 | try {
155 | return $this->client->rename($file, $newName);
156 | } catch (FtpClientException $ex) {
157 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
158 | }
159 | }
160 |
161 | public function getDirectoryTree()
162 | {
163 | try {
164 | return $this->client->listDirDetails('/', true, FtpClient::DIR_TYPE);
165 | } catch (FtpClientException $ex) {
166 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
167 | }
168 | }
169 |
170 | public function move($file, $newPath)
171 | {
172 | try {
173 | return $this->client->move(ltrim($file, '/'), $newPath);
174 | } catch (FtpClientException $ex) {
175 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
176 | }
177 | }
178 |
179 | public function permissions($file, $permissions)
180 | {
181 | try {
182 | return $this->client->setPermissions($file, $permissions);
183 | } catch (FtpClientException $ex) {
184 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
185 | }
186 | }
187 |
188 | public function download($file)
189 | {
190 | try {
191 | $fileContent = $this->getFileContent($file);
192 | return (new HttpResponse($fileContent))
193 | ->addHeader('Content-Type', 'application/octet-stream')
194 | ->addHeader('Content-Disposition', 'attachment; filename=' . basename($file));
195 | } catch (\Exception $ex) {
196 | throw new FtpClientAdapterException("Failed to download file $file.");
197 | }
198 | }
199 |
200 | public function upload($filePath, $remotePath, $resume)
201 | {
202 | try {
203 | return $this->client->upload($filePath, $remotePath, $resume);
204 | } catch (FtpClientException $ex) {
205 | throw new FtpClientAdapterException($this->normalizeExceptionMessage($ex->getMessage()));
206 | }
207 | }
208 |
209 | /**
210 | * Normalize FtpClient exception messages.
211 | *
212 | * Example:
213 | *
214 | * from:
215 | * [ConnectionException] - Failed to connect to remote server.
216 | *
217 | * to:
218 | * Failed to connect to remote server.
219 | *
220 | * @param string $message
221 | *
222 | * @return string
223 | */
224 | protected function normalizeExceptionMessage($message)
225 | {
226 | return preg_replace('/([\[\w\]]+)\s-\s/i', '', $message);
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/Modules/FtpAdapter/FtpClient/FtpClientAdapterException.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | = $renderer->render('includes/meta.html') ?>
4 | = $renderer->render('includes/styles.php') ?>
5 | = 'Error ' . $errorCode ?>
6 |
7 |
8 |
9 | Error
10 |
= $errorCode ?>
11 |
12 |
13 |