diff --git a/CHANGELOG.md b/CHANGELOG.md index 027d9c0a3..38a184b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Added * Support for creating server without having to assign a node and allocation manually. Simply select the checkbox or pass `auto_deploy=true` to the API to auto-select a node and allocation given a location. * Support for setting IP Aliases through the panel on the node overview page. Also cleaned up allocation removal. +* Support for renaming files through the panel's file mananger. ### Changed * Prevent clicking server start button until server is completely off, not just stopping. @@ -15,6 +16,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Trying to add a new node if no location exists redirects user to location management page and alerts them to add a location first. * `Server\AjaxController@postSetConnection` is now `Server\AjaxController@postSetPrimary` and accepts one post parameter of `allocation` rather than a combined `ip:port` value. * Port allocations on server view are now cleaner and should make more sense. +* Improved File Manager + * Rewritten Javascript to load, rename, and handle other file actions. + * Uses Ace Editor for editing files rather than a non-formatted textarea + * File actions that were previously icons to the right are now contained in a menu that appears when right-clicking a file or folder. ### Fixed * Team Fortress named 'Insurgency' in panel in database seeder. ([#96](https://github.com/Pterodactyl/Panel/issues/96), PR by [@MeltedLux](https://github.com/MeltedLux)) diff --git a/README.md b/README.md index 71970213c..7e6c6fce6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## Pterodactyl Panel -Pterodactyl is the free game server management panel designed by users, for users. Featuring support for Vanilla Minecraft, Spigot, Source Dedicated Servers, BungeeCord, and many more. Pterodactyl is built on the `Laravel PHP Framework (v5.2)`. +Pterodactyl is the free game server management panel designed by users, for users. Featuring support for Vanilla Minecraft, Spigot, Source Dedicated Servers, BungeeCord, and many more. Pterodactyl is built on the `Laravel PHP Framework (v5.3)`. ## Support & Documentation Support for using Pterodactyl can be found on our [wiki](https://github.com/Pterodactyl/Panel/wiki) or on our [Discord chat](https://discord.gg/0gYt8oU8QOkDhKLS). @@ -28,6 +28,8 @@ SOFTWARE. ``` ### Credits +Ace Editor - [license](https://github.com/ajaxorg/ace/blob/master/LICENSE) - [homepage](https://ace.c9.io) + Animate.css - [license](https://github.com/daneden/animate.css/blob/master/LICENSE) - [homepage](http://daneden.github.io/animate.css/) Async.js - [license](https://github.com/caolan/async/blob/master/LICENSE) - [homepage](https://github.com/caolan/async/) @@ -48,6 +50,8 @@ jQuery - [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) - [ jQuery Terminal - [license](https://github.com/jcubic/jquery.terminal/blob/master/LICENSE) - [homepage](http://terminal.jcubic.pl) +Lodash - [license](https://github.com/lodash/lodash/blob/master/LICENSE) - [homepage](https://lodash.com/) + MetricsGraphics.js - [license](https://github.com/mozilla/metrics-graphics/blob/master/LICENSE) - [homepage](http://metricsgraphicsjs.org/) Socket.io - [license](https://github.com/socketio/socket.io/blob/master/LICENSE) - [homepage](http://socket.io) diff --git a/app/Http/Controllers/Server/AjaxController.php b/app/Http/Controllers/Server/AjaxController.php index dd81c2184..b109063bd 100644 --- a/app/Http/Controllers/Server/AjaxController.php +++ b/app/Http/Controllers/Server/AjaxController.php @@ -137,7 +137,7 @@ class AjaxController extends Controller 'server' => $server, 'files' => $directoryContents->files, 'folders' => $directoryContents->folders, - 'extensions' => Repositories\HelperRepository::editableFiles(), + 'editableMime' => Repositories\HelperRepository::editableFiles(), 'directory' => $prevDir ]); diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index 9b3aecb55..d77c0d636 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -145,9 +145,9 @@ class ServerController extends Controller 'server' => $server, 'node' => Models\Node::find($server->node), 'file' => $file, - 'contents' => $fileContent->content, - 'directory' => (in_array($fileInfo->dirname, ['.', './', '/'])) ? '/' : trim($fileInfo->dirname, '/') . '/', - 'extension' => $fileInfo->extension + 'stat' => $fileContent['stat'], + 'contents' => $fileContent['file']->content, + 'directory' => (in_array($fileInfo->dirname, ['.', './', '/'])) ? '/' : trim($fileInfo->dirname, '/') . '/' ]); } diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php index e57a543c8..83d6916a5 100644 --- a/app/Repositories/Daemon/FileRepository.php +++ b/app/Repositories/Daemon/FileRepository.php @@ -85,7 +85,7 @@ class FileRepository * Get the contents of a requested file for the server. * * @param string $file - * @return string + * @return array */ public function returnFileContents($file) { @@ -95,22 +95,39 @@ class FileRepository } $file = (object) pathinfo($file); - if (!in_array($file->extension, HelperRepository::editableFiles())) { - throw new DisplayException('You do not have permission to edit this type of file.'); - } $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; - $res = $this->client->request('GET', '/server/file/' . rawurlencode($file->dirname.$file->basename), [ + $res = $this->client->request('GET', '/server/files/stat/' . rawurlencode($file->dirname.$file->basename) , [ + 'headers' => $this->headers + ]); + + $stat = json_decode($res->getBody()); + if($res->getStatusCode() !== 200 || !isset($stat->size)) { + throw new DisplayException('The daemon provided a non-200 error code on stat lookup: HTTP\\' . $res->getStatusCode()); + } + + if (!in_array($stat->mime, HelperRepository::editableFiles())) { + throw new DisplayException('You cannot edit that type of file (' . $stat->mime . ') through the panel.'); + } + + if ($stat->size > 5000000) { + throw new DisplayException('That file is too large to open in the browser, consider using a SFTP client.'); + } + + $res = $this->client->request('GET', '/server/file/' . rawurlencode($file->dirname.$file->basename) , [ 'headers' => $this->headers ]); $json = json_decode($res->getBody()); if($res->getStatusCode() !== 200 || !isset($json->content)) { - throw new DisplayException('Scales provided a non-200 error code: HTTP\\' . $res->getStatusCode()); + throw new DisplayException('The daemon provided a non-200 error code: HTTP\\' . $res->getStatusCode()); } - return $json; + return [ + 'file' => $json, + 'stat' => $stat + ]; } @@ -130,11 +147,24 @@ class FileRepository $file = (object) pathinfo($file); - if(!in_array($file->extension, HelperRepository::editableFiles())) { - throw new DisplayException('You do not have permission to edit this type of file.'); + $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; + + $res = $this->client->request('GET', '/server/files/stat/' . rawurlencode($file->dirname.$file->basename) , [ + 'headers' => $this->headers + ]); + + $stat = json_decode($res->getBody()); + if($res->getStatusCode() !== 200 || !isset($stat->size)) { + throw new DisplayException('The daemon provided a non-200 error code on stat lookup: HTTP\\' . $res->getStatusCode()); } - $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; + if (!in_array($stat->mime, HelperRepository::editableFiles())) { + throw new DisplayException('You cannot edit that type of file (' . $stat->mime . ') through the panel.'); + } + + if ($stat->size > 5000000) { + throw new DisplayException('That file is too large to save in the browser, consider using a SFTP client.'); + } $res = $this->client->request('POST', '/server/file/' . rawurlencode($file->dirname.$file->basename), [ 'headers' => $this->headers, diff --git a/app/Repositories/HelperRepository.php b/app/Repositories/HelperRepository.php index 1ed67f134..6eaa68329 100644 --- a/app/Repositories/HelperRepository.php +++ b/app/Repositories/HelperRepository.php @@ -30,25 +30,20 @@ class HelperRepository { * @var array */ protected static $editable = [ - 'txt', - 'yml', - 'yaml', - 'log', - 'conf', - 'config', - 'html', - 'json', - 'properties', - 'props', - 'cfg', - 'lang', - 'ini', - 'cmd', - 'sh', - 'lua', - '0' // Supports BungeeCord Files + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'text/xml', + 'text/css', + 'text/html', + 'text/plain', + 'text/x-perl', + 'text/x-shellscript', + 'inode/x-empty' ]; + public function __construct() { // diff --git a/public/themes/default/css/pterodactyl.css b/public/themes/default/css/pterodactyl.css index c7be4c6e2..b7819a85e 100755 --- a/public/themes/default/css/pterodactyl.css +++ b/public/themes/default/css/pterodactyl.css @@ -197,3 +197,41 @@ li.btn.btn-default.pill:active,li.btn.btn-default.pill:focus,li.btn.btn-default. .use-pointer { cursor: pointer !important; } + +.dropdown-menu > li.bg-danger { + background-color:#fdf7f7; + color:#474a54; + border-left: 4px solid #d9534f !important; +} + +.dropdown-menu > li.bg-info { + background-color:#fcf8f2; + color:#474a54; + border-left: 4px solid #f0ad4e !important; +} + +.dropdown-menu > li.bg-success { + background-color:#f4f8fa; + color:#474a54; + border-left: 4px solid #5bc0de !important; +} + +.dropdown-menu > li.bg-warning { + background-color:#fdf7f7; + color:#474a54; + border-left: 4px solid #d9534f !important; +} + +.dropdown-menu > li.bg-default { + border-left: 4px solid #bbbbbb !important; +} + +.dropdown-menu > li.bg-danger > a, +.dropdown-menu > li.bg-info > a, +.dropdown-menu > li.bg-success > a, +.dropdown-menu > li.bg-warning > a, +.dropdown-menu > li.bg-default > a { + padding-left: 11px !important; +} +/*.bg-danger:active,.bg-danger:focus,.bg-danger:hover{color:#fff;background-color:#d32a0e;border-color:#b1240c} +.bg-danger.disabled,.bg-danger.disabled:active,.bg-danger.disabled:focus,.bg-danger.disabled:hover,.bg-danger[disabled]{background-color:#f04124;border-color:#ea2f10}*/ diff --git a/resources/views/server/files/edit.blade.php b/resources/views/server/files/edit.blade.php index d76f19160..6ac8c4edc 100644 --- a/resources/views/server/files/edit.blade.php +++ b/resources/views/server/files/edit.blade.php @@ -26,65 +26,52 @@ @section('content')

Editing File: /home/container/{{ $file }}

-
-
-
- @if (in_array($extension, ['yaml', 'yml'])) -
- {!! trans('server.files.yaml_notice', [ - 'dropdown' => '' - ]) !!} -
- @endif - +
+
+
{{ $contents }}
+
+
+ @can('save-files', $server) +
+
+
+ + + {{ trans('server.files.back') }}
- @can('save-files', $server) -
-
- - {!! csrf_field() !!} - - {{ trans('server.files.back') }} -
-
- @endcan - + @endcan
+{!! Theme::js('js/vendor/ace/ace.js') !!} +{!! Theme::js('js/vendor/ace/ext-modelist.js') !!} diff --git a/resources/views/server/files/list.blade.php b/resources/views/server/files/list.blade.php index df850f9c5..ca5f78cd2 100644 --- a/resources/views/server/files/list.blade.php +++ b/resources/views/server/files/list.blade.php @@ -17,63 +17,109 @@ {{-- 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. --}} -

/home/container{{ $directory['header'] }}  

- +
- {{-- --}} + + + + @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 diff --git a/resources/views/server/js/filemanager/actions.blade.php b/resources/views/server/js/filemanager/actions.blade.php index 2eae52b04..b15e8a37b 100644 --- a/resources/views/server/js/filemanager/actions.blade.php +++ b/resources/views/server/js/filemanager/actions.blade.php @@ -21,7 +21,7 @@ // SOFTWARE. class FileActions { constructor() { - // + this.activeLine = null; } run() { @@ -31,53 +31,67 @@ class FileActions { makeMenu() { $(document).find('#fileOptionMenu').remove(); - return $(''; } rightClick() { - $('#file_listing > tbody').on('contextmenu', event => { + $('#file_listing > tbody td').on('contextmenu', event => { const parent = $(event.target).parent(); - const menu = this.makeMenu(); + const menu = $(this.makeMenu()); if (parent.data('type') === 'disabled') return; event.preventDefault(); - menu.appendTo('body'); - menu.data('invokedOn', $(event.target)).show().css({ + $(menu).appendTo('body'); + $(menu).data('invokedOn', $(event.target)).show().css({ position: 'absolute', left: event.pageX, top: event.pageY, }); + this.activeLine = parent; + this.activeLine.addClass('active'); + if (parent.data('type') === 'file') { - menu.find('li[data-action="download"]').removeClass('hidden'); + $(menu).find('li[data-action="download"]').removeClass('hidden'); + } + + if (parent.data('type') === 'folder') { + $(menu).find('li[data-action="compress"]').removeClass('hidden'); } 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"]').removeClass('hidden'); } // Handle Events - var Context = new ContextMenuActions(parent); - menu.find('li[data-action="move"]').unbind().on('click', e => { + var Context = new ContextMenuActions(parent, menu); + $(menu).find('li[data-action="move"]').unbind().on('click', e => { Context.move(); }); - menu.find('li[data-action="rename"]').unbind().on('click', e => { + $(menu).find('li[data-action="rename"]').unbind().on('click', e => { Context.rename(); }); + $(menu).find('li[data-action="download"]').unbind().on('click', e => { + e.preventDefault(); + Context.download(); + }); + $(window).on('click', () => { - menu.remove(); + $(menu).remove(); + if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active'); }); }); } @@ -85,7 +99,12 @@ class FileActions { directoryClick() { $('a[data-action="directory-view"]').on('click', function (event) { event.preventDefault(); - window.location.hash = encodeURIComponent($(this).parent().data('hash') || ''); + + const path = $(this).parent().data('path') || ''; + const name = $(this).parent().data('name') || ''; + + console.log('changing hash'); + window.location.hash = encodeURIComponent(path + name); Files.list(); }); } diff --git a/resources/views/server/js/filemanager/contextmenu.blade.php b/resources/views/server/js/filemanager/contextmenu.blade.php index 71f4a1112..f713b25b1 100644 --- a/resources/views/server/js/filemanager/contextmenu.blade.php +++ b/resources/views/server/js/filemanager/contextmenu.blade.php @@ -20,8 +20,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. class ContextMenuActions { - constructor(element) { + constructor(element, menu) { this.element = element; + this.menu = menu; } destroy() { @@ -32,28 +33,46 @@ class ContextMenuActions { alert($(this.element).data('path')); } + download() { + var baseURL = $(this.menu).find('li[data-action="download"] a').attr('href'); + var toURL = baseURL + $(this.element).find('td[data-identifier="name"]').data('name'); + + window.location = toURL; + } + rename() { - var desiredElement = $(this.element).find('td[data-identifier="name"]'); - var linkElement = desiredElement.find('a'); - var currentName = linkElement.html(); - var editField = ``; - desiredElement.find('a').remove(); - desiredElement.html(editField); + 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'); - const inputField = desiredElement.find('input'); inputField.focus(); - inputField.on('blur keypress', e => { // Save Field if (e.type === 'blur' || (e.type === 'keypress' && e.which !== 13)) { // Escape Key Pressed, don't save. if (e.which === 27 || e.type === 'blur') { - desiredElement.html(linkElement); + if (!_.isEmpty(currentLink)) { + nameBlock.html(currentLink); + } else { + nameBlock.html(currentName); + } inputField.remove(); + Actions.run(); } return; } + inputLoader.show(); + const currentPath = decodeURIComponent(nameBlock.data('path')); + $.ajax({ type: 'POST', headers: { @@ -64,16 +83,24 @@ class ContextMenuActions { url: '{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/server/files/rename', timeout: 10000, data: JSON.stringify({ - from: currentName, - to: inputField.val(), + from: `${currentPath}${currentName}`, + to: `${currentPath}${inputField.val()}`, }), }).done(data => { - this.element.attr('data-path', inputField.val()); - desiredElement.attr('data-hash', inputField.val()); - desiredElement.html(linkElement.html(inputField.val())); + nameBlock.attr('data-name', inputField.val()); + if (!_.isEmpty(currentLink)) { + const newLink = currentLink.attr('href').substr(0, currentLink.attr('href').lastIndexOf('/')) + '/' + inputField.val(); + currentLink.attr('href', newLink); + nameBlock.html( + currentLink.html(inputField.val()) + ); + } else { + nameBlock.html(inputField.val()); + } inputField.remove(); - Actions.run(); }).fail(jqXHR => { + nameBlock.addClass('has-error'); + inputLoader.remove(); console.error(jqXHR); var error = 'An error occured while trying to process this request.'; if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { @@ -84,6 +111,8 @@ class ContextMenuActions { title: '', text: error, }); + }).always(() => { + inputLoader.remove(); }); }); } diff --git a/resources/views/server/js/filemanager/index.blade.php b/resources/views/server/js/filemanager/index.blade.php index 78800c6ba..8e893f765 100644 --- a/resources/views/server/js/filemanager/index.blade.php +++ b/resources/views/server/js/filemanager/index.blade.php @@ -44,11 +44,13 @@ class FileManager { directory: path, }, }).done(data => { - $('#load_files').slideUp().html(data).slideDown(() => { + this.loader(false); + $('#load_files').slideUp().html(data).slideDown(100, () => { Actions.run(); }); $('#internal_alert').slideUp(); }).fail(jqXHR => { + this.loader(false); swal({ type: 'error', title: 'File Error', @@ -56,9 +58,7 @@ class FileManager { }); if (!isError) this.list('/', true); console.log(jqXHR); - }).always(() => { - this.loader(false); - }); + }) } loader(show) { @@ -76,9 +76,9 @@ class FileManager { 'font-size': '60px' }); - $('.ajax_loading_box').css('height', (height + 5)).fadeIn(); + $('.ajax_loading_box').css('height', (height + 5)).show(); } else { - $('.ajax_loading_box').fadeOut(100); + $('.ajax_loading_box').hide(); } }
File Name Size Last ModifiedOptions
+ /home/container{{ $directory['header'] }} + + + + + +
← {{ $directory['link_show'] }} + ← {{ $directory['link_show'] }} +
+
{{ $folder['entry'] }} {{ $folder['size'] }} {{ date('m/d/y H:i:s', $folder['date']) }} -
- - -
- @can('delete-files', $server) - - @endcan -
-
-
- @if(in_array($file['extension'], $extensions)) + + {{-- 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 @@ -85,22 +131,6 @@ {{ $file['size'] }} {{ date('m/d/y H:i:s', $file['date']) }} -
- - -
- @can('delete-files', $server) - - @endcan -
-
-