diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ebf4564 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +ifm.php -diff +build/* -diff diff --git a/README.md b/README.md index 6391e55..5bb24db 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ # IFM - improved file manager ## about -This is a simple filemanager. It is a single file solution which uses HTML5, CSS3, JavaScript and PHP. It works like a client-server system where HTML5/CSS3/JavaScript is the client part and the PHP API acts as the server, which reduces the traffic significant. +The IFM is a web-based filemanager, which comes as a single file solution using HTML5, CSS3, JavaScript and PHP. The IFM uses the following resources: * [ACE Editor](https://ace.c9.io) * [Bootstrap v3](https://getbootstrap.com) -* [jQuery](https://jquery.com) * custom icon set generated with [Fontello](http://fontello.com/) +* [jQuery](https://jquery.com) +* [Mustache](https://mustache.github.io/) + +## features +* create/edit files and directories +* copy/move files and directories +* download files and directories +* upload files directly or via URL +* extract zip archives +* change permissions +* image preview ## requirements | Client | Server | @@ -19,8 +29,8 @@ Just copy the ifm.php to your webspace - thats all :) ## key bindings * e - edit / extract current file -* hjkl - vim-style navigation -* g - focus path (goto) +* hjkl - vim-style navigation (alternative to arrow keys) +* g - focus the path input field (i.e. "goto") * r - refresh file table * u - upload a file * o - remote upload a file @@ -29,11 +39,12 @@ Just copy the ifm.php to your webspace - thats all :) * D - new directory * space - select a highlighted item * del - delete selected files +* Enter - open a file or change to the directory ## configuration -The configuration is located at the top of the script in a separate configuration class. The options in the class are commented and named laconically. If you have questions anyway [write me an email](mailto:marco@misterunknown.de). +The configuration is located at the top of the script. The options are commented and named laconically. If you have any questions [write me an email](mailto:marco@misterunknown.de). ### authentication -Meanwhile I added a super simple authentication feature using the configuration keys `auth` and `auth_source`. You can configure it like this: +The IFM offers a simple authentication feature using the configuration keys `auth` and `auth_source`. You can configure it like this: ```php "auth" => 1, "auth_source" => 'inline;:', @@ -46,7 +57,7 @@ The file should contain ONLY ONE LINE: ``` : ``` -The password hash has to be a hash generated by PHPs `password_hash()` function. The default credentials are "admin:admin". +The password hash has to be a hash generated by PHPs `password_hash()` function. The default credentials are "admin:admin". ## screenshots @@ -57,8 +68,5 @@ The password hash has to be a hash generated by PHPs `password_hash()` function. ## issues Currently there are no known issues. If you find any flaws please let me know. - ## security information -The IFM was developed with the assumption that the highest level of operation is the scripts base location. So it is neither possible to nagivate nor to use any API function in a level above the script root. - -It is highly recommended to restrict access to the script e.g. using a basic authentication. +By default, the IFM is locked to it's own directory, so you are not able to go above. You can change that by setting the $root_dir in the scripts configuration. diff --git a/build/ifm.js b/build/ifm.js new file mode 100644 index 0000000..0631a4c --- /dev/null +++ b/build/ifm.js @@ -0,0 +1,1240 @@ +/** + * IFM constructor + * + * @param object params - object with some configuration values, currently you only can set the api url + */ +function IFM( params ) { + var self = this; // reference to ourself, because "this" does not work within callbacks + + // set the backend for the application + params = params || {}; + self.api = params.api || window.location.pathname; + + this.editor = null; // global ace editor + this.fileChanged = false; // flag for check if file was changed already + this.currentDir = ""; // this is the global variable for the current directory; it is used for AJAX requests + this.rootElement = ""; + + /** + * Shows a bootstrap modal + * + * @param string content - content of the modal + * @param object options - options for the modal + */ + this.showModal = function( content, options = {} ) { + var modal = $( document.createElement( 'div' ) ) + .addClass( "modal fade" ) + .attr( 'id', 'ifmmodal' ) + .attr( 'role', 'dialog' ); + var modalDialog = $( document.createElement( 'div' ) ) + .addClass( "modal-dialog" ) + .attr( 'role', 'document' ); + if( options.large == true ) modalDialog.addClass( 'modal-lg' ); + var modalContent = $(document.createElement('div')) + .addClass("modal-content") + .append( content ); + modalDialog.append( modalContent ); + modal.append( modalDialog ); + $( document.body ).append( modal ); + modal.on('hide.bs.modal', function () { $(this).remove(); }); + modal.on('shown.bs.modal', function () { + var formElements = $(this).find('input, button'); + if( formElements.length > 0 ) { + formElements.first().focus(); + } + }); + modal.modal('show'); + }; + + /** + * Hides a bootstrap modal + */ + this.hideModal = function() { + $('#ifmmodal').modal('hide'); + }; + + /** + * Reloads the file table + */ + this.refreshFileTable = function () { + var id = self.generateGuid(); + self.task_add( "Refresh", id ); + $.ajax({ + url: self.api, + type: "POST", + data: { + api: "getFiles", + dir: self.currentDir + }, + dataType: "json", + success: self.rebuildFileTable, + error: function( response ) { self.showMessage( "General error occured: No or broken response", "e" ); }, + complete: function() { self.task_done( id ); } + }); + }; + + /** + * Rebuilds the file table with fetched items + * + * @param object data - object with items + */ + this.rebuildFileTable = function( data ) { + data.forEach( function( item ) { + item.guid = self.generateGuid(); + item.linkname = ( item.name == ".." ) ? "[ up ]" : item.name; + item.download = {}; + item.download.name = ( item.name == ".." ) ? "." : item.name; + item.download.allowed = self.config.download; + item.download.currentDir = self.currentDir; + if( ! self.config.chmod ) + item.readonly = "readonly"; + if( self.config.edit || self.config.rename || self.config.delete || self.config.extract || self.config.copymove ) { + item.ftbuttons = true; + item.button = []; + } + if( item.type == "dir" ) { + item.download.action = "zipnload"; + item.download.icon = "icon icon-download-cloud"; + item.rowclasses = "isDir"; + } else { + item.download.action = "download"; + item.download.icon = "icon icon-download"; + if( item.icon.indexOf( 'file-image' ) !== -1 && self.config.isDocroot ) + item.tooltip = 'data-toggle="tooltip" title=""'; + if( item.name.toLowerCase().substr(-4) == ".zip" ) + item.eaction = "extract"; + else + item.eaction = "edit"; + if( self.config.edit && item.name.toLowerCase().substr(-4) != ".zip" ) + item.button.push({ + action: "edit", + icon: "icon icon-pencil", + title: "edit" + }); + else + item.button.push({ + action: "extract", + icon: "icon icon-archive", + title: "extract" + }); + } + if( ! self.inArray( item.name, [".", ".."] ) ) { + if( self.config.copymove ) + item.button.push({ + action: "copymove", + icon: "icon icon-folder-open-empty", + title: "copy/move" + }); + if( self.config.rename ) + item.button.push({ + action: "rename", + icon: "icon icon-terminal", + title: "rename" + }); + if( self.config.delete ) + item.button.push({ + action: "delete", + icon: "icon icon-trash", + title: "delete" + }); + } + }); + var newTBody = Mustache.render( self.templates.filetable, { items: data, config: self.config } ); + $( "#filetable tbody" ).remove(); + $( "#filetable" ).append( $(newTBody) ); + $( '.clickable-row' ).click( function( event ) { + if( event.ctrlKey ) { + $( this ).toggleClass( 'selectedItem' ); + } + }); + $( 'a[data-toggle="tooltip"]' ).tooltip({ + animated: 'fade', + placement: 'right', + html: true + }); + $( 'a.ifmitem' ).each( function() { + if( $(this).data( "type" ) == "dir" ) { + $(this).on( 'click', function( e ) { + e.stopPropagation(); + self.changeDirectory( $(this).parent().parent().data( 'filename' ) ); + return false; + }); + } else { + if( self.config.isDocroot ) + $(this).attr( "href", self.pathCombine( self.currentDir, $(this).parent().parent().data( 'filename' ) ) ); + else + $(this).on( 'click', function() { + $( '#d_' + this.id ).submit(); + return false; + }); + } + }); + $( 'a[name="start_download"]' ).on( 'click', function(e) { + e.stopPropagation(); + $( '#d_' + $(this).data( 'guid' ) ).submit(); + return false; + }); + $( 'input[name="newpermissions"]' ).on( 'keypress', function( e ) { + if( e.key == "Enter" ) { + e.stopPropagation(); + self.changePermissions( $( this ).data( 'filename' ), $( this ).val() ); + return false; + } + }); + $( 'a[name^="do-"]' ).on( 'click', function() { + var action = this.name.substr( this.name.indexOf( '-' ) + 1 ); + switch( action ) { + case "rename": + self.showRenameFileDialog( $(this).data( 'name' ) ); + break; + case "extract": + self.showExtractFileDialog( $(this).data( 'name' ) ); + break; + case "edit": + self.editFile( $(this).data( 'name' ) ); + break; + case "delete": + self.showDeleteFileDialog( $(this).data( 'name' ) ); + break; + case "copymove": + self.showCopyMoveDialog( $(this).data( 'name' ) ); + break; + } + }); + + }; + + /** + * Changes the current directory + * + * @param string newdir - target directory + * @param object options - options for changing the directory + */ + this.changeDirectory = function( newdir, options={} ) { + config = { absolute: false, pushState: true }; + jQuery.extend( config, options ); + if( ! config.absolute ) newdir = self.pathCombine( self.currentDir, newdir ); + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "getRealpath", + dir: newdir + }), + dataType: "json", + success: function( data ) { + self.currentDir = data.realpath; + self.refreshFileTable(); + $( "#currentDir" ).val( self.currentDir ); + if( config.pushState ) history.pushState( { dir: self.currentDir }, self.currentDir, "#"+self.currentDir ); + }, + error: function() { self.showMessage( "General error occured: No or broken response", "e" ); } + }); + }; + + /** + * Shows a file, either a new file or an existing + */ + this.showFileDialog = function () { + var filename = arguments.length > 0 ? arguments[0] : "newfile.txt"; + var content = arguments.length > 1 ? arguments[1] : ""; + self.showModal( Mustache.render( self.templates.file, { filename: filename } ), { large: true } ); + var form = $('#formFile'); + form.find('input[name="filename"]').on( 'keypress', self.preventEnter ); + form.find('#buttonSave').on( 'click', function() { + self.saveFile( form.find('input[name=filename]').val(), self.editor.getValue() ); + self.hideModal(); + return false; + }); + form.find('#buttonSaveNotClose').on( 'click', function() { + self.saveFile( form.find('input[name=filename]').val(), self.editor.getValue() ); + return false; + }); + form.find('#buttonClose').on( 'click', function() { + self.hideModal(); + return false; + }); + form.find('#editoroptions').popover({ + html: true, + title: function() { return $('#editoroptions-head').html(); }, + content: function() { + var content = $('#editoroptions-content').clone() + var aceSession = self.editor.getSession(); + content.removeClass( 'hide' ); + content.find( '#editor-wordwrap' ) + .prop( 'checked', ( aceSession.getOption( 'wrap' ) == 'off' ? false : true ) ) + .on( 'change', function() { self.editor.setOption( 'wrap', $( this ).is( ':checked' ) ); }); + content.find( '#editor-softtabs' ) + .prop( 'checked', aceSession.getOption( 'useSoftTabs' ) ) + .on( 'change', function() { self.editor.setOption( 'useSoftTabs', $( this ).is( ':checked' ) ); }); + content.find( '#editor-tabsize' ) + .val( aceSession.getOption( 'tabSize' ) ) + .on( 'keydown', function( e ) { if( e.key == 'Enter' ) { self.editor.setOption( 'tabSize', $( this ).val() ); } }); + return content; + } + }); + form.on( 'remove', function () { self.editor = null; self.fileChanged = false; }); + // Start ACE + self.editor = ace.edit("content"); + self.editor.$blockScrolling = 'Infinity'; + self.editor.getSession().setValue(content); + self.editor.focus(); + self.editor.on("change", function() { self.fileChanged = true; }); + // word wrap checkbox + $('#aceWordWrap').on( 'change', function (event) { + self.editor.getSession().setUseWrapMode( $(this).is(':checked') ); + }); + }; + + /** + * Saves a file + */ + this.saveFile = function( filename, content ) { + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "saveFile", + dir: self.currentDir, + filename: filename, + content: content + }), + dataType: "json", + success: function( data ) { + if( data.status == "OK" ) { + self.showMessage( "File successfully edited/created.", "s" ); + self.refreshFileTable(); + } else self.showMessage( "File could not be edited/created:" + data.message, "e" ); + }, + error: function() { self.showMessage( "General error occured", "e" ); } + }); + self.fileChanged = false; + }; + + /** + * Edit a file + * + * @params string name - name of the file + */ + this.editFile = function( name ) { + $.ajax({ + url: self.api, + type: "POST", + dataType: "json", + data: ({ + api: "getContent", + dir: self.currentDir, + filename: name + }), + success: function( data ) { + if( data.status == "OK" && data.data.content != null ) { + self.showFileDialog( data.data.filename, data.data.content ); + } + else if( data.status == "OK" && data.data.content == null ) { + self.showMessage( "The content of this file cannot be fetched.", "e" ); + } + else self.showMessage( "Error: "+data.message, "e" ); + }, + error: function() { self.showMessage( "This file can not be displayed or edited.", "e" ); } + }); + }; + + /** + * Shows the create directory dialog + */ + this.showCreateDirDialog = function() { + self.showModal( self.templates.createdir ); + var form = $( '#formCreateDir' ); + form.find( 'input[name=dirname]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonSave' ).on( 'click', function() { + self.createDir( form.find( 'input[name=dirname] ').val() ); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Create a directory + */ + this.createDir = function( dirname ) { + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "createDir", + dir: self.currentDir, + dirname: dirname + }), + dataType: "json", + success: function( data ){ + if( data.status == "OK" ) { + self.showMessage( "Directory sucessfully created.", "s" ); + self.refreshFileTable(); + } + else { + self.showMessage( "Directory could not be created: "+data.message, "e" ); + } + }, + error: function() { self.showMessage( "General error occured.", "e" ); } + }); + }; + + + /** + * Shows the delete file dialog + * + * @param string name - name of the file + */ + this.showDeleteFileDialog = function( filename ) { + self.showModal( Mustache.render( self.templates.deletefile, { filename: name } ) ); + var form = $( '#formDeleteFile' ); + form.find( '#buttonYes' ).on( 'click', function() { + self.deleteFile( self.JSEncode( filename ) ); + self.hideModal(); + return false; + }); + form.find( '#buttonNo' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Deletes a file + * + * @params string name - name of the file + */ + this.deleteFile = function( filename ) { + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "delete", + dir: self.currentDir, + filename: filename + }), + dataType: "json", + success: function(data) { + if(data.status == "OK") { + self.showMessage("File successfully deleted", "s"); + self.refreshFileTable(); + } else self.showMessage("File could not be deleted", "e"); + }, + error: function() { self.showMessage("General error occured", "e"); } + }); + }; + + /** + * Show the rename file dialog + * + * @params string name - name of the file + */ + this.showRenameFileDialog = function( filename ) { + self.showModal( Mustache.render( self.templates.renamefile, { filename: filename } ) ); + var form = $( '#formRenameFile' ); + form.find( 'input[name=newname]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonRename' ).on( 'click', function() { + self.renameFile( filename, form.find( 'input[name=newname]' ).val() ); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Renames a file + * + * @params string name - name of the file + */ + this.renameFile = function( filename, newname ) { + $.ajax({ + url: ifm.api, + type: "POST", + data: ({ + api: "rename", + dir: ifm.currentDir, + filename: filename, + newname: newname + }), + dataType: "json", + success: function(data) { + if(data.status == "OK") { + ifm.showMessage("File successfully renamed", "s"); + ifm.refreshFileTable(); + } else ifm.showMessage("File could not be renamed: "+data.message, "e"); + }, + error: function() { ifm.showMessage("General error occured", "e"); } + }); + }; + + /** + * Show the copy/move dialog + * + * @params string name - name of the file + */ + this.showCopyMoveDialog = function( name ) { + self.showModal( self.templates.copymove ); + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "getFolderTree", + dir: self.currentDir + }), + dataType: "json", + success: function( data ) { + $( '#copyMoveTree' ).treeview( { data: data, levels: 0, expandIcon: "icon icon-folder-empty", collapseIcon: "icon icon-folder-open-empty" } ); + }, + error: function() { self.hideModal(); self.showMessage( "Error while fetching the folder tree.", "e" ) } + }); + $( '#copyButton' ).on( 'click', function() { + self.copyMove( name, $('#copyMoveTree .node-selected').data('path'), 'copy' ); + self.hideModal(); + return false; + }); + $( '#moveButton' ).on( 'click', function() { + self.copyMove( name, $('#copyMoveTree .node-selected').data('path'), 'move' ); + self.hideModal(); + return false; + }); + $( '#cancelButton' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Copy or moves a file + * + * @params string name - name of the file + */ + this.copyMove = function( source, destination, action ) { + var id = self.generateGuid(); + self.task_add( action.charAt(0).toUpperCase() + action.slice(1) + " " + source + " to " + destination, id ); + $.ajax({ + url: self.api, + type: "POST", + data: { + dir: self.currentDir, + api: "copyMove", + action: action, + filename: source, + destination: destination + }, + dataType: "json", + success: function(data) { + if( data.status == "OK" ) { + self.showMessage( data.message, "s" ); + } else { + self.showMessage( data.message, "e" ); + } + self.refreshFileTable(); + }, + error: function() { + self.showMessage( "General error occured.", "e" ); + }, + complete: function() { + self.task_done( id ); + } + }); + }; + + /** + * Shows the extract file dialog + * + * @param string name - name of the file + */ + this.showExtractFileDialog = function( filename ) { + var targetDirSuggestion = ''; + if( filename.lastIndexOf( '.' ) > 1 ) + targetDirSuggestion = filename.substr( 0, filename.lastIndexOf( '.' ) ); + else targetDirSuggestion = filename; + self.showModal( Mustache.render( self.templates.extractfile, { filename: filename, destination: targetDirSuggestion } ) ); + var form = $('#formExtractFile'); + form.find('#buttonExtract').on( 'click', function() { + var t = form.find('input[name=extractTargetLocation]:checked').val(); + if( t == "custom" ) t = form.find('#extractCustomLocation').val(); + self.extractFile( self.JSEncode( filename ), t ); + self.hideModal(); + return false; + }); + form.find('#buttonCancel').on( 'click', function() { + self.hideModal(); + return false; + }); + form.find('#extractCustomLocation').on( 'click', function(e) { + $(e.target).prev().children().first().prop( 'checked', true ); + }); + }; + + /** + * Extracts a file + * + * @param string filename - name of the file + * @param string destination - name of the target directory + */ + this.extractFile = function( filename, destination ) { + $.ajax({ + url: self.api, + type: "POST", + data: { + api: "extract", + dir: self.currentDir, + filename: filename, + targetdir: destination + }, + dataType: "json", + success: function( data ) { + if( data.status == "OK" ) { + self.showMessage( "File successfully extracted", "s" ); + self.refreshFileTable(); + } else self.showMessage( "File could not be extracted. Error: " + data.message, "e" ); + }, + error: function() { self.showMessage( "General error occured", "e" ); } + }); + }; + + /** + * Shows the upload file dialog + */ + this.showUploadFileDialog = function() { + self.showModal( self.templates.uploadfile ); + var form = $('#formUploadFile'); + form.find( 'input[name=newfilename]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonUpload' ).on( 'click', function() { + self.uploadFile(); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Uploads a file + */ + this.uploadFile = function() { + var ufile = document.getElementById( 'ufile' ).files[0]; + var data = new FormData(); + var newfilename = $("#formUploadFile input[name^=newfilename]").val(); + data.append('api', 'upload'); + data.append('dir', self.currentDir); + data.append('file', ufile); + data.append('newfilename', newfilename); + var id = self.generateGuid(); + $.ajax({ + url: self.api, + type: "POST", + data: data, + processData: false, + contentType: false, + dataType: "json", + xhr: function(){ + var xhr = $.ajaxSettings.xhr() ; + xhr.upload.onprogress = function(evt){ self.task_update(evt.loaded/evt.total*100,id); } ; + xhr.upload.onload = function(){ console.log('Uploading '+newfilename+' done.') } ; + return xhr ; + }, + success: function(data) { + if(data.status == "OK") { + self.showMessage("File successfully uploaded", "s"); + if(data.cd == self.currentDir) self.refreshFileTable(); + } else self.showMessage("File could not be uploaded: "+data.message, "e"); + }, + error: function() { self.showMessage("General error occured", "e"); }, + complete: function() { self.task_done(id); } + }); + self.task_add("Upload "+ufile.name, id); + }; + + /** + * Change the permissions of a file + * + * @params object e - event object + * @params string name - name of the file + */ + this.changePermissions = function( filename, newperms) { + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "changePermissions", + dir: self.currentDir, + filename: filename, + chmod: newperms + }), + dataType: "json", + success: function( data ){ + if( data.status == "OK" ) { + self.showMessage( "Permissions successfully changed.", "s" ); + self.refreshFileTable(); + } + else { + self.showMessage( "Permissions could not be changed: "+data.message, "e"); + } + }, + error: function() { self.showMessage("General error occured.", "e"); } + }); + }; + + /** + * Show the remote upload dialog + */ + this.showRemoteUploadDialog = function() { + self.showModal( self.templates.remoteupload ); + var form = $('#formRemoteUpload'); + form.find( '#url' ) + .on( 'keypress', self.preventEnter ) + .on( 'change keyup', function() { + $("#filename").val($(this).val().substr($(this).val().lastIndexOf("/")+1)); + }); + form.find( '#filename' ) + .on( 'keypress', self.preventEnter ) + .on( 'keyup', function() { $("#url").off( 'change keyup' ); }); + form.find( '#buttonUpload' ).on( 'click', function() { + self.remoteUpload(); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Remote uploads a file + */ + this.remoteUpload = function() { + var filename = $("#formRemoteUpload #filename").val(); + var id = ifm.generateGuid(); + $.ajax({ + url: ifm.api, + type: "POST", + data: ({ + api: "remoteUpload", + dir: ifm.currentDir, + filename: filename, + method: $("#formRemoteUpload input[name=method]:checked").val(), + url: encodeURI($("#url").val()) + }), + dataType: "json", + success: function(data) { + if(data.status == "OK") { + ifm.showMessage("File successfully uploaded", "s"); + ifm.refreshFileTable(); + } else ifm.showMessage("File could not be uploaded:
"+data.message, "e"); + }, + error: function() { ifm.showMessage("General error occured", "e"); }, + complete: function() { ifm.task_done(id); } + }); + ifm.task_add("Remote upload: "+filename, id); + }; + + /** + * Shows the ajax request dialog + */ + this.showAjaxRequestDialog = function() { + self.showModal( self.templates.ajaxrequest ); + var form = $('#formAjaxRequest'); + form.find( '#ajaxurl' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonRequest' ).on( 'click', function() { + self.ajaxRequest(); + return false; + }); + form.find( '#buttonClose' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Performs an ajax request + */ + this.ajaxRequest = function() { + $.ajax({ + url : $("#ajaxurl").val(), + cache : false, + data : $('#ajaxdata').val().replace(/\n/g,"&"), + type : $('#ajaxrequest input[name=arMethod]:checked').val(), + success : function(response) { $("#ajaxresponse").text(response); }, + error : function(e) { self.showMessage("Error: "+e, "e"); console.log(e); } + }); + }; + + /** + * Shows the delete dialog for multiple files + */ + this.showMultiDeleteDialog = function() { + self.showModal( Mustache.render( self.templates.multidelete, { count: $('#filetable tr.selectedItem').length } ) ); + var form = $('#formDeleteFiles'); + form.find( '#buttonYes' ).on( 'click', function() { + self.multiDelete(); + self.hideModal(); + return false; + }); + form.find( '#buttonNo' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Deletes multiple files + */ + this.multiDelete = function() { + var elements = $('#filetable tr.selectedItem'); + var filenames = []; + for(var i=0;typeof(elements[i])!='undefined';filenames.push(elements[i++].getAttribute('data-filename'))); + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "multidelete", + dir: self.currentDir, + filenames: filenames + }), + dataType: "json", + success: function(data) { + if(data.status == "OK") { + if(data.errflag == 1) + ifm.showMessage("All files successfully deleted.", "s"); + else if(data.errflag == 0) + ifm.showMessage("Some files successfully deleted. "+data.message); + else + ifm.showMessage("Files could not be deleted. "+data.message, "e"); + ifm.refreshFileTable(); + } else ifm.showMessage("Files could not be deleted:
"+data.message, "e"); + }, + error: function() { ifm.showMessage("General error occured", "e"); } + }); + }; + + // -------------------- + // helper functions + // -------------------- + + /** + * Shows a notification + * + * @param string m - message text + * @param string t - message type (e: error, s: success) + */ + this.showMessage = function(m, t) { + var msgType = (t == "e")?"danger":(t == "s")?"success":"info"; + var element = ( self.config.inline ) ? self.rootElement : "body"; + $.notify( + { message: m }, + { type: msgType, delay: 5000, mouse_over: 'pause', offset: { x: 15, y: 65 }, element: element } + ); + }; + + /** + * Combines two path components + * + * @param string a - component 1 + * @param string b - component 2 + */ + this.pathCombine = function(a, b) { + if(a == "" && b == "") return ""; + if(b[0] == "/") b = b.substring(1); + if(a == "") return b; + if(a[a.length-1] == "/") a = a.substring(0, a.length-1); + if(b == "") return a; + return a+"/"+b; + }; + + /** + * Prevents a user to submit a form via clicking enter + * + * @param object e - click event + */ + this.preventEnter = function(e) { + if( e.keyCode == 13 ) return false; + else return true; + } + + /** + * Checks if an element is part of an array + * + * @param obj needle - search item + * @param array haystack - array to search + */ + this.inArray = function(needle, haystack) { + for(var i = 0; i < haystack.length; i++) { if(haystack[i] == needle) return true; } return false; + }; + + /** + * Adds a task to the taskbar. + * + * @param string name - description of the task + * @param string id - identifier for the task + */ + this.task_add = function( name, id ) { + if( ! document.getElementById( "waitqueue" ) ) { + $( document.body ).prepend( '
' ); + } + $( "#waitqueue" ).prepend('\ +
\ +
\ +
\ +
\ + '+name+'\ +
\ +
\ +
\ + '); + }; + + /** + * Removes a task from the taskbar + * + * @param string id - task identifier + */ + this.task_done = function(id) { + $("#"+id).remove(); + if($("#waitqueue>div").length == 0) { + $("#waitqueue").remove(); + } + }; + + /** + * Updates a task + * + * @param integer progress - percentage of status + * @param string id - task identifier + */ + this.task_update = function(progress, id) { + $('#'+id+' .progress-bar').css('width', progress+'%').attr('aria-valuenow', progress); + }; + + /** + * Highlights an item in the file table + * + * @param object param - either an element id or a jQuery object + */ + this.highlightItem = function( param ) { + var highlight = function( el ) { + el.addClass( 'highlightedItem' ).siblings().removeClass( 'highlightedItem' ); + el.find( 'a' ).first().focus(); + if( ! self.isElementInViewport( el ) ) { + var scrollOffset = 0; + if( param=="prev" ) + scrollOffset = el.offset().top - ( window.innerHeight || document.documentElement.clientHeight ) + el.height() + 15; + else + scrollOffset = el.offset().top - 55; + $('html, body').animate( { scrollTop: scrollOffset }, 200 ); + } + }; + if( param.jquery ) { + highlight( param ); + } else { + var highlightedItem = $('.highlightedItem'); + if( ! highlightedItem.length ) { + highlight( $('#filetable tbody tr:first-child') ); + } else { + var newItem = ( param=="next" ? highlightedItem.next() : highlightedItem.prev() ); + + if( newItem.is( 'tr' ) ) { + highlight( newItem ); + } + } + } + }; + + /** + * Checks if an element is within the viewport + * + * @param object el - element object + */ + this.isElementInViewport = function (el) { + if (typeof jQuery === "function" && el instanceof jQuery) { + el = el[0]; + } + var rect = el.getBoundingClientRect(); + return ( + rect.top >= 60 && + rect.left >= 0 && + rect.bottom <= ( (window.innerHeight || document.documentElement.clientHeight) ) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + + /** + * Generates a GUID + */ + this.generateGuid = function() { + var result, i, j; + result = ''; + for(j=0; j<20; j++) { + i = Math.floor(Math.random()*16).toString(16).toUpperCase(); + result = result + i; + } + return result; + }; + + /** + * Logs a message if debug mode is on + * + * @param string m - message text + */ + this.log = function( m ) { + if( self.config.debug ) { + console.log( "IFM (debug): " + m ); + } + }; + + /** + * Encodes a string for use within javascript + * + * @param string s - encoding string + */ + this.JSEncode = function(s) { + return s.replace(/'/g, '\\x27').replace(/"/g, '\\x22'); + }; + + /** + * Handles the javascript pop states + * + * @param object event - event object + */ + this.historyPopstateHandler = function(event) { + var dir = ""; + if( event.state && event.state.dir ) dir = event.state.dir; + self.changeDirectory( dir, { pushState: false, absolute: true } ); + }; + + /** + * Handles keystrokes + * + * @param object e - event object + */ + this.handleKeystrokes = function( e ) { + // bind 'del' key + if( $(e.target).closest('input')[0] || $(e.target).closest('textarea')[0] ) { + return; + } + + switch( e.key ) { + case 'Delete': + if( self.config.delete ) { + if( $('#filetable tr.selectedItem').length > 0 ) { + e.preventDefault(); + self.showMultiDeleteDialog(); + } else { + var item = $('.highlightedItem'); + if( item.length ) + self.showDeleteFileDialog( item.data( 'filename' ) ); + } + } + break; + case 'e': + if( self.config.edit ) { + var item = $('.highlightedItem'); + if( item.length && ! item.hasClass( 'isDir' ) ) { + e.preventDefault(); + var action = item.data( 'eaction' ); + switch( action ) { + case 'extract': + self.showExtractFileDialog( item.data( 'filename' ) ); + break; + case 'edit': + self.editFile( item.data( 'filename' ) ); + } + } + } + break; + case 'g': + e.preventDefault(); + $('#currentDir').focus(); + break; + case 'r': + e.preventDefault(); + self.refreshFileTable(); + break; + case 'u': + if( self.config.upload ) { + e.preventDefault(); + self.showUploadFileDialog(); + } + break; + case 'o': + if( self.config.remoteupload ) { + e.preventDefault(); + self.showRemoteUploadDialog(); + } + break; + case 'a': + if( self.config.ajaxrequest ) { + e.preventDefault(); + self.showAjaxRequestDialog(); + } + break; + case 'F': + if( self.config.createfile ) { + e.preventDefault(); + self.showFileDialog(); + } + break; + case 'D': + if( self.config.createdir ) { + e.preventDefault(); + self.showCreateDirDialog(); + } + break; + case 'h': + case 'ArrowLeft': + e.preventDefault(); + self.changeDirectory( '..' ); + break; + case 'l': + case 'ArrowRight': + e.preventDefault(); + var item = $('.highlightedItem'); + if( item.hasClass('isDir') ) + self.changeDirectory( item.data( 'filename' ) ); + break; + case 'j': + case 'ArrowDown': + e.preventDefault(); + self.highlightItem('next'); + break; + case 'k': + case 'ArrowUp': + e.preventDefault(); + self.highlightItem('prev'); + break; + case 'Escape': + if( $(':focus').is( '.clickable-row td:first-child a:first-child' ) && $('.highlightedItem').length ) { + e.preventDefault(); + $('.highlightedItem').removeClass( 'highlightedItem' ); + } + break; + case ' ': // todo: make it work only when noting other is focused + case 'Enter': + if( $(':focus').is( '.clickable-row td:first-child a:first-child' ) ) { + var trParent = $(':focus').parent().parent(); + if( e.key == 'Enter' && trParent.hasClass( 'isDir' ) ) { + e.preventDefault(); + e.stopPropagation(); + self.changeDirectory( trParent.data( 'filename' ) ); + } else if( e.key == ' ' && ! trParent.is( ':first-child' ) ) { + e.preventDefault(); + e.stopPropagation(); + var item = $('.highlightedItem'); + if( item.is( 'tr' ) ) + item.toggleClass( 'selectedItem' ); + } + } + break; + } + } + + /** + * Initializes the application + */ + this.initLoadConfig = function() { + $.ajax({ + url: self.api, + type: "POST", + data: { + api: "getConfig" + }, + dataType: "json", + success: function(d) { + self.config = d; + self.log( "configuration loaded" ); + self.initLoadTemplates(); + }, + error: function() { + throw new Error( "IFM: could not load configuration" ); + } + }); + }; + + this.initLoadTemplates = function() { + // load the templates from the backend + $.ajax({ + url: self.api, + type: "POST", + data: { + api: "getTemplates" + }, + dataType: "json", + success: function(d) { + self.templates = d; + self.log( "templates loaded" ); + self.initApplication(); + }, + error: function() { + throw new Error( "IFM: could not load templates" ); + } + }); + }; + + this.initApplication = function() { + self.rootElement.html( + Mustache.render( + self.templates.app, + { + showpath: "/", + config: self.config, + ftbuttons: function(){ + return ( self.config.edit || self.config.rename || self.config.delete || self.config.zipnload || self.config.extract ); + } + } + ) + ); + // bind static buttons + $("#refresh").click(function(){ + self.refreshFileTable(); + }); + $("#createFile").click(function(){ + self.showFileDialog(); + }); + $("#createDir").click(function(){ + self.showCreateDirDialog(); + }); + $("#upload").click(function(){ + self.showUploadFileDialog(); + }); + $('#currentDir').on( 'keypress', function (event) { + if( event.keyCode == 13 ) { + event.preventDefault(); + self.changeDirectory( $(this).val(), { absolute: true } ); + } + }); + $('#buttonRemoteUpload').on( 'click', function() { + self.showRemoteUploadDialog(); + return false; + }); + $('#buttonAjaxRequest').on( 'click', function() { + self.showAjaxRequestDialog(); + return false; + }); + // handle keystrokes + $(document).on( 'keydown', self.handleKeystrokes ); + // handle history manipulation + window.onpopstate = self.historyPopstateHandler; + // load initial file table + if( window.location.hash ) { + self.changeDirectory( window.location.hash.substring( 1 ) ); + } else { + this.refreshFileTable(); + } + }; + + this.init = function( id ) { + self.rootElement = $('#'+id); + this.initLoadConfig(); + }; +} diff --git a/build/ifmlib.php b/build/ifmlib.php new file mode 100644 index 0000000..daf1659 --- /dev/null +++ b/build/ifmlib.php @@ -0,0 +1,2833 @@ + 0, + "auth_source" => 'inlineadmin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', + "root_dir" => "", + "tmp_dir" => "", + "defaulttimezone" => "Europe/Berlin", + + // api controls + "ajaxrequest" => 1, + "chmod" => 1, + "copymove" => 1, + "createdir" => 1, + "createfile" => 1, + "edit" => 1, + "delete" => 1, + "download" => 1, + "extract" => 1, + "upload" => 1, + "remoteupload" => 1, + "rename" => 1, + "zipnload" => 1, + + // gui controls + "showlastmodified" => 0, + "showfilesize" => 1, + "showowner" => 1, + "showgroup" => 1, + "showpermissions" => 2, + "showhtdocs" => 1, + "showhiddenfiles" => 1, + "showpath" => 0, + ); + + private $config = array(); + public $mode = ""; + + public function __construct( $config=array() ) { + if( session_status() !== PHP_SESSION_ACTIVE ) + session_start(); + $this->config = array_merge( $this->defaultconfig, $config ); + } + + /** + * This function contains the client-side application + */ + public function getApplication() { + print ' + + + IFM - improved file manager + + + '; + $this->getCSS(); + print ' + + +
'; + $this->getJS(); + print ' + + + + '; + } + + public function getInlineApplication() { + $this->getCSS(); + print '
'; + $this->getJS(); + } + + public function getCSS() { + print ' + + + + '; + } + + public function getJS() { + print ' + + + + + + + + '; + } + + /* + main functions + */ + + private function handleRequest() { + if($_REQUEST["api"] == "getRealpath") { + if( isset( $_REQUEST["dir"] ) && $_REQUEST["dir"] != "" ) + echo json_encode( array( "realpath" => $this->getValidDir( $_REQUEST["dir"] ) ) ); + else + echo json_encode( array( "realpath" => "" ) ); + } + elseif( $_REQUEST["api"] == "getFiles" ) { + if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) + $this->getFiles( $_REQUEST["dir"] ); + else + $this->getFiles( "" ); + } + elseif( $_REQUEST["api"] == "getConfig" ) { + $this->getConfig(); + } elseif( $_REQUEST["api"] == "getTemplates" ) { + echo json_encode( $this->getTemplates() ); + } else { + if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) { + switch( $_REQUEST["api"] ) { + case "createDir": $this->createDir( $_REQUEST["dir"], $_REQUEST["dirname"] ); break; + case "saveFile": $this->saveFile( $_REQUEST ); break; + case "getContent": $this->getContent( $_REQUEST ); break; + case "delete": $this->deleteFile( $_REQUEST ); break; + case "rename": $this->renameFile( $_REQUEST ); break; + case "download": $this->downloadFile( $_REQUEST ); break; + case "extract": $this->extractFile( $_REQUEST ); break; + case "upload": $this->uploadFile( $_REQUEST ); break; + case "copyMove": $this->copyMove( $_REQUEST ); break; + case "changePermissions": $this->changePermissions( $_REQUEST ); break; + case "zipnload": $this->zipnload( $_REQUEST); break; + case "remoteUpload": $this->remoteUpload( $_REQUEST ); break; + case "multidelete": $this->deleteMultipleFiles( $_REQUEST ); break; + case "getFolderTree": + echo json_encode( array_merge( array( 0 => array( "text" => "/ [root]", "nodes" => array(), "dataAttributes" => array( "path" => realpath( $this->config['root_dir'] ) ) ) ), $this->getFolderTreeRecursive( $this->config['root_dir'] ) ) ); + break; + default: + echo json_encode( array( "status" => "ERROR", "message" => "No valid api action given" ) ); + break; + } + } else { + print json_encode(array("status"=>"ERROR", "message"=>"No valid working directory")); + } + } + exit( 0 ); + } + + public function run( $mode="standalone" ) { + if ( $this->checkAuth() ) { + // go to our root_dir + if( ! is_dir( realpath( $this->config['root_dir'] ) ) || ! is_readable( realpath( $this->config['root_dir'] ) ) ) + die( "Cannot access root_dir."); + else + chdir( realpath( $this->config['root_dir'] ) ); + $this->mode = $mode; + if ( isset( $_REQUEST['api'] ) || $mode == "api" ) { + $this->handleRequest(); + } elseif( $mode == "standalone" ) { + $this->getApplication(); + } else { + $this->getInlineApplication(); + } + } + } + + /* + api functions + */ + + private function getFiles( $dir ) { + $dir = $this->getValidDir( $dir ); + $this->chDirIfNecessary( $dir ); + + unset( $files ); unset( $dirs ); $files = array(); $dirs = array(); + + if( $handle = opendir( "." ) ) { + while( false !== ( $result = readdir( $handle ) ) ) { + if( $result == basename( $_SERVER['SCRIPT_NAME'] ) && $this->getScriptRoot() == getcwd() ) { } + elseif( ( $result == ".htaccess" || $result==".htpasswd" ) && $this->config['showhtdocs'] != 1 ) {} + elseif( $result == "." ) {} + elseif( $result != ".." && substr( $result, 0, 1 ) == "." && $this->config['showhiddenfiles'] != 1 ) {} + else { + $item = array(); + $item["name"] = $result; + if( is_dir($result) ) { + $item["type"] = "dir"; + if( $result == ".." ) + $item["icon"] = "icon icon-up-open"; + else + $item["icon"] = "icon icon-folder-empty"; + } else { + $item["type"] = "file"; + $type = substr( strrchr( $result, "." ), 1 ); + $item["icon"] = $this->getTypeIcon( $type ); + } + if( $this->config['showlastmodified'] == 1 ) { $item["lastmodified"] = date( "d.m.Y, G:i e", filemtime( $result ) ); } + if( $this->config['showfilesize'] == 1 ) { + $item["size"] = filesize( $result ); + if( $item["size"] > 1073741824 ) $item["size"] = round( ( $item["size"]/1073741824 ), 2 ) . " GB"; + elseif($item["size"]>1048576)$item["size"] = round( ( $item["size"]/1048576 ), 2 ) . " MB"; + elseif($item["size"]>1024)$item["size"] = round( ( $item["size"]/1024 ), 2 ) . " KB"; + else $item["size"] = $item["size"] . " Byte"; + } + if( $this->config['showpermissions'] > 0 ) { + if( $this->config['showpermissions'] == 1 ) $item["fileperms"] = substr( decoct( fileperms( $result ) ), -3 ); + elseif( $this->config['showpermissions'] == 2 ) $item["fileperms"] = $this->filePermsDecode( fileperms( $result ) ); + if( $item["fileperms"] == "" ) $item["fileperms"] = " "; + $item["filepermmode"] = ( $this->config['showpermissions'] == 1 ) ? "short" : "long"; + } + if( $this->config['showowner'] == 1 ) { + if ( function_exists( "posix_getpwuid" ) && fileowner($result) !== false ) { + $ownerarr = posix_getpwuid( fileowner( $result ) ); + $item["owner"] = $ownerarr['name']; + } else $item["owner"] = false; + } + if( $this->config['showgroup'] == 1 ) { + if( function_exists( "posix_getgrgid" ) && filegroup( $result ) !== false ) { + $grouparr = posix_getgrgid( filegroup( $result ) ); + $item["group"] = $grouparr['name']; + } else $item["group"] = false; + } + if( is_dir( $result ) ) $dirs[] = $item; + else $files[] = $item; + } + } + closedir( $handle ); + } + usort( $dirs, array( $this, "sortByName" ) ); + usort( $files, array( $this, "sortByName" ) ); + echo json_encode( array_merge( $dirs, $files ) ); + } + + private function getConfig() { + $ret = $this->config; + $ret['inline'] = ( $this->mode == "inline" ) ? true : false; + $ret['isDocroot'] = ( realpath( $this->config['root_dir'] ) == dirname( __FILE__ ) ) ? "true" : "false"; + echo json_encode( $ret ); + } + + private function getFolderTreeRecursive( $start_dir ) { + $ret = array(); + $start_dir = realpath( $start_dir ); + if( $handle = opendir( $start_dir ) ) { + while (false !== ( $result = readdir( $handle ) ) ) { + if( is_dir( $this->pathCombine( $start_dir, $result ) ) && $result != "." && $result != ".." ) { + array_push( $ret, array( "text" => $result, "dataAttributes" => array( "path" => $this->pathCombine( $start_dir, $result ) ), "nodes" => $this->getFolderTreeRecursive( $this->pathCombine( $start_dir, $result ) ) ) ); + } + } + } + sort( $ret ); + return $ret; + } + + private function copyMove( $d ) { + if( $this->config['copymove'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to copy or move files." ) ); + exit( 1 ); + } + $this->chDirIfNecessary( $d['dir'] ); + if( ! isset( $d['destination'] ) || ! $this->isPathValid( realpath( $d['destination'] ) ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "No valid destination directory given." ) ); + exit( 1 ); + } + if( ! file_exists( $d['filename'] ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "No valid filename given." ) ); + exit( 1 ); + } + if( $d['action'] == "copy" ) { + if( $this->copyr( $d['filename'], $d['destination'] ) ) { + echo json_encode( array( "status" => "OK", "message" => "File(s) were successfully copied." ) ); + exit( 0 ); + } else { + $err = error_get_last(); + echo json_encode( array( "status" => "ERROR", "message" => $err['message'] ) ); + exit( 1 ); + } + } elseif( $d['action'] == "move" ) { + if( rename( $d['filename'], $this->pathCombine( $d['destination'], basename( $d['filename'] ) ) ) ) { + echo json_encode( array( "status" => "OK", "message" => "File(s) were successfully moved." ) ); + exit( 0 ); + } else { + $err = error_get_last(); + echo json_encode( array( "status" => "ERROR", "message" => $err['message'] ) ); + exit( 1 ); + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "No valid action given." ) ); + exit( 1 ); + } + } + + // creates a directory + private function createDir($w, $dn) { + if( $this->config['createDir'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to create directories.") ); + exit( 1 ); + } + if( $dn == "" ) { + echo json_encode( array( "status" => "ERROR", "message" => "No valid directory name") ); + } elseif( strpos( $dn, '/' ) !== false ) echo json_encode( array( "status" => "ERROR", "message" => "No slashes allowed in directory names" ) ); + else { + $this->chDirIfNecessary( $w ); + if( @mkdir( $dn ) ) { + echo json_encode( array( "status" => "OK", "message" => "Directory successful created" ) ); + } else { + echo json_encode( array( "status" => "ERROR", "message" => "Could not create directory" ) ); + } + } + } + + // save a file + private function saveFile( $d ) { + if( ( file_exists( $this->pathCombine( $d['dir'], $d['filename'] ) ) && $this->config['edit'] != 1 ) || ( ! file_exists( $this->pathCombine( $d['dir'], $d['filename'] ) ) && $this->config['createfile'] != 1 ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit/create this file." ) ); + exit( 1 ); + } + if( isset( $d['filename'] ) && $d['filename'] != "" ) { + // if you are not allowed to see .ht-docs you can't save one + if( $this->config['showhtdocs'] != 1 && substr( $d['filename'], 0, 3 ) == ".ht" ) { + echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit or create htdocs" ) ); + } + // same with hidden files + elseif( $this->config['showhiddenfiles'] != 1 && substr( $d['filename'], 0, 1 ) == "." ) { + echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit or create hidden files" ) ); + } + elseif(strpos($d['filename'],'/')!==false) { + echo json_encode( array( "status" => "ERROR", "message" => "Filenames cannot contain slashes." ) ); + } else { + if( isset( $d['content'] ) ) { + $this->chDirIfNecessary( $d['dir'] ); + // work around magic quotes + $content = get_magic_quotes_gpc() == 1 ? stripslashes( $d['content'] ) : $d['content']; + if( @file_put_contents( $d['filename'], $content ) !== false ) { + echo json_encode( array( "status" => "OK", "message" => "File successfully saved" ) ); + } else { + echo json_encode( array( "status" => "ERROR", "message" => "Could not write content" ) ); + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "Got no content" ) ); + } + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "No filename specified" ) ); + } + } + + // gets the content of a file + // notice: if the content is not JSON encodable it returns an error + private function getContent( array $d ) { + if( $this->config['edit'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit files." ) ); + else { + $this->chDirIfNecessary( $d['dir'] ); + if( file_exists( $d['filename'] ) ) { + $content = @file_get_contents( $d['filename'] ); + $utf8content = mb_convert_encoding( $content, 'UTF-8', mb_detect_encoding( $content, 'UTF-8, ISO-8859-1', true ) ); + echo json_encode( array( "status" => "OK", "data" => array( "filename" => $d['filename'], "content" => $utf8content ) ) ); + } else echo json_encode( array( "status" => "ERROR", "message" => "File not found" ) ); + } + } + + // deletes a file or a directory (recursive!) + private function deleteFile( array $d ) { + if( $this->config['delete'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete files" ) ); + } + else { + $this->chDirIfNecessary( $d['dir'] ); + if( is_dir( $d['filename'] ) ) { + $res = $this->rec_rmdir( $d['filename'] ); + if( $res != 0 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete files" ) ); + } else { + echo json_encode( array( "status" => "OK", "message" => "Directoy successful deleted" ) ); + } + } + else{ + if( @unlink( $d['filename'] ) ) { + echo json_encode( array( "status" => "OK", "message" => "File successful deleted" ) ); + } else { + echo json_encode( array( "status"=>"ERROR", "message" => "File could not be deleted" ) ); + } + } + } + } + + // deletes a bunch of files or directories + private function deleteMultipleFiles( array $d ) { + if( $this->config['delete'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete files" ) ); + else { + $this->chDirIfNecessary( $d['dir'] ); + $err = array(); $errFLAG = -1; // -1 -> no files deleted; 0 -> at least some files deleted; 1 -> all files deleted + foreach( $d['filenames'] as $file ) { + if( is_dir($file) ){ + $res = $this->rec_rmdir( $file ); + if( $res != 0 ) + array_push( $err, $file ); + else + $errFLAG = 0; + } else { + if( @unlink($file) ) + $errFLAG = 0; + else + array_push($err, $file); + } + } + if( empty( $err ) ) { + echo json_encode( array( "status" => "OK", "message" => "Files deleted successfully", "errflag" => "1" ) ); + } + else { + $errmsg = "The following files could not be deleted:
    "; + foreach($err as $item) + $errmsg .= "
  • ".$item."
  • "; + $errmsg .= "
"; + echo json_encode( array( "status" => "OK", "message" => $errmsg, "flag" => $errFLAG ) ); + } + } + } + + // renames a file + private function renameFile( array $d ) { + if( $this->config['rename'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to rename files" ) ); + } else { + $this->chDirIfNecessary( $d['dir'] ); + if( strpos( $d['newname'], '/' ) !== false ) + echo json_encode( array( "status" => "ERROR", "message" => "No slashes allowed in filenames" ) ); + elseif( $this->config['showhtdocs'] != 1 && ( substr( $d['newname'], 0, 3) == ".ht" || substr( $d['filename'], 0, 3 ) == ".ht" ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to rename this file" ) ); + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $d['newname'], 0, 1) == "." || substr( $d['filename'], 0, 1 ) == "." ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to rename file" ) ); + else { + if( @rename( $d['filename'], $d['newname'] ) ) + echo json_encode( array( "status" => "OK", "message" => "File successful renamed" ) ); + else + echo json_encode( array( "status" => "ERROR", "message" => "File could not be renamed" ) ); + } + } + } + + // provides a file for downloading + private function downloadFile( array $d ) { + if( $this->config['download'] != 1 ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to download files" ) ); + elseif( $this->config['showhtdocs'] != 1 && ( substr( $d['filename'], 0, 3 ) == ".ht" || substr( $d['filename'],0,3 ) == ".ht" ) ) + echo json_encode( array( "status" => "ERROR", "message"=>"Not allowed to download htdocs" ) ); + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $d['filename'], 0, 1 ) == "." || substr( $d['filename'],0,1 ) == "." ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to download hidden files" ) ); + else { + $this->chDirIfNecessary( $d["dir"] ); + $this->fileDownload( $d['filename'] ); + } + } + + // extracts a zip-archive + private function extractFile( array $d ) { + if( $this->config['extract'] != 1 ) + echo json_encode( array( "status" => "ERROR", "message" => "No permission to extract files" ) ); + else { + $this->chDirIfNecessary( $d['dir'] ); + if( ! file_exists( $d['filename'] ) || substr( $d['filename'],-4 ) != ".zip" ) { + echo json_encode( array( "status" => "ERROR","message" => "No valid zip file found" ) ); + exit( 1 ); + } + if( ! isset( $d['targetdir'] ) || trim( $d['targetdir'] ) == "" ) + $d['targetdir'] = "./"; + if( ! $this->isPathValid( $d['targetdir'] ) ) { + echo json_encode( array( "status" => "ERROR","message" => "Target directory is not valid." ) ); + exit( 1 ); + } + if( ! is_dir( $d['targetdir'] ) && ! mkdir( $d['targetdir'], 0777, true ) ) { + echo json_encode( array( "status" => "ERROR","message" => "Could not create target directory." ) ); + exit( 1 ); + } + if( ! IFMZip::extract( $d['filename'], $d['targetdir'] ) ) { + echo json_encode( array( "status" => "ERROR","message" => "File could not be extracted" ) ); + } else { + echo json_encode( array( "status" => "OK","message" => "File successfully extracted." ) ); + } + } + } + + // uploads a file + private function uploadFile( array $d ) { + if( $this->config['upload'] != 1 ) + echo json_encode( array( "status" => "ERROR", "message" => "No permission to upload files" ) ); + elseif( !isset( $_FILES['file'] ) ) + echo json_encode( array( "file" => $_FILE,"files" => $_FILES ) ); + else { + $this->chDirIfNecessary( $d['dir'] ); + $newfilename = ( isset( $d["newfilename"] ) && $d["newfilename"]!="" ) ? $d["newfilename"] : $_FILES['file']['name']; + if( $this->config['showhtdocs'] != 1 && ( substr( $newfilename, 0, 3 ) == ".ht" || substr( $newfilename,0,3 ) == ".ht" ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to upload htdoc file" ) ); + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $newfilename, 0, 1 ) == "." || substr( $newfilename,0,1 ) == "." ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to upload hidden file" ) ); + else { + if( $_FILES['file']['tmp_name'] ) { + if( is_writable( getcwd( ) ) ) { + if( move_uploaded_file( $_FILES['file']['tmp_name'], $newfilename ) ) + echo json_encode( array( "status" => "OK", "message" => "The file ".$_FILES['file']['name']." was uploaded successfully", "cd" => $d['dir'] ) ); + else + echo json_encode( array( "status" => "ERROR", "message" => "File could not be uploaded" ) ); + } + else { + echo json_encode( array( "status" => "ERROR", "message" => "File could not be uploaded since it has no permissions to write in this directory" ) ); + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "No file found" ) ); + } + } + } + } + + // change permissions of a file + private function changePermissions( array $d ) { + if( $this->config['chmod'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No rights to change permissions" ) ); + elseif( ! isset( $d["chmod"] )||$d['chmod']=="" ) echo json_encode( array( "status" => "ERROR", "message" => "Could not identify new permissions" ) ); + elseif( ! $this->isPathValid( $this->pathCombine( $d['dir'],$d['filename'] ) ) ) { echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to change the permissions" ) ); } + else { + $this->chDirIfNecessary( $d['dir'] ); $chmod = $d["chmod"]; $cmi = true; + if( ! is_numeric( $chmod ) ) { + $cmi = false; + $chmod = str_replace( " ","",$chmod ); + if( strlen( $chmod )==9 ) { + $cmi = true; + $arr = array( substr( $chmod,0,3 ),substr( $chmod,3,3 ),substr( $chmod,6,3 ) ); + $chtmp = "0"; + foreach( $arr as $right ) { + $rtmp = 0; + if( substr( $right,0,1 )=="r" ) $rtmp = $rtmp + 4; elseif( substr( $right,0,1 )<>"-" ) $cmi = false; + if( substr( $right,1,1 )=="w" ) $rtmp = $rtmp + 2; elseif( substr( $right,1,1 )<>"-" ) $cmi = false; + if( substr( $right,2,1 )=="x" ) $rtmp = $rtmp + 1; elseif( substr( $right,2,1 )<>"-" ) $cmi = false; + $chtmp = $chtmp . $rtmp; + } + $chmod = intval( $chtmp ); + } + } + else $chmod = "0" . $chmod; + + if( $cmi ) { + try { + chmod( $d["filename"], (int)octdec( $chmod ) ); + echo json_encode( array( "status" => "OK", "message" => "Permissions changed successfully" ) ); + } catch ( Exception $e ) { + echo json_encode( array( "status" => "ERROR", "message" => "Error while changing permissions" ) ); + } + } + else echo json_encode( array( "status" => "ERROR", "message" => "Could not determine permission format" ) ); + } + } + + // zips a directory and provides it for downloading + // it creates a temporary zip file in the current directory, so it has to be as much space free as the file size is + private function zipnload( array $d ) { + if( $this->config['zipnload'] != 1 ) + echo json_encode( array( "status" => "ERROR", "message" => "No permission to download directories" ) ); + else { + $this->chDirIfNecessary( $d['dir'] ); + if( ! file_exists( $d['filename'] ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Directory not found" ) ); + elseif ( ! $this->allowedFileName( $d['filename'] ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Filename not allowed" ) ); + else { + unset( $zip ); + $dfile = $this->pathCombine( $this->config['tmp_dir'], uniqid( "ifm-tmp-" ) . ".zip" ); // temporary filename + try { + IFMZip::create( realpath( $d['filename'] ), $dfile, ( $d['filename'] == "." ) ); + if( $d['filename'] == "." ) { + if( getcwd() == $this->getScriptRoot() ) + $d['filename'] = "root"; + else + $d['filename'] = basename( getcwd() ); + } + $this->fileDownload( $dfile, $d['filename'] . ".zip" ); + } catch ( Exception $e ) { + echo "An error occured: " . $e->getMessage(); + } finally { + if( file_exists( $dfile ) ) @unlink( $dfile ); + } + } + } + } + + // uploads a file from an other server using the curl extention + private function remoteUpload( array $d ) { + if( $this->config['remoteupload'] != 1 ) + echo json_encode( array( "status" => "ERROR", "message" => "No permission to remote upload files" ) ); + elseif( !isset( $d['method'] ) || !in_array( $d['method'], array( "curl", "file" ) ) ) + echo json_encode( array( "status" => "error", "message" => "No valid method given. Valid methods: ['curl', 'file']" ) ); + elseif( $d['method']=="curl" && $this->checkCurl( ) == false ) + echo json_encode( array( "status" => "ERROR", "message" => "cURL extention not installed. Please install the cURL extention to use remote file upload." ) ); + elseif( $d['method']=="curl" && $this->checkCurl( ) == true ) { + $filename = ( isset( $d['filename'] )&&$d['filename']!="" )?$d['filename']:"curl_".uniqid( ); + $this->chDirIfNecessary( $d['dir'] ); + $ch = curl_init( ); + if( $ch ) { + if( $this->allowedFileName( $filename ) == false ) + echo json_encode( array( "status" => "ERROR", "message" => "This filename is not allowed due to the config." ) ); + elseif( filter_var( $d['url'], FILTER_VALIDATE_URL ) === false ) + echo json_encode( array( "status" => "ERROR", "message" => "The passed URL is not valid" ) ); + else { + $fp = fopen( $filename, "w" ); + if( $fp ) { + if( !curl_setopt( $ch, CURLOPT_URL, $d['url'] ) || !curl_setopt( $ch, CURLOPT_FILE, $fp ) || !curl_setopt( $ch, CURLOPT_HEADER, 0 ) || !curl_exec( $ch ) ) + echo json_encode( array( "status" => "ERROR", "message" => "Failed to set options and execute cURL" ) ); + else { + echo json_encode( array( "status" => "OK", "message" => "File sucessfully uploaded" ) ); + } + curl_close( $ch ); + fclose( $fp ); + } else { + echo json_encode( array( "status" => "ERROR", "message" => "Failed to open file" ) ); + } + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "Failed to init cURL." ) ); + } + } + elseif( $d['method']=='file' ) { + $filename = ( isset( $d['filename'] ) && $d['filename']!="" ) ? $d['filename'] : "curl_".uniqid( ); + $this->chDirIfNecessary( $d['dir'] ); + try { + file_put_contents( $filename, file_get_contents( $d['url'] ) ); + echo json_encode( array( "status" => "OK", "message" => "File successfully uploaded" ) ); + } catch( Exception $e ) { + echo json_encode( array( "status" => "ERROR", "message" => $e->getMessage() ) ); + } + } + else echo json_encode( array( "status" => "error", "message" => "Corrupt parameter data" ) ); + } + + //apis + + /* + help functions + */ + + public function checkAuth() { + if( $this->config['auth'] == 1 && ( ! isset( $_SESSION['auth'] ) || $_SESSION['auth'] !== true ) ) { + $login_failed = false; + if( isset( $_POST["user"] ) && isset( $_POST["pass"] ) ) { + if( $this->checkCredentials( $_POST["user"], $_POST["pass"] ) ) { + $_SESSION['auth'] = true; + } + else { + $_SESSION['auth'] = false; + $login_failed = true; + } + } + + if( isset( $_SESSION['auth'] ) && $_SESSION['auth'] === true ) { + return true; + } else { + if( isset( $_POST["api"] ) ) { + if( $login_failed === true ) + echo json_encode( array( "status"=>"ERROR", "message"=>"authentication failed" ) ); + else + echo json_encode( array( "status"=>"ERROR", "message"=>"not authenticated" ) ); + } else { + $this->loginForm($login_failed); + } + return false; + } + } else { + return true; + } + } + + private function checkCredentials( $user, $pass ) { + list( $src, $srcopt ) = explode( ";", $this->config['auth_source'], 2 ); + switch( $src ) { + case "inline": + list( $uname, $hash ) = explode( ":", $srcopt ); + return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; + break; + case "file": + if( @file_exists( $srcopt ) && @is_readable( $srcopt ) ) { + list( $uname, $hash ) = explode( ":", fgets( fopen( $srcopt, 'r' ) ) ); + return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; + } else { + return false; + } + break; + case "ldap": + $authenticated = false; + list( $ldap_server, $rootdn ) = explode( ";", $srcopt ); + $u = "uid=" . $user . "," . $rootdn; + if( ! $ds = ldap_connect( $ldap_server ) ) { + trigger_error( "Could not reach the ldap server.", E_USER_ERROR ); + return false; + } + ldap_set_option( $ds, LDAP_OPT_PROTOCOL_VERSION, 3 ); + if( $ds ) { + $ldbind = @ldap_bind( $ds, $u, $pass ); + if( $ldbind ) { + $authenticated = true; + } else { + trigger_error( ldap_error( $ds ), E_USER_ERROR ); + $authenticated = false; + } + ldap_unbind( $ds ); + } else { + $authenticated = false; + } + return $authenticated; + break; + } + return false; + } + + private function loginForm($loginFailed=false) { + print ' + + + IFM - improved file manager + + + + +

IFM - Login

+
'; + if($loginFailed){ print '
Login attempt failed. Please try again.
'; } + print '
+
+ +
+ + '; + } + + private function filePermsDecode( $perms ) { + $oct = str_split( strrev( decoct( $perms ) ), 1 ); + $masks = array( '---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx' ); + return( + sprintf( + '%s %s %s', + array_key_exists( $oct[ 2 ], $masks ) ? $masks[ $oct[ 2 ] ] : '###', + array_key_exists( $oct[ 1 ], $masks ) ? $masks[ $oct[ 1 ] ] : '###', + array_key_exists( $oct[ 0 ], $masks ) ? $masks[ $oct[ 0 ] ] : '###') + ); + } + + private function getValidDir( $dir ) { + if( ! $this->isPathValid( $dir ) || ! is_dir( $dir ) ) { + return ""; + } else { + $rpDir = realpath( $dir ); + $rpConfig = realpath( $this->config['root_dir'] ); + if( $rpConfig == "/" ) + return $rpDir; + elseif( $rpDir == $rpConfig ) + return ""; + else + return substr( $rpDir, strlen( $rpConfig ) + 1 ); + } + } + + private function isPathValid( $dir ) { + /** + * This function is also used to check non-existent paths, but the PHP realpath function returns false for + * nonexistent paths. Hence we need to check the path manually in the following lines. + */ + $tmp_d = $dir; + $tmp_missing_parts = array(); + while( realpath( $tmp_d ) === false ) { + $tmp_i = pathinfo( $tmp_d ); + array_push( $tmp_missing_parts, $tmp_i['filename'] ); + $tmp_d = dirname( $tmp_d ); + } + $rpDir = $this->pathCombine( realpath( $tmp_d ), implode( "/", array_reverse( $tmp_missing_parts ) ) ); + $rpConfig = ( $this->config['root_dir'] == "" ) ? realpath( dirname( __FILE__ ) ) : realpath( $this->config['root_dir'] ); + if( ! is_string( $rpDir ) || ! is_string( $rpConfig ) ) // can happen if open_basedir is in effect + return false; + elseif( $rpDir == $rpConfig ) + return true; + elseif( 0 === strpos( $rpDir, $rpConfig ) ) + return true; + else + return false; + } + + private function getScriptRoot() { + return dirname( $_SERVER["SCRIPT_FILENAME"] ); + } + + private function chDirIfNecessary($d) { + if( substr( getcwd(), strlen( $this->getScriptRoot() ) ) != $this->getValidDir($d) ) { + chdir( $d ); + } + } + + private function getTypeIcon( $type ) { + switch( $type ) { + case "aac": case "aiff": case "mid": case "mp3": case "wav": return 'icon icon-file-audio'; break; + case "ai": case "bmp": case "eps": case "tiff": case "gif": case "jpg": case "jpeg": case "png": case "psd": return 'icon icon-file-image'; break; + case "avi": case "flv": case "mp4": case "mpg": case "mkv": case "mpeg": case "webm": case "wmv": case "mov": return 'icon icon-file-video'; break; + case "c": case "cpp": case "css": case "dat": case "h": case "html": case "php": case "java": case "py": case "sql": case "xml": case "yml": return 'icon icon-file-code'; break; + case "doc": case "dotx": case "odf": case "odt": case "rtf": return 'icon icon-file-word'; break; + case "ods": case "xls": case "xlsx": return 'icon icon-file-excel'; break; + case "odp": case "ppt": case "pptx": return 'icon icon-file-powerpoint'; break; + case "pdf": return 'icon icon-file-pdf'; break; + case "tgz": case "zip": case "tar": case "7z": case "rar": return 'icon icon-file-archive'; + default: return 'icon icon-doc'; + } + } + + private function rec_rmdir( $path ) { + if( !is_dir( $path ) ) { + return -1; + } + $dir = @opendir( $path ); + if( !$dir ) { + return -2; + } + while( ( $entry = @readdir( $dir ) ) !== false ) { + if( $entry == '.' || $entry == '..' ) continue; + if( is_dir( $path . '/' . $entry ) ) { + $res = $this->rec_rmdir( $path . '/' . $entry ); + if( $res == -1 ) { @closedir( $dir ); return -2; } + else if( $res == -2 ) { @closedir( $dir ); return -2; } + else if( $res == -3 ) { @closedir( $dir ); return -3; } + else if( $res != 0 ) { @closedir( $dir ); return -2; } + } else if( is_file( $path . '/' . $entry ) || is_link( $path . '/' . $entry ) ) { + $res = @unlink( $path . '/' . $entry ); + if( !$res ) { @closedir( $dir ); return -2; } + } else { @closedir( $dir ); return -3; } + } + @closedir( $dir ); + $res = @rmdir( $path ); + if( !$res ) { return -2; } + return 0; + } + + /** + * Copy a file, or recursively copy a folder and its contents + * + * @author Aidan Lister + * @version 1.0.1 + * @link http://aidanlister.com/2004/04/recursively-copying-directories-in-php/ + * @param string $source Source path + * @param string $dest Destination path + * @return bool Returns TRUE on success, FALSE on failure + */ + private function copyr( $source, $dest ) + { + // Check for symlinks + if (is_link($source)) { + return symlink(readlink($source), $dest); + } + + // Simple copy for a file + if (is_file($source)) { + $dest = ( is_dir( $dest ) ) ? $this->pathCombine( $dest, basename( $source ) ) : $dest; + return copy($source, $dest); + } else { + $dest = $this->pathCombine( $dest, basename( $source ) ); + } + + // Make destination directory + if (!is_dir($dest)) { + mkdir($dest); + } + + // Loop through the folder + $dir = dir($source); + while (false !== $entry = $dir->read()) { + // Skip pointers + if ($entry == '.' || $entry == '..') { + continue; + } + + // Deep copy directories + $this->copyr("$source/$entry", "$dest/$entry"); + } + + // Clean up + $dir->close(); + return true; + } + + // combines two parts to a valid path + private function pathCombine( $a, $b ) { + if( trim( $a ) == "" && trim( $b ) == "" ) + return ""; + elseif( trim( $a ) == "" ) + return ltrim( $b, '/' ); + else + return rtrim( $a, '/' ) . '/' . ltrim( $b, '/' ); + } + + // check if filename is allowed + private function allowedFileName( $f ) { + if( $this->config['showhtdocs'] != 1 && substr( $f, 0, 3 ) == ".ht" ) + return false; + elseif( $this->config['showhiddenfiles'] != 1 && substr( $f, 0, 1 ) == "." ) + return false; + elseif( ! $this->isPathValid( $f ) ) + return false; + return true; + } + + // sorting function for file and dir arrays + private function sortByName( $a, $b ) { + if( strtolower( $a['name'] ) == strtolower( $b['name'] ) ) return 0; + return ( strtolower( $a['name'] ) < strtolower( $b['name'] ) ) ? -1 : 1; + } + + // is cURL extention avaliable? + private function checkCurl() { + if( !function_exists( "curl_init" ) || + !function_exists( "curl_setopt" ) || + !function_exists( "curl_exec" ) || + !function_exists( "curl_close" ) ) return false; + else return true; + } + + private function fileDownload( $file, $name="" ) { + header( 'Content-Description: File Transfer' ); + header( 'Content-Type: application/octet-stream' ); + header( 'Content-Disposition: attachment; filename="' . ( trim( $name ) == "" ? basename( $file ) : $name ) . '"' ); + header( 'Expires: 0' ); + header( 'Cache-Control: must-revalidate' ); + header( 'Pragma: public' ); + header( 'Content-Length: ' . filesize( $file ) ); + + $file_stream = fopen( $file, 'rb' ); + $stdout_stream = fopen('php://output', 'wb'); + + stream_copy_to_stream($file_stream, $stdout_stream); + + fclose($file_stream); + fclose($stdout_stream); + } + + private function getTemplates() { + $templates = array(); + $templates['app'] = <<<'f00bar' + +
+ + + + + {{#config.download}} + + {{/config.download}} + {{#config.showlastmodified}} + + {{/config.showlastmodified}} + {{#config.showfilesize}} + + {{/config.showfilesize}} + {{#config.showpermissions}} + + {{/config.showpermissions}} + {{#config.showowner}} + + {{/config.showowner}} + {{#config.showgroup}} + + {{/config.showgroup}} + + + + + +
Filenamelast modifiedsize
+
+
+ +
+ +f00bar; + $templates['filetable'] = <<<'f00bar' + +{{#items}} + + + + + {{linkname}} + + + {{#config.download}} + +
+ + + +
+ + + + + {{/config.download}} + {{#config.showlastmodified}} + {{lastmodified}} + {{/config.showlastmodified}} + {{#config.showfilesize}} + {{size}} + {{/config.showfilesize}} + {{#config.showpermissions}} + + + + {{/config.showpermissions}} + {{#config.showowner}} + + {{owner}} + + {{/config.showowner}} + {{#config.showgroup}} + + {{group}} + + {{/config.showgroup}} + + {{#button}} + + + + {{/button}} + + +{{/items}} + + +f00bar; + $templates['file'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['createdir'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['ajaxrequest'] = <<<'f00bar' +
+
+ +f00bar; + $templates['copymove'] = <<<'f00bar' +
+
+ + +
+
+ +f00bar; + $templates['createdir'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['deletefile'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['extractfile'] = <<<'f00bar' +
+
+ + +
+
+ +f00bar; + $templates['file'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['multidelete'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['remoteupload'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['renamefile'] = <<<'f00bar' + + +f00bar; + $templates['uploadfile'] = <<<'f00bar' +
+ + +
+ +f00bar; + + return $templates; + } +} + +/* ======================================================================= + * Improved File Manager + * --------------------- + * License: This project is provided under the terms of the MIT LICENSE + * http://github.com/misterunknown/ifm/blob/master/LICENSE + * ======================================================================= + * + * zip class + * + * this was adapted from http://php.net/manual/de/class.ziparchive.php#110719 +*/ + +class IFMZip { + /** + * Add a folder to the zip file + */ + private static function folderToZip($folder, &$zipFile, $exclusiveLength) { + $handle = opendir( $folder ); + while( false !== $f = readdir( $handle ) ) { + if( $f != '.' && $f != '..' ) { + $filePath = "$folder/$f"; + if( file_exists( $filePath ) && is_readable( $filePath ) ) { + // Remove prefix from file path before add to zip. + $localPath = substr($filePath, $exclusiveLength); + if( is_file( $filePath ) ) { + $zipFile->addFile( $filePath, $localPath ); + } elseif( is_dir( $filePath ) ) { + // Add sub-directory. + $zipFile->addEmptyDir( $localPath ); + self::folderToZip( $filePath, $zipFile, $exclusiveLength ); + } + } + } + } + closedir( $handle ); + } + + /** + * Create a zip file + */ + public static function create( $src, $out, $root=false ) + { + $z = new ZipArchive(); + $z->open( $out, ZIPARCHIVE::CREATE); + if( $root ) { + self::folderToZip( realpath( $src ), $z, strlen( realpath( $src ) . '/' ) ); + } else { + $z->addEmptyDir( basename( $src ) ); + self::folderToZip( realpath( $src ), $z, strlen( dirname( $src ) . '/' ) ); + } + try { + if( ( $res = $z->close() ) !== true ) { + throw new Exception("Error while creating zip archive: ". $z->getStatusString()); + } + } catch ( Exception $e ) { + throw $e; + } + } + + /** + * Unzip a zip file + */ + public static function extract( $file, $destination="./" ) { + $zip = new ZipArchive; + $res = $zip->open( $file ); + if( $res === true ) { + $zip->extractTo( $destination ); + $zip->close(); + return true; + } else { + return false; + } + } +} diff --git a/compiler.php b/compiler.php index f6d471a..8192409 100755 --- a/compiler.php +++ b/compiler.php @@ -1,32 +1,56 @@ #!/usr/bin/env php run();' +), FILE_APPEND ); + +/** + * Build library + */ +file_put_contents( $IFM_BUILD_LIB_PHP, $main ); +file_put_contents( $IFM_BUILD_LIB_PHP, $phpincludes, FILE_APPEND ); diff --git a/ifm.php b/ifm.php index 4fe3dca..901b55e 100644 --- a/ifm.php +++ b/ifm.php @@ -1,157 +1,5 @@ not; 1 -> octal, 2 -> human readable - const showhtdocs = 1; // show .htaccess and .htpasswd - const showhiddenfiles = 1; // show files beginning with a dot (e.g. ".bashrc") - const showpath = 0; // show absolute path - - /* - authentication - - This provides a super simple authentication functionality. At the moment only one user can be - configured. The credential information can be either set inline or read from a file. The - password has to be a hash generated by PHPs password_hash function. The default credentials are - admin:admin. - - If you specify a file it should only contain one line, with the credentials in the following - format: - : - - examples: - const auth_source = 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC'; - const auth_source = 'file;/path/to/file'; - */ - const auth = 0; - const auth_source = 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC'; - - /* - root_dir - set a custom root directory instead of the script location - - This option is highly experimental and should only be set if you definitely know what you do. - Settings this option may cause black holes or other unwanted things. Use with special care. - - default setting: - const root_dir = ""; - */ - const root_dir = ""; - const defaulttimezone = "Europe/Berlin"; // set default timezone - - /** - * Temp directory for zip files - * - * Default is the upload_tmp_dir which is set in the php.ini, but you may also set an different path - */ - const tmp_dir = ""; - - // development tools - const ajaxrequest = 1; // formular to perform an ajax request - - static function getConstants() { - $oClass = new ReflectionClass(__CLASS__); - return $oClass->getConstants(); - } -} - -/* ======================================================================= - * Improved File Manager - * --------------------- - * License: This project is provided under the terms of the MIT LICENSE - * http://github.com/misterunknown/ifm/blob/master/LICENSE - * ======================================================================= - * - * zip class - * - * this was adapted from http://php.net/manual/de/class.ziparchive.php#110719 -*/ - -class IFMZip { - private static function folderToZip($folder, &$zipFile, $exclusiveLength) { - $handle = opendir( $folder ); - while( false !== $f = readdir( $handle ) ) { - if( $f != '.' && $f != '..' ) { - $filePath = "$folder/$f"; - if( file_exists( $filePath ) && is_readable( $filePath ) ) { - // Remove prefix from file path before add to zip. - $localPath = substr($filePath, $exclusiveLength); - if( is_file( $filePath ) ) { - $zipFile->addFile( $filePath, $localPath ); - } elseif( is_dir( $filePath ) ) { - // Add sub-directory. - $zipFile->addEmptyDir( $localPath ); - self::folderToZip( $filePath, $zipFile, $exclusiveLength ); - } - } - } - } - closedir( $handle ); - } - - public static function create_zip( $src, $out, $root=false ) - { - $z = new ZipArchive(); - $z->open( $out, ZIPARCHIVE::CREATE); - if( $root ) { - self::folderToZip( realpath( $src ), $z, strlen( realpath( $src ) . '/' ) ); - } else { - $z->addEmptyDir( basename( $src ) ); - self::folderToZip( realpath( $src ), $z, strlen( dirname( $src ) . '/' ) ); - } - try { - if( ( $res = $z->close() ) !== true ) { - throw new Exception("Error while creating zip archive: ". $z->getStatusString()); - } - } catch ( Exception $e ) { - throw $e; - } - } - - public static function unzip_file( $file ) { - $zip = new ZipArchive(); - $res = $zip->open( $file ); - if( $res === true ) { - $zip->extractTo( './' ); - $zip->close(); - return true; - } else { - return false; - } - } -} - /* ======================================================================= * Improved File Manager * --------------------- @@ -165,17 +13,56 @@ class IFMZip { error_reporting( E_ALL ); ini_set( 'display_errors', 'OFF' ); -class IFM { - const VERSION = '2.3.1'; - public function __construct() { - session_start(); +class IFM { + const VERSION = '2.4.0'; + + private $defaultconfig = array( + // general config + "auth" => 0, + "auth_source" => 'inlineadmin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC', + "root_dir" => "", + "tmp_dir" => "", + "defaulttimezone" => "Europe/Berlin", + + // api controls + "ajaxrequest" => 1, + "chmod" => 1, + "copymove" => 1, + "createdir" => 1, + "createfile" => 1, + "edit" => 1, + "delete" => 1, + "download" => 1, + "extract" => 1, + "upload" => 1, + "remoteupload" => 1, + "rename" => 1, + "zipnload" => 1, + + // gui controls + "showlastmodified" => 0, + "showfilesize" => 1, + "showowner" => 1, + "showgroup" => 1, + "showpermissions" => 2, + "showhtdocs" => 1, + "showhiddenfiles" => 1, + "showpath" => 0, + ); + + private $config = array(); + public $mode = ""; + + public function __construct( $config=array() ) { + if( session_status() !== PHP_SESSION_ACTIVE ) + session_start(); + $this->config = array_merge( $this->defaultconfig, $config ); } - /* - this function contains the client-side application + /** + * This function contains the client-side application */ - public function getApplication() { print ' @@ -183,16 +70,35 @@ class IFM { IFM - improved file manager - - - - - - - - -
- - - - '; - if( IFMConfig::download == 1 ) print ''; - if( IFMConfig::showlastmodified == 1 ) print ''; - if( IFMConfig::showfilesize == 1 ) print ''; - if( IFMConfig::showpermissions > 0 ) print ''; - if( IFMConfig::showowner == 1 && function_exists( "posix_getpwuid" ) ) print ''; - if( IFMConfig::showgroup == 1 && function_exists( "posix_getgrgid" ) ) print ''; - if( in_array( 1, array( IFMConfig::edit, IFMConfig::rename, IFMConfig::delete, IFMConfig::zipnload, IFMConfig::extract ) ) ) print ''; - print ' - - - -
Filenamelast modifiedsize
-
-
- -
+ '; + } + + public function getJS() { + print ' - - + + - - '; } @@ -1334,40 +1570,55 @@ ifm.init(); $this->getFiles( $_REQUEST["dir"] ); else $this->getFiles( "" ); - } else { + } + elseif( $_REQUEST["api"] == "getConfig" ) { + $this->getConfig(); + } elseif( $_REQUEST["api"] == "getTemplates" ) { + echo json_encode( $this->getTemplates() ); + } else { if( isset( $_REQUEST["dir"] ) && $this->isPathValid( $_REQUEST["dir"] ) ) { switch( $_REQUEST["api"] ) { case "createDir": $this->createDir( $_REQUEST["dir"], $_REQUEST["dirname"] ); break; case "saveFile": $this->saveFile( $_REQUEST ); break; case "getContent": $this->getContent( $_REQUEST ); break; - case "deleteFile": $this->deleteFile( $_REQUEST ); break; - case "renameFile": $this->renameFile( $_REQUEST ); break; - case "downloadFile": $this->downloadFile( $_REQUEST ); break; - case "extractFile": $this->extractFile( $_REQUEST ); break; - case "uploadFile": $this->uploadFile( $_REQUEST ); break; + case "delete": $this->deleteFile( $_REQUEST ); break; + case "rename": $this->renameFile( $_REQUEST ); break; + case "download": $this->downloadFile( $_REQUEST ); break; + case "extract": $this->extractFile( $_REQUEST ); break; + case "upload": $this->uploadFile( $_REQUEST ); break; + case "copyMove": $this->copyMove( $_REQUEST ); break; case "changePermissions": $this->changePermissions( $_REQUEST ); break; case "zipnload": $this->zipnload( $_REQUEST); break; case "remoteUpload": $this->remoteUpload( $_REQUEST ); break; - case "deleteMultipleFiles": $this->deleteMultipleFiles( $_REQUEST ); break; - default: echo json_encode(array("status"=>"ERROR", "message"=>"No valid api action given")); break; + case "multidelete": $this->deleteMultipleFiles( $_REQUEST ); break; + case "getFolderTree": + echo json_encode( array_merge( array( 0 => array( "text" => "/ [root]", "nodes" => array(), "dataAttributes" => array( "path" => realpath( $this->config['root_dir'] ) ) ) ), $this->getFolderTreeRecursive( $this->config['root_dir'] ) ) ); + break; + default: + echo json_encode( array( "status" => "ERROR", "message" => "No valid api action given" ) ); + break; } } else { print json_encode(array("status"=>"ERROR", "message"=>"No valid working directory")); } } + exit( 0 ); } - public function run() { + public function run( $mode="standalone" ) { if ( $this->checkAuth() ) { // go to our root_dir - if( ! is_dir( realpath( IFMConfig::root_dir ) ) || ! is_readable( realpath( IFMConfig::root_dir ) ) ) + if( ! is_dir( realpath( $this->config['root_dir'] ) ) || ! is_readable( realpath( $this->config['root_dir'] ) ) ) die( "Cannot access root_dir."); else - chdir( realpath( IFMConfig::root_dir ) ); - if ( ! isset($_REQUEST['api']) ) { - $this->getApplication(); - } else { + chdir( realpath( $this->config['root_dir'] ) ); + $this->mode = $mode; + if ( isset( $_REQUEST['api'] ) || $mode == "api" ) { $this->handleRequest(); + } elseif( $mode == "standalone" ) { + $this->getApplication(); + } else { + $this->getInlineApplication(); } } } @@ -1376,61 +1627,53 @@ ifm.init(); api functions */ - private function getFiles($dir) { - // SECURITY FUNCTION (check that we don't operate on a higher level that the script itself) - $dir=$this->getValidDir($dir); - // now we change in our target directory - $this->chDirIfNecessary($dir); - // unset our file and directory arrays - unset($files); unset($dirs); $files = array(); $dirs = array(); - // so lets loop over our directory - if ($handle = opendir(".")) { - while (false !== ($result = readdir($handle))) { // this awesome statement is the correct way to loop over a directory :) - if( $result == basename( $_SERVER['SCRIPT_NAME'] ) && $this->getScriptRoot() == getcwd() ) { } // we don't want to see the script itself - elseif( ( $result == ".htaccess" || $result==".htpasswd" ) && IFMConfig::showhtdocs != 1 ) {} // check if we are granted to see .ht-docs - elseif( $result == "." ) {} // the folder itself will also be invisible - elseif( $result != ".." && substr( $result, 0, 1 ) == "." && IFMConfig::showhiddenfiles != 1 ) {} // eventually hide hidden files, if we should not see them - elseif( ! @is_readable( $result ) ) {} - else { // thats are the files we should see + private function getFiles( $dir ) { + $dir = $this->getValidDir( $dir ); + $this->chDirIfNecessary( $dir ); + + unset( $files ); unset( $dirs ); $files = array(); $dirs = array(); + + if( $handle = opendir( "." ) ) { + while( false !== ( $result = readdir( $handle ) ) ) { + if( $result == basename( $_SERVER['SCRIPT_NAME'] ) && $this->getScriptRoot() == getcwd() ) { } + elseif( ( $result == ".htaccess" || $result==".htpasswd" ) && $this->config['showhtdocs'] != 1 ) {} + elseif( $result == "." ) {} + elseif( $result != ".." && substr( $result, 0, 1 ) == "." && $this->config['showhiddenfiles'] != 1 ) {} + else { $item = array(); - $i = 0; $item["name"] = $result; - $i++; if( is_dir($result) ) { $item["type"] = "dir"; - } else { - $item["type"] = "file"; - } - if( is_dir( $result ) ) { if( $result == ".." ) $item["icon"] = "icon icon-up-open"; else $item["icon"] = "icon icon-folder-empty"; } else { + $item["type"] = "file"; $type = substr( strrchr( $result, "." ), 1 ); $item["icon"] = $this->getTypeIcon( $type ); } - if( IFMConfig::showlastmodified == 1 ) { $item["lastmodified"] = date( "d.m.Y, G:i e", filemtime( $result ) ); } - if( IFMConfig::showfilesize == 1 ) { - $item["filesize"] = filesize( $result ); - if( $item["filesize"] > 1073741824 ) $item["filesize"] = round( ( $item["filesize"]/1073741824 ), 2 ) . " GB"; - elseif($item["filesize"]>1048576)$item["filesize"] = round( ( $item["filesize"]/1048576 ), 2 ) . " MB"; - elseif($item["filesize"]>1024)$item["filesize"] = round( ( $item["filesize"]/1024 ), 2 ) . " KB"; - else $item["filesize"] = $item["filesize"] . " Byte"; + if( $this->config['showlastmodified'] == 1 ) { $item["lastmodified"] = date( "d.m.Y, G:i e", filemtime( $result ) ); } + if( $this->config['showfilesize'] == 1 ) { + $item["size"] = filesize( $result ); + if( $item["size"] > 1073741824 ) $item["size"] = round( ( $item["size"]/1073741824 ), 2 ) . " GB"; + elseif($item["size"]>1048576)$item["size"] = round( ( $item["size"]/1048576 ), 2 ) . " MB"; + elseif($item["size"]>1024)$item["size"] = round( ( $item["size"]/1024 ), 2 ) . " KB"; + else $item["size"] = $item["size"] . " Byte"; } - if( IFMConfig::showpermissions > 0 ) { - if( IFMConfig::showpermissions == 1 ) $item["fileperms"] = substr( decoct( fileperms( $result ) ), -3 ); - elseif( IFMConfig::showpermissions == 2 ) $item["fileperms"] = $this->filePermsDecode( fileperms( $result ) ); + if( $this->config['showpermissions'] > 0 ) { + if( $this->config['showpermissions'] == 1 ) $item["fileperms"] = substr( decoct( fileperms( $result ) ), -3 ); + elseif( $this->config['showpermissions'] == 2 ) $item["fileperms"] = $this->filePermsDecode( fileperms( $result ) ); if( $item["fileperms"] == "" ) $item["fileperms"] = " "; - $item["filepermmode"] = ( IFMConfig::showpermissions == 1 ) ? "short" : "long"; + $item["filepermmode"] = ( $this->config['showpermissions'] == 1 ) ? "short" : "long"; } - if( IFMConfig::showowner == 1 ) { + if( $this->config['showowner'] == 1 ) { if ( function_exists( "posix_getpwuid" ) && fileowner($result) !== false ) { $ownerarr = posix_getpwuid( fileowner( $result ) ); $item["owner"] = $ownerarr['name']; } else $item["owner"] = false; } - if( IFMConfig::showgroup == 1 ) { + if( $this->config['showgroup'] == 1 ) { if( function_exists( "posix_getgrgid" ) && filegroup( $result ) !== false ) { $grouparr = posix_getgrgid( filegroup( $result ) ); $item["group"] = $grouparr['name']; @@ -1447,8 +1690,71 @@ ifm.init(); echo json_encode( array_merge( $dirs, $files ) ); } + private function getConfig() { + $ret = $this->config; + $ret['inline'] = ( $this->mode == "inline" ) ? true : false; + $ret['isDocroot'] = ( realpath( $this->config['root_dir'] ) == dirname( __FILE__ ) ) ? "true" : "false"; + echo json_encode( $ret ); + } + + private function getFolderTreeRecursive( $start_dir ) { + $ret = array(); + $start_dir = realpath( $start_dir ); + if( $handle = opendir( $start_dir ) ) { + while (false !== ( $result = readdir( $handle ) ) ) { + if( is_dir( $this->pathCombine( $start_dir, $result ) ) && $result != "." && $result != ".." ) { + array_push( $ret, array( "text" => $result, "dataAttributes" => array( "path" => $this->pathCombine( $start_dir, $result ) ), "nodes" => $this->getFolderTreeRecursive( $this->pathCombine( $start_dir, $result ) ) ) ); + } + } + } + sort( $ret ); + return $ret; + } + + private function copyMove( $d ) { + if( $this->config['copymove'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to copy or move files." ) ); + exit( 1 ); + } + $this->chDirIfNecessary( $d['dir'] ); + if( ! isset( $d['destination'] ) || ! $this->isPathValid( realpath( $d['destination'] ) ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "No valid destination directory given." ) ); + exit( 1 ); + } + if( ! file_exists( $d['filename'] ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "No valid filename given." ) ); + exit( 1 ); + } + if( $d['action'] == "copy" ) { + if( $this->copyr( $d['filename'], $d['destination'] ) ) { + echo json_encode( array( "status" => "OK", "message" => "File(s) were successfully copied." ) ); + exit( 0 ); + } else { + $err = error_get_last(); + echo json_encode( array( "status" => "ERROR", "message" => $err['message'] ) ); + exit( 1 ); + } + } elseif( $d['action'] == "move" ) { + if( rename( $d['filename'], $this->pathCombine( $d['destination'], basename( $d['filename'] ) ) ) ) { + echo json_encode( array( "status" => "OK", "message" => "File(s) were successfully moved." ) ); + exit( 0 ); + } else { + $err = error_get_last(); + echo json_encode( array( "status" => "ERROR", "message" => $err['message'] ) ); + exit( 1 ); + } + } else { + echo json_encode( array( "status" => "ERROR", "message" => "No valid action given." ) ); + exit( 1 ); + } + } + // creates a directory private function createDir($w, $dn) { + if( $this->config['createDir'] != 1 ) { + echo json_encode( array( "status" => "ERROR", "message" => "No permission to create directories.") ); + exit( 1 ); + } if( $dn == "" ) { echo json_encode( array( "status" => "ERROR", "message" => "No valid directory name") ); } elseif( strpos( $dn, '/' ) !== false ) echo json_encode( array( "status" => "ERROR", "message" => "No slashes allowed in directory names" ) ); @@ -1463,14 +1769,18 @@ ifm.init(); } // save a file - private function saveFile(array $d) { + private function saveFile( $d ) { + if( ( file_exists( $this->pathCombine( $d['dir'], $d['filename'] ) ) && $this->config['edit'] != 1 ) || ( ! file_exists( $this->pathCombine( $d['dir'], $d['filename'] ) ) && $this->config['createfile'] != 1 ) ) { + echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit/create this file." ) ); + exit( 1 ); + } if( isset( $d['filename'] ) && $d['filename'] != "" ) { // if you are not allowed to see .ht-docs you can't save one - if( IFMConfig::showhtdocs != 1 && substr( $d['filename'], 0, 3 ) == ".ht" ) { + if( $this->config['showhtdocs'] != 1 && substr( $d['filename'], 0, 3 ) == ".ht" ) { echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit or create htdocs" ) ); } // same with hidden files - elseif( IFMConfig::showhiddenfiles != 1 && substr( $d['filename'], 0, 1 ) == "." ) { + elseif( $this->config['showhiddenfiles'] != 1 && substr( $d['filename'], 0, 1 ) == "." ) { echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit or create hidden files" ) ); } elseif(strpos($d['filename'],'/')!==false) { @@ -1497,7 +1807,7 @@ ifm.init(); // gets the content of a file // notice: if the content is not JSON encodable it returns an error private function getContent( array $d ) { - if( IFMConfig::edit != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to edit files" ) ); + if( $this->config['edit'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "You are not allowed to edit files." ) ); else { $this->chDirIfNecessary( $d['dir'] ); if( file_exists( $d['filename'] ) ) { @@ -1510,7 +1820,7 @@ ifm.init(); // deletes a file or a directory (recursive!) private function deleteFile( array $d ) { - if( IFMConfig::delete != 1 ) { + if( $this->config['delete'] != 1 ) { echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete files" ) ); } else { @@ -1535,7 +1845,7 @@ ifm.init(); // deletes a bunch of files or directories private function deleteMultipleFiles( array $d ) { - if( IFMConfig::delete != 1 || IFMConfig::multiselect != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete multiple files" ) ); + if( $this->config['delete'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to delete files" ) ); else { $this->chDirIfNecessary( $d['dir'] ); $err = array(); $errFLAG = -1; // -1 -> no files deleted; 0 -> at least some files deleted; 1 -> all files deleted @@ -1568,15 +1878,15 @@ ifm.init(); // renames a file private function renameFile( array $d ) { - if( IFMConfig::rename != 1 ) { + if( $this->config['rename'] != 1 ) { echo json_encode( array( "status" => "ERROR", "message" => "No permission to rename files" ) ); } else { $this->chDirIfNecessary( $d['dir'] ); if( strpos( $d['newname'], '/' ) !== false ) echo json_encode( array( "status" => "ERROR", "message" => "No slashes allowed in filenames" ) ); - elseif( IFMConfig::showhtdocs != 1 && ( substr( $d['newname'], 0, 3) == ".ht" || substr( $d['filename'], 0, 3 ) == ".ht" ) ) + elseif( $this->config['showhtdocs'] != 1 && ( substr( $d['newname'], 0, 3) == ".ht" || substr( $d['filename'], 0, 3 ) == ".ht" ) ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to rename this file" ) ); - elseif( IFMConfig::showhiddenfiles != 1 && ( substr( $d['newname'], 0, 1) == "." || substr( $d['filename'], 0, 1 ) == "." ) ) + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $d['newname'], 0, 1) == "." || substr( $d['filename'], 0, 1 ) == "." ) ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to rename file" ) ); else { if( @rename( $d['filename'], $d['newname'] ) ) @@ -1589,73 +1899,58 @@ ifm.init(); // provides a file for downloading private function downloadFile( array $d ) { - if( IFMConfig::download != 1 ) + if( $this->config['download'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to download files" ) ); - elseif( IFMConfig::showhtdocs != 1 && ( substr( $d['filename'], 0, 3 ) == ".ht" || substr( $d['filename'],0,3 ) == ".ht" ) ) + elseif( $this->config['showhtdocs'] != 1 && ( substr( $d['filename'], 0, 3 ) == ".ht" || substr( $d['filename'],0,3 ) == ".ht" ) ) echo json_encode( array( "status" => "ERROR", "message"=>"Not allowed to download htdocs" ) ); - elseif( IFMConfig::showhiddenfiles != 1 && ( substr( $d['filename'], 0, 1 ) == "." || substr( $d['filename'],0,1 ) == "." ) ) + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $d['filename'], 0, 1 ) == "." || substr( $d['filename'],0,1 ) == "." ) ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to download hidden files" ) ); else { $this->chDirIfNecessary( $d["dir"] ); - $this->file_download( $d['filename'] ); + $this->fileDownload( $d['filename'] ); } } // extracts a zip-archive private function extractFile( array $d ) { - if( IFMConfig::extract != 1 ) + if( $this->config['extract'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to extract files" ) ); else { $this->chDirIfNecessary( $d['dir'] ); - if( ! file_exists( $d['filename'] ) || substr( $d['filename'],-4 ) != ".zip" ) + if( ! file_exists( $d['filename'] ) || substr( $d['filename'],-4 ) != ".zip" ) { echo json_encode( array( "status" => "ERROR","message" => "No valid zip file found" ) ); - else { - if( ! isset( $d['targetdir'] ) ) - $d['targetdir'] = ""; - if( strpos( $d['targetdir'], "/" ) !== false ) - echo json_encode( array( "status" => "ERROR","message" => "Target directory must not contain slashes" ) ); - else { - switch( $d['targetdir'] ){ - case "": - if( $this->unzip( $_POST["filename"] ) ) - echo json_encode( array( "status" => "OK","message" => "File successfully extracted." ) ); - else - echo json_encode( array( "status" => "ERROR","message" => "File could not be extracted" ) ); - break; - default: - if( ! mkdir( $d['targetdir'] ) ) - echo json_encode( array( "status" => "ERROR","message" => "Could not create target directory" ) ); - else { - chdir( $d['targetdir'] ); - if( ! $this->unzip( "../" . $d["filename"] ) ) { - chdir( ".." ); - rmdir( $d['targetdir'] ); - echo json_encode( array( "status" => "ERROR","message" => "Could not extract file" ) ); - } - else { - chdir( ".." ); - echo json_encode( array( "status" => "OK","message" => "File successfully extracted" ) ); - } - } - break; - } - } + exit( 1 ); + } + if( ! isset( $d['targetdir'] ) || trim( $d['targetdir'] ) == "" ) + $d['targetdir'] = "./"; + if( ! $this->isPathValid( $d['targetdir'] ) ) { + echo json_encode( array( "status" => "ERROR","message" => "Target directory is not valid." ) ); + exit( 1 ); + } + if( ! is_dir( $d['targetdir'] ) && ! mkdir( $d['targetdir'], 0777, true ) ) { + echo json_encode( array( "status" => "ERROR","message" => "Could not create target directory." ) ); + exit( 1 ); + } + if( ! IFMZip::extract( $d['filename'], $d['targetdir'] ) ) { + echo json_encode( array( "status" => "ERROR","message" => "File could not be extracted" ) ); + } else { + echo json_encode( array( "status" => "OK","message" => "File successfully extracted." ) ); } } } // uploads a file private function uploadFile( array $d ) { - if( IFMConfig::upload != 1 ) + if( $this->config['upload'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to upload files" ) ); elseif( !isset( $_FILES['file'] ) ) echo json_encode( array( "file" => $_FILE,"files" => $_FILES ) ); else { $this->chDirIfNecessary( $d['dir'] ); $newfilename = ( isset( $d["newfilename"] ) && $d["newfilename"]!="" ) ? $d["newfilename"] : $_FILES['file']['name']; - if( IFMConfig::showhtdocs != 1 && ( substr( $newfilename, 0, 3 ) == ".ht" || substr( $newfilename,0,3 ) == ".ht" ) ) + if( $this->config['showhtdocs'] != 1 && ( substr( $newfilename, 0, 3 ) == ".ht" || substr( $newfilename,0,3 ) == ".ht" ) ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to upload htdoc file" ) ); - elseif( IFMConfig::showhiddenfiles != 1 && ( substr( $newfilename, 0, 1 ) == "." || substr( $newfilename,0,1 ) == "." ) ) + elseif( $this->config['showhiddenfiles'] != 1 && ( substr( $newfilename, 0, 1 ) == "." || substr( $newfilename,0,1 ) == "." ) ) echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to upload hidden file" ) ); else { if( $_FILES['file']['tmp_name'] ) { @@ -1677,7 +1972,7 @@ ifm.init(); // change permissions of a file private function changePermissions( array $d ) { - if( IFMConfig::chmod != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No rights to change permissions" ) ); + if( $this->config['chmod'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No rights to change permissions" ) ); elseif( ! isset( $d["chmod"] )||$d['chmod']=="" ) echo json_encode( array( "status" => "ERROR", "message" => "Could not identify new permissions" ) ); elseif( ! $this->isPathValid( $this->pathCombine( $d['dir'],$d['filename'] ) ) ) { echo json_encode( array( "status" => "ERROR", "message" => "Not allowed to change the permissions" ) ); } else { @@ -1716,7 +2011,7 @@ ifm.init(); // zips a directory and provides it for downloading // it creates a temporary zip file in the current directory, so it has to be as much space free as the file size is private function zipnload( array $d ) { - if( IFMConfig::zipnload != 1 ) + if( $this->config['zipnload'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to download directories" ) ); else { $this->chDirIfNecessary( $d['dir'] ); @@ -1726,16 +2021,16 @@ ifm.init(); echo json_encode( array( "status" => "ERROR", "message" => "Filename not allowed" ) ); else { unset( $zip ); - $dfile = $this->pathCombine( IFMConfig::tmp_dir, uniqid( "ifm-tmp-" ) . ".zip" ); // temporary filename + $dfile = $this->pathCombine( $this->config['tmp_dir'], uniqid( "ifm-tmp-" ) . ".zip" ); // temporary filename try { - IFMZip::create_zip( realpath( $d['filename'] ), $dfile, ( $d['filename'] == "." ) ); + IFMZip::create( realpath( $d['filename'] ), $dfile, ( $d['filename'] == "." ) ); if( $d['filename'] == "." ) { if( getcwd() == $this->getScriptRoot() ) $d['filename'] = "root"; else $d['filename'] = basename( getcwd() ); } - $this->file_download( $dfile, $d['filename'] . ".zip" ); + $this->fileDownload( $dfile, $d['filename'] . ".zip" ); } catch ( Exception $e ) { echo "An error occured: " . $e->getMessage(); } finally { @@ -1747,7 +2042,7 @@ ifm.init(); // uploads a file from an other server using the curl extention private function remoteUpload( array $d ) { - if( IFMConfig::remoteupload != 1 ) + if( $this->config['remoteupload'] != 1 ) echo json_encode( array( "status" => "ERROR", "message" => "No permission to remote upload files" ) ); elseif( !isset( $d['method'] ) || !in_array( $d['method'], array( "curl", "file" ) ) ) echo json_encode( array( "status" => "error", "message" => "No valid method given. Valid methods: ['curl', 'file']" ) ); @@ -1800,7 +2095,7 @@ ifm.init(); */ public function checkAuth() { - if( IFMConfig::auth == 1 && ( ! isset( $_SESSION['auth'] ) || $_SESSION['auth'] !== true ) ) { + if( $this->config['auth'] == 1 && ( ! isset( $_SESSION['auth'] ) || $_SESSION['auth'] !== true ) ) { $login_failed = false; if( isset( $_POST["user"] ) && isset( $_POST["pass"] ) ) { if( $this->checkCredentials( $_POST["user"], $_POST["pass"] ) ) { @@ -1830,21 +2125,46 @@ ifm.init(); } } - private function checkCredentials($user, $pass) { - list($src, $srcopt) = explode(";", IFMConfig::auth_source, 2); - switch($src) { + private function checkCredentials( $user, $pass ) { + list( $src, $srcopt ) = explode( ";", $this->config['auth_source'], 2 ); + switch( $src ) { case "inline": - list($uname, $hash) = explode(":", $srcopt); + list( $uname, $hash ) = explode( ":", $srcopt ); + return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; break; case "file": - if(@file_exists($srcopt) && @is_readable($srcopt)) { - list($uname, $hash) = explode(":", fgets(fopen($srcopt, 'r'))); + if( @file_exists( $srcopt ) && @is_readable( $srcopt ) ) { + list( $uname, $hash ) = explode( ":", fgets( fopen( $srcopt, 'r' ) ) ); + return password_verify( $pass, trim( $hash ) ) ? ( $uname == $user ) : false; } else { return false; } break; + case "ldap": + $authenticated = false; + list( $ldap_server, $rootdn ) = explode( ";", $srcopt ); + $u = "uid=" . $user . "," . $rootdn; + if( ! $ds = ldap_connect( $ldap_server ) ) { + trigger_error( "Could not reach the ldap server.", E_USER_ERROR ); + return false; + } + ldap_set_option( $ds, LDAP_OPT_PROTOCOL_VERSION, 3 ); + if( $ds ) { + $ldbind = @ldap_bind( $ds, $u, $pass ); + if( $ldbind ) { + $authenticated = true; + } else { + trigger_error( ldap_error( $ds ), E_USER_ERROR ); + $authenticated = false; + } + ldap_unbind( $ds ); + } else { + $authenticated = false; + } + return $authenticated; + break; } - return password_verify($pass, trim($hash))?($uname == $user):false; + return false; } private function loginForm($loginFailed=false) { @@ -1891,7 +2211,7 @@ ifm.init(); return ""; } else { $rpDir = realpath( $dir ); - $rpConfig = realpath( IFMConfig::root_dir ); + $rpConfig = realpath( $this->config['root_dir'] ); if( $rpConfig == "/" ) return $rpDir; elseif( $rpDir == $rpConfig ) @@ -1902,15 +2222,25 @@ ifm.init(); } private function isPathValid( $dir ) { - $rpDir = realpath( $dir ); - $rpConfig = realpath( IFMConfig::root_dir ); + /** + * This function is also used to check non-existent paths, but the PHP realpath function returns false for + * nonexistent paths. Hence we need to check the path manually in the following lines. + */ + $tmp_d = $dir; + $tmp_missing_parts = array(); + while( realpath( $tmp_d ) === false ) { + $tmp_i = pathinfo( $tmp_d ); + array_push( $tmp_missing_parts, $tmp_i['filename'] ); + $tmp_d = dirname( $tmp_d ); + } + $rpDir = $this->pathCombine( realpath( $tmp_d ), implode( "/", array_reverse( $tmp_missing_parts ) ) ); + $rpConfig = ( $this->config['root_dir'] == "" ) ? realpath( dirname( __FILE__ ) ) : realpath( $this->config['root_dir'] ); if( ! is_string( $rpDir ) || ! is_string( $rpConfig ) ) // can happen if open_basedir is in effect return false; elseif( $rpDir == $rpConfig ) return true; - elseif( 0 === strpos( $rpDir, $rpConfig ) ) { + elseif( 0 === strpos( $rpDir, $rpConfig ) ) return true; - } else return false; } @@ -1967,6 +2297,53 @@ ifm.init(); return 0; } + /** + * Copy a file, or recursively copy a folder and its contents + * + * @author Aidan Lister + * @version 1.0.1 + * @link http://aidanlister.com/2004/04/recursively-copying-directories-in-php/ + * @param string $source Source path + * @param string $dest Destination path + * @return bool Returns TRUE on success, FALSE on failure + */ + private function copyr( $source, $dest ) + { + // Check for symlinks + if (is_link($source)) { + return symlink(readlink($source), $dest); + } + + // Simple copy for a file + if (is_file($source)) { + $dest = ( is_dir( $dest ) ) ? $this->pathCombine( $dest, basename( $source ) ) : $dest; + return copy($source, $dest); + } else { + $dest = $this->pathCombine( $dest, basename( $source ) ); + } + + // Make destination directory + if (!is_dir($dest)) { + mkdir($dest); + } + + // Loop through the folder + $dir = dir($source); + while (false !== $entry = $dir->read()) { + // Skip pointers + if ($entry == '.' || $entry == '..') { + continue; + } + + // Deep copy directories + $this->copyr("$source/$entry", "$dest/$entry"); + } + + // Clean up + $dir->close(); + return true; + } + // combines two parts to a valid path private function pathCombine( $a, $b ) { if( trim( $a ) == "" && trim( $b ) == "" ) @@ -1979,9 +2356,9 @@ ifm.init(); // check if filename is allowed private function allowedFileName( $f ) { - if( IFMConfig::showhtdocs != 1 && substr( $f, 0, 3 ) == ".ht" ) + if( $this->config['showhtdocs'] != 1 && substr( $f, 0, 3 ) == ".ht" ) return false; - elseif( IFMConfig::showhiddenfiles != 1 && substr( $f, 0, 1 ) == "." ) + elseif( $this->config['showhiddenfiles'] != 1 && substr( $f, 0, 1 ) == "." ) return false; elseif( ! $this->isPathValid( $f ) ) return false; @@ -1994,19 +2371,6 @@ ifm.init(); return ( strtolower( $a['name'] ) < strtolower( $b['name'] ) ) ? -1 : 1; } - // unzip an archive - private function unzip( $file ) { - $zip = new ZipArchive; - $res = $zip->open( $file ); - if( $res === true ) { - $zip->extractTo( './' ); - $zip->close(); - return true; - } else { - return false; - } - } - // is cURL extention avaliable? private function checkCurl() { if( !function_exists( "curl_init" ) || @@ -2016,7 +2380,7 @@ ifm.init(); else return true; } - private function file_download( $file, $name="" ) { + private function fileDownload( $file, $name="" ) { header( 'Content-Description: File Transfer' ); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . ( trim( $name ) == "" ? basename( $file ) : $name ) . '"' ); @@ -2034,12 +2398,437 @@ ifm.init(); fclose($stdout_stream); } - ///helper + private function getTemplates() { + $templates = array(); + $templates['app'] = <<<'f00bar' + +
+ + + + + {{#config.download}} + + {{/config.download}} + {{#config.showlastmodified}} + + {{/config.showlastmodified}} + {{#config.showfilesize}} + + {{/config.showfilesize}} + {{#config.showpermissions}} + + {{/config.showpermissions}} + {{#config.showowner}} + + {{/config.showowner}} + {{#config.showgroup}} + + {{/config.showgroup}} + + + + + +
Filenamelast modifiedsize
+
+
+ +
+ +f00bar; + $templates['filetable'] = <<<'f00bar' + +{{#items}} + + + + + {{linkname}} + + + {{#config.download}} + +
+ + + +
+ + + + + {{/config.download}} + {{#config.showlastmodified}} + {{lastmodified}} + {{/config.showlastmodified}} + {{#config.showfilesize}} + {{size}} + {{/config.showfilesize}} + {{#config.showpermissions}} + + + + {{/config.showpermissions}} + {{#config.showowner}} + + {{owner}} + + {{/config.showowner}} + {{#config.showgroup}} + + {{group}} + + {{/config.showgroup}} + + {{#button}} + + + + {{/button}} + + +{{/items}} + + +f00bar; + $templates['file'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['createdir'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['ajaxrequest'] = <<<'f00bar' +
+
+ +f00bar; + $templates['copymove'] = <<<'f00bar' +
+
+ + +
+
+ +f00bar; + $templates['createdir'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['deletefile'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['extractfile'] = <<<'f00bar' +
+
+ + +
+
+ +f00bar; + $templates['file'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['multidelete'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['remoteupload'] = <<<'f00bar' +
+ + +
+ +f00bar; + $templates['renamefile'] = <<<'f00bar' + + +f00bar; + $templates['uploadfile'] = <<<'f00bar' +
+ + +
+ +f00bar; + + return $templates; + } } -/* - start program - */ +/* ======================================================================= + * Improved File Manager + * --------------------- + * License: This project is provided under the terms of the MIT LICENSE + * http://github.com/misterunknown/ifm/blob/master/LICENSE + * ======================================================================= + * + * zip class + * + * this was adapted from http://php.net/manual/de/class.ziparchive.php#110719 +*/ -$ifm = new IFM(); -$ifm->run(); +class IFMZip { + /** + * Add a folder to the zip file + */ + private static function folderToZip($folder, &$zipFile, $exclusiveLength) { + $handle = opendir( $folder ); + while( false !== $f = readdir( $handle ) ) { + if( $f != '.' && $f != '..' ) { + $filePath = "$folder/$f"; + if( file_exists( $filePath ) && is_readable( $filePath ) ) { + // Remove prefix from file path before add to zip. + $localPath = substr($filePath, $exclusiveLength); + if( is_file( $filePath ) ) { + $zipFile->addFile( $filePath, $localPath ); + } elseif( is_dir( $filePath ) ) { + // Add sub-directory. + $zipFile->addEmptyDir( $localPath ); + self::folderToZip( $filePath, $zipFile, $exclusiveLength ); + } + } + } + } + closedir( $handle ); + } + + /** + * Create a zip file + */ + public static function create( $src, $out, $root=false ) + { + $z = new ZipArchive(); + $z->open( $out, ZIPARCHIVE::CREATE); + if( $root ) { + self::folderToZip( realpath( $src ), $z, strlen( realpath( $src ) . '/' ) ); + } else { + $z->addEmptyDir( basename( $src ) ); + self::folderToZip( realpath( $src ), $z, strlen( dirname( $src ) . '/' ) ); + } + try { + if( ( $res = $z->close() ) !== true ) { + throw new Exception("Error while creating zip archive: ". $z->getStatusString()); + } + } catch ( Exception $e ) { + throw $e; + } + } + + /** + * Unzip a zip file + */ + public static function extract( $file, $destination="./" ) { + $zip = new ZipArchive; + $res = $zip->open( $file ); + if( $res === true ) { + $zip->extractTo( $destination ); + $zip->close(); + return true; + } else { + return false; + } + } +} +/** * start IFM */$ifm = new IFM();$ifm->run(); \ No newline at end of file diff --git a/src/config.php b/src/config.php deleted file mode 100644 index 8c0ed61..0000000 --- a/src/config.php +++ /dev/null @@ -1,87 +0,0 @@ - not; 1 -> octal, 2 -> human readable - const showhtdocs = 1; // show .htaccess and .htpasswd - const showhiddenfiles = 1; // show files beginning with a dot (e.g. ".bashrc") - const showpath = 0; // show absolute path - - /* - authentication - - This provides a super simple authentication functionality. At the moment only one user can be - configured. The credential information can be either set inline or read from a file. The - password has to be a hash generated by PHPs password_hash function. The default credentials are - admin:admin. - - If you specify a file it should only contain one line, with the credentials in the following - format: - : - - examples: - const auth_source = 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC'; - const auth_source = 'file;/path/to/file'; - */ - const auth = 0; - const auth_source = 'inline;admin:$2y$10$0Bnm5L4wKFHRxJgNq.oZv.v7yXhkJZQvinJYR2p6X1zPvzyDRUVRC'; - - /* - root_dir - set a custom root directory instead of the script location - - This option is highly experimental and should only be set if you definitely know what you do. - Settings this option may cause black holes or other unwanted things. Use with special care. - - default setting: - const root_dir = ""; - */ - const root_dir = ""; - const defaulttimezone = "Europe/Berlin"; // set default timezone - - /** - * Temp directory for zip files - * - * Default is the upload_tmp_dir which is set in the php.ini, but you may also set an different path - */ - const tmp_dir = ""; - - // development tools - const ajaxrequest = 1; // formular to perform an ajax request - - static function getConstants() { - $oClass = new ReflectionClass(__CLASS__); - return $oClass->getConstants(); - } -} diff --git a/src/ifm.js b/src/ifm.js index eaab79d..bc7b632 100644 --- a/src/ifm.js +++ b/src/ifm.js @@ -1,16 +1,26 @@ -// IFM - js app - -function IFM() { +/** + * IFM constructor + * + * @param object params - object with some configuration values, currently you only can set the api url + */ +function IFM( params ) { var self = this; // reference to ourself, because "this" does not work within callbacks - this.IFM_SCFN = ""; - this.config = jQuery.parseJSON(''); // serialize the PHP config array, so we can use it in JS too - this.isDocroot = ; + // set the backend for the application + params = params || {}; + self.api = params.api || window.location.pathname; + this.editor = null; // global ace editor this.fileChanged = false; // flag for check if file was changed already this.currentDir = ""; // this is the global variable for the current directory; it is used for AJAX requests + this.rootElement = ""; - // modal functions + /** + * Shows a bootstrap modal + * + * @param string content - content of the modal + * @param object options - options for the modal + */ this.showModal = function( content, options = {} ) { var modal = $( document.createElement( 'div' ) ) .addClass( "modal fade" ) @@ -36,116 +46,176 @@ function IFM() { modal.modal('show'); }; + /** + * Hides a bootstrap modal + */ this.hideModal = function() { $('#ifmmodal').modal('hide'); }; + /** + * Reloads the file table + */ this.refreshFileTable = function () { - var id=self.generateGuid(); - self.task_add("Refresh", id); + var id = self.generateGuid(); + self.task_add( "Refresh", id ); $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", - data: "api=getFiles&dir=" + self.currentDir, + data: { + api: "getFiles", + dir: self.currentDir + }, dataType: "json", success: self.rebuildFileTable, - error: function(response) { ifm.showMessage("General error occured: No or broken response", "e"); }, + error: function( response ) { self.showMessage( "General error occured: No or broken response", "e" ); }, complete: function() { self.task_done( id ); } }); }; + /** + * Rebuilds the file table with fetched items + * + * @param object data - object with items + */ this.rebuildFileTable = function( data ) { - var newTBody = $(document.createElement('tbody')); - for( var i=0; i < data.length; i++ ) { - var newRow = '"'; - } else { - newRow += ' onclick="$(\'#d_'+guid+'\').submit();"'; - } - } else { - newRow += ' onclick="ifm.changeDirectory(\''+data[i].name+'\')"'; + data.forEach( function( item ) { + item.guid = self.generateGuid(); + item.linkname = ( item.name == ".." ) ? "[ up ]" : item.name; + item.download = {}; + item.download.name = ( item.name == ".." ) ? "." : item.name; + item.download.allowed = self.config.download; + item.download.currentDir = self.currentDir; + if( ! self.config.chmod ) + item.readonly = "readonly"; + if( self.config.edit || self.config.rename || self.config.delete || self.config.extract || self.config.copymove ) { + item.ftbuttons = true; + item.button = []; } - newRow += '> ' + ( data[i].name == '..' ? '[ up ]' : data[i].name ) + ''; - if( ( data[i].type != "dir" && self.config.download == 1 ) || ( data[i].type == "dir" && self.config.zipnload == 1 ) ) { - newRow += '
'; - newRow += ''; - newRow += ''; - newRow += ''; - newRow += '
'; + if( item.type == "dir" ) { + item.download.action = "zipnload"; + item.download.icon = "icon icon-download-cloud"; + item.rowclasses = "isDir"; } else { - newRow += ''; + item.download.action = "download"; + item.download.icon = "icon icon-download"; + if( item.icon.indexOf( 'file-image' ) !== -1 && self.config.isDocroot ) + item.tooltip = 'data-toggle="tooltip" title=""'; + if( item.name.toLowerCase().substr(-4) == ".zip" ) + item.eaction = "extract"; + else + item.eaction = "edit"; + if( self.config.edit && item.name.toLowerCase().substr(-4) != ".zip" ) + item.button.push({ + action: "edit", + icon: "icon icon-pencil", + title: "edit" + }); + if( self.config.extract && item.name.toLowerCase().substr(-4) == ".zip" ) + item.button.push({ + action: "extract", + icon: "icon icon-archive", + title: "extract" + }); } - // last-modified - if( self.config.showlastmodified > 0 ) - newRow += '' + data[i].lastmodified + ''; - // size - if( self.config.showfilesize > 0 ) - newRow += '' + data[i].filesize + ''; - // permissions - if( self.config.showpermissions > 0 ) - newRow += ''; - // owner - if( self.config.showowner > 0 ) - newRow += ''+data[i].owner+''; - // group - if( self.config.showgroup > 0 ) - newRow += '' + data[i].group + ''; - // actions - if( self.inArray( 1, [self.config.edit, self.config.rename, self.config.delete, self.config.extract] ) ) { - newRow += ''; - if( data[i].name.toLowerCase().substr(-4) == ".zip" && self.config.extract == 1 ) { - newRow += ''; - } else if( self.config.edit == 1 && data[i].type != "dir" ) { - newRow += ''; - } - if( data[i].name != ".." && data[i].name != "." ) { - if( self.config.rename == 1 ) - newRow += ''; - if( self.config.delete == 1 ) - newRow += ''; - } - newRow += ''; - } else { - newRow += ''; + if( ! self.inArray( item.name, [".", ".."] ) ) { + if( self.config.copymove ) + item.button.push({ + action: "copymove", + icon: "icon icon-folder-open-empty", + title: "copy/move" + }); + if( self.config.rename ) + item.button.push({ + action: "rename", + icon: "icon icon-terminal", + title: "rename" + }); + if( self.config.delete ) + item.button.push({ + action: "delete", + icon: "icon icon-trash", + title: "delete" + }); } - newTBody.append( newRow ); - } - $("#filetable tbody").remove(); - $("#filetable").append( newTBody ); - if( self.config.multiselect == 1 ) { - $('.clickable-row').click(function(event) { - if( event.ctrlKey ) { - $(this).toggleClass( 'selectedItem' ); - } - }); - } - $('a[data-toggle="tooltip"]').tooltip({ + }); + var newTBody = Mustache.render( self.templates.filetable, { items: data, config: self.config } ); + $( "#filetable tbody" ).remove(); + $( "#filetable" ).append( $(newTBody) ); + $( '.clickable-row' ).click( function( event ) { + if( event.ctrlKey ) { + $( this ).toggleClass( 'selectedItem' ); + } + }); + $( 'a[data-toggle="tooltip"]' ).tooltip({ animated: 'fade', placement: 'right', html: true }); + $( 'a.ifmitem' ).each( function() { + if( $(this).data( "type" ) == "dir" ) { + $(this).on( 'click', function( e ) { + e.stopPropagation(); + self.changeDirectory( $(this).parent().parent().data( 'filename' ) ); + return false; + }); + } else { + if( self.config.isDocroot ) + $(this).attr( "href", self.pathCombine( self.currentDir, $(this).parent().parent().data( 'filename' ) ) ); + else + $(this).on( 'click', function() { + $( '#d_' + this.id ).submit(); + return false; + }); + } + }); + $( 'a[name="start_download"]' ).on( 'click', function(e) { + e.stopPropagation(); + $( '#d_' + $(this).data( 'guid' ) ).submit(); + return false; + }); + $( 'input[name="newpermissions"]' ).on( 'keypress', function( e ) { + if( e.key == "Enter" ) { + e.stopPropagation(); + self.changePermissions( $( this ).data( 'filename' ), $( this ).val() ); + return false; + } + }); + $( 'a[name^="do-"]' ).on( 'click', function() { + var action = this.name.substr( this.name.indexOf( '-' ) + 1 ); + switch( action ) { + case "rename": + self.showRenameFileDialog( $(this).data( 'name' ) ); + break; + case "extract": + self.showExtractFileDialog( $(this).data( 'name' ) ); + break; + case "edit": + self.editFile( $(this).data( 'name' ) ); + break; + case "delete": + self.showDeleteFileDialog( $(this).data( 'name' ) ); + break; + case "copymove": + self.showCopyMoveDialog( $(this).data( 'name' ) ); + break; + } + }); + }; + /** + * Changes the current directory + * + * @param string newdir - target directory + * @param object options - options for changing the directory + */ this.changeDirectory = function( newdir, options={} ) { - console.log( "changeDirectory, newdir="+newdir ); config = { absolute: false, pushState: true }; jQuery.extend( config, options ); if( ! config.absolute ) newdir = self.pathCombine( self.currentDir, newdir ); $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", data: ({ api: "getRealpath", @@ -162,22 +232,29 @@ function IFM() { }); }; - this.showFileForm = function () { + /** + * Shows a file, either a new file or an existing + */ + this.showFileDialog = function () { var filename = arguments.length > 0 ? arguments[0] : "newfile.txt"; var content = arguments.length > 1 ? arguments[1] : ""; - var overlay = '
' + - '' + - '
'; - self.showModal( overlay, { large: true } ); - $('#editoroptions').popover({ + self.showModal( Mustache.render( self.templates.file, { filename: filename } ), { large: true } ); + var form = $('#formFile'); + form.find('input[name="filename"]').on( 'keypress', self.preventEnter ); + form.find('#buttonSave').on( 'click', function() { + self.saveFile( form.find('input[name=filename]').val(), self.editor.getValue() ); + self.hideModal(); + return false; + }); + form.find('#buttonSaveNotClose').on( 'click', function() { + self.saveFile( form.find('input[name=filename]').val(), self.editor.getValue() ); + return false; + }); + form.find('#buttonClose').on( 'click', function() { + self.hideModal(); + return false; + }); + form.find('#editoroptions').popover({ html: true, title: function() { return $('#editoroptions-head').html(); }, content: function() { @@ -196,7 +273,7 @@ function IFM() { return content; } }); - $('#ifmmodal').on( 'remove', function () { self.editor = null; self.fileChanged = false; }); + form.on( 'remove', function () { self.editor = null; self.fileChanged = false; }); // Start ACE self.editor = ace.edit("content"); self.editor.$blockScrolling = 'Infinity'; @@ -209,60 +286,18 @@ function IFM() { }); }; - this.createDirForm = function() { - self.showModal( '
\ - \ - \ -
' ); - }; - - this.ajaxRequestDialog = function() { - self.showModal( '
\ -
'); - }; - - this.ajaxRequest = function() { + /** + * Saves a file + */ + this.saveFile = function( filename, content ) { $.ajax({ - url : $("#ajaxurl").val(), - cache : false, - data : $('#ajaxdata').val().replace(/\n/g,"&"), - type : $('#ajaxrequest input[name=arMethod]:checked').val(), - success : function(response) { $("#ajaxresponse").text(response); }, - error : function(e) { self.showMessage("Error: "+e, "e"); console.log(e); } - }); - }; - - this.saveFile = function() { - $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", data: ({ api: "saveFile", dir: self.currentDir, - filename: $("#showFile input[name^=filename]").val(), - content: ifm.editor.getValue() + filename: filename, + content: content }), dataType: "json", success: function( data ) { @@ -276,9 +311,14 @@ function IFM() { self.fileChanged = false; }; + /** + * Edit a file + * + * @params string name - name of the file + */ this.editFile = function( name ) { $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", dataType: "json", data: ({ @@ -288,7 +328,7 @@ function IFM() { }), success: function( data ) { if( data.status == "OK" && data.data.content != null ) { - self.showFileForm( data.data.filename, data.data.content ); + self.showFileDialog( data.data.filename, data.data.content ); } else if( data.status == "OK" && data.data.content == null ) { self.showMessage( "The content of this file cannot be fetched.", "e" ); @@ -299,24 +339,83 @@ function IFM() { }); }; - this.deleteFileDialog = function( name ) { - self.showModal( '
\ - \ -
' ); + /** + * Shows the create directory dialog + */ + this.showCreateDirDialog = function() { + self.showModal( self.templates.createdir ); + var form = $( '#formCreateDir' ); + form.find( 'input[name=dirname]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonSave' ).on( 'click', function() { + self.createDir( form.find( 'input[name=dirname] ').val() ); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); }; - this.deleteFile = function( name ) { + + /** + * Create a directory + */ + this.createDir = function( dirname ) { $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", data: ({ - api: "deleteFile", + api: "createDir", dir: self.currentDir, - filename: name + dirname: dirname + }), + dataType: "json", + success: function( data ){ + if( data.status == "OK" ) { + self.showMessage( "Directory sucessfully created.", "s" ); + self.refreshFileTable(); + } + else { + self.showMessage( "Directory could not be created: "+data.message, "e" ); + } + }, + error: function() { self.showMessage( "General error occured.", "e" ); } + }); + }; + + + /** + * Shows the delete file dialog + * + * @param string name - name of the file + */ + this.showDeleteFileDialog = function( filename ) { + self.showModal( Mustache.render( self.templates.deletefile, { filename: name } ) ); + var form = $( '#formDeleteFile' ); + form.find( '#buttonYes' ).on( 'click', function() { + self.deleteFile( self.JSEncode( filename ) ); + self.hideModal(); + return false; + }); + form.find( '#buttonNo' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Deletes a file + * + * @params string name - name of the file + */ + this.deleteFile = function( filename ) { + $.ajax({ + url: self.api, + type: "POST", + data: ({ + api: "delete", + dir: self.currentDir, + filename: filename }), dataType: "json", success: function(data) { @@ -329,50 +428,40 @@ function IFM() { }); }; - this.createDir = function() { - $.ajax({ - url: self.IFM_SCFN, - type: "POST", - data: ({ - api: "createDir", - dir: self.currentDir, - dirname: $("#createDir input[name^=dirname]").val() - }), - dataType: "json", - success: function(data){ - if(data.status == "OK") { - self.showMessage("Directory sucessfully created.", "s"); - self.refreshFileTable(); - } - else { - self.showMessage("Directory could not be created: "+data.message, "e"); - } - }, - error: function() { self.showMessage("General error occured.", "e"); } + /** + * Show the rename file dialog + * + * @params string name - name of the file + */ + this.showRenameFileDialog = function( filename ) { + self.showModal( Mustache.render( self.templates.renamefile, { filename: filename } ) ); + var form = $( '#formRenameFile' ); + form.find( 'input[name=newname]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonRename' ).on( 'click', function() { + self.renameFile( filename, form.find( 'input[name=newname]' ).val() ); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; }); }; - this.renameFileDialog = function(name) { - self.showModal( '' ); - }; - - this.renameFile = function(name) { + /** + * Renames a file + * + * @params string name - name of the file + */ + this.renameFile = function( filename, newname ) { $.ajax({ - url: ifm.IFM_SCFN, + url: ifm.api, type: "POST", data: ({ - api: "renameFile", + api: "rename", dir: ifm.currentDir, - filename: name, - newname: $("#renameFile input[name^=newname]").val() + filename: filename, + newname: newname }), dataType: "json", success: function(data) { @@ -385,72 +474,165 @@ function IFM() { }); }; - this.extractFileDialog = function(name) { - var fuckWorkarounds=""; - if(name.lastIndexOf(".") > 1) - fuckWorkarounds = name.substr(0,name.length-4); - else fuckWorkarounds = name; - self.showModal( ''); - }; - - this.extractFile = function(name, t) { - var td = (t == 1)? name.substr(0,name.length-4) : ""; + /** + * Show the copy/move dialog + * + * @params string name - name of the file + */ + this.showCopyMoveDialog = function( name ) { + self.showModal( self.templates.copymove ); $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", data: ({ - api: "extractFile", - dir: self.currentDir, - filename: name, - targetdir: td + api: "getFolderTree", + dir: self.currentDir }), dataType: "json", - success: function(data) { - if(data.status == "OK") { - self.showMessage("File successfully extracted", "s"); - self.refreshFileTable(); - } else self.showMessage("File could not be extracted. Error: "+data.message, "e"); - }, - error: function() { self.showMessage("General error occured", "e"); } + success: function( data ) { + $( '#copyMoveTree' ).treeview( { data: data, levels: 0, expandIcon: "icon icon-folder-empty", collapseIcon: "icon icon-folder-open-empty" } ); + }, + error: function() { self.hideModal(); self.showMessage( "Error while fetching the folder tree.", "e" ) } + }); + $( '#copyButton' ).on( 'click', function() { + self.copyMove( name, $('#copyMoveTree .node-selected').data('path'), 'copy' ); + self.hideModal(); + return false; + }); + $( '#moveButton' ).on( 'click', function() { + self.copyMove( name, $('#copyMoveTree .node-selected').data('path'), 'move' ); + self.hideModal(); + return false; + }); + $( '#cancelButton' ).on( 'click', function() { + self.hideModal(); + return false; }); }; - this.uploadFileDialog = function() { - self.showModal( '
\ - \ -
'); + /** + * Copy or moves a file + * + * @params string name - name of the file + */ + this.copyMove = function( source, destination, action ) { + var id = self.generateGuid(); + self.task_add( action.charAt(0).toUpperCase() + action.slice(1) + " " + source + " to " + destination, id ); + $.ajax({ + url: self.api, + type: "POST", + data: { + dir: self.currentDir, + api: "copyMove", + action: action, + filename: source, + destination: destination + }, + dataType: "json", + success: function(data) { + if( data.status == "OK" ) { + self.showMessage( data.message, "s" ); + } else { + self.showMessage( data.message, "e" ); + } + self.refreshFileTable(); + }, + error: function() { + self.showMessage( "General error occured.", "e" ); + }, + complete: function() { + self.task_done( id ); + } + }); }; + /** + * Shows the extract file dialog + * + * @param string name - name of the file + */ + this.showExtractFileDialog = function( filename ) { + var targetDirSuggestion = ''; + if( filename.lastIndexOf( '.' ) > 1 ) + targetDirSuggestion = filename.substr( 0, filename.lastIndexOf( '.' ) ); + else targetDirSuggestion = filename; + self.showModal( Mustache.render( self.templates.extractfile, { filename: filename, destination: targetDirSuggestion } ) ); + var form = $('#formExtractFile'); + form.find('#buttonExtract').on( 'click', function() { + var t = form.find('input[name=extractTargetLocation]:checked').val(); + if( t == "custom" ) t = form.find('#extractCustomLocation').val(); + self.extractFile( self.JSEncode( filename ), t ); + self.hideModal(); + return false; + }); + form.find('#buttonCancel').on( 'click', function() { + self.hideModal(); + return false; + }); + form.find('#extractCustomLocation').on( 'click', function(e) { + $(e.target).prev().children().first().prop( 'checked', true ); + }); + }; + + /** + * Extracts a file + * + * @param string filename - name of the file + * @param string destination - name of the target directory + */ + this.extractFile = function( filename, destination ) { + $.ajax({ + url: self.api, + type: "POST", + data: { + api: "extract", + dir: self.currentDir, + filename: filename, + targetdir: destination + }, + dataType: "json", + success: function( data ) { + if( data.status == "OK" ) { + self.showMessage( "File successfully extracted", "s" ); + self.refreshFileTable(); + } else self.showMessage( "File could not be extracted. Error: " + data.message, "e" ); + }, + error: function() { self.showMessage( "General error occured", "e" ); } + }); + }; + + /** + * Shows the upload file dialog + */ + this.showUploadFileDialog = function() { + self.showModal( self.templates.uploadfile ); + var form = $('#formUploadFile'); + form.find( 'input[name=newfilename]' ).on( 'keypress', self.preventEnter ); + form.find( '#buttonUpload' ).on( 'click', function() { + self.uploadFile(); + self.hideModal(); + return false; + }); + form.find( '#buttonCancel' ).on( 'click', function() { + self.hideModal(); + return false; + }); + }; + + /** + * Uploads a file + */ this.uploadFile = function() { - var ufile = document.getElementById('ufile').files[0]; + var ufile = document.getElementById( 'ufile' ).files[0]; var data = new FormData(); - var newfilename = $("#uploadFile input[name^=newfilename]").val(); - data.append('api', 'uploadFile'); + var newfilename = $("#formUploadFile input[name^=newfilename]").val(); + data.append('api', 'upload'); data.append('dir', self.currentDir); data.append('file', ufile); data.append('newfilename', newfilename); var id = self.generateGuid(); $.ajax({ - url: self.IFM_SCFN, + url: self.api, type: "POST", data: data, processData: false, @@ -463,77 +645,86 @@ function IFM() { return xhr ; }, success: function(data) { - if(data.status == "OK") { - self.showMessage("File successfully uploaded", "s"); - if(data.cd == self.currentDir) self.refreshFileTable(); - } else self.showMessage("File could not be uploaded: "+data.message, "e"); - }, + if(data.status == "OK") { + self.showMessage("File successfully uploaded", "s"); + if(data.cd == self.currentDir) self.refreshFileTable(); + } else self.showMessage("File could not be uploaded: "+data.message, "e"); + }, error: function() { self.showMessage("General error occured", "e"); }, complete: function() { self.task_done(id); } }); self.task_add("Upload "+ufile.name, id); }; - this.changePermissions = function(e, name) { - if(e.keyCode == '13') - $.ajax({ - url: self.IFM_SCFN, + /** + * Change the permissions of a file + * + * @params object e - event object + * @params string name - name of the file + */ + this.changePermissions = function( filename, newperms) { + $.ajax({ + url: self.api, type: "POST", data: ({ api: "changePermissions", dir: self.currentDir, - filename: name, - chmod: e.target.value + filename: filename, + chmod: newperms }), dataType: "json", - success: function(data){ - if(data.status == "OK") { - self.showMessage("Permissions successfully changed.", "s"); - self.refreshFileTable(); - } - else { - self.showMessage("Permissions could not be changed: "+data.message, "e"); - } - }, + success: function( data ){ + if( data.status == "OK" ) { + self.showMessage( "Permissions successfully changed.", "s" ); + self.refreshFileTable(); + } + else { + self.showMessage( "Permissions could not be changed: "+data.message, "e"); + } + }, error: function() { self.showMessage("General error occured.", "e"); } }); }; - this.remoteUploadDialog = function() { - self.showModal( '
\ -