mirror of
https://github.com/neocities/neocities.git
synced 2025-04-24 17:22:35 +02:00
Merge branch 'master' of github.com:kyledrake/neocities-web
This commit is contained in:
commit
f367ed389e
24 changed files with 437 additions and 245 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,3 +20,4 @@ tests/coverage
|
|||
config.yml
|
||||
.DS_Store
|
||||
domains
|
||||
public/sites_test
|
||||
|
|
3
.travis.yml
Normal file
3
.travis.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
language: ruby
|
||||
rvm:
|
||||
- "2.1.0"
|
6
Gemfile
6
Gemfile
|
@ -15,6 +15,8 @@ gem 'selenium-webdriver', require: nil
|
|||
gem 'sidekiq'
|
||||
gem 'ago'
|
||||
gem 'mail'
|
||||
gem 'google-api-client', require: 'google/api_client'
|
||||
gem 'tilt'
|
||||
|
||||
platform :mri do
|
||||
gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic
|
||||
|
@ -52,6 +54,10 @@ group :test do
|
|||
gem 'webmock'
|
||||
gem 'mocha', require: nil
|
||||
gem 'rake', require: nil
|
||||
gem 'poltergeist'
|
||||
gem 'phantomjs', require: 'phantomjs/poltergeist'
|
||||
gem 'capybara'
|
||||
gem 'capybara_minitest_spec'
|
||||
|
||||
platform :mri do
|
||||
gem 'simplecov', require: nil
|
||||
|
|
58
Gemfile.lock
58
Gemfile.lock
|
@ -4,12 +4,26 @@ GEM
|
|||
addressable (2.3.6)
|
||||
ago (0.1.5)
|
||||
ansi (1.4.3)
|
||||
autoparse (0.3.3)
|
||||
addressable (>= 2.3.1)
|
||||
extlib (>= 0.9.15)
|
||||
multi_json (>= 1.0.0)
|
||||
bcrypt (3.1.7)
|
||||
builder (3.2.2)
|
||||
capybara (2.2.1)
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
capybara_minitest_spec (1.0.1)
|
||||
capybara (>= 2)
|
||||
minitest (>= 2)
|
||||
celluloid (0.15.2)
|
||||
timers (~> 1.1.0)
|
||||
childprocess (0.5.2)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
cliver (0.3.2)
|
||||
coderay (1.1.0)
|
||||
columnize (0.3.6)
|
||||
connection_pool (2.0.0)
|
||||
|
@ -22,15 +36,33 @@ GEM
|
|||
debugger-linecache (1.2.0)
|
||||
debugger-ruby_core_source (1.3.2)
|
||||
docile (1.1.3)
|
||||
extlib (0.9.16)
|
||||
fabrication (2.11.0)
|
||||
faker (1.3.0)
|
||||
i18n (~> 0.5)
|
||||
faraday (0.9.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.9.3)
|
||||
google-api-client (0.7.1)
|
||||
addressable (>= 2.3.2)
|
||||
autoparse (>= 0.3.3)
|
||||
extlib (>= 0.9.15)
|
||||
faraday (>= 0.9.0)
|
||||
jwt (>= 0.1.5)
|
||||
launchy (>= 2.1.1)
|
||||
multi_json (>= 1.0.0)
|
||||
retriable (>= 1.4)
|
||||
signet (>= 0.5.0)
|
||||
uuidtools (>= 2.1.0)
|
||||
hashie (2.0.5)
|
||||
hiredis (0.5.0)
|
||||
i18n (0.6.9)
|
||||
json (1.8.1)
|
||||
jwt (0.1.11)
|
||||
multi_json (>= 1.5)
|
||||
kgio (2.9.2)
|
||||
launchy (2.4.2)
|
||||
addressable (~> 2.3)
|
||||
magic (0.2.6)
|
||||
ffi (>= 0.6.3)
|
||||
mail (2.5.4)
|
||||
|
@ -39,6 +71,7 @@ GEM
|
|||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mini_portile (0.5.3)
|
||||
minitest (5.3.1)
|
||||
minitest-reporters (1.0.2)
|
||||
ansi
|
||||
|
@ -48,7 +81,16 @@ GEM
|
|||
mocha (1.0.0)
|
||||
metaclass (~> 0.0.1)
|
||||
multi_json (1.9.2)
|
||||
multipart-post (2.0.0)
|
||||
nokogiri (1.6.1)
|
||||
mini_portile (~> 0.5.0)
|
||||
pg (0.17.1)
|
||||
phantomjs (1.9.7.0)
|
||||
poltergeist (1.5.0)
|
||||
capybara (~> 2.1)
|
||||
cliver (~> 0.3.1)
|
||||
multi_json (~> 1.0)
|
||||
websocket-driver (>= 0.2.0)
|
||||
polyglot (0.3.4)
|
||||
powerbar (1.0.11)
|
||||
ansi (~> 1.4.0)
|
||||
|
@ -78,6 +120,7 @@ GEM
|
|||
redis (3.0.7)
|
||||
redis-namespace (1.4.1)
|
||||
redis (~> 3.0.4)
|
||||
retriable (1.4.1)
|
||||
rmagick (2.13.2)
|
||||
rubyzip (1.1.2)
|
||||
safe_yaml (1.0.1)
|
||||
|
@ -98,6 +141,11 @@ GEM
|
|||
json
|
||||
redis (>= 3.0.6)
|
||||
redis-namespace (>= 1.3.1)
|
||||
signet (0.5.0)
|
||||
addressable (>= 2.2.3)
|
||||
faraday (>= 0.9.0.rc5)
|
||||
jwt (>= 0.1.5)
|
||||
multi_json (>= 1.0.0)
|
||||
simplecov (0.8.2)
|
||||
docile (~> 1.1.0)
|
||||
multi_json
|
||||
|
@ -125,10 +173,14 @@ GEM
|
|||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
uuidtools (2.1.4)
|
||||
webmock (1.17.4)
|
||||
addressable (>= 2.2.7)
|
||||
crack (>= 0.3.2)
|
||||
websocket (1.0.7)
|
||||
websocket-driver (0.3.2)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -136,8 +188,11 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
ago
|
||||
bcrypt
|
||||
capybara
|
||||
capybara_minitest_spec
|
||||
fabrication
|
||||
faker
|
||||
google-api-client
|
||||
hiredis
|
||||
jdbc-postgres
|
||||
jruby-openssl
|
||||
|
@ -148,6 +203,8 @@ DEPENDENCIES
|
|||
minitest-reporters
|
||||
mocha
|
||||
pg
|
||||
phantomjs
|
||||
poltergeist
|
||||
pry
|
||||
pry-debugger
|
||||
puma
|
||||
|
@ -169,4 +226,5 @@ DEPENDENCIES
|
|||
sinatra-flash
|
||||
sinatra-xsendfile
|
||||
slim
|
||||
tilt
|
||||
webmock
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -7,7 +7,7 @@ end
|
|||
desc "Run all tests"
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "spec"
|
||||
t.test_files = FileList['tests/*_test.rb']
|
||||
t.test_files = FileList['tests/*_tests.rb']
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
|
|
155
app.rb
155
app.rb
|
@ -1,4 +1,6 @@
|
|||
require 'base64'
|
||||
require 'uri'
|
||||
require 'net/http'
|
||||
require './environment.rb'
|
||||
|
||||
use Rack::Session::Cookie, key: 'neocities',
|
||||
|
@ -102,15 +104,13 @@ get '/donate' do
|
|||
end
|
||||
|
||||
get '/blog' do
|
||||
# expires 500, :public, :must_revalidate
|
||||
return File.read File.join(DIR_ROOT, 'public', 'sites', 'blog', 'index.html')
|
||||
expires 500, :public, :must_revalidate
|
||||
return Net::HTTP.get_response(URI('http://blog.neocities.org')).body
|
||||
end
|
||||
|
||||
get '/blog/:article' do |article|
|
||||
# expires 500, :public, :must_revalidate
|
||||
path = File.join DIR_ROOT, 'public', 'sites', 'blog', "#{article}.html"
|
||||
pass if !File.exist?(path)
|
||||
File.read path
|
||||
expires 500, :public, :must_revalidate
|
||||
return Net::HTTP.get_response(URI("http://blog.neocities.org/#{article}.html")).body
|
||||
end
|
||||
|
||||
get '/new' do
|
||||
|
@ -137,22 +137,19 @@ end
|
|||
|
||||
post '/create' do
|
||||
dashboard_if_signed_in
|
||||
@site = Site.new username: params[:username], password: params[:password], email: params[:email], new_tags: params[:tags], is_nsfw: params[:is_nsfw], ip: request.ip
|
||||
@site = Site.new(
|
||||
username: params[:username],
|
||||
password: params[:password],
|
||||
email: params[:email],
|
||||
new_tags: params[:tags],
|
||||
is_nsfw: params[:is_nsfw],
|
||||
ip: request.ip
|
||||
)
|
||||
|
||||
recaptcha_is_valid = recaptcha_valid?
|
||||
recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?
|
||||
|
||||
if @site.valid? && recaptcha_is_valid
|
||||
|
||||
base_path = site_base_path @site.username
|
||||
|
||||
DB.transaction {
|
||||
@site.save
|
||||
|
||||
FileUtils.mkdir base_path
|
||||
|
||||
File.write File.join(base_path, 'index.html'), slim(:'templates/index', pretty: true, layout: false)
|
||||
File.write File.join(base_path, 'not_found.html'), slim(:'templates/not_found', pretty: true, layout: false)
|
||||
}
|
||||
@site.save
|
||||
|
||||
session[:id] = @site.id
|
||||
redirect '/dashboard'
|
||||
|
@ -225,19 +222,19 @@ end
|
|||
|
||||
post '/change_name' do
|
||||
require_login
|
||||
current_username = current_site.username
|
||||
old_username = current_site.username
|
||||
|
||||
if current_site.username == params[:name]
|
||||
if old_username == params[:name]
|
||||
flash[:error] = 'You already have this name.'
|
||||
redirect '/settings'
|
||||
end
|
||||
|
||||
|
||||
current_site.username = params[:name]
|
||||
|
||||
if current_site.valid?
|
||||
DB.transaction {
|
||||
current_site.save
|
||||
FileUtils.mv site_base_path(current_username), site_base_path(current_site.username)
|
||||
current_site.move_files_from old_username
|
||||
}
|
||||
|
||||
flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it</b>."
|
||||
|
@ -266,14 +263,13 @@ post '/site_files/create_page' do
|
|||
end
|
||||
|
||||
name = "#{params[:pagefilename]}.html"
|
||||
path = site_file_path name
|
||||
|
||||
if File.exist? path
|
||||
if current_site.file_exists?(name)
|
||||
@errors << %{Web page "#{name}" already exists! Choose another name.}
|
||||
halt slim(:'site_files/new_page')
|
||||
end
|
||||
|
||||
File.write path, slim(:'templates/index', pretty: true, layout: false)
|
||||
current_site.install_new_html_file name
|
||||
|
||||
flash[:success] = %{#{name} was created! <a style="color: #FFFFFF; text-decoration: underline" href="/site_files/text_editor/#{name}">Click here to edit it</a>.}
|
||||
|
||||
|
@ -297,12 +293,12 @@ post '/site_files/upload' do
|
|||
|
||||
if params[:newfile] == '' || params[:newfile].nil?
|
||||
@errors << 'You must select a file to upload.'
|
||||
halt http_error_code, 'Did not receive file upload.' # slim(:'site_files/new')
|
||||
halt http_error_code, 'Did not receive file upload.'
|
||||
end
|
||||
|
||||
if params[:newfile][:tempfile].size > Site::MAX_SPACE || (params[:newfile][:tempfile].size + current_site.total_space) > Site::MAX_SPACE
|
||||
@errors << 'File size must be smaller than available space.'
|
||||
halt http_error_code, 'File size must be smaller than available space.' # slim(:'site_files/new')
|
||||
halt http_error_code, 'File size must be smaller than available space.'
|
||||
end
|
||||
|
||||
mime_type = Magic.guess_file_mime_type params[:newfile][:tempfile].path
|
||||
|
@ -313,10 +309,7 @@ post '/site_files/upload' do
|
|||
end
|
||||
|
||||
sanitized_filename = params[:newfile][:filename].gsub(/[^a-zA-Z0-9_\-.]/, '')
|
||||
|
||||
dest_path = File.join(site_base_path(current_site.username), sanitized_filename)
|
||||
FileUtils.mv params[:newfile][:tempfile].path, dest_path
|
||||
File.chmod(0640, dest_path) if self.class.production?
|
||||
current_site.store_file sanitized_filename, params[:newfile][:tempfile]
|
||||
|
||||
if sanitized_filename =~ /index\.html/
|
||||
ScreenshotWorker.perform_async current_site.username
|
||||
|
@ -332,71 +325,56 @@ end
|
|||
post '/site_files/delete' do
|
||||
require_login
|
||||
sanitized_filename = params[:filename].gsub(/[^a-zA-Z0-9_\-.]/, '')
|
||||
begin
|
||||
FileUtils.rm File.join(site_base_path(current_site.username), sanitized_filename)
|
||||
rescue Errno::ENOENT
|
||||
flash[:error] = 'File was already deleted.'
|
||||
redirect '/dashboard'
|
||||
end
|
||||
|
||||
current_site.delete_file(sanitized_filename)
|
||||
|
||||
flash[:success] = "Deleted file #{params[:filename]}."
|
||||
redirect '/dashboard'
|
||||
end
|
||||
|
||||
get '/site_files/:username.zip' do |username|
|
||||
require_login
|
||||
file_path = "/tmp/neocities-site-#{username}.zip"
|
||||
|
||||
Zip::File.open(file_path, Zip::File::CREATE) do |zipfile|
|
||||
current_site.file_list.collect {|f| f.filename}.each do |filename|
|
||||
zipfile.add filename, site_file_path(filename)
|
||||
end
|
||||
end
|
||||
|
||||
# I don't want to have to deal with cleaning up old tmpfiles
|
||||
zipfile = File.read file_path
|
||||
File.delete file_path
|
||||
|
||||
zipfile = current_site.files_zip
|
||||
content_type 'application/octet-stream'
|
||||
attachment "#{current_site.username}.zip"
|
||||
|
||||
return zipfile
|
||||
zipfile
|
||||
end
|
||||
|
||||
get '/site_files/download/:filename' do |filename|
|
||||
require_login
|
||||
send_file File.join(site_base_path(current_site.username), filename), filename: filename, type: 'Application/octet-stream'
|
||||
content_type 'application/octet-stream'
|
||||
attachment filename
|
||||
current_site.get_file filename
|
||||
end
|
||||
|
||||
get '/site_files/text_editor/:filename' do |filename|
|
||||
require_login
|
||||
begin
|
||||
@file_data = File.read File.join(site_base_path(current_site.username), filename)
|
||||
@file_data = current_site.get_file filename
|
||||
rescue Errno::ENOENT
|
||||
flash[:error] = 'We could not find the requested file.'
|
||||
redirect '/dashboard'
|
||||
end
|
||||
slim :'site_files/text_editor'
|
||||
slim :'site_files/text_editor', indent: false
|
||||
end
|
||||
|
||||
post '/site_files/save/:filename' do |filename|
|
||||
require_login_ajax
|
||||
|
||||
tmpfile = Tempfile.new 'neocities_saving_file'
|
||||
tempfile = Tempfile.new 'neocities_saving_file'
|
||||
|
||||
if (tmpfile.size + current_site.total_space) > Site::MAX_SPACE
|
||||
if (tempfile.size + current_site.total_space) > Site::MAX_SPACE
|
||||
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
|
||||
|
||||
input = request.body.read
|
||||
tmpfile.set_encoding input.encoding
|
||||
tmpfile.write input
|
||||
tmpfile.close
|
||||
tempfile.set_encoding input.encoding
|
||||
tempfile.write input
|
||||
tempfile.close
|
||||
|
||||
sanitized_filename = filename.gsub(/[^a-zA-Z0-9_\-.]/, '')
|
||||
dest_path = File.join site_base_path(current_site.username), sanitized_filename
|
||||
|
||||
FileUtils.mv tmpfile.path, dest_path
|
||||
File.chmod(0640, dest_path) if self.class.production?
|
||||
current_site.store_file sanitized_filename, tempfile
|
||||
|
||||
if sanitized_filename =~ /index\.html/
|
||||
ScreenshotWorker.perform_async current_site.username
|
||||
|
@ -423,24 +401,6 @@ get '/admin' do
|
|||
slim :'admin'
|
||||
end
|
||||
|
||||
def ban_site(username)
|
||||
site = Site[username: username]
|
||||
return false if site.nil?
|
||||
return false if site.is_banned == true
|
||||
|
||||
DB.transaction {
|
||||
FileUtils.mv site_base_path(site.username), File.join(settings.public_folder, 'banned_sites', site.username)
|
||||
site.is_banned = true
|
||||
site.save(validate: false)
|
||||
}
|
||||
|
||||
if !['127.0.0.1', nil, ''].include? site.ip
|
||||
`sudo ufw insert 1 deny from #{site.ip}`
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
post '/admin/banip' do
|
||||
require_admin
|
||||
site = Site[username: params[:username]]
|
||||
|
@ -455,8 +415,8 @@ post '/admin/banip' do
|
|||
redirect '/admin'
|
||||
end
|
||||
|
||||
sites = Site.filter(ip: site.ip).all
|
||||
sites.each {|s| ban_site(s.username)}
|
||||
sites = Site.filter(ip: site.ip, is_banned: false).all
|
||||
sites.each {|s| s.ban!}
|
||||
flash[:error] = "#{sites.length} sites have been banned."
|
||||
redirect '/admin'
|
||||
end
|
||||
|
@ -476,7 +436,7 @@ post '/admin/banhammer' do
|
|||
redirect '/admin'
|
||||
end
|
||||
|
||||
ban_site params[:username]
|
||||
site.ban!
|
||||
|
||||
flash[:success] = 'MISSION ACCOMPLISHED'
|
||||
redirect '/admin'
|
||||
|
@ -577,18 +537,9 @@ post '/custom_domain' do
|
|||
require_login
|
||||
original_domain = current_site.domain
|
||||
current_site.domain = params[:domain]
|
||||
|
||||
if current_site.valid?
|
||||
|
||||
DB.transaction do
|
||||
current_site.save
|
||||
|
||||
if !params[:domain].empty? && !params[:domain].nil?
|
||||
File.open(File.join(DIR_ROOT, 'domains', "#{current_site.username}.conf"), 'w') do |file|
|
||||
file.write erb(:'templates/domain', layout: false)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
current_site.save
|
||||
flash[:success] = 'The domain has been successfully updated.'
|
||||
redirect '/custom_domain'
|
||||
else
|
||||
|
@ -660,18 +611,6 @@ def current_site
|
|||
@site ||= Site[id: session[:id]]
|
||||
end
|
||||
|
||||
def site_base_path(subname)
|
||||
File.join settings.public_folder, 'sites', subname
|
||||
end
|
||||
|
||||
def site_file_path(filename)
|
||||
File.join(site_base_path(current_site.username), filename)
|
||||
end
|
||||
|
||||
def template_site_title(username)
|
||||
"#{username.capitalize}#{username[username.length-1] == 's' ? "'" : "'s"} Site"
|
||||
end
|
||||
|
||||
def encoding_fix(file)
|
||||
begin
|
||||
Rack::Utils.escape_html file
|
||||
|
@ -679,4 +618,4 @@ def encoding_fix(file)
|
|||
return Rack::Utils.escape_html(file.force_encoding('BINARY')) if e.message =~ /invalid byte sequence in UTF-8/
|
||||
fail
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :hits
|
||||
DB.drop_column :sites, :hits
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :is_admin
|
||||
DB.drop_column :sites, :is_admin
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :is_banned
|
||||
DB.drop_column :sites, :is_banned
|
||||
}
|
||||
end
|
|
@ -4,6 +4,6 @@ Sequel.migration do
|
|||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :ip
|
||||
DB.drop_column :sites, :ip
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :password_reset_token
|
||||
DB.drop_index :sites, :password_reset_token
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :site_changed
|
||||
DB.drop_index :sites, :site_changed
|
||||
}
|
||||
end
|
|
@ -6,6 +6,5 @@ Sequel.migration do
|
|||
|
||||
down {
|
||||
DB.drop_column :sites, :changed_count
|
||||
DB.drop_index :sites, :changed_count
|
||||
}
|
||||
end
|
148
models/site.rb
148
models/site.rb
|
@ -1,11 +1,44 @@
|
|||
require 'tilt'
|
||||
|
||||
class Site < Sequel::Model
|
||||
# We might need to include fonts in here..
|
||||
VALID_MIME_TYPES = ['text/plain', 'text/html', 'text/css', 'application/javascript', 'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'application/vnd.ms-fontobject', 'application/x-font-ttf', 'application/octet-stream', 'text/csv', 'text/tsv', 'text/cache-manifest', 'image/x-icon', 'application/pdf', 'application/pgp-keys', 'text/xml', 'application/xml', 'audio/midi']
|
||||
VALID_EXTENSIONS = %w{ html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json geojson csv tsv mf ico pdf asc key pgp xml mid midi }
|
||||
#USERNAME_SHITLIST = %w{ payment secure login signin www ww web } # I thought they were funny personally, but everybody is freaking out so..
|
||||
VALID_MIME_TYPES = %w{
|
||||
text/plain
|
||||
text/html
|
||||
text/css
|
||||
application/javascript
|
||||
image/png
|
||||
image/jpeg
|
||||
image/gif
|
||||
image/svg+xml
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/octet-stream
|
||||
text/csv
|
||||
text/tsv
|
||||
text/cache-manifest
|
||||
image/x-icon
|
||||
application/pdf
|
||||
application/pgp-keys
|
||||
text/xml
|
||||
application/xml
|
||||
audio/midi
|
||||
}
|
||||
VALID_EXTENSIONS = %w{
|
||||
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json
|
||||
geojson csv tsv mf ico pdf asc key pgp xml mid midi
|
||||
}
|
||||
MAX_SPACE = (5242880*2) # 10MB
|
||||
MINIMUM_PASSWORD_LENGTH = 5
|
||||
BAD_USERNAME_REGEX = /[^\w-]/i
|
||||
VALID_HOSTNAME = /^[a-z0-9][a-z0-9-]+?[a-z0-9]$/i # http://tools.ietf.org/html/rfc1123
|
||||
|
||||
# FIXME smarter DIR_ROOT discovery
|
||||
DIR_ROOT = './'
|
||||
TEMPLATE_ROOT = File.join DIR_ROOT, 'views', 'templates'
|
||||
PUBLIC_ROOT = File.join DIR_ROOT, 'public'
|
||||
SITE_FILES_ROOT = File.join PUBLIC_ROOT, (ENV['RACK_ENV'] == 'test' ? 'sites_test' : 'sites')
|
||||
|
||||
many_to_one :server
|
||||
many_to_many :tags
|
||||
|
||||
|
@ -51,6 +84,87 @@ class Site < Sequel::Model
|
|||
super
|
||||
end
|
||||
|
||||
def save(validate={})
|
||||
DB.transaction do
|
||||
is_new = new?
|
||||
install_custom_domain if !domain.nil? && !domain.empty?
|
||||
result = super(validate)
|
||||
install_new_files if is_new
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def install_custom_domain
|
||||
File.open(File.join(DIR_ROOT, 'domains', "#{username}.conf"), 'w') do |file|
|
||||
file.write render_template('domain.erb')
|
||||
end
|
||||
end
|
||||
|
||||
def install_new_files
|
||||
FileUtils.mkdir_p files_path
|
||||
|
||||
%w{index not_found}.each do |name|
|
||||
File.write file_path("#{name}.html"), render_template("#{name}.slim")
|
||||
end
|
||||
end
|
||||
|
||||
def get_file(filename)
|
||||
File.read file_path(filename)
|
||||
end
|
||||
|
||||
def ban!
|
||||
DB.transaction {
|
||||
FileUtils.mv files_path, File.join(PUBLIC_ROOT, 'banned_sites', username)
|
||||
self.is_banned = true
|
||||
|
||||
if !['127.0.0.1', nil, ''].include? ip
|
||||
`sudo ufw insert 1 deny from #{ip}`
|
||||
end
|
||||
|
||||
save(validate: false)
|
||||
}
|
||||
end
|
||||
|
||||
def store_file(filename, uploaded)
|
||||
FileUtils.mv uploaded.path, file_path(filename)
|
||||
File.chmod(0640, file_path(filename))
|
||||
end
|
||||
|
||||
def files_zip
|
||||
file_path = "/tmp/neocities-site-#{username}.zip"
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO Don't dump the zipfile into memory
|
||||
zipfile = File.read file_path
|
||||
File.delete file_path
|
||||
zipfile
|
||||
end
|
||||
|
||||
def delete_file(filename)
|
||||
begin
|
||||
FileUtils.rm file_path(filename)
|
||||
rescue Errno::ENOENT
|
||||
# File was probably already deleted
|
||||
end
|
||||
end
|
||||
|
||||
def move_files_from(oldusername)
|
||||
FileUtils.mv files_path(oldusername), files_path
|
||||
end
|
||||
|
||||
def install_new_html_file(name)
|
||||
File.write file_path(name), render_template('index.slim')
|
||||
end
|
||||
|
||||
def file_exists?(filename)
|
||||
File.exist? file_path(filename)
|
||||
end
|
||||
|
||||
def after_save
|
||||
if @new_tag_strings
|
||||
@new_tag_strings.each do |new_tag_string|
|
||||
|
@ -77,13 +191,15 @@ class Site < Sequel::Model
|
|||
errors.add :over_capacity, 'We are currently at capacity, and cannot create your home page. We will fix this shortly. Please come back later and try again, our apologies.'
|
||||
end
|
||||
|
||||
if values[:username].nil? || values[:username].empty? || values[:username].match(BAD_USERNAME_REGEX)
|
||||
errors.add :username, 'A valid username is required.'
|
||||
if !values[:username].match(VALID_HOSTNAME)
|
||||
errors.add :username, 'A valid user/site name is required.'
|
||||
end
|
||||
|
||||
if values[:username].length > 32
|
||||
errors.add :username, 'User/site name cannot exceed 32 characters.'
|
||||
end
|
||||
|
||||
# Check for existing user
|
||||
|
||||
|
||||
# Check for existing user
|
||||
user = self.class.select(:id, :username).filter(username: values[:username]).first
|
||||
|
||||
if user
|
||||
|
@ -108,16 +224,24 @@ class Site < Sequel::Model
|
|||
end
|
||||
end
|
||||
|
||||
def file_path
|
||||
File.join DIR_ROOT, 'public', 'sites', username
|
||||
def render_template(name)
|
||||
Tilt.new(File.join(TEMPLATE_ROOT, name), pretty: true).render self
|
||||
end
|
||||
|
||||
def files_path(name=nil)
|
||||
File.join SITE_FILES_ROOT, (name || username)
|
||||
end
|
||||
|
||||
def file_path(filename)
|
||||
File.join files_path, filename
|
||||
end
|
||||
|
||||
def file_list
|
||||
Dir.glob(File.join(file_path, '*')).collect {|p| File.basename(p)}.sort.collect {|sitename| SiteFile.new sitename}
|
||||
Dir.glob(File.join(files_path, '*')).collect {|p| File.basename(p)}.sort.collect {|sitename| SiteFile.new sitename}
|
||||
end
|
||||
|
||||
def total_space
|
||||
space = Dir.glob(File.join(file_path, '*')).collect {|p| File.size(p)}.inject {|sum,x| sum += x}
|
||||
space = Dir.glob(File.join(files_path, '*')).collect {|p| File.size(p)}.inject {|sum,x| sum += x}
|
||||
space.nil? ? 0 : space
|
||||
end
|
||||
|
||||
|
|
143
tests/acceptance_tests.rb
Normal file
143
tests/acceptance_tests.rb
Normal file
|
@ -0,0 +1,143 @@
|
|||
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 Home Page')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signup' 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 visit_signup
|
||||
visit '/'
|
||||
click_button 'Create My Website'
|
||||
end
|
||||
|
||||
before do
|
||||
Capybara.reset_sessions!
|
||||
visit_signup
|
||||
end
|
||||
|
||||
it 'succeeds with valid data' do
|
||||
fill_in_valid
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Your Website'
|
||||
assert_equal(
|
||||
true,
|
||||
File.exist?(File.join(Site::SITE_FILES_ROOT, @site[:username], 'index.html'))
|
||||
)
|
||||
end
|
||||
|
||||
it 'fails to create for existing site' do
|
||||
fill_in_valid
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Your Website'
|
||||
Capybara.reset_sessions!
|
||||
visit_signup
|
||||
fill_in 'username', with: @site[:username]
|
||||
fill_in 'password', with: @site[:password]
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'already taken'
|
||||
end
|
||||
|
||||
it 'fails with missing password' do
|
||||
fill_in_valid
|
||||
fill_in 'password', with: ''
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Password must be at least 5 characters'
|
||||
end
|
||||
|
||||
it 'fails with short password' do
|
||||
fill_in_valid
|
||||
fill_in 'password', with: 'derp'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'Password must be at least 5 characters'
|
||||
end
|
||||
|
||||
it 'fails with invalid hostname for username' do
|
||||
fill_in_valid
|
||||
fill_in 'username', with: '|\|0p|E'
|
||||
click_button 'Create Home Page'
|
||||
page.current_path.must_equal '/create'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
fill_in 'username', with: 'nope-'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
fill_in 'username', with: '-nope'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'A valid user/site name is required'
|
||||
end
|
||||
|
||||
it 'fails with username greater than 32 characters' do
|
||||
fill_in_valid
|
||||
fill_in 'username', with: SecureRandom.hex+'1'
|
||||
click_button 'Create Home Page'
|
||||
page.must_have_content 'cannot exceed 32 characters'
|
||||
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
|
||||
|
||||
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'
|
||||
site = Fabricate.attributes_for(:site)
|
||||
fill_in 'username', with: site[:username]
|
||||
fill_in 'password', with: site[:password]
|
||||
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 Website'
|
||||
end
|
||||
end
|
|
@ -1,93 +0,0 @@
|
|||
require_relative './environment'
|
||||
|
||||
include Rack::Test::Methods
|
||||
|
||||
def app; App end
|
||||
def status; last_response.status end
|
||||
def headers; last_response.headers end
|
||||
def body; last_response.body end
|
||||
|
||||
describe 'index' do
|
||||
it 'loads' do
|
||||
get '/'
|
||||
status.must_equal 200
|
||||
end
|
||||
end
|
||||
|
||||
describe 'signin' do
|
||||
it 'fails for missing login' do
|
||||
post '/signin', username: 'derpie', password: 'lol'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'fails for bad password' do
|
||||
@site = Fabricate :site
|
||||
post '/signin', username: @site.username, password: 'derp'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'fails for no input' do
|
||||
post '/signin'
|
||||
fail_signin
|
||||
end
|
||||
|
||||
it 'succeeds for valid input' do
|
||||
password = '1tw0rkz'
|
||||
@account = Fabricate :account, password: password
|
||||
post '/accounts/signin', username: @account.email, password: password
|
||||
headers['Location'].must_equal 'http://example.org/dashboard'
|
||||
mock_dashboard_calls @account.email
|
||||
get '/dashboard'
|
||||
body.must_match /Dashboard/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'account creation' do
|
||||
it 'fails for no input' do
|
||||
post '/accounts/create'
|
||||
status.must_equal 200
|
||||
body.must_match /There were some errors.+Valid email address is required.+Password must be/
|
||||
end
|
||||
|
||||
it 'fails with invalid email' do
|
||||
post '/accounts/create', email: 'derplol'
|
||||
status.must_equal 200
|
||||
body.must_match /errors.+valid email/i
|
||||
end
|
||||
|
||||
it 'fails with invalid password' do
|
||||
post '/accounts/create', 'email@example.com', password: 'sdd'
|
||||
status.must_equal 200
|
||||
body.must_match /errors.+Password must be at least #{Account::MINIMUM_PASSWORD_LENGTH} characters/i
|
||||
end
|
||||
|
||||
it 'succeeds with valid info' do
|
||||
account_attributes = Fabricate.attributes_for :account
|
||||
|
||||
mock_dashboard_calls account_attributes[:email]
|
||||
|
||||
post '/accounts/create', account_attributes
|
||||
status.must_equal 302
|
||||
headers['Location'].must_equal 'http://example.org/dashboard'
|
||||
|
||||
get '/dashboard'
|
||||
body.must_match /Dashboard/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'temporary account login' do
|
||||
end
|
||||
|
||||
def fail_signin
|
||||
headers['Location'].must_equal 'http://example.org/'
|
||||
get '/'
|
||||
body.must_match /invalid signin/i
|
||||
end
|
||||
|
||||
def api_url
|
||||
uri = Addressable::URI.parse $config['bitcoind_rpchost'] ? $config['bitcoind_rpchost'] : 'http://localhost'
|
||||
uri.port = 8332 if uri.port.nil?
|
||||
uri.user = $config['bitcoind_rpcuser'] if uri.user.nil?
|
||||
uri.password = $config['bitcoind_rpcpassword'] if uri.password.nil?
|
||||
"#{uri.to_s}/"
|
||||
end
|
|
@ -20,7 +20,7 @@ Bundler.require :test
|
|||
require 'minitest/autorun'
|
||||
require 'sidekiq/testing/inline'
|
||||
|
||||
Account.bcrypt_cost = BCrypt::Engine::MIN_COST
|
||||
Site.bcrypt_cost = BCrypt::Engine::MIN_COST
|
||||
|
||||
MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
|
||||
|
||||
|
@ -29,8 +29,11 @@ Sequel.extension :migration
|
|||
|
||||
Sequel::Migrator.apply DB, './migrations', 0
|
||||
Sequel::Migrator.apply DB, './migrations'
|
||||
Server.create ip: '127.0.0.1', slots_available: 999999
|
||||
|
||||
Fabrication.configure do |config|
|
||||
config.fabricator_path = 'tests/fabricators'
|
||||
config.path_prefix = DIR_ROOT
|
||||
end
|
||||
end
|
||||
|
||||
I18n.enforce_available_locales = true
|
|
@ -1,4 +1,4 @@
|
|||
Fabricator(:site) do
|
||||
username { Faker::Internet.email }
|
||||
username { SecureRandom.hex }
|
||||
password { 'abcde' }
|
||||
end
|
|
@ -20,8 +20,7 @@ javascript:
|
|||
span
|
||||
<i class="icon-file-alt icon-3x"></i> <span style="font-size: 20pt">#{file.filename}</span>
|
||||
- if file.filename == 'index.html'
|
||||
p.tiny
|
||||
This is your index file! It is the "default file" that loads when you go to <a href="http://#{current_site.username}.neocities.org">#{current_site.username}.neocities.org</a>. In effect, it's your front page. If you want to change your front page, you need to edit (or overwrite) this file. The default file is always named <b>index.html</b>.
|
||||
p.tiny This is your index file! It is the "default file" that loads when you go to <a href="http://#{current_site.username}.neocities.org">#{current_site.username}.neocities.org</a>. In effect, it's your front page. If you want to change your front page, you need to edit (or overwrite) this file, which you should do right now if you just created your site. The default file is always named <b>index.html</b>, and you cannot delete it.
|
||||
|
||||
div style="margin-bottom:30px"
|
||||
span
|
||||
|
@ -36,14 +35,23 @@ javascript:
|
|||
span
|
||||
i class="icon-edit"
|
||||
span: a href="/site_files/download/#{file.filename}" Download <br />
|
||||
span
|
||||
i class="icon-trash"
|
||||
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete
|
||||
|
||||
- if file.filename != 'index.html'
|
||||
span
|
||||
i class="icon-trash"
|
||||
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete
|
||||
- else
|
||||
<i class="icon-picture icon-3x"></i> <span style="font-size: 20pt">#{file.filename}</span>
|
||||
div style="margin-top: 3px; margin-bottom:10px"
|
||||
| To use in an HTML file, paste this text: <code class="tiny" style="margin:0"><img src="/#{file.filename}"></code>
|
||||
div style="margin-top: 3px; margin-bottom: 30px"
|
||||
| To use in an HTML file, paste this text: <code class="tiny" style="margin:0"><img src="/#{file.filename}"></code>
|
||||
span
|
||||
i class="icon-globe"
|
||||
a href="http://#{current_site.username}.neocities.org/#{file.filename}" target="_blank" View <br />
|
||||
span
|
||||
i class="icon-edit"
|
||||
span: a href="/site_files/download/#{file.filename}" Download <br />
|
||||
span
|
||||
i class="icon-trash"
|
||||
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete
|
||||
|
||||
.col.col-40
|
||||
|
|
|
@ -19,35 +19,39 @@ javascript:
|
|||
h2.txt-Center Create a New Home Page
|
||||
|
||||
.col.col-50 style="margin:0 auto; float:none"
|
||||
p.tiny First, enter a username. This will also be used as your site path.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It cannot contain spaces, and can only use the following characters: a-z A-Z 0-9 _ -
|
||||
hr
|
||||
p.tiny First, enter a username. This will also be used as your site name.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It can only contain letters, numbers, underscores and hyphens, and can only be 32 characters long.
|
||||
br
|
||||
h5 Username
|
||||
p.tiny <input class="input-Area" name="username" type="text" placeholder="yourusername" value="#{@site.username}" autocapitalize="off" autocorrect="off">.neocities.org
|
||||
|
||||
hr
|
||||
p.tiny Next, enter a password. This will be used to allow you to login. Minimum 5 characters. If you don't make it a good password, Dade Murphy from the movie Hackers will come in and steal your "garbage files".
|
||||
|
||||
hr
|
||||
|
||||
h5 Password
|
||||
input class="input-Area" name="password" type="password"
|
||||
|
||||
br
|
||||
hr
|
||||
p.tiny Now you can enter an e-mail address. Your e-mail address is private and we will not show it to anyone for any reason. You don't have to provide one, but <b>we will not be able to reset your password without it, so don't lose your username and password if you leave this blank!</b>
|
||||
|
||||
hr
|
||||
|
||||
h5 Email
|
||||
input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}"
|
||||
|
||||
br
|
||||
hr
|
||||
|
||||
p.tiny You can optionally enter some tags! Tags will allow others to find your site based on your interests, or your site's theme. <b>Separate multiple tags with commas</b>. Don't think too hard about this, you can change them later. You can have a maximum of ten tags, and there is a two word per tag maximum (extra words in a tag will be removed).
|
||||
|
||||
hr
|
||||
h5 Tags
|
||||
p: input class="input-Area" name="tags" type="text" style="width: 400px; max-width:100%" placeholder="pokemon, video games, bulbasaur" value="#{params[:tags]}" autocapitalize="off" autocorrect="off"
|
||||
|
||||
hr
|
||||
input name="is_nsfw" type="hidden" value="false"
|
||||
p: strong If your page will contain objectionable (adult) content, check this box: <input name="is_nsfw" type="checkbox" value="true" style="margin-top:0">
|
||||
p If your page will contain objectionable (adult) content, check this box: <input name="is_nsfw" type="checkbox" value="true" style="margin-top:0">
|
||||
|
||||
hr
|
||||
p.tiny <b>Last thing!</b> Enter these two words correctly (with spaces) so we know you're not a robot (don't worry robots, we still love you).
|
||||
div style="background:#fff; width:100%; overflow:auto"
|
||||
== recaptcha_tag :challenge, ssl: true
|
||||
|
|
|
@ -53,9 +53,7 @@ css:
|
|||
option value="ace/theme/twilight" Twilight
|
||||
option value="ace/theme/vibrant_ink" Vibrant Ink
|
||||
|
||||
div id="editor" style="width: 100%; height: 600px; position: relative; margin-bottom:25px"
|
||||
== encoding_fix @file_data
|
||||
|
||||
<div id="editor" style="width: 100%; height: 600px; position: relative; margin-bottom:25px">#{{encoding_fix(@file_data)}}</div>
|
||||
|
||||
.row
|
||||
.col.col-33.txt-Center style="margin-bottom:10px"
|
||||
|
@ -125,4 +123,5 @@ css:
|
|||
editor.getSession().setUseWrapMode(true);
|
||||
editor.setFontSize(14);
|
||||
editor.setShowPrintMargin(false);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name <%= current_site.domain %> *.<%= current_site.domain %>;
|
||||
server_name <%= domain %> *.<%= domain %>;
|
||||
access_log /var/log/nginx/neocities-domains.log neocitiesdomain;
|
||||
root /home/web/neocities-web/public/sites/<%= current_site.username %>;
|
||||
root /home/web/neocities-web/public/sites/<%= username %>;
|
||||
index /index.html;
|
||||
|
||||
error_page 404 = @notfound;
|
||||
|
|
|
@ -3,7 +3,7 @@ html
|
|||
head
|
||||
meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
|
||||
|
||||
title #{template_site_title @site.username} - Front Page
|
||||
title #{username}
|
||||
|
||||
meta name="description" content=""
|
||||
meta name="keywords" content=""
|
||||
|
|
|
@ -3,7 +3,7 @@ html
|
|||
head
|
||||
meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
|
||||
|
||||
title #{template_site_title @site.username} - Not Found
|
||||
title #{username} - Page Not Found
|
||||
|
||||
link href="//groundfloor.neocities.org/default.css" rel="stylesheet" type="text/css" media="all"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue