From 2f73732daafef5c8ec40bfe4b46608efb7420695 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Tue, 19 Aug 2014 13:44:15 -0700 Subject: [PATCH] Folders. --- Gemfile | 6 +- Gemfile.lock | 32 ++- Rakefile | 2 +- app.rb | 82 ++++--- environment.rb | 1 - models/site.rb | 218 +++++++++++------- models/site_file.rb | 8 - tests/acceptance/dashboard_tests.rb | 26 +++ tests/acceptance/environment.rb | 7 + tests/acceptance/index_tests.rb | 11 + tests/acceptance/settings_tests.rb | 44 ++++ tests/acceptance/signin_tests.rb | 53 +++++ .../signup_tests.rb} | 113 +-------- tests/api_tests.rb | 58 ++++- tests/environment.rb | 11 +- tests/site_file_tests.rb | 33 +++ views/dashboard.erb | 91 +++++--- 17 files changed, 508 insertions(+), 288 deletions(-) delete mode 100644 models/site_file.rb create mode 100644 tests/acceptance/dashboard_tests.rb create mode 100644 tests/acceptance/environment.rb create mode 100644 tests/acceptance/index_tests.rb create mode 100644 tests/acceptance/settings_tests.rb create mode 100644 tests/acceptance/signin_tests.rb rename tests/{acceptance_tests.rb => acceptance/signup_tests.rb} (63%) create mode 100644 tests/site_file_tests.rb diff --git a/Gemfile b/Gemfile index 36634fb0..b5af3671 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'bcrypt' gem 'sinatra-flash', require: 'sinatra/flash' gem 'sinatra-xsendfile', require: 'sinatra/xsendfile' gem 'puma', require: nil -gem 'rubyzip', require: 'zip' gem 'rack-recaptcha', require: 'rack/recaptcha' gem 'rmagick', require: nil gem 'sidekiq' @@ -19,6 +18,8 @@ gem 'erubis' gem 'stripe', :git => 'https://github.com/stripe/stripe-ruby' gem 'screencap' gem 'cocaine' +gem 'zipruby' +gem 'always_verify_ssl_certificates' platform :mri do gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic @@ -54,13 +55,12 @@ group :test do gem 'minitest' gem 'minitest-reporters', require: 'minitest/reporters' gem 'rack-test', require: 'rack/test' - gem 'webmock' gem 'mocha', require: nil gem 'rake', require: nil gem 'poltergeist' gem 'phantomjs', require: 'phantomjs/poltergeist' - gem 'capybara' gem 'capybara_minitest_spec' + gem 'rack_session_access', require: nil platform :mri do gem 'simplecov', require: nil diff --git a/Gemfile.lock b/Gemfile.lock index e730f3ae..72faf470 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,6 +18,7 @@ GEM tzinfo (~> 1.1) addressable (2.3.6) ago (0.1.5) + always_verify_ssl_certificates (0.3.0) ansi (1.4.3) autoparse (0.3.3) addressable (>= 2.3.1) @@ -25,7 +26,7 @@ GEM multi_json (>= 1.0.0) bcrypt (3.1.7) builder (3.2.2) - capybara (2.2.1) + capybara (2.4.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -44,8 +45,6 @@ GEM coderay (1.1.0) columnize (0.3.6) connection_pool (2.0.0) - crack (0.4.2) - safe_yaml (~> 1.0.0) debugger (1.6.6) columnize (>= 0.3.1) debugger-linecache (~> 1.2.0) @@ -89,7 +88,7 @@ GEM metaclass (0.0.4) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.5.3) + mini_portile (0.6.0) minitest (5.3.1) minitest-reporters (1.0.2) ansi @@ -98,13 +97,13 @@ GEM powerbar mocha (1.0.0) metaclass (~> 0.0.1) - multi_json (1.9.2) + multi_json (1.10.1) multipart-post (2.0.0) - nokogiri (1.6.1) - mini_portile (~> 0.5.0) + nokogiri (1.6.3.1) + mini_portile (= 0.6.0) pg (0.17.1) phantomjs (1.9.7.0) - poltergeist (1.5.0) + poltergeist (1.5.1) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -129,6 +128,9 @@ GEM json rack-test (0.6.2) rack (>= 1.0) + rack_session_access (0.1.1) + builder (>= 2.0.0) + rack (>= 1.0.0) rainbows (4.6.1) kgio (~> 2.5) rack (~> 1.1) @@ -142,8 +144,6 @@ GEM mime-types (>= 1.16) retriable (1.4.1) rmagick (2.13.2) - rubyzip (1.1.2) - safe_yaml (1.0.1) sass (3.3.8) screencap (0.1.1) phantomjs @@ -191,20 +191,18 @@ GEM rack raindrops (~> 0.7) uuidtools (2.1.4) - webmock (1.17.4) - addressable (>= 2.2.7) - crack (>= 0.3.2) - websocket-driver (0.3.2) + websocket-driver (0.3.4) xpath (2.0.0) nokogiri (~> 1.3) + zipruby (0.3.6) PLATFORMS ruby DEPENDENCIES ago + always_verify_ssl_certificates bcrypt - capybara capybara_minitest_spec cocaine erubis @@ -228,12 +226,12 @@ DEPENDENCIES puma rack-recaptcha rack-test + rack_session_access rainbows rake redis rmagick ruby-debug - rubyzip sass screencap sequel (= 4.8.0) @@ -246,4 +244,4 @@ DEPENDENCIES sinatra-xsendfile stripe! tilt - webmock + zipruby diff --git a/Rakefile b/Rakefile index 9e80f27c..f2900d07 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ end desc "Run all tests" Rake::TestTask.new do |t| t.libs << "spec" - t.test_files = FileList['tests/*_tests.rb'] + t.test_files = FileList['tests/**/*_tests.rb'] t.verbose = true end diff --git a/app.rb b/app.rb index 1774496b..6e1feb2f 100644 --- a/app.rb +++ b/app.rb @@ -452,6 +452,13 @@ end get '/dashboard' do require_login + + if params[:dir] && params[:dir][0] != '/' + params[:dir] = '/'+params[:dir] + end + + @dir = params[:dir] + @file_list = current_site.file_list @dir erb :'dashboard' end @@ -562,7 +569,7 @@ post '/change_name' do end old_host = current_site.host - old_file_paths = current_site.file_list.collect {|f| f.filename} + old_file_paths = current_site.file_list.collect {|f| f[:path]} current_site.username = params[:name] @@ -629,7 +636,8 @@ def file_upload_response(error=nil) @error = error halt 200, erb(:'dashboard') else - redirect '/dashboard' + query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : '' + redirect "/dashboard#{query_string}" end else halt http_error_code, error if error @@ -637,6 +645,20 @@ def file_upload_response(error=nil) end end +post '/site/create_directory' do + require_login + + path = "#{params[:dir] || ''}/#{params[:name]}" + + result = current_site.create_directory path + + if result != true + flash[:error] = e.message + end + + redirect "/dashboard?dir=#{Rack::Utils.escape params[:dir]}" +end + post '/site_files/upload' do require_login @errors = [] @@ -647,12 +669,12 @@ post '/site_files/upload' do end params[:files].each do |file| + file[:filename] = "#{params[:dir]}/#{file[:filename]}" if params[:dir] if current_site.file_size_too_large? file[:tempfile].size - file_upload_response "#{file[:filename]} is too large, upload cancelled." + file_upload_response "#{params[:dir]}/#{file[:filename]} is too large, upload cancelled." end - if !Site.valid_file_type? file - file_upload_response "#{file[:filename]}: file type (or content in file) is not allowed on Neocities, upload cancelled." + file_upload_response "#{params[:dir]}/#{file[:filename]}: file type (or content in file) is not allowed on Neocities, upload cancelled." end end @@ -664,7 +686,7 @@ post '/site_files/upload' do results = [] params[:files].each do |file| - results << current_site.store_file(Site.sanitize_filename(file[:filename]), file[:tempfile]) + results << current_site.store_file(file[:filename], file[:tempfile]) end current_site.increment_changed_count if results.include?(true) @@ -674,20 +696,18 @@ end post '/site_files/delete' do require_login - sanitized_filename = Site.sanitize_filename params[:filename] + current_site.delete_file params[:filename] - current_site.delete_file(sanitized_filename) - - flash[:success] = "Deleted file #{params[:filename]}." + flash[:success] = "Deleted #{params[:filename]}." redirect '/dashboard' end get '/site_files/:username.zip' do |username| require_login - zipfile = current_site.files_zip + zipfile_path = current_site.files_zip content_type 'application/octet-stream' - attachment "#{current_site.username}.zip" - zipfile + attachment "neocities-#{current_site.username}.zip" + send_file zipfile_path end get '/site_files/download/:filename' do |filename| @@ -722,9 +742,7 @@ post '/site_files/save/:filename' do |filename| halt 'File is too large to fit in your space, it has NOT been saved. Please make a local copy and then try to reduce the size.' end - sanitized_filename = Site.sanitize_filename filename - - current_site.store_file sanitized_filename, tempfile + current_site.store_file filename, tempfile 'ok' end @@ -937,14 +955,11 @@ end post '/api/upload' do require_api_credentials - files = [] - params.each do |k,v| next unless v.is_a?(Hash) && v[:tempfile] - filename = k.to_s - api_error(400, 'bad_filename', "#{filename} is not a valid filename, files not uploaded") unless Site.valid_filename? filename - files << {filename: filename, tempfile: v[:tempfile]} + path = k.to_s + files << {filename: k || v[:filename], tempfile: v[:tempfile]} end api_error 400, 'missing_files', 'you must provide files to upload' if files.empty? @@ -959,6 +974,10 @@ post '/api/upload' do if !Site.valid_file_type?(file) api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content), files have not been uploaded" end + + if File.directory? file[:filename] + api_error 400, 'directory_exists', 'this name is being used by a directory, cannot continue' + end end results = [] @@ -976,26 +995,25 @@ post '/api/delete' do api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty? - filenames = [] - - params[:filenames].each do |filename| - unless filename.is_a?(String) && Site.valid_filename?(filename) - api_error 400, 'bad_filename', "#{filename} is not a valid filename, canceled deleting" + paths = [] + params[:filenames].each do |path| + unless path.is_a?(String) && Site.valid_path?(path) + api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting" end - if !current_site.file_exists?(filename) - api_error 400, 'missing_files', "#{filename} was not found on your site, canceled deleting" + if !current_site.file_exists?(path) + api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting" end - if filename == 'index.html' + if path == 'index.html' api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting' end - filenames << filename + paths << path end - filenames.each do |filename| - current_site.delete_file(filename) + paths.each do |path| + current_site.delete_file(path) end api_success 'file(s) have been deleted' diff --git a/environment.rb b/environment.rb index 10385a8f..e7f5c8ef 100644 --- a/environment.rb +++ b/environment.rb @@ -7,7 +7,6 @@ Encoding.default_external = 'UTF-8' require 'yaml' require 'json' require 'logger' -require 'zip' Bundler.require Bundler.require :development if ENV['RACK_ENV'] == 'development' diff --git a/models/site.rb b/models/site.rb index 11e3e0f2..8dc98596 100644 --- a/models/site.rb +++ b/models/site.rb @@ -1,6 +1,7 @@ require 'tilt' require 'rss' require 'nokogiri' +require 'pathname' class Site < Sequel::Model include Sequel::ParanoidDelete @@ -23,6 +24,7 @@ class Site < Sequel::Model image/x-icon application/pdf application/pgp-keys + application/pgp text/xml application/xml audio/midi @@ -70,6 +72,8 @@ class Site < Sequel::Model EMAIL_SANITY_REGEX = /.+@.+\..+/i + EDITABLE_FILE_EXT = /html|htm|txt|js|css|md/i + BANNED_TIME = 2592000 # 30 days in seconds TITLE_MAX = 100 @@ -197,16 +201,16 @@ class Site < Sequel::Model FileUtils.mkdir_p files_path %w{index not_found}.each do |name| - File.write file_path("#{name}.html"), render_template("#{name}.erb") + File.write files_path("#{name}.html"), render_template("#{name}.erb") purge_cache "#{name}.html" ScreenshotWorker.perform_async values[:username], "#{name}.html" end - FileUtils.cp template_file_path('cat.png'), file_path('cat.png') + FileUtils.cp template_file_path('cat.png'), files_path('cat.png') end - def get_file(filename) - File.read file_path(filename) + def get_file(path) + File.read files_path(path) end def before_destroy @@ -252,8 +256,8 @@ class Site < Sequel::Model FileUtils.mv files_path, File.join(PUBLIC_ROOT, 'banned_sites', username) } - site_files.file_list.collect {|f| f.filename}.each do |f| - purge_cache f + file_list.each do |path| + purge_cache path end end @@ -290,15 +294,11 @@ class Site < Sequel::Model !@blockings.select {|b| b.site_id == site.id}.empty? end - def self.valid_filename?(filename) - return false if sanitize_filename(filename) != filename + def self.valid_path?(path) + puts 'ditto restrictions scrub' true end - def self.sanitize_filename(filename) - filename.gsub(/[^a-zA-Z0-9_\-.]/, '') - end - def self.valid_username?(username) !username.empty? && username.match(/^[a-zA-Z0-9_\-]+$/i) end @@ -331,19 +331,21 @@ class Site < Sequel::Model true end - def purge_cache(filename) - payload = {site: username, path: filename} + def purge_cache(path) + payload = {site: username, path: path} payload[:domain] = domain if !domain.empty? PurgeCacheWorker.perform_async payload end - def store_file(filename, uploaded) - if File.exist?(file_path(filename)) && - Digest::SHA2.file(file_path(filename)).digest == Digest::SHA2.file(uploaded.path).digest + def store_file(path, uploaded) + path = files_path(path) + if File.exist?(path) && + Digest::SHA2.file(path).digest == Digest::SHA2.file(uploaded.path).digest return false end - if filename == 'index.html' + pathname = Pathname(path) + if pathname.basename.to_s == 'index.html' new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text if new_title.length < TITLE_MAX @@ -352,20 +354,26 @@ class Site < Sequel::Model end end - FileUtils.mv uploaded.path, file_path(filename) - File.chmod(0640, file_path(filename)) + dirname = pathname.dirname.to_s - purge_cache filename - - ext = File.extname(filename).gsub(/^./, '') - - if ext.match HTML_REGEX - ScreenshotWorker.perform_async values[:username], filename - elsif ext.match IMAGE_REGEX - ThumbnailWorker.perform_async values[:username], filename + if !File.exists? dirname + FileUtils.mkdir_p dirname end - SiteChange.record self, filename + FileUtils.mv uploaded.path, path + File.chmod 0640, path + + purge_cache path + + ext = File.extname(path).gsub(/^./, '') + + if ext.match HTML_REGEX + ScreenshotWorker.perform_async values[:username], path + elsif ext.match IMAGE_REGEX + ThumbnailWorker.perform_async values[:username], path + end + + SiteChange.record self, path if self.site_changed != true self.site_changed = true @@ -375,6 +383,20 @@ class Site < Sequel::Model true end + def is_directory?(path) + File.directory? files_path(path) + end + + def create_directory(path) + relative_path = files_path path + if Dir.exists?(relative_path) || File.exist?(relative_path) + return 'Directory (or file) already exists.' + end + + FileUtils.mkdir_p relative_path + true + end + def increment_changed_count self.changed_count += 1 self.updated_at = Time.now @@ -382,49 +404,59 @@ class Site < Sequel::Model end def files_zip - file_path = "/tmp/neocities-site-#{username}.zip" + zip_name = "neocities-#{username}" - Zip::File.open(file_path, Zip::File::CREATE) do |zipfile| - file_list.collect {|f| f.filename}.each do |filename| - zipfile.add filename, file_path(filename) + tmpfile = Tempfile.new 'neocities-site-zip' + tmpfile.close + + Zip::Archive.open(tmpfile.path, Zip::CREATE) do |ar| + ar.add_dir(zip_name) + + Dir.glob("#{base_files_path}/**/*").each do |path| + relative_path = path.gsub(base_files_path+'/', '') + + if File.directory?(path) + ar.add_dir(zip_name+'/'+relative_path) + else + ar.add_file(zip_name+'/'+relative_path, path) # add_file(, ) + end end end - # TODO Don't dump the zipfile into memory - zipfile = File.read file_path - File.delete file_path - zipfile + tmpfile.path end - def delete_file(filename) + def delete_file(path) begin - FileUtils.rm file_path(filename) + FileUtils.rm files_path(path) + rescue Errno::EISDIR + FileUtils.remove_dir files_path(path), true rescue Errno::ENOENT end - purge_cache filename + purge_cache path - ext = File.extname(filename).gsub(/^./, '') + ext = File.extname(path).gsub(/^./, '') - screenshots_delete(filename) if ext.match HTML_REGEX - thumbnails_delete(filename) if ext.match IMAGE_REGEX + screenshots_delete(path) if ext.match HTML_REGEX + thumbnails_delete(path) if ext.match IMAGE_REGEX - SiteChangeFile.filter(site_id: self.id, filename: filename).delete + SiteChangeFile.filter(site_id: self.id, filename: path).delete true end def move_files_from(oldusername) - FileUtils.mv files_path(oldusername), files_path + FileUtils.mv base_files_path(oldusername), base_files_path end - def install_new_html_file(name) - File.write file_path(name), render_template('index.erb') - purge_cache name + def install_new_html_file(path) + File.write files_path(path), render_template('index.erb') + purge_cache path end - def file_exists?(filename) - File.exist? file_path(filename) + def file_exists?(path) + File.exist? files_path(path) end def after_save @@ -453,7 +485,7 @@ class Site < Sequel::Model end # def after_destroy -# FileUtils.rm_rf file_path +# FileUtils.rm_rf files_path # super # end @@ -568,16 +600,48 @@ class Site < Sequel::Model File.join TEMPLATE_ROOT, name end - def files_path(name=nil) - File.join SITE_FILES_ROOT, (name || username) + def base_files_path(name=username) + raise 'username missing' if name.nil? || name.empty? + File.join SITE_FILES_ROOT, name end - def file_path(filename) - File.join files_path, filename + # https://practicingruby.com/articles/implementing-an-http-file-server?u=dc2ab0f9bb + def scrubbed_path(path='') + path ||= '' + clean = [] + + parts = path.split '/' + + parts.each do |part| + next if part.empty? || part == '.' + clean << part if part != '..' + end + + clean end - def file_list - Dir.glob(File.join(files_path, '*')).collect {|p| File.basename(p)}.sort.collect {|sitename| SiteFile.new sitename} + def files_path(path='') + File.join base_files_path, scrubbed_path(path) + end + + def file_list(path='') + list = Dir.glob(File.join(files_path(path), '*')).collect do |file_path| + file = { + path: file_path.gsub(base_files_path, ''), + name: File.basename(file_path), + ext: File.extname(file_path).gsub('.', ''), + is_directory: File.directory?(file_path), + is_root_index: file_path == "#{base_files_path}/index.html" + } + + file[:is_html] = !(file[:ext].match HTML_REGEX).nil? + file[:is_image] = !(file[:ext].match IMAGE_REGEX).nil? + file[:is_editable] = !(file[:ext].match EDITABLE_FILE_EXT).nil? + file + end + + list.select {|f| f[:is_directory]}.sort_by {|f| f[:name]} + + list.select {|f| f[:is_directory] == false}.sort_by{|f| f[:name]} end def file_size_too_large?(size_in_bytes) @@ -658,19 +722,19 @@ class Site < Sequel::Model values[:hits].to_s.reverse.gsub(/...(?=.)/,'\&,').reverse end - def screenshots_delete(filename) + def screenshots_delete(path) SCREENSHOT_RESOLUTIONS.each do |res| begin - FileUtils.rm screenshot_path(filename, res) + FileUtils.rm screenshot_path(path, res) rescue Errno::ENOENT end end end - def thumbnails_delete(filename) + def thumbnails_delete(path) THUMBNAIL_RESOLUTIONS.each do |res| begin - FileUtils.rm thumbnail_path(filename, res) + FileUtils.rm thumbnail_path(path, res) rescue Errno::ENOENT end end @@ -680,34 +744,34 @@ class Site < Sequel::Model Site.where(tags: tags).limit(limit, offset).order(:updated_at.desc).all end - def screenshot_path(filename, resolution) - File.join(SCREENSHOTS_ROOT, values[:username], "#{filename}.#{resolution}.jpg") + def screenshot_path(path, resolution) + File.join(SCREENSHOTS_ROOT, values[:username], "#{path}.#{resolution}.jpg") end - def screenshot_exists?(filename, resolution) - File.exist? File.join(SCREENSHOTS_ROOT, values[:username], "#{filename}.#{resolution}.jpg") + def screenshot_exists?(path, resolution) + File.exist? File.join(SCREENSHOTS_ROOT, values[:username], "#{path}.#{resolution}.jpg") end - def screenshot_url(filename, resolution) - "#{SCREENSHOTS_URL_ROOT}/#{values[:username]}/#{filename}.#{resolution}.jpg" + def screenshot_url(path, resolution) + "#{SCREENSHOTS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.jpg" end - def thumbnail_path(filename, resolution) - ext = File.extname(filename).gsub('.', '').match(LOSSY_IMAGE_REGEX) ? 'jpg' : 'png' - File.join THUMBNAILS_ROOT, values[:username], "#{filename}.#{resolution}.#{ext}" + def thumbnail_path(path, resolution) + ext = File.extname(path).gsub('.', '').match(LOSSY_IMAGE_REGEX) ? 'jpg' : 'png' + File.join THUMBNAILS_ROOT, values[:username], "#{path}.#{resolution}.#{ext}" end - def thumbnail_exists?(filename, resolution) - File.exist? thumbnail_path(filename, resolution) + def thumbnail_exists?(path, resolution) + File.exist? thumbnail_path(path, resolution) end - def thumbnail_delete(filename, resolution) - File.rm thumbnail_path(filename, resolution) + def thumbnail_delete(path, resolution) + File.rm thumbnail_path(path, resolution) end - def thumbnail_url(filename, resolution) - ext = File.extname(filename).gsub('.', '').match(LOSSY_IMAGE_REGEX) ? 'jpg' : 'png' - "#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{filename}.#{resolution}.#{ext}" + def thumbnail_url(path, resolution) + ext = File.extname(path).gsub('.', '').match(LOSSY_IMAGE_REGEX) ? 'jpg' : 'png' + "#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}" end def to_rss diff --git a/models/site_file.rb b/models/site_file.rb deleted file mode 100644 index ac280f17..00000000 --- a/models/site_file.rb +++ /dev/null @@ -1,8 +0,0 @@ -class SiteFile - attr_reader :filename, :ext - - def initialize(filename) - @filename = filename - @ext = File.extname(@filename).sub(/^./, '') - end -end \ No newline at end of file diff --git a/tests/acceptance/dashboard_tests.rb b/tests/acceptance/dashboard_tests.rb new file mode 100644 index 00000000..cf4b60a6 --- /dev/null +++ b/tests/acceptance/dashboard_tests.rb @@ -0,0 +1,26 @@ +require_relative './environment.rb' + +describe 'dashboard' do + describe 'create directory' do + + describe 'logged in' do + + include Capybara::DSL + + before do + Capybara.reset_sessions! + @site = Fabricate :site + page.set_rack_session id: @site.id + end + + it 'creates a base directory' do + visit '/dashboard' + click_link 'New Folder' + fill_in 'name', with: 'testimages' + click_button 'Create' + page.must_have_content /testimages/ + File.directory?(@site.files_path('testimages')).must_equal true + end + end + end +end \ No newline at end of file diff --git a/tests/acceptance/environment.rb b/tests/acceptance/environment.rb new file mode 100644 index 00000000..b0f3379b --- /dev/null +++ b/tests/acceptance/environment.rb @@ -0,0 +1,7 @@ +require_relative '../environment' + +Capybara.app = Sinatra::Application + +def teardown + Capybara.reset_sessions! +end \ No newline at end of file diff --git a/tests/acceptance/index_tests.rb b/tests/acceptance/index_tests.rb new file mode 100644 index 00000000..70b8a7ac --- /dev/null +++ b/tests/acceptance/index_tests.rb @@ -0,0 +1,11 @@ +require_relative './environment.rb' + +describe 'index' do + include Capybara::DSL + it 'goes to signup' do + Capybara.reset_sessions! + visit '/' + click_button 'Create My Website' + page.must_have_content('Create a New Website') + end +end \ No newline at end of file diff --git a/tests/acceptance/settings_tests.rb b/tests/acceptance/settings_tests.rb new file mode 100644 index 00000000..a425ee74 --- /dev/null +++ b/tests/acceptance/settings_tests.rb @@ -0,0 +1,44 @@ +require_relative './environment.rb' + +describe 'site/settings' do + describe 'change username' do + include Capybara::DSL + + def visit_signup + visit '/' + click_button 'Create My Website' + end + + def fill_in_valid + @site = Fabricate.attributes_for(:site) + fill_in 'username', with: @site[:username] + fill_in 'password', with: @site[:password] + fill_in 'email', with: @site[:email] + end + + before do + Capybara.reset_sessions! + visit_signup + end + + it 'does not allow bad usernames' do + visit '/' + click_button 'Create My Website' + fill_in_valid + click_button 'Create Home Page' + visit '/settings' + fill_in 'name', with: '' + click_button 'Change Name' + fill_in 'name', with: '../hack' + click_button 'Change Name' + fill_in 'name', with: 'derp../hack' + click_button 'Change Name' + ## TODO fix this without screwing up legacy sites + #fill_in 'name', with: '-' + #click_button 'Change Name' + page.must_have_content /valid.+name.+required/i + Site[username: @site[:username]].wont_equal nil + Site[username: ''].must_equal nil + end + end +end \ No newline at end of file diff --git a/tests/acceptance/signin_tests.rb b/tests/acceptance/signin_tests.rb new file mode 100644 index 00000000..78e13e46 --- /dev/null +++ b/tests/acceptance/signin_tests.rb @@ -0,0 +1,53 @@ +require_relative './environment.rb' + +describe 'signin' do + include Capybara::DSL + + def fill_in_valid + @site = Fabricate.attributes_for :site + fill_in 'username', with: @site[:username] + fill_in 'password', with: @site[:password] + end + + def fill_in_valid_signup + fill_in_valid + fill_in 'email', with: @site[:email] + end + + before do + Capybara.reset_sessions! + end + + it 'fails for invalid login' do + visit '/' + click_link 'Sign In' + page.must_have_content 'Welcome Back' + fill_in_valid + click_button 'Sign In' + page.must_have_content 'Invalid login' + end + + it 'fails for missing login' do + visit '/' + click_link 'Sign In' + auth = {username: SecureRandom.hex, password: Faker::Internet.password} + fill_in 'username', with: auth[:username] + fill_in 'password', with: auth[:password] + click_button 'Sign In' + page.must_have_content 'Invalid login' + end + + it 'logs in with proper credentials' do + visit '/' + click_button 'Create My Website' + fill_in_valid_signup + click_button 'Create Home Page' + Capybara.reset_sessions! + visit '/' + click_link 'Sign In' + fill_in 'username', with: @site[:username] + fill_in 'password', with: @site[:password] + click_button 'Sign In' + page.must_have_content 'Your Feed' + end +end \ No newline at end of file diff --git a/tests/acceptance_tests.rb b/tests/acceptance/signup_tests.rb similarity index 63% rename from tests/acceptance_tests.rb rename to tests/acceptance/signup_tests.rb index 2f58fe01..61ab5db5 100644 --- a/tests/acceptance_tests.rb +++ b/tests/acceptance/signup_tests.rb @@ -1,61 +1,4 @@ -require_relative './environment' - -Capybara.app = Sinatra::Application - -def teardown - Capybara.reset_sessions! - Capybara.use_default_driver -end - -describe 'index' do - include Capybara::DSL - it 'goes to signup' do - visit '/' - click_button 'Create My Website' - page.must_have_content('Create a New Website') - end -end - -describe 'change username' do - include Capybara::DSL - - def visit_signup - visit '/' - click_button 'Create My Website' - end - - def fill_in_valid - @site = Fabricate.attributes_for(:site) - fill_in 'username', with: @site[:username] - fill_in 'password', with: @site[:password] - fill_in 'email', with: @site[:email] - end - - before do - Capybara.reset_sessions! - visit_signup - end - - it 'does not allow bad usernames' do - visit '/' - click_button 'Create My Website' - fill_in_valid - click_button 'Create Home Page' - visit '/settings' - fill_in 'name', with: '' - click_button 'Change Name' - fill_in 'name', with: '../hack' - click_button 'Change Name' - fill_in 'name', with: 'derp../hack' - click_button 'Change Name' - ## TODO fix this without screwing up legacy sites - #fill_in 'name', with: '-' - #click_button 'Change Name' - page.must_have_content /valid.+name.+required/i - Site[username: @site[:username]].wont_equal nil - Site[username: ''].must_equal nil - end -end +require_relative './environment.rb' describe 'signup' do include Capybara::DSL @@ -215,56 +158,4 @@ describe 'signup' do page.must_have_content 'Your Feed' Site.last.tags.collect {|t| t.name}.must_equal ['derpie', 'shoujo'] end -end - -describe 'signin' do - include Capybara::DSL - - def fill_in_valid - @site = Fabricate.attributes_for :site - fill_in 'username', with: @site[:username] - fill_in 'password', with: @site[:password] - end - - def fill_in_valid_signup - fill_in_valid - fill_in 'email', with: @site[:email] - end - - before do - Capybara.reset_sessions! - end - - it 'fails for invalid login' do - visit '/' - click_link 'Sign In' - page.must_have_content 'Welcome Back' - fill_in_valid - click_button 'Sign In' - page.must_have_content 'Invalid login' - end - - it 'fails for missing login' do - visit '/' - click_link 'Sign In' - auth = {username: SecureRandom.hex, password: Faker::Internet.password} - fill_in 'username', with: auth[:username] - fill_in 'password', with: auth[:password] - click_button 'Sign In' - page.must_have_content 'Invalid login' - end - - it 'logs in with proper credentials' do - visit '/' - click_button 'Create My Website' - fill_in_valid_signup - click_button 'Create Home Page' - Capybara.reset_sessions! - visit '/' - click_link 'Sign In' - fill_in 'username', with: @site[:username] - fill_in 'password', with: @site[:password] - click_button 'Sign In' - page.must_have_content 'Your Feed' - end -end +end \ No newline at end of file diff --git a/tests/api_tests.rb b/tests/api_tests.rb index a6bb491a..07966838 100644 --- a/tests/api_tests.rb +++ b/tests/api_tests.rb @@ -84,17 +84,17 @@ describe 'api delete' do res[:error_type].must_equal 'cannot_delete_index' end - it 'fails with bad filename' do + it 'succeeds with weird filenames' do create_site basic_authorize @user, @pass @site.store_file 't$st.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') post '/api/delete', filenames: ['t$st.jpg'] - res[:error_type].must_equal 'bad_filename' + res[:result].must_equal 'success' create_site basic_authorize @user, @pass post '/api/delete', filenames: ['./config.yml'] - res[:error_type].must_equal 'bad_filename' + res[:error_type].must_equal 'missing_files' end it 'fails with missing files' do @@ -137,13 +137,59 @@ describe 'api upload' do res[:error_type].must_equal 'missing_files' end - it 'fails for invalid filenames' do + it 'resists directory traversal attack' do create_site basic_authorize @user, @pass post '/api/upload', { '../lol.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') } - res[:error_type].must_equal 'bad_filename' + res[:result].must_equal 'success' + File.exist?(File.join(Site::SITE_FILES_ROOT, @site.username, 'lol.jpg')).must_equal true + end + + it 'scrubs root path slash' do + create_site + basic_authorize @user, @pass + post '/api/upload', { + '/lol.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:result].must_equal 'success' + File.exist?(File.join(Site::SITE_FILES_ROOT, @site.username, 'lol.jpg')).must_equal true + end + + it 'fails for missing file name' do + create_site + basic_authorize @user, @pass + post '/api/upload', { + '/' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:error_type].must_equal 'invalid_file_type' + + create_site + basic_authorize @user, @pass + post '/api/upload', { + '' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:error_type].must_equal 'missing_files' + end + + it 'fails for file with no extension' do + create_site + basic_authorize @user, @pass + post '/api/upload', { + 'derpie' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:error_type].must_equal 'invalid_file_type' + end + + it 'creates path for file uploads' do + create_site + basic_authorize @user, @pass + post '/api/upload', { + 'derpie/derpingtons/lol.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:result].must_equal 'success' + File.exist?(@site.files_path('derpie/derpingtons/lol.jpg')).must_equal true end it 'fails for invalid files' do @@ -180,7 +226,7 @@ describe 'api upload' do end def site_file_exists?(file) - File.exist?(@site.file_path('test.jpg')) + File.exist?(@site.files_path('test.jpg')) end def res diff --git a/tests/environment.rb b/tests/environment.rb index 0ad4794a..ad3cb866 100644 --- a/tests/environment.rb +++ b/tests/environment.rb @@ -9,18 +9,23 @@ end SimpleCov.command_name 'minitest' +require 'rack_session_access' require './environment' -require 'webmock' -include WebMock::API require './app' Bundler.require :test #require 'minitest/pride' require 'minitest/autorun' - require 'sidekiq/testing' +Sinatra::Application.configure do |app| + app.use RackSessionAccess::Middleware +end + +require 'capybara/poltergeist' +require 'rack_session_access/capybara' + Site.bcrypt_cost = BCrypt::Engine::MIN_COST MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new diff --git a/tests/site_file_tests.rb b/tests/site_file_tests.rb new file mode 100644 index 00000000..0904df23 --- /dev/null +++ b/tests/site_file_tests.rb @@ -0,0 +1,33 @@ +require_relative './environment.rb' +require 'rack/test' + +include Rack::Test::Methods + +def app + Sinatra::Application +end + +describe 'site_files' do + describe 'upload' do + it 'succeeds with valid file' do + site = Fabricate :site + post '/site_files/upload', { + 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'csrf_token' => 'abcd' + }, {'rack.session' => { 'id' => site.id, '_csrf_token' => 'abcd' }} + last_response.body.must_match /successfully uploaded/i + File.exists?(site.files_path('test.jpg')).must_equal true + end + + it 'works with directory path' do + site = Fabricate :site + post '/site_files/upload', { + 'dir' => 'derpie/derptest', + 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'csrf_token' => 'abcd' + }, {'rack.session' => { 'id' => site.id, '_csrf_token' => 'abcd' }} + last_response.body.must_match /successfully uploaded/i + File.exists?(site.files_path('derpie/derptest/test.jpg')).must_equal true + end + end +end \ No newline at end of file diff --git a/views/dashboard.erb b/views/dashboard.erb index 28843acd..bb3cdeae 100644 --- a/views/dashboard.erb +++ b/views/dashboard.erb @@ -15,14 +15,6 @@ display: none; } -
@@ -88,10 +80,27 @@
- +
@@ -99,47 +108,49 @@
-
- <% current_site.file_list.each do |file| %> +
+ <% @file_list.each do |file| %>
- <% if file.ext.match(Site::HTML_REGEX) && current_site.screenshot_exists?(file.filename, '105x63') %> + <% if file[:is_html] && current_site.screenshot_exists?(file[:path], '105x63') %>
- +
- <% elsif file.ext.match(Site::IMAGE_REGEX) && current_site.thumbnail_exists?(file.filename, '105x63') %> + <% elsif file[:is_image] && current_site.thumbnail_exists?(file[:path], '105x63') %>
- +
- <% else %>
-
<%= file.ext %>
+
<%= file[:ext] %>
<% end %> - <% if file.filename.length > 14 %> - <%= file.filename.slice(0..14) %>… + <% if file[:name].length > 15 %> + <%= file[:name].slice(0..14) %>… <% else %> - <%= file.filename %> + <%= file[:name] %> <% end %>
- <% if file.ext.match(/html|htm|txt|js|css|md/) %> - Edit + <% if file[:is_editable] %> + Edit <% end %> - <% if file.filename != 'index.html' %> - Delete + <% if file[:is_directory] %> + Manage <% end %> - + <% if !file[:is_root_index] %> + Delete + <% end %> +
<% end %> @@ -158,7 +169,7 @@

Confirm deletion

- + + @@ -231,3 +243,24 @@ } } + + \ No newline at end of file