diff --git a/app/site_files.rb b/app/site_files.rb index 02d97edf..42ae30e8 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -1,104 +1,3 @@ -get '/site_files/new_page' do - require_login - @title = 'New Page' - erb :'site_files/new_page' -end - -# Redirect from original path -get '/site_files/new' do - require_login - redirect '/site_files/new_page' -end - -post '/site_files/create' do - require_login - @errors = [] - - filename = params[:filename] - - filename.gsub!(/[^a-zA-Z0-9_\-.]/, '') - - redirect_uri = '/dashboard' - redirect_uri += "?dir=#{Rack::Utils.escape params[:dir]}" if params[:dir] - - if filename.nil? || filename.strip.empty? - flash[:error] = 'You must provide a file name.' - redirect redirect_uri - end - - name = "#{filename}" - - name = "#{params[:dir]}/#{name}" if params[:dir] - - name = current_site.scrubbed_path name - - if current_site.file_exists?(name) - flash[:error] = %{Web page "#{Rack::Utils.escape_html name}" already exists! Choose another name.} - redirect redirect_uri - end - - if SiteFile.name_too_long?(name) - flash[:error] = "File name is too long (exceeds #{SiteFile::FILE_NAME_CHARACTER_LIMIT} characters)." - redirect redirect_uri - end - - extname = File.extname name - - unless extname.empty? || extname.match(/^\.#{Site::EDITABLE_FILE_EXT}/i) - flash[:error] = "Must be an editable text file type (#{Site::VALID_EDITABLE_EXTENSIONS.join(', ')})." - redirect redirect_uri - end - - site_file = current_site.site_files_dataset.where(path: name).first - - if site_file - flash[:error] = 'File already exists, cannot create.' - redirect redirect_uri - end - - if extname.match(/^\.html|^\.htm/i) - begin - current_site.install_new_html_file name - rescue Sequel::UniqueConstraintViolation - end - else - file_path = current_site.files_path(name) - File.open(file_path, 'a', 0640) {} - - site_file ||= SiteFile.new site_id: current_site.id, path: name - - site_file.size = 0 - site_file.set size: 0 - site_file.set sha1_hash: Digest::SHA1.hexdigest('') - site_file.set updated_at: Time.now - site_file.save - end - - escaped_name = Rack::Utils.escape_html name - - flash[:success] = %{#{escaped_name} was created! Click here to edit it.} - - redirect redirect_uri -end - -def file_upload_response(error=nil) - if error - flash[:error] = error - end - - if params[:from_button] - query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : '' - redirect "/dashboard#{query_string}" - else - halt 406, error if error - halt 200, 'File(s) successfully uploaded.' - end -end - -def require_login_file_upload_ajax - file_upload_response 'You are not signed in!' unless signed_in? -end - post '/site_files/delete' do require_login path = HTMLEntities.new.decode params[:filename] diff --git a/models/site.rb b/models/site.rb index 1931d817..0fc2f8d9 100644 --- a/models/site.rb +++ b/models/site.rb @@ -761,6 +761,11 @@ class Site < Sequel::Model end def self.valid_file_mime_type_and_ext?(mime_type, extname) + # For files with no extension, only check mime type + if extname == '' + return mime_type =~ /text/ || mime_type == 'application/json' || mime_type =~ /inode\/x-empty/ + end + valid_mime_type = Site::VALID_MIME_TYPES.include?(mime_type) || mime_type =~ /text/ || mime_type =~ /inode\/x-empty/ valid_extension = Site::VALID_EXTENSIONS.include?(extname.sub(/^./, '').downcase) unless valid_extension @@ -773,11 +778,6 @@ class Site < Sequel::Model mime_type = Magic.guess_file_mime_type uploaded_file[:tempfile].path extname = File.extname uploaded_file[:filename] - # Possibly needed logic for .dotfiles - #if extname == '' - # extname = uploaded_file[:filename] - #end - return false unless valid_file_mime_type_and_ext?(mime_type, extname) # clamdscan doesn't work on continuous integration for testing diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 33eb40d2..6de63671 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -36,8 +36,54 @@ $('#createDir').on('shown', function () { $('#createFile').on('shown', function () { $('#newFileInput').focus(); + $('#newFileInput').val(''); + clearCreateFileError(); }) +function showCreateFileError(message) { + var errorDiv = $('#createFileError'); + errorDiv.text(message); + errorDiv.show(); +} + +function clearCreateFileError() { + var errorDiv = $('#createFileError'); + errorDiv.hide(); + errorDiv.text(''); +} + +function fileExists(filePath) { + // Check if a file with this path already exists in the current file listing + // Each file has a hidden div containing the file path + var exists = false; + var lowerFilePath = filePath.toLowerCase(); + + $('#filesDisplay .file .overlay div[style*="display: none"]').each(function() { + var existingPath = $(this).text().trim(); + if (existingPath.toLowerCase() === lowerFilePath) { + exists = true; + return false; // Break out of each loop + } + }); + + return exists; +} + +$('#newFileInput').on('keypress', function(e) { + if (e.which === 13) { // Enter key + handleCreateFile(); + } +}) + +function handleCreateFile() { + var filename = $('#newFileInput').val(); + var dir = $('#createFileDir').val(); + var csrfToken = $('#createFileCSRFToken').val(); + + // Don't hide modal yet - wait for success or error + createFileViaAPI(filename, dir, csrfToken); +} + function listView() { if(localStorage) localStorage.setItem('viewType', 'list') @@ -153,5 +199,98 @@ function reloadDashboardFiles() { }); } +function createFileViaAPI(filename, dir, csrfToken) { + clearCreateFileError(); + showUploadProgress(); + + filename = filename.replace(/[^a-zA-Z0-9_\-.]/g, ''); + + if (!filename || filename.trim() === '') { + hideUploadProgress(); + showCreateFileError('You must provide a file name.'); + return; + } + + var extMatch = filename.match(/\.([^.]+)$/); + + // Check if extension is allowed for editing (if there is one) + if (extMatch) { + var extension = extMatch[1].toLowerCase(); + var validExtensions = [ + 'html', 'htm', 'txt', 'js', 'css', 'scss', 'md', 'manifest', 'less', + 'webmanifest', 'xml', 'json', 'opml', 'rdf', 'svg', 'gpg', 'pgp', + 'resolvehandle', 'pls', 'yaml', 'yml', 'toml', 'osdx', 'mjs', 'cjs', + 'ts', 'py', 'rss', 'glsl' + ]; + + if (validExtensions.indexOf(extension) === -1) { + hideUploadProgress(); + showCreateFileError('Must be an editable file type (' + validExtensions.join(', ') + ') or a file with no extension.'); + return; + } + } + // Files with no extension are allowed (they're treated as text files) + + var fullPath = dir ? joinPaths(dir, filename) : filename; + + // Check if file already exists + if (fileExists(fullPath)) { + hideUploadProgress(); + showCreateFileError('A file with this name already exists. Please choose a different name.'); + return; + } + + var isHTML = /\.(html|htm)$/i.test(filename); + + // Create default HTML template content + var htmlTemplate = '\n' + + '\n' + + '
\n' + + ' \n' + + ' \n' + + '