require 'base64'
require 'uri'
require 'net/http'
require './environment.rb'
use Rack::Session::Cookie, key: 'neocities',
path: '/',
expire_after: 31556926, # one year in seconds
secret: $config['session_secret']
use Rack::Recaptcha, public_key: $config['recaptcha_public_key'], private_key: $config['recaptcha_private_key']
helpers Rack::Recaptcha::Helpers
helpers do
def site_change_file_display_class(filename)
return 'html' if filename.match(Site::HTML_REGEX)
return 'image' if filename.match(Site::IMAGE_REGEX)
'misc'
end
def csrf_token_input_html
%{}
end
end
before do
if request.path.match /^\/api\//i
@api = true
content_type :json
elsif request.path.match /^\/stripe_webhook$/
# Skips the CSRF check for stripe web hooks
else
content_type :html, 'charset' => 'utf-8'
redirect '/' if request.post? && !csrf_safe?
end
end
not_found do
erb :'not_found'
end
error do
EmailWorker.perform_async({
from: 'web@neocities.org',
to: 'errors@neocities.org',
subject: "[Neocities Error] #{env['sinatra.error'].class}: #{env['sinatra.error'].message}",
body: "#{request.request_method} #{request.path}\n\n" +
(current_site ? "Site: #{current_site.username}\nEmail: #{current_site.email}\n\n" : '') +
env['sinatra.error'].backtrace.join("\n")
})
if @api
api_error 500, 'server_error', 'there has been an unknown server error, please try again later'
end
erb :'error'
end
# :nocov:
get '/newindex_mockup' do
if SimpleCache.expired?(:sites_count)
@sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 600 # 10 Minutes
else
@sites_count = SimpleCache.get :sites_count
end
erb :newindex_mockup, layout: false
end
get '/home_mockup' do
erb :'home_mockup'
end
get '/edit_mockup' do
erb :'edit_mockup'
end
get '/profile_mockup' do
require_login
erb :'profile_mockup', locals: {site: current_site}
end
get '/browse_mockup' do
erb :'browse_mockup'
end
get '/tips_mockup' do
erb :'tips_mockup'
end
# :nocov:
get '/site/:username.rss' do |username|
site = Site[username: username]
content_type :xml
site.to_rss.to_xml
end
get '/site/:username/?' do |username|
site = Site[username: username]
not_found if site.nil? || site.is_banned
@title = site.title
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
if params[:event_id]
event = Event.select(:id).where(id: params[:event_id]).first
not_found if event.nil?
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
else
events_dataset = site.latest_events(@current_page, 10)
end
@page_count = events_dataset.page_count || 1
@latest_events = events_dataset.all
erb :'site', locals: {site: site, is_current_site: site == current_site}
end
post '/site/:username/set_editor_theme' do
require_login
current_site.update editor_theme: params[:editor_theme]
'ok'
end
post '/settings/create_child' do
require_login
site = Site.new
site.parent_site_id = parent_site.id
site.username = params[:username]
if site.valid?
site.save
flash[:success] = 'Your new site has been created!'
redirect '/settings#sites'
else
flash[:error] = site.errors.first.last.first
redirect '/settings#sites'
end
end
post '/site/:username/comment' do |username|
require_login
site = Site[username: username]
if(site.profile_comments_enabled == false ||
params[:message].empty? ||
site.is_blocking?(current_site) ||
current_site.is_blocking?(site) ||
current_site.commenting_allowed? == false)
redirect "/site/#{username}"
end
site.add_profile_comment(
actioning_site_id: current_site.id,
message: params[:message]
)
redirect "/site/#{username}"
end
get '/stats/?' do
require_admin
@stats = {
total_sites: Site.count,
total_unbanned_sites: Site.where(is_banned: false).count,
total_banned_sites: Site.where(is_banned: true).count,
total_nsfw_sites: Site.where(is_nsfw: true).count,
total_unbanned_nsfw_sites: Site.where(is_banned: false, is_nsfw: true).count,
total_banned_nsfw_sites: Site.where(is_banned: true, is_nsfw: true).count
}
# Start with the date of the first created site
start = Site.select(:created_at).
exclude(created_at: nil).
order(:created_at).
first[:created_at].to_date
runner = start
monthly_stats = []
now = Time.now
until runner.year == now.year && runner.month == now.month+1
monthly_stats.push(
date: runner,
sites_created: Site.where(created_at: runner..runner.next_month).count,
total_from_start: Site.where(created_at: start..runner.next_month).count,
supporters: Site.where(created_at: start..runner.next_month).exclude(stripe_customer_id: nil).count,
)
runner = runner.next_month
end
@stats[:monthly_stats] = monthly_stats
customers = Stripe::Customer.all
@stats[:total_recurring_revenue] = 0.0
subscriptions = []
cancelled_subscriptions = 0
customers.each do |customer|
sub = {created_at: Time.at(customer.created)}
if customer[:subscriptions]
if customer[:subscriptions][:data].empty?
sub[:status] = 'cancelled'
else
sub[:status] = 'active'
sub[:amount] = (customer[:subscriptions][:data].first[:plan][:amount] / 100.0).round(2)
@stats[:total_recurring_revenue] += sub[:amount]
end
end
subscriptions.push sub
end
@stats[:subscriptions] = subscriptions
erb :'stats'
end
get '/?' do
if current_site
require_login
@suggestions = current_site.suggestions
@current_page = params[:current_page].to_i
@current_page = 1 if @current_page == 0
if params[:activity] == 'mine'
events_dataset = current_site.latest_events(@current_page, 10)
elsif params[:event_id]
event = Event.select(:id).where(id: params[:event_id]).first
not_found if event.nil?
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
else
events_dataset = current_site.news_feed(@current_page, 10)
end
@page_count = events_dataset.page_count || 1
@events = events_dataset.all
halt erb :'home', locals: {site: current_site}
end
if SimpleCache.expired?(:sites_count)
@sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 600 # 10 Minutes
else
@sites_count = SimpleCache.get :sites_count
end
erb :index, layout: false
end
get '/plan/?' do
@title = 'Supporter'
erb :'plan/index'
end
post '/plan/create' do
require_login
DB.transaction do
customer = Stripe::Customer.create(
card: params[:stripe_token],
description: current_site.username,
email: current_site.email,
plan: params[:selected_plan]
)
current_site.update stripe_customer_id: customer.id, plan_ended: false
plan_name = customer.subscriptions.first['plan']['name']
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: current_site.email,
subject: "[Neocities] You've become a supporter!",
body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: plan_name)
})
end
redirect '/plan'
end
def get_plan_name(customer_id)
subscriptions = Stripe::Customer.retrieve(current_site.stripe_customer_id).subscriptions.all
@plan_name = subscriptions.first.plan.name
end
get '/plan/manage' do
require_login
redirect '/plan' unless current_site.supporter? && !current_site.plan_ended
@title = 'Manage Plan'
@plan_name = get_plan_name current_site.stripe_customer_id
erb :'plan/manage'
end
get '/plan/end' do
require_login
redirect '/plan' unless current_site.supporter? && !current_site.plan_ended
@title = 'End Plan'
@plan_name = get_plan_name current_site.stripe_customer_id
erb :'plan/end'
end
post '/plan/end' do
require_login
redirect '/plan' unless current_site.supporter? && !current_site.plan_ended
recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?
if !recaptcha_is_valid
@error = 'Recaptcha was filled out incorrectly, please try re-entering.'
@plan_name = get_plan_name current_site.stripe_customer_id
halt erb :'plan/end'
end
customer = Stripe::Customer.retrieve current_site.stripe_customer_id
subscriptions = customer.subscriptions.all
DB.transaction do
subscriptions.each do |subscription|
customer.subscriptions.retrieve(subscription.id).delete
end
current_site.update plan_ended: true
end
redirect '/plan'
end
get '/site/:username/tip' do |username|
@site = Site[username: username]
@title = "Tip #{@site.title}"
erb :'tip'
end
post '/site/:site_id/toggle_follow' do |site_id|
require_login
content_type :json
site = Site[id: site_id]
{result: (current_site.toggle_follow(site) ? 'followed' : 'unfollowed')}.to_json
end
post '/tags/add' do
require_login
current_site.new_tags_string = params[:tags]
if current_site.valid?
current_site.save_changes
else
flash[:errors] = current_site.errors.first
end
redirect request.referer
end
post '/tags/remove' do
require_login
DB.transaction {
params[:tags].each {|tag| current_site.remove_tag Tag[name: tag]}
}
redirect request.referer
end
get '/tags/autocomplete/:name.json' do |name|
Tag.autocomplete(name).collect {|t| t[:name]}.to_json
end
get '/browse/?' do
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
site_dataset = Site.filter(is_banned: false, is_crashing: false).filter(site_changed: true)
if current_site
if !current_site.blocking_site_ids.empty?
site_dataset.where!(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
end
if current_site.blocks_dataset.count
site_dataset.where!(
Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocks_dataset.select(:actioning_site_id).all.collect {|s| s.actioning_site_id})
)
end
end
case params[:sort_by]
when 'hits'
site_dataset.order!(:hits.desc)
when 'views'
site_dataset.order!(:views.desc)
when 'newest'
site_dataset.order!(:created_at.desc)
when 'oldest'
site_dataset.order!(:created_at)
when 'random'
site_dataset.where! 'random() < 0.01'
when 'last_updated'
params[:sort_by] = 'last_updated'
site_dataset.order!(:updated_at.desc, :views.desc)
else
if params[:tag]
params[:sort_by] = 'views'
site_dataset.order!(:views.desc)
else
params[:sort_by] = 'last_updated'
site_dataset.order!(:updated_at.desc, :views.desc)
end
end
site_dataset.filter! is_nsfw: (params[:is_nsfw] == 'true' ? true : false)
if params[:tag]
site_dataset = site_dataset.association_join(:tags)
site_dataset.where! ['tags.name = ?', params[:tag]]
end
site_dataset = site_dataset.paginate @current_page, 300
@page_count = site_dataset.page_count || 1
@sites = site_dataset.all
erb :browse
end
get '/api' do
@title = 'Developers API'
erb :'api'
end
get '/tutorials' do
erb :'tutorials'
end
get '/donate' do
erb :'donate'
end
get '/blog' do
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
return Net::HTTP.get_response(URI("http://blog.neocities.org/#{article}.html")).body
end
get '/new' do
dashboard_if_signed_in
require_unbanned_ip
@site = Site.new
@site.username = params[:username] unless params[:username].nil?
erb :'new'
end
post '/create' do
require_unbanned_ip
dashboard_if_signed_in
@site = Site.new(
username: params[:username],
password: params[:password],
email: params[:email],
new_tags_string: params[:tags],
is_nsfw: params[:is_nsfw],
ip: request.ip
)
recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?
if @site.valid? && recaptcha_is_valid
DB.transaction do
if !params[:stripe_token].nil? && params[:stripe_token] != ''
customer = Stripe::Customer.create(
card: params[:stripe_token],
description: @site.username,
email: @site.email,
plan: params[:selected_plan]
)
@site.stripe_customer_id = customer.id
plan_name = customer.subscriptions.first['plan']['name']
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: @site.email,
subject: "[Neocities] You've become a supporter!",
body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: plan_name)
})
end
@site.save
end
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: @site.email,
subject: "[Neocities] Welcome to Neocities!",
body: Tilt.new('./views/templates/email_welcome.erb', pretty: true).render(self)
})
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: @site.email,
subject: "[Neocities] Welcome to Neocities!",
body: Tilt.new('./views/templates/email_welcome.erb', pretty: true).render(self)
})
send_confirmation_email @site
session[:id] = @site.id
redirect '/'
else
@site.errors.add :captcha, 'You must type in the captcha correctly! Try again.' if !recaptcha_is_valid
erb :'/new'
end
end
get '/dashboard' do
require_login
if params[:dir] && params[:dir][0] != '/'
params[:dir] = '/'+params[:dir]
end
if !File.directory?(current_site.files_path(params[:dir]))
redirect '/dashboard'
end
@dir = params[:dir]
@file_list = current_site.file_list @dir
erb :'dashboard'
end
get '/settings/?' do
require_login
@site = parent_site
erb :'settings/account'
end
def require_ownership_for_settings
@site = Site[username: params[:username]]
unless @site.owned_by? parent_site
flash[:error] = 'Cannot edit this site, you do not have permission.'
redirect request.referrer
end
end
get '/settings/:username/?' do
require_login
require_ownership_for_settings
erb :'settings/site'
end
post '/settings/:username/profile' do
require_login
require_ownership_for_settings
@site.update(
profile_comments_enabled: params[:site][:profile_comments_enabled]
)
flash[:success] = 'Profile settings changed.'
redirect "/settings/#{@site.username}#profile"
end
post '/settings/:username/ssl' do
require_login
require_ownership_for_settings
unless params[:key] && params[:cert]
flash[:error] = 'SSL key and certificate are required.'
redirect "/settings/#{@site.username}#custom_domain"
end
begin
key = OpenSSL::PKey::RSA.new params[:key][:tempfile].read, ''
rescue => e
flash[:error] = 'Could not process SSL key, file may be incorrect, damaged, or passworded (you need to remove the password).'
redirect "/settings/#{@site.username}#custom_domain"
end
if !key.private?
flash[:error] = 'SSL Key file does not have private key data.'
redirect "/settings/#{@site.username}#custom_domain"
end
certs_string = params[:cert][:tempfile].read
cert_array = certs_string.lines.slice_before(/-----BEGIN CERTIFICATE-----/).to_a.collect {|a| a.join}
if cert_array.empty?
flash[:error] = 'Cert file does not contain any certificates.'
redirect "/settings/#{@site.username}#custom_domain"
end
cert_valid_for_domain = false
cert_array.each do |cert_string|
begin
cert = OpenSSL::X509::Certificate.new cert_string
rescue => e
flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.'
redirect "/settings/#{@site.username}#custom_domain"
end
if cert.not_after < Time.now
flash[:error] = 'SSL Certificate has expired, please create a new one.'
redirect "/settings/#{@site.username}#custom_domain"
end
cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1]
cert_valid_for_domain = true if cert_cn && cert_cn.match(@site.domain)
end
unless cert_valid_for_domain
flash[:error] = "Your certificate CN (common name) does not match your domain: #{@site.domain}"
redirect "/settings/#{@site.username}#custom_domain"
end
# Everything else was worse.
crtfile = Tempfile.new 'crtfile'
crtfile.write cert_array.join
crtfile.close
keyfile = Tempfile.new 'keyfile'
keyfile.write key.to_pem
keyfile.close
if ENV['TRAVIS'] != 'true'
nginx_testfile = Tempfile.new 'nginx_testfile'
nginx_testfile.write %{
pid /tmp/throwaway.pid;
events {}
error_log /dev/null error;
http {
access_log off;
server {
listen 60000 ssl;
server_name #{@site.domain} *.#{@site.domain};
ssl_certificate #{crtfile.path};
ssl_certificate_key #{keyfile.path};
}
}
}
nginx_testfile.close
line = Cocaine::CommandLine.new(
"nginx", "-t -c :path",
expected_outcodes: [0],
swallow_stderr: true
)
begin
output = line.run path: nginx_testfile.path
rescue Cocaine::ExitStatusError => e
flash[:error] = "There is something wrong with your certificate, please check with your issuing CA."
redirect "/settings/#{@site.username}#custom_domain"
end
end
@site.update ssl_key: key.to_pem, ssl_cert: cert_array.join
flash[:success] = 'Updated SSL key/certificate.'
redirect "/settings/#{@site.username}#custom_domain"
end
post '/settings/:username/change_name' do
require_login
require_ownership_for_settings
old_username = @site.username
if params[:name] == nil || params[:name] == ''
flash[:error] = 'Name cannot be blank.'
redirect "/settings/#{@site.username}#username"
end
if old_username == params[:name]
flash[:error] = 'You already have this name.'
redirect "/settings/#{@site.username}#username"
end
old_host = @site.host
old_file_paths = @site.file_list.collect {|f| f[:path]}
@site.username = params[:name]
if @site.valid?
DB.transaction {
@site.save_changes
@site.move_files_from old_username
}
old_file_paths.each do |file_path|
@site.purge_cache file_path
end
flash[:success] = "Site/user name has been changed. You will need to use this name to login, don't forget it."
redirect "/settings/#{@site.username}#username"
else
flash[:error] = @site.errors.first.last.first
redirect "/settings/#{old_username}#username"
end
end
post '/settings/:username/change_nsfw' do
require_login
require_ownership_for_settings
@site.update is_nsfw: params[:is_nsfw]
flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+'
redirect "/settings/#{@site.username}#nsfw"
end
post '/settings/:username/custom_domain' do
require_login
require_ownership_for_settings
@site.domain = params[:domain]
if @site.valid?
@site.save_changes
flash[:success] = 'The domain has been successfully updated.'
redirect "/settings/#{@site.username}#custom_domain"
else
flash[:error] = @site.errors.first.last.first
redirect "/settings/#{@site.username}#custom_domain"
end
end
post '/settings/change_password' do
require_login
if !Site.valid_login?(parent_site.username, params[:current_password])
flash[:error] = 'Your provided password does not match the current one.'
redirect "/settings#password"
end
parent_site.password = params[:new_password]
parent_site.valid?
if params[:new_password] != params[:new_password_confirm]
parent_site.errors.add :password, 'New passwords do not match.'
end
if parent_site.errors.empty?
parent_site.save_changes
flash[:success] = 'Successfully changed password.'
redirect "/settings#password"
else
flash[:error] = current_site.errors.first.last.first
redirect '/settings#password'
end
end
post '/settings/change_email' do
require_login
if params[:email] == parent_site.email
flash[:error] = 'You are already using this email address for this account.'
redirect '/settings#email'
end
parent_site.email = params[:email]
parent_site.email_confirmation_token = SecureRandom.hex 3
parent_site.email_confirmed = false
if parent_site.valid?
parent_site.save_changes
send_confirmation_email
flash[:success] = 'Successfully changed email. We have sent a confirmation email, please use it to confirm your email address.'
redirect '/settings#email'
end
flash[:error] = parent_site.errors.first.last.first
redirect '/settings#email'
end
get '/password_reset' do
erb :'password_reset'
end
post '/send_password_reset' do
sites = Site.filter(email: params[:email]).all
if sites.length > 0
token = SecureRandom.uuid.gsub('-', '')
sites.each do |site|
site.update password_reset_token: token
end
body = <<-EOT
Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. Purrrr.
Go to this URL to reset your password: http://neocities.org/password_reset_confirm?token=#{token}
After clicking on this link, your password for all the sites registered to this email address will be changed to this token.
Token: #{token}
If you didn't request this reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
Meow,
the Neocities Cat
EOT
body.strip!
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:email],
subject: '[Neocities] Password Reset',
body: body
})
end
flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.'
redirect '/'
end
get '/password_reset_confirm' do
if params[:token].nil? || params[:token].empty?
flash[:error] = 'Could not find a site with this token.'
redirect '/'
end
reset_site = Site[password_reset_token: params[:token]]
if reset_site.nil?
flash[:error] = 'Could not find a site with this token.'
redirect '/'
end
sites = Site.filter(email: reset_site.email).all
if sites.length > 0
sites.each do |site|
site.password = reset_site.password_reset_token
site.save_changes
end
flash[:success] = 'Your password for all sites with your email address has been changed to the token sent in your e-mail. Please login and change your password as soon as possible.'
else
flash[:error] = 'Could not find a site with this token.'
end
redirect '/'
end
get '/signin/?' do
dashboard_if_signed_in
erb :'signin'
end
post '/signin' do
dashboard_if_signed_in
if Site.valid_login? params[:username], params[:password]
site = Site.get_with_identifier params[:username]
if site.is_banned
flash[:error] = 'Invalid login.'
flash[:username] = params[:username]
redirect '/signin'
end
session[:id] = site.id
redirect '/'
else
flash[:error] = 'Invalid login.'
flash[:username] = params[:username]
redirect '/signin'
end
end
get '/signout' do
require_login
session[:id] = nil
redirect '/'
end
get '/signin/:username' do
require_login
@site = Site[username: params[:username]]
not_found if @site.nil?
if @site.owned_by? current_site
session[:id] = @site.id
redirect request.referrer
end
flash[:error] = 'You do not have permission to switch to this site.'
redirect request.referrer
end
get '/about' do
erb :'about'
end
get '/site_files/new_page' do
require_login
erb :'site_files/new_page'
end
post '/site_files/create_page' do
require_login
@errors = []
params[:pagefilename].gsub!(/[^a-zA-Z0-9_\-.]/, '')
params[:pagefilename].gsub!(/\.html$/i, '')
if params[:pagefilename].nil? || params[:pagefilename].empty?
@errors << 'You must provide a file name.'
halt erb(:'site_files/new_page')
end
name = "#{params[:pagefilename]}.html"
if current_site.file_exists?(name)
@errors << %{Web page "#{name}" already exists! Choose another name.}
halt erb(:'site_files/new_page')
end
current_site.install_new_html_file name
flash[:success] = %{#{name} was created! Click here to edit it.}
redirect '/dashboard'
end
get '/site_files/new' do
require_login
erb :'site_files/new'
end
def file_upload_response(error=nil)
http_error_code = 406
if params[:from_button]
if error
@error = error
halt 200, erb(:'dashboard')
else
query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : ''
redirect "/dashboard#{query_string}"
end
else
halt http_error_code, error if error
halt 200, 'File(s) successfully uploaded.'
end
end
post '/site/create_directory' do
require_login
path = "#{params[:dir] || ''}/#{params[:name]}"
result = current_site.create_directory path
if result != true
flash[:error] = e.message
end
redirect "/dashboard?dir=#{Rack::Utils.escape params[:dir]}"
end
post '/site_files/upload' do
require_login
@errors = []
http_error_code = 406
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
params[:files].each do |file|
file[:filename] = "#{params[:dir]}/#{file[:filename]}" if params[:dir]
if current_site.file_size_too_large? file[:tempfile].size
file_upload_response "#{params[:dir]}/#{file[:filename]} is too large, upload cancelled."
end
if !Site.valid_file_type? file
file_upload_response "#{params[:dir]}/#{file[:filename]}: file type (or content in file) is not allowed on Neocities, upload cancelled."
end
end
uploaded_size = params[:files].collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }
if current_site.file_size_too_large? uploaded_size
file_upload_response "File(s) do not fit in your available space, upload cancelled."
end
results = []
params[:files].each do |file|
results << current_site.store_file(file[:filename], file[:tempfile])
end
current_site.increment_changed_count if results.include?(true)
file_upload_response
end
post '/site_files/delete' do
require_login
current_site.delete_file params[:filename]
flash[:success] = "Deleted #{params[:filename]}."
redirect '/dashboard'
end
get '/site_files/:username.zip' do |username|
require_login
zipfile_path = current_site.files_zip
content_type 'application/octet-stream'
attachment "neocities-#{current_site.username}.zip"
send_file zipfile_path
end
get '/site_files/download/:filename' do |filename|
require_login
content_type 'application/octet-stream'
attachment filename
current_site.get_file filename
end
get %r{\/site_files\/text_editor\/(.+)} do
require_login
@filename = params[:captures].first
begin
@file_data = current_site.get_file @filename
rescue Errno::ENOENT
flash[:error] = 'We could not find the requested file.'
redirect '/dashboard'
end
erb :'site_files/text_editor'
end
post %r{\/site_files\/save\/(.+)} do
require_login_ajax
filename = params[:captures].first
tempfile = Tempfile.new 'neocities_saving_file'
input = request.body.read
tempfile.set_encoding input.encoding
tempfile.write input
tempfile.close
if current_site.file_size_too_large? tempfile.size
halt 'File is too large to fit in your space, it has NOT been saved. You will need to reduce the size or upgrade to a new plan.'
end
current_site.store_file filename, tempfile
'ok'
end
get '/site_files/allowed_types' do
erb :'site_files/allowed_types'
end
get '/site_files/mount_info' do
erb :'site_files/mount_info'
end
get '/terms' do
erb :'terms'
end
get '/privacy' do
erb :'privacy'
end
get '/admin' do
require_admin
@banned_sites = Site.select(:username).filter(is_banned: true).order(:username).all
@nsfw_sites = Site.select(:username).filter(is_nsfw: true).order(:username).all
erb :'admin'
end
post '/admin/banip' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
if site.ip.nil? || site.ip.empty?
flash[:error] = 'IP is blank, cannot continue'
redirect '/admin'
end
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
post '/admin/banhammer' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
if site.is_banned
flash[:error] = 'User is already banned'
redirect '/admin'
end
site.ban!
flash[:success] = 'MISSION ACCOMPLISHED'
redirect '/admin'
end
post '/admin/mark_nsfw' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
site.is_nsfw = true
site.save_changes validate: false
flash[:success] = 'MISSION ACCOMPLISHED'
redirect '/admin'
end
get '/contact' do
erb :'contact'
end
post '/contact' do
@errors = []
if params[:email].empty? || params[:subject].empty? || params[:body].empty?
@errors << 'Please fill out all fields'
end
if !recaptcha_valid?
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
end
if !@errors.empty?
erb :'contact'
else
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: params[:email],
to: 'contact@neocities.org',
subject: "[Neocities Contact]: #{params[:subject]}",
body: params[:body]
})
flash[:success] = 'Your contact has been sent.'
redirect '/'
end
end
post '/stripe_webhook' do
event = JSON.parse request.body.read
if event['type'] == 'customer.created'
username = event['data']['object']['description']
email = event['data']['object']['email']
end
'ok'
end
post '/api/upload' do
require_api_credentials
files = []
params.each do |k,v|
next unless v.is_a?(Hash) && v[:tempfile]
path = k.to_s
files << {filename: k || v[:filename], tempfile: v[:tempfile]}
end
api_error 400, 'missing_files', 'you must provide files to upload' if files.empty?
uploaded_size = files.collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }
if current_site.file_size_too_large? uploaded_size
api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files'
end
files.each do |file|
if !Site.valid_file_type?(file)
api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content), files have not been uploaded"
end
if File.directory? file[:filename]
api_error 400, 'directory_exists', 'this name is being used by a directory, cannot continue'
end
end
results = []
files.each do |file|
results << current_site.store_file(file[:filename], file[:tempfile])
end
current_site.increment_changed_count if results.include?(true)
api_success 'your file(s) have been successfully uploaded'
end
post '/api/delete' do
require_api_credentials
api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?
paths = []
params[:filenames].each do |path|
unless path.is_a?(String)
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
end
if !current_site.file_exists?(path)
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
end
if path == 'index.html'
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
end
paths << path
end
paths.each do |path|
current_site.delete_file(path)
end
api_success 'file(s) have been deleted'
end
get '/api/info' do
if params[:sitename]
site = Site[username: params[:sitename]]
api_error 400, 'site_not_found', "could not find site #{params[:sitename]}" if site.nil? || site.is_banned
api_success api_info_for(site)
else
init_api_credentials
api_success api_info_for(current_site)
end
end
def api_info_for(site)
{
info: {
sitename: site.username,
views: site.views,
hits: site.hits,
created_at: site.created_at.rfc2822,
last_updated: site.updated_at.rfc2822,
domain: site.domain,
tags: site.tags.collect {|t| t.name}
}
}
end
# Catch-all for missing api calls
get '/api/:name' do
api_not_found
end
post '/api/:name' do
api_not_found
end
post '/event/:event_id/toggle_like' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
liked_response = event.toggle_site_like(current_site) ? 'liked' : 'unliked'
{result: liked_response, event_like_count: event.likes_dataset.count, liking_site_names: event.liking_site_usernames}.to_json
end
post '/event/:event_id/comment' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
site = event.site
if site.is_blocking?(current_site) ||
site.profile_comments_enabled == false ||
current_site.commenting_allowed? == false
return {result: 'error'}.to_json
end
event.add_site_comment current_site, params[:message]
{result: 'success'}.to_json
end
post '/event/:event_id/update_profile_comment' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
return {result: 'error'}.to_json unless current_site.id == event.profile_comment.actioning_site_id
event.profile_comment.update message: params[:message]
return {result: 'success'}.to_json
end
post '/event/:event_id/delete' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
if event.site_id == current_site.id || event.created_by?(current_site)
event.delete
return {result: 'success'}.to_json
end
return {result: 'error'}.to_json
end
post '/comment/:comment_id/toggle_like' do |comment_id|
require_login
content_type :json
comment = Comment[id: comment_id]
liked_response = comment.toggle_site_like(current_site) ? 'liked' : 'unliked'
{result: liked_response, comment_like_count: comment.comment_likes_dataset.count, liking_site_names: comment.liking_site_usernames}.to_json
end
post '/comment/:comment_id/delete' do |comment_id|
require_login
content_type :json
comment = Comment[id: comment_id]
if comment.event.site == current_site || comment.actioning_site == current_site
comment.delete
return {result: 'success'}.to_json
end
return {result: 'error'}.to_json
end
get '/site/:username/confirm_email/:token' do
site = Site[username: params[:username]]
if site.email_confirmation_token == params[:token]
site.email_confirmed = true
site.save_changes
erb :'site_email_confirmed'
else
erb :'site_email_not_confirmed'
end
end
post '/site/:username/report' do |username|
site = Site[username: username]
redirect request.referer if site.nil?
report = Report.new site_id: site.id, type: params[:type], comments: params[:comments]
if current_site
redirect request.referer if current_site.id == site.id
report.reporting_site_id = current_site.id
else
report.ip = request.ip
end
report.save
EmailWorker.perform_async({
from: 'web@neocities.org',
to: 'report@neocities.org',
subject: "[Neocities Report] #{site.username} has been reported for #{report.type}",
body: "Reported by #{report.reporting_site_id ? report.reporting_site.username : report.ip}: #{report.comments}"
})
redirect request.referer
end
post '/site/:username/block' do |username|
require_login
site = Site[username: username]
redirect request.referer if current_site.id == site.id
current_site.block! site
if request.referer.match /\/site\/#{username}/i
redirect '/'
else
redirect request.referer
end
end
def require_admin
redirect '/' unless signed_in? && current_site.is_admin
end
def dashboard_if_signed_in
redirect '/dashboard' if signed_in?
end
def require_login_ajax
halt 'You are banned.' if Site.banned_ip?(request.ip)
halt 'You are not logged in!' unless signed_in?
end
def csrf_safe?
csrf_token == params[:csrf_token] || csrf_token == request.env['HTTP_X_CSRF_TOKEN']
end
def csrf_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
def require_login
require_unbanned_ip
redirect '/' unless signed_in?
end
def signed_in?
!session[:id].nil?
end
def current_site
return nil if session[:id].nil?
@_site ||= Site[id: session[:id]]
end
def parent_site
current_site.parent? ? current_site : current_site.parent
end
def require_unbanned_ip
if Site.banned_ip?(request.ip)
session[:id] = nil
flash[:error] = 'Your IP address has been banned due to misconduct. '+
'If you believe this to be in error, contact the site admin.'
redirect '/'
end
end
def title
out = "Neocities"
return out if request.path == '/'
return "#{out} - #{@title}" if @title
"#{out} - #{request.path.gsub('/', '').capitalize}"
end
def encoding_fix(file)
begin
Rack::Utils.escape_html file
rescue ArgumentError => e
return Rack::Utils.escape_html(file.force_encoding('BINARY')) if e.message =~ /invalid byte sequence in UTF-8/
fail
end
end
def require_api_credentials
if !request.env['HTTP_AUTHORIZATION'].nil?
init_api_credentials
else
api_error_invalid_auth
end
end
def init_api_credentials
auth = request.env['HTTP_AUTHORIZATION']
begin
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
rescue
api_error_invalid_auth
end
if Site.valid_login? user, pass
site = Site[username: user]
if site.nil? || site.is_banned
api_error_invalid_auth
end
session[:id] = site.id
else
api_error_invalid_auth
end
end
def api_success(message_or_obj)
output = {result: 'success'}
if message_or_obj.is_a?(String)
output[:message] = message_or_obj
else
output.merge! message_or_obj
end
api_response(200, output)
end
def api_response(status, output)
halt status, JSON.pretty_generate(output)+"\n"
end
def api_error(status, error_type, message)
api_response(status, result: 'error', error_type: error_type, message: message)
end
def api_error_invalid_auth
api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password'
end
def api_not_found
api_error 404, 'not_found', 'the requested api call does not exist'
end
def send_confirmation_email(site=current_site)
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: site.email,
subject: "[Neocities] Confirm your email address",
body: Tilt.new('./views/templates/email_confirm.erb', pretty: true).render(self, site: site)
})
end