Merge branch 'site_file_rename'

This commit is contained in:
Kyle Drake 2019-02-26 09:38:14 -08:00
commit ef1f448a40
7 changed files with 282 additions and 5 deletions

View file

@ -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

View file

@ -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

View file

@ -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='')

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -153,6 +153,7 @@
<a href="?dir=<%= Rack::Utils.escape file[:path] %>"><i class="fa fa-edit" title="Manage"></i> Manage</a>
<% end %>
<% if !file[:is_root_index] %>
<a href="#" onclick="confirmFileRename($('#<%= Digest::SHA256.hexdigest file[:path] %>').text())"><i class="fa fa-file" title="Rename"></i> Rename</a>
<a href="#" onclick="confirmFileDelete($('#<%= Digest::SHA256.hexdigest file[:path] %>').text())"><i class="fa fa-trash" title="Delete"></i> Delete</a>
<% end %>
<% if file[:is_directory] %>
@ -186,6 +187,28 @@
</div>
</div>
<div class="modal hide fade" id="renameModal" tabindex="-1" role="dialog" aria-labelledby="renameModalLabel" aria-hidden="true">
<form method="post" action="/site_files/rename">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<input type="hidden" id="renamePathInput" name="path">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="renameModalLabel">Rename / Move</h3>
</div>
<div class="modal-body">
<input id="renameNewPathInput" name="new_path" type="text">
<p>Note: We will automatically scrub any characters not matching: a-z A-Z 0-9 _ - .</p>
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button type="submit" class="btn-Action">Rename</button>
</div>
</form>
</div>
<div class="site-actions" style="margin-bottom:25px">
<% if !current_site.plan_feature(:no_file_restrictions) %>
<a href="/site_files/allowed_types">Allowed file types</a> |
@ -249,6 +272,13 @@
<script src="/js/dropzone.min.js"></script>
<script>
function confirmFileRename(path) {
console.log(path)
$('#renamePathInput').val(path.replace('/', ''));
$('#renameNewPathInput').val(path.replace('/', ''));
$('#renameModal').modal();
}
function confirmFileDelete(name) {
$('#deleteFileName').text(name.replace('/',''));
$('#deleteConfirmModal').modal();