diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index 97d81d71f..f5aa3dd77 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -142,6 +142,7 @@ class ServerController extends Controller { $server = Models\Server::getByUUID($uuid); $this->authorize('edit-files', $server); + $node = Models\Node::find($server->node); $fileInfo = (object) pathinfo($file); $controller = new FileRepository($uuid); @@ -159,9 +160,15 @@ class ServerController extends Controller return redirect()->route('server.files.index', $uuid); } + Javascript::put([ + 'server' => collect($server->makeVisible('daemonSecret'))->only(['uuid', 'uuidShort', 'daemonSecret', 'username']), + 'node' => collect($node)->only('fqdn', 'scheme', 'daemonListen'), + 'stat' => $fileContent['stat'], + ]); + return view('server.files.edit', [ 'server' => $server, - 'node' => Models\Node::find($server->node), + 'node' => $node, 'file' => $file, 'stat' => $fileContent['stat'], 'contents' => $fileContent['file']->content, diff --git a/package.json b/package.json index da55a0004..2a9697409 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,6 @@ "babel-preset-es2015": "6.18.0" }, "scripts": { - "build": "./node_modules/babel-cli/bin/babel.js public/js/files --source-maps --out-file public/js/filemanager.min.js" + "build": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js" } } diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 2f1aeb11a..df3c800b7 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -61,3 +61,45 @@ code { .middle { vertical-align: middle !important; } + +#fileOptionMenu.dropdown-menu > li > a { + padding:3px 6px; +} + +.hasFileHover { + border: 2px dashed #0087F7; + border-top: 0 !important; + border-radius: 5px; + margin: 0; + opacity: 0.5; +} + +.hasFileHover * { + pointer-events: none; +} + +td.has-progress { + padding: 0px !important; + border-top: 0px !important; +} + +.progress.progress-table-bottom { + margin: 0 !important; + height:5px !important; + padding:0; + border:0; +} + +.muted { + filter: alpha(opacity=20); + opacity: 0.2; +} + +.muted-hover:hover { + filter: alpha(opacity=100); + opacity: 1; +} + +.use-pointer { + cursor: pointer !important; +} diff --git a/public/themes/pterodactyl/js/frontend/files/editor.js b/public/themes/pterodactyl/js/frontend/files/editor.js new file mode 100644 index 000000000..d21156042 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/editor.js @@ -0,0 +1,73 @@ +// Copyright (c) 2015 - 2016 Dane Everitt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +$(document).ready(function () { + $('.server-files').addClass('active'); + const Editor = ace.edit('editor'); + const Modelist = ace.require('ace/ext/modelist') + + Editor.setTheme('ace/theme/chrome'); + Editor.getSession().setMode(Modelist.getModeForPath(Pterodactyl.stat.name).mode); + Editor.getSession().setUseWrapMode(true); + Editor.setShowPrintMargin(false); + + Editor.commands.addCommand({ + name: 'save', + bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, + exec: function(editor) { + save(); + }, + readOnly: false + }); + + $('#save_file').on('click', function (e) { + e.preventDefault(); + save(); + }); + + function save() { + var fileName = $('input[name="file"]').val(); + $('#save_file').html(' Saving File').addClass('disabled'); + $.ajax({ + type: 'POST', + url: Router.route('server.files.save', { server: Pterodactyl.server.uuidShort }), + headers: { + 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), + }, + data: { + file: fileName, + contents: Editor.getValue() + } + }).done(function (data) { + $.notify({ + message: 'File was successfully saved.' + }, { + type: 'success' + }); + }).fail(function (jqXHR) { + $.notify({ + message: jqXHR.responseText + }, { + type: 'danger' + }); + }).always(function () { + $('#save_file').html('  Save File').removeClass('disabled'); + }); + } +}); diff --git a/public/themes/pterodactyl/js/frontend/files/filemanager.min.js b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js new file mode 100644 index 000000000..9ef135600 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js @@ -0,0 +1,2 @@ + +//# sourceMappingURL=filemanager.min.js.map \ No newline at end of file diff --git a/public/themes/pterodactyl/js/frontend/files/filemanager.min.js.map b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js.map new file mode 100644 index 000000000..d8eb44cf0 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":[],"names":[],"mappings":"","file":"filemanager.min.js"} \ No newline at end of file diff --git a/public/themes/pterodactyl/js/frontend/files/src/actions.js b/public/themes/pterodactyl/js/frontend/files/src/actions.js new file mode 100644 index 000000000..9ff978539 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/src/actions.js @@ -0,0 +1,394 @@ +"use strict"; + +// Copyright (c) 2015 - 2016 Dane Everitt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +class ActionsClass { + constructor(element, menu) { + this.element = element; + this.menu = menu; + } + + destroy() { + this.element = undefined; + } + + folder() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const currentName = decodeURIComponent(nameBlock.attr('data-name')); + const currentPath = decodeURIComponent(nameBlock.data('path')); + + let inputValue = `${currentPath}${currentName}/`; + if ($(this.element).data('type') === 'file') { + inputValue = currentPath; + } + swal({ + type: 'input', + title: 'Create Folder', + text: 'Please enter the path and folder name below.', + showCancelButton: true, + showConfirmButton: true, + closeOnConfirm: false, + showLoaderOnConfirm: true, + inputValue: inputValue + }, (val) => { + $.ajax({ + type: 'POST', + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/folder`, + timeout: 10000, + data: JSON.stringify({ + path: val, + }), + }).done(data => { + swal.close(); + Files.list(); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + swal({ + type: 'error', + title: '', + text: error, + }); + }); + }); + } + + move() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const currentName = decodeURIComponent(nameBlock.attr('data-name')); + const currentPath = decodeURIComponent(nameBlock.data('path')); + + swal({ + type: 'input', + title: 'Move File', + text: 'Please enter the new path for the file below.', + showCancelButton: true, + showConfirmButton: true, + closeOnConfirm: false, + showLoaderOnConfirm: true, + inputValue: `${currentPath}${currentName}`, + }, (val) => { + $.ajax({ + type: 'POST', + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/move`, + timeout: 10000, + data: JSON.stringify({ + from: `${currentPath}${currentName}`, + to: `${val}`, + }), + }).done(data => { + nameBlock.parent().addClass('warning').delay(200).fadeOut(); + swal.close(); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + swal({ + type: 'error', + title: '', + text: error, + }); + }); + }); + + } + + rename() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const currentLink = nameBlock.find('a'); + const currentName = decodeURIComponent(nameBlock.attr('data-name')); + const attachEditor = ` + + + `; + + nameBlock.html(attachEditor); + const inputField = nameBlock.find('input'); + const inputLoader = nameBlock.find('.input-loader'); + + inputField.focus(); + inputField.on('blur keydown', e => { + // Save Field + if ( + (e.type === 'keydown' && e.which === 27) + || e.type === 'blur' + || (e.type === 'keydown' && e.which === 13 && currentName === inputField.val()) + ) { + if (!_.isEmpty(currentLink)) { + nameBlock.html(currentLink); + } else { + nameBlock.html(currentName); + } + inputField.remove(); + ContextMenu.unbind().run(); + return; + } + + if (e.type === 'keydown' && e.which !== 13) return; + + inputLoader.show(); + const currentPath = decodeURIComponent(nameBlock.data('path')); + + $.ajax({ + type: 'POST', + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/rename`, + timeout: 10000, + data: JSON.stringify({ + from: `${currentPath}${currentName}`, + to: `${currentPath}${inputField.val()}`, + }), + }).done(data => { + nameBlock.attr('data-name', inputField.val()); + if (!_.isEmpty(currentLink)) { + let newLink = currentLink.attr('href'); + if (nameBlock.parent().data('type') !== 'folder') { + newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val(); + } + currentLink.attr('href', newLink); + nameBlock.html( + currentLink.html(inputField.val()) + ); + } else { + nameBlock.html(inputField.val()); + } + inputField.remove(); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + nameBlock.addClass('has-error').delay(2000).queue(() => { + nameBlock.removeClass('has-error').dequeue(); + }); + inputField.popover({ + animation: true, + placement: 'top', + content: error, + title: 'Save Error' + }).popover('show'); + }).always(() => { + inputLoader.remove(); + ContextMenu.unbind().run(); + }); + }); + } + + copy() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const currentName = decodeURIComponent(nameBlock.attr('data-name')); + const currentPath = decodeURIComponent(nameBlock.data('path')); + + swal({ + type: 'input', + title: 'Copy File', + text: 'Please enter the new path for the copied file below.', + showCancelButton: true, + showConfirmButton: true, + closeOnConfirm: false, + showLoaderOnConfirm: true, + inputValue: `${currentPath}${currentName}`, + }, (val) => { + $.ajax({ + type: 'POST', + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/copy`, + timeout: 10000, + data: JSON.stringify({ + from: `${currentPath}${currentName}`, + to: `${val}`, + }), + }).done(data => { + swal({ + type: 'success', + title: '', + text: 'File successfully copied.' + }); + Files.list(); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + swal({ + type: 'error', + title: '', + text: error, + }); + }); + }); + } + + download() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const fileName = decodeURIComponent(nameBlock.attr('data-name')); + const filePath = decodeURIComponent(nameBlock.data('path')); + + window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`; + } + + delete() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const delPath = decodeURIComponent(nameBlock.data('path')); + const delName = decodeURIComponent(nameBlock.data('name')); + + swal({ + type: 'warning', + title: '', + text: 'Are you sure you want to delete ' + delName + '? There is no reversing this action.', + html: true, + showCancelButton: true, + showConfirmButton: true, + closeOnConfirm: false, + showLoaderOnConfirm: true + }, () => { + $.ajax({ + type: 'DELETE', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/f/${delPath}${delName}`, + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + } + }).done(data => { + nameBlock.parent().addClass('warning').delay(200).fadeOut(); + swal({ + type: 'success', + title: 'File Deleted' + }); + }).fail(jqXHR => { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + html: true, + text: 'An error occured while attempting to delete this file. Please try again.', + }); + }); + }); + } + + decompress() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const compPath = decodeURIComponent(nameBlock.data('path')); + const compName = decodeURIComponent(nameBlock.data('name')); + + swal({ + title: ' Decompressing...', + text: 'This might take a few seconds to complete.', + html: true, + allowOutsideClick: false, + allowEscapeKey: false, + showConfirmButton: false, + }); + + $.ajax({ + type: 'POST', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/decompress`, + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({ + files: `${compPath}${compName}` + }) + }).done(data => { + swal.close(); + Files.list(compPath); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + swal({ + type: 'error', + title: 'Whoops!', + html: true, + text: error + }); + }); + } + + compress() { + const nameBlock = $(this.element).find('td[data-identifier="name"]'); + const compPath = decodeURIComponent(nameBlock.data('path')); + const compName = decodeURIComponent(nameBlock.data('name')); + + $.ajax({ + type: 'POST', + url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/compress`, + headers: { + 'X-Access-Token': Pterodactyl.server.daemonSecret, + 'X-Access-Server': Pterodactyl.server.uuid, + }, + contentType: 'application/json; charset=utf-8', + data: JSON.stringify({ + files: `${compPath}${compName}`, + to: compPath.toString() + }) + }).done(data => { + Files.list(compPath, err => { + if (err) return; + const fileListing = $('#file_listing').find(`[data-name="${data.saved_as}"]`).parent(); + fileListing.addClass('success pulsate').delay(3000).queue(() => { + fileListing.removeClass('success pulsate').dequeue(); + }); + }); + }).fail(jqXHR => { + console.error(jqXHR); + var error = 'An error occured while trying to process this request.'; + if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { + error = jqXHR.responseJSON.error; + } + swal({ + type: 'error', + title: 'Whoops!', + html: true, + text: error + }); + }); + } +} diff --git a/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js b/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js new file mode 100644 index 000000000..f61b7bb28 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js @@ -0,0 +1,195 @@ +"use strict"; + +// Copyright (c) 2015 - 2016 Dane Everitt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +class ContextMenuClass { + constructor() { + this.activeLine = null; + } + + run() { + this.directoryClick(); + this.rightClick(); + } + + makeMenu(parent) { + $(document).find('#fileOptionMenu').remove(); + if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active'); + + let newFilePath = $('#headerTableRow').attr('data-currentDir'); + if (parent.data('type') === 'folder') { + const nameBlock = parent.find('td[data-identifier="name"]'); + const currentName = decodeURIComponent(nameBlock.attr('data-name')); + const currentPath = decodeURIComponent(nameBlock.data('path')); + newFilePath = `${currentPath}${currentName}`; + } + + let buildMenu = ''; + return buildMenu; + } + + rightClick() { + $('[data-action="toggleMenu"]').on('mousedown', () => { + event.preventDefault(); + this.showMenu(event); + }); + $('#file_listing > tbody td').on('contextmenu', event => { + this.showMenu(event); + }); + } + + showMenu(event) { + const parent = $(event.target).closest('tr'); + const menu = $(this.makeMenu(parent)); + + if (parent.data('type') === 'disabled') return; + event.preventDefault(); + + $(menu).appendTo('body'); + $(menu).data('invokedOn', $(event.target)).show().css({ + position: 'absolute', + left: event.pageX - 150, + top: event.pageY, + }); + + this.activeLine = parent; + this.activeLine.addClass('active'); + + // Handle Events + const Actions = new ActionsClass(parent, menu); + if (Pterodactyl.permissions.moveFiles) { + $(menu).find('li[data-action="move"]').unbind().on('click', e => { + e.preventDefault(); + Actions.move(); + }); + $(menu).find('li[data-action="rename"]').unbind().on('click', e => { + e.preventDefault(); + Actions.rename(); + }); + } + + if (Pterodactyl.permissions.copyFiles) { + $(menu).find('li[data-action="copy"]').unbind().on('click', e => { + e.preventDefault(); + Actions.copy(); + }); + } + + if (Pterodactyl.permissions.compressFiles) { + if (parent.data('type') === 'folder') { + $(menu).find('li[data-action="compress"]').removeClass('hidden'); + } + $(menu).find('li[data-action="compress"]').unbind().on('click', e => { + e.preventDefault(); + Actions.compress(); + }); + } + + if (Pterodactyl.permissions.decompressFiles) { + if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) { + $(menu).find('li[data-action="decompress"]').removeClass('hidden'); + } + $(menu).find('li[data-action="decompress"]').unbind().on('click', e => { + e.preventDefault(); + Actions.decompress(); + }); + } + + if (Pterodactyl.permissions.createFiles) { + $(menu).find('li[data-action="folder"]').unbind().on('click', e => { + e.preventDefault(); + Actions.folder(); + }); + } + + if (Pterodactyl.permissions.downloadFiles) { + if (parent.data('type') === 'file') { + $(menu).find('li[data-action="download"]').removeClass('hidden'); + } + $(menu).find('li[data-action="download"]').unbind().on('click', e => { + e.preventDefault(); + Actions.download(); + }); + } + + if (Pterodactyl.permissions.deleteFiles) { + $(menu).find('li[data-action="delete"]').unbind().on('click', e => { + e.preventDefault(); + Actions.delete(); + }); + } + + $(window).on('click', () => { + $(menu).remove(); + if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active'); + }); + } + + directoryClick() { + $('a[data-action="directory-view"]').on('click', function (event) { + event.preventDefault(); + + const path = $(this).parent().data('path') || ''; + const name = $(this).parent().data('name') || ''; + + window.location.hash = encodeURIComponent(path + name); + Files.list(); + }); + } +} + +window.ContextMenu = new ContextMenuClass; diff --git a/public/themes/pterodactyl/js/frontend/files/src/index.js b/public/themes/pterodactyl/js/frontend/files/src/index.js new file mode 100644 index 000000000..7165397ed --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/src/index.js @@ -0,0 +1,87 @@ +"use strict"; + +// Copyright (c) 2015 - 2016 Dane Everitt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +class FileManager { + constructor() { + this.list(this.decodeHash()); + } + + list(path, next) { + if (_.isUndefined(path)) { + path = this.decodeHash(); + } + + this.loader(true); + $.ajax({ + type: 'POST', + url: Pterodactyl.meta.directoryList, + headers: { + 'X-CSRF-Token': Pterodactyl.meta.csrftoken, + }, + data: { + directory: path, + }, + }).done(data => { + this.loader(false); + $('#load_files').slideUp(10).html(data).slideDown(10, () => { + ContextMenu.run(); + this.reloadFilesButton(); + if (_.isFunction(next)) { + return next(); + } + }); + $('#internal_alert').slideUp(); + }).fail(jqXHR => { + this.loader(false); + if (_.isFunction(next)) { + return next(new Error('Failed to load file listing.')); + } + swal({ + type: 'error', + title: 'File Error', + text: 'An error occured while attempting to process this request. Please try again.', + }); + console.error(jqXHR); + }); + } + + loader(show) { + if (show){ + $('.file-overlay').fadeIn(100); + } else { + $('.file-overlay').fadeOut(100); + } + } + + reloadFilesButton() { + $('i[data-action="reload-files"]').unbind().on('click', () => { + $('i[data-action="reload-files"]').addClass('fa-spin'); + this.list(); + }); + } + + decodeHash() { + return decodeURIComponent(window.location.hash.substring(1)); + } + +} + +window.Files = new FileManager; diff --git a/public/themes/pterodactyl/js/frontend/files/upload.js b/public/themes/pterodactyl/js/frontend/files/upload.js new file mode 100644 index 000000000..b78ddc410 --- /dev/null +++ b/public/themes/pterodactyl/js/frontend/files/upload.js @@ -0,0 +1,129 @@ +// Copyright (c) 2015 - 2016 Dane Everitt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +(function initUploader() { + var notifyUploadSocketError = false; + uploadSocket = io(Pterodactyl.node.scheme + '://' + Pterodactyl.node.fqdn + ':' + Pterodactyl.node.daemonListen + '/upload/' + Pterodactyl.server.uuid, { + 'query': 'token=' + Pterodactyl.server.daemonSecret, + }); + + uploadSocket.io.on('connect_error', function (err) { + if(typeof notifyUploadSocketError !== 'object') { + notifyUploadSocketError = $.notify({ + message: 'There was an error attempting to establish a connection to the uploader endpoint.

' + err, + }, { + type: 'danger', + delay: 0 + }); + } + }); + + uploadSocket.on('error', err => { + siofu.destroy(); + console.error(err); + }); + + uploadSocket.on('connect', function () { + if (notifyUploadSocketError !== false) { + notifyUploadSocketError.close(); + notifyUploadSocketError = false; + } + }); + + var siofu = new SocketIOFileUpload(uploadSocket); + siofu.listenOnDrop(document.getElementById("load_files")); + + window.addEventListener('dragover', function (event) { + event.preventDefault(); + }, false); + + window.addEventListener('drop', function (event) { + event.preventDefault(); + }, false); + + var dropCounter = 0; + $('#load_files').bind({ + dragenter: function (event) { + event.preventDefault(); + dropCounter++; + $(this).addClass('hasFileHover'); + }, + dragleave: function (event) { + dropCounter--; + if (dropCounter === 0) { + $(this).removeClass('hasFileHover'); + } + }, + drop: function (event) { + dropCounter = 0; + $(this).removeClass('hasFileHover'); + } + }); + + siofu.addEventListener('start', function (event) { + event.file.meta.path = $('#headerTableRow').attr('data-currentdir'); + event.file.meta.identifier = Math.random().toString(36).slice(2); + + $('#append_files_to').append(' \ + \ + ' + event.file.name + ' \ +   \ + \ + \ +
\ +
\ +
\ + \ + \ + '); + }); + + siofu.addEventListener('progress', function(event) { + var percent = event.bytesLoaded / event.file.size * 100; + if (percent >= 100) { + $('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-success').parent().removeClass('active'); + } else { + $('.prog-bar-' + event.file.meta.identifier).css('width', percent + '%'); + } + }); + + // Do something when a file is uploaded: + siofu.addEventListener('complete', function(event){ + if (!event.success) { + $('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-danger'); + $.notify({ + message: 'An error was encountered while attempting to upload this file.' + }, { + type: 'danger', + delay: 5000 + }); + } + }); + + siofu.addEventListener('error', function(event){ + console.error(event); + $('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-danger'); + $.notify({ + message: 'An error was encountered while attempting to upload this file: ' + event.message + '.', + }, { + type: 'danger', + delay: 8000 + }); + }); +})(); diff --git a/public/themes/pterodactyl/vendor/siofu/client.min.js b/public/themes/pterodactyl/vendor/siofu/client.min.js new file mode 100755 index 000000000..997b54875 --- /dev/null +++ b/public/themes/pterodactyl/vendor/siofu/client.min.js @@ -0,0 +1,15 @@ +/* Socket IO File Upload Client-Side Library + * Copyright (C) 2015 Shane Carr and others + * Released under the X11 License + * For more information, visit: https://github.com/vote539/socketio-file-upload + */ +(function(g,d,f){"function"===typeof define&&define.amd?define([],f):"object"===typeof module&&module.exports?module.exports=f():g[d]=f()})(this,"SocketIOFileUpload",function(){return function(g){var d=this;if(!window.File||!window.FileReader)throw Error("Socket.IO File Upload: Browser Not Supported");window.siofu_global||(window.siofu_global={instances:0,downloads:0});var f={},p={},y={},q={},h={};d.fileInputElementId="siofu_input_"+siofu_global.instances++;d.resetFileInputs=!0;d.useText=!1;d.serializedOctets= +!1;d.useBuffer=!0;d.chunkSize=102400;var t=function(a,b){var c=document.createEvent("Event");c.initEvent(a,!1,!1);for(var v in b)b.hasOwnProperty(v)&&(c[v]=b[v]);return d.dispatchEvent(c)},r=[],e=function(a,b,c,d){a.addEventListener(b,c,d);r.push(arguments)},z=function(a,b,c,d){a.removeEventListener&&a.removeEventListener(b,c,d)},B=function(){for(var a=r.length-1;0<=a;a--)z.apply(this,r[a]);r=[]},C=function(a){if(null!==d.maxFileSize&&a.size>d.maxFileSize)t("error",{file:a,message:"Attempt by client to upload file exceeding the maximum file size", +code:1});else if(t("start",{file:a})){var b=new FileReader,c=siofu_global.downloads++,f=!1,h=d.useText,w=0,r;b._realReader&&(b=b._realReader);p[c]=a;var u={id:c},x=d.chunkSize;if(x>=a.size||0>=x)x=a.size;var n=function(){if(!u.abort){var c=a.slice(w,Math.min(w+x,a.size));h?b.readAsText(c):b.readAsArrayBuffer(c)}},A=function(e){if(!u.abort){var v=Math.min(w+x,a.size);a:{var p=w;e=e.target.result;var n=!1;if(!h)try{var l=new Uint8Array(e);if(d.serializedOctets)e=l;else if(d.useBuffer)e=l.buffer;else{var n= +!0,m,q=l.buffer.byteLength,k="";for(m=0;m>2],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(l[m]&3)<<4|l[m+1]>>4],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(l[m+1]&15)<<2|l[m+2]>>6],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[l[m+2]&63];2===q%3?k=k.substring(0,k.length-1)+"=":1===q%3&&(k=k.substring(0,k.length-2)+"==");e=k}}catch(E){g.emit("siofu_done", +{id:c,interrupt:!0});break a}g.emit("siofu_progress",{id:c,size:a.size,start:p,end:v,content:e,base64:n})}t("progress",{file:a,bytesLoaded:v,name:r});w+=x;w>=a.size&&(g.emit("siofu_done",{id:c}),t("load",{file:a,reader:b,name:r}),f=!0)}};e(b,"load",A);e(b,"error",function(){g.emit("siofu_done",{id:c,interrupt:!0});z(b,"load",A)});e(b,"abort",function(){g.emit("siofu_done",{id:c,interrupt:!0});z(b,"load",A)});g.emit("siofu_start",{name:a.name,mtime:a.lastModified,meta:a.meta,size:a.size,encoding:h? +"text":"octet",id:c});q[c]=function(a){r=a;n()};y[c]=function(){f||n()};return u}},u=function(a){if(0!==a.length){for(var b=0;b 'SFTP Settings', 'startup_parameters' => 'Startup Parameters', 'databases' => 'Databases', + 'edit_file' => 'Edit File', ], ]; diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index de1085940..358a098e3 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -6,6 +6,23 @@ return [ 'header' => 'Server Console', 'header_sub' => 'Control your server in real time.', ], + 'files' => [ + 'header' => 'File Manager', + 'header_sub' => 'Manage all of your files directly from the web.', + 'loading' => 'Loading initial file structure, this could take a few seconds.', + 'path' => 'When configuring any file paths in your server plugins or settings you should use :path as your base path. The maximum size for web-based file uploads to this node is :size.', + 'seconds_ago' => 'seconds ago', + 'file_name' => 'File Name', + 'size' => 'Size', + 'last_modified' => 'Last Modified', + 'add_new' => 'Add New File', + 'edit' => [ + 'header' => 'Edit File', + 'header_sub' => 'Make modifications to a file from the web.', + 'save' => 'Save File', + 'return' => 'Return to File Manager', + ], + ], 'config' => [ 'startup' => [ 'header' => 'Start Configuration', diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 49c38acf9..8399d0bc0 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -140,7 +140,11 @@ @lang('navigation.server.console') -
  • +
  • @lang('navigation.server.file_management') @@ -149,9 +153,8 @@
  • diff --git a/resources/themes/pterodactyl/server/files/edit.blade.php b/resources/themes/pterodactyl/server/files/edit.blade.php new file mode 100644 index 000000000..d060fee5d --- /dev/null +++ b/resources/themes/pterodactyl/server/files/edit.blade.php @@ -0,0 +1,63 @@ +{{-- Copyright (c) 2015 - 2016 Dane Everitt --}} + +{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}} +{{-- of this software and associated documentation files (the "Software"), to deal --}} +{{-- in the Software without restriction, including without limitation the rights --}} +{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}} +{{-- copies of the Software, and to permit persons to whom the Software is --}} +{{-- furnished to do so, subject to the following conditions: --}} + +{{-- The above copyright notice and this permission notice shall be included in all --}} +{{-- copies or substantial portions of the Software. --}} + +{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}} +{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}} +{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}} +{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}} +{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}} +{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}} +{{-- SOFTWARE. --}} +@extends('layouts.master') + +@section('title') + @lang('server.files.edit.header') +@endsection + +@section('content-header') +

    @lang('server.files.edit.header')@lang('server.files.edit.header_sub')

    + +@endsection + +@section('content') +
    +
    +
    + + +
    {{ $contents }}
    + +
    +
    +
    +@endsection + +@section('footer-scripts') + @parent + {!! Theme::js('js/frontend/server.socket.js') !!} + {!! Theme::js('js/vendor/ace/ace.js') !!} + {!! Theme::js('js/vendor/ace/ext-modelist.js') !!} + {!! Theme::js('js/frontend/files/editor.js') !!} +@endsection diff --git a/resources/themes/pterodactyl/server/files/index.blade.php b/resources/themes/pterodactyl/server/files/index.blade.php new file mode 100644 index 000000000..f9d46e069 --- /dev/null +++ b/resources/themes/pterodactyl/server/files/index.blade.php @@ -0,0 +1,66 @@ +{{-- Copyright (c) 2015 - 2016 Dane Everitt --}} + +{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}} +{{-- of this software and associated documentation files (the "Software"), to deal --}} +{{-- in the Software without restriction, including without limitation the rights --}} +{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}} +{{-- copies of the Software, and to permit persons to whom the Software is --}} +{{-- furnished to do so, subject to the following conditions: --}} + +{{-- The above copyright notice and this permission notice shall be included in all --}} +{{-- copies or substantial portions of the Software. --}} + +{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}} +{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}} +{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}} +{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}} +{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}} +{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}} +{{-- SOFTWARE. --}} +@extends('layouts.master') + +@section('title') + @lang('server.files.header') +@endsection + +@section('content-header') +

    @lang('server.files.header')@lang('server.files.header_sub')

    + +@endsection + +@section('content') +
    +
    +
    +
    +
    +
    @lang('server.files.loading')
    +
    + +
    +
    +
    +@endsection + +@section('footer-scripts') + @parent + {!! Theme::js('js/frontend/server.socket.js') !!} + {!! Theme::js('js/vendor/async/async.min.js') !!} + {!! Theme::js('js/vendor/lodash/lodash.js') !!} + {!! Theme::js('vendor/siofu/client.min.js') !!} + @if(App::environment('production')) + {!! Theme::js('js/frontend/files/filemanager.min.js') !!} + @else + {!! Theme::js('js/frontend/files/src/index.js') !!} + {!! Theme::js('js/frontend/files/src/contextmenu.js') !!} + {!! Theme::js('js/frontend/files/src/actions.js') !!} + @endif + {!! Theme::js('js/frontend/files/upload.js') !!} +@endsection diff --git a/resources/themes/pterodactyl/server/files/list.blade.php b/resources/themes/pterodactyl/server/files/list.blade.php new file mode 100644 index 000000000..b0797ec59 --- /dev/null +++ b/resources/themes/pterodactyl/server/files/list.blade.php @@ -0,0 +1,160 @@ +{{-- Copyright (c) 2015 - 2016 Dane Everitt --}} + +{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}} +{{-- of this software and associated documentation files (the "Software"), to deal --}} +{{-- in the Software without restriction, including without limitation the rights --}} +{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}} +{{-- copies of the Software, and to permit persons to whom the Software is --}} +{{-- furnished to do so, subject to the following conditions: --}} + +{{-- The above copyright notice and this permission notice shall be included in all --}} +{{-- copies or substantial portions of the Software. --}} + +{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}} +{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}} +{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}} +{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}} +{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}} +{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}} +{{-- SOFTWARE. --}} + + + + + + + + + + + + + + + + @if (isset($directory['first']) && $directory['first'] === true) + + + + + + + + @endif + @if (isset($directory['show']) && $directory['show'] === true) + + + + + + + + @endif + @foreach ($folders as $folder) + + + + + + + + @endforeach + @foreach ($files as $file) + + + + + + + + @endforeach + +
    @lang('server.files.file_name')@lang('server.files.size')@lang('server.files.last_modified')
    + /home/container{{ $directory['header'] }} + + + + + +
    + ← {{ $directory['link_show'] }} +
    + {{ $folder['entry'] }} + {{ $folder['size'] }} + timezone(env('APP_TIMEZONE', 'America/New_York')); ?> + @if($carbon->diffInMinutes(Carbon::now()) > 60) + {{ $carbon->format('m/d/y H:i:s') }} + @elseif($carbon->diffInSeconds(Carbon::now()) < 5 || $carbon->isFuture()) + @lang('server.files.seconds_ago') + @else + {{ $carbon->diffForHumans() }} + @endif +
    + {{-- oh boy --}} + @if(in_array($file['mime'], [ + 'application/x-7z-compressed', + 'application/zip', + 'application/x-compressed-zip', + 'application/x-tar', + 'application/x-gzip', + 'application/x-bzip', + 'application/x-bzip2', + 'application/java-archive' + ])) + + @elseif(in_array($file['mime'], [ + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'text/xml', + 'text/css', + 'text/html', + 'text/x-perl', + 'text/x-shellscript' + ])) + + @elseif(starts_with($file['mime'], 'image')) + + @elseif(starts_with($file['mime'], 'video')) + + @elseif(starts_with($file['mime'], 'video')) + + @elseif(starts_with($file['mime'], 'application/vnd.ms-powerpoint')) + + @elseif(in_array($file['mime'], [ + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'application/msword' + ]) || starts_with($file['mime'], 'application/vnd.ms-word')) + + @elseif(in_array($file['mime'], [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + ]) || starts_with($file['mime'], 'application/vnd.ms-excel')) + + @elseif($file['mime'] === 'application/pdf') + + @else + + @endif + + @if(in_array($file['mime'], $editableMime)) + @can('edit-files', $server) + {{ $file['entry'] }} + @else + {{ $file['entry'] }} + @endcan + @else + {{ $file['entry'] }} + @endif + {{ $file['size'] }} + timezone(env('APP_TIMEZONE', 'America/New_York')); ?> + @if($carbon->diffInMinutes(Carbon::now()) > 60) + {{ $carbon->format('m/d/y H:i:s') }} + @elseif($carbon->diffInSeconds(Carbon::now()) < 5 || $carbon->isFuture()) + @lang('server.files.seconds_ago') + @else + {{ $carbon->diffForHumans() }} + @endif +