diff --git a/app/api.rb b/app/api.rb index 60884132..ebac1f9a 100644 --- a/app/api.rb +++ b/app/api.rb @@ -76,6 +76,37 @@ post '/api/upload' do api_success 'your file(s) have been successfully uploaded' end +post '/api/rename' do + require_api_credentials + + api_error 400, 'missing_arguments', 'you must provide path and new_path' if params[:path].blank? || params[:new_path].blank? + + path = current_site.scrubbed_path params[:path] + new_path = current_site.scrubbed_path params[:new_path] + + unless path.is_a?(String) + api_error 400, 'bad_path', "#{path} is not a valid path, cancelled renaming" + end + + unless new_path.is_a?(String) + api_error 400, 'bad_new_path', "#{new_path} is not a valid new_path, cancelled renaming" + end + + site_file = current_site.site_files.select {|sf| sf.path == path}.first + + if site_file.nil? + api_error 400, 'missing_file', "could not find #{path}" + end + + res = site_file.rename new_path + + if res.first == true + api_success "#{path} has been renamed to #{new_path}" + else + api_error 400, 'rename_error', res.last + end +end + post '/api/delete' do require_api_credentials diff --git a/app/site_files.rb b/app/site_files.rb index bfc4e53f..25286887 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -166,6 +166,26 @@ post '/site_files/delete' do redirect "/dashboard#{dir_query}" end +post '/site_files/rename' do + require_login + path = HTMLEntities.new.decode params[:path] + new_path = HTMLEntities.new.decode params[:new_path] + site_file = current_site.site_files.select {|s| s.path == path}.first + + res = site_file.rename new_path + + if res.first == true + flash[:success] = "Renamed #{path} to #{new_path}" + else + flash[:error] = "Failed to rename #{path} to #{new_path}: #{res.last}" + end + + dirname = Pathname(path).dirname + dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}" + + redirect "/dashboard#{dir_query}" +end + get '/site_files/:username.zip' do |username| require_login diff --git a/models/site.rb b/models/site.rb index 5a91ed71..a1377afb 100644 --- a/models/site.rb +++ b/models/site.rb @@ -651,6 +651,14 @@ class Site < Sequel::Model false end + def self.valid_file_mime_type_and_ext?(mime_type, extname) + unless (Site::VALID_MIME_TYPES.include?(mime_type) || mime_type =~ /text/ || mime_type =~ /inode\/x-empty/) && + Site::VALID_EXTENSIONS.include?(extname.sub(/^./, '').downcase) + return false + end + true + end + def self.valid_file_type?(uploaded_file) mime_type = Magic.guess_file_mime_type uploaded_file[:tempfile].path extname = File.extname uploaded_file[:filename] @@ -660,10 +668,7 @@ class Site < Sequel::Model # extname = uploaded_file[:filename] #end - unless (Site::VALID_MIME_TYPES.include?(mime_type) || mime_type =~ /text/ || mime_type =~ /inode\/x-empty/) && - Site::VALID_EXTENSIONS.include?(extname.sub(/^./, '').downcase) - return false - end + return false unless valid_file_mime_type_and_ext?(mime_type, extname) # clamdscan doesn't work on travis for testing return true if ENV['TRAVIS'] == 'true' @@ -1162,7 +1167,14 @@ class Site < Sequel::Model clean << part if part != '..' end - clean.join '/' + clean_path = clean.join '/' + + # Scrub carriage garbage (everything below 32 bytes.. http://www.asciitable.com/) + clean_path.each_codepoint do |c| + raise ArgumentError, 'invalid character for filename' if c < 32 + end + + clean_path end def current_files_path(path='') diff --git a/models/site_file.rb b/models/site_file.rb index 7da4be4a..52c44837 100644 --- a/models/site_file.rb +++ b/models/site_file.rb @@ -40,6 +40,55 @@ class SiteFile < Sequel::Model super end + def rename(new_path) + current_path = self.path + new_path = site.scrubbed_path new_path + + if current_path == 'index.html' + return false, 'cannot rename or move root index.html' + end + + if site.site_files.select {|sf| sf.path == new_path}.length > 0 + return false, "#{is_directory ? 'directory' : 'file'} already exists" + end + + unless is_directory + mime_type = Magic.guess_file_mime_type site.files_path(self.path) + extname = File.extname new_path + + return false, 'unsupported file type' unless site.class.valid_file_mime_type_and_ext?(mime_type, extname) + end + + begin + FileUtils.mv site.files_path(path), site.files_path(new_path) + rescue Errno::ENOENT => e + return false, 'destination directory does not exist' if e.message =~ /No such file or directory/i + raise e + rescue ArgumentError => e + raise e unless e.message =~ /same file/ + end + + DB.transaction do + self.path = new_path + self.save_changes + site.purge_cache current_path + site.purge_cache new_path + + if is_directory + site_files_in_dir = site.site_files.select {|sf| sf.path =~ /^#{current_path}\//} + site_files_in_dir.each do |site_file| + original_site_file_path = site_file.path + site_file.path = site_file.path.gsub(/^#{current_path}\//, "#{new_path}\/") + site_file.save_changes + site.purge_cache original_site_file_path + site.purge_cache site_file.path + end + end + end + + return true, nil + end + def after_destroy super unless is_directory diff --git a/tests/api_tests.rb b/tests/api_tests.rb index a2aa1365..433d2ccb 100644 --- a/tests/api_tests.rb +++ b/tests/api_tests.rb @@ -241,6 +241,40 @@ describe 'api upload hash' do end end +describe 'api rename' do + before do + create_site + basic_authorize @user, @pass + post '/api/upload', { + 'testdir/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + end + + it 'succeeds' do + post '/api/rename', path: 'testdir/test.jpg', new_path: 'testdir/test2.jpg' + res[:result].must_equal 'success' + end + + it 'fails to overwrite index file' do + post '/api/rename', path: 'testdir/test.jpg', new_path: 'index.html' + res[:result].must_equal 'error' + res[:error_type].must_equal 'rename_error' + res[:message].must_equal 'file already exists' + end + + it 'fails to overwrite existing file' do + post '/api/rename', path: 'testdir/test.jpg', new_path: 'not_found.html' + res[:result].must_equal 'error' + res[:error_type].must_equal 'rename_error' + end + + it 'succeeds with directory' do + @site.create_directory 'derpiedir' + post '/api/rename', path: 'derpiedir', new_path: 'notderpiedir' + res[:result].must_equal 'success' + end +end + describe 'api upload' do it 'fails with no auth' do post '/api/upload' diff --git a/tests/site_file_tests.rb b/tests/site_file_tests.rb index 3cdda559..e9261baa 100644 --- a/tests/site_file_tests.rb +++ b/tests/site_file_tests.rb @@ -23,6 +23,107 @@ describe 'site_files' do ScreenshotWorker.jobs.clear end + describe 'rename' do + before do + PurgeCacheWorker.jobs.clear + end + + it 'renames in same path' do + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file + + @site.site_files.last.path.must_equal 'test.jpg' + @site.site_files.last.rename('derp.jpg') + @site.site_files.last.path.must_equal('derp.jpg') + PurgeCacheWorker.jobs.first['args'].last.must_equal '/test.jpg' + File.exist?(@site.files_path('derp.jpg')).must_equal true + end + + it 'fails for bad extension change' do + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file + + @site.site_files.last.path.must_equal 'test.jpg' + res = @site.site_files.last.rename('dasharezone.exe') + res.must_equal [false, 'unsupported file type'] + @site.site_files.last.path.must_equal('test.jpg') + end + + + it 'works for directory' do + @site.create_directory 'dirone' + @site.site_files.last.path.must_equal 'dirone' + @site.site_files.last.is_directory.must_equal true + res = @site.site_files.last.rename('dasharezone') + res.must_equal [true, nil] + @site.site_files.last.path.must_equal('dasharezone') + @site.site_files.last.is_directory.must_equal true + + PurgeCacheWorker.jobs.first['args'].last.must_equal 'dirone' + PurgeCacheWorker.jobs.last['args'].last.must_equal 'dasharezone' + 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') + ) + + @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 + + PurgeCacheWorker.jobs.collect {|p| p['args'].last}.must_equal ["/test/test.jpg", "/test/index.html", "/test/", "test", "test2", "test/test.jpg", "test2/test.jpg", "test/index.html", "test/", "test2/index.html", "test2/"] + 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') + ) + + res = @site.site_files.last.rename('test/test.jpg') + res.must_equal [false, 'file already exists'] + end + + it 'doesnt wipe out existing dir' do + @site.create_directory 'dirone' + @site.create_directory 'dirtwo' + res = @site.site_files.last.rename 'dirone' + res.must_equal [false, 'directory already exists'] + end + + it 'refuses to move index.html' do + res = @site.site_files.select {|sf| sf.path == 'index.html'}.first.rename('notindex.html') + res.must_equal [false, 'cannot rename or move root index.html'] + end + + it 'works with unicode characters' do + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file + @site.site_files.last.rename("HELL💩؋.jpg") + @site.site_files.last.path.must_equal "HELL💩؋.jpg" + 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 + proc { + @site.site_files.last.rename("\r\n\t.jpg") + }.must_raise ArgumentError + @site.site_files.last.path.must_equal "test.jpg" + end + end + describe 'delete' do before do PurgeCacheWorker.jobs.clear diff --git a/views/dashboard.erb b/views/dashboard.erb index 4d5032a8..63197bfa 100644 --- a/views/dashboard.erb +++ b/views/dashboard.erb @@ -153,6 +153,7 @@ Manage <% end %> <% if !file[:is_root_index] %> + Rename Delete <% end %> <% if file[:is_directory] %> @@ -186,6 +187,28 @@ +
+ +