Merge branch 'master' into apng

This commit is contained in:
Winterhuman 2024-01-08 16:55:35 +00:00 committed by GitHub
commit a1d32b67a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 1381 additions and 1450 deletions

View file

@ -36,6 +36,6 @@ jobs:
- run: sudo apt-get update && sudo apt-get -y install libimlib2-dev chromium-browser
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
ruby-version: '3.3'
bundler-cache: true
- run: bundle exec rake

15
Gemfile
View file

@ -7,15 +7,14 @@ gem 'redis-namespace'
gem 'bcrypt'
gem 'sinatra-flash', require: 'sinatra/flash'
gem 'sinatra-xsendfile', require: 'sinatra/xsendfile'
gem 'puma', '5.6.5', require: nil
gem 'sidekiq', '~> 7.0.8'
gem 'puma', '< 7', require: nil
gem 'sidekiq', '~> 7'
gem 'mail'
gem 'net-smtp'
gem 'tilt'
gem 'erubis'
gem 'stripe' #, source: 'https://code.stripe.com/'
gem 'terrapin'
gem 'zipruby'
gem 'sass', require: nil
gem 'dav4rack', git: 'https://github.com/neocities/dav4rack.git', ref: '3ecde122a0b8bcc1d85581dc85ef3a7120b6a8f0'
gem 'filesize'
@ -27,7 +26,6 @@ gem 'paypal-recurring', require: 'paypal/recurring'
gem 'geoip'
gem 'io-extra', require: 'io/extra'
#gem 'rye'
gem 'base32'
gem 'coveralls_reborn', require: false
gem 'sanitize'
gem 'will_paginate'
@ -58,6 +56,13 @@ gem 'rss'
gem 'webp-ffi'
gem 'rszr'
gem 'zip_tricks'
gem 'adequate_crypto_address'
gem 'twilio-ruby'
gem 'phonelib'
gem 'dnsbl-client'
gem 'minfraud'
gem 'image_optimizer' # apt install optipng jpegoptim pngquant
gem 'rubyzip', require: 'zip'
group :development, :test do
gem 'pry'
@ -77,6 +82,7 @@ group :test do
gem 'mocha', require: nil
gem 'rake', '>= 12.3.3', require: nil
gem 'capybara', require: nil #, '2.10.1', require: nil
gem 'selenium-webdriver'
gem 'rack_session_access', require: nil
gem 'webmock', require: nil
gem 'stripe-ruby-mock', '~> 3.1.0.rc3', require: 'stripe_mock'
@ -84,5 +90,4 @@ group :test do
gem 'mock_redis'
gem 'simplecov', require: nil
gem 'm'
gem 'apparition', github: 'twalpole/apparition', ref: 'ca86be4d54af835d531dbcd2b86e7b2c77f85f34'
end

View file

@ -10,33 +10,34 @@ GIT
rack (>= 1.6)
uuidtools (~> 2.1.1)
GIT
remote: https://github.com/twalpole/apparition.git
revision: ca86be4d54af835d531dbcd2b86e7b2c77f85f34
ref: ca86be4d54af835d531dbcd2b86e7b2c77f85f34
specs:
apparition (0.6.0)
capybara (~> 3.13, < 4)
websocket-driver (>= 0.6.5)
GEM
remote: https://rubygems.org/
specs:
acme-client (2.0.11)
acme-client (2.0.15)
faraday (>= 1.0, < 3.0.0)
faraday-retry (~> 1.0)
activesupport (7.0.4.3)
faraday-retry (>= 1.0, < 3.0.0)
activesupport (7.1.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.1)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
adequate_crypto_address (0.1.9)
base58 (~> 0.2)
keccak (~> 1.3)
ansi (1.5.0)
base32 (0.3.4)
bcrypt (3.1.18)
base58 (0.2.3)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.5)
builder (3.2.4)
capybara (3.38.0)
capybara (3.39.2)
addressable
matrix
mini_mime (>= 0.1.3)
@ -46,31 +47,34 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
certified (1.0.0)
climate_control (0.2.0)
climate_control (1.2.0)
coderay (1.1.3)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
coveralls_reborn (0.25.0)
simplecov (>= 0.18.1, < 0.22.0)
term-ansicolor (~> 1.6)
thor (>= 0.20.3, < 2.0)
tins (~> 1.16)
connection_pool (2.4.1)
coveralls_reborn (0.28.0)
simplecov (~> 0.22.0)
term-ansicolor (~> 1.7)
thor (~> 1.2)
tins (~> 1.32)
crack (0.4.5)
rexml
crass (1.0.6)
dante (0.2.0)
date (3.3.4)
dnsbl-client (1.1.1)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20231109)
drb (2.2.0)
ruby2_keywords
erubis (2.7.0)
exifr (1.3.10)
fabrication (2.30.0)
facter (4.2.13)
exifr (1.4.0)
fabrication (2.31.0)
facter (4.5.1)
hocon (~> 1.3)
thor (>= 1.0.1, < 2.0)
faker (3.0.0)
faker (3.2.2)
i18n (>= 1.8.11, < 2)
faraday (1.10.2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -100,7 +104,7 @@ GEM
faraday_middleware (>= 0.9)
loofah (>= 2.0)
sax-machine (>= 1.0)
ffi (1.15.5)
ffi (1.16.3)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
@ -110,14 +114,14 @@ GEM
hashie
xmlrpc
geoip (1.6.4)
hashdiff (1.0.1)
hashdiff (1.1.0)
hashie (5.0.0)
hiredis (0.6.3)
hocon (1.3.1)
hoe (3.26.0)
hocon (1.4.0)
hoe (4.1.0)
rake (>= 0.8, < 15.0)
htmlentities (4.3.4)
http (5.1.0)
http (5.1.1)
addressable (~> 2.8)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
@ -126,113 +130,137 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
i18n (1.12.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
image_optim (0.31.1)
image_optim (0.31.3)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (>= 1.5, < 4)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
image_optim_pack (0.9.1.20221104-x86_64-linux)
image_optim_pack (0.10.1-x86_64-linux)
fspath (>= 2.1, < 4)
image_optim (~> 0.19)
image_size (3.2.0)
image_optimizer (1.9.0)
image_size (3.3.0)
in_threads (1.6.0)
io-extra (1.4.0)
ipaddress (0.8.3)
json (2.6.2)
json (2.7.1)
jwt (2.7.1)
keccak (1.3.1)
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
loofah (2.19.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
m (1.6.0)
nokogiri (>= 1.12.0)
m (1.6.2)
method_source (>= 0.6.7)
rake (>= 0.9.2.2)
magic (0.2.9)
ffi (>= 0.6.3)
mail (2.7.1)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
matrix (0.4.2)
maxmind-db (1.1.1)
maxmind-db (1.2.0)
maxmind-geoip2 (1.2.0)
connection_pool (~> 2.2)
http (>= 4.3, < 6.0)
maxmind-db (~> 1.2)
method_source (1.0.0)
mime-types (3.4.1)
mime-types (3.5.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mini_mime (1.1.2)
minitest (5.16.3)
minitest-reporters (1.5.0)
mime-types-data (3.2023.1205)
minfraud (2.3.0)
connection_pool (~> 2.2)
http (>= 4.3, < 6.0)
maxmind-geoip2 (~> 1.2)
simpleidn (~> 0.1, >= 0.1.1)
mini_mime (1.1.5)
minitest (5.20.0)
minitest-reporters (1.6.1)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
mocha (2.0.1)
mocha (2.1.0)
ruby2_keywords (>= 0.0.5)
mock_redis (0.34.0)
ruby2_keywords
mock_redis (0.41.0)
monetize (1.12.0)
money (~> 6.12)
money (6.16.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.6.0)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.2.3)
multipart-post (2.3.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
net-protocol (0.1.3)
mutex_m (0.2.0)
net-imap (0.4.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.3.3)
net-smtp (0.4.0)
net-protocol
netrc (0.11.0)
nio4r (2.5.8)
nokogiri (1.14.3-x86_64-linux)
nio4r (2.7.0)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
ox (2.14.11)
ox (2.14.17)
paypal-recurring (1.1.0)
pg (1.5.3)
pg (1.5.4)
phonelib (0.8.6)
progress (3.6.0)
pry (0.14.1)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (5.0.0)
puma (5.6.5)
public_suffix (5.0.4)
puma (6.4.0)
nio4r (~> 2.0)
racc (1.7.1)
rack (2.2.6.4)
rack-cache (1.13.0)
racc (1.7.3)
rack (2.2.8)
rack-cache (1.15.0)
rack (>= 0.4)
rack-protection (3.0.4)
rack
rack-test (2.0.2)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-test (2.1.0)
rack (>= 1.3)
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
rake (13.0.6)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.5.1)
redis-client (0.14.1)
redis (5.0.8)
redis-client (>= 0.17.0)
redis-client (0.19.1)
connection_pool
redis-namespace (1.9.0)
redis-namespace (1.11.0)
redis (>= 4)
regexp_parser (2.6.0)
regexp_parser (2.8.3)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.5)
rexml (3.2.6)
rinku (2.0.6)
rss (0.2.9)
rss (0.3.0)
rexml
rszr (1.3.0)
ruby-progressbar (1.11.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sanitize (6.0.2)
rubyzip (2.3.2)
sanitize (6.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sass (3.7.4)
@ -241,18 +269,23 @@ GEM
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sax-machine (1.3.2)
sequel (5.68.0)
selenium-webdriver (4.16.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sequel (5.75.0)
bigdecimal
sequel_pg (1.17.1)
pg (>= 0.18.0, != 1.2.0)
sequel (>= 4.38.0)
shotgun (0.9.2)
rack (>= 1.0)
sidekiq (7.0.8)
sidekiq (7.2.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.11.0)
simplecov (0.21.2)
redis-client (>= 0.14.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
@ -260,10 +293,10 @@ GEM
simplecov_json_formatter (0.1.4)
simpleidn (0.2.1)
unf (~> 0.1.4)
sinatra (3.0.4)
sinatra (3.2.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.0.4)
rack-protection (= 3.2.0)
tilt (~> 2.0)
sinatra-flash (0.3.0)
sinatra (>= 1.0.0)
@ -277,39 +310,40 @@ GEM
sync (0.5.0)
term-ansicolor (1.7.1)
tins (~> 1.0)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
terrapin (1.0.1)
climate_control
thor (1.3.0)
thread (0.2.2)
tilt (2.0.11)
timecop (0.9.5)
timeout (0.3.0)
tins (1.31.1)
tilt (2.3.0)
timecop (0.9.8)
timeout (0.4.1)
tins (1.32.1)
sync
twilio-ruby (6.9.0)
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0)
nokogiri (>= 1.6, < 2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unf_ext (0.0.9.1)
uuidtools (2.1.5)
webmock (3.18.1)
webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webp-ffi (0.3.1)
webp-ffi (0.4.0)
ffi (>= 1.9.0)
ffi-compiler (>= 0.1.2)
webrick (1.7.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (3.3.1)
xmlrpc (0.3.2)
webrick (1.8.1)
websocket (1.2.10)
will_paginate (4.0.0)
xmlrpc (0.3.3)
webrick
xpath (3.2.0)
nokogiri (~> 1.8)
zip_tricks (5.6.0)
zipruby (0.3.6)
PLATFORMS
x86_64-linux
@ -318,13 +352,13 @@ DEPENDENCIES
acme-client (~> 2.0.0)
activesupport
addressable (>= 2.8.0)
apparition!
base32
adequate_crypto_address
bcrypt
capybara
certified
coveralls_reborn
dav4rack!
dnsbl-client
erubis
fabrication
facter
@ -339,6 +373,7 @@ DEPENDENCIES
http
image_optim
image_optim_pack
image_optimizer
io-extra
ipaddress
json (>= 2.3.0)
@ -346,6 +381,7 @@ DEPENDENCIES
magic
mail
maxmind-db
minfraud
minitest
minitest-reporters
mocha
@ -356,8 +392,9 @@ DEPENDENCIES
nokogiri
paypal-recurring
pg
phonelib
pry
puma (= 5.6.5)
puma (< 7)
rack-cache
rack-test
rack_session_access
@ -368,12 +405,14 @@ DEPENDENCIES
rinku
rss
rszr
rubyzip
sanitize
sass
selenium-webdriver
sequel
sequel_pg
shotgun
sidekiq (~> 7.0.8)
sidekiq (~> 7)
simplecov
simpleidn
sinatra
@ -385,12 +424,12 @@ DEPENDENCIES
thread
tilt
timecop
twilio-ruby
webmock
webp-ffi
will_paginate
xmlrpc
zip_tricks
zipruby
BUNDLED WITH
2.3.10

390
Rakefile
View file

@ -14,22 +14,6 @@ end
task :default => :test
=begin
desc "send domain update email"
task :send_domain_update_email => [:environment] do
Site.exclude(domain: nil).exclude(domain: '').all.each do |site|
msg = <<-HERE
MESSAGE GOES HERE TEST
HERE
site.send_email(
subject: 'SUBJECT GOES HERE',
body: msg
)
end
end
=end
desc "prune logs"
task :prune_logs => [:environment] do
Stat.prune!
@ -45,7 +29,8 @@ end
desc 'Update disposable email blacklist'
task :update_disposable_email_blacklist => [:environment] do
uri = URI.parse('https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf')
# Formerly: https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf
uri = URI.parse('https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt')
File.write(Site::DISPOSABLE_EMAIL_BLACKLIST_PATH, Net::HTTP.get(uri))
end
@ -54,196 +39,33 @@ desc 'Update banned IPs list'
task :update_blocked_ips => [:environment] do
filename = 'listed_ip_365_ipv46'
zip_path = "/tmp/#{filename}.zip"
File.open("/tmp/#{filename}.zip", 'wb') do |file|
File.open(zip_path, 'wb') do |file|
response = HTTP.get "https://www.stopforumspam.com/downloads/#{filename}.zip"
response.body.each do |chunk|
file.write chunk
end
end
Zip::Archive.open("/tmp/#{filename}.zip") do |ar|
ar.fopen("#{filename}.txt") do |f|
ips = f.read
insert_hashes = []
ips.each_line {|ip| insert_hashes << {ip: ip.strip, created_at: Time.now}}
ips = nil
Zip::File.open(zip_path) do |zip_file|
zip_file.each do |entry|
if entry.name == "#{filename}.txt"
ips = entry.get_input_stream.read
insert_hashes = []
ips.each_line { |ip| insert_hashes << { ip: ip.strip, created_at: Time.now } }
ips = nil
DB.transaction do
DB[:blocked_ips].delete
DB[:blocked_ips].multi_insert insert_hashes
end
end
end
FileUtils.rm "/tmp/#{filename}.zip"
end
desc 'parse tor exits'
task :parse_tor_exits => [:environment] do
exit_ips = Net::HTTP.get(URI.parse('https://check.torproject.org/exit-addresses'))
exit_ips.split("\n").collect {|line|
line.match(/ExitAddress (\d+\.\d+\.\d+\.\d+)/)&.captures&.first
}.compact
# ^^ Array of ip addresses of known exit nodes
end
desc 'Compile nginx mapfiles'
task :compile_nginx_mapfiles => [:environment] do
FileUtils.mkdir_p './files/maps'
File.open('./files/maps/domains.txt', 'w') do |file|
Site.exclude(domain: nil).exclude(domain: '').select(:username,:domain).all.each do |site|
file.write ".#{site.values[:domain]} #{site.username};\n"
end
end
File.open('./files/maps/supporters.txt', 'w') do |file|
Site.select(:username, :domain).exclude(plan_type: 'free').exclude(plan_type: nil).all.each do |parent_site|
sites = [parent_site] + parent_site.children
sites.each do |site|
file.write "#{site.username}.neocities.org 1;\n"
unless site.host.match(/\.neocities\.org$/)
file.write ".#{site.values[:domain]} 1;\n"
# Database transaction
DB.transaction do
DB[:blocked_ips].delete
DB[:blocked_ips].multi_insert insert_hashes
end
end
end
end
File.open('./files/maps/subdomain-to-domain.txt', 'w') do |file|
Site.select(:username, :domain).exclude(domain: nil).exclude(domain: '').all.each do |site|
file.write "#{site.username}.neocities.org #{site.values[:domain]};\n"
end
end
File.open('./files/maps/sandboxed.txt', 'w') do |file|
usernames = DB["select username from sites where created_at > ? and parent_site_id is null and (plan_type is null or plan_type='free') and is_banned != 't' and is_deleted != 't'", 2.days.ago].all.collect {|s| s[:username]}.each {|username| file.write "#{username} 1;\n"}
end
# Compile letsencrypt ssl keys
sites = DB[%{select username,ssl_key,ssl_cert,domain from sites where ssl_cert is not null and ssl_key is not null and (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all
ssl_path = './files/maps/ssl'
FileUtils.mkdir_p ssl_path
sites.each do |site|
[site[:domain], "www.#{site[:domain]}"].each do |domain|
begin
key = OpenSSL::PKey::RSA.new site[:ssl_key]
crt = OpenSSL::X509::Certificate.new site[:ssl_cert]
rescue => e
puts "SSL ERROR: #{e.class} #{e.inspect}"
next
end
File.open(File.join(ssl_path, "#{domain}.key"), 'wb') {|f| f.write key.to_der}
File.open(File.join(ssl_path, "#{domain}.crt"), 'wb') {|f| f.write site[:ssl_cert]}
end
end
end
desc 'Produce SSL config package for proxy'
task :buildssl => [:environment] do
sites = Site.select(:id, :username, :domain, :ssl_key, :ssl_cert).
exclude(domain: nil).
exclude(ssl_key: nil).
exclude(ssl_cert: nil).
all
payload = []
begin
FileUtils.rm './files/sslsites.zip'
rescue Errno::ENOENT
end
Zip::Archive.open('./files/sslsites.zip', Zip::CREATE) do |ar|
ar.add_dir 'ssl'
sites.each do |site|
ar.add_buffer "ssl/#{site.username}.key", site.ssl_key
ar.add_buffer "ssl/#{site.username}.crt", site.ssl_cert
payload << {username: site.username, domain: site.domain}
end
ar.add_buffer 'sslsites.json', payload.to_json
end
end
desc 'Set existing stripe customers to internal supporter plan'
task :primenewstriperunonlyonce => [:environment] do
# Site.exclude(stripe_customer_id: nil).all.each do |site|
# site.plan_type = 'supporter'
# site.save_changes validate: false
# end
Site.exclude(stripe_customer_id: nil).where(plan_type: nil).where(plan_ended: false).all.each do |s|
customer = Stripe::Customer.retrieve(s.stripe_customer_id)
subscription = customer.subscriptions.first
next if subscription.nil?
puts "set subscription id to #{subscription.id}"
puts "set plan type to #{subscription.plan.id}"
s.stripe_subscription_id = subscription.id
s.plan_type = subscription.plan.id
s.save_changes(validate: false)
end
end
desc 'dedupe tags'
task :dedupetags => [:environment] do
Tag.all.each do |tag|
begin
tag.reload
rescue Sequel::Error => e
next if e.message =~ /Record not found/
end
matching_tags = Tag.exclude(id: tag.id).where(name: tag.name).all
matching_tags.each do |matching_tag|
DB[:sites_tags].where(tag_id: matching_tag.id).update(tag_id: tag.id)
matching_tag.delete
end
end
end
desc 'Clean tags'
task :cleantags => [:environment] do
Site.select(:id).all.each do |site|
if site.tags.length > 5
site.tags.slice(5, site.tags.length).each {|tag| site.remove_tag tag}
end
end
empty_tag = Tag.where(name: '').first
if empty_tag
DB[:sites_tags].where(tag_id: empty_tag.id).delete
end
Tag.all.each do |tag|
if tag.name.length > Tag::NAME_LENGTH_MAX || tag.name.match(/ /)
DB[:sites_tags].where(tag_id: tag.id).delete
DB[:tags].where(id: tag.id).delete
else
tag.update name: tag.name.downcase.strip
end
end
Tag.where(name: 'porn').first.update is_nsfw: true
end
desc 'update screenshots'
task :update_screenshots => [:environment] do
Site.select(:username).where(site_changed: true, is_banned: false, is_crashing: false).filter(~{updated_at: nil}).order(:updated_at.desc).all.each do |site|
ScreenshotWorker.perform_async site.username, 'index.html'
end
FileUtils.rm zip_path
end
desc 'rebuild_thumbnails'
@ -266,142 +88,11 @@ task :rebuild_thumbnails => [:environment] do
end
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.save_changes validate: false
end
end
desc 'prime site_updated_at'
task :prime_site_updated_at => [:environment] do
Site.select(:id,:username,:site_updated_at, :updated_at).all.each do |s|
s.site_updated_at = s.updated_at
s.save_changes validate: false
end
end
desc 'prime_site_files'
task :prime_site_files => [:environment] do
Site.where(is_banned: false).where(is_deleted: false).select(:id, :username).all.each do |site|
Dir.glob(File.join(site.files_path, '**/*')).each do |file|
path = file.gsub(site.base_files_path, '').sub(/^\//, '')
site_file = site.site_files_dataset[path: path]
if site_file.nil?
mtime = File.mtime file
site_file_opts = {
path: path,
updated_at: mtime,
created_at: mtime
}
if File.directory? file
site_file_opts.merge! is_directory: true
else
site_file_opts.merge!(
size: File.size(file),
sha1_hash: Digest::SHA1.file(file).hexdigest
)
end
site.add_site_file site_file_opts
end
end
end
end
desc 'dedupe_follows'
task :dedupe_follows => [:environment] do
follows = Follow.all
deduped_follows = Follow.all.uniq {|f| "#{f.site_id}_#{f.actioning_site_id}"}
follows.each do |follow|
unless deduped_follows.include?(follow)
puts "deleting dedupe: #{follow.inspect}"
follow.delete
end
end
end
desc 'flush_empty_index_sites'
task :flush_empty_index_sites => [:environment] do
sites = Site.select(:id).all
counter = 0
sites.each do |site|
if site.empty_index?
counter += 1
site.site_changed = false
site.save_changes validate: false
end
end
puts "#{counter} sites set to not changed."
end
desc 'compute_scores'
task :compute_scores => [:environment] do
Site.compute_scores
end
=begin
desc 'Update screenshots'
task :update_screenshots => [:environment] do
Site.select(:username).filter(is_banned: false).filter(~{updated_at: nil}).order(:updated_at.desc).all.collect {|s|
ScreenshotWorker.perform_async s.username
}
end
=end
desc 'prime_classifier'
task :prime_classifier => [:environment] do
Site.select(:id, :username).where(is_banned: false, is_deleted: false).all.each do |site|
next if site.site_files_dataset.where(classifier: 'spam').count > 0
html_files = site.site_files_dataset.where(path: /\.html$/).all
html_files.each do |html_file|
print "training #{site.username}/#{html_file.path}..."
site.train html_file.path
print "done.\n"
end
end
end
desc 'train_spam'
task :train_spam => [:environment] do
paths = File.read('./spam.txt')
paths.split("\n").each do |path|
username, site_file_path = path.match(/^([a-zA-Z0-9_\-]+)\/(.+)$/i).captures
site = Site[username: username]
next if site.nil?
site_file = site.site_files_dataset.where(path: site_file_path).first
next if site_file.nil?
site.train site_file_path, :spam
site.ban!
puts "Deleted #{site_file_path}, banned #{site.username}"
end
end
desc 'regenerate_ssl_certs'
task :regenerate_ssl_certs => [:environment] do
sites = DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all
seconds = 2
sites.each do |site|
LetsEncryptWorker.perform_in seconds, site[:id]
seconds += 10
end
puts "#{sites.length.to_s} records are primed"
end
desc 'renew_ssl_certs'
task :renew_ssl_certs => [:environment] do
delay = 0
@ -418,24 +109,6 @@ task :purge_tmp_turds => [:environment] do
end
end
desc 'shard_migration'
task :shard_migration => [:environment] do
#Site.exclude(is_deleted: true).exclude(is_banned: true).select(:username).each do |site|
# FileUtils.mkdir_p File.join('public', 'testsites', site.username)
#end
#exit
Dir.chdir('./public/testsites')
Dir.glob('*').each do |dir|
sharding_dir = Site.sharding_dir(dir)
FileUtils.mkdir_p File.join('..', 'newtestsites', sharding_dir)
FileUtils.mv dir, File.join('..', 'newtestsites', sharding_dir)
end
sleep 1
FileUtils.rmdir './public/testsites'
sleep 1
FileUtils.mv './public/newtestsites', './public/testsites'
end
desc 'compute_follow_count_scores'
task :compute_follow_count_scores => [:environment] do
@ -449,37 +122,6 @@ task :compute_follow_count_scores => [:environment] do
end
end
desc 'prime_redis_proxy_ssl'
task :prime_redis_proxy_ssl => [:environment] do
site_ids = DB[%{
select id from sites where domain is not null and ssl_cert is not null and ssl_key is not null
and is_deleted != ? and is_banned != ?
}, true, true].all.collect {|site_id| site_id[:id]}
site_ids.each do |site_id|
Site[site_id].store_ssl_in_redis_proxy
end
end
desc 'dedupe_site_blocks'
task :dedupe_site_blocks => [:environment] do
duped_blocks = []
block_ids = Block.select(:id).all.collect {|b| b.id}
block_ids.each do |block_id|
next unless duped_blocks.select {|db| db.id == block_id}.empty?
block = Block[block_id]
if block
blocks = Block.exclude(id: block.id).where(site_id: block.site_id).where(actioning_site_id: block.actioning_site_id).all
duped_blocks << blocks
duped_blocks.flatten!
end
end
duped_blocks.each do |duped_block|
duped_block.destroy
end
end
desc 'ml_screenshots_list_dump'
task :ml_screenshots_list_dump => [:environment] do
['phishing', 'spam', 'ham', nil].each do |classifier|

4
Vagrantfile vendored
View file

@ -1,12 +1,12 @@
VAGRANTFILE_API_VERSION = '2'
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = 'ubuntu/bionic64'
config.vm.box = 'ubuntu/jammy64'
config.vm.provision :shell, path: './vagrant/development.sh'
config.vm.network :forwarded_port, guest: 9292, host: 9292
config.vm.provider :virtualbox do |vb|
vb.customize ['modifyvm', :id, '--memory', '2048']
vb.customize ['modifyvm', :id, '--memory', '8192']
vb.name = 'neocities'
end
end

10
app.rb
View file

@ -75,8 +75,10 @@ before do
content_type :json
elsif request.path.match /^\/webhooks\//
# Skips the CSRF/validation check for stripe web hooks
elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter/)
elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter|^\/signout/)
redirect "/site/#{current_site.username}/confirm_email"
elsif !email_not_validated? && current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone|^\/signout/)
redirect "/site/#{current_site.username}/confirm_phone"
else
content_type :html, 'charset' => 'utf-8'
redirect '/' if request.post? && !csrf_safe?
@ -89,9 +91,9 @@ after do
end
end
#after do
#response.headers['Content-Security-Policy'] = %{block-all-mixed-content; default-src 'self'; connect-src 'self' https://api.stripe.com https://assets.hcaptcha.com; frame-src https://assets.hcaptcha.com https://js.stripe.com; script-src 'self' 'unsafe-inline' https://js.stripe.com https://hcaptcha.com https://assets.hcaptcha.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: }
#end
after do
response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} unless self.class.development?
end
not_found do
api_not_found if @api

View file

@ -250,7 +250,8 @@ post '/admin/banhammer' do
StopForumSpamWorker.perform_async(
username: site.username,
email: site.email,
ip: site.ip
ip: site.ip,
classifier: params[:classifier]
)
end
end

View file

@ -167,8 +167,7 @@ def api_info_for(site)
created_at: site.created_at.rfc2822,
last_updated: site.site_updated_at ? site.site_updated_at.rfc2822 : nil,
domain: site.domain,
tags: site.tags.collect {|t| t.name},
latest_ipfs_hash: site.latest_archive ? site.latest_archive.ipfs_hash : nil
tags: site.tags.collect {|t| t.name}
}
}
end

View file

@ -9,6 +9,10 @@ post '/contact' do
@errors << 'Please fill out all fields'
end
if params[:faq_check] == 'no'
@errors << 'Please check Frequently Asked Questions before sending a contact message'
end
unless hcaptcha_valid?
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
end

View file

@ -91,6 +91,11 @@ post '/create' do
return {result: 'error'}.to_json
end
if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(@site, request)
flash[:error] = 'Site creation is currently unavailable, please try again later.'
return {result: 'error'}.to_json
end
if !@site.valid?
flash[:error] = @site.errors.first.last.first
return {result: 'error'}.to_json
@ -98,6 +103,20 @@ post '/create' do
end
@site.email_confirmed = true if self.class.development?
@site.phone_verified = true if self.class.development?
begin
@site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(@site)
rescue => e
EmailWorker.perform_async({
from: 'web@neocities.org',
to: 'errors@neocities.org',
subject: "[Neocities Error] Phone verification exception",
body: "#{e.inspect}\n#{e.backtrace}",
no_footer: true
})
end
@site.save
unless education_whitelisted?

View file

@ -80,6 +80,8 @@ get '/?' do
@blog_feed_html = SimpleCache.get :blog_feed_html
end
@create_disabled = false
erb :index, layout: :index_layout
end

View file

@ -42,7 +42,7 @@ the Neocities Cat
end
end
flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.'
flash[:success] = "We sent an e-mail with password reset instructions. Check your spam folder if you don't see it in your inbox."
redirect '/'
end

View file

@ -15,6 +15,13 @@ def require_ownership_for_settings
end
end
get '/settings/invoices/?' do
require_login
@title = 'Invoices'
@invoices = parent_site.stripe_customer_id ? Stripe::Invoice.list(customer: parent_site.stripe_customer_id) : []
erb :'settings/invoices'
end
get '/settings/:username/?' do |username|
# This is for the email_unsubscribe below
pass if Site.select(:id).where(username: username).first.nil?
@ -56,8 +63,7 @@ post '/settings/:username/profile' do
@site.update(
profile_comments_enabled: params[:site][:profile_comments_enabled],
profile_enabled: params[:site][:profile_enabled],
ipfs_archiving_enabled: params[:site][:ipfs_archiving_enabled]
profile_enabled: params[:site][:profile_enabled]
)
flash[:success] = 'Profile settings changed.'
redirect "/settings/#{@site.username}#profile"
@ -92,8 +98,8 @@ post '/settings/:username/change_name' do
}
old_site.delete_all_thumbnails_and_screenshots
old_site.delete_all_cache
@site.delete_all_cache
old_site.purge_all_cache
@site.purge_all_cache
@site.regenerate_thumbnails_and_screenshots
flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it!</b>"

View file

@ -38,14 +38,6 @@ get '/site/:username/?' do |username|
erb :'site', locals: {site: site, is_current_site: site == current_site}
end
get '/site/:username/archives' do
@site = Site[username: params[:username]]
not_found if @site.nil? || @site.is_banned || @site.is_deleted || !@site.ipfs_archiving_enabled
@title = "Site archives for #{@site.title}"
@archives = @site.archives_dataset.limit(300).order(:updated_at.desc).all
erb :'site/archives'
end
MAX_STAT_POINTS = 30
get '/site/:username/stats' do
@default_stat_points = 7
@ -295,4 +287,91 @@ get '/site/:username/unblock' do |username|
current_site.unblock! site
redirect request.referer
end
get '/site/:username/confirm_phone' do
require_login
redirect '/' unless current_site.phone_verification_needed?
@title = 'Verify your Phone Number'
erb :'site/confirm_phone'
end
def restart_phone_verification
current_site.phone_verification_sent_at = nil
current_site.phone_verification_sid = nil
current_site.save_changes validate: false
redirect "/site/#{current_site.username}/confirm_phone"
end
post '/site/:username/confirm_phone' do
require_login
redirect '/' unless current_site.phone_verification_needed?
if params[:phone_intl]
phone = Phonelib.parse params[:phone_intl]
if !phone.valid?
flash[:error] = "Invalid phone number, please try again."
redirect "/site/#{current_site.username}/confirm_phone"
end
if phone.types.include?(:premium_rate) || phone.types.include?(:shared_cost)
flash[:error] = 'Neocities does not support this type of number, please use another number.'
redirect "/site/#{current_site.username}/confirm_phone"
end
current_site.phone_verification_sent_at = Time.now
current_site.phone_verification_attempts += 1
if current_site.phone_verification_attempts > Site::PHONE_VERIFICATION_LOCKOUT_ATTEMPTS
flash[:error] = 'You have exceeded the number of phone verification attempts allowed.'
redirect "/site/#{current_site.username}/confirm_phone"
end
current_site.save_changes validate: false
verification = $twilio.verify
.v2
.services($config['twilio_service_sid'])
.verifications
.create(to: phone.e164, channel: 'sms')
current_site.phone_verification_sid = verification.sid
current_site.save_changes validate: false
flash[:success] = 'Validation message sent! Check your phone and enter the code below.'
else
restart_phone_verification if current_site.phone_verification_sent_at < Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME
minutes_remaining = ((current_site.phone_verification_sent_at - (Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME))/60).round
begin
# Check code
vc = $twilio.verify
.v2
.services($config['twilio_service_sid'])
.verification_checks
.create(verification_sid: current_site.phone_verification_sid, code: params[:code])
# puts vc.status (pending if failed, approved if it passed)
if vc.status == 'approved'
current_site.phone_verified = true
current_site.save_changes validate: false
else
flash[:error] = "Code was not correct, please try again. If the phone number you entered was incorrect, you can re-enter the number after #{minutes_remaining} more minutes have passed."
end
rescue Twilio::REST::RestError => e
if e.message =~ /60202/
flash[:error] = "You have exhausted your check attempts. Please try again in #{minutes_remaining} minutes."
elsif e.message =~ /20404/ # Unable to create record
restart_phone_verification
else
raise e
end
end
end
# Will redirect to / automagically if phone was verified
redirect "/site/#{current_site.username}/confirm_phone"
end

View file

@ -186,12 +186,20 @@ post '/site_files/rename' do
redirect "/dashboard#{dir_query}"
end
get '/site_files/:username.zip' do |username|
get '/site_files/download' do
require_login
if !current_site.dl_queued_at.nil? && current_site.dl_queued_at > 1.hour.ago
flash[:error] = 'Site downloads are currently limited to once per hour, please try again later.'
redirect request.referer
end
content_type 'application/zip'
attachment "neocities-#{current_site.username}.zip"
current_site.dl_queued_at = Time.now
current_site.save_changes validate: false
directory_path = current_site.files_path
stream do |out|
@ -200,7 +208,7 @@ get '/site_files/:username.zip' do |username|
next if File.directory?(file)
zip_path = file.sub("#{directory_path}/", '')
zip.write_deflated_file(zip_path) do |file_writer|
zip.write_stored_file(zip_path) do |file_writer|
File.open(file, 'rb') do |file|
IO.copy_stream(file, file_writer)
end

View file

@ -20,4 +20,9 @@ cache_control_ips:
- 1.2.3.4
- 4.5.6.7
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
twilio_account_sid: ACEDERPDERP
twilio_auth_token: derpderpderp
twilio_service_sid: VADERPDERPDERP
minfraud_account_id: 696969420
minfraud_license_key: DERPDERPDERP

View file

@ -18,6 +18,8 @@ development:
paypal_api_signature: tonz
letsencrypt_key: ./tests/files/letsencrypt.key
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
minfraud_account_id: 696969420
minfraud_license_key: DERPDERPDERP
proxy_ips:
- 10.0.0.1
- 10.0.0.2
@ -55,3 +57,8 @@ test:
cache_control_ips:
- 1.2.3.4
- 4.5.6.7
twilio_account_sid: ACEDERPDERP
twilio_auth_token: derpderpderp
twilio_service_sid: VADERPDERPDERP
minfraud_account_id: 696969420
minfraud_license_key: DERPDERPDERP

View file

@ -1,3 +1,4 @@
RubyVM::YJIT.enable
ENV['RACK_ENV'] ||= 'development'
ENV['TZ'] = 'UTC'
DIR_ROOT = File.expand_path File.dirname(__FILE__)
@ -177,3 +178,11 @@ $image_optim = ImageOptim.new pngout: false, svgo: false
Money.locale_backend = nil
Money.default_currency = Money::Currency.new("USD")
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
$twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token']
Minfraud.configure do |c|
c.account_id = $config['minfraud_account_id']
c.license_key = $config['minfraud_license_key']
c.enable_validation = true
end

View file

@ -1,49 +0,0 @@
class BitcoinValidator
class << self
def address_version
"00"
end
def p2sh_version
"05"
end
def valid_address?(address)
hex = decode_base58(address) rescue nil
return false unless hex && hex.bytesize == 50
return false unless [address_version, p2sh_version].include?(hex[0...2])
base58_checksum?(address)
end
def decode_base58(base58_val)
s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
s = '' if s == '00'
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
s
end
def base58_checksum?(base58)
hex = decode_base58(base58) rescue nil
return false unless hex
checksum( hex[0...42] ) == hex[-8..-1]
end
def checksum(hex)
b = [hex].pack("H*") # unpack hex
Digest::SHA256.hexdigest( Digest::SHA256.digest(b) )[0...8]
end
def base58_to_int(base58_val)
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
int_val, base = 0, alpha.size
base58_val.reverse.each_char.with_index do |char,index|
raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = alpha.index(char)
int_val += char_index*(base**index)
end
int_val
end
end
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.add_column :sites, :dl_queued_at, Time
}
down {
DB.drop_column :sites, :dl_queued_at
}
end

View file

@ -0,0 +1,15 @@
Sequel.migration do
up {
DB.add_column :sites, :phone_verification_required, :boolean, default: false
DB.add_column :sites, :phone_verified, :boolean, default: false
DB.add_column :sites, :phone_verification_sid, :text
DB.add_column :sites, :phone_verification_sent_at, :time
}
down {
DB.drop_column :sites, :phone_verification_required
DB.drop_column :sites, :phone_verified
DB.drop_column :sites, :phone_verification_sid
DB.drop_column :sites, :phone_verification_sent_at
}
end

View file

@ -0,0 +1,11 @@
Sequel.migration do
up {
DB.drop_column :sites, :phone_verification_sent_at
DB.add_column :sites, :phone_verification_sent_at, Time
}
down {
DB.drop_column :sites, :phone_verification_sent_at
DB.add_column :sites, :phone_verification_sent_at, :time
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.add_column :sites, :phone_verification_attempts, :integer, default: 0
}
down {
DB.drop_column :sites, :phone_verification_attempts
}
end

View file

@ -0,0 +1,17 @@
Sequel.migration do
up {
DB.drop_table :archives
DB.drop_column :sites, :ipfs_archiving_enabled
}
down {
DB.create_table! :archives do
Integer :site_id, index: true
String :ipfs_hash
DateTime :updated_at, index: true
unique [:site_id, :ipfs_hash]
end
DB.add_column :sites, :ipfs_archiving_enabled, :boolean, default: false
}
end

View file

@ -1,39 +0,0 @@
require 'base32'
class Archive < Sequel::Model
many_to_one :site
set_primary_key [:site_id, :ipfs_hash]
unrestrict_primary_key
MAXIMUM_ARCHIVES_PER_SITE = 5
ARCHIVE_WAIT_TIME = 1.minute
def before_destroy
unpin
super
end
def unpin
return nil
# Not ideal. An SoA version is in progress.
if ENV['RACK_ENV'] == 'production' && $config['ipfs_ssh_host'] && $config['ipfs_ssh_user']
rbox = Rye::Box.new $config['ipfs_ssh_host'], :user => $config['ipfs_ssh_user']
rbox.disable_safe_mode
begin
response = rbox.execute "ipfs pin rm #{ipfs_hash}"
output_array = response
rescue => e
return true if e.message =~ /indirect pins cannot be removed directly/
ensure
rbox.disconnect
end
else
line = Terrapin::CommandLine.new('ipfs', 'pin rm :ipfs_hash')
response = line.run ipfs_hash: ipfs_hash
output_array = response.to_s.split("\n")
end
end
def url
"https://#{ipfs_hash}.ipfs.neocitiesops.net"
end
end

View file

@ -45,11 +45,11 @@ class Site < Sequel::Model
}
VALID_EXTENSIONS = %w{
html htm txt text css js jpg jpeg png apng gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp less sass rss kml dae obj mtl scss webp avif xcf epub gltf bin webmanifest knowl atom opml rdf map gpg resolveHandle
html htm txt text css js jpg jpeg png apng gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp less sass rss kml dae obj mtl scss webp avif xcf epub gltf bin webmanifest knowl atom opml rdf map gpg resolveHandle pls
}
VALID_EDITABLE_EXTENSIONS = %w{
html htm txt js css scss md manifest less webmanifest xml json opml rdf svg gpg pgp resolveHandle
html htm txt js css scss md manifest less webmanifest xml json opml rdf svg gpg pgp resolveHandle pls
}
MINIMUM_PASSWORD_LENGTH = 5
@ -141,7 +141,7 @@ class Site < Sequel::Model
DISPOSABLE_EMAIL_BLACKLIST_PATH = File.join(DIR_ROOT, 'files', 'disposable_email_blacklist.conf')
BANNED_EMAIL_BLACKLIST_PATH = File.join(DIR_ROOT, 'files', 'banned_email_blacklist.conf')
BLOCK_JERK_THRESHOLD = 4
BLOCK_JERK_THRESHOLD = 25
MAXIMUM_TAGS = 5
MAX_USERNAME_LENGTH = 32.freeze
@ -168,6 +168,9 @@ class Site < Sequel::Model
BLACK_BOX_WAIT_TIME = 10.seconds
MAX_DISPLAY_FOLLOWS = 56*3
PHONE_VERIFICATION_EXPIRATION_TIME = 10.minutes
PHONE_VERIFICATION_LOCKOUT_ATTEMPTS = 3
many_to_many :tags
one_to_many :profile_comments
@ -204,8 +207,6 @@ class Site < Sequel::Model
one_to_many :stat_locations
one_to_many :stat_paths
one_to_many :archives
def self.supporter_ids
parent_supporters = DB[%{SELECT id FROM sites WHERE plan_type IS NOT NULL AND plan_type != 'free'}].all.collect {|s| s[:id]}
child_supporters = DB[%{select a.id as id from sites a, sites b where a.parent_site_id is not null and a.parent_site_id=b.id and (a.plan_type != 'free' or b.plan_type != 'free')}].all.collect {|s| s[:id]}
@ -502,6 +503,7 @@ class Site < Sequel::Model
def after_destroy
update_redis_proxy_record
purge_all_cache
end
def undelete!
@ -514,8 +516,8 @@ class Site < Sequel::Model
save_changes
}
delete_all_cache
update_redis_proxy_record
purge_all_cache
true
end
@ -541,8 +543,6 @@ class Site < Sequel::Model
self.banned_at = Time.now
save validate: false
destroy
delete_all_cache
end
def ban_all_sites_on_account!
@ -628,10 +628,23 @@ class Site < Sequel::Model
@blocking_site_ids ||= blockings_dataset.select(:site_id).all.collect {|s| s.site_id}
end
def unfollow_blocked_sites!
blockings.each do |blocking|
follows.each do |follow|
follow.destroy if follow.actioning_site_id == blocking.site_id
end
followings.each do |following|
following.destroy if following.site_id == blocking.site_id
end
end
end
def block!(site)
block = blockings_dataset.filter(site_id: site.id).first
return true if block
add_blocking site: site
unfollow_blocked_sites!
end
def unblock!(site)
@ -646,7 +659,7 @@ class Site < Sequel::Model
end
def self.valid_username?(username)
!username.empty? && username.match(/^[a-zA-Z0-9_\-]+$/i)
!username.empty? && username.match(/^[a-zA-Z0-9][a-zA-Z0-9_\-]+[a-zA-Z0-9]$/i)
end
def self.disposable_email_domains
@ -767,79 +780,12 @@ class Site < Sequel::Model
end
end
def delete_all_cache
def purge_all_cache
site_files.each do |site_file|
delete_cache site_file.path
purge_cache site_file.path
end
end
def delete_cache(path)
purge_cache path
end
#Rye::Cmd.add_command :ipfs
def add_to_ipfs
# Not ideal. An SoA version is in progress.
return nil
if archives_dataset.count > Archive::MAXIMUM_ARCHIVES_PER_SITE
archives_dataset.order(:updated_at).first.destroy
end
if $config['ipfs_ssh_host'] && $config['ipfs_ssh_user']
rbox = Rye::Box.new $config['ipfs_ssh_host'], user: $config['ipfs_ssh_user']
begin
cidv0 = rbox.ipfs(:add, :r, :Q, "sites/#{sharding_dir}/#{self.username.gsub(/\/|\.\./, '')}").first
cidv1b32 = rbox.ipfs(:cid, :base32, cidv0).first
ensure
rbox.disconnect
end
else
line = Terrapin::CommandLine.new('ipfs', 'add -r -Q :path')
response = line.run(path: files_path).strip
line = Terrapin::CommandLine.new('ipfs', 'cid base32 :hash')
cidv1b32 = line.run(hash: response).strip
end
cidv1b32
end
def purge_old_archives
archives_dataset.order(:updated_at).offset(Archive::MAXIMUM_ARCHIVES_PER_SITE).all.each do |archive|
archive.destroy
end
end
def archive!
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
begin
add_archive ipfs_hash: ipfs_hash, updated_at: Time.now
rescue Sequel::UniqueConstraintViolation
# Record already exists, update timestamp
archives_dataset.where(ipfs_hash: ipfs_hash).first.update updated_at: Time.now
end
end
add_redis_proxy_dnslink
end
def add_redis_proxy_dnslink
if host =~ /(.+)\.neocities\.org/ && latest_archive
$redis_proxy.hset "dns-#{host}", 'TXT', "dnslink=/ipfs/#{latest_archive.ipfs_hash}"
end
end
def latest_archive
@latest_archive ||= archives_dataset.order(:updated_at.desc).first
end
def is_directory?(path)
File.directory? files_path(path)
end
@ -881,33 +827,6 @@ class Site < Sequel::Model
true
end
def files_zip
zip_name = "neocities-#{username}"
tmpfile = Tempfile.new 'neocities-site-zip'
tmpfile.close
begin
Zip::Archive.open(tmpfile.path, Zip::CREATE) do |ar|
ar.add_dir(zip_name)
Dir.glob("#{base_files_path}/**/*").each do |path|
relative_path = path.gsub(base_files_path+'/', '')
if File.directory?(path)
ar.add_dir(zip_name+'/'+relative_path)
else
ar.add_file(zip_name+'/'+relative_path, path) # add_file(<entry name>, <source path>)
end
end
end
rescue => e
tmpfile.unlink
raise e
end
tmpfile.path
end
def move_files_from(oldusername)
FileUtils.mkdir_p self.class.sharding_base_path(username)
FileUtils.mkdir_p self.class.sharding_screenshots_path(username)
@ -987,11 +906,6 @@ class Site < Sequel::Model
parent_site_id.nil?
end
# def after_destroy
# FileUtils.rm_rf files_path
# super
# end
def ssl_installed?
!domain.blank? && !ssl_key.blank? && !ssl_cert.blank?
end
@ -1029,6 +943,11 @@ class Site < Sequel::Model
$redis_proxy.hset d_www_key, 'ssl_cert', ssl_cert
$redis_proxy.hset d_www_key, 'ssl_key', ssl_key
end
if is_deleted
$redis_proxy.del d_root_key
$redis_proxy.del d_www_key
end
else
$redis_proxy.hdel u_key, 'domain'
end
@ -1039,8 +958,6 @@ class Site < Sequel::Model
if is_deleted
$redis_proxy.del u_key
$redis_proxy.del d_root_key
$redis_proxy.del d_www_key
end
true
@ -1070,7 +987,7 @@ class Site < Sequel::Model
super
if !self.class.valid_username?(values[:username])
errors.add :username, 'Usernames can only contain letters, numbers, and hyphens.'
errors.add :username, 'Usernames can only contain letters, numbers, and hyphens, and cannot start or end with a hyphen.'
end
if !values[:username].blank?
@ -1120,7 +1037,7 @@ class Site < Sequel::Model
errors.add :tipping_paypal, 'A valid PayPal tipping email address is required.'
end
if !values[:tipping_bitcoin].blank? && !BitcoinValidator.valid_address?(values[:tipping_bitcoin])
if !values[:tipping_bitcoin].blank? && !AdequateCryptoAddress.valid?(values[:tipping_bitcoin], 'BTC')
errors.add :tipping_bitcoin, 'Bitcoin tipping address is not valid.'
end
@ -1572,6 +1489,10 @@ class Site < Sequel::Model
File.exist? File.join(base_screenshots_path, "#{path}.#{resolution}.webp")
end
def sharing_screenshot_url
'https://neocities.org'+base_screenshots_url+'/index.html.jpg'
end
def screenshot_url(path, resolution)
path[0] = '' if path[0] == '/'
out = ''
@ -1602,18 +1523,20 @@ class Site < Sequel::Model
end
def to_rss
RSS::Maker.make("atom") do |maker|
maker.channel.title = title
maker.channel.updated = (updated_at ? updated_at : created_at)
maker.channel.author = username
maker.channel.id = "#{username}.neocities.org"
RSS::Maker.make("2.0") do |m|
m.channel.title = title
m.channel.link = uri
m.channel.description = "Site feed for #{title}"
m.image.url = sharing_screenshot_url
m.image.title = title
latest_events.each do |event|
if event.site_change_id
maker.items.new_item do |item|
item.link = "https://#{host}"
item.title = "#{title} has been updated"
item.updated = event.site_change.created_at
m.items.new_item do |i|
i.title = "#{title} has been updated."
i.link = "https://neocities.org/site/#{username}?event_id=#{event.id.to_s}"
i.pubDate = event.created_at
end
end
end
@ -1712,10 +1635,6 @@ class Site < Sequel::Model
time,
self.id
].first
if ipfs_archiving_enabled == true
ArchiveWorker.perform_in Archive::ARCHIVE_WAIT_TIME, self.id
end
end
reload
@ -1790,6 +1709,11 @@ class Site < Sequel::Model
end
end
def phone_verification_needed?
return true if phone_verification_required && !phone_verified
false
end
private
def store_file(path, uploaded, opts={})

View file

@ -105,7 +105,7 @@ class SiteFile < Sequel::Model
DB['update sites set space_used=space_used-? where id=?', size, site_id].first
end
site.delete_cache site.files_path(path)
site.purge_cache site.files_path(path)
SiteChangeFile.filter(site_id: site_id, filename: path).delete
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

BIN
public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

70
public/img/plaincat.svg Normal file
View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 154 145"
width="154.0pt"
height="145.0pt"
id="svg93"
sodipodi:docname="plaincat.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs97" />
<sodipodi:namedview
id="namedview95"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="3.0181034"
inkscape:cx="205.59269"
inkscape:cy="114.8072"
inkscape:window-width="2490"
inkscape:window-height="1376"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg93" />
<path
d="m 112.83494,2.7962244 c 5.31573,0.7723715 8.39385,4.7591726 11.41519,8.7686886 5.72463,6.621952 9.90454,14.629628 13.80046,22.501003 4.67967,11.914969 11.11988,24.409214 12.68735,37.141988 1.71512,11.108521 2.95318,23.886729 -1.79463,34.438686 -3.63468,8.01904 -9.87046,12.18757 -15.77683,18.28703 -4.14582,3.78235 -9.30253,6.53108 -14.62962,8.21213 -6.29256,2.12403 -12.66463,4.15717 -19.127567,5.64513 -11.222101,1.88549 -21.637759,4.54336 -33.143824,2.74873 C 55.804377,138.65412 44.082501,136.08711 34.9617,130.41927 26.976741,123.54744 21.081729,114.92641 16.810968,105.33992 11.858702,98.27499 9.7346813,90.301385 6.9745879,82.236918 5.9296149,79.033847 4.430306,76.364621 4.0554777,72.957099 3.3739738,66.903069 2.7265442,60.826322 2.1472659,54.749574 2.1359072,45.617415 3.4421245,36.689709 5.3276193,27.773359 c 1.8400614,-0.601995 3.2939376,-1.385725 5.2362257,-0.522488 7.68964,3.509749 11.312973,10.801847 18.582352,14.675063 2.737376,1.601535 5.327093,3.316654 7.825942,5.270299 4.35027,-1.090405 8.382503,-2.862317 12.460173,-4.668304 C 59.030164,38.950031 68.90062,35.974128 78.725641,33.11181 84.64337,31.442124 90.629249,31.328541 96.717356,31.487559 100.05673,23.6389 103.24844,15.847033 107.1898,8.202825 c 1.36302,-2.1353791 2.74874,-5.247583 5.64514,-5.4066006 z"
fill="#000000"
id="path79"
style="stroke-width:1.13584" />
<path
d="m 114.47053,13.893387 c 8.18942,9.20031 14.28888,20.513282 18.52557,32.042065 2.77146,6.167613 5.31574,12.335228 7.22395,18.832236 -1.91957,2.044514 -3.81642,4.009517 -6.31527,5.338451 -1.59019,1.044973 -3.91866,1.919571 -4.49793,3.884576 -0.31804,1.953645 0.94274,3.305297 2.6465,4.009517 3.43025,-0.567922 6.74689,-1.942288 10.09763,-2.907752 0.19309,2.646508 0.49977,5.383883 0.32938,8.04175 -2.55563,2.964545 -7.82593,3.49839 -9.79094,6.746895 0.1931,4.622871 6.89456,3.70284 9.89318,2.532925 -0.11359,2.146738 -0.10221,4.372985 -0.53385,6.48565 -3.06677,9.16623 -9.46154,14.66369 -16.42425,20.87675 -3.89594,2.95318 -8.32572,4.49793 -12.94858,5.89501 4.14581,-3.94137 7.25802,-8.50744 7.86001,-14.34566 0.22717,-5.25894 0.38619,-10.54061 -4.60015,-13.6528 l -0.26125,-0.49978 -0.72692,-1.13584 c -3.05542,-2.41934 -6.5311,-4.532003 -10.2453,-5.747353 -3.37346,-0.942748 -7.178507,-0.670146 -10.65418,-0.726938 -0.726938,-4.759172 -1.544742,-9.450194 -3.112204,-14.016273 -1.385725,-3.907293 -1.226707,-8.973141 -3.487031,-12.437455 -2.192171,-1.999079 -5.679202,-2.816885 -8.450653,-3.736915 -3.884575,3.305296 -2.055871,10.25664 -0.795089,14.470609 2.703301,6.019956 6.315275,11.710516 9.711438,17.367003 -3.055412,2.737375 -6.91727,4.759172 -8.325713,8.734612 -1.431158,4.00952 -5.270299,8.4393 -4.248043,12.74413 0.511128,3.00999 1.124483,6.15627 2.373908,8.95042 2.555641,3.74828 5.985879,7.03087 9.427477,9.96134 -5.06585,0.24987 -10.120341,0.19308 -15.197548,0.19308 -5.997239,0.11359 -11.846818,-2.01044 -17.480587,-3.89593 2.907751,-2.04451 6.201689,-2.51021 7.223946,-6.20169 0.318035,-2.93047 -2.158098,-3.53246 -4.509287,-3.79371 -4.407063,0.6815 -9.064008,4.47521 -13.425637,4.69102 -3.509747,-1.73783 -6.292557,-5.27031 -9.086724,-7.96224 4.441136,-1.19262 8.734615,-2.62379 12.698698,-5.02041 1.499309,-0.88596 2.521566,-2.22626 3.248505,-3.78236 -0.193094,-3.20307 -3.21443,-4.05495 -5.929089,-3.60061 -5.065849,1.18129 -9.995398,3.02134 -14.993096,4.48657 C 26.874515,103.7611 26.13622,100.1832 24.352949,96.650738 22.56968,92.98197 20.50245,89.574447 19.355251,85.621722 17.265305,78.624944 14.823247,72.286953 14.153101,64.960781 13.142203,56.884955 12.88096,49.229388 13.414804,41.09677 c 5.145359,1.908212 9.086725,6.565159 13.61873,9.745513 2.498849,1.669685 5.33845,5.156716 8.586956,4.168535 7.825941,-1.306217 14.198007,-4.668305 21.739989,-6.724177 9.938605,-2.816885 20.058946,-4.838681 29.940757,-7.894093 4.85004,-1.70376 10.211207,-0.272602 14.902234,-1.98772 2.9759,-2.464776 4.30483,-6.656026 5.75871,-10.108982 2.05588,-4.838681 4.15716,-9.700079 6.50835,-14.402459 z"
fill="#f59c32"
id="path81"
style="stroke-width:1.13584" />
<path
d="m 78.998242,59.372446 c 2.771451,0.92003 6.258482,1.737836 8.450653,3.736915 2.260324,3.464314 2.101306,8.530162 3.487031,12.437455 1.567462,4.566079 2.385266,9.257101 3.112204,14.016273 3.475673,0.05679 7.28072,-0.21581 10.65418,0.726938 3.7142,1.21535 7.18988,3.328013 10.2453,5.747353 l 0.72692,1.13584 c -4.17989,1.10177 -8.75732,2.21488 -12.93722,0.4657 -3.452953,-1.101765 -5.917732,-3.770995 -8.655105,-6.019959 -1.238065,5.327096 -2.578359,11.029019 -6.12218,15.436079 -3.100846,3.56653 -9.631929,1.59017 -12.096702,5.33844 0.954105,1.19264 1.828703,2.57837 2.998618,3.56655 3.225788,0.42026 6.769611,-0.43162 10.029473,-0.62472 2.317114,3.33937 5.417959,5.98589 9.166239,7.57607 4.543357,1.8287 10.267997,-0.36348 12.539677,-4.66832 1.27213,-3.91865 0.28395,-8.14397 -0.90867,-11.94904 3.82777,-2.11266 6.86047,-3.54383 6.24712,-8.62102 4.98634,3.11219 4.82732,8.39386 4.60015,13.6528 -0.60199,5.83822 -3.7142,10.40429 -7.86001,14.34566 -8.12126,2.57836 -16.99218,5.98589 -25.5337,5.9291 -3.441598,-2.93047 -6.871836,-6.21306 -9.427477,-9.96134 -1.249425,-2.79415 -1.86278,-5.94043 -2.373908,-8.95042 -1.022256,-4.30483 2.816885,-8.73461 4.248043,-12.74413 1.408443,-3.97544 5.270301,-5.997237 8.325713,-8.734612 C 84.518428,85.553571 80.906454,79.863011 78.203153,73.843055 76.942371,69.629086 75.113667,62.677742 78.998242,59.372446 Z"
fill="#ffffff"
id="path83"
style="stroke-width:1.13584" />
<path
d="m 99.625105,68.833998 c 5.156715,-1.783269 7.905455,4.395703 7.178515,8.450654 -1.5561,5.111283 -10.461091,5.190792 -11.721875,-0.181735 -1.20399,-3.941367 0.885956,-6.951344 4.54336,-8.268919 z"
fill="#000000"
id="path85"
style="stroke-width:1.13584" />
<path
d="m 67.957871,78.272834 c 1.987722,-0.556563 4.327553,-0.715581 6.201689,0.283959 3.657407,1.476593 5.020416,7.314814 2.158099,10.120341 -2.998621,3.782348 -9.098084,3.589256 -11.540141,-0.726938 -1.033616,-3.464314 -0.36347,-7.939526 3.180353,-9.677362 z"
fill="#000000"
id="path87"
style="stroke-width:1.13584" />
<path
d="m 94.082205,91.618961 c 2.737373,2.248964 5.202152,4.918194 8.655105,6.019959 4.1799,1.74918 8.75733,0.63607 12.93722,-0.4657 l 0.26125,0.49978 c 0.61335,5.07719 -2.41935,6.50836 -6.24712,8.62102 1.19262,3.80507 2.1808,8.03039 0.90867,11.94904 -2.27168,4.30484 -7.99632,6.49702 -12.539677,4.66832 -3.74828,-1.59018 -6.849125,-4.2367 -9.166239,-7.57607 -3.259862,0.1931 -6.803685,1.04498 -10.029473,0.62472 -1.169915,-0.98818 -2.044513,-2.37391 -2.998618,-3.56655 2.464773,-3.74827 8.995856,-1.77191 12.096702,-5.33844 3.543821,-4.40706 4.884115,-10.108983 6.12218,-15.436079 z"
fill="#000000"
id="path89"
style="stroke-width:1.13584" />
<path
d="m 99.238919,103.39763 c 1.999081,1.0904 4.225331,1.81734 5.213511,4.03224 1.49931,3.07812 0.88596,7.92816 -2.94183,8.9277 -3.078124,0.28397 -5.508823,-3.18035 -7.632845,-4.98633 2.055871,-2.49887 3.691476,-5.22488 5.361164,-7.97361 z"
fill="#ea1d3c"
id="path91"
style="stroke-width:1.13584" />
</svg>

After

Width:  |  Height:  |  Size: 8 KiB

File diff suppressed because one or more lines are too long

20
public/js/chart.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,14 @@
require 'facter'
threads 1, 1
#threads 5, 5
#threads 1, 1
environment 'production'
#daemonize
pidfile '/var/run/neocities/neocities.pid'
stdout_redirect '/var/log/neocities/neocities.stdout.log', '/var/log/neocities/neocities.stderr.log', true
quiet
workers Facter.value('processors')['count']
preload_app!
#preload_app!
prune_bundler
on_worker_boot { DB.disconnect }
bind 'unix:/var/run/neocities/neocities.sock?backlog=2048'
supported_http_methods Puma::Const::IANA_HTTP_METHODS

View file

@ -2,6 +2,8 @@ require_relative './environment.rb'
describe '/admin' do
include Capybara::DSL
include Capybara::Minitest::Assertions
before do
Capybara.reset_sessions!

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe '/browse' do
include Capybara::DSL
include Capybara::Minitest::Assertions
=begin
describe 'as admin' do

View file

@ -4,8 +4,8 @@ describe 'dashboard' do
describe 'create directory' do
describe 'logged in' do
include Capybara::DSL
include Capybara::Minitest::Assertions
before do
Capybara.reset_sessions!

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe 'signup' do
include Capybara::DSL
include Capybara::Minitest::Assertions
def fill_in_valid
@site = Fabricate.attributes_for(:site)
@ -13,7 +14,7 @@ describe 'signup' do
end
before do
Capybara.default_driver = :apparition
Capybara.default_driver = :selenium_chrome_headless
Capybara.reset_sessions!
visit '/education'
_(page).must_have_content 'Neocities' # Used to force load wait

View file

@ -1,38 +1,17 @@
require_relative '../environment'
require 'capybara'
require 'capybara/minitest'
require 'capybara/minitest/spec'
require 'rack_session_access/capybara'
require 'capybara/apparition'
Capybara.app = Sinatra::Application
include Capybara::Minitest::Assertions
Capybara.default_max_wait_time = 5
#Capybara.register_driver :apparition do |app|
# Capybara::Apparition::Driver.new(app, headless: false)
#end
Capybara.register_driver :selenium_chrome_headless_largewindow do |app|
options = ::Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')
options.add_argument('--window-size=1280,800') # Set your desired window size
=begin
def setup
Capybara.current_driver = :apparition
end
def teardown
Capybara.reset_sessions!
Capybara.use_default_driver
end
=end
=begin
require 'capybara'
require 'capybara/dsl'
require 'capybara/poltergeist'
require 'rack_session_access/capybara'
Capybara.app = Sinatra::Application
def teardown
Capybara.reset_sessions!
end
=end
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe '/' do
include Capybara::DSL
include Capybara::Minitest::Assertions
describe 'news feed' do
before do
@ -21,7 +22,8 @@ describe '/' do
@another_site = Fabricate :site
@followed_site.toggle_follow @another_site
visit '/'
_(find('.news-item', match: :first).text).must_match /#{@followed_site.username} followed #{@another_site.username}/i
_(page).must_have_link(@followed_site.title, href: "/site/#{@followed_site.username}")
#_(find('.news-item', match: :first).text).must_match /#{@followed_site.username} followed #{@another_site.username}/i
end
it 'loads my activities only' do
@ -30,7 +32,7 @@ describe '/' do
@another_site = Fabricate :site
@followed_site.toggle_follow @another_site
visit '/?activity=mine'
_(find('.news-item').text).must_match //i
_(page).must_have_link(@followed_site.title, href: "/site/#{@followed_site.username}")
end
it 'loads a specific event with the id' do

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe '/password_reset' do
include Capybara::DSL
include Capybara::Minitest::Assertions
before do
Capybara.reset_sessions!
@ -67,7 +68,7 @@ describe '/password_reset' do
fill_in 'email', with: @site.email
click_button 'Send Reset Token'
_(body).must_match /send an e-mail to your account with password reset instructions/
_(body).must_match /We sent an e-mail with password reset instructions/
_(@site.reload.password_reset_token.blank?).must_equal false
_(EmailWorker.jobs.first['args'].first['body']).must_match /#{Rack::Utils.build_query(username: @site.username, token: @site.password_reset_token)}/

View file

@ -1,9 +1,10 @@
require_relative '../environment.rb'
describe 'site/settings' do
describe 'email' do
include Capybara::DSL
include Capybara::DSL
include Capybara::Minitest::Assertions
describe 'email' do
before do
EmailWorker.jobs.clear
@email = "#{SecureRandom.uuid.gsub('-', '')}@exampleedsdfdsf.com"
@ -84,8 +85,6 @@ describe 'site/settings' do
end
describe 'unsubscribe email' do
include Capybara::DSL
before do
@email = "#{SecureRandom.uuid.gsub('-', '')}@exampleedsdfdsf.com"
@site = Fabricate :site, email: @email
@ -127,8 +126,6 @@ describe 'site/settings' do
end
describe 'change password' do
include Capybara::DSL
before do
EmailWorker.jobs.clear
@site = Fabricate :site, password: 'derpie'

View file

@ -1,88 +1,10 @@
require_relative '../environment.rb'
def generate_ssl_certs(opts={})
# https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb
res = {}
ca_keypair = OpenSSL::PKey::RSA.new(2048)
ca_cert = OpenSSL::X509::Certificate.new
ca_cert.not_before = Time.now
ca_cert.subject = OpenSSL::X509::Name.new([
["C", "US"],
["ST", "Oregon"],
["L", "Portland"],
["CN", "Neocities CA"]
])
ca_cert.issuer = ca_cert.subject
ca_cert.not_after = Time.now + 1000000000 # 40 or so years
ca_cert.serial = 1
ca_cert.public_key = ca_keypair.public_key
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = ca_cert
ef.issuer_certificate = ca_cert
# Read more about the various extensions here: http://www.openssl.org/docs/apps/x509v3_config.html
ca_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
ca_cert.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true))
ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new)
res[:ca_cert] = ca_cert
res[:ca_keypair] = ca_keypair
ca_cert = OpenSSL::X509::Certificate.new(res[:ca_cert].to_pem)
our_cert_keypair = OpenSSL::PKey::RSA.new(2048)
our_cert_req = OpenSSL::X509::Request.new
our_cert_req.subject = OpenSSL::X509::Name.new([
["C", "US"],
["ST", "Oregon"],
["L", "Portland"],
["O", "Neocities User"],
["CN", "*.#{opts[:domain]}"]
])
our_cert_req.public_key = our_cert_keypair.public_key
our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new
our_cert = OpenSSL::X509::Certificate.new
our_cert.subject = our_cert_req.subject
our_cert.issuer = ca_cert.subject
our_cert.not_before = Time.now
if opts[:expired]
our_cert.not_after = Time.now - 100000000
else
our_cert.not_after = Time.now + 100000000
end
our_cert.serial = 123 # Should be an unique number, the CA probably has a database.
our_cert.public_key = our_cert_req.public_key
# To make the certificate valid for both wildcard and top level domain name, we need an extension.
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = our_cert
ef.issuer_certificate = ca_cert
our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false))
our_cert.sign res[:ca_keypair], OpenSSL::Digest::SHA1.new
our_cert_tmpfile = Tempfile.new 'our_cert'
our_cert_tmpfile.write our_cert.to_pem
our_cert_tmpfile.close
res[:cert_path] = our_cert_tmpfile.path
res[:key_path] = '/tmp/nc_test_our_cert_keypair'
File.write res[:key_path], our_cert_keypair.to_pem
res[:cert_intermediate_path] = '/tmp/nc_test_ca_cert'
File.write res[:cert_intermediate_path], res[:ca_cert].to_pem
res[:combined_cert_path] = '/tmp/nc_test_combined_cert'
File.write res[:combined_cert_path], "#{File.read(res[:cert_path])}\n#{File.read(res[:cert_intermediate_path])}"
res[:bad_combined_cert_path] = '/tmp/nc_test_bad_combined_cert'
File.write res[:bad_combined_cert_path], "#{File.read(res[:cert_intermediate_path])}\n#{File.read(res[:cert_path])}"
res
end
describe 'site/settings' do
describe 'permissions' do
include Capybara::DSL
include Capybara::DSL
include Capybara::Minitest::Assertions
describe 'permissions' do
before do
@parent_site = Fabricate :site
@child_site = Fabricate :site, parent_site_id: @parent_site.id
@ -104,8 +26,6 @@ describe 'site/settings' do
end
describe 'changing username' do
include Capybara::DSL
before do
Capybara.reset_sessions!
@site = Fabricate :site
@ -143,142 +63,138 @@ describe 'site/settings' do
_(page).must_have_content /You already have this name/
end
end
end
describe 'api key' do
include Capybara::DSL
describe 'api key' do
before do
Capybara.reset_sessions!
@site = Fabricate :site
@child_site = Fabricate :site, parent_site_id: @site.id
page.set_rack_session id: @site.id
end
before do
Capybara.reset_sessions!
@site = Fabricate :site
@child_site = Fabricate :site, parent_site_id: @site.id
page.set_rack_session id: @site.id
it 'sets api key' do
visit "/settings/#{@child_site[:username]}#api_key"
_(@site.api_key).must_be_nil
_(@child_site.api_key).must_be_nil
click_button 'Generate API Key'
_(@site.reload.api_key).must_be_nil
_(@child_site.reload.api_key).wont_be_nil
_(page.body).must_match @child_site.api_key
end
it 'regenerates api key for child site' do
visit "/settings/#{@child_site[:username]}#api_key"
@child_site.generate_api_key!
api_key = @child_site.api_key
click_button 'Generate API Key'
_(@child_site.reload.api_key).wont_equal api_key
end
end
it 'sets api key' do
visit "/settings/#{@child_site[:username]}#api_key"
_(@site.api_key).must_be_nil
_(@child_site.api_key).must_be_nil
click_button 'Generate API Key'
_(@site.reload.api_key).must_be_nil
_(@child_site.reload.api_key).wont_be_nil
_(page.body).must_match @child_site.api_key
describe 'delete' do
before do
Capybara.reset_sessions!
@site = Fabricate :site
page.set_rack_session id: @site.id
visit "/settings/#{@site[:username]}#delete"
end
it 'fails for incorrect entered username' do
fill_in 'username', with: 'NOPE'
click_button 'Delete Site'
_(page.body).must_match /Site user name and entered user name did not match/i
_(@site.reload.is_deleted).must_equal false
end
it 'succeeds' do
deleted_reason = 'Penelope left a hairball on my site'
fill_in 'confirm_username', with: @site.username
fill_in 'deleted_reason', with: deleted_reason
click_button 'Delete Site'
@site.reload
_(@site.is_deleted).must_equal true
_(@site.deleted_reason).must_equal deleted_reason
_(page.current_path).must_equal '/'
_(File.exist?(@site.files_path('./index.html'))).must_equal false
_(Dir.exist?(@site.files_path)).must_equal false
path = File.join Site::DELETED_SITES_ROOT, Site.sharding_dir(@site.username), @site.username
_(Dir.exist?(path)).must_equal true
_(File.exist?(File.join(path, 'index.html'))).must_equal true
visit "/site/#{@site.username}"
_(page.status_code).must_equal 404
end
it 'stops charging for supporter account' do
customer = Stripe::Customer.create(
source: $stripe_helper.generate_card_token
)
subscription = customer.subscriptions.create plan: 'supporter'
@site.update(
stripe_customer_id: customer.id,
stripe_subscription_id: subscription.id,
plan_type: 'supporter'
)
@site.plan_type = subscription.plan.id
@site.save_changes
fill_in 'confirm_username', with: @site.username
fill_in 'deleted_reason', with: 'derp'
click_button 'Delete Site'
_(Stripe::Customer.retrieve(@site.stripe_customer_id).subscriptions.count).must_equal 0
@site.reload
_(@site.stripe_subscription_id).must_be_nil
_(@site.is_deleted).must_equal true
end
it 'should fail unless owned by current user' do
someone_elses_site = Fabricate :site
page.set_rack_session id: @site.id
page.driver.post "/settings/#{someone_elses_site.username}/delete", {
username: someone_elses_site.username,
deleted_reason: 'Dade Murphy enters Acid Burns turf'
}
_(page.driver.status_code).must_equal 302
_(URI.parse(page.driver.response_headers['Location']).path).must_equal '/'
someone_elses_site.reload
_(someone_elses_site.is_deleted).must_equal false
end
it 'should not show NSFW tab for admin NSFW flag' do
owned_site = Fabricate :site, parent_site_id: @site.id, admin_nsfw: true
visit "/settings/#{owned_site.username}"
_(page.body).wont_match /18\+/
end
it 'should succeed if you own the site' do
owned_site = Fabricate :site, parent_site_id: @site.id
visit "/settings/#{owned_site.username}#delete"
fill_in 'confirm_username', with: owned_site.username
click_button 'Delete Site'
@site.reload
owned_site.reload
_(owned_site.is_deleted).must_equal true
_(@site.is_deleted).must_equal false
_(page.current_path).must_equal "/settings"
end
it 'fails to delete parent site if children exist' do
owned_site = Fabricate :site, parent_site_id: @site.id
visit "/settings/#{@site.username}#delete"
_(page.body).must_match /You cannot delete the parent site without deleting the children sites first/i
end
end
it 'regenerates api key for child site' do
visit "/settings/#{@child_site[:username]}#api_key"
@child_site.generate_api_key!
api_key = @child_site.api_key
click_button 'Generate API Key'
_(@child_site.reload.api_key).wont_equal api_key
end
end
describe 'delete' do
include Capybara::DSL
before do
Capybara.reset_sessions!
@site = Fabricate :site
page.set_rack_session id: @site.id
visit "/settings/#{@site[:username]}#delete"
end
it 'fails for incorrect entered username' do
fill_in 'username', with: 'NOPE'
click_button 'Delete Site'
_(page.body).must_match /Site user name and entered user name did not match/i
_(@site.reload.is_deleted).must_equal false
end
it 'succeeds' do
deleted_reason = 'Penelope left a hairball on my site'
fill_in 'confirm_username', with: @site.username
fill_in 'deleted_reason', with: deleted_reason
click_button 'Delete Site'
@site.reload
_(@site.is_deleted).must_equal true
_(@site.deleted_reason).must_equal deleted_reason
_(page.current_path).must_equal '/'
_(File.exist?(@site.files_path('./index.html'))).must_equal false
_(Dir.exist?(@site.files_path)).must_equal false
path = File.join Site::DELETED_SITES_ROOT, Site.sharding_dir(@site.username), @site.username
_(Dir.exist?(path)).must_equal true
_(File.exist?(File.join(path, 'index.html'))).must_equal true
visit "/site/#{@site.username}"
_(page.status_code).must_equal 404
end
it 'stops charging for supporter account' do
customer = Stripe::Customer.create(
source: $stripe_helper.generate_card_token
)
subscription = customer.subscriptions.create plan: 'supporter'
@site.update(
stripe_customer_id: customer.id,
stripe_subscription_id: subscription.id,
plan_type: 'supporter'
)
@site.plan_type = subscription.plan.id
@site.save_changes
fill_in 'confirm_username', with: @site.username
fill_in 'deleted_reason', with: 'derp'
click_button 'Delete Site'
_(Stripe::Customer.retrieve(@site.stripe_customer_id).subscriptions.count).must_equal 0
@site.reload
_(@site.stripe_subscription_id).must_be_nil
_(@site.is_deleted).must_equal true
end
it 'should fail unless owned by current user' do
someone_elses_site = Fabricate :site
page.set_rack_session id: @site.id
page.driver.post "/settings/#{someone_elses_site.username}/delete", {
username: someone_elses_site.username,
deleted_reason: 'Dade Murphy enters Acid Burns turf'
}
_(page.driver.status_code).must_equal 302
_(URI.parse(page.driver.response_headers['Location']).path).must_equal '/'
someone_elses_site.reload
_(someone_elses_site.is_deleted).must_equal false
end
it 'should not show NSFW tab for admin NSFW flag' do
owned_site = Fabricate :site, parent_site_id: @site.id, admin_nsfw: true
visit "/settings/#{owned_site.username}"
_(page.body).wont_match /18\+/
end
it 'should succeed if you own the site' do
owned_site = Fabricate :site, parent_site_id: @site.id
visit "/settings/#{owned_site.username}#delete"
fill_in 'confirm_username', with: owned_site.username
click_button 'Delete Site'
@site.reload
owned_site.reload
_(owned_site.is_deleted).must_equal true
_(@site.is_deleted).must_equal false
_(page.current_path).must_equal "/settings"
end
it 'fails to delete parent site if children exist' do
owned_site = Fabricate :site, parent_site_id: @site.id
visit "/settings/#{@site.username}#delete"
_(page.body).must_match /You cannot delete the parent site without deleting the children sites first/i
end
end
end

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe 'signin' do
include Capybara::DSL
include Capybara::Minitest::Assertions
def fill_in_valid
@site = Fabricate.attributes_for :site

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe 'signup' do
include Capybara::DSL
include Capybara::Minitest::Assertions
def fill_in_valid
@site = Fabricate.attributes_for(:site)
@ -24,7 +25,7 @@ describe 'signup' do
end
before do
Capybara.default_driver = :apparition
Capybara.default_driver = :selenium_chrome_headless_largewindow
Capybara.reset_sessions!
visit_signup
end
@ -39,7 +40,6 @@ describe 'signup' do
fill_in_valid
click_signup_button
site_created?
click_link 'Continue'
_(page).must_have_content /almost ready!/
fill_in 'token', with: Site[username: @site[:username]].email_confirmation_token
@ -109,10 +109,10 @@ describe 'signup' do
_(page).must_have_content 'Usernames can only contain'
fill_in 'username', with: 'nope-'
click_signup_button
_(page).must_have_content 'A valid user/site name is required'
_(page).must_have_content 'Usernames can only contain'
fill_in 'username', with: '-nope'
click_signup_button
_(page).must_have_content 'A valid user/site name is required'
_(page).must_have_content 'Usernames can only contain'
end
it 'fails with username greater than 32 characters' do

View file

@ -2,6 +2,7 @@ require_relative './environment.rb'
describe 'site page' do
include Capybara::DSL
include Capybara::Minitest::Assertions
after do
Capybara.default_driver = :rack_test
@ -103,6 +104,35 @@ describe 'site page' do
visit "/browse?tag=#{@tag}"
_(page.find('.website-Gallery .username a')['href']).must_match /\/site\/#{@blocked_site.username}/
end
it 'removes follows/followings when blocking' do
site = Fabricate :site
not_blocked_site = Fabricate :site
blocked_site = Fabricate :site
site.add_follow actioning_site: not_blocked_site
site.add_following site: not_blocked_site
site.add_follow actioning_site: blocked_site
site.add_following site: blocked_site
_(site.follows.count).must_equal 2
_(site.followings.count).must_equal 2
page.set_rack_session id: site.id
visit "/site/#{blocked_site.username}"
click_link 'Block'
click_button 'Block Site'
_(site.follows.count).must_equal 1
_(site.followings.count).must_equal 1
_(site.follows.count {|s| s.actioning_site == blocked_site}).must_equal 0
_(site.followings.count {|s| s.site == blocked_site}).must_equal 0
end
end
it '404s if site is banned' do

View file

@ -2,9 +2,10 @@ require_relative './environment.rb'
describe '/supporter' do
include Capybara::DSL
include Capybara::Minitest::Assertions
before do
Capybara.default_driver = :apparition
Capybara.default_driver = :selenium_chrome_headless
Capybara.reset_sessions!
@site = Fabricate :site

View file

@ -95,7 +95,6 @@ describe 'api' do
it 'succeeds for valid sitename' do
create_site
@site.update hits: 31337, domain: 'derp.com', new_tags_string: 'derpie, man'
@site.add_archive ipfs_hash: 'QmXGTaGWTT1uUtfSb2sBAvArMEVLK4rQEcQg5bv7wwdzwU'
get '/api/info', sitename: @user
_(res[:result]).must_equal 'success'
_(res[:info][:sitename]).must_equal @site.username
@ -104,16 +103,9 @@ describe 'api' do
_(res[:info][:last_updated]).must_be_nil
_(res[:info][:domain]).must_equal 'derp.com'
_(res[:info][:tags]).must_equal ['derpie', 'man']
_(res[:info][:latest_ipfs_hash]).must_equal 'QmXGTaGWTT1uUtfSb2sBAvArMEVLK4rQEcQg5bv7wwdzwU'
_(@site.reload.api_calls).must_equal 0
end
it 'shows latest ipfs hash as nil when not present' do
create_site
get '/api/info', sitename: @user
_(res[:info][:latest_ipfs_hash]).must_be_nil
end
it 'fails for bad auth' do
basic_authorize 'derp', 'fake'
get '/api/info'

View file

@ -48,7 +48,7 @@ end
Site.bcrypt_cost = BCrypt::Engine::MIN_COST
MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
# Bootstrap the database
Sequel.extension :migration
@ -80,4 +80,4 @@ StripeMock.start
product = $stripe_helper.create_product name: 'supporter'
$stripe_helper.create_plan id: 'supporter', amount: 500, product: product.id
$stripe_helper.create_plan id: 'free', amount: 0, product: product.id
$stripe_helper.create_plan id: 'special', amount: 0, product: product.id
$stripe_helper.create_plan id: 'special', amount: 0, product: product.id

View file

@ -1,29 +0,0 @@
require_relative '../environment.rb'
describe ArchiveWorker do
it 'stores an IPFS archive' do
return if ENV['CI']
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).wont_be_nil
_(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

@ -21,3 +21,9 @@ sed -i 's|UsePAM yes|UsePAM no|g' /etc/ssh/sshd_config
#sed -i 's|[#]*PermitRootLogin yes|PermitRootLogin no|g' /etc/ssh/sshd_config
service ssh restart
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
sudo apt-get install -f -y
rm google-chrome-stable_current_amd64.deb

View file

@ -12,10 +12,10 @@ sudo su postgres -c "createuser -d vagrant"
sudo su vagrant -c "createdb neocities"
sudo su vagrant -c "createdb neocities_test"
sudo sh -c 'echo "local all postgres trust" > /etc/postgresql/10/main/pg_hba.conf'
sudo sh -c 'echo "local all all trust" >> /etc/postgresql/10/main/pg_hba.conf'
sudo sh -c 'echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/10/main/pg_hba.conf'
sudo sh -c 'echo "host all all ::1/128 trust" >> /etc/postgresql/10/main/pg_hba.conf'
sudo sh -c 'echo "local all postgres trust" > /etc/postgresql/14/main/pg_hba.conf'
sudo sh -c 'echo "local all all trust" >> /etc/postgresql/14/main/pg_hba.conf'
sudo sh -c 'echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/14/main/pg_hba.conf'
sudo sh -c 'echo "host all all ::1/128 trust" >> /etc/postgresql/14/main/pg_hba.conf'
sudo systemctl restart postgresql
# Create empty file for disposable email accounts

View file

@ -1,7 +1,11 @@
#!/bin/bash
apt-get -y install python-software-properties
apt-add-repository -y ppa:brightbox/ruby-ng
apt-get -y update
apt-get -y install ruby2.6 ruby2.6-dev
gem install bundler --no-document
sudo apt-get -y install autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev
wget https://cache.ruby-lang.org/pub/ruby/3.3/ruby-3.3.0.tar.gz
gzip -dc ruby-3.3.0.tar.gz | tar xf -
cd ruby-3.3.0
./autogen.sh
./configure --enable-yjit --disable-install-doc
make -j && sudo make install
cd ..

View file

@ -29,10 +29,15 @@ apt-get install -y \
sed -i 's|[#]*DetectPUA false|DetectPUA true|g' /etc/clamav/clamd.conf
freshclam
service clamav-freshclam start
service clamav-daemon start
#sudo freshclam
#sudo systemctl start clamav-freshclam
# clamav download mirrors have insanely stupid limits so we just put in a github mirror for now
rm -f main.cvd daily.cld daily.cvd bytecode.cvd main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 main.cvd.* daily.cvd.* && curl -LSOs https://github.com/ladar/clamav-data/raw/main/main.cvd.[01-10] -LSOs https://github.com/ladar/clamav-data/raw/main/main.cvd.sha256 -LSOs https://github.com/ladar/clamav-data/raw/main/daily.cvd.[01-10] -LSOs https://github.com/ladar/clamav-data/raw/main/daily.cvd.sha256 -LSOs https://github.com/ladar/clamav-data/raw/main/bytecode.cvd -LSOs https://github.com/ladar/clamav-data/raw/main/bytecode.cvd.sha256 && cat main.cvd.01 main.cvd.02 main.cvd.03 main.cvd.04 main.cvd.05 main.cvd.06 main.cvd.07 main.cvd.08 main.cvd.09 main.cvd.10 > main.cvd && cat daily.cvd.01 daily.cvd.02 daily.cvd.03 daily.cvd.04 daily.cvd.05 daily.cvd.06 daily.cvd.07 daily.cvd.08 daily.cvd.09 daily.cvd.10 > daily.cvd && sha256sum -c main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 || { printf "ClamAV database download failed.\n" ; rm -f main.cvd daily.cvd bytecode.cvd ; } ; rm -f main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 main.cvd.* daily.cvd.* && sudo mv *.cvd /var/lib/clamav/
sudo systemctl enable clamav-daemon
sudo systemctl start clamav-daemon
usermod -G vagrant clamav
cd /vagrant

View file

@ -41,8 +41,9 @@
</li>
<li class="divider"></li>
<% end %>
<li><a href="/dashboard">Edit Site</a></li>
<li><a href="<%= current_site.uri %>" target="_blank">View Site</a></li>
<li><a href="/dashboard">Edit</a></li>
<li><a href="<%= current_site.uri %>" target="_blank">View</a></li>
<li><a href="/site/<%= current_site.username %>/stats">Stats</a></li>
<li class="divider"></li>
<li><a href="/settings">Settings</a></li>

13
views/_meta.erb Normal file
View file

@ -0,0 +1,13 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="description" content="Create and surf awesome websites for free.">
<link rel="canonical" href="https://neocities.org<%= request.fullpath %>">
<meta property="og:title" content="Neocities">
<meta property="og:type" content="website">
<meta property="og:image" content="https://neocities.org/img/neocities-front-screenshot.jpg">
<meta property="og:description" content="Create and surf awesome websites for free.">
<link rel="icon" type="image/x-icon" href="/img/favicon.png">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1">

View file

@ -3,7 +3,7 @@
page_uri = site.uri
end
%>
<a href="/site/<%= site.username %>.rss" target="_blank"><span>RSS/Atom Feed</span></a>
<a href="/site/<%= site.username %>.rss" target="_blank"><span>RSS Feed</span></a>
<br>
<a href="https://facebook.com/sharer.php?<%= Rack::Utils.build_query(u: "#{page_uri}") %>" target="_blank">Facebook</a>
<br>

View file

@ -1,26 +0,0 @@
<hr />
<section>
<h1 class="beta txt-Center">The Neocities Team</h1>
<div class="row txt-Center">
<div class="col col-50">
<a href="https://kyledrake.neocities.org" title="Visit Kyle's Website">
<img src="https://0.gravatar.com/avatar/62a43048a3c2c688654274abdc0ecb9c?d=https%3A%2F%2Fidenticons.github.com%2Ffde07ba82b25f95afa9d080819f95717.png&amp;r=x&amp;s=440" alt="kyle drake" class="pic-Rounded" />
</a>
<br />
<a href="https://kyledrake.neocities.org" title="Kyle Drake" class="eps">Kyle Drake</a>
</div>
<div class="col col-50">
<a href="https://victoria.neocities.org" title="Visit Victoria's Website">
<img src="https://1.gravatar.com/avatar/2b577f8b3e5ab79bc927ed5185c0eae0?d=https%3A%2F%2Fidenticons.github.com%2Fe03006819f4a835afa237716f6701c95.png&amp;r=x&amp;s=440" alt="Victoria Wang" class="pic-Rounded" />
</a>
<br />
<a href="https://victoria.neocities.org" title="Visit Victoria's Website" class="eps">Victoria Wang</a>
</div>
</div>
</section>
<hr />
<section>
<h2 class="txt-Center">Follow us on <a href="https://twitter.com/neocities">Twitter</a> or <a href="https://www.facebook.com/neocities">Facebook</a></h2>
</section>

View file

@ -13,15 +13,51 @@
</div>
<% end %>
<h2>Contact</h2>
<p>
Please note that we can only respond to english inquiries.
Please note that we can only respond to messages in english and klingon. Thank you!
</p>
<p>
Frequently asked questions:
</p>
<h2>Frequently Asked Questions</h2>
<h4>Sites / Editing</h3>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cemailresetnotfound">
I didn't receive my password reset email.
</a>
</div>
<div id="cemailresetnotfound" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Did you use a different email address to create the site? Also, check the spam folder of your email.
</p>
<p>
This is also a great opportunity to talk about password managers. A password manager keeps a database of randomized strong passwords for all of your sites for you, so you only need to remember
one password which then controls all of the other ones. This reduces the chances of forgetting your password and having to reset it. Popular options include <a href="https://bitwarden.com/">BitWarden</a> and <a href="https://1password.com/">1Password</a>.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#ctwo">
I lost my email address, can you reset the password?
</a>
</div>
<div id="ctwo" class="accordion-body collapse">
<div class="accordion-inner">
<p>
For security reasons, we cannot reset your password if you don't have control of your email address, because there is no way to know if you're the legitimate owner or an attacker. You will have to make a new site (dont worry, its free!). If you didnt get an email from the password reset form, check your spam folder.
</p>
<p>
This is also a great opportunity to talk about password managers. A password manager keeps a database of randomized strong passwords for all of your sites for you, so you only need to remember
one password which then controls all of the other ones. This reduces the chances of forgetting your password and having to reset it. Popular options include <a href="https://bitwarden.com/">BitWarden</a> and <a href="https://1password.com/">1Password</a>.
</p>
</div>
</div>
</div>
<div class="accordion">
<div class="accordion-group">
@ -39,6 +75,23 @@
</div>
</div>
<div class="accordion">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#editingissues">
I'm having issues using the editor, it is behaving strangly or redirecting instead of saving.
</a>
</div>
<div id="editingissues" class="accordion-body collapse">
<div class="accordion-inner">
<p>
This can sometimes be caused by ad blockers or other browser extensions. Try disabling them for Neocities and try again (you won't need them here anyways since we don't do any advertising).
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cads">
@ -75,15 +128,17 @@
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#ctwo">
I didn't enter an email for my site, can you reset the password?
<a class="accordion-toggle" data-toggle="collapse" href="#hitsviews">
What is the difference between hits and views?
</a>
</div>
<div id="ctwo" class="accordion-body collapse">
<div id="hitsviews" class="accordion-body collapse">
<div class="accordion-inner">
<p>
For security reasons, we cannot reset your password if you did not enter an email for your site. You will have to make a new site (dont worry, its free!). If you didnt get an email from the password reset form, you didnt enter an email (or its in your spam folder). Again you will have to make a new site; we cannot help you for security reasons.
<strong>Hits</strong> are number of requests for any file on a site, <strong>views</strong> count for one unique IP address per hour. Views provide better insight into how many individuals (or search engines, etc) are looking at your site. There is no such thing as perfectly accurate stats, so this doesn't necessarily reflect how many actual people are viewing your site, but it does provide a rough way to see how popular your site is becoming. If you put a hit counter on your site, it may give a different number than what Neocities records and that is expected.
</p>
<p>We don't provide a way to look at individual visitor records on Neocities, the main reason being that the information isn't very useful anymore, since most sources block HTTP Referer headers, making it difficult to determine origin.</p>
</div>
</div>
</div>
@ -97,7 +152,7 @@
<div id="cthree" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We don't support FTP, sorry! Aside from security problems with FTP, it isn't used by many people anymore. We need to stay focused on what we work on, so we only support ways to upload content that most people use these days.
We don't support FTP, apologies! Aside from security problems with FTP, it isn't used by many people anymore. We need to stay focused on what we work on, so we only support ways to upload content that most people use these days.
</p>
<p>
@ -126,6 +181,155 @@
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cten">
Can you delete my site for me?
</a>
</div>
<div id="cten" class="accordion-body collapse">
<div class="accordion-inner">
<p>
For security reasons, we can't. But you can delete your own site by logging in and going to the settings page!
</p>
<p>
For safety reasons, if you have more than one site, you'll need to delete all sites before deleting the main account. Click on "Manage Site Settings" to delete each site individually.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#csitebackup">
Site backup downloading is not working
</a>
</div>
<div id="csitebackup" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Site downloading issues are almost always related to the internet connectivity issues. If your download is continuing to get cancelled or interrupted, check your WiFi router, or try plugging in directly to the router to see if that resolves it. You could also try downloading from a local library or coffee shop and see if you have the same issue.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#csitename">
A site name is taken but is not being actively used or is not found, can I take it over?
</a>
</div>
<div id="csitename" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Neocities does not have a policy of transferring inactive sites to different owners and can't make manual exceptions at the moment. This is a tricky subject because we don't have a way to be sure if a user has actually abandoned their site, even if it's just the default "new site" template. We may have a different policy in the future regarding this.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#ctags">
Why can't I add more tags to my site profile?
</a>
</div>
<div id="ctags" class="accordion-body collapse">
<div class="accordion-inner">
<p>
There is a limit of <%= Site::MAXIMUM_TAGS.to_s %> per site, so try removing a tag and then you will be able to add another.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cfiles">
Do you support WASM / MP3 / MP4/ ZIP for free sites?
</a>
</div>
<div id="cfiles" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We don't currently support certain types of files, particularly multimedia files and WebASM with free sites because of persistent issues with abuse (piracy, bitcoin mining, excessive bandwidth usage). In general, these files are not conducive to Neocities' mission of bringing back personal home pages vs being a piracy and webapp hosting platform. You can embed audio and videos through other providers like YouTube, which is preferred because they will automatically provide the streaming media in formats that work better for your specific users' devices (such as mobile phones).
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cfilesizes">
Can I upload very large files to Neocities?
</a>
</div>
<div id="cfilesizes" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We currently have a file size limit for very large files on Neocities, including supporter sites. This is mainly because our CDN (content delivery network) is optimized for lots of small files (the kind that web sites use), and also because large files tend to be easily abused (think piracy). Files used to make web pages will not hit these limits, but things like very large zipballs are more likely to. These also tend to be the file types that lead to abuse and would make Neocities unsustainable to operate in the long run (piracy, malware, etc), another reason we're hesitant to add support for them.
</p>
<p>
Even if we supported this, it wouldn't work very well because HTTP servers are not good at this anyways and there are almost always better alternatives for very large file distribution. For example if you need to upload a video, YouTube or Vimeo are better because they automatically format in the different file types that different browsers support, or if you are releasing a large game, it's probably better to release it through a game platform like Steam. Using BitTorrent is also a really effective way to have your site visitors help distribute your content quickly (provided it's legal!)
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cinvoices">
Where can I find invoices for supporter membership?
</a>
</div>
<div id="cinvoices" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Invoices are available for download as PDF files <a href="/settings/invoices">here</a>.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#celeven">
How can I load Neocities sites on old web browsers?
</a>
</div>
<div id="celeven" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We recommend using an "old web browser" proxy server like <a href="https://github.com/atauenis/webone">WebOne</a> or <a href="https://github.com/tenox7/wrp">WRP</a>. These translate between modern browser
standards and old browser standards, allowing for us to provide the speed and security of the new web without compromising the ability to use old browsers.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cnine">
Do you support .htaccess files?
</a>
</div>
<div id="cnine" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Neocities does not support .htaccess files. This is an Apache HTTP server specific file type, and we don't use Apache for our backend. In addition, we generally don't add features to Neocities that make the functionality of sites depend on custom backend functionality (the sole exception being that we make sure "page not found" goes to "not_found.html" so you can create a custom 404.)
</p>
<p>
Finally, we sometimes see people trying to use .htaccess for authentication (with usernames and passwords). Neocities is only for sites and information you want to make public, and it is dangerous to use it for private sites! We make backups of your site and attackers can easily find any information you're trying to hide using our archivers. Our goal is to make sure your sites stay up for a long time and persist, not to enforce secrecy zones on your site. If you want to make a controlled access site, Neocities is probably not what you're looking for.
</p>
</div>
</div>
</div>
<h3>Legal / Abuse</h3>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cfive">
@ -135,7 +339,7 @@
<div id="cfive" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We will not remove a site unless it is violating our <a href="/terms">Terms of Service</a> or breaking the law. Chances are, it isn't. And the First Amendment of the United States Constitution defends our right to do this. If you'd like a more in-depth explanation of our policies, see our <a href="/legal">legal guide for Neocities</a>.
We will not remove a site unless it is violating our <a href="/terms">Terms of Service</a> or breaking the law. Chances are, it isn't. And the laws in place defend our right to do this. If you'd like a more in-depth explanation of our policies, see our <a href="/legal">legal guide for Neocities</a>.
</p>
</div>
</div>
@ -144,16 +348,13 @@
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#csix">
I want to sue you.
I don't like something you're hosting and I'm going to sue you!
</a>
</div>
<div id="csix" class="accordion-body collapse">
<div class="accordion-inner">
<p>
Cool.
</p>
<p>
Please consult our <a href="/legal">legal guide for Neocities</a> for more information.
Cool! Please consult our <a href="/legal">legal guide for Neocities</a> for more information on why this is a really bad idea.
</p>
</div>
</div>
@ -188,67 +389,6 @@
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cnine">
Do you support .htaccess files?
</a>
</div>
<div id="cnine" class="accordion-body collapse">
<div class="accordion-inner">
<p>
No. And we don't intend to.
</p>
<p>
The .htaccess file is an Apache specific thing, and we don't use Apache for our backend. In addition, we generally don't add features to Neocities that make the functionality of sites depend on custom backend functionality (the sole exception being that we make sure "page not found" goes to "not_found.html" so you can create a custom 404.)
</p>
<p>
Finally, we sometimes see people trying to use .htaccess for authentication (with usernames and passwords). Neocities is only for sites and information you want to make public, and it is dangerous to use it for private sites! We make backups of your site and attackers can easily find any information you're trying to hide using our archivers. Our goal is to make sure your sites stay up for a long time and persist, not to enforce secrecy zones on your site. If you want to make a controlled access site, Neocities is not for you.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#cten">
Can you delete my site for me?
</a>
</div>
<div id="cten" class="accordion-body collapse">
<div class="accordion-inner">
<p>
For security reasons, we can't. But you can delete your own site by logging in and going to the settings page!
</p>
<p>
For safety reasons, if you have more than one site, you'll need to delete all sites before deleting the main account. Click on "Manage Site Settings" to delete each site individually.
</p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#celeven">
How can I load Neocities sites on old web browsers?
</a>
</div>
<div id="celeven" class="accordion-body collapse">
<div class="accordion-inner">
<p>
We recommend using an "old web browser" proxy server like <a href="https://github.com/atauenis/webone">WebOne</a> or <a href="https://github.com/tenox7/wrp">WRP</a>. These translate between modern browser
standards and old browser standards, allowing for us to provide the speed and security of the new web without compromising the ability to use old browsers.
</p>
</div>
</div>
</div>
</div>
<h2>Contact Form</h2>
@ -265,6 +405,14 @@
<label for="your_comments">Comments</label>
<textarea name="body" id="your_comments" class="col-75" rows="10"><%= params[:body] %></textarea>
<label class="text-Label" for="faq_check">We may not be able to respond to questions that are already answered.<br>Did you check the Frequently Asked Questions to see if it answered your question?</label>
<div class="select-Container" style="width: 100px; float: none">
<select name="faq_check" id="faq_check" class="input-Select">
<option value="no">No</option>
<option value="yes">Yes</option>
</select>
</div>
<label>Fill out the captcha so we know youre not a robot:</label>
<%== hcaptcha_input %>

View file

@ -218,7 +218,7 @@
<% 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/download">Download entire site</a> |
<% unless is_education? %>
<a href="/site_files/mount_info">Mount your site as a drive on your computer</a>
<% end %>

View file

@ -1,37 +0,0 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Neocities and the Distributed 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 implementation of <a href="https://ipfs.io">IPFS</a>, a protocol for the distributed web. The idea is simple: Instead of serving web sites from central servers, a distributed web allows any computer to help serve a site.
</p>
<p>
This is still very early stage technology and subject to change. To learn more, see our <a href="https://blog.neocities.org/blog/2015/09/08/its-time-for-the-distributed-web.html">blog post</a>.
</p>
<p>
IPFS archiving is now enabled on all sites. You'll see an IPFS CID 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 retrieve content from our IPFS node servers. All you need to do is <a href="https://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_CID_FOR_YOUR_SITE</code>
</p>
<% if signed_in? %>
<p>
IPFS archiving is not enabled by default for all sites. If you would like to enable IPFS for your site, please visit your site's <a href="/settings/<%= current_site.username %>">settings</a> and enable it.
</p>
<% end %>
</article>
</div>

View file

@ -96,54 +96,56 @@
<fieldset class="content">
<h2 class="gamma">Sign up for free</h2>
<hr />
<div class="siteCreateInputs">
<label for="create-Input">Username</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>
<% if @create_disabled %>
<p>Sign up is not currently available, please try again later.</p>
<% else %>
<div class="siteCreateInputs">
<label for="create-Input">Username</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">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" />
<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 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" 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">
<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>
<%== hcaptcha_input %>
</div>
<div class="col col-50" style="padding-left:0">
<label>
Confirm you are human
</label>
<%== hcaptcha_input %>
</div>
<div class="col col-50">
<div style="margin-top: 15px">
<input type="submit" value="Create My Site" class="btn-Action float-Right" />
<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>
</div>
<% end %>
</fieldset>
</form>
<% end %>
</div> <!-- end .col-50 -->
@ -218,7 +220,7 @@
<div class="row">
<div class="col col-50">
<h3>
<i class="fa fa-eye-slash"></i>Zero advertising
<i class="fa fa-eye-slash"></i>Zero Advertising
</h3>
<p>
<strong>Neocities will never sell your personal data or put advertising on your site.</strong> Instead, we are funded directly by people just like you with <a href="/supporter">supporter accounts</a> and <a href="/donate">donations</a>.
@ -226,14 +228,14 @@
</div>
<div class="col col-50">
<h3><i class="fa fa-tachometer"></i>Blazing Fast Performance</h3>
<p>Unlike many other web hosts, we don't skimp on infrastructure. Neocities operates our own caching CDN in 11 datacenters all over the world to quickly serve your site. We also force 100% strong SSL on all sites, and have full support for HTTP/2. Because of our commitment to quality, we routinely out-perform the pricey cloud services on reliability, speed and uptime. Whether its your personal home page or a busy professional site, <strong>your site loads fast</strong>.</p>
<h3><i class="fa fa-tachometer"></i>Fast Site Loading</h3>
<p>Neocities operates our own caching anycast CDN in over a dozen datacenters all over the world to quickly serve your site to visitors with strong SSL and support for HTTP/2. Our strict focus on static web hosting allows us to routinely out-perform the pricey cloud services on reliability, speed and uptime.</p>
</div>
</div>
<div class="row">
<div class="col col-50">
<h3><i class="fa fa-wrench"></i>Developer tools</h3>
<h3><i class="fa fa-wrench"></i>Developer Tools</h3>
<p>Our fast static hosting comes with a great in-browser HTML editor, easy file uploading, a <a href="/cli">command line tool</a>, RSS feeds for every site, <a href="/api">APIs</a> for building developer applications, and much more!
</div>

View file

@ -1,36 +1,11 @@
<!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]-->
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- <link rel="manifest" href="/manifest.json"> -->
<title>Neocities: Create your own free website!</title>
<meta itemprop="name" content="Neocities" />
<meta itemprop="description" content="Free web hosting and tools that allow anyone to create a website. Join our community today!" />
<meta name="description" content="Free web hosting and tools that allow anyone to create a website. Join our community today!" />
<meta name="keywords" content="free website, free web hosting, html, css, learn to code, free hosting, build a website, create a web page, static hosting, how to make a website" />
<link rel="canonical" href="//neocities.org" />
<%== erb :'_meta' %>
<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="Free web hosting and tools that allow anyone to create a website. Join our community today!"/>
<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"/>
<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>
@ -41,4 +16,4 @@
<%== yield %>
</html>
</html>

View file

@ -1,22 +1,9 @@
<!doctype html>
<html>
<html lang="en">
<head>
<title><%= title %></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- <link rel="manifest" href="/manifest.json"> -->
<meta itemprop="name" content="Neocities.org">
<meta itemprop="description" content="Free web hosting and tools that allow anyone to create a website. Join our community today!">
<meta name="description" content="Free web hosting and tools that allow anyone to create a website. Join our community today!">
<meta name="keywords" content="free website, html, css, learn to code, free hosting, build a website, create a web page">
<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="/img/cat-larger.png">
<meta property="og:url" content="https://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.">
<meta name="referrer" content="no-referrer-when-downgrade">
<link rel="canonical" href="https://neocities.org<%= request.fullpath %>" />
<%== erb :'_meta' %>
<% if meta_robots %>
<meta name="robots" content="<%= meta_robots %>">
@ -24,13 +11,6 @@
<link href="/css/neo.css" rel="stylesheet" type="text/css" media="all">
<link href="/favicon.ico?v=4" rel="shortcut icon" type="image/ico">
<!-- 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">
<% if @dont_browser_cache %>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
@ -61,7 +41,7 @@
<script src="/js/nav.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/typeahead.bundle.js"></script>
<script>
$("a#like").tooltip({html: true})
$("a.comment_like").tooltip({html: true})

View file

@ -98,7 +98,7 @@
<h2>Legal Threats</h2>
<p>
<strong>Baseless legal threats are not welcome at Neocities, and will be considered an attack on our protections under the law that all online service providers have a strong interest in defending. As per our <a href="http://www.opencompany.org/">Open Company</a> principles, we reserve the right to publish and expose any and all legal threats made against Neocities. Baseless legal threats will be considered an attempt to harm Neocities by abusing the legal system, and we will respond accordingly. We will additionally invite other major social networks and service providers to join us in defending against baseless legal claims if we conclude it is in their best interests. <u>Think very carefully before sending Neocities legal threats</u>.</strong>
<strong>Baseless legal threats are not welcome at Neocities, and will be considered an attack on our protections under the law that all online service providers have a strong interest in defending. We reserve the right to publish and expose any and all legal threats made against Neocities. Baseless legal threats will be considered an attempt to harm Neocities by abusing the legal system, and we will respond accordingly. We will additionally invite other major social networks and service providers to join us in defending against baseless legal claims if we conclude it is in their best interests. <u>Think very carefully before sending Neocities legal threats</u>.</strong>
</p>
<p>
It is strongly recommended that you consider <a href="http://en.wikipedia.org/wiki/Streisand_effect">the consequences</a> of sending us legal threats or filing a lawsuit against Neocities before doing so. We are intimately familiar with our rights under the law, we have access to legal representation, and we can afford it. You will very likely lose your (embarassing and public) court case, and you will very likely be required to compensate us for legal fees.

View file

@ -13,6 +13,16 @@
<div class="col-left">
<div class="col col-66" style="min-height: 43em;">
<div class="press-news">
<h3><a href="https://www.polygon.com/23024357/neocities-social-media-alternative"><strong>Polygon</strong>: Neocities celebrates the old internet, offering relief from 24/7 social feeds</a></h3>
<blockquote>
Rather than a constantly rushing river of information, Neocities sites are like homes where users fix them up, spend time on them, and invite others to visit.
</blockquote>
<h3><a href="https://www.techspot.com/news/99085-neocities-bringing-eye-bleeding-spirit-geocities-back-modern.html"><strong>TechSpot</strong>: Neocities is bringing the eye-bleeding "spirit" of GeoCities back to the modern web</a></h3>
<blockquote>
Neocities is yet another alternative to social networking and pure nostalgia trips down memory lane. It offers a hosting space for hundreds of thousands of websites that don't need to comply with static rules or well-defined design policies to be online. Neocities introduces itself as a "social network" that brings back the "lost individual creativity of the web."
</blockquote>
<h3><a href="http://www.hostingadvice.com/blog/neocities-empowers-site-owners-to-showcase-their-creativity-online/"><strong>Hosting Advice</strong>: How a Blank-Canvas, Static Hosting Approach Empowers Site Owners to Showcase Their Creativity Online</a></h3>
<blockquote>
Neocities has acquired upward of 100,000 users in its relatively short lifespan, which is a testament to its focus on creative web design and its offer of cost-free hosting without host-branded ads — a service thats the exception rather than the rule in the industry. By embracing ingenuity and a templateless-approach, the organization has effectively picked up where early hosts have left off — with a crucial difference. Neocities provides the modern tools, such as an in-browser HTML editor and a command line prompt, among other features, that make web development a bit more accessible to todays crop of web visionaries.
@ -25,15 +35,6 @@
It's easy to assume that those attending the Web 1.0 Conference in Portland, Oregon are caught up on an obsolete era of the internet. The conference's organizers, however, think the lowly HTML website may very well be the future of the web.
</blockquote>
<h3><a href="http://motherboard.vice.com/read/the-interplanetary-file-system-wants-to-create-a-permanent-web"><strong>Vice</strong>: The InterPlanetary File System Wants to Create a Permanent Web</a></h3>
<blockquote>
In addition to satisfying the cravings of some for Geocities clip-art nostalgia, Drake has more serious plans up his sleeve. He wants to give people the ability to "build web sites that persist forever."
</blockquote>
<br>
<blockquote>
"Building an information network that will stay up forever is as modern as it gets," he wrote. "[IPFS] will pull the internet out of the Dark Ages of fast information destruction, and move us from a short-term tech culture into a tech civilization, maintaining distributed libraries of information that could continue to persist for hundreds or even thousands of years."
</blockquote>
<h3><a href="http://recode.net/2015/07/17/why-we-all-need-to-make-the-internet-fun-again/"><strong>Re/code</strong>:
Why We All Need to Make the Internet Fun Again</a></h3>
<blockquote>
@ -57,17 +58,20 @@ Why We All Need to Make the Internet Fun Again</a></h3>
<h3><a href="http://www.fastcodesign.com/3037028/why-the-internet-is-time-traveling-back-to-1994"><strong>Fast Company</strong>: Oh, Snap! '90s Web Design Is Hot Again</a></h3>
<blockquote>“What I'm trying to do with Neocities is re-enable that creativity, and show people that it isnt just a nostalgia thing any more,” [Kyle Drake] says, adding that the site now hosts about 26,000 sites and has proven financially self-sustaining.</blockquote>
<h3><a href="http://www.wired.com/2013/07/neocities/"><strong>Wired</strong>: NeoCities Wants to Save Us From the Crushing Boredom of Social Networking</a></h3>
<h3><a href="http://www.wired.com/2013/07/neocities/"><strong>Wired</strong>: Neocities Wants to Save Us From the Crushing Boredom of Social Networking</a></h3>
<blockquote>There needs to be an alternative to the current pre-formatted, template-driven, standardizing platforms, which make it easy to have a web presence, but hard to make that presence your own.</blockquote>
<h3><a href="https://www.vice.com/en/article/d77mpw/neocities-is-recreating-the-garish-web-10-creativity-of-geocities"><strong>Vice</strong>: NeoCities Is Recreating the Garish, Web 1.0 Creativity of Geocities</a></h3>
<h3><a href="https://www.vice.com/en/article/d77mpw/neocities-is-recreating-the-garish-web-10-creativity-of-geocities"><strong>Vice</strong>: Neocities Is Recreating the Garish, Web 1.0 Creativity of Geocities</a></h3>
<blockquote>The project is a way to recreate not only the aesthetic of the early personal websites, but also the original mission of Geocities: to give anyone with internet access a free place on the web.</blockquote>
<h3><a href="http://arstechnica.com/tech-policy/2014/05/web-host-gives-fcc-a-28-8kbps-slow-lane-in-net-neutrality-protest/"><strong>Ars Technica</strong>: Web host gives FCC a 28.8Kbps slow lane in net neutrality protest</a></h3>
<blockquote>Lots of people are angry about FCC Chairman Tom Wheeler's Internet "fast lane" proposal that would let Internet service providers charge Web services for priority access to consumers. But one Web hosting service called NeoCities isn't just writing letters to the FCC. Instead, the company found the FCC's internal IP address range and throttled all connections to 28.8Kbps speeds.</blockquote>
<blockquote>Lots of people are angry about FCC Chairman Tom Wheeler's Internet "fast lane" proposal that would let Internet service providers charge Web services for priority access to consumers. But one Web hosting service called Neocities isn't just writing letters to the FCC. Instead, the company found the FCC's internal IP address range and throttled all connections to 28.8Kbps speeds.</blockquote>
</div>
</div>
<div class="col col-33">
<p>When writing about us, we prefer "Neocities" over "NeoCities". Thank you!</p>
<h2 style="margin-bottom: 9px;">Contact Us</h2>
<p><a href="https://neocities.org/contact"><i class="fa fa-envelope-o"></i> Contact Form</a></p>

View file

@ -24,10 +24,10 @@
<div class="tabbable" style="margin-top: 20px"> <!-- Only required for left/right tabs -->
<ul class="nav nav-tabs">
<li class="active"><a href="#sites" data-toggle="tab">Manage Sites</a></li>
<li><a href="#supporter" data-toggle="tab">Supporter Info</a></li>
<li><a href="#password" data-toggle="tab">Change Password</a></li>
<li><a href="#email" data-toggle="tab">Change Email</a></li>
<li class="active"><a href="#sites" data-toggle="tab">Sites</a></li>
<li><a href="#supporter" data-toggle="tab">Supporter</a></li>
<li><a href="#password" data-toggle="tab">Password</a></li>
<li><a href="#email" data-toggle="tab">Email</a></li>
<% if current_site.stripe_paying_supporter? %>
<li><a href="#billing" data-toggle="tab">Change Card</a></li>
<% end %>

View file

@ -4,16 +4,26 @@
</p>
<% if parent_site.paying_supporter? %>
<a class="btn-Action" href="/supporter" style="margin-bottom: 20px">Supporter Info</a>
<a href="/supporter" style="margin-bottom: 20px">Supporter Info</a>
<small><a href="#" onclick="$('#endSupporterConfirm').modal()" style="font-size: 8pt">End Supporter Membership</a></small>
<% end %>
<% else %>
<p class="tiny">
You currently have the <strong>Free Plan (<%= current_site.maximum_space.to_space_pretty %>)</strong>.<br>Want to get more space and help Neocities? Become a supporter!
</p>
<a class="btn-Action" href="/supporter">Supporter Info</a>
<a href="/supporter">Supporter Info</a>
<% if parent_site.stripe_customer_id || parent_site.paypal_profile_id %>
<br>
<a href="/settings/invoices">Generated Invoices</a>
<% end %>
<% end %>
<% if parent_site.stripe_customer_id || parent_site.paypal_profile_id %>
<br>
<a href="/settings/invoices">Generated Invoices</a>
<% end %>
<div class="modal hide fade" id="endSupporterConfirm" tabindex="-1" role="dialog" aria-labelledby="endSupporterConfirmLabel" aria-hidden="true">
<form method="POST" action="/supporter/end">
<%== csrf_token_input_html %>

View file

@ -0,0 +1,65 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>Account Settings</h1>
<h3 class="subtitle">Manage the account for your sites</h3>
</div>
</div>
<div class="content single-Col misc-page txt-Center">
<article>
<section>
<div class="txt-Center">
<% if flash[:success] %>
<div class="alert alert-block alert-success" style="margin-top: 20px">
<%== flash[:success] %>
</div>
<% end %>
<% if flash[:error] %>
<div class="alert alert-block alert-error" style="margin-top: 20px">
<%== flash[:error] %>
</div>
<% end %>
</div>
<h2>Generated Invoices</h2>
<% if @invoices.empty? && !current_site.paypal_profile_id %>
<p>No generated invoices.</p>
<% else %>
<p>
<% @invoices.each do |invoice| %>
<%= Time.at(invoice.date).strftime('%m/%d/%y') %> - <a href="<%= invoice.invoice_pdf %>">PDF</a><br>
<% end %>
</p>
<% end %>
<% if current_site.paypal_profile_id %>
<p>PayPal invoices are available on their <a href="https://paypal.com">web site</a>.</p>
<% end %>
<p><a href="/settings">Back</a></p>
</section>
</article>
</div>
<div class="modal hide fade" id="deleteSite" tabindex="-1" role="dialog" aria-labelledby="deleteSiteLabel" aria-hidden="true">
<form method="POST" action="/site/delete">
<%== csrf_token_input_html %>
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i></button>
<h3 id="deleteSiteLabel">Permanently Delete Site</h3>
</div>
<div class="modal-body">
<strong style="color: red">WARNING: This will permanently delete your web site and Neocities account. There is no undo!</strong>
<p>Delete Site Name: <strong><%= current_site.username %></strong></p>
<p>Confirm your site name by typing it here:</p>
<input class="input-Area" name="username" type="text">
</div>
<div class="modal-footer">
<button class="btn cancel" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button type="submit" class="btn-Action">Permanently Delete Site</button>
</div>
</form>
</div>

View file

@ -16,7 +16,7 @@
<p><code>198.51.233.1</code></p>
<h3>Step 2 (optional)</h3>
<h3>Step 2 (optional but recommended)</h3>
<p>
Next, you can add an "AAAA record" that also points to your root domain. This isn't strictly required, but provides IPv6 support which helps future proof your site. Use this address for the AAAA record:
@ -24,10 +24,10 @@
<p><code>2620:2:6000::bad:dab:cafe</code></p>
<h3>Step 3 (optional)</h3>
<h3>Step 3 (optional but recommended)</h3>
<p>
If you would like to have a <strong>www</strong> for your domain, create a CNAME record pointing <strong>www</strong> to <strong>yourdomain.com</strong>:
Sometimes users will type in www before a domain despite it no longer being necessary to load web sites. If you would like to have a <strong>www</strong> for your domain, create a CNAME record pointing <strong>www</strong> to <strong>yourdomain.com</strong>:
</p>
<p><code>www CNAME yourdomain.com</code></p>
@ -36,10 +36,16 @@
<h3>Step 4</h3>
<p>Wait about 5 minutes for the nameserver changes to update. Sometimes it can take a short while for your domain provider to update their records.</p>
<p>
Remove any "URL redirects" if they are present, they are not needed and cause issues with connecting the domain. For example, Namecheap has a <strong>URL Redirect Record</strong> on new domains that needs to be deleted.
</p>
<h3>Step 5</h3>
<p>Wait about 5 minutes for the nameserver changes to update. Sometimes it can take a short while for your domain provider to update their records.</p>
<h3>Step 6</h3>
<p>
Finally, add your domain name to the box below (just the <strong>yourdomain.com</strong>, don't add any subdomains), and your domain should come online within 5 minutes! We will automatically create SSL certs for your domain.
</p>

View file

@ -20,15 +20,6 @@
> Disable Site Profile
<br>
</div>
<h3>IPFS Archiving</h3>
<div style="display: inline-block; text-align: left; margin-bottom: 10px">
<input name="site[ipfs_archiving_enabled]" type="hidden" value="false">
<input name="site[ipfs_archiving_enabled]" type="checkbox" value="true"
<% if @site.ipfs_archiving_enabled == true %>checked<% end %>
> Enable IPFS Archiving <small>(<a href="/distributed-web">what is this?</a>)</small>
</div>
</div>
<input class="btn-Action" type="submit" value="Update Settings">

View file

@ -19,11 +19,6 @@
<div class="col col-50 profile-info">
<h2 class="eps title-with-badge"><span><%= site.title %></span> <% if site.supporter? %><a href="/supporter" class="supporter-badge" title="Neocities Supporter"></a> <% end %></h2>
<p class="site-url"><a href="<%= site.uri %>"><%= site.host %></a></p>
<!--
<% if false #site.latest_archive %>
<p><a href="<%= site.latest_archive.url %>" style="margin-right: 5px"><%= site.latest_archive.ipfs_hash %></a><small style="font-size: 7pt"><a href="/permanent-web">(what is this?)</a></small></p>
<% end %>
-->
<% follow_count = site.follows_dataset.count %>
<div class="stats">
<div class="stat"><strong><%= site.views.format_large_number %></strong> <span>view<%= site.views == 1 ? '' : 's' %></span></div>
@ -36,10 +31,6 @@
<a href="/dashboard" class="btn-Action edit"><i class="fa fa-edit" title="Edit"></i> Edit Site</a>
<% end %>
<% if site.latest_archive && site.ipfs_archiving_enabled %>
<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) %>

View file

@ -1,33 +0,0 @@
<div class="header-Outro">
<div class="row content single-Col">
<h1>IPFS 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 CID <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>
This is a preview release of a new technology. We're still figuring things out, and may stop hosting archives without notice. <a href="/permanent-web">Learn how you can host your own copies of these archives</a>.
</p>
<p>
Archives are captured once every <%= Archive::ARCHIVE_WAIT_TIME / 60 %> minutes, so if you don't see your latest changes, check back later.
</p>
<% end %>
</article>
</div>

View file

@ -6,7 +6,9 @@
You're almost ready!<br>
<% end %>
We sent an email to <strong><%= current_site.email %></strong> to make sure it's correct.<br>Please check your email, enter the confirmation code here, and you're all set.
We sent an email to <strong><%= current_site.email %></strong> to make sure it's correct.<br>
Please check your email, and enter the confirmation code here.<br>
If you don't see the email in your inbox, try looking in the spam folder.
</h3>
<div class="row">

View file

@ -0,0 +1,90 @@
<section class="section plans welcome">
<h2>Verify your phone number</h2>
<div class="txt-Center"><img src="/img/catbus.png" width="90px"></div>
<h3 class="subtitle">
Last thing!<br>
To prevent spam and keep the searchability of your site high, we have one last step:
<br>please verify your mobile phone number.
</h3>
<div class="row">
<div class="col col-100 txt-Center" style="margin-top: 10px;">
<% if flash[:success] %>
<div class="alert alert-block alert-success" style="margin-top: 20px">
<%== flash[:success] %>
</div>
<% end %>
<% if flash[:error] %>
<div class="alert alert-block alert-error" style="margin-top: 20px">
<%== flash[:error] %>
</div>
<% end %>
<form method="POST" action="/site/<%= current_site.username %>/confirm_phone" class="content">
<%== csrf_token_input_html %>
<% if current_site.phone_verification_sid %>
<fieldset>
<label for="token">Enter the 6 digit code:<br></label>
<input id="code" name="code" type="text" class="input-Area" autofill="off" autocapitalize="off" autocorrect="off" value="<%= flash[:code] %>" style="width: 100px" maxlength=6>
</fieldset>
<input id="submitButton" class="btn-Action" type="submit" value="Verify Code" style="display: none" autocomplete="off">
<script>
document.getElementById('code').addEventListener('input', function(e) {
var inputVal = e.target.value;
var submitButton = document.getElementById('submitButton');
// Check if there are exactly 6 digits in the input
var isValid = /^\d{6}$/.test(inputVal);
if(isValid) {
submitButton.style = 'display: inline-block';
} else {
submitButton.style = 'display: none';
}
});
</script>
<% else %>
<fieldset>
<label for="phone">Enter your phone number<br><small>(including country code)</small></label>
<input id="phone" name="phone" type="text" class="input-Area" autofill="off" autocapitalize="off" autocorrect="off" autocomplete="off" style="width: 290px">
<input id="phone_intl" name="phone_intl" type="hidden">
</fieldset>
<input id="submitButton" class="btn-Action" type="submit" value="Send Verification Code" style="display: none">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/css/intlTelInput.css">
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/intlTelInput.min.js"></script>
<script>
const input = document.querySelector("#phone");
const iti = window.intlTelInput(input, {
nationalMode: true,
utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js",
});
const handleChange = () => {
let text;
if(iti.isValidNumber()) {
document.getElementById('submitButton').style = "display: inline-block"
document.getElementById('phone_intl').value = iti.getNumber()
} else {
document.getElementById('submitButton').style = "display: none"
}
};
// listen to "keyup", but also "change" to update when the user selects a country
input.addEventListener('change', handleChange);
input.addEventListener('keyup', handleChange);
</script>
<% end %>
</form>
</div>
</div>
</section>

View file

@ -139,7 +139,7 @@
<h2>
Total Visitors
<small>
<% if params[:days].blank? %>
<% if params[:days].to_s.blank? %>
last <%= @default_stat_points %> days
<% elsif params[:days] == 'sincethebigbang' %>
all time
@ -174,6 +174,22 @@
</div>
<% end %>
<div class="row">
<div class="col col-100">
<h3>What are these numbers?</h3>
<p>
<strong>Hits</strong> occur each time our servers send a file. For example, if a webpage consists of an HTML file, three images, and two JavaScript files, accessing this page would result in six hits.
</p>
<p>
<strong>Visits</strong> are a count of unique IP addresses requesting pages from a web site per hour, regardless of how many requests that IP address makes. Visits generally give a more accurate representation of website traffic in terms of real users.
</p>
<p>
Due to bots, search engine crawlers, and proxy servers these numbers should not be considered completely accurate.
</p>
</div>
</div>
<!--
<div class="row">
<div class="col col-50">
@ -276,7 +292,7 @@
</div>
<!-- <script src="//www.webglearth.com/v2/api.js"></script> -->
<script src="/js/Chart.min.js"></script>
<script src="/js/chart.js"></script>
<script>
//OpenGL globe
$(document).ready(function() {
@ -324,39 +340,75 @@
});
*/
//chart.js
var data = {
const data = {
labels: <%== @stats[:stat_days].collect {|s| s.created_at.strftime("%b %-d, %Y")}.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)",
label: 'Hits',
backgroundColor: 'rgba(220,220,220,0.2)',
fill: true,
borderColor: 'rgba(220,220,220,1)',
pointBackgroundColor: 'rgba(220,220,220,1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '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)",
label: 'Visits',
backgroundColor: 'rgba(151,187,205,0.2)',
fill: true,
borderColor: 'rgba(151,187,205,1)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '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>
};
const ctx = $("#myChart").get(0).getContext("2d");
const config = {
type: 'line',
data: data,
options: {
responsive: true,
scales: {
x: {
beginAtZero: true
},
y: {
beginAtZero: true
}
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
bodyFont: {
size: 14,
},
bodyAlign: 'right',
titleFont: {
size: 14,
},
callbacks: {
afterTitle: function(context) {
let tooltipData = [];
if (context.length > 0) {
const index = context[0].dataIndex;
}
return tooltipData;
}
}
}
}
}
};
const myLineChart = new Chart(ctx, config);
});
</script>

View file

@ -27,5 +27,4 @@
<a href="/">Get Started</a>
</p>
</article>
<%== erb :'_team', layout: false %>
</div>

View file

@ -1,39 +0,0 @@
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} for site_id #{site_id}"
job.delete
end
end
scheduled_jobs = Sidekiq::ScheduledSet.new.select do |scheduled_job|
scheduled_job.klass == 'ArchiveWorker' &&
scheduled_job.args[0] == site_id
end
scheduled_jobs.each do |scheduled_job|
logger.info "DELETING scheduled job #{scheduled_job.jid} for site_id #{site_id}"
scheduled_job.delete
end
logger.info "ARCHIVING: #{site.username}"
site.archive!
end
end

View file

@ -1,3 +1,4 @@
require 'sidekiq/api'
require 'securerandom'
require 'open3'
@ -6,7 +7,7 @@ class ScreenshotWorker
HARD_TIMEOUT = 30.freeze
PAGE_WAIT_TIME = 5.freeze # 3D/VR sites take a bit to render after loading usually.
include Sidekiq::Worker
sidekiq_options queue: :screenshots, retry: 10, backtrace: true
sidekiq_options queue: :screenshots, backtrace: true
def perform(username, path)
site = Site[username: username]
@ -34,8 +35,6 @@ class ScreenshotWorker
path = "/#{path}" unless path[0] == '/'
path_for_screenshot = path
uri = Addressable::URI.parse $config['screenshot_urls'].sample
api_user, api_password = uri.user, uri.password
uri = "#{uri.scheme}://#{uri.host}:#{uri.port}" + '?' + Rack::Utils.build_query(
@ -51,16 +50,24 @@ class ScreenshotWorker
File.write base_image_tmpfile_path, http_resp.to_s
user_screenshots_path = File.join SCREENSHOTS_PATH, Site.sharding_dir(username), username
screenshot_path = File.join user_screenshots_path, File.dirname(path_for_screenshot)
screenshot_path = File.join user_screenshots_path, File.dirname(path)
FileUtils.mkdir_p screenshot_path unless Dir.exist?(screenshot_path)
FileUtils.cp base_image_tmpfile_path, File.join(user_screenshots_path, "#{path_for_screenshot}.png")
# We only need the full PNG for the main index right now
if path.match /^\/index.html?$/
ImageOptimizer.new(base_image_tmpfile_path, level: 1).optimize
FileUtils.cp base_image_tmpfile_path, File.join(user_screenshots_path, "#{path}.png")
end
# Optimized image for open graph link expanders
image = Rszr::Image.load base_image_tmpfile_path
image.resize! 1200, 630, crop: :n
image.save File.join(user_screenshots_path, "#{path}.jpg"), quality: 85
ImageOptimizer.new(File.join(user_screenshots_path, "#{path}.jpg")).optimize
Site::SCREENSHOT_RESOLUTIONS.each do |res|
width, height = res.split('x').collect {|r| r.to_i}
full_screenshot_path = File.join(user_screenshots_path, "#{path_for_screenshot}.#{res}.webp")
full_screenshot_path = File.join(user_screenshots_path, "#{path}.#{res}.webp")
opts = {resize_w: width, resize_h: height, near_lossless: 0}
if width == height

View file

@ -3,8 +3,25 @@ class StopForumSpamWorker
sidekiq_options queue: :stop_forum_spam, retry: 1, backtrace: true
def perform(opts)
opts.merge! api_key: $config['stop_forum_spam_api_key']
res = HTTP.post 'https://stopforumspam.com/add', form: opts
puts res.inspect
txn = Minfraud::Components::Report::Transaction.new(
ip_address: opts['ip'],
tag: :spam_or_abuse,
# The following key/values are not mandatory but are encouraged
#maxmind_id: 'noideawhatthisis',
#minfraud_id: '01c25cb0-f067-4e02-8ed0-a094c580f5e4',
#transaction_id: 'txn123'
#chargeback_code: 'BL'
notes: opts['classifier']
)
reporter = Minfraud::Report.new transaction: txn
reporter.report_transaction
HTTP.post 'https://stopforumspam.com/add', form: {
api_key: $config['stop_forum_spam_api_key'],
username: opts['username'],
email: opts['email'],
ip: opts['ip']
}
end
end