Merge pull request #497 from neocities/fileuploadrefactor

File Upload Refactor
This commit is contained in:
Kyle Drake 2024-03-11 11:44:12 -05:00 committed by GitHub
commit d506f6cb3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 704 additions and 647 deletions

View file

@ -42,23 +42,56 @@ get '/api/list' do
end
def extract_files(params, 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?(Hash) || value.is_a?(Array)
# If the value is a Hash or Array, recursively search for more files.
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 }
@ -68,16 +101,28 @@ post '/api/upload' do
end
if current_site.too_many_files?(files.length)
api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)}), #{current_site.supporter? ? 'please contact support' : 'please upgrade to a supporter account'}"
api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)})"
end
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 allowed file type for free sites, supporter required"
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 +236,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

View file

@ -8,7 +8,7 @@ get '/dashboard' do
current_site.save_changes validate: false
end
erb :'dashboard'
erb :'dashboard/index'
end
def dashboard_init
@ -30,3 +30,11 @@ def dashboard_init
@dir = params[:dir]
@file_list = current_site.file_list @dir
end
get '/dashboard/files' do
require_login
dashboard_init
dont_browser_cache
erb :'dashboard/files', layout: false
end

View file

@ -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 <a href="/supporter">supporter accounts</a>. <a href="/site_files/allowed_types">Why We Do This</a>}
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]
@ -246,7 +177,16 @@ get %r{\/site_files\/text_editor\/(.+)} do
dont_browser_cache
@filename = params[:captures].first
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
end
get '/site_files/text_editor' do
require_login
dont_browser_cache
@filename = params[:filename]
extname = File.extname @filename
@ace_mode = case extname
when /htm|html/ then 'html'
when /js/ then 'javascript'

View file

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

View file

@ -84,7 +84,7 @@ class Site < Sequel::Model
SCREENSHOT_RESOLUTIONS = ['540x405', '210x158', '100x100', '50x50']
THUMBNAIL_RESOLUTIONS = ['210x158']
MAX_FILE_SIZE = 10**8 # 100 MB
MAX_FILE_SIZE = 10**8 # 100 MB, change dashboard.js dropzone file size limit if you change this
CLAMAV_THREAT_MATCHES = [
/^VBS/,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

58
public/img/drag-drop.svg Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="67.754738mm"
height="3.4416525mm"
viewBox="0 0 67.754738 3.4416525"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="drag-drop.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="3.1108586"
inkscape:cx="71.523663"
inkscape:cy="-55.772384"
inkscape:window-width="2490"
inkscape:window-height="1376"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-23.11286,-40.759304)">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52778px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="22.954386"
y="43.43959"
id="text82970"><tspan
sodipodi:role="line"
id="tspan82968"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#f6f0e6;fill-opacity:1;stroke-width:0.264583"
x="22.954386"
y="43.43959">drag and drop files here to upload</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

157
public/js/dashboard.js Normal file
View file

@ -0,0 +1,157 @@
if(localStorage && localStorage.getItem('viewType') == 'list')
$('#filesDisplay').addClass('list-view')
function confirmFileRename(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')
}
$('#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')
}
function alertAdd(text) {
var a = $('#alertDialogue');
a.css('display', 'block');
a.append(text+'<br>');
}
function alertClear(){
var a = $('#alertDialogue');
a.css('display', 'none');
a.text('');
}
function alertType(type){
var a = $('#alertDialogue');
a.removeClass('alert-success');
a.removeClass('alert-error');
a.addClass('alert-'+type);
}
var processedFiles = 0;
var uploadedFiles = 0;
var uploadedFileErrors = 0;
function joinPaths(...paths) {
return paths
.map(path => path.replace(/(^\/|\/$)/g, ''))
.filter(path => path !== '')
.join('/');
}
function reInitDashboardFiles() {
new Dropzone("#uploads", {
url: "/api/upload",
paramName: 'file',
dictDefaultMessage: "",
uploadMultiple: false,
parallelUploads: 1,
maxFilesize: 104857600, // 100MB
clickable: document.getElementById('uploadButton'),
init: function() {
this.on("processing", function(file) {
var dir = $('#uploads input[name="dir"]').val();
if(file.fullPath) {
this.options.paramName = joinPaths(dir,file.fullPath);
} else {
this.options.paramName = joinPaths(dir, file.name);
}
processedFiles++;
$('#uploadFileName').text(this.options.paramName).prepend('<i class="icon-file"></i> ');
});
this.on("success", function(file) {
uploadedFiles++;
});
this.on("error", function(file, message) {
uploadedFiles++;
uploadedFileErrors++;
alertType('error');
if (message && message.message) {
alertAdd(message.message);
} else {
alertAdd(this.options.paramName+' failed to upload');
}
});
this.on("queuecomplete", function() {
hideUploadProgress();
if(uploadedFileErrors > 0) {
alertType('error');
alertAdd(uploadedFiles-uploadedFileErrors+'/'+uploadedFiles+' files uploaded successfully');
} else {
alertType('success');
alertAdd(uploadedFiles+' files uploaded successfully');
}
reloadDashboardFiles();
});
this.on("addedfiles", function(files) {
uploadedFiles = 0;
uploadedFileErrors = 0;
alertClear();
showUploadProgress();
});
}
});
document.getElementById('uploadButton').addEventListener('click', function(event) {
event.preventDefault();
});
}
function reloadDashboardFiles() {
var dir = $('#uploads input[name="dir"]').val();
$.get('/dashboard/files?dir='+encodeURIComponent(dir), function(data) {
$('#filesDisplay').html(data);
reInitDashboardFiles();
});
}
// for first time load
reInitDashboardFiles();

2
public/js/dropzone-min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
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="<xyz></xyz>";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<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);

View file

@ -60,7 +60,7 @@ a{
@import 'project-Footer'; // Project Specific Footer Styling
.alert-error {
background-color:#F5BA00; color:#fff;
background-color:#f39c12; color:#fff;
}
.modal {

View file

@ -352,7 +352,7 @@
min-height: 300px;
}
.files .list .upload-Boundary.with-instruction {
background: url(/img/drag-drop.png) no-repeat center center;
background: url(/img/drag-drop.svg) no-repeat center center;
@media (max-device-width:480px), screen and (max-width:800px) {
background: 0;
@ -370,7 +370,7 @@
font-style: italic;
margin-left: auto;
margin-right: auto;
width: 400px;
width: 90%;
margin-top: 14%;
padding: 25px 40px 28px 40px;
-webkit-box-shadow: 1px 1px 21px 5px rgba(50, 50, 50, 0.5);

View file

@ -308,6 +308,18 @@ describe 'api' do
_(site_file_exists?('test.jpg')).must_equal true
end
it 'fails api_key auth unless controls site' do
create_site
@site.generate_api_key!
@other_site = Fabricate :site
header 'Authorization', "Bearer #{@site.api_key}"
post '/api/upload', 'test.jpg' => 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!

View file

@ -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)['error_type']).must_equal 'invalid_file_type'
_(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

View file

@ -1,419 +0,0 @@
<style>
.dz-default {
display: none;
}
.dz-preview {
display: none;
}
.dz-processing {
display: none;
}
.dz-error {
display: none;
}
.dz-image-preview {
display: none;
}
</style>
<div class="header-Outro with-site-image dashboard">
<div class="row content wide">
<div class="col col-50 signup-Area">
<div class="signup-Form">
<fieldset class="content">
<a href="<%= current_site.uri %>" class="screenshot dashboard" style="background-image:url(<%= current_site.screenshot_url('index.html', '540x405') %>);"></a>
</fieldset>
</div>
</div>
<div class="col col-50">
<h2 class="eps"><%= current_site.title %></h2>
<p class="site-url">
<a href="<%= current_site.uri %>" target="_blank"><%= current_site.host %></a>
<a href="#" id="shareButton" class="btn-Action" data-container="body" data-toggle="popover" data-placement="bottom" data-content='<%== erb :'_share', layout: false, locals: {site: current_site} %>'><i class="fa fa-share-alt"></i> <span>Share</span></a>
</p>
<ul>
<% if current_site.site_updated_at %>
<li>Last updated <%= current_site.site_updated_at.ago.downcase %></li>
<% end %>
<li>Using <strong><%= current_site.space_percentage_used %>% (<%= current_site.total_space_used.to_space_pretty %>) of your <%= current_site.maximum_space.to_space_pretty %></strong>.
<br>
<% unless current_site.is_education || current_site.supporter? %>Need more space? <a href="/supporter">Become a Supporter!</a><% end %></li>
<li><strong><%= current_site.views.format_large_number %></strong> views</li>
</ul>
</div>
</div> <!-- end .row -->
</div> <!-- end .header-Outro -->
<main class="content-Base" role="main">
<div class="content wide">
<% unless current_site.changed_count >= 1 %>
<div class="welcome">
<!-- <div class="close-button"></div> -->
<h4>Hello! Welcome to your new site.</h4>
To get started, click on the <strong>index.html</strong> file below to edit your home page. Once you make changes your website will begin appearing in our <a href="/browse">website gallery</a>. You can add more files (such as images) by dragging them from your computer into the box below. Need help building web sites? Try our <a href="/tutorial/html/">HTML tutorial</a>!
</div>
<% end %>
<%== flash_display %>
<div id="filesDisplay" class="files">
<script>
if(localStorage && localStorage.getItem('viewType') == 'list')
$('#filesDisplay').addClass('list-view')
</script>
<div id="uploadingOverlay" class="uploading-overlay" style="display: none">
<div class="uploading">
<p>Uploading, please wait...</p>
<div id="progressBar" class="progress-bar" style="display: none"><div id="uploadingProgress" class="progress" style="width: 0%"></div></div>
</div>
</div>
<div id="movingOverlay" class="uploading-overlay" style="display: none">
<div class="uploading">
<p>Moving file, please wait...</p>
</div>
</div>
<div class="header">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-default iconview-button" title="Icon View" onclick="iconView()"><i class="fa fa-th"></i></button>
<button type="button" class="btn btn-default listview-button" title="List View" onclick="listView()"><i class="fa fa-list"></i></button>
</div>
<div class="breadcrumbs">
<% if params[:dir].nil? || params[:dir].empty? || params[:dir] == '/' %>
Home
<% else %>
<a href="/dashboard">Home</a>
<% end %>
<% if @dir %>
<% dir_array = @dir.split '/' %>
<% dir_array.each_with_index do |dir,i| %>
<% if i+1 < dir_array.length %>
<a href="/dashboard?dir=<%= Rack::Utils.escape dir_array[1..i].join('/') %>"><%= dir %></a> <i class="fa fa-angle-right"></i>
<% else %>
<%= dir %>
<% end %>
<% end %>
<% end %>
</div>
<div class="actions">
<a href="#createFile" class="btn-Action" data-toggle="modal"><i class="fa fa-file"></i> New File</a>
<a href="#createDir" class="btn-Action" data-toggle="modal"><i class="fa fa-folder"></i> New Folder</a>
<a href="#" class="btn-Action" onclick="clickUploadFiles(); return false"><i class="fa fa-arrow-circle-up"></i> Upload</a>
</div>
</div>
<div class="list">
<form action="/site_files/upload" class="dropzone" id="uploads">
<div class="dz-message" style="display: none"></div>
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input name="dir" type="hidden" value="<%= @dir %>">
<div class="upload-Boundary <%= @file_list.length <= 5 ? 'with-instruction' : '' %>">
<% @file_list.each do |file| %>
<div class="file filehover">
<% if file[:is_html] && current_site.screenshot_exists?(file[:path], '210x158') %>
<div class="html-thumbnail html fileimagehover">
<img src="<%= current_site.screenshot_url(file[:path], '210x158') %>" alt="">
<div class="overlay"></div>
</div>
<% elsif file[:is_image] && current_site.thumbnail_exists?(file[:path], '210x158') %>
<div class="html-thumbnail image fileimagehover">
<img src="<%= current_site.thumbnail_url(file[:path], '210x158') %>" alt="">
<div class="overlay"></div>
</div>
<% elsif file[:is_directory] %>
<div class="html-thumbnail folder fileimagehover" ondrop="moveFileToFolder(event)">
<div class="folder-icon"></div>
<div class="overlay"></div>
</div>
<% else %>
<div class="html-thumbnail misc fileimagehover">
<div class="misc-icon"><%= file[:ext][0..3] %></div>
<div class="overlay"></div>
</div>
<% end %>
<a class="title">
<%= file[:name] %>
</a>
<div class="column size">
<% if file[:size] %>
<%= file[:size].to_bytes_pretty %>
<% end %>
</div>
<div class="column updated">
<% if file[:updated_at] %>
<%= file[:updated_at].ago %>
<% end %>
</div>
<div class="overlay">
<div id="<%= Digest::SHA256.hexdigest file[:path] %>" style="display: none"><%= file[:path] %></div>
<% if file[:is_editable] %>
<a href="/site_files/text_editor/<%= file[:path] %>"><i class="fa fa-edit" title="Edit"></i> Edit</a>
<% end %>
<% if file[:is_directory] %>
<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] %>
<a class="link-overlay" href="?dir=<%= Rack::Utils.escape file[:path] %>" title="View <%= file[:path] %>"></a>
<% else %>
<a class="link-overlay" href="<%= current_site.file_uri file[:path] %>" title="View <%= file[:path] == '/index.html' ? 'your site index' : file[:path] %>" target="_blank"></a>
<% end %>
</div>
</div>
<% end %>
</div>
</form>
</div>
</div>
<form method="POST" action="/site_files/delete" id="deleteFilenameForm">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input type="hidden" id="deleteFilenameInput" name="filename">
</form>
<div class="modal hide fade" id="deleteConfirmModal" tabindex="-1" role="dialog" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="deleteConfirmModalLabel">Confirm deletion</h3>
</div>
<div class="modal-body">
<p>You are about to delete <strong><span id="deleteFileName"></span></strong>. Are you sure?</p>
</div>
<div class="modal-footer">
<button class="btn cancel" data-dismiss="modal" aria-hidden="true" type="button">Cancel</button>
<button class="btn-Action btn-danger" type="button" onclick="fileDelete()"><i class="fa fa-trash" title="Delete"></i>Delete</button>
</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> |
<% end %>
<a href="/site_files/download">Download entire site</a> |
<% unless is_education? %>
<a href="/site_files/mount_info">Mount your site as a drive on your computer</a>
<% end %>
</div>
</div>
</main>
<form id="uploadFilesButtonForm" method="POST" action="/site_files/upload" enctype="multipart/form-data" style="display: none" onsubmit="showUploadProgress()">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input name="from_button" type="hidden" value="true">
<input name="dir" type="hidden" value="<%= @dir %>">
<input id="uploadFiles" type="file" name="files[]" multiple onchange="$('#uploadFilesButtonForm').submit()">
</form>
<div class="modal hide fade" id="createDir" tabindex="-1" role="dialog" aria-labelledby="createDirLabel" aria-hidden="true">
<form method="post" action="/site/create_directory">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createDirLabel">Create Folder</h3>
</div>
<div class="modal-body">
<input id="newDirInput" name="name" type="text" placeholder="folder_name">
</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">Create</button>
</div>
</form>
</div>
<div class="modal hide fade" id="createFile" tabindex="-1" role="dialog" aria-labelledby="createFileLabel" aria-hidden="true">
<form method="post" action="/site_files/create">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createFileLabel">Create New File</h3>
</div>
<div class="modal-body">
<input id="newFileInput" name="filename" type="text" placeholder="newfile.html">
<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">Create</button>
</div>
</form>
</div>
<script src="/js/dropzone.min.js"></script>
<script>
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')
}
allUploadsComplete = false
Dropzone.options.uploads = {
paramName: 'files',
maxFilesize: <%= current_site.remaining_space.to_mb %>,
clickable: false,
addRemoveLinks: false,
dictDefaultMessage: '',
uploadMultiple: true,
init: function() {
this.on("completemultiple", function(file) {
if(allUploadsComplete == true)
location.reload()
})
this.on("error", function(file, errorMessage) {
hideUploadProgress()
// Guess a directory upload error
if(file.status == 'error' && file.name.match(/.+\..+/) == null && errorMessage == 'Server responded with 0 code.') {
alert('Recursive directory upload is only supported by the Chrome web browser.')
} else {
location.href = '/dashboard<%= @dir ? "?dir=#{Rack::Utils.escape @dir}" : "" %>'
}
})
this.on("totaluploadprogress", function(progress, totalBytes, totalBytesSent) {
if(progress == 100)
allUploadsComplete = true
showUploadProgress()
$('#progressBar').css('display', 'block')
$('#uploadingProgress').css('width', progress+'%')
})
this.on("sending", function(file) {
if(file.fullPath !== undefined)
$('#uploads').append('<input type="hidden" name="file_paths[]" value="'+file.fullPath+'">')
})
}
}
$('#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')
}
</script>

108
views/dashboard/files.erb Normal file
View file

@ -0,0 +1,108 @@
<div id="uploadingOverlay" class="uploading-overlay" style="display: none">
<div class="uploading">
<p>Uploading, please wait...</p>
<p id="uploadFileName"></p>
<!-- <div id="progressBar" class="progress-bar" style="display: none"><div id="uploadingProgress" class="progress" style="width: 0%"></div></div> -->
</div>
</div>
<div id="movingOverlay" class="uploading-overlay" style="display: none">
<div class="uploading">
<p>Moving file, please wait...</p>
</div>
</div>
<div class="header">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-default iconview-button" title="Icon View" onclick="iconView()"><i class="fa fa-th"></i></button>
<button type="button" class="btn btn-default listview-button" title="List View" onclick="listView()"><i class="fa fa-list"></i></button>
</div>
<div class="breadcrumbs">
<% if params[:dir].nil? || params[:dir].empty? || params[:dir] == '/' %>
Home
<% else %>
<a href="/dashboard">Home</a>
<% end %>
<% if @dir %>
<% dir_array = @dir.split '/' %>
<% dir_array.each_with_index do |dir,i| %>
<% if i+1 < dir_array.length %>
<a href="/dashboard?dir=<%= Rack::Utils.escape dir_array[1..i].join('/') %>"><%= dir %></a> <i class="fa fa-angle-right"></i>
<% else %>
<%= dir %>
<% end %>
<% end %>
<% end %>
</div>
<div class="actions">
<a href="#createFile" class="btn-Action" data-toggle="modal"><i class="fa fa-file"></i> New File</a>
<a href="#createDir" class="btn-Action" data-toggle="modal"><i class="fa fa-folder"></i> New Folder</a>
<a href="#" id="uploadButton" class="btn-Action"><i class="fa fa-arrow-circle-up"></i> Upload</a>
</div>
</div>
<div class="list">
<form action="/site_files/upload" class="dropzone" id="uploads">
<div class="dz-message" style="display: none"></div>
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input name="dir" type="hidden" value="<%= @dir %>" id="dir">
<div class="upload-Boundary with-instruction">
<% @file_list.each do |file| %>
<div class="file filehover">
<% if file[:is_html] && current_site.screenshot_exists?(file[:path], '210x158') %>
<div class="html-thumbnail html fileimagehover">
<img src="<%= current_site.screenshot_url(file[:path], '210x158') %>" alt="">
<div class="overlay"></div>
</div>
<% elsif file[:is_image] && current_site.thumbnail_exists?(file[:path], '210x158') %>
<div class="html-thumbnail image fileimagehover">
<img src="<%= current_site.thumbnail_url(file[:path], '210x158') %>" alt="">
<div class="overlay"></div>
</div>
<% elsif file[:is_directory] %>
<div class="html-thumbnail folder fileimagehover" ondrop="moveFileToFolder(event)">
<div class="folder-icon"></div>
<div class="overlay"></div>
</div>
<% else %>
<div class="html-thumbnail misc fileimagehover">
<div class="misc-icon"><%= file[:ext][0..3] %></div>
<div class="overlay"></div>
</div>
<% end %>
<a class="title">
<%= file[:name] %>
</a>
<div class="column size">
<% if file[:size] %>
<%= file[:size].to_bytes_pretty %>
<% end %>
</div>
<div class="column updated">
<% if file[:updated_at] %>
<%= file[:updated_at].ago %>
<% end %>
</div>
<div class="overlay">
<div id="<%= Digest::SHA256.hexdigest file[:path] %>" style="display: none"><%= file[:path] %></div>
<% if file[:is_editable] %>
<a href="/site_files/text_editor?filename=<%= Rack::Utils.escape file[:path] %>"><i class="fa fa-edit" title="Edit"></i> Edit</a>
<% end %>
<% if file[:is_directory] %>
<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] %>
<a class="link-overlay" href="?dir=<%= Rack::Utils.escape file[:path] %>" title="View <%= file[:path] %>"></a>
<% else %>
<a class="link-overlay" href="<%= current_site.file_uri file[:path] %>" title="View <%= file[:path] == '/index.html' ? 'your site index' : file[:path] %>" target="_blank"></a>
<% end %>
</div>
</div>
<% end %>
</div>
</form>
</div>

173
views/dashboard/index.erb Normal file
View file

@ -0,0 +1,173 @@
<script src="/js/dropzone-min.js"></script>
<style>
.dz-default {
display: none;
}
.dz-preview {
display: none;
}
.dz-processing {
display: none;
}
.dz-error {
display: none;
}
.dz-image-preview {
display: none;
}
</style>
<div class="header-Outro with-site-image dashboard">
<div class="row content wide">
<div class="col col-50 signup-Area">
<div class="signup-Form">
<fieldset class="content">
<a href="<%= current_site.uri %>" class="screenshot dashboard" style="background-image:url(<%= current_site.screenshot_url('index.html', '540x405') %>);"></a>
</fieldset>
</div>
</div>
<div class="col col-50">
<h2 class="eps"><%= current_site.title %></h2>
<p class="site-url">
<a href="<%= current_site.uri %>" target="_blank"><%= current_site.host %></a>
<a href="#" id="shareButton" class="btn-Action" data-container="body" data-toggle="popover" data-placement="bottom" data-content='<%== erb :'_share', layout: false, locals: {site: current_site} %>'><i class="fa fa-share-alt"></i> <span>Share</span></a>
</p>
<ul>
<% if current_site.site_updated_at %>
<li>Last updated <%= current_site.site_updated_at.ago.downcase %></li>
<% end %>
<li>Using <strong><%= current_site.space_percentage_used %>% (<%= current_site.total_space_used.to_space_pretty %>) of your <%= current_site.maximum_space.to_space_pretty %></strong>.
<br>
<% unless current_site.is_education || current_site.supporter? %>Need more space? <a href="/supporter">Become a Supporter!</a><% end %></li>
<li><strong><%= current_site.views.format_large_number %></strong> views</li>
</ul>
</div>
</div> <!-- end .row -->
</div> <!-- end .header-Outro -->
<main class="content-Base" role="main">
<div class="content wide">
<% unless current_site.changed_count >= 1 %>
<div class="welcome">
<!-- <div class="close-button"></div> -->
<h4>Hello! Welcome to your new site.</h4>
To get started, click on the <strong>index.html</strong> file below to edit your home page. Once you make changes your website will begin appearing in our <a href="/browse">website gallery</a>. You can add more files (such as images) by dragging them from your computer into the box below. Need help building web sites? Try our <a href="/tutorial/html/">HTML tutorial</a>!
</div>
<% end %>
<% if flash.keys.length > 0 %>
<div id="alertDialogue" class="alert alert-block alert-<%= flash.keys.first %>" style="display: block; max-height: 200px; overflow-y: auto;">
<% flash.keys.select {|k| [:success, :error, :errors].include?(k)}.each do |key| %>
<%== flash[key] %>
<% end %>
</div>
<% else %>
<div id="alertDialogue" class="alert alert-block alert-<%= flash.keys.first || 'success' %>" style="display: none; max-height: 200px; overflow-y: auto;"></div>
<% end %>
<div id="filesDisplay" class="files">
<%== erb :'dashboard/files' %>
</div>
<form method="POST" action="/site_files/delete" id="deleteFilenameForm">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input type="hidden" id="deleteFilenameInput" name="filename">
</form>
<div class="modal hide fade" id="deleteConfirmModal" tabindex="-1" role="dialog" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="deleteConfirmModalLabel">Confirm deletion</h3>
</div>
<div class="modal-body">
<p>You are about to delete <strong><span id="deleteFileName"></span></strong>. Are you sure?</p>
</div>
<div class="modal-footer">
<button class="btn cancel" data-dismiss="modal" aria-hidden="true" type="button">Cancel</button>
<button class="btn-Action btn-danger" type="button" onclick="fileDelete()"><i class="fa fa-trash" title="Delete"></i>Delete</button>
</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" style="width: 100%">
<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> |
<% end %>
<a href="/site_files/download">Download entire site</a> |
<% unless is_education? %>
<a href="/site_files/mount_info">Mount your site as a drive on your computer</a>
<% end %>
</div>
</div>
</main>
<form id="uploadFilesButtonForm" method="POST" action="/api/upload" enctype="multipart/form-data" style="display: none" onsubmit="event.preventDefault(); showUploadProgress(); uploadFileFromButton();">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input name="from_button" type="hidden" value="true">
<input name="dir" type="hidden" id="dir" value="<%= @dir %>">
<input id="uploadFiles" type="file" name="file" multiple>
</form>
<div class="modal hide fade" id="createDir" tabindex="-1" role="dialog" aria-labelledby="createDirLabel" aria-hidden="true">
<form method="post" action="/site/create_directory">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createDirLabel">Create Folder</h3>
</div>
<div class="modal-body">
<input id="newDirInput" name="name" type="text" placeholder="folder_name">
</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">Create</button>
</div>
</form>
</div>
<div class="modal hide fade" id="createFile" tabindex="-1" role="dialog" aria-labelledby="createFileLabel" aria-hidden="true">
<form method="post" action="/site_files/create">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createFileLabel">Create New File</h3>
</div>
<div class="modal-body">
<input id="newFileInput" name="filename" type="text" placeholder="newfile.html">
<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">Create</button>
</div>
</form>
</div>
<script src="/js/dashboard.js"><script>

View file

@ -17,10 +17,6 @@
<meta http-equiv="Expires" content="0">
<% end %>
<!--[if lt IE 9]>
<script type="text/javascript" src="/js/html5.min.js"></script>
<![endif]-->
<script src="/js/jquery-1.11.0.min.js"></script>
<script src="/js/highlight.pack.js"></script>

View file

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