Merge branch 'master' of github.com:kyledrake/neocities-web

This commit is contained in:
Kyle Drake 2014-04-05 01:21:10 -04:00
commit f367ed389e
24 changed files with 437 additions and 245 deletions

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ tests/coverage
config.yml config.yml
.DS_Store .DS_Store
domains domains
public/sites_test

3
.travis.yml Normal file
View file

@ -0,0 +1,3 @@
language: ruby
rvm:
- "2.1.0"

View file

@ -15,6 +15,8 @@ gem 'selenium-webdriver', require: nil
gem 'sidekiq' gem 'sidekiq'
gem 'ago' gem 'ago'
gem 'mail' gem 'mail'
gem 'google-api-client', require: 'google/api_client'
gem 'tilt'
platform :mri do platform :mri do
gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic
@ -52,6 +54,10 @@ group :test do
gem 'webmock' gem 'webmock'
gem 'mocha', require: nil gem 'mocha', require: nil
gem 'rake', require: nil gem 'rake', require: nil
gem 'poltergeist'
gem 'phantomjs', require: 'phantomjs/poltergeist'
gem 'capybara'
gem 'capybara_minitest_spec'
platform :mri do platform :mri do
gem 'simplecov', require: nil gem 'simplecov', require: nil

View file

@ -4,12 +4,26 @@ GEM
addressable (2.3.6) addressable (2.3.6)
ago (0.1.5) ago (0.1.5)
ansi (1.4.3) 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) bcrypt (3.1.7)
builder (3.2.2) 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) celluloid (0.15.2)
timers (~> 1.1.0) timers (~> 1.1.0)
childprocess (0.5.2) childprocess (0.5.2)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
cliver (0.3.2)
coderay (1.1.0) coderay (1.1.0)
columnize (0.3.6) columnize (0.3.6)
connection_pool (2.0.0) connection_pool (2.0.0)
@ -22,15 +36,33 @@ GEM
debugger-linecache (1.2.0) debugger-linecache (1.2.0)
debugger-ruby_core_source (1.3.2) debugger-ruby_core_source (1.3.2)
docile (1.1.3) docile (1.1.3)
extlib (0.9.16)
fabrication (2.11.0) fabrication (2.11.0)
faker (1.3.0) faker (1.3.0)
i18n (~> 0.5) i18n (~> 0.5)
faraday (0.9.0)
multipart-post (>= 1.2, < 3)
ffi (1.9.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) hashie (2.0.5)
hiredis (0.5.0) hiredis (0.5.0)
i18n (0.6.9) i18n (0.6.9)
json (1.8.1) json (1.8.1)
jwt (0.1.11)
multi_json (>= 1.5)
kgio (2.9.2) kgio (2.9.2)
launchy (2.4.2)
addressable (~> 2.3)
magic (0.2.6) magic (0.2.6)
ffi (>= 0.6.3) ffi (>= 0.6.3)
mail (2.5.4) mail (2.5.4)
@ -39,6 +71,7 @@ GEM
metaclass (0.0.4) metaclass (0.0.4)
method_source (0.8.2) method_source (0.8.2)
mime-types (1.25.1) mime-types (1.25.1)
mini_portile (0.5.3)
minitest (5.3.1) minitest (5.3.1)
minitest-reporters (1.0.2) minitest-reporters (1.0.2)
ansi ansi
@ -48,7 +81,16 @@ GEM
mocha (1.0.0) mocha (1.0.0)
metaclass (~> 0.0.1) metaclass (~> 0.0.1)
multi_json (1.9.2) multi_json (1.9.2)
multipart-post (2.0.0)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
pg (0.17.1) 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) polyglot (0.3.4)
powerbar (1.0.11) powerbar (1.0.11)
ansi (~> 1.4.0) ansi (~> 1.4.0)
@ -78,6 +120,7 @@ GEM
redis (3.0.7) redis (3.0.7)
redis-namespace (1.4.1) redis-namespace (1.4.1)
redis (~> 3.0.4) redis (~> 3.0.4)
retriable (1.4.1)
rmagick (2.13.2) rmagick (2.13.2)
rubyzip (1.1.2) rubyzip (1.1.2)
safe_yaml (1.0.1) safe_yaml (1.0.1)
@ -98,6 +141,11 @@ GEM
json json
redis (>= 3.0.6) redis (>= 3.0.6)
redis-namespace (>= 1.3.1) 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) simplecov (0.8.2)
docile (~> 1.1.0) docile (~> 1.1.0)
multi_json multi_json
@ -125,10 +173,14 @@ GEM
kgio (~> 2.6) kgio (~> 2.6)
rack rack
raindrops (~> 0.7) raindrops (~> 0.7)
uuidtools (2.1.4)
webmock (1.17.4) webmock (1.17.4)
addressable (>= 2.2.7) addressable (>= 2.2.7)
crack (>= 0.3.2) crack (>= 0.3.2)
websocket (1.0.7) websocket (1.0.7)
websocket-driver (0.3.2)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS PLATFORMS
ruby ruby
@ -136,8 +188,11 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
ago ago
bcrypt bcrypt
capybara
capybara_minitest_spec
fabrication fabrication
faker faker
google-api-client
hiredis hiredis
jdbc-postgres jdbc-postgres
jruby-openssl jruby-openssl
@ -148,6 +203,8 @@ DEPENDENCIES
minitest-reporters minitest-reporters
mocha mocha
pg pg
phantomjs
poltergeist
pry pry
pry-debugger pry-debugger
puma puma
@ -169,4 +226,5 @@ DEPENDENCIES
sinatra-flash sinatra-flash
sinatra-xsendfile sinatra-xsendfile
slim slim
tilt
webmock webmock

View file

@ -7,7 +7,7 @@ end
desc "Run all tests" desc "Run all tests"
Rake::TestTask.new do |t| Rake::TestTask.new do |t|
t.libs << "spec" t.libs << "spec"
t.test_files = FileList['tests/*_test.rb'] t.test_files = FileList['tests/*_tests.rb']
t.verbose = true t.verbose = true
end end

155
app.rb
View file

@ -1,4 +1,6 @@
require 'base64' require 'base64'
require 'uri'
require 'net/http'
require './environment.rb' require './environment.rb'
use Rack::Session::Cookie, key: 'neocities', use Rack::Session::Cookie, key: 'neocities',
@ -102,15 +104,13 @@ get '/donate' do
end end
get '/blog' do get '/blog' do
# expires 500, :public, :must_revalidate expires 500, :public, :must_revalidate
return File.read File.join(DIR_ROOT, 'public', 'sites', 'blog', 'index.html') return Net::HTTP.get_response(URI('http://blog.neocities.org')).body
end end
get '/blog/:article' do |article| get '/blog/:article' do |article|
# expires 500, :public, :must_revalidate expires 500, :public, :must_revalidate
path = File.join DIR_ROOT, 'public', 'sites', 'blog', "#{article}.html" return Net::HTTP.get_response(URI("http://blog.neocities.org/#{article}.html")).body
pass if !File.exist?(path)
File.read path
end end
get '/new' do get '/new' do
@ -137,22 +137,19 @@ end
post '/create' do post '/create' do
dashboard_if_signed_in 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 if @site.valid? && recaptcha_is_valid
@site.save
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)
}
session[:id] = @site.id session[:id] = @site.id
redirect '/dashboard' redirect '/dashboard'
@ -225,19 +222,19 @@ end
post '/change_name' do post '/change_name' do
require_login 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.' flash[:error] = 'You already have this name.'
redirect '/settings' redirect '/settings'
end end
current_site.username = params[:name] current_site.username = params[:name]
if current_site.valid? if current_site.valid?
DB.transaction { DB.transaction {
current_site.save 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>." 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 end
name = "#{params[:pagefilename]}.html" 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.} @errors << %{Web page "#{name}" already exists! Choose another name.}
halt slim(:'site_files/new_page') halt slim(:'site_files/new_page')
end 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>.} 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? if params[:newfile] == '' || params[:newfile].nil?
@errors << 'You must select a file to upload.' @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 end
if params[:newfile][:tempfile].size > Site::MAX_SPACE || (params[:newfile][:tempfile].size + current_site.total_space) > Site::MAX_SPACE 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.' @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 end
mime_type = Magic.guess_file_mime_type params[:newfile][:tempfile].path mime_type = Magic.guess_file_mime_type params[:newfile][:tempfile].path
@ -313,10 +309,7 @@ post '/site_files/upload' do
end end
sanitized_filename = params[:newfile][:filename].gsub(/[^a-zA-Z0-9_\-.]/, '') sanitized_filename = params[:newfile][:filename].gsub(/[^a-zA-Z0-9_\-.]/, '')
current_site.store_file sanitized_filename, params[:newfile][:tempfile]
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?
if sanitized_filename =~ /index\.html/ if sanitized_filename =~ /index\.html/
ScreenshotWorker.perform_async current_site.username ScreenshotWorker.perform_async current_site.username
@ -332,71 +325,56 @@ end
post '/site_files/delete' do post '/site_files/delete' do
require_login require_login
sanitized_filename = params[:filename].gsub(/[^a-zA-Z0-9_\-.]/, '') sanitized_filename = params[:filename].gsub(/[^a-zA-Z0-9_\-.]/, '')
begin
FileUtils.rm File.join(site_base_path(current_site.username), sanitized_filename) current_site.delete_file(sanitized_filename)
rescue Errno::ENOENT
flash[:error] = 'File was already deleted.'
redirect '/dashboard'
end
flash[:success] = "Deleted file #{params[:filename]}." flash[:success] = "Deleted file #{params[:filename]}."
redirect '/dashboard' redirect '/dashboard'
end end
get '/site_files/:username.zip' do |username| get '/site_files/:username.zip' do |username|
require_login require_login
file_path = "/tmp/neocities-site-#{username}.zip" zipfile = current_site.files_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
content_type 'application/octet-stream' content_type 'application/octet-stream'
attachment "#{current_site.username}.zip" attachment "#{current_site.username}.zip"
zipfile
return zipfile
end end
get '/site_files/download/:filename' do |filename| get '/site_files/download/:filename' do |filename|
require_login 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 end
get '/site_files/text_editor/:filename' do |filename| get '/site_files/text_editor/:filename' do |filename|
require_login require_login
begin begin
@file_data = File.read File.join(site_base_path(current_site.username), filename) @file_data = current_site.get_file filename
rescue Errno::ENOENT rescue Errno::ENOENT
flash[:error] = 'We could not find the requested file.' flash[:error] = 'We could not find the requested file.'
redirect '/dashboard' redirect '/dashboard'
end end
slim :'site_files/text_editor' slim :'site_files/text_editor', indent: false
end end
post '/site_files/save/:filename' do |filename| post '/site_files/save/:filename' do |filename|
require_login_ajax 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.' 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 end
input = request.body.read input = request.body.read
tmpfile.set_encoding input.encoding tempfile.set_encoding input.encoding
tmpfile.write input tempfile.write input
tmpfile.close tempfile.close
sanitized_filename = filename.gsub(/[^a-zA-Z0-9_\-.]/, '') 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 current_site.store_file sanitized_filename, tempfile
File.chmod(0640, dest_path) if self.class.production?
if sanitized_filename =~ /index\.html/ if sanitized_filename =~ /index\.html/
ScreenshotWorker.perform_async current_site.username ScreenshotWorker.perform_async current_site.username
@ -423,24 +401,6 @@ get '/admin' do
slim :'admin' slim :'admin'
end 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 post '/admin/banip' do
require_admin require_admin
site = Site[username: params[:username]] site = Site[username: params[:username]]
@ -455,8 +415,8 @@ post '/admin/banip' do
redirect '/admin' redirect '/admin'
end end
sites = Site.filter(ip: site.ip).all sites = Site.filter(ip: site.ip, is_banned: false).all
sites.each {|s| ban_site(s.username)} sites.each {|s| s.ban!}
flash[:error] = "#{sites.length} sites have been banned." flash[:error] = "#{sites.length} sites have been banned."
redirect '/admin' redirect '/admin'
end end
@ -476,7 +436,7 @@ post '/admin/banhammer' do
redirect '/admin' redirect '/admin'
end end
ban_site params[:username] site.ban!
flash[:success] = 'MISSION ACCOMPLISHED' flash[:success] = 'MISSION ACCOMPLISHED'
redirect '/admin' redirect '/admin'
@ -577,18 +537,9 @@ post '/custom_domain' do
require_login require_login
original_domain = current_site.domain original_domain = current_site.domain
current_site.domain = params[:domain] current_site.domain = params[:domain]
if current_site.valid? if current_site.valid?
current_site.save
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
flash[:success] = 'The domain has been successfully updated.' flash[:success] = 'The domain has been successfully updated.'
redirect '/custom_domain' redirect '/custom_domain'
else else
@ -660,18 +611,6 @@ def current_site
@site ||= Site[id: session[:id]] @site ||= Site[id: session[:id]]
end 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) def encoding_fix(file)
begin begin
Rack::Utils.escape_html file 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/ return Rack::Utils.escape_html(file.force_encoding('BINARY')) if e.message =~ /invalid byte sequence in UTF-8/
fail fail
end end
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :hits DB.drop_column :sites, :hits
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :is_admin DB.drop_column :sites, :is_admin
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :is_banned DB.drop_column :sites, :is_banned
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :ip DB.drop_column :sites, :ip
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :password_reset_token DB.drop_column :sites, :password_reset_token
DB.drop_index :sites, :password_reset_token
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :site_changed DB.drop_column :sites, :site_changed
DB.drop_index :sites, :site_changed
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :changed_count DB.drop_column :sites, :changed_count
DB.drop_index :sites, :changed_count
} }
end end

View file

@ -1,11 +1,44 @@
require 'tilt'
class Site < Sequel::Model class Site < Sequel::Model
# We might need to include fonts in here.. # 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_MIME_TYPES = %w{
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 } text/plain
#USERNAME_SHITLIST = %w{ payment secure login signin www ww web } # I thought they were funny personally, but everybody is freaking out so.. 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 MAX_SPACE = (5242880*2) # 10MB
MINIMUM_PASSWORD_LENGTH = 5 MINIMUM_PASSWORD_LENGTH = 5
BAD_USERNAME_REGEX = /[^\w-]/i 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_one :server
many_to_many :tags many_to_many :tags
@ -51,6 +84,87 @@ class Site < Sequel::Model
super super
end 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 def after_save
if @new_tag_strings if @new_tag_strings
@new_tag_strings.each do |new_tag_string| @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.' 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 end
if values[:username].nil? || values[:username].empty? || values[:username].match(BAD_USERNAME_REGEX) if !values[:username].match(VALID_HOSTNAME)
errors.add :username, 'A valid username is required.' 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 end
# Check for existing user # Check for existing user
user = self.class.select(:id, :username).filter(username: values[:username]).first user = self.class.select(:id, :username).filter(username: values[:username]).first
if user if user
@ -108,16 +224,24 @@ class Site < Sequel::Model
end end
end end
def file_path def render_template(name)
File.join DIR_ROOT, 'public', 'sites', username 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 end
def file_list 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 end
def total_space 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 space.nil? ? 0 : space
end end

143
tests/acceptance_tests.rb Normal file
View 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

View file

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

View file

@ -20,7 +20,7 @@ Bundler.require :test
require 'minitest/autorun' require 'minitest/autorun'
require 'sidekiq/testing/inline' 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 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', 0
Sequel::Migrator.apply DB, './migrations' Sequel::Migrator.apply DB, './migrations'
Server.create ip: '127.0.0.1', slots_available: 999999
Fabrication.configure do |config| Fabrication.configure do |config|
config.fabricator_path = 'tests/fabricators' config.fabricator_path = 'tests/fabricators'
config.path_prefix = DIR_ROOT config.path_prefix = DIR_ROOT
end end
I18n.enforce_available_locales = true

View file

@ -1,4 +1,4 @@
Fabricator(:site) do Fabricator(:site) do
username { Faker::Internet.email } username { SecureRandom.hex }
password { 'abcde' } password { 'abcde' }
end end

View file

@ -20,8 +20,7 @@ javascript:
span span
<i class="icon-file-alt icon-3x"></i>&nbsp;&nbsp; <span style="font-size: 20pt">#{file.filename}</span> <i class="icon-file-alt icon-3x"></i>&nbsp;&nbsp; <span style="font-size: 20pt">#{file.filename}</span>
- if file.filename == 'index.html' - if file.filename == 'index.html'
p.tiny 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.
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>.
div style="margin-bottom:30px" div style="margin-bottom:30px"
span span
@ -36,14 +35,23 @@ javascript:
span span
i class="icon-edit" &nbsp;&nbsp; i class="icon-edit" &nbsp;&nbsp;
span: a href="/site_files/download/#{file.filename}" Download <br /> span: a href="/site_files/download/#{file.filename}" Download <br />
span
i class="icon-trash" &nbsp;&nbsp; - if file.filename != 'index.html'
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete span
i class="icon-trash" &nbsp;&nbsp;
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete
- else - else
<i class="icon-picture icon-3x"></i>&nbsp;&nbsp; <span style="font-size: 20pt">#{file.filename}</span> <i class="icon-picture icon-3x"></i>&nbsp;&nbsp; <span style="font-size: 20pt">#{file.filename}</span>
div style="margin-top: 3px; margin-bottom:10px" div style="margin-top: 3px; margin-bottom: 30px"
| To use in an HTML file, paste this text: <code class="tiny" style="margin:0">&lt;img src="/#{file.filename}"&gt;</code> | To use in an HTML file, paste this text: <code class="tiny" style="margin:0">&lt;img src="/#{file.filename}"&gt;</code>
span
i class="icon-globe" &nbsp;&nbsp;
a href="http://#{current_site.username}.neocities.org/#{file.filename}" target="_blank" View <br /> a href="http://#{current_site.username}.neocities.org/#{file.filename}" target="_blank" View <br />
span
i class="icon-edit" &nbsp;&nbsp;
span: a href="/site_files/download/#{file.filename}" Download <br />
span
i class="icon-trash" &nbsp;&nbsp;
a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete a href="#" onclick="confirmFileDelete('#{file.filename}')" Delete
.col.col-40 .col.col-40

View file

@ -19,35 +19,39 @@ javascript:
h2.txt-Center Create a New Home Page h2.txt-Center Create a New Home Page
.col.col-50 style="margin:0 auto; float:none" .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 br
h5 Username h5 Username
p.tiny <input class="input-Area" name="username" type="text" placeholder="yourusername" value="#{@site.username}" autocapitalize="off" autocorrect="off">.neocities.org 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". 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 h5 Password
input class="input-Area" name="password" type="password" input class="input-Area" name="password" type="password"
br 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> 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 h5 Email
input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}" input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}"
br 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). 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 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" 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" input name="is_nsfw" type="hidden" value="false"
p: strong If your page will contain objectionable (adult) content, check this box:&nbsp;&nbsp;&nbsp;<input name="is_nsfw" type="checkbox" value="true" style="margin-top:0"> p If your page will contain objectionable (adult) content, check this box:&nbsp;&nbsp;&nbsp;<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). 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" div style="background:#fff; width:100%; overflow:auto"
== recaptcha_tag :challenge, ssl: true == recaptcha_tag :challenge, ssl: true

View file

@ -53,9 +53,7 @@ css:
option value="ace/theme/twilight" Twilight option value="ace/theme/twilight" Twilight
option value="ace/theme/vibrant_ink" Vibrant Ink option value="ace/theme/vibrant_ink" Vibrant Ink
div id="editor" style="width: 100%; height: 600px; position: relative; margin-bottom:25px" <div id="editor" style="width: 100%; height: 600px; position: relative; margin-bottom:25px">#{{encoding_fix(@file_data)}}</div>
== encoding_fix @file_data
.row .row
.col.col-33.txt-Center style="margin-bottom:10px" .col.col-33.txt-Center style="margin-bottom:10px"
@ -125,4 +123,5 @@ css:
editor.getSession().setUseWrapMode(true); editor.getSession().setUseWrapMode(true);
editor.setFontSize(14); editor.setFontSize(14);
editor.setShowPrintMargin(false); editor.setShowPrintMargin(false);
});
});

View file

@ -1,8 +1,8 @@
server { server {
listen 80; listen 80;
server_name <%= current_site.domain %> *.<%= current_site.domain %>; server_name <%= domain %> *.<%= domain %>;
access_log /var/log/nginx/neocities-domains.log neocitiesdomain; 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; index /index.html;
error_page 404 = @notfound; error_page 404 = @notfound;

View file

@ -3,7 +3,7 @@ html
head head
meta http-equiv="Content-Type" content="text/html; charset=UTF-8" 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="description" content=""
meta name="keywords" content="" meta name="keywords" content=""

View file

@ -3,7 +3,7 @@ html
head head
meta http-equiv="Content-Type" content="text/html; charset=UTF-8" 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" link href="//groundfloor.neocities.org/default.css" rel="stylesheet" type="text/css" media="all"