diff --git a/app/api.rb b/app/api.rb index 8009a91e..429c566c 100644 --- a/app/api.rb +++ b/app/api.rb @@ -42,23 +42,57 @@ get '/api/list' do end def extract_files(params, files = []) - params.each do |key, value| - # If the value is a Hash and contains a :tempfile key, it's considered an uploaded file. - if value.is_a?(Hash) && value.has_key?(:tempfile) && !value[:tempfile].nil? - files << {filename: value[:name], tempfile: value[:tempfile]} - elsif value.is_a?(Hash) || value.is_a?(Array) - # If the value is a Hash or Array, recursively search for more files. - extract_files(value, files) + # Check if the entire input is directly an array of files + if params.is_a?(Array) + params.each do |item| + # Call extract_files on each item if it's an Array or Hash to handle nested structures + if item.is_a?(Array) || item.is_a?(Hash) + extract_files(item, files) + end + end + elsif params.is_a?(Hash) + params.each do |key, value| + # If the value is a Hash and contains a :tempfile key, it's considered an uploaded file. + if value.is_a?(Hash) && value.has_key?(:tempfile) && !value[:tempfile].nil? + files << {filename: value[:name], tempfile: value[:tempfile]} + elsif value.is_a?(Array) + value.each do |val| + if val.is_a?(Hash) && val.has_key?(:tempfile) && !val[:tempfile].nil? + # Directly add the file info if it's an uploaded file within an array + files << {filename: val[:name], tempfile: val[:tempfile]} + elsif val.is_a?(Hash) || val.is_a?(Array) + # Recursively search for more files if the element is a Hash or Array + extract_files(val, files) + end + end + elsif value.is_a?(Hash) + # Recursively search for more files if the value is a Hash + extract_files(value, files) + end end end files end + post '/api/upload' do require_api_credentials - files = extract_files params + if !params[:username].blank? + site = Site[username: params[:username]] + + if site.nil? || site.is_deleted + api_error 400, 'site_not_found', "could not find site" + end + + if site.owned_by?(current_site) + @_site = site + else + api_error 400, 'site_not_allowed', "not allowed to change this site with your current logged in site" + end + end + api_error 400, 'missing_files', 'you must provide files to upload' if files.empty? uploaded_size = files.collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x } @@ -73,11 +107,23 @@ post '/api/upload' do files.each do |file| if !current_site.okay_to_upload?(file) - api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content) for this site, files have not been uploaded" + api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content) for this site, please upgrade to a supporter account to upload this file type" end if File.directory? file[:filename] - api_error 400, 'directory_exists', 'this name is being used by a directory, cannot continue' + api_error 400, 'directory_exists', "#{file[:filename]} being used by a directory" + end + + if current_site.file_size_too_large? file[:tempfile].size + api_error 400, 'file_too_large' "#{file[:filename]} is too large" + end + + if SiteFile.path_too_long? file[:filename] + api_error 400, 'file_path_too_long', "#{file[:filename]} path is too long" + end + + if SiteFile.name_too_long? file[:filename] + api_error 400, 'file_name_too_long', "#{file[:filename]} filename is too long" end end @@ -191,7 +237,7 @@ post '/api/:name' do end def require_api_credentials - return true if current_site + return true if current_site && csrf_safe? if !request.env['HTTP_AUTHORIZATION'].nil? init_api_credentials diff --git a/app/site_files.rb b/app/site_files.rb index f183dacc..ea9e7dcb 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -75,7 +75,9 @@ post '/site_files/create' do end def file_upload_response(error=nil) - flash[:error] = error if error + if error + flash[:error] = error + end if params[:from_button] query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : '' @@ -90,77 +92,6 @@ def require_login_file_upload_ajax file_upload_response 'You are not signed in!' unless signed_in? end -post '/site_files/upload' do - if params[:filename] - require_login_file_upload_ajax - tempfile = Tempfile.new 'neocities_saving_file' - - input = request.body.read - tempfile.set_encoding input.encoding - tempfile.write input - tempfile.close - - params[:files] = [{filename: params[:filename], tempfile: tempfile}] - else - require_login - end - - @errors = [] - - if params[:files].nil? - file_upload_response "Uploaded files were not seen by the server, cancelled. We don't know what's causing this yet. Please contact us so we can help fix it. Thanks!" - end - - # For migration from original design.. some pages out there won't have the site_id param yet for a while. - site = params[:site_id].nil? ? current_site : Site[params[:site_id]] - - unless site.owned_by?(current_site) - file_upload_response 'You do not have permission to save this file. Did you sign in as a different user?' - end - - params[:files].each_with_index do |file,i| - dir_name = '' - dir_name = params[:dir] if params[:dir] - - unless params[:file_paths].nil? || params[:file_paths].empty? || params[:file_paths].length == 0 - file_path = params[:file_paths][i] - unless file_path.nil? - dir_name += '/' + Pathname(file_path).dirname.to_s - end - end - - file_base_name = site.scrubbed_path file[:filename].force_encoding('UTF-8') - - file[:filename] = "#{dir_name.force_encoding('UTF-8')}/#{file_base_name}" - - if current_site.file_size_too_large? file[:tempfile].size - file_upload_response "#{Rack::Utils.escape_html file[:filename]} is too large, upload cancelled." - end - if !site.okay_to_upload? file - file_upload_response %{#{Rack::Utils.escape_html file[:filename]}: file type (or content in file) is only supported by supporter accounts. Why We Do This} - end - if SiteFile.path_too_long? file[:filename] - file_upload_response "#{Rack::Utils.escape_html file[:filename]}: path is too long, upload cancelled." - end - if SiteFile.name_too_long? file_base_name - file_upload_response "#{Rack::Utils.escape_html file[:filename]}: file name is too long, upload cancelled." - end - end - - uploaded_size = params[:files].collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x } - - if site.file_size_too_large? uploaded_size - file_upload_response "File(s) do not fit in your available free space, upload cancelled." - end - - if site.too_many_files? params[:files].length - file_upload_response "Your site has exceeded the maximum number of files, please delete some files first." - end - - results = site.store_files params[:files] - file_upload_response -end - post '/site_files/delete' do require_login path = HTMLEntities.new.decode params[:filename] diff --git a/app_helpers.rb b/app_helpers.rb index df0d4991..a8ba94e7 100644 --- a/app_helpers.rb +++ b/app_helpers.rb @@ -131,3 +131,15 @@ def hcaptcha_valid? false end end + +JS_ESCAPE_MAP = {"\\" => "\\\\", "" => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'", "`" => "\\`", "$" => "\\$"} + +def escape_javascript(javascript) + javascript = javascript.to_s + if javascript.empty? + result = "" + else + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP) + end + result +end \ No newline at end of file diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 00000000..39abe669 --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,280 @@ +if(localStorage && localStorage.getItem('viewType') == 'list') + $('#filesDisplay').addClass('list-view') + +function uploadFileFromButton() { + var form = $('#uploadFilesButtonForm')[0]; + var dirValue = $('#dir').val(); + var formData = new FormData(); + + // Append other form data + formData.append('csrf_token', $(form).find('input[name="csrf_token"]').val()); + formData.append('from_button', $(form).find('input[name="from_button"]').val()); + formData.append('dir', dirValue); + + // Append files with modified filenames + $.each($('#uploadFiles')[0].files, function(i, file) { + var modifiedFileName = dirValue + '/' + file.name; + formData.append(modifiedFileName, file); + }); + + // Submit the form data using jQuery's AJAX + $.ajax({ + url: '/api/upload', + type: 'POST', + data: formData, + contentType: false, // This is required for FormData + processData: false, // This is required for FormData + success: function(data) { + console.log('Files successfully uploaded.'); + location.reload() + }, + error: function(xhr, status, error) { + console.error('Upload failed: ' + error); + location.reload() + } + }); +} + +$('#uploadFiles').change(function() { + $('#uploadFilesButtonForm').submit(); +}); + +var uploadForm = $('#uploadFilesButtonForm')[0]; + var deleteForm = $('#deleteFilenameForm')[0]; + + function moveFileToFolder(event) { + var link = event.dataTransfer.getData("Text"); + if(link) link = link.trim(); + if(!link || link.startsWith('https://neocities.org/dashboard')) return; + event.preventDefault(); + var name = link.split('.neocities.org/').slice(1).join('.neocities.org/'); + var oReq = new XMLHttpRequest(); + oReq.open("GET", "/site_files/download/" + name, true); + oReq.responseType = "arraybuffer"; + + $('#movingOverlay').css('display', 'block') + + oReq.onload = function() { + var newFile = new File([oReq.response], name); + var dataTransfer = new DataTransfer(); + var currentFolder = new URL(location.href).searchParams.get('dir'); + if(!currentFolder) currentFolder = ''; + else currentFolder = currentFolder + '/'; + + dataTransfer.items.add(newFile); + $('#uploadFilesButtonForm > input[name="dir"]')[0].value = currentFolder + event.target.parentElement.parentElement.getElementsByClassName('title')[0].innerText.trim(); + $('#uploadFiles')[0].files = dataTransfer.files; + $.ajax({ + type: uploadForm.method, + url: uploadForm.action, + data: new FormData(uploadForm), + processData: false, + contentType: false, + success: function() { + let csrf = $('#uploadFilesButtonForm > input[name="csrf_token"]')[0].value; + var dReq = new XMLHttpRequest(); + dReq.open(deleteForm.method, deleteForm.action, true); + dReq.onload = function() { + location.reload() + } + dReq.setRequestHeader("content-type", 'application/x-www-form-urlencoded'); + dReq.send("csrf_token=" + encodeURIComponent(csrf) + "&filename=" + name.replace(/\s/g, '+')); + }, + error: function() { + location.reload() + } + }); + }; + oReq.send(); + } + + function confirmFileRename(path) { + console.log(path) + $('#renamePathInput').val(path); + $('#renameNewPathInput').val(path); + $('#renameModal').modal(); + } + + function confirmFileDelete(name) { + $('#deleteFileName').text(name); + $('#deleteConfirmModal').modal(); + } + + function fileDelete() { + $('#deleteFilenameInput').val($('#deleteFileName').html()); + $('#deleteFilenameForm').submit(); + } + + function clickUploadFiles() { + $("input[id='uploadFiles']").click() + } + + function showUploadProgress() { + $('#uploadingOverlay').css('display', 'block') + } + + function hideUploadProgress() { + $('#progressBar').css('display', 'none') + $('#uploadingOverlay').css('display', 'none') + } + + +/* + this.on("totaluploadprogress", function(progress, totalBytes, totalBytesSent) { + if(progress == 100) + allUploadsComplete = true + + showUploadProgress() + $('#progressBar').css('display', 'block') + $('#uploadingProgress').css('width', progress+'%') + }) +*/ + + allUploadsComplete = false + + $('#createDir').on('shown', function () { + $('#newDirInput').focus(); + }) + + $('#createFile').on('shown', function () { + $('#newFileInput').focus(); + }) + + function listView() { + if(localStorage) + localStorage.setItem('viewType', 'list') + + $('#filesDisplay').addClass('list-view') + } + + function iconView() { + if(localStorage) + localStorage.removeItem('viewType') + + $('#filesDisplay').removeClass('list-view') + } + + + + + + + +// Drop handler function to get all files +async function getAllFileEntries(dataTransferItemList) { + let fileEntries = []; + // Use BFS to traverse entire directory/file structure + let queue = []; + for (let i = 0; i < dataTransferItemList.length; i++) { + queue.push(dataTransferItemList[i].webkitGetAsEntry()); + } + while (queue.length > 0) { + let entry = queue.shift(); + if (entry.isFile) { + fileEntries.push(entry); + } else if (entry.isDirectory) { + let reader = entry.createReader(); + queue.push(...await readAllDirectoryEntries(reader)); + } + } + return fileEntries; +} + +// Get all the entries (files or sub-directories) in a directory +async function readAllDirectoryEntries(directoryReader) { + let entries = []; + let readEntries = await readEntriesPromise(directoryReader); + while (readEntries.length > 0) { + entries.push(...readEntries); + readEntries = await readEntriesPromise(directoryReader); + } + return entries; +} + +// Wrap readEntries in a promise +async function readEntriesPromise(directoryReader) { + try { + return await new Promise((resolve, reject) => { + directoryReader.readEntries(resolve, reject); + }); + } catch (err) { + console.log(err); + } +} + +async function uploadFile(file, dir, additionalFormData) { + const formData = new FormData(); + + // Append additional form data (from other input fields) to each file's FormData + for (const [key, value] of Object.entries(additionalFormData)) { + formData.append(key, value); + } + + // Append the file to the FormData, using the file name as key + var modifiedFileName = dir + '/' + file.webkitRelativePath || file.name; + formData.append(modifiedFileName, file, modifiedFileName); + + $('#uploadFileName').text(modifiedFileName).prepend(' '); + + // Send the FormData with the file and additional data + try { + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + const result = await response.json(); + console.log('Upload successful for', file.name, result); + } catch (err) { + console.error('Upload error for', file.name, err); + } +} + +async function processEntry(entry, dir, additionalFormData) { + await new Promise((resolve) => { + entry.file((file) => { + uploadFile(file, dir, additionalFormData).then(resolve); + }); + }); +} + +async function uploadFiles(fileEntries) { + // Collect additional form data + const form = document.getElementById('dropzone'); + let additionalFormData = {}; + for (let i = 0; i < form.elements.length; i++) { + const input = form.elements[i]; + if (input.name && input.type !== "file") { // Avoid file inputs + additionalFormData[input.name] = input.value; + } + } + + const dir = additionalFormData['dir'] || ''; + + var totalFiles = fileEntries.length; + $('#progressBar').css('display', 'block') + + fileUploadCount = 0 + + for (let entry of fileEntries) { + await processEntry(entry, dir, additionalFormData); + fileUploadCount++; + var progress = (fileUploadCount / totalFiles) * 100; + $('#uploadingProgress').css('width', progress+'%'); + } + + allUploadsComplete = true + location.reload(); +} + +var elDrop = document.getElementById('dropzone'); + +elDrop.addEventListener('dragover', function (event) { + event.preventDefault(); +}); + +elDrop.addEventListener('drop', async function (event) { + event.preventDefault(); + showUploadProgress(); + let items = await getAllFileEntries(event.dataTransfer.items); + await uploadFiles(items); +}); diff --git a/public/js/html5.min.js b/public/js/html5.min.js deleted file mode 100644 index 448cebd7..00000000 --- a/public/js/html5.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed -*/ -(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); -a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; -c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| -"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); -if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), 'username' => @other_site.username + + _(res[:result]).must_equal 'error' + _(@other_site.site_files.select {|s| s.path == 'test.jpg'}).must_equal [] + _(res[:error_type]).must_equal 'site_not_allowed' + end + it 'succeeds with square bracket in filename' do create_site @site.generate_api_key! @@ -329,6 +341,34 @@ describe 'api' do _(site_file_exists?('test.jpg')).must_equal true end + it 'succeeds with valid user session controlled site' do + create_site + @other_site = Fabricate :site, parent_site_id: @site.id + post '/api/upload', + {'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'csrf_token' => 'abcd', + 'username' => @other_site.username}, + {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} + + _(res[:result]).must_equal 'success' + _(last_response.status).must_equal 200 + _(@other_site.site_files.select {|sf| sf.path == 'test.jpg'}.length).must_equal 1 + end + + it 'fails session upload unless controls site' do + create_site + @other_site = Fabricate :site + post '/api/upload', { + 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'username' => @other_site.username, + 'csrf_token' => 'abcd'}, + {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} + + _(res[:result]).must_equal 'error' + _(@other_site.site_files.select {|s| s.path == 'test.jpg'}).must_equal [] + _(res[:error_type]).must_equal 'site_not_allowed' + end + it 'fails with bad api key' do create_site @site.generate_api_key! diff --git a/tests/site_file_tests.rb b/tests/site_file_tests.rb index b450ec7f..0be81427 100644 --- a/tests/site_file_tests.rb +++ b/tests/site_file_tests.rb @@ -9,7 +9,7 @@ describe 'site_files' do end def upload(hash) - post '/site_files/upload', hash.merge(csrf_token: 'abcd'), {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} + post '/api/upload', hash.merge(csrf_token: 'abcd'), {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} end def delete_file(hash) @@ -45,7 +45,7 @@ describe 'site_files' do it 'works with html file' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/notindex.html', 'text/html') - upload 'files[]' => uploaded_file + upload 'notindex.html' => uploaded_file PurgeCacheWorker.jobs.clear testfile = @site.site_files_dataset.where(path: 'notindex.html').first testfile.rename 'notindex2.html' @@ -55,7 +55,7 @@ describe 'site_files' do it 'renames in same path' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => uploaded_file testfile = @site.site_files_dataset.where(path: 'test.jpg').first _(testfile).wont_equal nil @@ -66,9 +66,6 @@ describe 'site_files' do end it 'fails when file does not exist' do - #uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - #upload 'files[]' => uploaded_file - post '/site_files/rename', {path: 'derp.jpg', new_path: 'derp2.jpg', csrf_token: 'abcd'}, {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} _(last_response.headers['Location']).must_match /dashboard/ get '/dashboard', {}, {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }} @@ -77,7 +74,7 @@ describe 'site_files' do it 'fails for bad extension change' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => uploaded_file testfile = @site.site_files_dataset.where(path: 'test.jpg').first res = testfile.rename('dasharezone.exe') @@ -89,7 +86,7 @@ describe 'site_files' do no_file_restriction_plans = Site::PLAN_FEATURES.select {|p,v| v[:no_file_restrictions] == true} no_file_restriction_plans.each do |plan_type,hash| @site = Fabricate :site, plan_type: plan_type - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') + upload 'flowercrime.wav' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') testfile = @site.site_files_dataset.where(path: 'flowercrime.wav').first res = testfile.rename('flowercrime.exe') _(res.first).must_equal true @@ -126,18 +123,13 @@ describe 'site_files' do end it 'changes path of files and dirs within directory when changed' do - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'image/jpeg') - ) + upload 'test/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'test/index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'image/jpeg') PurgeCacheWorker.jobs.clear @site.site_files.select {|s| s.path == 'test'}.first.rename('test2') + _(@site.site_files.select {|sf| sf.path =~ /test2\/index.html/}.length).must_equal 1 _(@site.site_files.select {|sf| sf.path =~ /test2\/test.jpg/}.length).must_equal 1 _(@site.site_files.select {|sf| sf.path =~ /test\/test.jpg/}.length).must_equal 0 @@ -146,14 +138,8 @@ describe 'site_files' do end it 'doesnt wipe out existing file' do - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'image/jpeg') - ) + upload 'test/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'test/index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'image/jpeg') res = @site.site_files_dataset.where(path: 'test/index.html').first.rename('test/test.jpg') _(res).must_equal [false, 'file already exists'] @@ -172,15 +158,14 @@ describe 'site_files' do end it 'works with unicode characters' do - uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') @site.site_files_dataset.where(path: 'test.jpg').first.rename("HELL💩؋.jpg") _(@site.site_files_dataset.where(path: "HELL💩؋.jpg").first).wont_equal nil end it 'scrubs weird carriage return shit characters' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(proc { @site.site_files_dataset.where(path: 'test.jpg').first.rename("\r\n\t.jpg") }).must_raise ArgumentError @@ -196,7 +181,7 @@ describe 'site_files' do it 'works' do initial_space_used = @site.space_used uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => uploaded_file PurgeCacheWorker.jobs.clear @@ -217,30 +202,21 @@ describe 'site_files' do end it 'property deletes directories with regexp special chars in them' do - upload 'dir' => '8)', 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload '8)/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') delete_file filename: '8)' _(@site.reload.site_files.select {|f| f.path =~ /#{Regexp.quote '8)'}/}.length).must_equal 0 end it 'deletes with escaped apostrophe' do - upload( - 'dir' => "test'ing", - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload "test'ing/test.jpg" => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(@site.reload.site_files.select {|s| s.path == "test'ing"}.length).must_equal 1 delete_file filename: "test'ing" _(@site.reload.site_files.select {|s| s.path == "test'ing"}.length).must_equal 0 end it 'deletes a directory and all files in it' do - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) - upload( - 'dir' => '', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'test/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') space_used = @site.reload.space_used delete_file filename: 'test' @@ -253,10 +229,7 @@ describe 'site_files' do end it 'deletes records for nested directories' do - upload( - 'dir' => 'derp/ing/tons', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'derp/ing/tons/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') expected_site_file_paths = ['derp', 'derp/ing', 'derp/ing/tons', 'derp/ing/tons/test.jpg'] @@ -274,16 +247,11 @@ describe 'site_files' do end it 'goes back to deleting directory' do - upload( - 'dir' => 'test', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'test/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') delete_file filename: 'test/test.jpg' _(last_response.headers['Location']).must_equal "http://example.org/dashboard?dir=test" - upload( - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') delete_file filename: 'test.jpg' _(last_response.headers['Location']).must_equal "http://example.org/dashboard" end @@ -291,17 +259,17 @@ describe 'site_files' do describe 'upload' do it 'works with empty files' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/empty.js', 'text/javascript') + upload 'empty.js' => Rack::Test::UploadedFile.new('./tests/files/empty.js', 'text/javascript') _(File.exists?(@site.files_path('empty.js'))).must_equal true end it 'manages files with invalid UTF8' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/invalidutf8.html', 'text/html') + upload 'invalidutf8.html' => Rack::Test::UploadedFile.new('./tests/files/invalidutf8.html', 'text/html') _(File.exists?(@site.files_path('invalidutf8.html'))).must_equal true end it 'works with manifest files' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/cache.manifest', 'text/cache-manifest') + upload 'cache.manifest' => Rack::Test::UploadedFile.new('./tests/files/cache.manifest', 'text/cache-manifest') _(File.exists?(@site.files_path('cache.manifest'))).must_equal true end @@ -312,7 +280,7 @@ describe 'site_files' do file.write("derp") end - upload 'files[]' => Rack::Test::UploadedFile.new(file_path, 'text/html') + upload file_path => Rack::Test::UploadedFile.new(file_path, 'text/html') _(last_response.body).must_match /name is too long/i ensure FileUtils.rm file_path @@ -320,20 +288,17 @@ describe 'site_files' do end it 'fails with path greater than limit' do - upload( - 'dir' => (("a" * 50 + "/") * (SiteFile::FILE_PATH_CHARACTER_LIMIT / 50 - 1) + "a" * 50), - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload "#{(("a" * 50 + "/") * (SiteFile::FILE_PATH_CHARACTER_LIMIT / 50 - 1) + "a" * 50)}/test.jpg" => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(last_response.body).must_match /path is too long/i end it 'works with otf fonts' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/chunkfive.otf', 'application/vnd.ms-opentype') + upload 'chunkfive.otf' => Rack::Test::UploadedFile.new('./tests/files/chunkfive.otf', 'application/vnd.ms-opentype') _(File.exists?(@site.files_path('chunkfive.otf'))).must_equal true end it 'purges cache for html file with extension removed' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/notindex.html', 'text/html') + upload 'notindex.html' => Rack::Test::UploadedFile.new('./tests/files/notindex.html', 'text/html') _(PurgeCacheWorker.jobs.length).must_equal 1 PurgeCacheWorker.new.perform @site.username, '/notindex.html' _(PurgeCacheWorker.jobs.first['args'].last).must_equal '/notindex' @@ -341,7 +306,7 @@ describe 'site_files' do it 'succeeds with index.html file' do _(@site.site_changed).must_equal false - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') + upload 'index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') _(last_response.body).must_match /successfully uploaded/i _(File.exists?(@site.files_path('index.html'))).must_equal true @@ -367,9 +332,9 @@ describe 'site_files' do it 'provides the correct space used after overwriting an existing file' do initial_space_used = @site.space_used uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => uploaded_file second_uploaded_file = Rack::Test::UploadedFile.new('./tests/files/img/test.jpg', 'image/jpeg') - upload 'files[]' => second_uploaded_file + upload 'test.jpg' => second_uploaded_file _(@site.reload.space_used).must_equal initial_space_used + second_uploaded_file.size _(@site.space_used).must_equal @site.actual_space_used end @@ -377,27 +342,22 @@ describe 'site_files' do it 'does not change title for subdir index.html' do title = @site.title upload( - 'dir' => 'derpie', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') + 'derpie/index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') ) _(@site.reload.title).must_equal title end it 'purges cache for /subdir/' do # (not /subdir which is just a redirect to /subdir/) upload( - 'dir' => 'subdir', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') + 'subdir/index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') ) _(PurgeCacheWorker.jobs.select {|j| j['args'].last == '/subdir/'}.length).must_equal 1 end it 'succeeds with multiple files' do upload( - 'file_paths' => ['one/test.jpg', 'two/test.jpg'], - 'files' => [ - Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), - Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ] + 'one/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'two/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') ) _(@site.site_files.select {|s| s.path == 'one'}.length).must_equal 1 @@ -409,7 +369,7 @@ describe 'site_files' do it 'succeeds with valid file' do initial_space_used = @site.space_used uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'test.jpg' => uploaded_file _(last_response.body).must_match /successfully uploaded/i _(File.exists?(@site.files_path('test.jpg'))).must_equal true @@ -434,22 +394,23 @@ describe 'site_files' do it 'works with square bracket filename' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/te[s]t.jpg', 'image/jpeg') - upload 'files[]' => uploaded_file + upload 'te[s]t.jpg' => uploaded_file _(last_response.body).must_match /successfully uploaded/i _(File.exists?(@site.files_path('te[s]t.jpg'))).must_equal true end it 'sets site changed to false if index is empty' do uploaded_file = Rack::Test::UploadedFile.new('./tests/files/blankindex/index.html', 'text/html') - upload 'files[]' => uploaded_file + upload 'index.html' => uploaded_file _(last_response.body).must_match /successfully uploaded/i _(@site.empty_index?).must_equal true _(@site.site_changed).must_equal false end it 'fails with unsupported file' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') - _(last_response.body).must_match /only supported by.+supporter account/i + upload 'flowercrime.wav' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') + + _(JSON.parse(last_response.body)['message']).must_match /please upgrade to a supporter account/i _(File.exists?(@site.files_path('flowercrime.wav'))).must_equal false _(@site.site_changed).must_equal false end @@ -458,17 +419,17 @@ describe 'site_files' do no_file_restriction_plans = Site::PLAN_FEATURES.select {|p,v| v[:no_file_restrictions] == true} no_file_restriction_plans.each do |plan_type,hash| @site = Fabricate :site, plan_type: plan_type - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') + upload 'flowercrime.wav' => Rack::Test::UploadedFile.new('./tests/files/flowercrime.wav', 'audio/x-wav') _(last_response.body).must_match /successfully uploaded/i _(File.exists?(@site.files_path('flowercrime.wav'))).must_equal true end end it 'overwrites existing file with new file' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(last_response.body).must_match /successfully uploaded/i digest = @site.reload.site_files.first.sha1_hash - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/img/test.jpg', 'image/jpeg') + upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/img/test.jpg', 'image/jpeg') _(last_response.body).must_match /successfully uploaded/i _(@site.reload.changed_count).must_equal 2 _(@site.site_files.select {|f| f.path == 'test.jpg'}.length).must_equal 1 @@ -476,10 +437,7 @@ describe 'site_files' do end it 'works with directory path' do - upload( - 'dir' => 'derpie/derptest', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'derpie/derptest/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(last_response.body).must_match /successfully uploaded/i _(File.exists?(@site.files_path('derpie/derptest/test.jpg'))).must_equal true @@ -504,41 +462,29 @@ describe 'site_files' do end it 'works with unicode chars on filename and dir' do - upload( - 'dir' => '詩經', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/詩經.jpg', 'image/jpeg') - ) + upload '詩經/詩經.jpg' => Rack::Test::UploadedFile.new('./tests/files/詩經.jpg', 'image/jpeg') _(@site.site_files_dataset.where(path: '詩經/詩經.jpg').count).must_equal 1 end it 'does not register site changing until root index.html is changed' do - upload( - 'dir' => 'derpie/derptest', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'derpie/derptest/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(@site.reload.site_changed).must_equal false - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') + upload 'index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') _(@site.reload.site_changed).must_equal true - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/chunkfive.otf', 'application/vnd.ms-opentype') + upload 'chunkfive.otf' => Rack::Test::UploadedFile.new('./tests/files/chunkfive.otf', 'application/vnd.ms-opentype') _(@site.reload.site_changed).must_equal true end it 'does not store new file if hash matches' do - upload( - 'dir' => 'derpie/derptest', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'derpie/derptest/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(@site.reload.changed_count).must_equal 1 - upload( - 'dir' => 'derpie/derptest', - 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - ) + upload 'derpie/derptest/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') _(@site.reload.changed_count).must_equal 1 - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') + upload 'index.html' => Rack::Test::UploadedFile.new('./tests/files/index.html', 'text/html') _(@site.reload.changed_count).must_equal 2 end @@ -554,21 +500,6 @@ describe 'site_files' do puts "TODO FINISH CLASSIFIER" #$trainer.instance_variable_get('@db').redis.flushall end -=begin - it 'trains files' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/classifier/ham.html', 'text/html') - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/classifier/spam.html', 'text/html') - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/classifier/phishing.html', 'text/html') - - @site.train 'ham.html' - @site.train 'spam.html', 'spam' - @site.train 'phishing.html', 'phishing' - - _(@site.classify('ham.html')).must_equal 'ham' - _(@site.classify('spam.html')).must_equal 'spam' - _(@site.classify('phishing.html')).must_equal 'phishing' - end -=end end end end diff --git a/views/dashboard.erb b/views/dashboard.erb index 068d1abb..6dcd2d7e 100644 --- a/views/dashboard.erb +++ b/views/dashboard.erb @@ -61,13 +61,10 @@ <%== flash_display %> - Uploading, please wait... + @@ -106,7 +103,7 @@ - + @@ -227,14 +224,13 @@ - + - - + + - @@ -272,148 +268,4 @@ - - + - - diff --git a/views/site_files/text_editor.erb b/views/site_files/text_editor.erb index 1c4e9e52..1f972c6d 100644 --- a/views/site_files/text_editor.erb +++ b/views/site_files/text_editor.erb @@ -132,17 +132,31 @@ function saveTextFile(quit) { if(unsavedChanges == false) return + + var formData = new FormData(); + var fileContent = new Blob([editor.getValue()], { type: 'text/html' }); + formData.append('<%= escape_javascript @filename %>', fileContent, '<%= escape_javascript @filename %>'); + formData.append('csrf_token', '<%= escape_javascript csrf_token %>'); + formData.append('username', '<%= escape_javascript current_site.username %>'); + $.ajax({ - url: "/site_files/upload?csrf_token=<%= Rack::Utils.escape csrf_token %>&filename=<%= Rack::Utils.escape @filename %>&site_id=<%= current_site.id %>", - data: editor.getValue(), + url: '/api/upload', + data: formData, processData: false, contentType: false, type: 'POST', error: function(jqXHR, textStatus, errorThrown) { var errorMessage = 'There has been an error saving your file, please try again. If it continues to fail, make a copy of the file locally so you don\'t lose your changes!' - if(jqXHR.responseText) - errorMessage += ' ERROR MESSAGE: '+jqXHR.responseText + if(jqXHR.responseText) { + try { + // Attempt to parse the JSON responseText to get the error message + var parsedResponse = JSON.parse(jqXHR.responseText); + errorMessage += ' ERROR MESSAGE: ' + parsedResponse.message; + } catch (error) { + } + } + $('#saveButton').tooltip('show') $('#editorUpdates span').text(errorMessage)
Uploading, please wait...