catch branch up with master

This commit is contained in:
Kyle Drake 2015-07-13 20:14:55 -07:00
commit af0a31d6a2
102 changed files with 4055 additions and 919 deletions

1
.gitignore vendored
View file

@ -34,3 +34,4 @@ files/sslsites.zip
.vagrant
public/banned_sites
public/deleted_sites
tests/stat_logs/*

View file

@ -28,6 +28,10 @@ gem 'rack-cache'
gem 'rest-client', require: 'rest_client'
gem 'addressable'
gem 'paypal-recurring', require: 'paypal/recurring'
gem 'geoip'
gem 'io-extra', require: 'io/extra'
gem 'rye'
gem 'dnsruby'
platform :mri, :rbx do
gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic
@ -74,6 +78,7 @@ group :test do
gem 'rack_session_access', require: nil
gem 'webmock', require: nil
gem 'stripe-ruby-mock', '~> 2.0.1', require: 'stripe_mock'
gem 'timecop'
platform :mri, :rbx do
gem 'simplecov', require: nil

View file

@ -1,44 +1,45 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.1.4)
i18n (~> 0.6, >= 0.6.9)
activesupport (4.2.3)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.3.7)
addressable (2.3.8)
ago (0.1.5)
ansi (1.4.3)
annoy (0.5.6)
highline (>= 1.5.0)
ansi (1.5.0)
autoparse (0.3.3)
addressable (>= 2.3.1)
extlib (>= 0.9.15)
multi_json (>= 1.0.0)
bcrypt (3.1.7)
bcrypt (3.1.10)
blankslate (3.1.3)
builder (3.2.2)
byebug (2.7.0)
columnize (~> 0.3)
debugger-linecache (~> 1.2)
capybara (2.4.1)
byebug (4.0.5)
columnize (= 0.9.0)
capybara (2.4.4)
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_minitest_spec (1.0.5)
capybara (>= 2)
minitest (>= 2)
celluloid (0.15.2)
timers (~> 1.1.0)
minitest (>= 4)
celluloid (0.16.0)
timers (~> 4.0.0)
climate_control (0.0.3)
activesupport (>= 3.0)
cliver (0.3.2)
cocaine (0.5.4)
cocaine (0.5.7)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.0)
columnize (0.8.9)
connection_pool (2.0.0)
columnize (0.9.0)
connection_pool (2.2.0)
crack (0.4.2)
safe_yaml (~> 1.0.0)
dante (0.2.0)
@ -46,186 +47,212 @@ GEM
nokogiri (>= 1.4.2)
rack (>= 1.1.0)
uuidtools (~> 2.1.1)
debugger-linecache (1.2.0)
docile (1.1.3)
domain_name (0.5.23)
dnsruby (1.58.0)
docile (1.1.5)
domain_name (0.5.24)
unf (>= 0.0.5, < 1.0.0)
drydock (0.6.9)
erubis (2.7.0)
extlib (0.9.16)
fabrication (2.11.0)
faker (1.3.0)
fabrication (2.13.2)
faker (1.4.3)
i18n (~> 0.5)
faraday (0.9.0)
faraday (0.9.1)
multipart-post (>= 1.2, < 3)
ffi (1.9.6)
ffi (1.9.10)
ffi-compiler (0.1.3)
ffi (>= 1.0.0)
rake
filesize (0.0.3)
google-api-client (0.7.1)
addressable (>= 2.3.2)
autoparse (>= 0.3.3)
extlib (>= 0.9.15)
faraday (>= 0.9.0)
jwt (>= 0.1.5)
launchy (>= 2.1.1)
multi_json (>= 1.0.0)
retriable (>= 1.4)
signet (>= 0.5.0)
uuidtools (>= 2.1.0)
hashie (2.0.5)
hiredis (0.5.0)
filesize (0.1.0)
geoip (1.6.1)
google-api-client (0.8.6)
activesupport (>= 3.2)
addressable (~> 2.3)
autoparse (~> 0.3)
extlib (~> 0.9)
faraday (~> 0.9)
googleauth (~> 0.3)
launchy (~> 2.4)
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
googleauth (0.4.1)
faraday (~> 0.9)
jwt (~> 1.4)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (= 1.11)
signet (~> 0.6)
highline (1.7.2)
hiredis (0.6.0)
hitimes (1.2.2)
http-cookie (1.0.2)
domain_name (~> 0.5)
i18n (0.6.9)
i18n (0.7.0)
io-extra (1.2.8)
jimson-temp (0.9.5)
blankslate (>= 3.1.2)
multi_json (~> 1.0)
rack (~> 1.4)
rest-client (~> 1.0)
json (1.8.1)
jwt (0.1.11)
multi_json (>= 1.5)
kgio (2.9.2)
launchy (2.4.2)
json (1.8.3)
jwt (1.5.1)
kgio (2.9.3)
launchy (2.4.3)
addressable (~> 2.3)
magic (0.2.6)
little-plugger (1.1.3)
logging (2.0.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
magic (0.2.9)
ffi (>= 0.6.3)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mail (2.6.3)
mime-types (>= 1.16, < 3)
memoist (0.12.0)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.0)
minitest (5.3.1)
minitest-reporters (1.0.2)
mime-types (2.6.1)
mini_portile (0.6.2)
minitest (5.7.0)
minitest-reporters (1.0.18)
ansi
builder
minitest (>= 5.0)
powerbar
mocha (1.0.0)
ruby-progressbar
mocha (1.1.0)
metaclass (~> 0.0.1)
multi_json (1.10.1)
multi_json (1.11.0)
multipart-post (2.0.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (2.9.2)
netrc (0.10.3)
nokogiri (1.6.3.1)
mini_portile (= 0.6.0)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
paypal-recurring (1.1.0)
pg (0.17.1)
phantomjs (1.9.7.1)
poltergeist (1.5.1)
pg (0.18.2)
phantomjs (1.9.8.0)
poltergeist (1.6.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyglot (0.3.4)
powerbar (1.0.11)
ansi (~> 1.4.0)
hashie (>= 1.1.0)
pry (0.9.12.6)
coderay (~> 1.0)
method_source (~> 0.8)
pry (0.10.1)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (1.3.2)
byebug (~> 2.7)
pry (~> 0.9.12)
puma (2.8.1)
pry-byebug (3.1.0)
byebug (~> 4.0)
pry (~> 0.10)
puma (2.11.3)
rack (>= 1.1, < 2.0)
rack (1.5.2)
rack (1.6.4)
rack-cache (1.2)
rack (>= 0.4)
rack-protection (1.5.2)
rack-protection (1.5.3)
rack
rack-recaptcha (0.6.6)
json
rack-test (0.6.2)
rack-test (0.6.3)
rack (>= 1.0)
rack_session_access (0.1.1)
builder (>= 2.0.0)
rack (>= 1.0.0)
rainbows (4.6.1)
rainbows (4.6.2)
kgio (~> 2.5)
rack (~> 1.1)
unicorn (~> 4.8)
raindrops (0.13.0)
rake (10.3.2)
redis (3.0.7)
redis-namespace (1.4.1)
redis (~> 3.0.4)
raindrops (0.14.0)
rake (10.4.2)
redis (3.2.1)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
retriable (1.4.1)
rmagick (2.13.3)
rmagick (2.15.2)
ruby-progressbar (1.7.5)
rye (0.9.13)
annoy
docile (>= 1.0.1)
highline (>= 1.5.1)
net-scp (>= 1.0.2)
net-ssh (>= 2.0.13)
sysinfo (>= 0.8.1)
safe_yaml (1.0.4)
sass (3.3.8)
screencap (0.1.1)
sass (3.4.16)
screencap (0.1.2)
phantomjs
scrypt (2.0.0)
scrypt (2.0.2)
ffi-compiler (>= 0.0.2)
rake
sequel (4.8.0)
sequel_pg (1.6.9)
sequel_pg (1.6.13)
pg (>= 0.8.0)
sequel (>= 3.39.0)
shotgun (0.9)
shotgun (0.9.1)
rack (>= 1.0)
sidekiq (3.0.0)
celluloid (>= 0.15.2)
connection_pool (>= 2.0.0)
sidekiq (3.4.1)
celluloid (~> 0.16.0)
connection_pool (>= 2.1.1)
json
redis (>= 3.0.6)
redis-namespace (>= 1.3.1)
signet (0.5.0)
addressable (>= 2.2.3)
faraday (>= 0.9.0.rc5)
jwt (>= 0.1.5)
multi_json (>= 1.0.0)
simplecov (0.8.2)
signet (0.6.1)
addressable (~> 2.3)
extlib (~> 0.9)
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
simplecov (0.10.0)
docile (~> 1.1.0)
multi_json
simplecov-html (~> 0.8.0)
simplecov-html (0.8.0)
sinatra (1.4.4)
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
tilt (>= 1.3, < 3)
sinatra-flash (0.3.0)
sinatra (>= 1.0.0)
sinatra-xsendfile (0.4.2)
sinatra (>= 0.9.1)
slop (3.5.0)
stripe (1.15.0)
slop (3.6.0)
storable (0.8.9)
stripe (1.23.0)
json (~> 1.8.1)
mime-types (>= 1.25, < 3.0)
rest-client (~> 1.4)
stripe-ruby-mock (2.0.1)
stripe-ruby-mock (2.0.5)
dante (>= 0.2.0)
jimson-temp
stripe (>= 1.15.0)
thread (0.1.4)
thread_safe (0.3.4)
tilt (1.4.1)
timers (1.1.0)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
sysinfo (0.8.1)
drydock
storable
thread (0.2.1)
thread_safe (0.3.5)
tilt (2.0.1)
timecop (0.7.4)
timers (4.0.1)
hitimes
tzinfo (1.2.2)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.6)
unicorn (4.8.2)
unf_ext (0.0.7.1)
unicorn (4.9.0)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
uuidtools (2.1.4)
webmock (1.17.4)
addressable (>= 2.2.7)
uuidtools (2.1.5)
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
websocket-driver (0.3.4)
websocket-driver (0.6.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
xpath (2.0.0)
nokogiri (~> 1.3)
zipruby (0.3.6)
@ -240,12 +267,15 @@ DEPENDENCIES
capybara_minitest_spec
cocaine
dav4rack
dnsruby
erubis
fabrication
faker
filesize
geoip
google-api-client
hiredis
io-extra
jdbc-postgres
jruby-openssl
json
@ -270,6 +300,7 @@ DEPENDENCIES
rest-client
rmagick
ruby-debug
rye
sass
screencap
scrypt
@ -285,5 +316,9 @@ DEPENDENCIES
stripe-ruby-mock (~> 2.0.1)
thread
tilt
timecop
webmock
zipruby
BUNDLED WITH
1.10.2

View file

@ -31,40 +31,11 @@ end
desc "parse logs"
task :parse_logs => [:environment] do
Dir[File.join($config['logs_path'], '*.log')].each do |log_path|
hits = {}
visits = {}
visit_ips = {}
logfile = File.open log_path, 'r'
while hit = logfile.gets
time, username, size, path, ip = hit.split ' '
hits[username] ||= 0
hits[username] += 1
visit_ips[username] = [] if !visit_ips[username]
unless visit_ips[username].include?(ip)
visits[username] ||= 0
visits[username] += 1
visit_ips[username] << ip
end
end
logfile.close
hits.each do |username,hitcount|
DB['update sites set hits=hits+? where username=?', hitcount, username].first
end
visits.each do |username,visitcount|
DB['update sites set views=views+? where username=?', visitcount, username].first
end
FileUtils.rm log_path
end
Stat.prune!
StatLocation.prune!
StatReferrer.prune!
StatPath.prune!
Stat.parse_logfiles $config['logs_path']
end
desc 'Update banned IPs list'
@ -223,7 +194,7 @@ end
desc 'prime_space_used'
task :prime_space_used => [:environment] do
Site.select(:id,:username,:space_used).all.each do |s|
s.space_used += s.actual_space_used
s.space_used = s.actual_space_used
s.save_changes validate: false
end
end

3
app.rb
View file

@ -36,6 +36,7 @@ before do
end
not_found do
@title = 'Not Found'
erb :'not_found'
end
@ -44,7 +45,7 @@ error do
from: 'web@neocities.org',
to: 'errors@neocities.org',
subject: "[Neocities Error] #{env['sinatra.error'].class}: #{env['sinatra.error'].message}",
body: erb(:'views/templates/email/error'),
body: erb(:'templates/email/error', layout: false),
no_footer: true
})

View file

@ -5,6 +5,56 @@ get '/admin' do
erb :'admin'
end
get '/admin/reports' do
require_admin
@reports = Report.order(:created_at.desc).all
erb :'admin/reports'
end
get '/admin/email' do
require_admin
erb :'admin/email'
end
post '/admin/email' do
require_admin
%i{subject body}.each do |k|
if params[k].nil? || params[k].empty?
flash[:error] = "#{k.capitalize} is missing."
redirect '/admin/email'
end
end
sites = Site.newsletter_sites
day = 0
until sites.empty?
seconds = 0.0
queued_sites = []
Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times {
break if sites.empty?
queued_sites << sites.pop
}
queued_sites.each do |site|
EmailWorker.perform_at((day.days.from_now + seconds), {
from: 'Kyle from Neocities <kyle@neocities.org>',
to: site.email,
subject: params[:subject],
body: params[:body]
})
seconds += 0.5
end
day += 1
end
flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day."
redirect '/'
end
post '/admin/banip' do
require_admin
site = Site[username: params[:username]]
@ -18,7 +68,7 @@ post '/admin/banip' do
flash[:error] = 'IP is blank, cannot continue'
redirect '/admin'
end
sites = Site.filter(ip: Site.hash_ip(site.ip), is_banned: false).all
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'

View file

@ -7,6 +7,7 @@ end
post '/api/upload' do
require_api_credentials
files = []
params.each do |k,v|
next unless v.is_a?(Hash) && v[:tempfile]
@ -22,6 +23,10 @@ post '/api/upload' do
api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files'
end
if current_site.too_many_files?(files.length)
api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)}), #{current_site.supporter? ? 'please contact support' : 'please upgrade to a supporter account'}"
end
files.each do |file|
if !current_site.okay_to_upload?(file)
api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content) for this site, files have not been uploaded"
@ -32,13 +37,7 @@ post '/api/upload' do
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)
results = current_site.store_files files
api_success 'your file(s) have been successfully uploaded'
end
@ -53,6 +52,10 @@ post '/api/delete' do
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
end
if current_site.files_path(path) == current_site.files_path
api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site'
end
if !current_site.file_exists?(path)
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
end

View file

@ -1,6 +1,16 @@
get '/browse/?' do
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty?
site_dataset = browse_sites_dataset
if is_education?
site_dataset = education_sites_dataset
else
site_dataset = browse_sites_dataset
end
site_dataset = site_dataset.paginate @current_page, Site::BROWSE_PAGINATION_LENGTH
@page_count = site_dataset.page_count || 1
@sites = site_dataset.all
@ -10,11 +20,14 @@ get '/browse/?' do
erb :browse
end
def browse_sites_dataset
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
def education_sites_dataset
site_dataset = Site.filter is_deleted: false
site_dataset = site_dataset.association_join(:tags).select_all(:sites)
params[:tag] = current_site.tags.first.name
site_dataset.where! ['tags.name = ?', params[:tag]]
end
def browse_sites_dataset
site_dataset = Site.filter(is_deleted: false, is_banned: false, is_crashing: false).filter(site_changed: true)
if current_site
@ -30,6 +43,19 @@ def browse_sites_dataset
end
case params[:sort_by]
when 'followers'
site_dataset = site_dataset.association_left_join :follows
site_dataset.select_all! :sites
site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count")
site_dataset.group! :sites__id
site_dataset.order! :follow_count.desc, :updated_at.desc
when 'supporters'
site_dataset.exclude! plan_type: nil
site_dataset.exclude! plan_type: 'free'
site_dataset.order! :views.desc, :site_updated_at.desc
when 'featured'
site_dataset.exclude! featured_at: nil
site_dataset.order! :featured_at.desc
when 'hits'
site_dataset.where!{views > 100}
site_dataset.order!(:hits.desc, :site_updated_at.desc)
@ -53,15 +79,17 @@ def browse_sites_dataset
params[:sort_by] = 'views'
site_dataset.order!(:views.desc, :site_updated_at.desc)
else
params[:sort_by] = 'last_updated'
site_dataset.where!{views > 100}
site_dataset.order!(:site_updated_at.desc, :views.desc)
site_dataset = site_dataset.association_left_join :follows
site_dataset.select_all! :sites
site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count")
site_dataset.group! :sites__id
site_dataset.order! :follow_count.desc, :updated_at.desc
end
end
site_dataset.where! ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
if params[:tag]
if params[:tag] && params[:sort_by] != 'followers'
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)]

View file

@ -16,9 +16,11 @@ def new_recaptcha_valid?
end
end
CREATE_MATCH_REGEX = /^username$|^password$|^email$|^new_tags_string$|^is_education$/
post '/create_validate_all' do
content_type :json
fields = params.select {|p| p.match /^username$|^password$|^email$|^new_tags_string$/}
fields = params.select {|p| p.match CREATE_MATCH_REGEX}
site = Site.new fields
@ -33,11 +35,12 @@ end
post '/create_validate' do
content_type :json
if !params[:field].match /^username$|^password$|^email$|^new_tags_string$/
if !params[:field].match CREATE_MATCH_REGEX
return {error: 'not a valid field'}.to_json
end
end
site = Site.new(params[:field] => params[:value])
site.is_education = params[:is_education]
site.valid?
field_sym = params[:field].to_sym
@ -58,7 +61,8 @@ post '/create' do
username: params[:username],
password: params[:password],
email: params[:email],
new_tags_string: params[:tags],
new_tags_string: params[:new_tags_string],
is_education: params[:is_education] == 'true' ? true : false,
ip: request.ip
)
@ -85,4 +89,4 @@ post '/create' do
session[:id] = @site.id
{result: 'ok'}.to_json
end
end

View file

@ -2,6 +2,8 @@ get '/?' do
if current_site
require_login
redirect '/dashboard' if current_site.is_education
@suggestions = current_site.suggestions
@current_page = params[:current_page].to_i
@ -34,7 +36,7 @@ get '/?' do
@sites_count = SimpleCache.get :sites_count
end
erb :index, layout: false
erb :index, layout: :index_layout
end
get '/welcome' do
@ -43,6 +45,11 @@ get '/welcome' do
erb :'welcome', locals: {site: current_site}
end
get '/education' do
redirect '/' if signed_in?
erb :education, layout: :index_layout
end
get '/tutorials' do
erb :'tutorials'
end
@ -71,3 +78,7 @@ get '/legal/?' do
@title = 'Legal Guide to Neocities'
erb :'legal'
end
get '/permanent-web' do
erb :'permanent_web'
end

View file

@ -24,4 +24,9 @@ get '/welcome_mockup' do
require_login
erb :'welcome_mockup', locals: {site: current_site}
end
get '/stats_mockup' do
require_login
erb :'stats_mockup', locals: {site: current_site}
end
# :nocov:

4
app/search.rb Normal file
View file

@ -0,0 +1,4 @@
get '/search' do
erb :'search'
end

View file

@ -9,6 +9,8 @@ get '/site/:username/?' do |username|
# TODO: There should probably be a "this site was deleted" page.
not_found if site.nil? || site.is_banned || site.is_deleted
redirect '/' if site.is_education
@title = site.title
@current_page = params[:current_page]
@ -16,6 +18,7 @@ get '/site/:username/?' do |username|
@current_page = 1 if @current_page == 0
if params[:event_id]
not_found unless params[:event_id].is_integer?
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)
@ -29,6 +32,76 @@ get '/site/:username/?' do |username|
erb :'site', locals: {site: site, is_current_site: site == current_site}
end
get '/site/:username/archives' do
require_login
@site = Site[username: params[:username]]
not_found if @site.nil?
redirect request.referrer unless current_site.id == @site.id
@archives = @site.archives_dataset.limit(300).order(:updated_at.desc).all
erb :'site/archives'
end
get '/site/:username/stats' do
@site = Site[username: params[:username]]
not_found if @site.nil?
@title = "Site stats for #{@site.host}"
@stats = {}
%i{referrers locations paths}.each do |stat|
@stats[stat] = @site.send("stat_#{stat}_dataset".to_sym).order(:views.desc).limit(100).all
end
@stats[:locations].collect! do |location|
location_name = ''
location_name += location.city_name if location.city_name
if location.region_name
# Some of the region names are numbers for some reason.
begin
Integer(location.region_name)
rescue
location_name += ', ' unless location_name == ''
location_name += location.region_name
end
end
if location.country_code2 && !$country_codes[location.country_code2].nil?
location_name += ', ' unless location_name == ''
location_name += $country_codes[location.country_code2]
end
location_hash = {name: location_name, views: location.views}
if location.latitude && location.longitude
location_hash.merge! latitude: location.latitude, longitude: location.longitude
end
location_hash
end
stats_dataset = @site.stats_dataset.order(:created_at.desc).exclude(created_at: Date.today)
if @site.supporter?
unless params[:days].to_s == 'sincethebigbang'
if params[:days]
stats_dataset.limit! params[:days]
else
stats_dataset.limit! 7
end
end
else
stats_dataset.limit! 7
end
@stats[:stat_days] = stats_dataset.all.reverse
@multi_tooltip_template = "<%= datasetLabel %> - <%= value %>"
erb :'site/stats', locals: {site: @site}
end
post '/site/:username/set_editor_theme' do
require_login
current_site.editor_theme = params[:editor_theme]

View file

@ -9,32 +9,67 @@ get '/site_files/new' do
redirect '/site_files/new_page'
end
post '/site_files/create_page' do
post '/site_files/create' do
require_login
@errors = []
params[:pagefilename].gsub!(/[^a-zA-Z0-9_\-.]/, '')
params[:pagefilename].gsub!(/\.html$/i, '')
filename = params[:pagefilename] || params[:filename]
if params[:pagefilename].nil? || params[:pagefilename].strip.empty?
@errors << 'You must provide a file name.'
halt erb(:'site_files/new_page')
filename.gsub!(/[^a-zA-Z0-9_\-.]/, '')
redirect_uri = '/dashboard'
redirect_uri += "?dir=#{Rack::Utils.escape params[:dir]}" if params[:dir]
if filename.nil? || filename.strip.empty?
flash[:error] = 'You must provide a file name.'
redirect redirect_uri
end
name = "#{params[:pagefilename]}.html"
name = "#{filename}"
name = "#{params[:dir]}/#{name}" if params[:dir]
name = current_site.scrubbed_path name
if current_site.file_exists?(name)
@errors << %{Web page "#{name}" already exists! Choose another name.}
halt erb(:'site_files/new_page')
flash[:error] = %{Web page "#{name}" already exists! Choose another name.}
redirect redirect_uri
end
current_site.install_new_html_file name
extname = File.extname name
unless extname.match /^\.#{Site::EDITABLE_FILE_EXT}/i
flash[:error] = "Must be an text editable file type (#{Site::VALID_EDITABLE_EXTENSIONS.join(', ')})."
redirect redirect_uri
end
site_file = current_site.site_files_dataset.where(path: name).first
if site_file
flash[:error] = 'File already exists, cannot create.'
redirect redirect_uri
end
if extname.match(/^\.html|^\.htm/i)
current_site.install_new_html_file name
else
file_path = current_site.files_path(name)
FileUtils.touch file_path
File.chmod 0640, file_path
site_file ||= SiteFile.new site_id: current_site.id, path: name
site_file.set_all(
size: 0,
sha1_hash: Digest::SHA1.hexdigest(''),
updated_at: Time.now
)
site_file.save
end
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'
redirect redirect_uri
end
def file_upload_response(error=nil)
@ -59,8 +94,22 @@ post '/site_files/upload' do
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]
params[:files].each_with_index do |file,i|
dir_name = ''
dir_name = params[:dir] if params[:dir]
unless params[:file_paths].nil? || params[:file_paths].empty? || params[:file_paths].length == 0
file_path = params[:file_paths].select {|file_path|
file[:filename] == Pathname(file_path).basename.to_s
}.first
unless file_path.nil?
dir_name += '/' + Pathname(file_path).dirname.to_s
end
end
file[:filename] = "#{dir_name}/#{file[:filename]}"
if current_site.file_size_too_large? file[:tempfile].size
file_upload_response "#{file[:filename]} is too large, upload cancelled."
end
@ -75,21 +124,23 @@ post '/site_files/upload' do
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])
if current_site.too_many_files? params[:files].length
file_upload_response "Too many files, cannot upload"
end
current_site.increment_changed_count if results.include?(true)
results = current_site.store_files params[:files]
file_upload_response
end
post '/site_files/delete' do
require_login
current_site.delete_file params[:filename]
flash[:success] = "Deleted #{params[:filename]}."
redirect '/dashboard'
dirname = Pathname(params[:filename]).dirname
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
redirect "/dashboard#{dir_query}"
end
get '/site_files/:username.zip' do |username|
@ -147,7 +198,7 @@ post %r{\/site_files\/save\/(.+)} do
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
current_site.store_files [{filename: filename, tempfile: tempfile}]
'ok'
end

View file

@ -1,4 +1,5 @@
get '/surf/?' do
@current_page = params[:current_page].to_i || 1
params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty?
site_dataset = browse_sites_dataset
site_dataset = site_dataset.paginate @current_page, 1

View file

@ -26,8 +26,7 @@ post '/webhooks/stripe' do
end
if event['type'] == 'charge.failed'
site_id = event['data']['object']['description'].split(' - ').last
site = Site[site_id]
site = stripe_get_site_from_event event
EmailWorker.perform_async({
from: 'web@neocities.org',
@ -38,8 +37,7 @@ post '/webhooks/stripe' do
end
if event['type'] == 'customer.subscription.deleted'
site_id = event['data']['object']['description'].split(' - ').last
site = Site[site_id]
site = stripe_get_site_from_event event
site.stripe_subscription_id = nil
site.plan_type = nil
site.save_changes validate: false
@ -54,3 +52,23 @@ post '/webhooks/stripe' do
'ok'
end
def stripe_get_site_from_event(event)
customer_id = event['data']['object']['customer']
customer = Stripe::Customer.retrieve customer_id
# Some old accounts only have a username for the desc
desc_split = customer.description.split(' - ')
if desc_split.length == 1
site_where = {username: desc_split.first}
end
if desc_split.last.to_i == 0
site_where = {username: desc_split.first}
else
site_where = {id: desc_split.last}
end
Site.where(site_where).first
end

View file

@ -1,3 +1,13 @@
def kickstarter_days_remaining
ending = Time.parse('Sat, Jul 25 2015 3:05 PM PDT')
today = Time.now
remaining = ending - today
return 0 if remaining < 0
((ending - today) / 86400).to_i
end
def dashboard_if_signed_in
redirect '/dashboard' if signed_in?
end
@ -15,6 +25,10 @@ def csrf_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
def is_education?
current_site && current_site.is_education
end
def require_login
redirect '/' unless signed_in?
if session[:banned] || current_site.is_banned || parent_site.is_banned
@ -39,10 +53,11 @@ def parent_site
end
def require_unbanned_ip
if session[:banned] || Site.banned_ip?(request.ip)
if session[:banned] || (is_banned_ip = Site.banned_ip?(request.ip))
signout
session[:banned] = true
flash[:error] = 'Site creation has been banned due to ToS violation/spam. '+
session[:banned] = request.ip if !session[:banned]
flash[:error] = 'Site creation has been banned due to a Terms of Service violation from your location. '+
'If you believe this to be in error, <a href="/contact">contact the site admin</a>.'
return {result: 'error'}.to_json
end

View file

@ -10,3 +10,4 @@ email_unsubscribe_token: "somethingrandomderrrrp"
paypal_api_username: derp
paypal_api_password: ing
paypal_api_signature: tonz
logs_path: "/tmp/neocitiestestlogs"

View file

@ -75,18 +75,6 @@ if ENV['RACK_ENV'] == 'development'
end
# :nocov:
# :nocov:
if $config['pubsub_url']
$pubsub_pool = ConnectionPool.new(size: 10, timeout: 5) {
Redis.new url: $config['pubsub_url']
}
end
if $config['pubsub_url'].nil? && ENV['RACK_ENV'] == 'production'
raise 'pubsub_url is missing from config'
end
# :nocov:
Sequel.datetime_class = Time
Sequel.extension :core_extensions
Sequel.extension :migration
@ -138,3 +126,11 @@ PayPal::Recurring.configure do |config|
config.password = $config['paypal_api_password']
config.signature = $config['paypal_api_signature']
end
require 'csv'
$country_codes = {}
CSV.foreach("./files/country_codes.csv") do |row|
$country_codes[row.last] = row.first
end

View file

@ -10,15 +10,24 @@ class Numeric
end
def to_bytes_pretty
space = (self.to_f / ONE_MEGABYTE).round(2)
space = space.to_i if space.denominator == 1
# if space >= 1000000
# "#{space/1000000} TB"
if space >= 1000
"#{(space/1000).to_comma_separated} GB"
else
"#{space.to_comma_separated} MB"
end
computed = nil
unit = nil
{
'B' => 1000,
'KB' => 1000 * 1000,
'MB' => 1000 * 1000 * 1000,
'GB' => 1000 * 1000 * 1000 * 1000,
'TB' => 1000 * 1000 * 1000 * 1000 * 1000
}.each_pair { |e, s|
if self < s
computed = (self.to_f / (s / 1000)).round(2)
unit = e
break
end
}
computed = computed.to_i if computed.modulo(1) == 0.0
"#{computed} #{unit}"
end
def to_comma_separated

View file

@ -11,4 +11,8 @@ class String
def unindent
gsub /^#{scan(/^\s*/).min_by{|l|l.length}}/, ""
end
def is_integer?
true if Integer(self) rescue false
end
end

BIN
files/GeoLiteCity.dat Normal file

Binary file not shown.

250
files/country_codes.csv Normal file
View file

@ -0,0 +1,250 @@
Afghanistan,AF
Åland Islands,AX
Albania,AL
Algeria,DZ
American Samoa,AS
Andorra,AD
Angola,AO
Anguilla,AI
Antarctica,AQ
Antigua and Barbuda,AG
Argentina,AR
Armenia,AM
Aruba,AW
Australia,AU
Austria,AT
Azerbaijan,AZ
Bahamas,BS
Bahrain,BH
Bangladesh,BD
Barbados,BB
Belarus,BY
Belgium,BE
Belize,BZ
Benin,BJ
Bermuda,BM
Bhutan,BT
"Bolivia, Plurinational State of",BO
"Bonaire, Sint Eustatius and Saba",BQ
Bosnia and Herzegovina,BA
Botswana,BW
Bouvet Island,BV
Brazil,BR
British Indian Ocean Territory,IO
Brunei Darussalam,BN
Bulgaria,BG
Burkina Faso,BF
Burundi,BI
Cambodia,KH
Cameroon,CM
Canada,CA
Cape Verde,CV
Cayman Islands,KY
Central African Republic,CF
Chad,TD
Chile,CL
China,CN
Christmas Island,CX
Cocos (Keeling) Islands,CC
Colombia,CO
Comoros,KM
Congo,CG
"Congo, the Democratic Republic of the",CD
Cook Islands,CK
Costa Rica,CR
Côte d'Ivoire,CI
Croatia,HR
Cuba,CU
Curaçao,CW
Cyprus,CY
Czech Republic,CZ
Denmark,DK
Djibouti,DJ
Dominica,DM
Dominican Republic,DO
Ecuador,EC
Egypt,EG
El Salvador,SV
Equatorial Guinea,GQ
Eritrea,ER
Estonia,EE
Ethiopia,ET
Falkland Islands (Malvinas),FK
Faroe Islands,FO
Fiji,FJ
Finland,FI
France,FR
French Guiana,GF
French Polynesia,PF
French Southern Territories,TF
Gabon,GA
Gambia,GM
Georgia,GE
Germany,DE
Ghana,GH
Gibraltar,GI
Greece,GR
Greenland,GL
Grenada,GD
Guadeloupe,GP
Guam,GU
Guatemala,GT
Guernsey,GG
Guinea,GN
Guinea-Bissau,GW
Guyana,GY
Haiti,HT
Heard Island and McDonald Mcdonald Islands,HM
Holy See (Vatican City State),VA
Honduras,HN
Hong Kong,HK
Hungary,HU
Iceland,IS
India,IN
Indonesia,ID
"Iran, Islamic Republic of",IR
Iraq,IQ
Ireland,IE
Isle of Man,IM
Israel,IL
Italy,IT
Jamaica,JM
Japan,JP
Jersey,JE
Jordan,JO
Kazakhstan,KZ
Kenya,KE
Kiribati,KI
"Korea, Democratic People's Republic of",KP
"Korea, Republic of",KR
Kuwait,KW
Kyrgyzstan,KG
Lao People's Democratic Republic,LA
Latvia,LV
Lebanon,LB
Lesotho,LS
Liberia,LR
Libya,LY
Liechtenstein,LI
Lithuania,LT
Luxembourg,LU
Macao,MO
"Macedonia, the Former Yugoslav Republic of",MK
Madagascar,MG
Malawi,MW
Malaysia,MY
Maldives,MV
Mali,ML
Malta,MT
Marshall Islands,MH
Martinique,MQ
Mauritania,MR
Mauritius,MU
Mayotte,YT
Mexico,MX
"Micronesia, Federated States of",FM
"Moldova, Republic of",MD
Monaco,MC
Mongolia,MN
Montenegro,ME
Montserrat,MS
Morocco,MA
Mozambique,MZ
Myanmar,MM
Namibia,NA
Nauru,NR
Nepal,NP
Netherlands,NL
New Caledonia,NC
New Zealand,NZ
Nicaragua,NI
Niger,NE
Nigeria,NG
Niue,NU
Norfolk Island,NF
Northern Mariana Islands,MP
Norway,NO
Oman,OM
Pakistan,PK
Palau,PW
"Palestine, State of",PS
Panama,PA
Papua New Guinea,PG
Paraguay,PY
Peru,PE
Philippines,PH
Pitcairn,PN
Poland,PL
Portugal,PT
Puerto Rico,PR
Qatar,QA
Réunion,RE
Romania,RO
Russian Federation,RU
Rwanda,RW
Saint Barthélemy,BL
"Saint Helena, Ascension and Tristan da Cunha",SH
Saint Kitts and Nevis,KN
Saint Lucia,LC
Saint Martin (French part),MF
Saint Pierre and Miquelon,PM
Saint Vincent and the Grenadines,VC
Samoa,WS
San Marino,SM
Sao Tome and Principe,ST
Saudi Arabia,SA
Senegal,SN
Serbia,RS
Seychelles,SC
Sierra Leone,SL
Singapore,SG
Sint Maarten (Dutch part),SX
Slovakia,SK
Slovenia,SI
Solomon Islands,SB
Somalia,SO
South Africa,ZA
South Georgia and the South Sandwich Islands,GS
South Sudan,SS
Spain,ES
Sri Lanka,LK
Sudan,SD
Suriname,SR
Svalbard and Jan Mayen,SJ
Swaziland,SZ
Sweden,SE
Switzerland,CH
Syrian Arab Republic,SY
"Taiwan, Province of China",TW
Tajikistan,TJ
"Tanzania, United Republic of",TZ
Thailand,TH
Timor-Leste,TL
Togo,TG
Tokelau,TK
Tonga,TO
Trinidad and Tobago,TT
Tunisia,TN
Turkey,TR
Turkmenistan,TM
Turks and Caicos Islands,TC
Tuvalu,TV
Uganda,UG
Ukraine,UA
United Arab Emirates,AE
United Kingdom,GB
United States,US
United States Minor Outlying Islands,UM
Uruguay,UY
Uzbekistan,UZ
Vanuatu,VU
"Venezuela, Bolivarian Republic of",VE
Viet Nam,VN
"Virgin Islands, British",VG
"Virgin Islands, U.S.",VI
Wallis and Futuna,WF
Western Sahara,EH
Yemen,YE
Zambia,ZM
Zimbabwe,ZW
European Union, EU
1 Afghanistan AF
2 Åland Islands AX
3 Albania AL
4 Algeria DZ
5 American Samoa AS
6 Andorra AD
7 Angola AO
8 Anguilla AI
9 Antarctica AQ
10 Antigua and Barbuda AG
11 Argentina AR
12 Armenia AM
13 Aruba AW
14 Australia AU
15 Austria AT
16 Azerbaijan AZ
17 Bahamas BS
18 Bahrain BH
19 Bangladesh BD
20 Barbados BB
21 Belarus BY
22 Belgium BE
23 Belize BZ
24 Benin BJ
25 Bermuda BM
26 Bhutan BT
27 Bolivia, Plurinational State of BO
28 Bonaire, Sint Eustatius and Saba BQ
29 Bosnia and Herzegovina BA
30 Botswana BW
31 Bouvet Island BV
32 Brazil BR
33 British Indian Ocean Territory IO
34 Brunei Darussalam BN
35 Bulgaria BG
36 Burkina Faso BF
37 Burundi BI
38 Cambodia KH
39 Cameroon CM
40 Canada CA
41 Cape Verde CV
42 Cayman Islands KY
43 Central African Republic CF
44 Chad TD
45 Chile CL
46 China CN
47 Christmas Island CX
48 Cocos (Keeling) Islands CC
49 Colombia CO
50 Comoros KM
51 Congo CG
52 Congo, the Democratic Republic of the CD
53 Cook Islands CK
54 Costa Rica CR
55 Côte d'Ivoire CI
56 Croatia HR
57 Cuba CU
58 Curaçao CW
59 Cyprus CY
60 Czech Republic CZ
61 Denmark DK
62 Djibouti DJ
63 Dominica DM
64 Dominican Republic DO
65 Ecuador EC
66 Egypt EG
67 El Salvador SV
68 Equatorial Guinea GQ
69 Eritrea ER
70 Estonia EE
71 Ethiopia ET
72 Falkland Islands (Malvinas) FK
73 Faroe Islands FO
74 Fiji FJ
75 Finland FI
76 France FR
77 French Guiana GF
78 French Polynesia PF
79 French Southern Territories TF
80 Gabon GA
81 Gambia GM
82 Georgia GE
83 Germany DE
84 Ghana GH
85 Gibraltar GI
86 Greece GR
87 Greenland GL
88 Grenada GD
89 Guadeloupe GP
90 Guam GU
91 Guatemala GT
92 Guernsey GG
93 Guinea GN
94 Guinea-Bissau GW
95 Guyana GY
96 Haiti HT
97 Heard Island and McDonald Mcdonald Islands HM
98 Holy See (Vatican City State) VA
99 Honduras HN
100 Hong Kong HK
101 Hungary HU
102 Iceland IS
103 India IN
104 Indonesia ID
105 Iran, Islamic Republic of IR
106 Iraq IQ
107 Ireland IE
108 Isle of Man IM
109 Israel IL
110 Italy IT
111 Jamaica JM
112 Japan JP
113 Jersey JE
114 Jordan JO
115 Kazakhstan KZ
116 Kenya KE
117 Kiribati KI
118 Korea, Democratic People's Republic of KP
119 Korea, Republic of KR
120 Kuwait KW
121 Kyrgyzstan KG
122 Lao People's Democratic Republic LA
123 Latvia LV
124 Lebanon LB
125 Lesotho LS
126 Liberia LR
127 Libya LY
128 Liechtenstein LI
129 Lithuania LT
130 Luxembourg LU
131 Macao MO
132 Macedonia, the Former Yugoslav Republic of MK
133 Madagascar MG
134 Malawi MW
135 Malaysia MY
136 Maldives MV
137 Mali ML
138 Malta MT
139 Marshall Islands MH
140 Martinique MQ
141 Mauritania MR
142 Mauritius MU
143 Mayotte YT
144 Mexico MX
145 Micronesia, Federated States of FM
146 Moldova, Republic of MD
147 Monaco MC
148 Mongolia MN
149 Montenegro ME
150 Montserrat MS
151 Morocco MA
152 Mozambique MZ
153 Myanmar MM
154 Namibia NA
155 Nauru NR
156 Nepal NP
157 Netherlands NL
158 New Caledonia NC
159 New Zealand NZ
160 Nicaragua NI
161 Niger NE
162 Nigeria NG
163 Niue NU
164 Norfolk Island NF
165 Northern Mariana Islands MP
166 Norway NO
167 Oman OM
168 Pakistan PK
169 Palau PW
170 Palestine, State of PS
171 Panama PA
172 Papua New Guinea PG
173 Paraguay PY
174 Peru PE
175 Philippines PH
176 Pitcairn PN
177 Poland PL
178 Portugal PT
179 Puerto Rico PR
180 Qatar QA
181 Réunion RE
182 Romania RO
183 Russian Federation RU
184 Rwanda RW
185 Saint Barthélemy BL
186 Saint Helena, Ascension and Tristan da Cunha SH
187 Saint Kitts and Nevis KN
188 Saint Lucia LC
189 Saint Martin (French part) MF
190 Saint Pierre and Miquelon PM
191 Saint Vincent and the Grenadines VC
192 Samoa WS
193 San Marino SM
194 Sao Tome and Principe ST
195 Saudi Arabia SA
196 Senegal SN
197 Serbia RS
198 Seychelles SC
199 Sierra Leone SL
200 Singapore SG
201 Sint Maarten (Dutch part) SX
202 Slovakia SK
203 Slovenia SI
204 Solomon Islands SB
205 Somalia SO
206 South Africa ZA
207 South Georgia and the South Sandwich Islands GS
208 South Sudan SS
209 Spain ES
210 Sri Lanka LK
211 Sudan SD
212 Suriname SR
213 Svalbard and Jan Mayen SJ
214 Swaziland SZ
215 Sweden SE
216 Switzerland CH
217 Syrian Arab Republic SY
218 Taiwan, Province of China TW
219 Tajikistan TJ
220 Tanzania, United Republic of TZ
221 Thailand TH
222 Timor-Leste TL
223 Togo TG
224 Tokelau TK
225 Tonga TO
226 Trinidad and Tobago TT
227 Tunisia TN
228 Turkey TR
229 Turkmenistan TM
230 Turks and Caicos Islands TC
231 Tuvalu TV
232 Uganda UG
233 Ukraine UA
234 United Arab Emirates AE
235 United Kingdom GB
236 United States US
237 United States Minor Outlying Islands UM
238 Uruguay UY
239 Uzbekistan UZ
240 Vanuatu VU
241 Venezuela, Bolivarian Republic of VE
242 Viet Nam VN
243 Virgin Islands, British VG
244 Virgin Islands, U.S. VI
245 Wallis and Futuna WF
246 Western Sahara EH
247 Yemen YE
248 Zambia ZM
249 Zimbabwe ZW
250 European Union EU

View file

@ -0,0 +1,85 @@
raise 'nope'
Sequel.migration do
up {
raise 'derp'
DB.drop_table :stats
DB.drop_table :stat_referrers
DB.drop_table :stat_paths
DB.drop_table :stat_locations
DB.create_table! :hits do
primary_key :id
Integer :site_id, index: true
Integer :hit_referrer_id
Integer :hit_path_id
Integer :hit_location_id
Bignum :bandwidth
Time :accessed_at, index: true
end
DB.create_table! :hit_referrers do
primary_key :id
String :uri, index: {unique: true}
end
DB.create_table! :hit_locations do
primary_key :id
String :country_code2
String :region_name
String :city_name
Float :latitude
Float :longitude
end
DB.create_table! :hit_paths do
primary_key :id
String :path, index: {unique: true}
end
}
down {
raise 'No.' if ENV['RACK_ENV'] == 'production'
%i{hits hit_referrers hit_locations hit_paths}.each do |t|
DB.drop_table t
end
DB.create_table! :stats do
primary_key :id
Integer :site_id
Date :created_at
Integer :hits
Integer :views
Integer :comments
Integer :follows
Integer :site_updates
end
DB.create_table! :stat_referrers do
primary_key :id
Integer :stat_id
String :url
String :views
end
DB.create_table! :stat_locations do
primary_key :id
Integer :stat_id
String :country_code2
String :region_name
String :city_name
Decimal :latitude
Decimal :longitude
Integer :views
end
DB.create_table :stat_paths do
primary_key :id
Integer :stat_id
String :name
Integer :views
end
}
end

View file

@ -0,0 +1,55 @@
Sequel.migration do
up {
DB.drop_table :stats
DB.create_table! :stats do
primary_key :id
Integer :site_id, index: true
Date :created_at, index: true
Integer :hits, default: 0
Integer :views, default: 0
Integer :comments, default: 0
Integer :follows, default: 0
Integer :site_updates, default: 0
end
DB.create_table! :stat_referrers do
primary_key :id
Integer :stat_id, index: true
String :url
Integer :views, default: 0
end
DB.create_table! :stat_locations do
primary_key :id
Integer :stat_id, index: true
String :country_code2
String :region_name
String :city_name
Decimal :latitude
Decimal :longitude
Integer :views, default: 0
end
DB.create_table! :stat_paths do
primary_key :id
Integer :stat_id, index: true
String :name
Integer :views, default: 0
end
}
down {
DB.drop_table :stats
DB.create_table! :stats do
primary_key :id
Integer :site_id, index: true
Integer :hits, default: 0
Integer :views, default: 0
DateTime :created_at, index: true
end
DB.drop_table :stat_referrers
DB.drop_table :stat_locations
DB.drop_table :stat_paths
}
end

View file

@ -0,0 +1,18 @@
# This migration detaches stat_referrers, stat_locations and stat_paths
# from stats. Instead of stat_id, we'll add a created_at timestamp and remove
# after 7 days for both free and supporter plans (for now).
Sequel.migration do
up {
[:stat_referrers, :stat_paths, :stat_locations].each do |stat_table|
drop_column stat_table, :stat_id
add_column stat_table, :created_at, :date, index: true
end
}
down {
[:stat_referrers, :stat_paths, :stat_locations].each do |stat_table|
drop_column stat_table, :created_at
add_column stat_table, :stat_id, :integer, index: true
end
}
end

View file

@ -0,0 +1,16 @@
# This migration detaches stat_referrers, stat_locations and stat_paths
# from stats. Instead of stat_id, we'll add a created_at timestamp and remove
# after 7 days for both free and supporter plans (for now).
Sequel.migration do
up {
[:stat_referrers, :stat_paths, :stat_locations].each do |stat_table|
add_column stat_table, :site_id, :integer, index: true
end
}
down {
[:stat_referrers, :stat_paths, :stat_locations].each do |stat_table|
drop_column stat_table, :site_id
end
}
end

View file

@ -0,0 +1,12 @@
Sequel.migration do
up {
drop_column :stat_locations, :latitude
drop_column :stat_locations, :longitude
add_column :stat_locations, :latitude, :float
add_column :stat_locations, :longitude, :float
}
down {
# meh.
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
add_column :stats, :bandwidth, :bigint, default: 0
}
down {
drop_column :stats, :bandwidth
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
add_column :sites, :is_education, :boolean, default: false
}
down {
drop_column :sites, :is_education
}
end

View file

@ -0,0 +1,14 @@
Sequel.migration do
up {
DB.create_table! :archives do
Integer :site_id, index: true
String :ipfs_hash
DateTime :updated_at, index: true
unique [:site_id, :ipfs_hash]
end
}
down {
DB.drop_table :archives
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.add_index :sites, :username
}
down {
DB.drop_index :sites, :username
}
end

View file

@ -0,0 +1,13 @@
Sequel.migration do
up {
DB['create index stat_referrers_hash_multi on stat_referrers (site_id, md5(url))'].first
DB.add_index :stat_locations, :site_id
DB.add_index :stat_paths, :site_id
}
down {
DB['drop index stat_referrers_hash_multi'].first
DB.drop_index :stat_locations, :site_id
DB.drop_index :stat_paths, :site_id
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.add_index :stat_referrers, :site_id
}
down {
DB.drop_index :stat_referrers, :site_id
}
end

View file

@ -0,0 +1,13 @@
Sequel.migration do
up {
%i{stat_referrers stat_locations stat_paths}.each do |t|
DB.add_index t, :created_at
end
}
down {
%i{stat_referrers stat_locations stat_paths}.each do |t|
DB.drop_index t, :created_at
end
}
end

9
models/archive.rb Normal file
View file

@ -0,0 +1,9 @@
class Archive < Sequel::Model
many_to_one :site
set_primary_key [:site_id, :ipfs_hash]
unrestrict_primary_key
def url
"https://#{ipfs_hash}.ipfs.neocities.org"
end
end

View file

@ -36,6 +36,10 @@ class Site < Sequel::Model
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp
}
VALID_EDITABLE_EXTENSIONS = %w{
html htm txt js css md manifest
}
MINIMUM_PASSWORD_LENGTH = 5
BAD_USERNAME_REGEX = /[^\w-]/i
VALID_HOSTNAME = /^[a-z0-9][a-z0-9-]+?[a-z0-9]$/i # http://tools.ietf.org/html/rfc1123
@ -73,7 +77,7 @@ class Site < Sequel::Model
PHISHING_FORM_REGEX = /www.formbuddy.com\/cgi-bin\/form.pl/i
SPAM_MATCH_REGEX = ENV['RACK_ENV'] == 'test' ? /pillz/ : /#{$config['spam_smart_filter'].join('|')}/i
EMAIL_SANITY_REGEX = /.+@.+\..+/i
EDITABLE_FILE_EXT = /html|htm|txt|js|css|md|manifest/i
EDITABLE_FILE_EXT = /#{VALID_EDITABLE_EXTENSIONS.join('|')}/i
BANNED_TIME = 2592000 # 30 days in seconds
TITLE_MAX = 100
@ -98,20 +102,35 @@ class Site < Sequel::Model
unlimited_site_creation: true,
custom_ssl_certificates: true,
no_file_restrictions: true,
custom_domains: true
custom_domains: true,
maximum_site_files: 25000
}
PLAN_FEATURES[:free] = PLAN_FEATURES[:supporter].merge(
name: 'Free',
space: Filesize.from('50MB').to_i,
space: Filesize.from('100MB').to_i,
bandwidth: Filesize.from('50GB').to_i,
price: 0,
unlimited_site_creation: false,
custom_ssl_certificates: false,
no_file_restrictions: false,
custom_domains: false
custom_domains: false,
maximum_site_files: 1000
)
def self.newsletter_sites
Site.select(:email).
exclude(email: 'nil').exclude(is_banned: true).
where{updated_at > EMAIL_BLAST_MAXIMUM_AGE}.
where{changed_count > 0}.
order(:updated_at.desc).
all
end
def too_many_files?(file_count=0)
(site_files_dataset.count + file_count) > plan_feature(:maximum_site_files)
end
def plan_feature(key)
PLAN_FEATURES[plan_type.to_sym][key.to_sym]
end
@ -128,7 +147,15 @@ class Site < Sequel::Model
plan_five: 5
}
BROWSE_PAGINATION_LENGTH = 300
BROWSE_PAGINATION_LENGTH = 100
EMAIL_BLAST_MAXIMUM_AGE = 6.months.ago
if ENV['RACK_ENV'] == 'test'
EMAIL_BLAST_MAXIMUM_PER_DAY = 2
else
EMAIL_BLAST_MAXIMUM_PER_DAY = 1000
end
many_to_many :tags
@ -161,6 +188,13 @@ class Site < Sequel::Model
one_to_many :site_files
one_to_many :stats
one_to_many :stat_referrers
one_to_many :stat_locations
one_to_many :stat_paths
one_to_many :archives
def account_sites_dataset
Site.where(Sequel.|({id: owner.id}, {parent_site_id: owner.id})).order(:parent_site_id.desc, :username)
end
@ -360,25 +394,26 @@ class Site < Sequel::Model
def install_new_files
FileUtils.mkdir_p files_path
files = []
%w{index not_found}.each do |name|
tmpfile = Tempfile.new "newinstall-#{name}"
tmpfile.write render_template("#{name}.erb")
tmpfile.close
store_file "#{name}.html", tmpfile, new_install: true
purge_cache "/#{name}.html"
ScreenshotWorker.perform_async values[:username], "#{name}.html"
files << {filename: "#{name}.html", tempfile: tmpfile}
end
tmpfile = Tempfile.new 'style.css'
tmpfile.close
FileUtils.cp template_file_path('style.css'), tmpfile.path
store_file 'style.css', tmpfile, new_install: true
files << {filename: 'style.css', tempfile: tmpfile}
tmpfile = Tempfile.new 'cat.png'
tmpfile.close
FileUtils.cp template_file_path('cat.png'), tmpfile.path
store_file 'cat.png', tmpfile, new_install: true
files << {filename: 'cat.png', tempfile: tmpfile}
store_files files, new_install: true
end
def get_file(path)
@ -539,77 +574,57 @@ class Site < Sequel::Model
end
def purge_cache(path)
relative_path = path.gsub(base_files_path, '')
payload = {site: username, path: relative_path}
payload[:domain] = domain if !domain.empty?
PurgeCacheWorker.perform_async payload
relative_path = path.gsub base_files_path, ''
# We gotta flush the dirname too if it's an index file.
if relative_path != '' && relative_path.match(/\/$|index\.html?$/i)
PurgeCacheOrderWorker.perform_async username, relative_path
PurgeCacheOrderWorker.perform_async username, Pathname(relative_path).dirname.to_s
else
PurgeCacheOrderWorker.perform_async username, relative_path
end
end
def store_file(path, uploaded, opts={})
relative_path = scrubbed_path path
path = files_path path
pathname = Pathname(path)
Rye::Cmd.add_command :ipfs, nil, 'add', :r
site_file = site_files_dataset.where(path: relative_path).first
uploaded_sha1 = Digest::SHA1.file(uploaded.path).hexdigest
if site_file && site_file.sha1_hash == uploaded_sha1
return false
end
if relative_path == 'index.html' && opts[:new_install] != true
def add_to_ipfs
# Not ideal. An SoA version is in progress.
if $config['ipfs_ssh_host'] && $config['ipfs_ssh_user']
rbox = Rye::Box.new $config['ipfs_ssh_host'], :user => $config['ipfs_ssh_user']
begin
new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text
rescue NoMethodError => e
else
if new_title.length < TITLE_MAX
self.title = new_title
end
response = rbox.ipfs "sites/#{self.username.gsub(/\/|\.\./, '')}"
output_array = response
ensure
rbox.disconnect
end
self.site_changed = true
self.site_updated_at = Time.now
self.updated_at = Time.now
save_changes(validate: false)
else
line = Cocaine::CommandLine.new('ipfs', 'add -r :path')
response = line.run path: files_path
output_array = response.to_s.split("\n")
end
if pathname.extname.match HTML_REGEX
# SPAM and phishing checking code goes here
output_array.last.split(' ')[1]
end
def archive!
#if ENV["RACK_ENV"] == 'test'
# ipfs_hash = "QmcKi2ae3uGb1kBg1yBpsuwoVqfmcByNdMiZ2pukxyLWD8"
#else
#end
ipfs_hash = add_to_ipfs
archive = archives_dataset.where(ipfs_hash: ipfs_hash).first
if archive
archive.updated_at = Time.now
archive.save_changes
else
add_archive ipfs_hash: ipfs_hash, updated_at: Time.now
end
end
dirname = pathname.dirname.to_s
if !File.exists? dirname
FileUtils.mkdir_p dirname
end
uploaded_size = uploaded.size
FileUtils.cp uploaded.path, path
File.chmod 0640, path
site_file ||= SiteFile.new site_id: self.id, path: relative_path
site_file.set_all(
size: uploaded_size,
sha1_hash: uploaded_sha1,
updated_at: Time.now
)
site_file.save
purge_cache path
if pathname.extname.match HTML_REGEX
ScreenshotWorker.perform_async values[:username], relative_path
elsif pathname.extname.match IMAGE_REGEX
ThumbnailWorker.perform_async values[:username], relative_path
end
SiteChange.record self, relative_path unless opts[:new_install]
true
def latest_archive
@latest_archive ||= archives_dataset.order(:updated_at.desc).first
end
def is_directory?(path)
@ -626,11 +641,6 @@ class Site < Sequel::Model
true
end
def increment_changed_count
self.changed_count += 1
save_changes(validate: false)
end
def files_zip
zip_name = "neocities-#{username}"
@ -654,41 +664,17 @@ class Site < Sequel::Model
tmpfile.path
end
def delete_file(path)
begin
FileUtils.rm files_path(path)
rescue Errno::EISDIR
site_files.each do |site_file|
if site_file.path.match /^#{path}\//
site_file.destroy
end
end
FileUtils.remove_dir files_path(path), true
rescue Errno::ENOENT
end
purge_cache path
ext = File.extname(path).gsub(/^./, '')
screenshots_delete(path) if ext.match HTML_REGEX
thumbnails_delete(path) if ext.match IMAGE_REGEX
path = path[1..path.length] if path[0] == '/'
site_files_dataset.where(path: path).delete
SiteChangeFile.filter(site_id: self.id, filename: path).delete
true
end
def move_files_from(oldusername)
FileUtils.mv base_files_path(oldusername), base_files_path
end
def install_new_html_file(path)
File.write files_path(path), render_template('index.erb')
tmpfile = Tempfile.new 'neocities_html_template'
tmpfile.write render_template('index.erb')
tmpfile.close
store_files [{filename: path, tempfile: tmpfile}]
purge_cache path
tmpfile.unlink
end
def file_exists?(path)
@ -830,6 +816,18 @@ class Site < Sequel::Model
new_tags.compact!
@new_filtered_tags = []
if values[:is_education] == true
if new?
if @new_tags_string.nil? || @new_tags_string.empty?
errors.add :new_tags_string, 'A Class Tag is required.'
end
if new_tags.length > 1
errors.add :new_tags_string, 'Must only have one tag'
end
end
end
if ((new? ? 0 : tags_dataset.count) + new_tags.length > 5)
errors.add :new_tags_string, 'Cannot have more than 5 tags for your site.'
end
@ -856,7 +854,7 @@ class Site < Sequel::Model
break
end
next if tags.collect {|t| t.name}.include? tag
next if !new? && tags.collect {|t| t.name}.include?(tag)
@new_filtered_tags << tag
@new_filtered_tags.uniq!
@ -945,6 +943,7 @@ class Site < Sequel::Model
((total_space_used.to_f / maximum_space) * 100).round(1)
end
# Note: Change Stat#prune! if you change this business logic.
def supporter?
owner.plan_type != 'free'
end
@ -966,6 +965,7 @@ class Site < Sequel::Model
!values[:plan_type].match(/plan_/).nil?
end
# Note: Change Stat#prune! if you change this business logic.
def plan_type
return 'free' if owner.values[:plan_type].nil?
return 'supporter' if owner.values[:plan_type].match /^plan_/
@ -1089,4 +1089,149 @@ class Site < Sequel::Model
end
end
end
# array of hashes: filename, tempfile, opts.
def store_files(files, opts={})
results = []
new_size = 0
html_uploaded = false
if too_many_files?(files.length)
results << false
return results
end
files.each do |file|
html_uploaded = true if file[:filename].match HTML_REGEX
existing_size = 0
site_file = site_files_dataset.where(path: scrubbed_path(file[:filename])).first
if site_file
existing_size = site_file.size
end
res = store_file(file[:filename], file[:tempfile], file[:opts] || opts)
if res == true
new_size -= existing_size
new_size += file[:tempfile].size
end
results << res
end
if results.include? true && opts[:new_install] != true
time = Time.now
sql = DB["update sites set site_changed=?, site_updated_at=?, updated_at=?, changed_count=changed_count+1, space_used=space_used#{new_size < 0 ? new_size.to_s : '+'+new_size.to_s} where id=?",
true,
time,
time,
self.id
]
sql.first
reload
#SiteChange.record self, relative_path unless opts[:new_install]
ArchiveWorker.perform_async self.id
end
results
end
def delete_file(path)
return false if files_path(path) == files_path
begin
FileUtils.rm files_path(path)
rescue Errno::EISDIR
site_files.each do |site_file|
if site_file.path.match /^#{path}\//
site_file.destroy
end
end
FileUtils.remove_dir files_path(path), true
rescue Errno::ENOENT
end
purge_cache path
ext = File.extname(path).gsub(/^./, '')
screenshots_delete(path) if ext.match HTML_REGEX
thumbnails_delete(path) if ext.match IMAGE_REGEX
path = path[1..path.length] if path[0] == '/'
DB.transaction do
site_file = site_files_dataset.where(path: path).first
if site_file
DB['update sites set space_used=space_used-? where id=?', site_file.size, self.id].first
site_file.delete
end
SiteChangeFile.filter(site_id: self.id, filename: path).delete
end
true
end
private
def store_file(path, uploaded, opts={})
relative_path = scrubbed_path path
path = files_path path
pathname = Pathname(path)
site_file = site_files_dataset.where(path: relative_path).first
uploaded_sha1 = Digest::SHA1.file(uploaded.path).hexdigest
if site_file && site_file.sha1_hash == uploaded_sha1
return false
end
if relative_path == 'index.html'
begin
new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text
rescue NoMethodError => e
else
if new_title.length < TITLE_MAX
self.title = new_title
save_changes validate: false
end
end
end
if pathname.extname.match HTML_REGEX
# SPAM and phishing checking code goes here
end
dirname = pathname.dirname.to_s
if !File.exists? dirname
FileUtils.mkdir_p dirname
end
uploaded_size = uploaded.size
FileUtils.cp uploaded.path, path
File.chmod 0640, path
SiteChange.record self, relative_path unless opts[:new_install]
site_file ||= SiteFile.new site_id: self.id, path: relative_path
site_file.set_all(
size: uploaded_size,
sha1_hash: uploaded_sha1,
updated_at: Time.now
)
site_file.save
purge_cache path
if pathname.extname.match HTML_REGEX
ScreenshotWorker.perform_async values[:username], relative_path
elsif pathname.extname.match IMAGE_REGEX
ThumbnailWorker.perform_async values[:username], relative_path
end
true
end
end

View file

@ -1,6 +1,5 @@
class SiteFile < Sequel::Model
unrestrict_primary_key
plugin :update_primary_key
many_to_one :site
end
end

View file

@ -1,3 +1,244 @@
class Stat < Sequel::Model
FREE_RETAINMENT_DAYS = 30
many_to_one :site
end
one_to_many :stat_referrers
one_to_many :stat_locations
one_to_many :stat_paths
class << self
def prune!
DB[
"DELETE FROM stats WHERE created_at < ? AND site_id NOT IN (SELECT id FROM sites WHERE plan_type IS NOT NULL OR plan_type != 'free')",
(FREE_RETAINMENT_DAYS-1).days.ago.to_date.to_s
].first
end
def parse_logfiles(path)
Dir["#{path}/*.log"].each do |log_path|
site_logs = {}
logfile = File.open log_path, 'r'
while hit = logfile.gets
hit_array = hit.strip.split "\t"
raise ArgumentError, hit.inspect if hit_array.length > 6
time, username, size, path, ip, referrer = hit_array
next if !referrer.nil? && referrer.match(/bot/i)
site_logs[username] = {
hits: 0,
views: 0,
bandwidth: 0,
view_ips: [],
ips: [],
referrers: {},
paths: {}
} unless site_logs[username]
site_logs[username][:hits] += 1
site_logs[username][:bandwidth] += size.to_i
unless site_logs[username][:view_ips].include?(ip)
site_logs[username][:views] += 1
site_logs[username][:view_ips] << ip
if referrer != '-' && !referrer.nil?
site_logs[username][:referrers][referrer] ||= 0
site_logs[username][:referrers][referrer] += 1
end
end
site_logs[username][:paths][path] ||= 0
site_logs[username][:paths][path] += 1
end
logfile.close
current_time = Time.now.utc
current_day_string = current_time.to_date.to_s
Site.select(:id, :username).where(username: site_logs.keys).all.each do |site|
site_logs[site.username][:id] = site.id
end
DB.transaction do
site_logs.each do |username, site_log|
DB['update sites set hits=hits+?, views=views+? where username=?',
site_log[:hits],
site_log[:views],
username
].first
opts = {site_id: site_log[:id], created_at: current_day_string}
stat = Stat.select(:id).where(opts).first
DB[:stats].lock('EXCLUSIVE') { stat = Stat.create opts } if stat.nil?
DB[
'update stats set hits=hits+?, views=views+?, bandwidth=bandwidth+? where id=?',
site_log[:hits],
site_log[:views],
site_log[:bandwidth],
stat.id
].first
=begin
site_log[:referrers].each do |referrer, views|
stat_referrer = StatReferrer.create_or_get site_log[:id], referrer
DB['update stat_referrers set views=views+? where site_id=?', views, site_log[:id]].first
end
site_log[:view_ips].each do |ip|
site_location = StatLocation.create_or_get site_log[:id], ip
next if site_location.nil?
DB['update stat_locations set views=views+1 where id=?', site_location.id].first
end
site_log[:paths].each do |path, views|
site_path = StatPath.create_or_get site_log[:id], path
next if site_path.nil?
DB['update stat_paths set views=views+? where id=?', views, site_path.id].first
end
=end
end
end
FileUtils.rm log_path
end
end
end
end
=begin
require 'io/extra'
require 'geoip'
# Note: This isn't really a class right now.
module Stat
class << self
def parse_logfiles(path)
Dir["#{path}/*.log"].each do |logfile_path|
parse_logfile logfile_path
FileUtils.rm logfile_path
end
end
def parse_logfile(path)
geoip = GeoIP.new GEOCITY_PATH
logfile = File.open path, 'r'
hits = []
while hit = logfile.gets
time, username, size, path, ip, referrer = hit.split ' '
site = Site.select(:id).where(username: username).first
next unless site
paths_dataset = StatsDB[:paths]
path_record = paths_dataset[name: path]
path_id = path_record ? path_record[:id] : paths_dataset.insert(name: path)
referrers_dataset = StatsDB[:referrers]
referrer_record = referrers_dataset[name: referrer]
referrer_id = referrer_record ? referrer_record[:id] : referrers_dataset.insert(name: referrer)
location_id = nil
if city = geoip.city(ip)
locations_dataset = StatsDB[:locations].select(:id)
location_hash = {country_code2: city.country_code2, region_name: city.region_name, city_name: city.city_name}
location = locations_dataset.where(location_hash).first
location_id = location ? location[:id] : locations_dataset.insert(location_hash)
end
hits << [site.id, referrer_id, path_id, location_id, size, time]
end
StatsDB[:hits].import(
[:site_id, :referrer_id, :path_id, :location_id, :bytes_sent, :logged_at],
hits
)
end
end
end
=begin
def parse_logfile(path)
hits = {}
visits = {}
visit_ips = {}
logfile = File.open path, 'r'
while hit = logfile.gets
time, username, size, path, ip, referrer = hit.split ' '
hits[username] ||= 0
hits[username] += 1
visit_ips[username] = [] if !visit_ips[username]
unless visit_ips[username].include? ip
visits[username] ||= 0
visits[username] += 1
visit_ips[username] << ip
end
end
logfile.close
hits.each do |username,hitcount|
DB['update sites set hits=hits+? where username=?', hitcount, username].first
end
visits.each do |username,visitcount|
DB['update sites set views=views+? where username=?', visitcount, username].first
end
end
end
=end
=begin
def self.parse(logfile_path)
hits = {}
visits = {}
visit_ips = {}
logfile = File.open logfile_path, 'r'
while hit = logfile.gets
time, username, size, path, ip = hit.split ' '
hits[username] ||= 0
hits[username] += 1
visit_ips[username] = [] if !visit_ips[username]
unless visit_ips[username].include?(ip)
visits[username] ||= 0
visits[username] += 1
visit_ips[username] << ip
end
end
logfile.close
hits.each do |username,hitcount|
DB['update sites set hits=hits+? where username=?', hitcount, username].first
end
visits.each do |username,visitcount|
DB['update sites set views=views+? where username=?', visitcount, username].first
end
end
=end

27
models/stat_location.rb Normal file
View file

@ -0,0 +1,27 @@
require 'geoip'
class StatLocation < Sequel::Model
GEOCITY_PATH = './files/GeoLiteCity.dat'
RETAINMENT_DAYS = 7
many_to_one :site
def self.prune!
where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete
end
def self.create_or_get(site_id, ip)
geoip = GeoIP.new GEOCITY_PATH
city = geoip.city ip
return nil if city.nil?
opts = {site_id: site_id, country_code2: city.country_code2, region_name: city.region_name, city_name: city.city_name}
stat_location = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first
DB[table_name].lock('EXCLUSIVE') {
stat_location = create opts.merge(latitude: city.latitude, longitude: city.longitude, created_at: Date.today)
} if stat_location.nil?
stat_location
end
end

19
models/stat_path.rb Normal file
View file

@ -0,0 +1,19 @@
class StatPath < Sequel::Model
RETAINMENT_DAYS = 7
many_to_one :site
def self.prune!
where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete
end
def self.create_or_get(site_id, name)
opts = {site_id: site_id, name: name}
stat_path = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first
DB[table_name].lock('EXCLUSIVE') {
stat_path = create opts.merge created_at: Date.today
} if stat_path.nil?
stat_path
end
end

19
models/stat_referrer.rb Normal file
View file

@ -0,0 +1,19 @@
class StatReferrer < Sequel::Model
many_to_one :site
RETAINMENT_DAYS = 7
def self.prune!
where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete
end
def self.create_or_get(site_id, url)
opts = {site_id: site_id, url: url}
stat_referrer = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first
DB[table_name].lock('EXCLUSIVE') {
stat_referrer = create opts.merge(created_at: Date.today)
} if stat_referrer.nil?
stat_referrer
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

11
public/js/Chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -3,16 +3,13 @@ var Comment = {
var form = $(form)
var comment = form.find('[name="comment"]').val()
form.remove()
$.post('/event/'+eventId+'/comment', {csrf_token: csrfToken, message: comment}, function(res) {
console.log(res)
location.reload()
})
},
delete: function(commentId, csrfToken) {
$.post('/comment/'+commentId+'/delete', {csrf_token: csrfToken}, function(res) {
console.log(res)
location.reload()
})
},

View file

@ -59,7 +59,7 @@ a{
@import 'project-Main'; // Project Specific Main Content Area Styling
@import 'project-Footer'; // Project Specific Footer Styling
.alert{
.alert-error {
background-color:#F5BA00; color:#fff;
}

View file

@ -83,6 +83,23 @@
}
}
}
.hp.education {
.col.intro {
padding-top: 20px;
img {
@include vendor(transform, scaleX(-1));
width: 100px;
margin-right: 25px;
&.float-Right {
margin-left: 10px;
}
}
.intro-text {
font-size: 1.1em;
}
}
}
.intro-List{
@extend %kill-List;
@ -490,4 +507,4 @@
}
}
.interior .constant-Nav{margin:0}
.interior .constant-Nav, .hp.education .constant-Nav{margin:0}

View file

@ -7,18 +7,18 @@
background:#f6f0e6;
min-height:500px;
padding-bottom:50px;
h1, h2, h3, h4, h5, h6{
color:#e93250
}
}
.content, .footer-Content {
padding: 20px 3%;
@media (max-device-width:480px), screen and (max-width:800px) {
padding: 20px 7%;
}
@media screen and (min-width:1300px){
padding: 20px 0;
}
@ -34,7 +34,7 @@
.interior .header-Outro {
padding-top: 30px;
overflow: hidden;
@media (max-device-width:480px), screen and (max-width:800px) {
padding-top: 15px;
}
@ -47,13 +47,13 @@
@media (max-device-width:480px), screen and (max-width:800px) {
float: right;
width: 35%;
.col-50 {
width:100%;
margin-bottom: 1em!important;
}
}
}
.site-url {
font-size:18px;
@ -78,11 +78,11 @@
-webkit-box-shadow: 0px 1px 1px 1px rgba(0,0,0,0.10);
box-shadow: 0px 1px 1px 1px rgba(0,0,0,0.10);
padding: 25px 3% 40px 3%;
@media (max-device-width:480px), screen and (max-width:800px) {
padding: 22px 7% 40px 7%;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1em;
&:first-child {
@ -122,19 +122,19 @@
}
.site-url {
margin-top: -9px;
}
}
.btn-Action {
font-weight: normal;
font-size: 12px;
padding: 7px 15px;
@media (max-device-width:480px), screen and (max-width:800px) {
margin: 6px 0px 8px;
}
}
.row.content {
margin-left: 6%;
margin-right: 6%;
margin-right: 6%;
}
}
.interior .header-Outro a {color:#E93250}
@ -151,7 +151,7 @@
-webkit-box-shadow: 1px 2px 5px 2px rgba(0,0,0,0.10);
box-shadow: 1px 2px 5px 2px rgba(0,0,0,0.10);
border: 4px solid white;
@media (max-device-width:480px), screen and (max-width:800px) {
margin: 0 auto;
}
@ -165,7 +165,7 @@
-webkit-background-size:cover;
background-size:cover;
background-position: center top;
@media (max-device-width:480px), screen and (max-width:800px) {
width: 100%;
height: 300px;
@ -203,7 +203,7 @@
}
.interior .header-Outro.dashboard .col-50:nth-of-type(2) {
float: left;
@media (max-device-width: 480px), screen and (max-width: 800px) {
margin-left: 22px;
margin-top: 18px;
@ -215,7 +215,7 @@
background-size: 77px 81px;
padding: 20px 20px 20px 111px;
margin-bottom: 13px;
@media (max-device-width: 480px), screen and (max-width: 800px) {
padding: 111px 20px 20px 20px;
background-position: center 20px;
@ -233,7 +233,7 @@
}
.files {
float:left;
background: #E4D8CB;
background: #EAE1D5;
width: 100%;
position: relative;
margin-top: 7px;
@ -262,7 +262,7 @@
}
.files .btn-Action {
margin-left: 8px;
@media (max-device-width:480px), screen and (max-width:800px) {
margin: 4px 8px 4px 0;
}
@ -278,12 +278,12 @@
padding: 10px;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
border-radius: 6px;
min-height: 300px;
}
.files .list .upload-Boundary.with-instruction {
background: url(/img/drag-drop.png) no-repeat center center;
@media (max-device-width:480px), screen and (max-width:800px) {
background: 0;
}
@ -312,7 +312,7 @@
margin-bottom: 2px;
}
.files .progress-bar {
background:#CCCCCC;
background:#CCCCCC;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
@ -472,7 +472,7 @@
position: relative;
clear: both;
width: 100%;
h2:first-of-type, h3:first-of-type {
margin-top: 0;
}
@ -516,7 +516,7 @@
width: 66.95%;
left: 33.05%;
background: white;
@media (max-device-width:480px), screen and (max-width:800px) {
width: 100%;
border-bottom: 1px solid #ddd;
@ -526,14 +526,14 @@
.site-profile .content.misc-page.columns .col-66 {
padding: 0;
min-height: 38em;
@media (max-device-width:480px), screen and (max-width:800px) {
min-height: 0px;
}
}
.news-feed .content.misc-page.columns .col-66 {
min-height: 56em;
@media (max-device-width:480px), screen and (max-width:800px) {
min-height: 0;
}
@ -542,14 +542,14 @@
width: 33%;
left: 33%;
float: right;
@media (max-device-width:480px), screen and (max-width:800px) {
width: 100%;
}
}
.interior .header-Outro.with-columns {
padding-top: 22px;
}
}
.interior .header-Outro.with-columns h3 {
float: left;
margin-bottom: 0;
@ -560,14 +560,14 @@
}
.interior .header-Outro.with-columns .col {
padding: 25px 0 8px 30px;
@media (max-device-width:480px), screen and (max-width:800px) {
padding: 10px 0 0 27px;
}
}
.interior .header-Outro.with-columns .col-32 {
width: 33%;
h3 {
@media (max-device-width:480px), screen and (max-width:800px) {
display: none;
@ -583,10 +583,10 @@
.interior .header-Outro.with-columns .col-66 {
width: 67%;
border-right: 1px solid #0B0F11;
@media (max-device-width:480px), screen and (max-width:800px) {
border-right: 0;
padding: 15px 0 27px 30px;
padding: 15px 0 27px 30px;
}
}
.interior .header-Outro.with-columns .col-32 .edit {
@ -600,7 +600,7 @@
margin-top: 1.4em;
font-size: 0.8em;
margin-left: 1.5em;
@media (max-device-width:480px), screen and (max-width:800px) {
margin: 0;
clear: left;
@ -635,7 +635,7 @@
background-size: cover;
float: left;
margin-bottom: 20px;
@media (max-device-width:480px), screen and (max-width:800px) {
width: 60%;
}
@ -661,11 +661,11 @@
clear: both;
margin: 20px auto 2em auto;
text-align: center;
.btn-Action:last-child {
margin-left: 10px;
}
@media (max-device-width:480px), screen and (max-width:800px) {
text-align: left;
}
@ -750,7 +750,7 @@ a.tag:hover {
background-repeat: no-repeat;
}
.news-item .icon-mini {
}
.news-item.update .icon {
background: #E93250;
@ -797,7 +797,7 @@ a.tag:hover {
float: left;
width: 100%;
}
.news-item .file a:hover {
.news-item .file a:hover {
text-decoration: none;
}
.news-item .html-thumbnail {
@ -863,7 +863,7 @@ a.tag:hover {
.signup-Area.large {
width: 418px;
height: 236px;
@media (max-device-width:480px), screen and (max-width:800px) {
height: auto;
}
@ -894,14 +894,14 @@ a.tag:hover {
a:first-of-type {
margin-left: 0;
}
}
.interior .header-Outro .stats {
margin-bottom: 1.3em;
float: left;
width: 100%;
margin-top: 1.9em;
@media (max-device-width:480px), screen and (max-width:800px) {
float: none;
width: 270px;
@ -925,7 +925,7 @@ a.tag:hover {
text-align: center;
margin-right: 28px;
color: #84997E;
@media (max-device-width:480px), screen and (max-width:800px) {
margin: 0;
width: 90px;
@ -941,21 +941,21 @@ a.tag:hover {
.following {
display: none;
}
.unfollow {
.unfollow {
display: none;
}
}
#followLink.is-following {
.follow {
display: none;
}
.following {
.following {
display: block;
width: 5.9em;
}
.unfollow {
.unfollow {
display: none;
}
}
@ -963,10 +963,10 @@ a.tag:hover {
.follow {
display: none;
}
.following {
.following {
display: none;
}
.unfollow {
.unfollow {
display: block;
width: 5.9em;
}
@ -997,7 +997,7 @@ a.tag:hover {
border: 3px solid white;
-webkit-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3);
-moz-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3);
box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3);
box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3);
width: 72px;
}
.archives img {
@ -1014,11 +1014,11 @@ a.tag:hover {
width: 100%;
padding: 20px 0 15px 30px;
margin: 0 0 3px 0;
input {
width: 86%;
float: left;
@media (max-width:950px) {
width: 82%;
}
@ -1031,7 +1031,7 @@ a.tag:hover {
.comment-policy {
font-style: italic;
font-size: .8em;
margin-right: 45px;
margin-right: 45px;
clear: both;
margin-top: 2em;
}
@ -1096,11 +1096,11 @@ a.tag:hover {
.row {
margin: 5.5em 10%;
padding: 0;
@media (max-device-width:480px), screen and (max-width:800px){
margin: 4em 5%;
}
&:first-of-type {
margin-top: 3.5em;
}
@ -1114,7 +1114,7 @@ a.tag:hover {
padding-top: 2em;
float: left;
}
@media (max-device-width:480px), screen and (max-width:800px){
width: 100%;
padding-right: 0px;
@ -1129,7 +1129,7 @@ a.tag:hover {
background-repeat: no-repeat;
border: 13px solid #4f7d88;
@include box-shadow(1px 1px 2px 0px rgba(0, 0, 0, 0.18));
}
&:nth-child(3) .screenshot {
border: 13px solid $c-Brand-1;
@ -1160,7 +1160,7 @@ a.tag:hover {
border-bottom: 1px solid #801629;
padding: 30px 0;
position: relative;
h2.delta {
color: white;
margin: 0;
@ -1193,17 +1193,45 @@ a.tag:hover {
background-image:url(/img/hpgallery-next.png);
right: 0;
}
.section.instructor-quotes {
background: #971D31;
h2, h3, p {
color: #fff;
text-align: left;
}
h2 {
margin-left: 20px;
}
p {
font-size: 1em;
font-style: italic;
margin-right: 30px;
margin-top: 20px;
clear: both;
}
.image {
float: left;
margin-right: 22px;
}
}
.section.features {
background: #4F7E89;
text-align: center;
.row:first-of-type {
margin-bottom: 0;
}
}
.section.support {
padding-bottom: 100px;
p {
font-size: 1.1em;
}
}
.section.features .col {
padding-left: 40px;
@media (max-device-width:480px), screen and (max-width:800px){
padding-left: 20px;
width: 100%;
@ -1237,7 +1265,7 @@ a.tag:hover {
clear: both;
margin-top: 30px;
margin-bottom: 0px;
@media (max-device-width:480px), screen and (max-width:800px){
margin-top: 0;
}
@ -1247,10 +1275,10 @@ a.tag:hover {
background-repeat: no-repeat;
margin-right: 30px;
background-size: 100%;
@media (max-width:1170px) and (min-width:900px){
margin-right: 25px!important;
&:last-child {
margin-right: 0!important;
}
@ -1264,11 +1292,11 @@ a.tag:hover {
}
.section .logo.wired {
width: 211px;
height: 44px;
height: 44px;
background-image: url(/img/wired-logo.png);
margin-top: 6px;
margin-right: 38px;
@media (max-width:1170px) and (min-width:900px){
width: 170px;
}
@ -1278,7 +1306,7 @@ a.tag:hover {
}
.section .logo.fastco {
width: 262px;
height: 39px;
height: 39px;
background-image: url(/img/fastcompany-logo.png);
margin-top: 7px;
@media (max-width:1170px) and (min-width:900px){
@ -1287,7 +1315,7 @@ a.tag:hover {
}
.section .logo.vice {
width: 160px;
height: 50px;
height: 50px;
background-image: url(/img/vice-logo.png);
@media (max-width:1170px) and (min-width:900px){
width: 120px;
@ -1295,7 +1323,7 @@ a.tag:hover {
}
}
.section .logo.ars {
width: 187px;
width: 187px;
height: 62px;
background-image: url(/img/ars-logo.png);
margin-top: -4px;
@ -1313,7 +1341,7 @@ a.tag:hover {
.section .quote {
width: 70%;
margin: 0 auto .5em auto;
h3 {
margin-top: .5em;
text-align: center;
@ -1323,9 +1351,9 @@ a.tag:hover {
font-family: $times;
color: #afcbd1;
}
a {
color: #afcbd1;
}
}
.section.press .quote a {
color: #afcbd1;
}
.section.plans {
@media(max-device-width:480px), screen and (max-width:550px) {
@ -1470,12 +1498,12 @@ a.tag:hover {
border: 0;
background: 0;
padding-right: 15px;
span {
border-bottom: 1px dotted #bbb;
cursor: help;
}
@media(max-device-width:480px), screen and (max-width:800px) {
width: 26%;
}
@ -1493,7 +1521,7 @@ a.tag:hover {
}
.section.plans.welcome {
padding: 63px 3% 0 3%;
h3 {
color: #5e95a1;
}
@ -1501,7 +1529,7 @@ a.tag:hover {
width: 68%;
margin-left: auto;
margin-right: auto;
@media(max-device-width:480px), screen and (max-width:550px) {
width: 100%;
}
@ -1511,18 +1539,18 @@ a.tag:hover {
margin-right: auto;
max-width: 990px;
min-width: 900px;
@media(max-device-width:480px), screen and (max-width:550px) {
min-width: 0;
}
}
>.row >.col {
padding-left: 40px;
&:first-child {
padding-left: 0;
}
@media(max-device-width:480px), screen and (max-width:550px) {
padding-left: 0;
width: 100%;
@ -1540,10 +1568,10 @@ a.tag:hover {
float: left;
background: 0;
padding-bottom: 0;
ul {
clear: both;
&.main-features {
font-size: 140%;
margin-bottom: .7em;
@ -1579,7 +1607,7 @@ a.tag:hover {
border-right-width: 0;
margin-top: 15px;
margin-bottom: 15px;
.main-features {
margin-bottom: 76px;
}
@ -1590,7 +1618,7 @@ a.tag:hover {
border: 1px solid #E0E0E0;
@include box-shadow(-1px 1px 5px 0px rgba(0,0,0,0.1));
margin-bottom: 1.2em!important;
.col:first-child {
width: 38%;
}
@ -1614,7 +1642,7 @@ a.tag:hover {
margin-right: 3px;
}
}
@media(max-device-width:480px), screen and (max-width:550px) {
@media(max-device-width:480px), screen and (max-width:550px) {
.col.free, .col.supporter, .col.supporter .col {
width: 100%!important;
}
@ -1633,11 +1661,11 @@ a.tag:hover {
.section.bottom-signup {
// padding-top: 1em;
// padding-bottom: 6.5em;
padding-top: 0;
background: #4F7E89;
padding-bottom: 7em;
a {
color: white;
text-decoration: underline;
@ -1743,7 +1771,7 @@ a.tag:hover {
width: 100%;
margin-top: 22px;
font-size: 15px;
a, a:visited {
color: white;
}
@ -1762,17 +1790,17 @@ a.tag:hover {
.tools {
color: #8099A7;
float: right;
.theme {
font-size: .9em;
display: inline;
select#theme {
vertical-align: baseline;
background: #25333c;
color: #8099A7;
}
}
@media (max-device-width:480px), screen and (max-width:800px) {
display: none;
}
@ -1780,7 +1808,7 @@ a.tag:hover {
#saveButton {
margin-top: 0;
}
.tooltip {
.tooltip {
&.bottom .tooltip-arrow {
border-bottom-color: #971D31;
}
@ -1820,7 +1848,7 @@ a.tag:hover {
background: rgba(228, 228, 228, 0.42);
width: 100%;
text-align: center;
padding: 26px 0 5px;
padding: 26px 0 5px;
p {
margin-top: 10px;
}
@ -1852,4 +1880,75 @@ a.tag:hover {
@media (max-device-width:480px), screen and (max-width:800px) {
display: inline;
}
}
table#latest-visitors {
width: 100%;
color: #777;
font-size: .8em;
td {
white-space: nowrap;
max-width: 0;
}
span {
display: block;
}
.location {
color: #2f4149;
font-weight: bold;
font-size: 1.2em;
}
.paths {
overflow: hidden;
text-overflow: ellipsis;
}
}
#earth_div {
width: 100%;
height: 400px;
}
.content.misc-page.columns .col.globe {
padding-right: 0;
padding-top: 4px;
}
.news-feed .content.misc-page .col-50 {
@media (max-device-width:480px), screen and (max-width:800px) {
width: 100%;
}
}
.intro-List.kickstarter .col {
padding-top: 1em;
padding-bottom: .8em;
margin-left: 0;
&:first-child{
padding-left: 2px;
}
.title {
margin-top: 1%;
}
.title a {
color: white;
font-weight: bold;
text-decoration: none;
}
p {
margin-top: 15px;
}
}
.welcome.kickstarter {
background: #daeea5 url(/img/tutorialthumbnail.png) no-repeat;
background-position: right center;
background-size: auto 100%;
padding: 15px 100px 4px 23px;
margin-bottom: 13px;
font-size: 95%;
h4 {
margin-bottom: .2em;
a {
color: #2c3e50!important;
}
}
}

View file

@ -51,4 +51,4 @@ describe '/admin' do
end
end
end
end

View file

@ -17,10 +17,22 @@ describe 'dashboard' do
visit '/dashboard'
click_link 'New Folder'
fill_in 'name', with: 'testimages'
click_button 'Create'
#click_button 'Create'
all('#createDir button[type=submit]').first.click
page.must_have_content /testimages/
File.directory?(@site.files_path('testimages')).must_equal true
end
it 'creates a new file' do
random = SecureRandom.uuid.gsub('-', '')
visit '/dashboard'
click_link 'New File'
fill_in 'filename', with: "#{random}.html"
#click_button 'Create'
all('#createFile button[type=submit]').first.click
page.must_have_content /#{random}\.html/
File.exist?(@site.files_path("#{random}.html")).must_equal true
end
end
end
end
end

View file

@ -0,0 +1,61 @@
require_relative './environment.rb'
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: false)
end
describe 'signup' do
include Capybara::DSL
def fill_in_valid
@site = Fabricate.attributes_for(:site)
@class_tag = SecureRandom.uuid.gsub('-', '')[0..Tag::NAME_LENGTH_MAX-1]
fill_in 'username', with: @site[:username]
fill_in 'password', with: @site[:password]
fill_in 'email', with: @site[:email]
fill_in 'new_tags_string', with: @class_tag
end
before do
Capybara.default_driver = :poltergeist
Capybara.reset_sessions!
visit '/education'
page.must_have_content 'Neocities' # Used to force load wait
end
after do
Capybara.default_driver = :rack_test
end
it 'succeeds with valid data' do
fill_in_valid
click_button 'Create My Site'
page.must_have_content /Welcome to your new site/
index_file_path = File.join Site::SITE_FILES_ROOT, @site[:username], 'index.html'
File.exist?(index_file_path).must_equal true
site = Site[username: @site[:username]]
site.site_files.length.must_equal 4
site.site_changed.must_equal false
site.site_updated_at.must_equal nil
site.is_education.must_equal true
site.tags.length.must_equal 1
site.tags.first.name.must_equal @class_tag
end
it 'fails to create for existing site' do
@existing_site = Fabricate :site
fill_in_valid
fill_in :username, with: @existing_site.username
click_button 'Create My Site'
page.must_have_content 'already taken'
end
it 'fails for multiple tags' do
fill_in_valid
fill_in :new_tags_string, with: 'derp, ie'
click_button 'Create My Site'
page.must_have_content 'Must only have one tag'
end
end

View file

@ -4,4 +4,6 @@ Capybara.app = Sinatra::Application
def teardown
Capybara.reset_sessions!
end
end
Capybara.default_wait_time = 5

View file

@ -11,8 +11,8 @@ describe '/' do
it 'loads the news feed with welcome' do
visit '/'
page.body.must_match /Neocities news feed/i
page.body.must_match /You arent following any websites yet/i
page.body.must_match /Thanks for joining the Neocities community/i
page.body.wont_match /You arent following any websites yet/i
end
it 'displays a follow and an unrelated follow' do

View file

@ -30,6 +30,7 @@ describe 'signup' do
Capybara.default_driver = :poltergeist
Capybara.reset_sessions!
visit_signup
page.must_have_content 'Neocities' # Used to force load wait
end
after do
@ -48,19 +49,15 @@ describe 'signup' do
site.site_files.length.must_equal 4
site.site_changed.must_equal false
site.site_updated_at.must_equal nil
site.is_education.must_equal false
site.ip.must_equal Site.hash_ip('127.0.0.1')
end
it 'fails to create for existing site' do
@existing_site = Fabricate :site
fill_in_valid
click_signup_button
page.must_have_content 'Welcome to Neocities'
Capybara.reset_sessions!
visit_signup
sleep 0.3
fill_in 'username', with: @site[:username]
fill_in 'password', with: @site[:password]
fill_in 'username', with: @existing_site.username
click_signup_button
page.must_have_content 'already taken'
end
@ -113,9 +110,6 @@ describe 'signup' do
page.must_have_content /email.+exists/
end
puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS"
=begin
it 'succeeds with no tags' do
fill_in_valid
fill_in 'new_tags_string', with: ''
@ -139,7 +133,7 @@ puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS"
Site.last.tags.collect {|t| t.name}.must_equal ['derpie', 'shoujo']
end
it 'fails with invalid tag chars' do
it 'fails with invalid tag chars' do
fill_in_valid
fill_in 'new_tags_string', with: '$POLICE OFFICER$$$$$, derp'
click_signup_button
@ -179,9 +173,10 @@ puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS"
fill_in 'new_tags_string', with: 'one, one'
click_signup_button
site = Site.last
page.must_have_content /Welcome to Neocities/
site = Site[username: @site[:username]]
site.tags.length.must_equal 1
site.tags.first.name.must_equal 'one'
end
=end
end

View file

@ -21,7 +21,7 @@ describe 'site page' do
click_button 'Post'
@site.profile_comments.count.must_equal 1
profile_comment = @site.profile_comments.first
profile_comment.actioning_site.must_equal @commenting_site
profile_comment.actioning_site.id.must_equal @commenting_site.id
profile_comment.message.must_equal 'I love your site!'
end

56
tests/admin_tests.rb Normal file
View file

@ -0,0 +1,56 @@
require_relative './environment.rb'
require 'rack/test'
include Rack::Test::Methods
def app
Sinatra::Application
end
describe 'email blasting' do
before do
EmailWorker.jobs.clear
@admin_site = Fabricate :site, is_admin: true
end
it 'works' do
DB['update sites set changed_count=?', 0].first
relevant_emails = []
sites_emailed_count = Site::EMAIL_BLAST_MAXIMUM_PER_DAY*2
sites_emailed_count.times {
site = Fabricate :site, updated_at: Time.now, changed_count: 1
relevant_emails << site.email
}
EmailWorker.jobs.clear
time = Time.now
Timecop.freeze(time) do
post '/admin/email', {
:csrf_token => 'abcd',
:subject => 'Subject Test',
:body => 'Body Test'}, {
'rack.session' => { 'id' => @admin_site.id, '_csrf_token' => 'abcd' }
}
relevant_jobs = EmailWorker.jobs.select{|j| relevant_emails.include?(j['args'].first['to']) }
relevant_jobs.length.must_equal sites_emailed_count
relevant_jobs.each do |job|
args = job['args'].first
args['from'].must_equal 'Kyle from Neocities <kyle@neocities.org>'
args['subject'].must_equal 'Subject Test'
args['body'].must_equal 'Body Test'
end
relevant_jobs.select {|j| j['at'].nil? || j['at'] == Time.now.to_f}.length.must_equal 1
relevant_jobs.select {|j| j['at'] == (Time.now + 0.5).to_f}.length.must_equal 1
relevant_jobs.select {|j| j['at'] == (time+1.day.to_i).to_f}.length.must_equal 1
relevant_jobs.select {|j| j['at'] == (time+1.day.to_i+0.5).to_f}.length.must_equal 1
end
end
end

View file

@ -89,7 +89,7 @@ describe 'api delete' do
it 'succeeds with weird filenames' do
create_site
basic_authorize @user, @pass
@site.store_file 't$st.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
@site.store_files [{filename: 't$st.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
post '/api/delete', filenames: ['t$st.jpg']
res[:result].must_equal 'success'
@ -102,16 +102,37 @@ describe 'api delete' do
it 'fails with missing files' do
create_site
basic_authorize @user, @pass
@site.store_file 'test.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
@site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
post '/api/delete', filenames: ['doesntexist.jpg']
res[:error_type].must_equal 'missing_files'
end
it 'fails to delete site directory' do
create_site
basic_authorize @user, @pass
post '/api/delete', filenames: ['/']
res[:error_type].must_equal 'cannot_delete_site_directory'
File.exist?(@site.files_path).must_equal true
end
it 'fails to delete other directories' do
create_site
@other_site = @site
create_site
basic_authorize @user, @pass
post '/api/delete', filenames: ["../#{@other_site.username}"]
File.exist?(@other_site.base_files_path).must_equal true
res[:error_type].must_equal 'missing_files'
post '/api/delete', filenames: ["../#{@other_site.username}/index.html"]
File.exist?(@other_site.base_files_path+'/index.html').must_equal true
res[:error_type].must_equal 'missing_files'
end
it 'succeeds with valid filenames' do
create_site
basic_authorize @user, @pass
@site.store_file 'test.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
@site.store_file 'test2.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
@site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
@site.store_files [{filename: 'test2.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
post '/api/delete', filenames: ['test.jpg', 'test2.jpg']
res[:result].must_equal 'success'
site_file_exists?('test.jpg').must_equal false
@ -139,6 +160,19 @@ describe 'api upload' do
res[:error_type].must_equal 'missing_files'
end
it 'fails with too many files' do
create_site
basic_authorize @user, @pass
@site.plan_feature(:maximum_site_files).times {
uuid = SecureRandom.uuid.gsub('-', '')+'.html'
@site.add_site_file path: uuid
}
post '/api/upload', {
'/lol.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
}
res[:error_type].must_equal 'too_many_files'
end
it 'resists directory traversal attack' do
create_site
basic_authorize @user, @pass
@ -167,14 +201,6 @@ describe 'api upload' do
'/' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
}
res[:error_type].must_equal 'invalid_file_type'
create_site
basic_authorize @user, @pass
post '/api/upload', {
'' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
}
res[:error_type].must_equal 'missing_files'
end
it 'fails for file with no extension' do

View file

@ -50,4 +50,4 @@ I18n.enforce_available_locales = true
Mail.defaults do
delivery_method :test
end
end

View file

@ -18,21 +18,25 @@ describe 'site_files' do
before do
@site = Fabricate :site
ThumbnailWorker.jobs.clear
PurgeCacheOrderWorker.jobs.clear
PurgeCacheWorker.jobs.clear
ScreenshotWorker.jobs.clear
end
describe 'delete' do
it 'works' do
upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
upload 'files[]' => uploaded_file
@site.reload.space_used.must_equal uploaded_file.size
file_path = @site.files_path 'test.jpg'
File.exists?(file_path).must_equal true
delete_file filename: 'test.jpg'
File.exists?(file_path).must_equal false
SiteFile[site_id: @site.id, path: 'test.jpg'].must_be_nil
@site.reload.space_used.must_equal 0
end
it 'deletes all files in a directory' do
it 'deletes a directory and all files in it' do
upload(
'dir' => 'test',
'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
@ -45,6 +49,21 @@ describe 'site_files' do
@site.site_files.select {|f| f.path =~ /^test\//}.length.must_equal 0
@site.site_files.select {|f| f.path =~ /^test/}.length.must_equal 1
end
it 'goes back to deleting directory' do
upload(
'dir' => 'test',
'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
)
delete_file filename: 'test/test.jpg'
last_response.headers['Location'].must_equal "http://example.org/dashboard?dir=test"
upload(
'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
)
delete_file filename: 'test.jpg'
last_response.headers['Location'].must_equal "http://example.org/dashboard"
end
end
describe 'upload' do
@ -82,10 +101,31 @@ describe 'site_files' do
args = ScreenshotWorker.jobs.first['args']
args.first.must_equal @site.username
args.last.must_equal 'index.html'
@site.title.must_equal "#{@site.username}.neocities.org"
@site.title.must_equal "The web site of #{@site.username}"
@site.reload
@site.site_changed.must_equal true
@site.title.must_equal 'Hello?'
# Purge cache needs to flush / and index.html for either scenario.
PurgeCacheOrderWorker.jobs.length.must_equal 2
first_purge = PurgeCacheOrderWorker.jobs.first
dirname_purge = PurgeCacheOrderWorker.jobs.last
username, pathname = first_purge['args']
username.must_equal @site.username
pathname.must_equal '/index.html'
username, pathame = nil
username, pathname = dirname_purge['args']
username.must_equal @site.username
pathname.must_equal '/'
end
it 'provides the correct space used after overwriting an existing file' do
uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
upload 'files[]' => uploaded_file
second_uploaded_file = Rack::Test::UploadedFile.new('./tests/files/img/test.jpg', 'image/jpeg')
upload 'files[]' => second_uploaded_file
@site.reload.space_used.must_equal second_uploaded_file.size
end
it 'does not change title for subdir index.html' do
@ -97,15 +137,19 @@ describe 'site_files' do
@site.reload.title.must_equal title
end
it 'succeeds with valid file' do
upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
upload 'files[]' => uploaded_file
last_response.body.must_match /successfully uploaded/i
File.exists?(@site.files_path('test.jpg')).must_equal true
queue_args = PurgeCacheWorker.jobs.first['args'].first
queue_args['site'].must_equal @site.username
queue_args['path'].must_equal '/test.jpg'
username, path = PurgeCacheOrderWorker.jobs.first['args']
username.must_equal @site.username
path.must_equal '/test.jpg'
@site.reload
@site.space_used.wont_equal 0
@site.space_used.must_equal uploaded_file.size
ThumbnailWorker.jobs.length.must_equal 1
ThumbnailWorker.drain
@ -114,7 +158,7 @@ describe 'site_files' do
File.exists?(@site.thumbnail_path('test.jpg', resolution)).must_equal true
end
@site.site_changed.must_equal false
@site.site_changed.must_equal true
end
it 'fails with unsupported file' do
@ -153,9 +197,18 @@ describe 'site_files' do
last_response.body.must_match /successfully uploaded/i
File.exists?(@site.files_path('derpie/derptest/test.jpg')).must_equal true
PurgeCacheWorker.jobs.length.must_equal 1
queue_args = PurgeCacheWorker.jobs.first['args'].first
queue_args['path'].must_equal '/derpie/derptest/test.jpg'
PurgeCacheOrderWorker.jobs.length.must_equal 1
username, path = PurgeCacheOrderWorker.jobs.first['args']
path.must_equal '/derpie/derptest/test.jpg'
PurgeCacheOrderWorker.drain
PurgeCacheWorker.jobs.length.must_equal 2
ip, username, path = PurgeCacheWorker.jobs.first['args']
ip.must_equal '10.0.0.1'
username.must_equal @site.username
path.must_equal '/derpie/derptest/test.jpg'
PurgeCacheWorker.jobs.last['args'].first.must_equal '10.0.0.2'
ThumbnailWorker.jobs.length.must_equal 1
ThumbnailWorker.drain

View file

@ -53,10 +53,12 @@ describe Site do
end
it 'should match plan_type' do
%w{supporter neko catbus fatcat}.each do |plan_type|
%w{supporter free}.each do |plan_type|
site = Fabricate :site, plan_type: plan_type
site.plan_type.must_equal plan_type
end
site = Fabricate :site, plan_type: nil
site.plan_type.must_equal 'free'
end
end
@ -77,4 +79,4 @@ describe Site do
site.suggestions.length.must_equal Site::SUGGESTIONS_LIMIT
end
end
end
end

153
tests/stat_tests.rb Normal file
View file

@ -0,0 +1,153 @@
require_relative './environment.rb'
STAT_LOGS_PATH = 'tests/stat_logs'
STAT_LOGS_DIR_MATCH = "#{STAT_LOGS_PATH}/*.log"
describe 'stats' do
before do
Dir[STAT_LOGS_DIR_MATCH].each {|f| FileUtils.rm f}
@site_one = Fabricate :site
@site_two = Fabricate :site
@time = Time.now
@time_iso8601 = @time.iso8601
log = [
"#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t67.180.75.140\thttp://example.com",
"#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t67.180.75.140\thttp://example.com",
"#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t172.56.16.152\thttp://example.com",
"#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t172.56.16.152\t-",
"#{@time_iso8601}\t#{@site_two.username}\t5000\t/\t67.180.75.140\thttp://example.com",
"#{@time_iso8601}\t#{@site_two.username}\t5000\t/\t127.0.0.1\t-",
"#{@time_iso8601}\t#{@site_two.username}\t5000\t/derp.html\t127.0.0.2\thttps://example.com"
]
File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file|
file.write log.join("\n")
end
end
it 'deals with spaces in paths' do
@site = Fabricate :site
File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file|
file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com\n"
file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com\n"
end
Stat.parse_logfiles STAT_LOGS_PATH
@site.stats.first.bandwidth.must_equal 612917*2
#@site.stat_referrers.first.url.must_equal 'http://derp.com'
#@site.stat_locations.first.city_name.must_equal 'Menlo Park'
end
it 'deals with spaces in referrer' do
@site = Fabricate :site
File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file|
file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com?q=what the lump\n"
end
Stat.parse_logfiles STAT_LOGS_PATH
end
it 'prunes logs for free sites' do
@free_site = Fabricate :site
@supporter_site = Fabricate :site, plan_type: 'supporter'
day = Date.today
(Stat::FREE_RETAINMENT_DAYS+1).times do |i|
[@free_site, @supporter_site].each do |site|
Stat.create site_id: site.id, created_at: day
end
day = day - 1
end
count_site_ids = [@free_site.id, @supporter_site.id]
expected_stat_count = (Stat::FREE_RETAINMENT_DAYS+1)*2
Stat.where(site_id: count_site_ids).count.must_equal expected_stat_count
Stat.prune!
Stat.where(site_id: count_site_ids).count.must_equal expected_stat_count-1
Stat.where(site_id: @supporter_site.id).count.must_equal expected_stat_count/2
end
it 'prunes referrers' do
stat_referrer_now = @site_one.add_stat_referrer created_at: Date.today, url: 'http://example.com/now'
stat_referrer = @site_one.add_stat_referrer created_at: (StatReferrer::RETAINMENT_DAYS-1).days.ago, url: 'http://example.com'
StatReferrer[stat_referrer.id].wont_be_nil
@site_one.stat_referrers_dataset.count.must_equal 2
StatReferrer.prune!
@site_one.stat_referrers_dataset.count.must_equal 1
StatReferrer[stat_referrer.id].must_be_nil
end
it 'prunes locations' do
stat_location = @site_one.add_stat_location(
created_at: (StatLocation::RETAINMENT_DAYS-1).days.ago,
country_code2: 'US',
region_name: 'Minnesota',
city_name: 'Minneapolis'
)
StatLocation[stat_location.id].wont_be_nil
StatLocation.prune!
StatLocation[stat_location.id].must_be_nil
end
it 'prunes paths' do
stat_path = @site_one.add_stat_path(
created_at: (StatPath::RETAINMENT_DAYS-1).days.ago,
name: '/derpie.html'
)
StatPath[stat_path.id].wont_be_nil
StatPath.prune!
StatPath[stat_path.id].must_be_nil
end
it 'parses logfile' do
Stat.parse_logfiles STAT_LOGS_PATH
@site_one.reload
@site_one.hits.must_equal 4
@site_one.views.must_equal 2
stat = @site_one.stats.first
stat.hits.must_equal 4
stat.views.must_equal 2
stat.bandwidth.must_equal 20_000
#@site_one.stat_referrers.count.must_equal 1
#stat_referrer = @site_one.stat_referrers.first
#stat_referrer.url.must_equal 'http://example.com'
#stat_referrer.created_at.must_equal @time.to_date
#stat_referrer.views.must_equal 2
#@site_one.stat_paths.length.must_equal 1
#stat_path = @site_one.stat_paths.first
#stat_path.name.must_equal '/'
#stat_path.views.must_equal 4
#@site_one.stat_locations.length.must_equal 2
#stat_location = @site_one.stat_locations.first
#stat_location.country_code2.must_equal 'US'
#stat_location.region_name.must_equal 'CA'
#stat_location.city_name.must_equal 'Menlo Park'
#stat_location.views.must_equal 1
@site_two.reload
@site_two.hits.must_equal 3
@site_two.views.must_equal 3
stat = @site_two.stats.first
stat.hits.must_equal 3
stat.views.must_equal 3
stat.bandwidth.must_equal 15_000
#@site_two.stat_referrers.count.must_equal 2
#stat_referrer = @site_two.stat_referrers.first
#stat_referrer.url.must_equal 'http://example.com'
#stat_referrer.views.must_equal 2
#stat_paths = @site_two.stat_paths
#stat_paths.length.must_equal 2
#stat_paths.first.name.must_equal '/'
#stat_paths.last.name.must_equal '/derp.html'
# [geoip.city('67.180.75.140'), geoip.city('172.56.16.152')]
end
end

View file

@ -0,0 +1,28 @@
require_relative '../environment.rb'
describe ArchiveWorker do
it 'stores an IPFS archive' do
return if ENV['TRAVIS']
site = Fabricate :site
ipfs_hash = site.add_to_ipfs
ArchiveWorker.new.perform site.id
site.archives.length.must_equal 1
archive_one = site.archives.first
archive_one.ipfs_hash.must_equal ipfs_hash
archive_one.updated_at.wont_be_nil
new_updated_at = Time.now - 500
archive_one.update updated_at: new_updated_at
ArchiveWorker.new.perform site.id
archive_one.reload.updated_at.wont_equal new_updated_at
site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
ArchiveWorker.new.perform site.id
site.reload
site.archives.length.must_equal 2
archive_two = site.archives_dataset.exclude(ipfs_hash: archive_one.ipfs_hash).first
archive_two.ipfs_hash.wont_be_nil
end
end

View file

@ -0,0 +1,21 @@
require_relative '../environment.rb'
describe PurgeCacheWorker do
before do
PurgeCacheOrderWorker.jobs.clear
PurgeCacheWorker.jobs.clear
end
it 'queues up purges' do
PurgeCacheOrderWorker.new.perform('kyledrake', '/test.jpg')
job_one_args = PurgeCacheWorker.jobs.first['args']
job_two_args = PurgeCacheWorker.jobs.last['args']
job_one_args[0].must_equal '10.0.0.1'
job_one_args[1].must_equal 'kyledrake'
job_one_args[2].must_equal '/test.jpg'
job_two_args[0].must_equal '10.0.0.2'
job_two_args[1].must_equal 'kyledrake'
job_two_args[2].must_equal '/test.jpg'
end
end

View file

@ -0,0 +1,64 @@
require_relative '../environment.rb'
describe PurgeCacheWorker do
before do
@test_ip = '10.0.0.1'
end
it 'throws exception without 200 or 404 http status' do
stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg").
with(headers: {'Host' => 'kyledrake.neocities.org'})
.to_return(status: 503)
worker = PurgeCacheWorker.new
proc {
worker.perform @test_ip, 'kyledrake', '/test.jpg'
}.must_raise RestClient::ServiceUnavailable
end
it 'handles 404 without exception' do
stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg").
with(headers: {'Host' => 'kyledrake.neocities.org'})
.to_return(status: 404)
worker = PurgeCacheWorker.new
worker.perform @test_ip, 'kyledrake', '/test.jpg'
end
it 'sends a purge request' do
stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg").
with(headers: {'Host' => 'kyledrake.neocities.org'})
.to_return(status: 200)
worker = PurgeCacheWorker.new
worker.perform @test_ip, 'kyledrake', '/test.jpg'
assert_requested :get, "http://#{@test_ip}/:cache/purge/test.jpg"
end
it 'handles spaces correctly' do
stub_request(:get, "http://#{@test_ip}/:cache/purge/te st.jpg").
with(headers: {'Host' => 'kyledrake.neocities.org'})
.to_return(status: 200)
url = Addressable::URI.encode_component(
"http://#{@test_ip}/:cache/purge/te st.jpg",
Addressable::URI::CharacterClasses::QUERY
)
worker = PurgeCacheWorker.new
worker.perform @test_ip, 'kyledrake', '/te st.jpg'
assert_requested :get, url
end
it 'works without forward slash' do
stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg").
with(headers: {'Host' => 'kyledrake.neocities.org'})
.to_return(status: 200)
worker = PurgeCacheWorker.new
worker.perform @test_ip, 'kyledrake', 'test.jpg'
end
end

View file

@ -10,9 +10,11 @@
<ul class="tiny h-Nav">
<li><a href="/about">About</a></li>
<li><a href="/donate">Donate</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/api">API</a></li>
<li><a href="/press">Press</a></li>
<% unless is_education? %>
<li><a href="/blog">Blog</a></li>
<li><a href="/api">API</a></li>
<li><a href="/press">Press</a></li>
<% end %>
<li><a href="/terms" rel="nofollow">Terms</a></li>
<li><a href="/privacy" rel="nofollow">Privacy</a></li>
<li><a href="/contact" rel="nofollow">Contact</a></li>

View file

@ -30,17 +30,19 @@
</span>
</a>
<ul class="dropdown-menu">
<li><a href="/site/<%= current_site.username %>">Profile</a></li>
<li>
<a href="/?activity=mine"><span class="float-Left">Activity</span>
<% if current_site.unseen_notifications_count > 0 %>
<span class="notification-value"><%= current_site.unseen_notifications_count %></span>
<% end %>
</a>
</li>
<li class="divider"></li>
<% unless is_education? %>
<li><a href="/site/<%= current_site.username %>">Profile</a></li>
<li>
<a href="/?activity=mine"><span class="float-Left">Activity</span>
<% if current_site.unseen_notifications_count > 0 %>
<span class="notification-value"><%= current_site.unseen_notifications_count %></span>
<% end %>
</a>
</li>
<li class="divider"></li>
<% end %>
<li><a href="/dashboard">Edit Site</a></li>
<li><a href="//<%= current_site.host %>" target="_blank">View Site</a></li>
<li><a href="<%= current_site.uri %>" target="_blank">View Site</a></li>
<li class="divider"></li>
<li><a href="/settings">Settings</a></li>

View file

@ -1,4 +1,4 @@
<% if request.path == '/' && !signed_in? %>
<% if (request.path == '/' && !signed_in?) || request.path == '/education' %>
<li>
<a href="/">Neocities</a>
</li>
@ -6,12 +6,19 @@
<li>
<a href="/browse">Websites</a>
</li>
<% unless is_education? %>
<li>
<a href="/search">Search</a>
</li>
<li>
<a href="/activity">Activity</a>
</li>
<% end %>
<li>
<a href="/tutorials">Learn</a>
</li>
<% unless is_education? %>
<li>
<a href="/plan">Support Us<i class="fa fa-heart"><i class="fa fa-heart"></i></i></a>
<a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev">Support Our Kickstarter<i class="fa fa-heart"><i class="fa fa-heart"></i></i></a>
</li>
<% end %>

View file

@ -0,0 +1,41 @@
<script src="/js/app.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script>
$('#createSiteForm').on('submit', function(obj) {
$.post('/create_validate_all', $(obj.target).serialize(), function(errors) {
if(errors.length == 0) {
$.post('/create', $('#createSiteForm').serialize(), function(res) {
if($('input[name=is_education]').val() == 'true') {
window.location.href = '/dashboard'
} else {
window.location.href = '/welcome'
}
})
} else {
for(var i=0; i<errors.length;i++) {
if(errors[i][0] == 'captcha') {
var captchaDiv = $('#captcha-input')
captchaDiv.attr('data-original-title', errors[i][1])
captchaDiv.tooltip('show')
} else {
var ele = $('input[name='+errors[i][0]+']')
ele.attr('data-original-title', errors[i][1])
ele.tooltip('show')
}
}
}
})
})
$('input[type=text],input[type=password]').on('change focusout', function(obj) {
$.post('/create_validate', {field: obj.target.name, value: obj.target.value, is_education: $('input[name=is_education]')[0].value, csrf_token: '<%= csrf_token %>'}, function(res) {
if(res.result == 'ok') {
return $(obj.target).tooltip('hide')
}
$(obj.target).attr('data-original-title', res.error)
$(obj.target).tooltip('show')
})
})
</script>

View file

@ -1,5 +1,5 @@
<div id="comment-template" style="display: none">
<form onsubmit="Comment.create({{ eventId }}, '<%= csrf_token %>', this); location.reload(); return false">
<form onsubmit="Comment.create({{ eventId }}, '<%= csrf_token %>', this); return false">
<input name="comment" type="text" autocomplete="off" maxlength="<%= Site::MAX_COMMENT_SIZE %>" style="width: 100%" placeholder="Comment on this...">
<button class="btn-Action">Post</button>
</form>

View file

@ -7,6 +7,12 @@
<div class="content misc-page single-Col txt-Center" style="padding-top: 20px;">
<div class="row">
<div class="col col-100">
<a href="/admin/reports">Site Reports</a>
</div>
</div>
<% if flash.keys.length > 0 %>
<div class="alert alert-error alert-block">
<% flash.keys.each do |key| %>

49
views/admin/email.erb Normal file
View file

@ -0,0 +1,49 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Email Mass Send</h1>
<h2 class="subtitle">Use wisely.</h2>
</div>
</div>
<div class="content misc-page single-Col txt-Center" style="padding-top: 20px;">
<% if flash.keys.length > 0 %>
<div class="alert alert-error alert-block">
<% flash.keys.each do |key| %>
<%== flash[key] %>
<% end %>
</div>
<% end %>
<div class="row">
<div class="col col-100">
<p>
This sends to all emails on Neocities that have not opted out. It trickles it out, 4000/day to prevent spam report issues.
</p>
<form method="POST" action="/admin/email">
<%== csrf_token_input_html %>
<p>
Subject:
</p>
<p>
<input name="subject" type="text" style="width: 600px">
</p>
<p>
Body:
</p>
<p>
<textarea name="body" style="width: 600px" rows="10"></textarea>
</p>
<p>
<input type="submit" value="Send" class="btn">
</p>
</form>
</div>
</div>
</div>

45
views/admin/reports.erb Normal file
View file

@ -0,0 +1,45 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Site Reports</h1>
</div>
</div>
<div class="content" style="background: white">
<form method="POST" action="/admin/report">
<table class="table">
<tr>
<th>Site</th>
<th>Type</th>
<th>Comments</th>
<th>Actions</th>
</tr>
<% @reports.each do |report| %>
<tr>
<td>
<a href="<%= report.site.uri %>"><%= report.site.title %></a>
<br>
<img src="<%= report.site.screenshot_url('/index.html', '540x405') %>">
<br>
Reported <%= report.created_at.ago %>
<% if report.reporting_site %>
by <a href="<%= report.reporting_site.uri %>"><%= report.reporting_site.username %></a>
<% end %>
</td>
<td><%= report.type %></td>
<td><%= report.comments[0...100] %></td>
<td>
<select name="sites[<%= report.site_id %>]">
<option value="">No Action</option>
<option value="ban">Ban Site</option>
<option value="nsfw">Mark NSFW</option>
</select>
</td>
</tr>
<% end %>
</table>
<input type="submit" value="Perform Actions" class="btn">
</form>
</div>

View file

@ -21,31 +21,39 @@
<form id="search_criteria" action="/browse" method="GET">
<div class="col col-50 filter">
<fieldset class="grouping">
<label class="text-Label" for="sort_by">Sort by:</label>
<div class="select-Container">
<select name="sort_by" id="sort_by" class="input-Select">
<option value="last_updated" <%= params[:sort_by] == 'last_updated' ? 'selected' : '' %>>Last Updated</option>
<option value="views" <%= params[:sort_by] == 'views' ? 'selected' : '' %>>Most Views</option>
<option value="hits" <%= params[:sort_by] == 'hits' ? 'selected' : '' %>>Most Hits</option>
<option value="newest" <%= params[:sort_by] == 'newest' ? 'selected' : '' %>>Newest</option>
<option value="oldest" <%= params[:sort_by] == 'oldest' ? 'selected' : '' %>>Oldest</option>
<option value="random" <%= params[:sort_by] == 'random' ? 'selected' : '' %>>Random</option>
</select>
</div>
<!--
<div>
<input name="is_nsfw" type="checkbox" value="true" <%= params[:is_nsfw].to_s == 'true' ? 'checked' : '' %>> Show 18+ content
</div>
-->
<% unless is_education? %>
<label class="text-Label" for="sort_by">Sort by:</label>
<div class="select-Container">
<select name="sort_by" id="sort_by" class="input-Select">
<option value="followers" <%= params[:sort_by] == 'followers' ? 'selected' : '' %>>Most Followed</option>
<option value="last_updated" <%= params[:sort_by] == 'last_updated' ? 'selected' : '' %>>Last Updated</option>
<option value="supporters" <%= params[:sort_by] == 'supporters' ? 'selected' : '' %>>Neocities Supporters</option>
<option value="featured" <%= params[:sort_by] == 'featured' ? 'selected' : '' %>>Featured</option>
<option value="views" <%= params[:sort_by] == 'views' ? 'selected' : '' %>>Most Views</option>
<option value="hits" <%= params[:sort_by] == 'hits' ? 'selected' : '' %>>Most Hits</option>
<option value="newest" <%= params[:sort_by] == 'newest' ? 'selected' : '' %>>Newest</option>
<option value="oldest" <%= params[:sort_by] == 'oldest' ? 'selected' : '' %>>Oldest</option>
<option value="random" <%= params[:sort_by] == 'random' ? 'selected' : '' %>>Random</option>
</select>
</div>
<!--
<div>
<input name="is_nsfw" type="checkbox" value="true" <%= params[:is_nsfw].to_s == 'true' ? 'checked' : '' %>> Show 18+ content
</div>
-->
<input class="btn-Action" type="submit" value="Update">
<% end %>
</fieldset>
</div>
<div class="col col-50 filter">
<form method="GET" action="browse">
<fieldset class="grouping">
<label class="text-Label" for="tag"><span class="hide-on-mobile">Search by </span>Tag:</label>
<input class="input-Area typeahead" id="tag" name="tag" type="text" placeholder="pokemon" value="<%= params[:tag] %>" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" dir="auto">
<input style="vertical-align: -4px;margin-left: 4px;" type="submit" class="btn-Action" value="Search">
<% unless is_education? || params[:sort_by] == 'followers' %>
<label class="text-Label" for="tag"><span class="hide-on-mobile">Search by </span>Tag:</label>
<input class="input-Area typeahead" id="tag" name="tag" type="text" placeholder="pokemon" value="<%= params[:tag] %>" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" dir="auto">
<input style="vertical-align: -4px;margin-left: 4px;" type="submit" class="btn-Action" value="Search">
<% end %>
</fieldset>
</div>
</div>
@ -141,12 +149,14 @@
<% end %>
<% end %>
<% unless is_education? %>
<div class="row website-Gallery content int-Gall tags">
<h3>Popular Tags</h3>
<p>
<% Tag.popular_names(100).each do |tag| %>
<a href="/browse?tag=<%= Rack::Utils.escape tag[:name] %>"><%= tag[:name] %></a>&nbsp;&nbsp;
<% end %>
</p>
<h3>Popular Tags</h3>
<p>
<% Tag.popular_names(100).each do |tag| %>
<a href="/browse?tag=<%= Rack::Utils.escape tag[:name] %>"><%= tag[:name] %></a>&nbsp;&nbsp;
<% end %>
</p>
</div>
<% end %>
</div>

View file

@ -18,7 +18,6 @@
<div class="header-Outro with-site-image dashboard">
<div class="row content wide">
<div class="col col-50 signup-Area">
<div class="signup-Form">
<fieldset class="content">
@ -30,7 +29,7 @@
<div class="col col-50">
<h2 class="eps"><%= current_site.title %></h2>
<p class="site-url">
<a href="//<%= current_site.host %>" target="_blank"><%= current_site.host %></a>
<a href="<%= current_site.uri %>" target="_blank"><%= current_site.host %></a>
<a href="#" id="shareButton" class="btn-Action" data-container="body" data-toggle="popover" data-placement="bottom" data-content='<%== erb :'_share', layout: false, locals: {site: current_site} %>'><i class="fa fa-share-alt"></i> <span>Share</span></a>
</p>
<ul>
@ -39,7 +38,7 @@
<% end %>
<li>Using <strong><%= current_site.space_percentage_used %>% (<%= current_site.total_space_used.to_space_pretty %>) of your <%= current_site.maximum_space.to_space_pretty %></strong>.
<br>
<% if !current_site.supporter? %>Need more space? <a href="/plan">Become a Supporter!</a><% end %></li>
<% unless current_site.is_education || current_site.supporter? %>Need more space? <a href="/plan">Become a Supporter!</a><% end %></li>
<li><strong><%= current_site.views.format_large_number %></strong> views</li>
</ul>
</div>
@ -51,7 +50,7 @@
<div class="content wide">
<% unless current_site.changed_count > 5 %>
<% unless current_site.changed_count >= 1 %>
<div class="welcome">
<!-- <div class="close-button"></div> -->
<h4>Hello! Welcome to your new site.</h4>
@ -102,7 +101,7 @@
<% end %>
</div>
<div class="actions">
<a href="/site_files/new_page?dir=<%= Rack::Utils.escape @dir %>" class="btn-Action"><i class="fa fa-file"></i> New Page</a>
<a href="#createFile" class="btn-Action" data-toggle="modal"><i class="fa fa-file"></i> New File</a>
<a href="#createDir" class="btn-Action" data-toggle="modal"><i class="fa fa-folder"></i> New Folder</a>
<a href="#" class="btn-Action" onclick="clickUploadFiles(); return false"><i class="fa fa-arrow-circle-up"></i> Upload</a>
</div>
@ -189,8 +188,10 @@
<% if !current_site.plan_feature(:no_file_restrictions) %>
<a href="/site_files/allowed_types">Allowed file types</a> |
<% end %>
<a href="/site_files/<%= current_site.username %>.zip">Download entire site</a> |
<a href="/site_files/mount_info">Mount your site as a drive on your computer!</a>
<a href="/site_files/<%= current_site.username %>.zip">Download entire site</a>
<% unless is_education? %>
| <a href="/site_files/mount_info">Mount your site as a drive on your computer!</a>
<% end %>
</div>
</div>
@ -203,6 +204,44 @@
<input id="uploadFiles" type="file" name="files[]" multiple onchange="$('#uploadFilesButtonForm').submit()">
</form>
<div class="modal hide fade" id="createDir" tabindex="-1" role="dialog" aria-labelledby="createDirLabel" aria-hidden="true">
<form method="post" action="/site/create_directory">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createDirLabel">Create Folder</h3>
</div>
<div class="modal-body">
<input id="newDirInput" name="name" type="text" placeholder="folder_name">
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button type="submit" class="btn-Action">Create</button>
</div>
</form>
</div>
<div class="modal hide fade" id="createFile" tabindex="-1" role="dialog" aria-labelledby="createFileLabel" aria-hidden="true">
<form method="post" action="/site_files/create">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createFileLabel">Create New File</h3>
</div>
<div class="modal-body">
<input id="newFileInput" name="filename" type="text" placeholder="newfile.html">
<p>Note: We will automatically scrub any characters not matching: a-z A-Z 0-9 _ - .</p>
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button type="submit" class="btn-Action">Create</button>
</div>
</form>
</div>
<script src="/js/dropzone.min.js"></script>
<script>
@ -246,8 +285,12 @@
this.on("error", function(file, errorMessage) {
hideUploadProgress()
location.href = '/dashboard<%= @dir ? "?dir=#{@dir}" : "" %>'
// alert('Failed: '+errorMessage)
// Guess a directory upload error
if(file.status == 'error' && file.name.match(/.+\..+/) == null && errorMessage == 'Server responded with 0 code.') {
alert('Recursive directory upload is only supported by the Chrome web browser.')
} else {
location.href = '/dashboard<%= @dir ? "?dir=#{@dir}" : "" %>'
}
})
this.on("totaluploadprogress", function(progress, totalBytes, totalBytesSent) {
@ -258,24 +301,18 @@
$('#progressBar').css('display', 'block')
$('#uploadingProgress').css('width', progress+'%')
})
this.on("sending", function(file) {
$('#uploads').append('<input type="hidden" name="file_paths[]" value="'+file.fullPath+'">')
})
}
}
</script>
<div class="modal hide fade" id="createDir" tabindex="-1" role="dialog" aria-labelledby="createDirLabel" aria-hidden="true">
<form method="post" action="/site/create_directory">
<input type="hidden" value="<%= csrf_token %>" name="csrf_token">
<input type="hidden" value="<%= @dir %>" name="dir">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="createDirLabel">Create Folder</h3>
</div>
<div class="modal-body">
<input name="name" type="text" placeholder="folder_name">
</div>
<div class="modal-footer">
<button type="button" class="btn cancel" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button type="submit" class="btn-Action">Create</button>
</div>
</form>
</div>
$('#createDir').on('shown', function () {
$('#newDirInput').focus();
})
$('#createFile').on('shown', function () {
$('#newFileInput').focus();
})
</script>

240
views/education.erb Normal file
View file

@ -0,0 +1,240 @@
<body class="hp education">
<a id="new"></a>
<div class="page">
<header class="header-Base" role="banner">
<nav class="header-Nav clearfix" role="navigation">
<a href="#!" title="show small screen nav" class="small-Nav">
<img src="/img/nav-Icon.png" alt="navigation icon" />
</a>
<ul class="h-Nav constant-Nav" role="presentation">
<%== erb :'_header_links', layout: false %>
</ul>
<ul class="status-Nav">
<% if !signed_in? %>
<li>
<a href="/signin" class="sign-In">Sign In</a>
</li>
<% else %>
<li>
<a href="/dashboard" class="sign-In">Dashboard</a>
</li>
<li>
<a href="/settings" class="sign-In">Settings</a>
</li>
<li>
<a href="/signout" class="sign-In">Sign Out</a>
</li>
<% end %>
</ul>
</nav>
<% if flash.keys.length > 0 %>
<div class="alert alert-block txt-Center">
<% flash.keys.each do |key| %>
<%== flash[key] %>
<% end %>
</div>
<% end %>
<div class="int-Logo hp-Logo">
<a href="/" title="back to home">
<span class="hidden">Neocities.org</span>
<img src="/img/cat.png" alt="Neocities.org" />
</a>
</div>
<section class="header-Intro">
<h1 class="logo header-Content content">
<span class="hidden">Neocities for Education</span>
<img src="/img/neocities-logo-education.png" alt="Neocities.org" />
</h1>
</section>
<div class="header-Outro">
<div class="row header-Content content">
<div class="col intro">
<h3 class="delta">A great place to learn how to build websites</h2>
<img src="/img/heartcat.png" class="float-Right">
<p class="intro-text">Completely free and continuously under development, we're building Neocities into a simple and intuitive web hosting service for students learning HTML and CSS for the first time. Give us a try and <a href="/contact">let us know</a> what you think!</p>
<h3 class="delta">Tools for creation and review</h2>
<img src="/img/about-neocities.png" class="float-Left">
<p class="intro-text">Neocities has a great built-in HTML editor and drag-and-drop upload. Instructors can easily review all class websites using our class tag feature. By signing up using this education page, students will get an experience tailored for them, and they'll only see sites from their class in the gallery.</p>
</div>
<div class="col signup-Area">
<% if signed_in? %>
<div class="signup-Form">
<div class="content">
<h3 class="gamma txt-Center">Build your Website!</h3>
</div>
<p class="txt-Center">
Go to your dashboard to<br> start editing your website!
</p>
<br />
<div class="txt-Center">
<a href="/dashboard" class="btn-Action">Get Started</a>
</div>
</div>
<% else %>
<form id="createSiteForm" class="signup-Form" onsubmit="return false">
<input type="hidden" name="csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="is_education" value="true">
<fieldset class="content">
<h2 class="gamma">Class Sign Up</h2>
<hr />
<div class="siteCreateInputs">
<label for="create-Input">User Name</label>
<input type="text" name="prevent_autofill_username" id="prevent_autofill_username" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="password" name="prevent_autofill_password" id="prevent_autofill_password" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="text" class="input-Area" id="create-Input" name="username" placeholder="my-site-name" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<label for="create-Input" id="domain-name">.neocities.org</label>
<label for="tags-input">Class Tag</label>
<input type="text" class="input-Area" id="tags-input" name="new_tags_string" placeholder="E.g. SmithSummer2015" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<div class="col col-50" style="padding-left:0;">
<label for="password-input">
Password
</label>
<input type="password" class="input-Area" id="password-input"
name="password" placeholder="password"
data-placement="left" data-trigger="manual"
autocapitalize="off" autocorrect="off" autocomplete="off" />
</div>
<div class="col col-50">
<label for="email-input">
Email
</label>
<input type="email" class="input-Area"
id="email-input" name="email"
placeholder="me@example.com" data-placement="left"
data-trigger="manual" autocapitalize="off"
autocorrect="off" autocomplete="off" />
</div>
<div class="col col-50" style="padding-left:0;">
<label>
Confirm you are human
</label>
<div id="captcha-input" class="g-recaptcha"
data-sitekey="<%= $config['recaptcha_public_key'] %>"
data-theme="dark" data-placement="left" data-trigger="manual">
</div>
</div>
<div class="col col-50">
<div style="margin-top: 15px">
<input type="submit" value="Create My Site" class="btn-Action float-Right" />
</div>
</div>
</div>
</fieldset>
</form>
<% end %>
</div> <!-- end .col-50 -->
</div> <!-- end .row -->
</div> <!-- end .header-Outro -->
</header>
<main class="content-Base">
<div class="section instructor-quotes">
<h2 class="delta">What Instructors Say</h2>
<div class="row content">
<div class="col col-33">
<div class="image" style="width:130px;height:130px;background:#aaa"></div>
<h3>Instructor Name<br>
Location<br>
Class name</h3>
<p>"Neocities was an excellent resource for my students - it made everything very easy.
The Neocities team did a great job responding to any questions I had."</p>
</div>
<div class="col col-33">
<div class="image" style="width:130px;height:130px;background:#aaa"></div>
<h3>Instructor Name<br>
Location<br>
Class name</h3>
<p>"Neocities was an excellent resource for my students - it made everything very easy.
The Neocities team did a great job responding to any questions I had."</p>
</div>
<div class="col col-33">
<div class="image" style="width:130px;height:130px;background:#aaa"></div>
<h3>Instructor Name<br>
Location<br>
Class name</h3>
<p>"Neocities was an excellent resource for my students - it made everything very easy.
The Neocities team did a great job responding to any questions I had."</p>
</div>
</div>
</div>
<div class="section support">
<h2>Support Us</h2>
<div class="row quote">
<div class="col" style="">
<p>Neocities is funded directly by our community through supporter plans and donations. We will never sell users' personal data or embed advertising on member sites. Your support allows us to pay for server costs and continue working on Neocities full-time. You can support us by making a <a href="/donate">one-time donation</a> or by <a href="/plan">subscribing for $5/month</a>.</p>
</div>
</div>
</div>
</main>
<footer class="footer-Base" role="contentinfo">
<div class="footer-Intro">
<div class="footer-Content">
<div class="row">
<div class="col col-33">
<div class="f-Col f-Col-1 clearfix">
<span class="footer-icon"></span>
<h2 class="delta">Support Us</h2>
<p class="tiny">
Neocities is funded by <a href="/plan">supporters</a> and <a href="/donate">donations</a>. If youd like to contribute, you can help us pay our server costs using credit card, Bitcoin, or PayPal.
</p>
<a href="/donate" title="Donate to Neocities" class="action-Link">Donate Today</a>
</div>
</div>
<div class="col col-33">
<div class="f-Col f-Col-2 clearfix">
<span class="footer-icon"></span>
<h2 class="delta">About Us</h2>
<p class="tiny">
Neocities is here to bring back the creativity and
free expression to the world wide web that made it great.
</p>
<a href="/about" title="More about Neocities" class="action-Link">More About Us</a>
</div>
</div>
<div class="col col-33">
<div class="f-Col f-Col-3 clearfix">
<span class="footer-icon"></span>
<h2 class="delta">Latest News</h2>
<p class="tiny">
The latest news on Neocities can be found on our blog.
</p>
<a href="/blog" title="Read about Neocities news and updates" class="action-Link">Read More</a>
</div>
</div>
</div>
</div>
</div>
<%== erb :'_footer', layout: false %>
</footer>
</div>
<%== erb :'_index_signup_script', layout: false %>
</body>

View file

@ -21,7 +21,18 @@
<div class="content misc-page columns right-col">
<div class="col-left">
<div class="col col-66">
<% if site.followings_dataset.count == 0 %>
<div class="welcome kickstarter">
<h4><a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev">The Neocities Kickstarter: An HTML/CSS course for everyone</a></h4>
<p><strong><%= kickstarter_days_remaining %> days left</strong> to get Neocities rewards and support our education goals! <a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev">Learn More&nbsp;>></a></p>
</div>
<% if site.site_changed == false || site.changed_count == 0 %>
<div class="welcome">
<h4>Thanks for joining the Neocities community!</h4>
<p>Now start <a href="/dashboard">building your website</a>!</a>
</p>
</div>
<% elsif site.followings_dataset.count == 0 %>
<div class="welcome">
<h4>Welcome to your Neocities news feed!</h4>
<p>
@ -32,14 +43,6 @@
</div>
<% end %>
<% if !site.site_changed && site.followings_dataset.count > 0 %>
<div class="welcome">
<h4>Thanks for joining the Neocities community!</h4>
<p>Now start <a href="/dashboard">building your website</a>!</a>
</p>
</div>
<% end %>
<% if !@events.empty? %>
<%== erb :'_news', layout: false, locals: {site: current_site, events: @events} %>
<% end %>
@ -87,7 +90,7 @@
<div class="col col-33">
<div class="news-site-info">
<p class="site-url"><a href="//<%= current_site.host %>" target="_blank"><%= site.title %></a></p>
<p class="site-url"><a href="<%= current_site.uri %>" target="_blank"><%= site.title %></a></p>
<div class="stats">
<div class="col col-50">
<% if site.updated_at %>
@ -104,7 +107,7 @@
</div>
</div>
<a href="//<%= site.host %>" class="large-portrait" style="background-image:url(<%= site.screenshot_url('index.html', '540x405') %>);"></a>
<a href="<%= site.uri %>" class="large-portrait" style="background-image:url(<%= site.screenshot_url('index.html', '540x405') %>);"></a>
<div class="news-profile-button">
<a href="/site/<%= site.username %>" class="btn-Action"><i class="fa fa-user"></i> Profile</a>

View file

@ -1,203 +1,175 @@
<!doctype html>
<!--[if IE 8 ]><html lang="en" class="ieAll ie8"><![endif]-->
<!--[if IE 9 ]><html lang="en" class="ieAll ie9"><![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--><html lang="en"><!--<![endif]-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<body class="hp">
<a id="new"></a>
<title>Neocities: Create your free website now!</title>
<meta itemprop="name" content="Neocities.org" />
<meta itemprop="description" content="Create your own free home page, and do whatever you want with it." />
<meta name="description" content="Neocities is the new Geocities. Create your own free home page, and do whatever you want with it." />
<meta name="keywords" content="free website, html, css, learn to code, free hosting, build a website, create a web page" />
<div class="page">
<link rel="canonical" href="//neocities.org" />
<header class="header-Base" role="banner">
<nav class="header-Nav clearfix" role="navigation">
<a href="#!" title="show small screen nav" class="small-Nav">
<img src="/img/nav-Icon.png" alt="navigation icon" />
</a>
<ul class="h-Nav constant-Nav" role="presentation">
<%== erb :'_header_links', layout: false %>
</ul>
<meta property="og:title" content="Neocities"/>
<meta property="og:site_name" content="Neocities | neocities.org"/>
<meta property="og:type" content="website"/>
<meta property="og:image" content=""/>
<meta property="og:url" content="//www.neocities.org"/>
<meta property="og:description" content="Neocities is the new Geocities. Create your own free home page, and do whatever you want with it."/>
<ul class="status-Nav">
<link rel="shortcut icon" type="image/ico" href="/favicon.ico?v=4" />
<link rel="apple-touch-icon-precomposed" href="#apple-icon-144.png" />
<link rel="apple-touch-startup-image" href="#startup.png" />
<% if !signed_in? %>
<li>
<a href="/signin" class="sign-In">Sign In</a>
</li>
<% else %>
<li>
<a href="/dashboard" class="sign-In">Dashboard</a>
</li>
<li>
<a href="/settings" class="sign-In">Settings</a>
</li>
<li>
<a href="/signout" class="sign-In">Sign Out</a>
</li>
<% end %>
</ul>
</nav>
<!-- Mobile Meta -->
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1" />
<link href="/css/neo.css" rel="stylesheet" type="text/css" media="all"/>
<!--[if lt IE 9]>
<script type="text/javascript" src="/js/html5.min.js"></script>
<![endif]-->
<script src="/js/jquery-1.11.0.min.js"></script>
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body class="hp">
<a id="new"></a>
<div class="page">
<header class="header-Base" role="banner">
<nav class="header-Nav clearfix" role="navigation">
<a href="#!" title="show small screen nav" class="small-Nav">
<img src="/img/nav-Icon.png" alt="navigation icon" />
</a>
<ul class="h-Nav constant-Nav" role="presentation">
<%== erb :'_header_links', layout: false %>
</ul>
<ul class="status-Nav">
<% if !signed_in? %>
<li>
<a href="/signin" class="sign-In">Sign In</a>
</li>
<% else %>
<li>
<a href="/dashboard" class="sign-In">Dashboard</a>
</li>
<li>
<a href="/settings" class="sign-In">Settings</a>
</li>
<li>
<a href="/signout" class="sign-In">Sign Out</a>
</li>
<% end %>
</ul>
</nav>
<% if flash.keys.length > 0 %>
<div class="alert txt-Center">
<p style="padding:5px">
<% flash.keys.each do |key| %>
<%== flash[key] %>
<% end %>
</p>
</div>
<% end %>
<% if flash.keys.length > 0 %>
<div class="alert alert-block txt-Center">
<% flash.keys.each do |key| %>
<%== flash[key] %>
<% end %>
</div>
<% end %>
<div class="int-Logo hp-Logo">
<a href="/" title="back to home">
<span class="hidden">Neocities.org</span>
<img src="/img/cat.png" alt="Neocities.org" />
</a>
<a href="/" title="back to home">
<span class="hidden">Neocities.org</span>
<img src="/img/cat.png" alt="Neocities.org" />
</a>
</div>
<section class="header-Intro">
<h1 class="logo header-Content content">
<span class="hidden">Neocities.org</span>
<img src="/img/neocities-Logo.png" alt="Neocities.org" />
</h1>
<h1 class="logo header-Content content">
<span class="hidden">Neocities.org</span>
<img src="/img/neocities-Logo.png" alt="Neocities.org" />
</h1>
</section>
<div class="header-Outro">
<div class="row header-Content content">
<div class="col intro">
<h2 class="section-header">Create your own free web site, and discover new ones.</h2>
<p class="intro-text">
Neocities is a community of <a href="/browse"><%= @sites_count.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse %> sites</a> that are bringing back the lost individual creativity of the web. We provide free hosting and tools that allow anyone to make a web site. Only your imagination is required. Join us!
</p>
<ul class="intro-List">
<li class="intro-Social">
<span class="intro-Icon"></span>
<h3 class="delta">Share your web creation with the world</h3>
<p class="base">
Follow your favorite Neocities sites to keep up with all their latest updates. Discover new websites related to your interests using tags, comment on them, and share them. Unlimited creativity, zero ads.
</p>
</li>
<li class="intro-Tools">
<span class="intro-Icon"></span>
<h3 class="delta">Powerful new features to help you build</h3>
<p class="base">
Weve made it easier to build your website and explore other sites. Neocities features an in-browser HTML editor, custom domain support, faster site loading, easy file uploading, RSS feeds, folder support, and much more.
</li>
</ul>
</div>
<div class="col signup-Area">
<% if signed_in? %>
<div class="signup-Form">
<div class="content">
<h3 class="gamma txt-Center">Build your Website!</h3>
</div>
<p class="txt-Center">
Go to your dashboard to<br> start editing your website!
</p>
<br />
<div class="txt-Center">
<a href="/dashboard" class="btn-Action">Get Started</a>
</div>
<div class="row header-Content content">
<div class="col intro">
<h2 class="section-header">Create your own free web site. Unlimited creativity, zero ads.</h2>
<p class="intro-text">
Neocities is a community of <a href="/browse"><%= @sites_count.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse %> sites</a> that are bringing back the lost individual creativity of the web. We provide free hosting and tools that allow anyone to make a web site. Only your imagination is required. Join us!
</p>
<div class="intro-List kickstarter">
<div class="col col-40">
<a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev"><img src="/img/kickstarterthumbnail.jpg" style="width: 100%; float: left; margin-right: 20px; border: 3px solid white;margin-bottom: 1em"></a>
</div>
<div class="col col-60">
<div class="title">
<a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev">
SUPPORT OUR
<img src="/img/kickstarterlogo.png" style="width: 280px; display:block; margin: 11px 0px;">
<%= kickstarter_days_remaining %> DAYS LEFT
</a>
</div>
<% else %>
<form id="createSiteForm" class="signup-Form" onsubmit="return false">
<input type="hidden" name="csrf_token" value="<%= csrf_token %>">
<fieldset class="content">
<h2 class="gamma">Sign up for free</h2>
<hr />
<div class="siteCreateInputs">
<label for="create-Input">User Name</label>
<input type="text" name="prevent_autofill_username" id="prevent_autofill_username" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="password" name="prevent_autofill_password" id="prevent_autofill_password" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="text" class="input-Area" id="create-Input" name="username" placeholder="my-site-name" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<label for="create-Input" id="domain-name">.neocities.org</label>
<p>We need your help to build an amazing HTML/CSS course into Neocities! <a href="https://www.kickstarter.com/projects/1262953102/neocities-30-an-interactive-html-css-course-for-ev">Learn More&nbsp;>></a></p>
</div>
</div>
<!--<ul class="intro-List">
<li class="intro-Social">
<span class="intro-Icon"></span>
<h3 class="delta">Share your web creation with the world</h3>
<p class="base">
Follow your favorite Neocities sites to keep up with all their latest updates. Discover new websites related to your interests using tags, comment on them, and share them.
</p>
</li>
<li class="intro-Tools">
<span class="intro-Icon"></span>
<h3 class="delta">Powerful new features to help you build</h3>
<p class="base">
Weve made it easier to build your website and explore other sites. Neocities features an in-browser HTML editor, custom domain support, faster site loading, easy file uploading, RSS feeds, folder support, and much more.
</li>
</ul>-->
</div>
<label for="tags-input">Tags (your interests, site topics)</label>
<input type="text" class="input-Area" id="tags-input" name="new_tags_string" placeholder="art, videogames, food, music, programming, gardening, cats" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<div class="col signup-Area">
<% if signed_in? %>
<div class="col col-50" style="padding-left:0;">
<label for="password-input">
Password
</label>
<input type="password" class="input-Area" id="password-input"
name="password" placeholder="password"
data-placement="left" data-trigger="manual"
autocapitalize="off" autocorrect="off" autocomplete="off" />
</div>
<div class="signup-Form">
<div class="content">
<h3 class="gamma txt-Center">Build your Website!</h3>
</div>
<p class="txt-Center">
Go to your dashboard to<br> start editing your website!
</p>
<br />
<div class="txt-Center">
<a href="/dashboard" class="btn-Action">Get Started</a>
</div>
</div>
<% else %>
<form id="createSiteForm" class="signup-Form" onsubmit="return false">
<input type="hidden" name="csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="is_education" value="false">
<fieldset class="content">
<h2 class="gamma">Sign up for free</h2>
<hr />
<div class="siteCreateInputs">
<label for="create-Input">User Name</label>
<input type="text" name="prevent_autofill_username" id="prevent_autofill_username" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="password" name="prevent_autofill_password" id="prevent_autofill_password" value="" style="position:absolute; top:-2000px; left:-2000px;" />
<input type="text" class="input-Area" id="create-Input" name="username" placeholder="my-site-name" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<label for="create-Input" id="domain-name">.neocities.org</label>
<div class="col col-50">
<label for="email-input">
Email
</label>
<input type="email" class="input-Area"
id="email-input" name="email"
placeholder="me@example.com" data-placement="left"
data-trigger="manual" autocapitalize="off"
autocorrect="off" autocomplete="off" />
</div>
<label for="tags-input">Tags (your interests, site topics)</label>
<input type="text" class="input-Area" id="tags-input" name="new_tags_string" placeholder="art, videogames, food, music, programming, gardening, cats" data-placement="left" data-trigger="manual" autocapitalize="off" autocorrect="off" autocomplete="off" />
<div class="col col-50" style="padding-left:0;">
<label>
Confirm you are human
</label>
<div id="captcha-input" class="g-recaptcha"
data-sitekey="<%= $config['recaptcha_public_key'] %>"
data-theme="dark" data-placement="left" data-trigger="manual">
</div>
</div>
<div class="col col-50" style="padding-left:0;">
<label for="password-input">
Password
</label>
<input type="password" class="input-Area" id="password-input"
name="password" placeholder="password"
data-placement="left" data-trigger="manual"
autocapitalize="off" autocorrect="off" autocomplete="off" />
</div>
<div class="col col-50">
<div style="margin-top: 15px">
<input type="submit" value="Create My Site" class="btn-Action float-Right" />
</div>
<div class="col col-50">
<label for="email-input">
Email
</label>
<input type="email" class="input-Area"
id="email-input" name="email"
placeholder="me@example.com" data-placement="left"
data-trigger="manual" autocapitalize="off"
autocorrect="off" autocomplete="off" />
</div>
<div class="col col-50" style="padding-left:0;">
<label>
Confirm you are human
</label>
<div id="captcha-input" class="g-recaptcha"
data-sitekey="<%= $config['recaptcha_public_key'] %>"
data-theme="dark" data-placement="left" data-trigger="manual">
</div>
</div>
</fieldset>
</form>
<% end %>
<div class="col col-50">
<div style="margin-top: 15px">
<input type="submit" value="Create My Site" class="btn-Action float-Right" />
</div>
</div>
</div>
</fieldset>
</form>
</div> <!-- end .col-50 -->
<% end %>
</div> <!-- end .row -->
</div> <!-- end .col-50 -->
</div> <!-- end .row -->
</div> <!-- end .header-Outro -->
@ -206,118 +178,118 @@
<main class="content-Base">
<div class="section featured-Websites">
<h2 class="delta">Featured Sites</h2>
<!--
<div class="nav prev"></div>
-->
<ul class="website-Gallery hp-Gallery">
<% Site.featured.each do |site| %>
<li>
<a href="<%= site.uri %>" title="<%= site.title %>" target="_blank">
<img src="<%= site.screenshot_url 'index.html', '210x158' %>" class="neo-SS" alt="<%= site.title %>" />
</a>
</li>
<% end %>
</ul>
<!--
<div class="nav next"></div>
-->
<a href="/browse" class="btn-Action float-Right">Browse all sites</a>
<h2 class="delta">Featured Sites</h2>
<!--
<div class="nav prev"></div>
-->
<ul class="website-Gallery hp-Gallery">
<% Site.featured.each do |site| %>
<li>
<a href="<%= site.uri %>" title="<%= site.title %>" target="_blank">
<img src="<%= site.screenshot_url 'index.html', '210x158' %>" class="neo-SS" alt="<%= site.title %>" />
</a>
</li>
<% end %>
</ul>
<!--
<div class="nav next"></div>
-->
<a href="/browse" class="btn-Action float-Right">Browse all sites</a>
</div>
<div class="section previews">
<h2 class="delta">Our mission: To make the web fun again by giving you back control of how you express yourself online.</h2>
<h2 class="delta">Our mission: To make the web fun again by giving you back control of how you express yourself online.</h2>
<div class="row content">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-editor-screenshot.png)"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-edit"></i> HTML editor, right in your browser</h3>
<p>No tools needed! With our easy-to-use HTML editor, you're ready to start building your awesome web site right now.</p>
<p>If you'd rather use your favorite desktop editor, no problem! Uploading files is as easy as drag-and-drop. We also support WebDAV uploading for <a href="/plan">supporter accounts</a>.</p>
</div>
</div>
<div class="row content right">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-browse-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-globe"></i> It's time to bring back web surfing!</h3>
<p>We collect all Neocities sites in our <a href="/browse">website gallery</a>. We make it easy to browse sites with our optional surf bar.</p>
<p>Using tags, our version of web rings, you can easily discover new sites related to your interests.</p>
</div>
</div>
<div class="row content">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-follow-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-user-plus"></i> Follow your favorite Neocities sites</h3>
<p>Once you find some interesting sites in our website gallery, you can keep track of all new updates by following them. Any changes to the sites show up in your news feed. You'll also see what sites are followed by your favorite site builders.</p>
</div>
</div>
<div class="row content right">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-comment-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-comments-o"></i> Web creativity plus community</h3>
<p>Interact with your favorite web builders by posting comments, liking updates, and sharing their sites on your social network of choice!</p>
</div>
<div class="row content">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-editor-screenshot.png)"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-edit"></i> HTML editor, right in your browser</h3>
<p>No tools needed. With our easy-to-use HTML editor, you're ready to start building your awesome web site right now.</p>
<p>If you'd rather use your favorite desktop editor, no problem. Uploading files is as easy as drag-n-drop.</p>
</div>
</div>
<section class="section features">
<div class="row">
<div class="col col-50">
<div class="row content right">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-browse-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-globe"></i> It's time to bring back web surfing.</h3>
<p>All Neocities sites are viewable in our <a href="/browse">website gallery</a>. And it's easy to browse sites with our optional surf bar.</p>
<p>Using tags (our version of Web Rings) you can easily discover new sites related to your interests.</p>
</div>
</div>
<div class="row content">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-follow-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-user-plus"></i> Follow your favorite Neocities sites</h3>
<p>Keep track of all your favorite sites by following them. Any changes to the sites automatically show up in your news feed. You'll also see what sites they follow.</p>
</div>
</div>
<div class="row content right">
<div class="col col-50"><div class="screenshot" style="background-image:url(/img/front-comment-screenshot.png);"></div></div>
<div class="col col-50 text">
<h3><i class="fa fa-comments-o"></i> Web creativity plus community</h3>
<p>Interact with your favorite web builders by posting comments, and sharing their sites on your social network of choice.</p>
</div>
</div>
</div>
<section class="section features">
<div class="row">
<div class="col col-50">
<h3>
<i class="fa fa-eye-slash"></i>Zero advertising
</h3>
<p>
Neocities will never sell your personal data or embed advertising on your site. Instead, we are funded directly by our community through <a href="/plan">supporter plans</a> and <a href="/donate">donations</a>. This allows us to base all our decisions on making the best possible web building experience for you, rather than on appeasing ad companies.
</p>
</div>
<div class="col col-50">
<h3><i class="fa fa-tachometer"></i>More space, speed, and security</h3>
<p>Neocities now uses distributed, globally-cached web servers in datacenters all over the world to serve your site. Whether its your personal home page or a busy professional site, your site loads fast. And if you need more space, <a href="/plan">we've got you covered</a>. We also provide Snowden-grade SSL cryptography on all sites, preventing snoops from seeing what you browse.</p>
</div>
</div>
<div class="row">
<div class="col col-50">
<h3><i class="fa fa-wrench"></i>Developer tools</h3>
<p>We have an in-browser HTML editor, easy file uploading, WebDAV publishing, support for custom domains, RSS feeds for every site, powerful APIs for building developer applications, and much more!</p>
</div>
<div class="col col-50">
<h3><i class="fa fa-university"></i>Open Company</h3>
<p>Neocities is a member of the <a href="http://www.opencompany.org/" target="_blank">Open Company Initative</a>, working to help improve trustability in tech companies. We <a href="https://github.com/neocities" target="_blank">publish</a> the code that powers the site for inspection, and strive for openness in our company's operations. We want to win your trust—not lock you in.</p>
</div>
</div>
<div class="col col-100">
<div class="row press">
<a href="http://www.wired.com/2013/07/neocities/" class="logo wired"></a>
<a href="http://www.fastcodesign.com/3037028/why-the-internet-is-time-traveling-back-to-1994" class="logo fastco"></a>
<a href="http://motherboard.vice.com/blog/neocities-is-recreating-the-garish-web-10-creativity-of-geocities" class="logo vice"></a>
<a href="http://arstechnica.com/tech-policy/2014/05/web-host-gives-fcc-a-28-8kbps-slow-lane-in-net-neutrality-protest/" class="logo ars"></a>
<!--<a href="/press" class="more">See all press »</a>-->
</div>
<div class="row quote">
<h3>
<i class="fa fa-eye-slash"></i>Zero advertising
"Designed as a 21st century reincarnation of GeoCities, Neocities lets you make your own site for free. <strong>And it just might spark a renaissance of creativity online.</strong>"
<br />
<cite>— Matthew Guay, <a href="http://web.appstorm.net/reviews/web-dev/neocities-the-free-place-to-code-your-own-site-from-scratch" target="_blank">AppStorm</a>. <a href="/press">View All Press »</a></cite>
</h3>
<p>
Neocities will never sell your personal data or embed advertising on your site. Instead, we are funded directly by our community through <a href="/plan">supporter plans</a> and <a href="/donate">donations</a>. This allows us to base all our decisions on making the best possible web building experience for you, rather than on appeasing ad companies.
</p>
</div>
<div class="col col-50">
<h3><i class="fa fa-tachometer"></i>More space, speed, and security</h3>
<p>Neocities now uses distributed, globally-cached web servers in datacenters all over the world to serve your site. Whether its your personal home page or a busy professional site, your site loads fast. And if you need more space, <a href="/plan">we've got you covered</a>. We also provide Snowden-grade SSL cryptography on all sites, preventing snoops from seeing what you browse.</p>
</div>
</div>
</div>
</section>
<div class="row">
<div class="col col-50">
<h3><i class="fa fa-wrench"></i>Developer tools</h3>
<p>We have an in-browser HTML editor, easy file uploading, WebDAV publishing, support for custom domains, RSS feeds for every site, powerful APIs for building developer applications, and much more!</p>
</div>
<% # erb :'plan/_pricing' %>
<div class="col col-50">
<h3><i class="fa fa-university"></i>Open Company</h3>
<p>Neocities is a member of the <a href="http://www.opencompany.org/" target="_blank">Open Company Initative</a>, working to help improve trustability in tech companies. We <a href="https://github.com/neocities" target="_blank">publish</a> the code that powers the site for inspection, and strive for openness in our company's operations. We want to win your trust—not lock you in.</p>
</div>
</div>
<div class="col col-100">
<div class="row press">
<a href="http://www.wired.com/2013/07/neocities/" class="logo wired"></a>
<a href="http://www.fastcodesign.com/3037028/why-the-internet-is-time-traveling-back-to-1994" class="logo fastco"></a>
<a href="http://motherboard.vice.com/blog/neocities-is-recreating-the-garish-web-10-creativity-of-geocities" class="logo vice"></a>
<a href="http://arstechnica.com/tech-policy/2014/05/web-host-gives-fcc-a-28-8kbps-slow-lane-in-net-neutrality-protest/" class="logo ars"></a>
<!--<a href="/press" class="more">See all press »</a>-->
</div>
<div class="row quote">
<h3>
"Designed as a 21st century reincarnation of GeoCities, Neocities lets you make your own site for free. <strong>And it just might spark a renaissance of creativity online.</strong>"
<br />
<cite>— Matthew Guay, <a href="http://web.appstorm.net/reviews/web-dev/neocities-the-free-place-to-code-your-own-site-from-scratch" target="_blank">AppStorm</a>. <a href="/press">View All Press »</a></cite>
</h3>
</div>
</div>
</section>
<% # erb :'plan/_pricing' %>
<section class="section bottom-signup">
<h2>What are you waiting for? <a href="#new">Start building your web site now!</a></h2>
</section>
<section class="section bottom-signup">
<h2>What are you waiting for? <a href="#new">Start building your web site now!</a></h2>
</section>
</main>
<footer class="footer-Base" role="contentinfo">
@ -358,47 +330,8 @@
</div>
</div>
</div>
<%== erb :'_footer', layout: false %>
</footer>
</div>
<script src="/js/app.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script>
$('#createSiteForm').on('submit', function(obj) {
$.post('/create_validate_all', $(obj.target).serialize(), function(errors) {
if(errors.length == 0) {
$.post('/create', $('#createSiteForm').serialize(), function(res) {
window.location.href = '/welcome'
})
} else {
for(var i=0; i<errors.length;i++) {
if(errors[i][0] == 'captcha') {
var captchaDiv = $('#captcha-input')
captchaDiv.attr('data-original-title', errors[i][1])
captchaDiv.tooltip('show')
} else {
var ele = $('input[name='+errors[i][0]+']')
ele.attr('data-original-title', errors[i][1])
ele.tooltip('show')
}
}
}
})
})
$('input[type=text],input[type=password]').on('change focusout', function(obj) {
$.post('/create_validate', {field: obj.target.name, value: obj.target.value, csrf_token: '<%= csrf_token %>'}, function(res) {
if(res.result == 'ok') {
return $(obj.target).tooltip('hide')
}
$(obj.target).attr('data-original-title', res.error)
$(obj.target).tooltip('show')
})
})
</script>
</body>
</html>
</div>
<%== erb :'_index_signup_script', layout: false %>
</body>

44
views/index_layout.erb Normal file
View file

@ -0,0 +1,44 @@
<!doctype html>
<!--[if IE 8 ]><html lang="en" class="ieAll ie8"><![endif]-->
<!--[if IE 9 ]><html lang="en" class="ieAll ie9"><![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--><html lang="en"><!--<![endif]-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Neocities: Create your free website now!</title>
<meta itemprop="name" content="Neocities" />
<meta itemprop="description" content="Create your own free web site, and do whatever you want with it." />
<meta name="description" content="Create your own free web site, and do whatever you want with it." />
<meta name="keywords" content="free website, html, css, learn to code, free hosting, build a website, create a web page" />
<link rel="canonical" href="//neocities.org" />
<meta property="og:title" content="Neocities"/>
<meta property="og:site_name" content="Neocities | neocities.org"/>
<meta property="og:type" content="website"/>
<meta property="og:image" content=""/>
<meta property="og:url" content="//www.neocities.org"/>
<meta property="og:description" content="Create your own free web site, and do whatever you want with it."/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico?v=4" />
<link rel="apple-touch-icon-precomposed" href="#apple-icon-144.png" />
<link rel="apple-touch-startup-image" href="#startup.png" />
<!-- Mobile Meta -->
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1" />
<link href="/css/neo.css" rel="stylesheet" type="text/css" media="all"/>
<!--[if lt IE 9]>
<script type="text/javascript" src="/js/html5.min.js"></script>
<![endif]-->
<script src="/js/jquery-1.11.0.min.js"></script>
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<%== yield %>
</html>

35
views/permanent_web.erb Normal file
View file

@ -0,0 +1,35 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Neocities and the Permanent Web</h1>
<h2 class="subtitle">Working to build a faster, better, more permanent web.</h2>
</div>
</div>
<div class="content single-Col misc-page">
<img src="/img/neocities-ipfs.jpg" style="margin-bottom: 20px">
<article role="article">
<p>
Neocities has launched an experimental implementation of <a href="http://ipfs.io">IPFS</a>. IPFS is short for the "Inter-Planetary File System", and is the foundation for a new way to distribute web content that is being called The Permanent Web. The idea behind the Permanent Web is simple: Instead of serving web sites from central servers, we believe that web serving should be decentralized, and that IPFS is an eventual replacement to the aging HTTP protocol.
</p>
<p>
This is still very early stage technology, so if you don't understand what this stuff is, don't worry about it for now. But if you'd like to read more about why we're interested in this new technology, please see our <a href="#">blog post</a> on the topic.
</p>
<p>
IPFS archiving and downloading is now supported by <strong>all web sites</strong> on Neocities. You'll see an IPFS hash link on the site profile, and an archive link that allows you to see past versions of your site (note: this is still a preview, so past site archives may still disappear, but we're working on making it better).
</p>
<p>
If you want to play around with this new technology, you can get IPFS for your computer and use it to retreive content from our IPFS node servers. All you need to do is <a href="http://ipfs.io/docs/install/">download the IPFS daemon</a> (OSX/Linux only for now), and run the following command in your terminal:
</p>
<p>
<code>$ ipfs pin add -r THE_IPFS_HASH_FOR_YOUR_SITE</code>
</p>
<p>
We have a lot of very interesting projects we're working on with IPFS that will make Neocities even better. Stay tuned for some interesting new technology over the next year!
</p>
</article>
</div>

View file

@ -130,7 +130,7 @@
<strong>It's safe.</strong> We use <a href="https://stripe.com" target="_blank">Stripe</a> for payments, and never store your credit card information directly.
</li>
<li>
<strong>It's affordable.</strong> As low as <strong>$<%= Site::PLAN_FEATURES[:supporter][:price] %>/month</strong> (billed once every year). Higher tiers are optional (and appreciated!)
<strong>It's affordable.</strong> Only $<%= Site::PLAN_FEATURES[:supporter][:price] %> per month.
</li>
<li>
<strong>You can cancel or change plans anytime.</strong> If you do, we'll refund or credit the amount you didn't use.

28
views/search.erb Normal file
View file

@ -0,0 +1,28 @@
<div class="header-Outro">
<div class="row content single-Col txt-Center">
<h1>Search Neocities Sites</h1>
<h3 class="subtitle"></h3>
</div>
</div>
<div class="content txt-Center single-Col misc-page">
<form id="searchForm" method="GET" action="https://duckduckgo.com" class="content" onsubmit="return addSiteToSearch()">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<fieldset>
<input id="searchQuery" name="q" type="text" placeholder="keywords" class="input-Area" autocapitalize="off" autocorrect="off" value="<%= flash[:username] %>" style="width: 290px">
</fieldset>
<input class="btn-Action" type="submit" value="Search">
</form>
<p>Search powered by <a href="https://duckduckgo.com">DuckDuckGo</a>, a search engine that cares about your privacy as much as we do.</p>
</div>
<script>
function addSiteToSearch() {
var searchQuery = $('#searchQuery')
var finalSearchQuery = searchQuery.val() + ' site:neocities.org'
window.location = 'https://duckduckgo.com/?q='+encodeURI(finalSearchQuery)
return false
}
</script>

View file

@ -75,4 +75,4 @@ $(document).ready(function() {
return location.hash = $(e.target).attr('href').substr(1);
});
});
</script>
</script>

View file

@ -6,7 +6,7 @@
</p>
<p>
You will have to purchase a domain name from a registrar like <a href="https://www.namecheap.com/?aff=53678" target="_blank">Namecheap</a> first. We are working on providing domain purchasing from Neocities in the future, but in general it is best if you own the domain, because then you control your site.
You will have to purchase a domain name from a registrar like <a href="http://www.namecheap.com/?aff=87835" target="_blank">Namecheap</a> first. We are working on providing domain purchasing from Neocities in the future, but in general it is best if you own the domain, because then you control your site.
</p>
<% if current_site.custom_domain_available? %>
@ -17,7 +17,7 @@
<h3>Step 1</h3>
<p>
First, you need to add an "A record" to point your root domain (catsknitting.com) to the following IP address:
First, you need to add an "A record" to point your root domain (sometimes shown with an @ symbol) (catsknitting.com) to the following IP address:
</p>
<p><code>54.68.34.66</code></p>
@ -81,4 +81,4 @@
</form>
<% end %>
-->
-->

View file

@ -21,6 +21,11 @@
<div class="col col-50 profile-info">
<h2 class="eps title-with-badge"><span><%= site.title %></span> <% if site.supporter? %><a href="/plan" class="supporter-badge" title="Neocities Supporter"></a> <% end %></h2>
<p class="site-url"><a href="<%= site.uri %>"><%= site.host %></a></p>
<!--
<% if site.latest_archive %>
<p><a href="<%= site.latest_archive.url %>" style="margin-right: 5px">IPFS Link</a><small style="font-size: 7pt"><a href="/permanent-web">(what is this?)</a></small></p>
<% end %>
-->
<div class="stats">
<div class="stat"><strong><%= site.views.format_large_number %></strong> <span>view<%= site.views == 1 ? '' : 's' %></span></div>
<% follows_count = site.follows_dataset.count %>
@ -33,6 +38,12 @@
<a href="/dashboard" class="btn-Action edit"><i class="fa fa-edit" title="Edit"></i> Edit Site</a>
<% end %>
<!--
<% if current_site && current_site.id == site.id && site.latest_archive %>
<a href="/site/<%= site.username %>/archives" class="btn-Action edit"><i class="fa fa-history" title="Archives"></i> Archives</a>
<% end %>
-->
<% if current_site && current_site != site %>
<% is_following = current_site.is_following?(site) %>
@ -109,6 +120,7 @@
</strong>
</div>
<div class="stat"><span>Created</span><strong><%= site.created_at.strftime('%b %-d, %Y') %></strong></div>
<a href="/site/<%= site.username %>/stats">Site Traffic Stats</a>
</div>
<%== erb :'_follows', layout: false, locals: {site: site, is_current_site: site == current_site} %>

30
views/site/archives.erb Normal file
View file

@ -0,0 +1,30 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Permanent Web Archives</h1>
</div>
</div>
<div class="content single-Col misc-page">
<article role="article">
<% if @archives.length == 0 %>
No archives yet.
<% else %>
<table class="table">
<tr>
<th>IPFS Hash <small style="display: inline"><a href="/permanent-web">(what is this?)</a></small></th>
<th>Archived Time</th>
</tr>
<% @archives.each do |archive| %>
<tr>
<td><a href="<%= archive.url %>"><%= archive.ipfs_hash %></a></td>
<td><%= archive.updated_at.ago.downcase %></td>
</tr>
<% end %>
</table>
<p>
Note: This is a very early preview release of a new technology! We're still figuring things out. We may stop hosting archives without notice. <a href="/permanent-web">Learn how you can host your own copies of these archives</a>.
</p>
<% end %>
</article>
</div>

342
views/site/stats.erb Normal file
View file

@ -0,0 +1,342 @@
<div class="header-Outro with-columns">
<div class="row content">
<div class="col col-100">
<h3>Site Statistics</h3>
<div class="feed-filter">
<% if !@events.empty? && (site.followings_dataset.count > 0) %>
<a href="/" <% if params[:activity].nil? %>class="selected"<% end %>>All</a>
<a href="/?activity=mine" <% if params[:activity] == 'mine' %>class="selected"<% end %>>Profile Activity</a>
<% end %>
</div>
</div>
</div>
</div>
<div class="container news-feed">
<div class="content misc-page columns right-col">
<div class="col-left">
<div class="col col-66">
<!--
<div class="row">
<div class="col col-100 globe">
<div id="earth_div"></div>
</div>
<!--
<div class="col col-50" style="padding-right: 0;">
<table class="table table-striped" id="latest-visitors">
<tbody>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> neocities.org</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Portland, OR</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">London, UK</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Twitter URL</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Facebook URL</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> neocities.org</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Portland, OR</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">London, UK</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Twitter URL</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a>, <a href="">/tech</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
-->
<h2>Total Visitors <small>last 7 days</small></h2>
<% if current_site && current_site.id == @site.id %>
<% if current_site.supporter? %>
<ul class="nav h-Nav">
<li><a href="?days=30">30 days</a></li>
<li><a href="?days=90">90 days</a></li>
<li><a href="?days=365">1 year</a></li>
<li><a href="?days=sincethebigbang">All time</a></li>
</ul>
<% else %>
<p>(<a href="/plan">Upgrade</a> to see up to see stats for all time)</p>
<% end %>
<% end %>
<canvas id="myChart" style="width:100%;height:300px;display:block"></canvas>
<!--
<div class="row">
<div class="col col-50">
<h2>Top Paths <small>last 7 days</small></h2>
<table class="table table-striped">
<thead>
<tr>
<th>Path</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<% @stats[:paths].each do |path| %>
<tr>
<td><a href="<%= @site.uri+path.name %>" target="_blank"><%= path.name.gsub(/\?.+/i, '') %></a></td>
<td><%= path.views %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="col col-50">
<h2>Top Locations <small>last 7 days</small></h2>
<table class="table table-striped">
<thead>
<tr>
<th>City</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<% @stats[:locations].each do |location| %>
<tr>
<td>
<%= location[:name] %>
</td>
<td><%= location[:views] %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% if current_site && current_site.id == @site.id %>
<div class="row">
<div class="col col-100">
<h2>Top Referrers</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Referrer</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<% @stats[:referrers].each do |referrer| %>
<tr>
<td><%= referrer.url %></td>
<td><%= referrer.views %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
-->
</div>
<div class="col col-33">
<div class="news-site-info">
<p class="site-url"><a href="<%= @site.uri %>" target="_blank"><%= @site.title %></a></p>
<div class="stats">
<div class="col col-50">
<% if site.updated_at %>
Last updated<br><strong><%= site.updated_at.ago %></strong>
<% else %>
Your new website!<br><strong><a href="/dashboard"><i class="fa fa-edit" title="Edit"></i> Start Building</a></strong>
<% end %>
</div>
<div class="col col-50">
<div><strong><%= site.views.format_large_number %></strong> views</div>
<% follows_count = site.follows_dataset.count %>
<div><strong><%= follows_count.format_large_number %></strong> follower<%= follows_count == 1 ? '' : 's' %></div>
</div>
</div>
</div>
<a href="<%= site.uri %>" class="large-portrait" style="background-image:url(<%= site.screenshot_url('index.html', '540x405') %>);"></a>
<div class="news-profile-button">
<a href="/site/<%= site.username %>" class="btn-Action"><i class="fa fa-user"></i> Profile</a>
</div>
</div>
</div>
</div>
</div>
<!-- <script src="//www.webglearth.com/v2/api.js"></script> -->
<script src="/js/Chart.min.js"></script>
<script>
//OpenGL globe
$(document).ready(function() {
/*
var options = {
sky: true,
atmosphere: false,
dragging: true,
tilting: true,
center: [46.8011, 8.2266],
zoom: 2
}
var earth = new WE.map('earth_div', options)
earth.setView([20, -100], 2.07)
// WE.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
// attribution: '© OpenStreetMap'
//}).addTo(earth);
WE.tileLayer('http://data.webglearth.com/natural-earth-color/{z}/{x}/{y}.jpg', {
tileSize: 256,
bounds: [[-85, -180], [85, 180]],
minZoom: 0,
maxZoom: 16,
attribution: 'WebGL Earth Tiles',
tms: true
}).addTo(earth)
<% @stats[:locations].each do |location| %>
var marker = WE.marker([<%= location[:latitude] %>, <%= location[:longitude] %>]).addTo(earth);
marker.bindPopup("<b><%= location[:name] %></b><br><%= location[:views] %> views", {maxWidth: 150, closeButton: true})
<% end %>
// Start a simple rotation animation
var before = null
requestAnimationFrame(function animate(now) {
var c = earth.getPosition()
var elapsed = before? now - before: 0
before = now
earth.setCenter([c[0], c[1] + 0.1*(elapsed/30)])
requestAnimationFrame(animate)
});
*/
//chart.js
var data = {
labels: <%== @stats[:stat_days].collect {|s| s.created_at.strftime("%b %-d")}.to_json %>,
datasets: [
{
label: "Hits",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: <%== @stats[:stat_days].collect {|s| s.hits}.to_json %>
},
{
label: "Unique Visits",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: <%== @stats[:stat_days].collect {|s| s.views}.to_json %>
}
]
}
// Get context with jQuery - using jQuery's .get() method.
var ctx = $("#myChart").get(0).getContext("2d")
// This will get the first returned node in the jQuery collection.
//var myNewChart = new Chart(ctx);
var myLineChart = new Chart(ctx).Line(data, {
bezierCurve: false,
multiTooltipTemplate: "<%== @multi_tooltip_template %>"
})
})
</script>

View file

@ -17,11 +17,11 @@
<% end %>
</section>
<section>
<form method="POST" action="/site_files/create_page" enctype="multipart/form-data">
<form method="POST" action="/site_files/create" enctype="multipart/form-data">
<%== csrf_token_input_html %>
<input name="dir" type="hidden" value="<%= params[:dir] %>">
<h2>What's the name of your page?</h2>
<p><input type="text" name="pagefilename" autocapitalize="off" autocorrect="off">.html</p>
<p><input type="text" name="filename" autocapitalize="off" autocorrect="off">.html</p>
<p><input class="btn-Action" type="submit" value="Create Page"></p>
<p>Note: We will automatically scrub any characters not matching: a-z A-Z 0-9 _ - .</p>
@ -29,4 +29,4 @@
<p>If you want to make this the index page (and an index page doesn't exist), name it <strong>index.html</strong>.</p>
</section>
</article>
</div>
</div>

View file

@ -27,7 +27,12 @@
<div class="header-Outro editor">
<div class="row content">
<div class="breadcrumbs">
<a href="/dashboard">Dashboard</a> > <span class="filename"><%= @filename %></span>
<a href="/dashboard">Dashboard</a> >
<span class="filename">
<% dir_array = Pathname(@filename).dirname.to_s.split '/' %>
<% dir_array = [] if dir_array == ['.'] %>
<% dir_array.each_with_index do |dir,i| %><a href="/dashboard?dir=<%= Rack::Utils.escape dir_array[0..i].join('/') %>"><%= dir %></a>/<% end %><%= Pathname(@filename).basename %>
</span>
</div>
<div class="tools">
<div class="theme">
@ -70,7 +75,7 @@
</select>
</div>
<a id="saveButton" class="btn-Action" href="#" onclick="saveTextFile(false); return false" style="opacity: 0.5"><i class="fa fa-save"></i>Save</a>
<a class="btn-Action" href="//<%= current_site.host %>/<%= @filename %>" target="_blank">View</a>
<a class="btn-Action" href="<%= current_site.uri %>/<%= @filename %>" target="_blank">View</a>
<a href="#" id="shareButton" class="btn-Action" data-container="body" data-toggle="popover" data-placement="bottom" data-content='<%== erb :'_share', layout: false, locals: {site: current_site, page_uri: "#{current_site.uri}/#{@filename}"} %>'>Share</a>
<!-- <a id="saveAndExitButton" class="btn-Action" href="#" onclick="saveTextFile(true); return false" style="opacity: 0.5"><i class="fa fa-save"></i>&nbsp;&nbsp;Save and Exit</a> -->
<div id="editorUpdates" class="tooltip fade bottom in hidden" style="top: 90px;right: 12.5em;">
@ -85,8 +90,7 @@
<div class="row editor">
<div class="col col-100">
<div id="editor"><%==encoding_fix(@file_data) %>
</div>
<div id="editor"><%==encoding_fix(@file_data) %></div>
</div>
</div>

342
views/stats_mockup.erb Normal file
View file

@ -0,0 +1,342 @@
<div class="header-Outro with-columns">
<div class="row content">
<div class="col col-66">
<h3>Your Stats</h3>
<div class="feed-filter">
<% if !@events.empty? && (site.followings_dataset.count > 0) %>
<a href="/" <% if params[:activity].nil? %>class="selected"<% end %>>All</a>
<a href="/?activity=mine" <% if params[:activity] == 'mine' %>class="selected"<% end %>>Profile Activity</a>
<% end %>
<a href="/activity">Global Activity</a>
</div>
</div>
<div class="col col-32">
<h3>Your Site</h3>
<a href="/dashboard" class="btn-Action edit"><i class="fa fa-edit" title="Edit"></i>Edit Site</a>
</div>
</div>
</div>
<div class="container news-feed">
<div class="content misc-page columns right-col">
<div class="col-left">
<div class="col col-66">
<div class="row">
<div class="col col-50 globe">
<h2>Latest Visitors</h2>
<div id="earth_div"></div>
</div>
<div class="col col-50" style="padding-right: 0;">
<table class="table table-striped" id="latest-visitors">
<tbody>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> neocities.org</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Portland, OR</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">London, UK</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Twitter URL</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Facebook URL</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">San Francisco, CA</span>
<a class="referrer" href=""><i class="fa fa-link"></i> neocities.org</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Portland, OR</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">London, UK</span>
<a class="referrer" href=""><i class="fa fa-link"></i> Twitter URL</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a>, <a href="">/tech</a>, <a href="">/about</a></span>
</td>
</tr>
<tr>
<td>
<span class="location">Hong Kong, China</span>
<a class="referrer" href=""><i class="fa fa-search"></i> Google search</a>
</td>
<td>
<span class="time">7:11PM - 4/27/15</span>
<span class="paths"><a href="">/index</a>, <a href="">/links</a>, <a href="">/art</a>, <a href="">/music</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h2>Last 7 Days</h2>
<p>(<a href="/plan">Upgrade</a> to see up to see stats for all time)</p>
<ul class="nav h-Nav">
<li><a href="">Month</a></li>
<li><a href="">3 months</a></li>
<li><a href="">1 Year</a></li>
<li><a href="">All time</a></li>
</ul>
<canvas id="myChart" style="width:100%;height:300px;display:block"></canvas>
<div class="row">
<div class="col col-50">
<h2>Top Paths</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Path</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>130</td>
</tr>
<tr>
<td>/contact</td>
<td>110</td>
</tr>
<tr>
<td>/art</td>
<td>101</td>
</tr>
<tr>
<td>/about</td>
<td>99</td>
</tr>
<tr>
<td>/links</td>
<td>33</td>
</tr>
</tbody>
</table>
<h2>Top Locations</h2>
<table class="table table-striped">
<thead>
<tr>
<th>City</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Portland, OR, USA</td>
<td>22</td>
</tr>
<tr>
<td>Portland, OR, USA</td>
<td>22</td>
</tr>
<tr>
<td>Portland, OR, USA</td>
<td>22</td>
</tr>
<tr>
<td>Portland, OR, USA</td>
<td>22</td>
</tr>
<tr>
<td>Portland, OR, USA</td>
<td>22</td>
</tr>
</tbody>
</table>
</div>
<div class="col col-50">
<h2>Top Referrers</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Referrer</th>
<th>Visits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Google search</td>
<td>22</td>
</tr>
<tr>
<td>Google search</td>
<td>22</td>
</tr>
<tr>
<td>Google search</td>
<td>22</td>
</tr>
<tr>
<td>Google search</td>
<td>22</td>
</tr>
<tr>
<td>Google search</td>
<td>22</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col col-33">
<div class="news-site-info">
<p class="site-url"><a href="<%= current_site.uri %>" target="_blank"><%= site.title %></a></p>
<div class="stats">
<div class="col col-50">
<% if site.updated_at %>
Last updated<br><strong><%= site.updated_at.ago %></strong>
<% else %>
Your new website!<br><strong><a href="/dashboard"><i class="fa fa-edit" title="Edit"></i> Start Building</a></strong>
<% end %>
</div>
<div class="col col-50">
<div><strong><%= site.views.format_large_number %></strong> views</div>
<% follows_count = site.follows_dataset.count %>
<div><strong><%= follows_count.format_large_number %></strong> follower<%= follows_count == 1 ? '' : 's' %></div>
</div>
</div>
</div>
<a href="<%= site.uri %>" class="large-portrait" style="background-image:url(<%= site.screenshot_url('index.html', '540x405') %>);"></a>
<div class="news-profile-button">
<a href="/site/<%= site.username %>" class="btn-Action"><i class="fa fa-user"></i> Profile</a>
<a href="#" id="shareButton" class="btn-Action" data-container="body" data-toggle="popover" data-placement="bottom" data-content='<%== erb :'_share', layout: false, locals: {site: current_site} %>'><i class="fa fa-share-alt"></i> Share</a>
</div>
<%== erb :'_follows', layout: false, locals: {site: site, is_current_site: site == current_site} %>
<%== erb :'_tags', layout: false, locals: {site: site, is_current_site: site == current_site} %>
</div>
</div>
</div>
</div>
<script src="http://www.webglearth.com/v2/api.js"></script>
<script src="/js/Chart.min.js"></script>
<script>
//OpenGL globe
$(document).ready(function() {
var earth = new WE.map('earth_div');
earth.setView([20, -100], 2.07);
WE.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution: '© OpenStreetMap'
}).addTo(earth);
// Start a simple rotation animation
var before = null;
requestAnimationFrame(function animate(now) {
var c = earth.getPosition();
var elapsed = before? now - before: 0;
before = now;
earth.setCenter([c[0], c[1] + 0.1*(elapsed/30)]);
requestAnimationFrame(animate);
});
//chart.js
var data = {
labels: ["May 1", "May 2", "May 3", "May 4", "May 5", "May 6", "May7"],
datasets: [
{
label: "Hits",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 81, 56, 55, 65]
},
{
label: "Visits",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [28, 48, 40, 66, 33, 27, 45]
}
]
};
// Get context with jQuery - using jQuery's .get() method.
var ctx = $("#myChart").get(0).getContext("2d");
// This will get the first returned node in the jQuery collection.
//var myNewChart = new Chart(ctx);
var myLineChart = new Chart(ctx).Line(data, {bezierCurve: false});
})
</script>

View file

@ -2,9 +2,9 @@ Hello,
We're writing to let you know that Neocities has had an issue charging your card. We will try again automatically in a few days, but we just wanted to give you a heads up in case something is wrong with your card.
We will retry over the next few weeks. If we can't make a successful charge, your account will be downgraded from supporter to the free plan.
We will retry over the next few weeks. If we can't make a successful charge, we will automatically switch your account over to the free plan.
If you need to change the card on file, you can do so on the settings page. If you run into any issues, please contact us at https://neocities.org/contact.
If you need to change the card on file, you can do so on your account settings page. If you run into any issues, please contact us at https://neocities.org/contact.
Regards,
- The Neocities Team

View file

@ -1,12 +1,28 @@
<%= request.request_method %> <%= request.path %>
#### Route
<%== request.request_method %> <%== request.path %>
<% if current_site %>
Site: <%= current_site.username %>
Email: <%= current_site.email %>
#### Site
<%== current_site.username %>
<% if current_site.email %>
Email: <%== current_site.email %>
<% else %>
User does not have an email address.
<% end %>
<% else %>
Not a logged in session.
<% end %>
Params:
<%= params.inspect %>
#### Params
<%== params.inspect %>
Backtrace:
<%= env['sinatra.error'].backtrace.join("\n") %>
#### Session
<%== session.inspect %>
#### HTTP REFERRER
<%== request.referrer %>
#### Backtrace
<%== env['sinatra.error'].backtrace.join("\n") %>

27
workers/archive_worker.rb Normal file
View file

@ -0,0 +1,27 @@
require 'sidekiq/api'
class ArchiveWorker
include Sidekiq::Worker
sidekiq_options queue: :archive, retry: 2, backtrace: true
def perform(site_id)
site = Site[site_id]
return if site.nil? || site.is_banned? || site.is_deleted
if site.site_files_dataset.count > 1000
logger.info 'skipping #{site_id} (#{site.username}) due to > 1000 files'
return
end
queue = Sidekiq::Queue.new self.class.sidekiq_options_hash['queue']
logger.info "JOB ID: #{jid} #{site_id.inspect}"
queue.each do |job|
if job.args == [site_id] && job.jid != jid
logger.info "DELETING #{job.jid} #{job.args.inspect}"
job.delete
end
end
site.archive!
end
end

View file

@ -3,12 +3,13 @@ class EmailWorker
sidekiq_options queue: :emails, retry: 10, backtrace: true
def perform(args={})
raise 'no' if ENV['RACK_ENV'].nil? || ENV['RACK_ENV'] == 'development'
unsubscribe_token = Site.email_unsubscribe_token args['to']
if args['no_footer']
footer = ''
else
footer = "\n\n---\nYou are receiving this email because you have a Neocities site. If you would like to subscribe from Neocities emails, just visit this url:\nhttps://neocities.org/settings/unsubscribe_email?email=#{Rack::Utils.escape args['to']}&token=#{unsubscribe_token}"
footer = "\n\n---\nYou are receiving this email because you have a Neocities site. If you would like to unsubscribe from Neocities emails, just visit this url:\nhttps://neocities.org/settings/unsubscribe_email?email=#{Rack::Utils.escape args['to']}&token=#{unsubscribe_token}"
end
Mail.deliver do

View file

@ -0,0 +1,23 @@
class PurgeCacheOrderWorker
include Sidekiq::Worker
sidekiq_options queue: :purgecacheorder, retry: 1000, backtrace: true, average_scheduled_poll_interval: 1
sidekiq_retry_in do |count|
return 10 if count < 10
180
end
RESOLVER = Dnsruby::Resolver.new
def perform(username, path)
if ENV['RACK_ENV'] == 'test'
proxy_ips = ['10.0.0.1', '10.0.0.2']
else
proxy_ips = RESOLVER.query($config['cache_purge_ips_uri']).answer.collect {|a| a.address.to_s}
end
proxy_ips.each do |proxy_ip|
PurgeCacheWorker.perform_async proxy_ip, username, path
end
end
end

View file

@ -1,22 +1,25 @@
require 'open-uri'
class PurgeCacheWorker
include Sidekiq::Worker
sidekiq_options queue: :purgecache, retry: 10, backtrace: true
sidekiq_options queue: :purgecache, retry: 1000, backtrace: false, average_scheduled_poll_interval: 1
def perform(payload)
# :nocov:
attempt = 0
begin
attempt += 1
$pubsub_pool.with do |redis|
redis.publish 'purgecache', payload.to_json
end
rescue Redis::BaseConnectionError => error
raise if attempt > 3
puts "pubsub error: #{error}, retrying in 1s"
sleep 1
retry
end
# :nocov:
sidekiq_retry_in do |count|
return 10 if count < 10
180
end
end
def perform(proxy_ip, username, path)
# Must always have a forward slash
path = '/' + path if path[0] != '/'
url = Addressable::URI.encode_component(
"http://#{proxy_ip}/:cache/purge#{path}",
Addressable::URI::CharacterClasses::QUERY
)
begin
RestClient.get(url, host: URI::encode("#{username}.neocities.org"))
rescue RestClient::ResourceNotFound
end
end
end

Some files were not shown because too many files have changed in this diff Show more