neocities/app.rb
Kyle Drake 4983a9b24e Cryptographically scramble IPs stored by Neocities
"There is a time when the operation of the machine becomes so odious,
makes you so sick at heart, that you can't take part; you can't even
passively take part, and you've got to put your bodies upon the gears
and upon the wheels, upon the levers, upon all the apparatus, and you've
got to make it stop. And you've got to indicate to the people who run
it, to the people who own it, that unless you're free, the machine will
be prevented from working at all."

Mario Savio
Sproul Hall Steps
University of California, Berkeley
December 3, 1964
2014-11-07 00:41:10 -08:00

1660 lines
No EOL
42 KiB
Ruby

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
%{<input name="csrf_token" type="hidden" value="#{csrf_token}">}
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 '/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
current_site.events_dataset.update notification_seen: true
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
@blackbox_question = BlackBox.generate
@question_first_number, @question_last_number = generate_question
erb :index, layout: false
end
def generate_question
if ENV['RACK_ENV'] == 'test'
question_first_number = 1
question_last_number = 1
else
question_first_number = rand 5
question_last_number = rand 5
end
session[:question_answer] = (question_first_number + question_last_number).to_s
[question_first_number, question_last_number]
end
get '/plan/?' do
@title = 'Supporter'
erb :'plan/index'
end
post '/plan/update' do
require_login
DB.transaction do
if parent_site.stripe_subscription_id
customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
subscription = customer.subscriptions.retrieve parent_site.stripe_subscription_id
subscription.plan = params[:plan_type]
subscription.save
parent_site.update(
plan_ended: false,
plan_type: params[:plan_type]
)
else
customer = Stripe::Customer.create(
card: params[:stripe_token],
description: "#{parent_site.username} - #{parent_site.id}",
email: (current_site.email || parent_site.email),
plan: params[:plan_type]
)
parent_site.update(
stripe_customer_id: customer.id,
stripe_subscription_id: customer.subscriptions.first.id,
plan_ended: false,
plan_type: params[:plan_type]
)
end
end
if current_site.email || parent_site.email
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: current_site.email || parent_site.email,
subject: "[Neocities] You've become a supporter!",
body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: Site::PLAN_FEATURES[params[:plan_type].to_sym][:name], plan_space: Site::PLAN_FEATURES[params[:plan_type].to_sym][:space].to_space_pretty)
})
end
redirect params[:plan_type] == 'free' ? '/plan' : '/plan/thanks'
end
get '/plan/thanks' do
require_login
erb :'plan/thanks'
end
=begin
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: "#{parent_site.username} - #{parent_site.id}",
email: current_site.email,
plan: params[:selected_plan]
)
parent_site.update stripe_customer_id: customer.id, plan_ended: false
plan_name = customer.subscriptions.first['plan']['name']
if current_site.email || parent_site.email
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: current_site.email || parent_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
end
redirect '/plan'
end
def get_plan_name(customer_id)
subscriptions = Stripe::Customer.retrieve(parent_site.stripe_customer_id).subscriptions.all
@plan_name = subscriptions.first.plan.name
end
def require_active_subscription
redirect '/plan' unless parent_site.supporter? && !parent_site.plan_ended
end
get '/plan/manage' do
require_login
require_active_subscription
@title = 'Manage Plan'
@plan_name = get_plan_name parent_site.stripe_customer_id
erb :'plan/manage'
end
get '/plan/end' do
require_login
require_active_subscription
@title = 'End Plan'
@plan_name = get_plan_name parent_site.stripe_customer_id
erb :'plan/end'
end
post '/plan/end' do
require_login
require_active_subscription
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 parent_site.stripe_customer_id
halt erb :'plan/end'
end
customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
subscriptions = customer.subscriptions.all
DB.transaction do
subscriptions.each do |subscription|
customer.subscriptions.retrieve(subscription.id).delete
end
parent_site.update plan_ended: true
end
redirect '/plan'
end
=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_tags
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
def browse_sites_dataset
@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, :site_updated_at.desc)
when 'views'
site_dataset.order!(:views.desc, :site_updated_at.desc)
when 'newest'
site_dataset.order!(:created_at.desc, :views.desc)
when 'oldest'
site_dataset.order!(:created_at, :views.desc)
when 'random'
site_dataset.where! 'random() < 0.01'
when 'last_updated'
params[:sort_by] = 'last_updated'
site_dataset.order!(:site_updated_at.desc, :views.desc)
else
if params[:tag]
params[:sort_by] = 'views'
site_dataset.order!(:views.desc, :site_updated_at.desc)
else
params[:sort_by] = 'last_updated'
site_dataset.order!(:site_updated_at.desc, :views.desc)
end
end
site_dataset.where! ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
if params[:tag]
site_dataset = site_dataset.association_join(:tags).select_all(:sites)
site_dataset.where! ['tags.name = ?', params[:tag]]
site_dataset.where! ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
end
site_dataset
end
get '/browse/?' do
params.delete 'tag' if params[:tag].nil? || params[:tag].empty?
site_dataset = browse_sites_dataset
site_dataset = site_dataset.paginate @current_page, 300
@page_count = site_dataset.page_count || 1
@sites = site_dataset.all
erb :browse
end
get '/surf/?' do
params.delete 'tag' if params[:tag].nil? || params[:tag].empty?
site_dataset = browse_sites_dataset
site_dataset = site_dataset.paginate @current_page, 1
@page_count = site_dataset.page_count || 1
@site = site_dataset.first
redirect "/browse?#{Rack::Utils.build_query params}" if @site.nil?
erb :'surf', layout: false
end
get '/surf/:username' do |username|
@site = Site.select(:id, :username, :title, :domain, :views, :stripe_customer_id).where(username: username).first
@title = @site.title
not_found if @site.nil?
erb :'surf', layout: false
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_validate_all' do
content_type :json
fields = params.select {|p| p.match /^username$|^password$|^email$|^new_tags_string$/}
site = Site.new fields
return [].to_json if site.valid?
site.errors.collect {|e| [e.first, e.last.first]}.to_json
end
post '/create_validate' do
content_type :json
if !params[:field].match /^username$|^password$|^email$|^new_tags_string$/
return {error: 'not a valid field'}.to_json
end
site = Site.new(params[:field] => params[:value])
site.valid?
field_sym = params[:field].to_sym
if site.errors[field_sym]
return {error: site.errors[field_sym].first}.to_json
end
{result: 'ok'}.to_json
end
post '/create' do
content_type :json
require_unbanned_ip
dashboard_if_signed_in
@site = Site.new(
username: params[:username],
password: params[:password],
email: params[:email],
new_tags_string: params[:tags],
ip: request.ip
)
black_box_answered = BlackBox.valid? params[:blackbox_answer], request.ip
question_answered_correctly = params[:question_answer] == session[:question_answer]
if !question_answered_correctly
question_first_number, question_last_number = generate_question
return {
result: 'bad_answer',
question_first_number: question_first_number,
question_last_number: question_last_number
}.to_json
end
if !black_box_answered || !@site.valid? || Site.ip_create_limit?(request.ip)
flash[:error] = 'There was an unknown error, please try again.'
return {result: 'error'}.to_json
end
@site.save
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
{result: 'ok'}.to_json
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]]
not_found if @site.nil?
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, <b>don't forget it</b>."
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"
name = "#{params[:dir]}/#{name}" if params[:dir]
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! <a style="color: #FFFFFF; text-decoration: underline" href="/site_files/text_editor/#{name}">Click here to edit it</a>.}
redirect params[:dir] ? "/dashboard?dir=#{Rack::Utils.escape params[:dir]}" : '/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 '/press' do
erb :'press'
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.hash_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.site_updated_at ? site.site_updated_at.rfc2822 : nil,
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 = Site.hash_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 not logged in!' unless signed_in?
halt 'You are banned.' if current_site.is_banned? || parent_site.is_banned?
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
redirect '/' unless signed_in?
if current_site.is_banned || parent_site.is_banned
session[:id] = nil
redirect '/'
end
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
return nil if current_site.nil?
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/spam. '+
'If you believe this to be in error, <a href="/contact">contact the site admin</a>.'
return {result: 'error'}.to_json
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