Compare commits

..

No commits in common. "master" and "0.3.0" have entirely different histories.

1727 changed files with 6999 additions and 766707 deletions

View file

@ -1,46 +0,0 @@
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-22.04
services:
postgres:
image: postgres
env:
POSTGRES_DB: ci_test
POSTGRES_PASSWORD: citestpassword
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
redis:
image: redis
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps port 6379 on service container to the host
- 6379:6379
steps:
- uses: actions/checkout@v2
- run: sudo apt-get update && sudo apt-get -y install libimlib2-dev chromium-browser
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Run tests with Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: bundle exec rake

41
.gitignore vendored
View file

@ -1,31 +1,24 @@
*.gem
*.rbc
.bundle
.config
coverage
InstalledFiles
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
# YARD artifacts
.yardoc
_yardoc
doc/
tests/coverage
config.yml
.DS_Store
public/css/neo.css
public/css/neo.css.map
public/assets/css/.sass-cache/
public/site_thumbnails
public/sites
public/site_screenshots
public/site_screenshots_test
public/site_thumbnails_test
*.swp
files/map.txt
files/supporter-map.txt
files/maps
.sass-cache
.sass-cache/*
files/sslsites.zip
.tm_properties
.vagrant
public/banned_sites
public/deleted_sites
files/disposable_email_whitelist.conf
files/disposable_email_blacklist.conf
files/banned_email_blacklist.conf
files/letsencrypt.key
files/tor.txt
.bundle
ext/black_box.rb
files/trumpplan.txt
public/sitemap

7
.travis.yml Normal file
View file

@ -0,0 +1,7 @@
language: ruby
rvm:
- "2.1.0"
addons:
postgresql: "9.3"
before_script:
- psql -c 'create database travis_ci_test;' -U postgres

118
Gemfile
View file

@ -3,90 +3,66 @@ source 'https://rubygems.org'
gem 'sinatra'
gem 'redis'
gem 'sequel'
gem 'redis-namespace'
gem 'slim'
gem 'bcrypt'
gem 'sinatra-flash', require: 'sinatra/flash'
gem 'sinatra-xsendfile', require: 'sinatra/xsendfile'
gem 'puma', '< 7', require: nil
gem 'sidekiq', '~> 7'
gem 'puma', require: nil
gem 'rubyzip', require: 'zip'
gem 'rack-recaptcha', require: 'rack/recaptcha'
gem 'rmagick', require: nil
gem 'sidekiq'
gem 'ago'
gem 'mail'
gem 'google-api-client', require: 'google/api_client'
gem 'tilt'
gem 'erubi'
gem 'stripe' #, source: 'https://code.stripe.com/'
gem 'terrapin'
gem 'sass', require: nil
gem 'dav4rack', git: 'https://github.com/neocities/dav4rack.git', ref: '1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b'
gem 'filesize'
gem 'thread'
gem 'rest-client', require: 'rest_client'
gem 'addressable', '>= 2.8.0', require: 'addressable/uri'
gem 'paypal-recurring', require: 'paypal/recurring'
gem 'geoip'
gem 'io-extra', require: 'io/extra'
#gem 'rye'
gem 'coveralls_reborn', require: false
gem 'sanitize'
gem 'will_paginate'
gem 'simpleidn'
gem 'gandi'
gem 'hoe', require: nil
gem 'msgpack'
gem 'acme-client', '~> 2.0.0'
gem 'http'
gem 'htmlentities'
gem 'rinku'
gem 'image_optim'
gem 'image_optim_pack'
gem 'ipaddress'
gem 'feedjira', '2.1.4'
gem 'monetize'
gem 'xmlrpc'
gem 'magic'
gem 'pg'
gem 'sequel_pg', require: nil
gem 'hiredis'
gem 'activesupport'
gem 'facter', require: nil
gem 'maxmind-db'
gem 'json', '>= 2.3.0'
gem 'nokogiri'
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'
gem 'airbrake'
gem 'csv'
gem 'erubis'
gem 'stripe', :git => 'https://github.com/stripe/stripe-ruby'
gem 'screencap'
group :development, :test do
gem 'pry'
platform :mri do
gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic
gem 'pg'
gem 'sequel_pg', require: nil
gem 'hiredis'
gem 'rainbows', require: nil
group :development, :test do
gem 'pry'
gem 'pry-debugger'
end
end
platform :jruby do
gem 'jruby-openssl'
gem 'json'
gem 'jdbc-postgres'
group :development do
gem 'ruby-debug', require: nil
gem 'sass', require: nil
end
end
group :development do
gem 'shotgun', require: nil
gem 'certified'
end
group :test do
gem 'faker'
gem 'fabrication', require: 'fabrication'
gem 'fabrication', require: 'fabrication'
gem 'minitest'
gem 'minitest-reporters', require: 'minitest/reporters'
gem 'rack-test', require: 'rack/test'
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'
gem 'timecop'
gem 'mock_redis'
gem 'simplecov', require: nil
gem 'm'
gem 'minitest-reporters', require: 'minitest/reporters'
gem 'rack-test', require: 'rack/test'
gem 'webmock'
gem 'mocha', require: nil
gem 'rake', require: nil
gem 'poltergeist'
gem 'phantomjs', require: 'phantomjs/poltergeist'
gem 'capybara'
gem 'capybara_minitest_spec'
platform :mri do
gem 'simplecov', require: nil
end
end

View file

@ -1,488 +1,239 @@
GIT
remote: https://github.com/neocities/dav4rack.git
revision: 1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b
ref: 1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b
remote: https://github.com/stripe/stripe-ruby
revision: 48f76057f425ab5c3bb147f3d71c3d36d951159f
specs:
dav4rack (0.3.0)
nokogiri (>= 1.4.2)
rack (~> 3.0)
uuidtools (~> 2.1.1)
webrick
stripe (1.11.0)
json (~> 1.8.1)
mime-types (~> 1.25)
rest-client (~> 1.4)
GEM
remote: https://rubygems.org/
specs:
acme-client (2.0.19)
base64 (~> 0.2.0)
faraday (>= 1.0, < 3.0.0)
faraday-retry (>= 1.0, < 3.0.0)
activesupport (8.0.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
adequate_crypto_address (0.1.9)
base58 (~> 0.2)
keccak (~> 1.3)
airbrake (13.0.5)
airbrake-ruby (~> 6.0)
airbrake-ruby (6.2.2)
rbtree3 (~> 0.6)
ansi (1.5.0)
base58 (0.2.3)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
bigdecimal (3.1.9)
builder (3.3.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
certified (1.0.0)
climate_control (1.2.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
coveralls_reborn (0.28.0)
simplecov (~> 0.22.0)
term-ansicolor (~> 1.7)
thor (~> 1.2)
tins (~> 1.32)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.2)
dante (0.2.0)
date (3.4.1)
dnsbl-client (1.1.1)
docile (1.4.1)
domain_name (0.6.20240107)
drb (2.2.1)
erubi (1.13.1)
exifr (1.4.1)
fabrication (2.31.0)
facter (4.10.0)
hocon (~> 1.3)
thor (>= 1.0.1, < 1.3)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
feedjira (2.1.4)
faraday (>= 0.9)
faraday_middleware (>= 0.9)
loofah (>= 2.0)
sax-machine (>= 1.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
filesize (0.2.0)
fspath (3.1.2)
gandi (3.3.28)
hashie
xmlrpc
geoip (1.6.4)
hashdiff (1.1.2)
hashie (5.0.0)
hiredis (0.6.3)
hocon (1.4.0)
hoe (4.2.2)
rake (>= 0.8, < 15.0)
htmlentities (4.3.4)
http (5.2.0)
addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-accept (1.7.0)
http-cookie (1.0.8)
domain_name (~> 0.5)
http-form_data (2.3.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_optim (0.31.4)
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.11.2)
fspath (>= 2.1, < 4)
image_optim (~> 0.19)
image_optim_pack (0.11.2-x86_64-darwin)
fspath (>= 2.1, < 4)
image_optim (~> 0.19)
image_optim_pack (0.11.2-x86_64-linux)
fspath (>= 2.1, < 4)
image_optim (~> 0.19)
image_optimizer (1.9.0)
image_size (3.4.0)
in_threads (1.6.0)
io-extra (1.4.0)
ipaddress (0.8.3)
json (2.9.1)
jwt (2.10.1)
base64
keccak (1.3.2)
llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.5)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
m (1.6.2)
method_source (>= 0.6.7)
rake (>= 0.9.2.2)
magic (0.2.9)
addressable (2.3.6)
ago (0.1.5)
ansi (1.4.3)
autoparse (0.3.3)
addressable (>= 2.3.1)
extlib (>= 0.9.15)
multi_json (>= 1.0.0)
bcrypt (3.1.7)
builder (3.2.2)
capybara (2.2.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara_minitest_spec (1.0.1)
capybara (>= 2)
minitest (>= 2)
celluloid (0.15.2)
timers (~> 1.1.0)
cliver (0.3.2)
coderay (1.1.0)
columnize (0.3.6)
connection_pool (2.0.0)
crack (0.4.2)
safe_yaml (~> 1.0.0)
debugger (1.6.6)
columnize (>= 0.3.1)
debugger-linecache (~> 1.2.0)
debugger-ruby_core_source (~> 1.3.2)
debugger-linecache (1.2.0)
debugger-ruby_core_source (1.3.2)
docile (1.1.3)
erubis (2.7.0)
extlib (0.9.16)
fabrication (2.11.0)
faker (1.3.0)
i18n (~> 0.5)
faraday (0.9.0)
multipart-post (>= 1.2, < 3)
ffi (1.9.3)
google-api-client (0.7.1)
addressable (>= 2.3.2)
autoparse (>= 0.3.3)
extlib (>= 0.9.15)
faraday (>= 0.9.0)
jwt (>= 0.1.5)
launchy (>= 2.1.1)
multi_json (>= 1.0.0)
retriable (>= 1.4)
signet (>= 0.5.0)
uuidtools (>= 2.1.0)
hashie (2.0.5)
hiredis (0.5.0)
i18n (0.6.9)
json (1.8.1)
jwt (0.1.11)
multi_json (>= 1.5)
kgio (2.9.2)
launchy (2.4.2)
addressable (~> 2.3)
magic (0.2.6)
ffi (>= 0.6.3)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
matrix (0.4.2)
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.1.0)
mime-types (3.6.0)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2025.0107)
minfraud (2.6.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.25.4)
minitest-reporters (1.7.1)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.5.3)
minitest (5.3.1)
minitest-reporters (1.0.2)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
mock_redis (0.49.0)
redis (~> 5)
monetize (1.13.0)
money (~> 6.12)
money (6.19.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mustermann (3.0.3)
ruby2_keywords (~> 0.0.1)
net-imap (0.5.6)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
netrc (0.11.0)
nio4r (2.7.4)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
ostruct (0.6.1)
paypal-recurring (1.1.0)
pg (1.5.9)
phonelib (0.10.3)
progress (3.6.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.12)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-session (2.1.0)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbtree3 (0.7.1)
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.23.2)
connection_pool
redis-namespace (1.11.0)
redis (>= 4)
regexp_parser (2.10.0)
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.4.1)
rinku (2.0.6)
rszr (1.5.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
sanitize (7.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.16.8)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sax-machine (1.3.2)
securerandom (0.4.1)
selenium-webdriver (4.28.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sequel (5.89.0)
bigdecimal
sequel_pg (1.17.1)
pg (>= 0.18.0, != 1.2.0)
sequel (>= 4.38.0)
shotgun (0.9.2)
powerbar
mocha (1.0.0)
metaclass (~> 0.0.1)
multi_json (1.9.2)
multipart-post (2.0.0)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
pg (0.17.1)
phantomjs (1.9.7.0)
poltergeist (1.5.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyglot (0.3.4)
powerbar (1.0.11)
ansi (~> 1.4.0)
hashie (>= 1.1.0)
pry (0.9.12.6)
coderay (~> 1.0)
method_source (~> 0.8)
slop (~> 3.4)
pry-debugger (0.2.2)
debugger (~> 1.3)
pry (~> 0.9.10)
puma (2.8.1)
rack (>= 1.1, < 2.0)
rack (1.5.2)
rack-protection (1.5.2)
rack
rack-recaptcha (0.6.6)
json
rack-test (0.6.2)
rack (>= 1.0)
sidekiq (7.3.8)
base64
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
simpleidn (0.2.3)
sinatra (4.1.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
rack (>= 3.0.0, < 4)
rack-protection (= 4.1.1)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
rainbows (4.6.1)
kgio (~> 2.5)
rack (~> 1.1)
unicorn (~> 4.8)
raindrops (0.13.0)
rake (10.2.1)
redis (3.0.7)
redis-namespace (1.4.1)
redis (~> 3.0.4)
rest-client (1.6.7)
mime-types (>= 1.16)
retriable (1.4.1)
rmagick (2.13.2)
rubyzip (1.1.2)
safe_yaml (1.0.1)
screencap (0.1.1)
phantomjs
sequel (4.8.0)
sequel_pg (1.6.9)
pg (>= 0.8.0)
sequel (>= 3.39.0)
shotgun (0.9)
rack (>= 1.0)
sidekiq (3.0.0)
celluloid (>= 0.15.2)
connection_pool (>= 2.0.0)
json
redis (>= 3.0.6)
redis-namespace (>= 1.3.1)
signet (0.5.0)
addressable (>= 2.2.3)
faraday (>= 0.9.0.rc5)
jwt (>= 0.1.5)
multi_json (>= 1.0.0)
simplecov (0.8.2)
docile (~> 1.1.0)
multi_json
simplecov-html (~> 0.8.0)
simplecov-html (0.8.0)
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
sinatra-flash (0.3.0)
sinatra (>= 1.0.0)
sinatra-xsendfile (0.4.2)
sinatra (>= 0.9.1)
stripe (5.55.0)
stripe-ruby-mock (3.1.0)
dante (>= 0.2.0)
multi_json (~> 1.0)
stripe (> 5, < 6)
sync (0.5.0)
term-ansicolor (1.11.2)
tins (~> 1.0)
terrapin (1.0.1)
climate_control
thor (1.2.2)
thread (0.2.2)
tilt (2.6.0)
timecop (0.9.10)
timeout (0.4.3)
tins (1.38.0)
bigdecimal
sync
twilio-ruby (7.4.3)
benchmark
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0)
nokogiri (>= 1.6, < 2.0)
ostruct
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.0.3)
uuidtools (2.1.5)
webmock (3.25.0)
addressable (>= 2.8.0)
slim (2.0.2)
temple (~> 0.6.6)
tilt (>= 1.3.3, < 2.1)
slop (3.5.0)
temple (0.6.7)
tilt (1.4.1)
timers (1.1.0)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
unicorn (4.8.2)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
uuidtools (2.1.4)
webmock (1.17.4)
addressable (>= 2.2.7)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webp-ffi (0.4.0)
ffi (>= 1.9.0)
ffi-compiler (>= 0.1.2)
webrick (1.9.1)
websocket (1.2.11)
will_paginate (4.0.1)
xmlrpc (0.3.3)
webrick
xpath (3.2.0)
nokogiri (~> 1.8)
zip_tricks (5.6.0)
websocket-driver (0.3.2)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
ruby
DEPENDENCIES
acme-client (~> 2.0.0)
activesupport
addressable (>= 2.8.0)
adequate_crypto_address
airbrake
ago
bcrypt
capybara
certified
coveralls_reborn
csv
dav4rack!
dnsbl-client
erubi
capybara_minitest_spec
erubis
fabrication
facter
faker
feedjira (= 2.1.4)
filesize
gandi
geoip
google-api-client
hiredis
hoe
htmlentities
http
image_optim
image_optim_pack
image_optimizer
io-extra
ipaddress
json (>= 2.3.0)
m
jdbc-postgres
jruby-openssl
json
magic
mail
maxmind-db
minfraud
minitest
minitest-reporters
mocha
mock_redis
monetize
msgpack
nokogiri
paypal-recurring
pg
phonelib
phantomjs
poltergeist
pry
puma (< 7)
pry-debugger
puma
rack-recaptcha
rack-test
rack_session_access
rake (>= 12.3.3)
rainbows
rake
redis
redis-namespace
rest-client
rinku
rszr
rmagick
ruby-debug
rubyzip
sanitize
sass
selenium-webdriver
screencap
sequel
sequel_pg
shotgun
sidekiq (~> 7)
sidekiq
simplecov
simpleidn
sinatra
sinatra-flash
sinatra-xsendfile
stripe
stripe-ruby-mock (~> 3.1.0.rc3)
terrapin
thread
slim
stripe!
tilt
timecop
twilio-ruby
webmock
webp-ffi
will_paginate
xmlrpc
zip_tricks
BUNDLED WITH
2.6.3

View file

@ -1,36 +1,63 @@
### NOTE: THIS IS NOT FOR NEOCITIES SUPPORT! Any issues filed not related to the source code itself will be closed. For support please contact: https://neocities.org/contact
# NeoCities.org
# Neocities.org
[![Build Status](https://travis-ci.org/neocities/neocities.png?branch=master)](https://travis-ci.org/neocities/neocities)
[![Build Status](https://github.com/neocities/neocities/actions/workflows/ci.yml/badge.svg)](https://github.com/neocities/neocities/actions?query=workflow%3ACI)
[![Coverage Status](https://coveralls.io/repos/neocities/neocities/badge.svg?branch=master&service=github)](https://coveralls.io/github/neocities/neocities?branch=master)
The web site for NeoCities! It's open source. Want a feature on the site? Send a pull request!
The web site for Neocities! It's open source. Want a feature on the site? Send a pull request!
## Getting Started
Neocities can be quickly launched in development mode with [Vagrant](https://www.vagrantup.com). Vagrant builds a virtual machine that automatically installs everything you need to run Neocities as a developer. Install Vagrant, then from the command line:
## Installation (OSX)
Install homebrew:
```
vagrant up --provision
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
```
![Vagrant takes a while, make a pizza while waiting](https://i.imgur.com/dKa8LUs.png)
Make a copy of `config.yml.template` in the root directory, and rename it to `config.yml`. Then:
Install deps:
```
vagrant ssh
bundle exec rackup -o 0.0.0.0
$ brew install redis postgresql phantomjs libmagic imagemagick
```
Now you can access the running site from your browser: http://127.0.0.1:9292
Fork the repository on Github.
Clone the forked repo to your local machine: git clone git@github.com:YOURUSERNAME/neocities.git
Install deps:
```
$ cd neocities
$ gem install bundler
$ bundle install
```
Create postgres databases:
```
createdb neocities_test
createdb neocities_dev
```
Copy config.yml.template to config.yml and edit to something like this:
```
development:
database: 'postgres://neocities@127.0.0.1/neocities_dev'
database_pool: 1
session_secret: SECRET1234
recaptcha_public_key: ENTER RECAPTCHA PUBLIC KEY HERE
recaptcha_private_key: ENTER RECAPTCHA PRIVATE KEY HERE
sidekiq_user: sidekiq
sidekiq_pass: sidekiq
phantomjs_url:
- http://localhost:8910
```
Run the tests to see if they work:
```
bundle exec rake test
```
## Want to contribute?
If you'd like to fix a bug, or make an improvement, or add a new feature, it's easy! Just send us a Pull Request.
1. Fork it (https://github.com/neocities/neocities/fork)
1. Fork it ( http://github.com/YOURUSERNAME/neocities/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)

253
Rakefile
View file

@ -1,4 +1,4 @@
require 'rake/testtask'
require "rake/testtask"
task :environment do
require './environment.rb'
@ -7,253 +7,36 @@ end
desc "Run all tests"
Rake::TestTask.new do |t|
t.libs << "spec"
t.test_files = FileList['tests/**/*_tests.rb']
t.verbose = false
t.warning = false
t.test_files = FileList['tests/*_tests.rb']
t.verbose = true
end
task :default => :test
desc "prune logs"
task :prune_logs => [:environment] do
Stat.prune!
StatLocation.prune!
StatReferrer.prune!
StatPath.prune!
end
desc "parse logs"
task :parse_logs => [:environment] do
Stat.parse_logfiles $config['logs_path']
end
hits = {}
logfile = File.open '/var/log/nginx/neocities-sites.log.1', 'r'
while hit = logfile.gets
hit = hit.split ' '
desc 'Update disposable email blacklist'
task :update_disposable_email_blacklist => [:environment] do
# 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, HTTP.get(uri))
end
desc 'Update banned IPs list'
task :update_blocked_ips => [:environment] do
filename = 'listed_ip_365_ipv46'
zip_path = "/tmp/#{filename}.zip"
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
# It says hits, but really we're tracking visits to index"
if hit[3] == '/'
hits[hit[1]] ||= 0
hits[hit[1]] += 1
end
end
logfile.close
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
# Database transaction
DB.transaction do
DB[:blocked_ips].delete
DB[:blocked_ips].multi_insert insert_hashes
end
end
end
hits.each do |username,hitcount|
DB['update sites set hits=hits+? where username=?', hitcount, username].first
end
FileUtils.rm zip_path
end
desc 'rebuild_thumbnails'
task :rebuild_thumbnails => [:environment] do
dirs = Dir[Site::SITE_FILES_ROOT+'/**/*'].collect {|s| s.sub(Site::SITE_FILES_ROOT, '')}.collect {|s| s.sub('/', '')}
dirs.each do |d|
next if File.directory?(d)
full_path = d.split('/')
username = full_path.first
path = '/'+full_path[1..full_path.length].join('/')
if Pathname(path).extname.gsub('.', '').match Site::IMAGE_REGEX
begin
ThumbnailWorker.new.perform username, path
rescue Magick::ImageMagickError
end
end
end
end
desc 'compute_scores'
task :compute_scores => [:environment] do
Site.compute_scores
end
desc 'renew_ssl_certs'
task :renew_ssl_certs => [:environment] do
delay = 0
DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't' and (cert_updated_at is null or cert_updated_at < ?)}, 60.days.ago].all.each do |site|
LetsEncryptWorker.perform_in delay.seconds, site[:id]
delay += 10
end
end
desc 'purge_tmp_turds'
task :purge_tmp_turds => [:environment] do
['neocities_screenshot*', 'RackMultipart*', 'neocities_saving_file*', 'newinstall-*', '*.dmp', 'davfile*', 'magick*', '*.scan', '*.jpg'].each do |target|
Dir.glob("/tmp/#{target}").select {|filename| File::Stat.new(filename).ctime < (Time.now - 3600)}.each {|filename| FileUtils.rm(filename)}
end
end
desc 'compute_follow_count_scores'
task :compute_follow_count_scores => [:environment] do
Site.select(:id,:username,:follow_count).all.each do |site|
count = site.scorable_follow_count
if count != 0
puts "#{site.username} #{site.follow_count} => #{count}"
end
DB['update sites set follow_count=? where id=?', count, site.id].first
end
end
desc 'ml_screenshots_list_dump'
task :ml_screenshots_list_dump => [:environment] do
['phishing', 'spam', 'ham', nil].each do |classifier|
File.open("./files/screenshot-urls#{classifier.nil? ? '' : '-'+classifier.to_s}.txt", 'w') do |fp|
SiteFile.where(classifier: classifier).where(path: 'index.html').each do |site_file|
begin
fp.write "#{site_file.site.screenshot_url('index.html', Site::SCREENSHOT_RESOLUTIONS.first)}\n"
rescue NoMethodError
end
end
end
end
end
desc 'generate_sitemap'
task :generate_sitemap => [:environment] do
sorted_sites = {}
# We pop off array, so highest scores go last.
sites = Site.
select(:id, :username, :updated_at, :profile_enabled).
where(site_changed: true).
exclude(updated_at: nil).
exclude(is_deleted: true).
order(:score).
all
site_files = []
sites.each do |site|
site.site_files_dataset.exclude(path: 'not_found.html').where(path: /\.html?$/).all.each do |site_file|
if site.uri(site_file.path) == site.uri
priority = 0.5
else
priority = 0.4
end
site_files << [site.uri(site_file.path), site_file.updated_at.utc.iso8601, priority]
end
end
sites = nil
GC.start
sitemap_root = File.join Site::PUBLIC_ROOT, 'sitemap'
FileUtils.mkdir_p sitemap_root
index = 0
until site_files.empty?
sfs = site_files.pop 50000
file = File.open File.join(sitemap_root, "sites-#{index}.xml.gz"), 'w'
Zlib::GzipWriter.open File.join(sitemap_root, "sites-#{index}.xml.gz") do |gz|
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
gz.write %{<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
sfs.each do |sf|
gz.write %{<url><loc>#{sf[0].encode(xml: :text)}</loc><lastmod>#{sf[1].encode(xml: :text)}</lastmod><priority>#{sf[2].to_s.encode(xml: :text)}</priority></url>\n}
end
gz.write %{</urlset>}
end
index += 1
end
# Set basic neocities.org root paths
builder = Nokogiri::XML::Builder.new { |xml|
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
File.read(File.join(DIR_ROOT, 'files', 'root_site_uris.txt')).each_line { |uri|
priority, changefreq, uri = uri.strip.split(',')
xml.url {
xml.loc uri
xml.changefreq changefreq
xml.priority priority
}
}
}
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
}
Zlib::GzipWriter.open File.join(sitemap_root, 'root.xml.gz') do |gz|
gz.write builder.to_xml(encoding: 'UTF-8')
end
# Tagged sites sitemap
builder = Nokogiri::XML::Builder.new { |xml|
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
Tag.popular_names(Site.count).each { |tag|
xml.url {
xml.loc "https://neocities.org/browse?sort_by=views&tag=#{tag[:name]}"
xml.changefreq 'daily'
xml.lastmod Time.now.utc.iso8601
}
}
}
}
Zlib::GzipWriter.open File.join(sitemap_root, 'tags.xml.gz') do |gz|
gz.write builder.to_xml(encoding: 'UTF-8')
end
# Final index.xml.gz entrypoint
Zlib::GzipWriter.open File.join(sitemap_root, 'index.xml.gz') do |gz|
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
gz.write %{<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
gz.write %{<sitemap><loc>https://neocities.org/sitemap/root.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
gz.write %{<sitemap><loc>https://neocities.org/sitemap/tags.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
0.upto(index-1).each do |i|
gz.write %{<sitemap><loc>https://neocities.org/sitemap/sites-#{i}.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
end
gz.write %{</sitemapindex>}
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

12
Vagrantfile vendored
View file

@ -1,12 +0,0 @@
VAGRANTFILE_API_VERSION = '2'
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
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', '8192']
vb.name = 'neocities'
end
end

1018
app.rb

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
get '/activity' do
@page = params[:page] || 1
if params[:tag]
query1 = Event
.join(:sites, id: :site_id)
.join(:sites_tags, site_id: :id)
.join(:tags, id: :tag_id)
.where(tags__name: params[:tag])
.where(events__is_deleted: false, sites__is_deleted: false)
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
.where(sites__is_nsfw: false)
.where(follow_id: nil)
.select_all(:events)
query2 = Event
.join(:sites, id: :actioning_site_id)
.join(:sites_tags, site_id: :id)
.join(:tags, id: :tag_id)
.where(tags__name: params[:tag])
.where(events__is_deleted: false, sites__is_deleted: false)
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
.where(sites__is_nsfw: false)
.where(follow_id: nil)
.select_all(:events)
if current_site
blocking_site_ids = current_site.blocking_site_ids
query1 = query1.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
query2 = query2.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
end
ds = query1.union(query2, all: false).order(Sequel.desc(:created_at))
else
ds = Event.news_feed_default_dataset.exclude(sites__is_nsfw: true)
if current_site
blocking_site_ids = current_site.blocking_site_ids
ds = ds.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
end
ds = ds.where(
Sequel.expr(Sequel[:sites][:score] > Event::GLOBAL_SCORE_LIMIT) |
Sequel.expr(Sequel[:actioning_sites][:score] > Event::GLOBAL_SCORE_LIMIT)
)
end
@pagination_dataset = ds.paginate @page.to_i, Event::GLOBAL_PAGINATION_LENGTH
@events = @pagination_dataset.all
erb :'activity'
end

View file

@ -1,308 +0,0 @@
get '/admin' do
require_admin
@banned_sites = Site.select(:username).filter(is_banned: true).order(:username).all
@nsfw_sites = Site.select(:username).filter(is_nsfw: true).order(:username).all
erb :'admin'
end
get '/admin/reports' do
require_admin
@reports = Report.order(:created_at.desc).all
erb :'admin/reports'
end
get '/admin/site/:username' do |username|
require_admin
@site = Site[username: username]
not_found if @site.nil?
@title = "Site Inspector - #{@site.username}"
erb :'admin/site'
end
post '/admin/reports' do
end
post '/admin/site_files/train' do
require_admin
site = Site[params[:site_id]]
site_file = site.site_files_dataset.where(path: params[:path]).first
not_found if site_file.nil?
site.untrain site_file.path
site.train site_file.path, params[:classifier]
'ok'
end
get '/admin/usage' do
require_admin
today = Date.today
current_month = Date.new today.year, today.month, 1
@monthly_stats = []
month = current_month
until month.year == 2016 && month.month == 2 do
stats = DB["select sum(views) as views, sum(hits) as hits,sum(bandwidth) as bandwidth from daily_site_stats where created_at::text LIKE '#{month.year}-#{month.strftime('%m')}-%'"].first
stats.keys.each do |key|
stats[key] ||= 0
end
stats.collect {|s| s == 0}.uniq
if stats[:views] != 0 && stats[:hits] != 0 && stats[:bandwidth] != 0
popular_sites = DB[
'select sum(bandwidth) as bandwidth,username from stats left join sites on sites.id=stats.site_id where stats.created_at >= ? and stats.created_at < ? group by username order by bandwidth desc limit 50',
month,
month.next_month
].all
@monthly_stats.push stats.merge(date: month).merge(popular_sites: popular_sites)
end
month = month.prev_month
end
erb :'admin/usage'
end
get '/admin/email' do
require_admin
erb :'admin/email'
end
get '/admin/stats' do
require_admin
@stats = {
total_hosted_site_hits: DB['SELECT SUM(hits) FROM sites'].first[:sum],
total_hosted_site_views: DB['SELECT SUM(views) FROM sites'].first[:sum],
total_site_changes: DB['select max(changed_count) from sites'].first[:max],
total_sites: Site.count
}
# Start with the date of the first created site
start = Site.select(:created_at).
exclude(created_at: nil).
order(:created_at).
first[:created_at].to_date
runner = start
monthly_stats = []
now = Date.today
while Date.new(runner.year, runner.month, 1) <= Date.new(now.year, now.month, 1)
monthly_stats.push(
date: runner,
sites_created: Site.where(created_at: runner..runner.next_month).count,
total_from_start: Site.where(created_at: start..runner.next_month).count,
supporters: Site.where(created_at: start..runner.next_month).exclude(stripe_customer_id: nil).count,
)
runner = runner.next_month
end
@stats[:monthly_stats] = monthly_stats
if $stripe_cache && Time.now < $stripe_cache[:time] + 14400
customers = $stripe_cache[:customers]
else
customers = Stripe::Customer.all limit: 100000
$stripe_cache = {
customers: customers,
time: Time.now
}
end
@stats[:monthly_revenue] = 0.0
subscriptions = []
@stats[:cancelled_subscriptions] = 0
customers.each do |customer|
sub = {created_at: Time.at(customer.created)}
if customer[:subscriptions][:data].empty?
@stats[:cancelled_subscriptions] += 1
next
end
next if customer[:subscriptions][:data].first[:plan][:amount] == 0
sub[:status] = 'active'
plan = customer[:subscriptions][:data].first[:plan]
sub[:amount_without_fees] = (plan[:amount] / 100.0).round(2)
sub[:percentage_fee] = (sub[:amount_without_fees]/(100/2.9)).ceil_to(2)
sub[:fixed_fee] = 0.30
sub[:amount] = sub[:amount_without_fees] - sub[:percentage_fee] - sub[:fixed_fee]
if(plan[:interval] == 'year')
sub[:amount] = (sub[:amount] / 12).round(2)
end
@stats[:monthly_revenue] += sub[:amount]
subscriptions.push sub
end
@stats[:subscriptions] = subscriptions
# Hotwired for now
@stats[:expenses] = 300.0 #/mo
@stats[:percent_until_profit] = (
(@stats[:monthly_revenue].to_f / @stats[:expenses]) * 100
)
@stats[:poverty_threshold] = 11_945
@stats[:poverty_threshold_percent] = (@stats[:monthly_revenue].to_f / ((@stats[:poverty_threshold]/12) + @stats[:expenses])) * 100
# http://en.wikipedia.org/wiki/Poverty_threshold
@stats[:average_developer_salary] = 93_280.00 # google "average developer salary"
@stats[:percent_until_developer_salary] = (@stats[:monthly_revenue].to_f / ((@stats[:average_developer_salary]/12) + @stats[:expenses])) * 100
erb :'admin/stats'
end
post '/admin/email' do
require_admin
%i{subject body}.each do |k|
if params[k].nil? || params[k].empty?
flash[:error] = "#{k.capitalize} is missing."
redirect '/admin/email'
end
end
sites = Site.newsletter_sites
day = 0
until sites.empty?
seconds = 0.0
queued_sites = []
Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times {
break if sites.empty?
queued_sites << sites.pop
}
queued_sites.each do |site|
EmailWorker.perform_at((day.days.from_now + seconds), {
from: 'Kyle from Neocities <kyle@neocities.org>',
to: site.email,
subject: params[:subject],
body: params[:body]
})
seconds += 0.5
end
day += 1
end
flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day."
redirect '/'
end
post '/admin/banhammer' do
require_admin
if params[:usernames].empty?
flash[:error] = 'no usernames provided'
redirect '/admin'
end
usernames = params[:usernames].split("\n").collect {|u| u.strip}
deleted_count = 0
ip_deleted_count = 0
usernames.each do |username|
next if username == ''
site = Site[username: username]
next if site.nil? || site.is_banned
if !params[:classifier].empty?
site.untrain 'index.html'
site.train 'index.html', params[:classifier]
end
site.ban!
deleted_count += 1
if !params[:ban_using_ips].empty? && IPAddress.valid?(site.ip)
sites = Site.filter(ip: site.ip, is_banned: false).all
sites.each do |s|
next if usernames.include?(s.username)
s.ban!
end
ip_deleted_count += 1
end
if params[:classifier] == 'spam' || params[:classifier] == 'phishing'
next unless IPAddress.valid?(site.ip)
StopForumSpamWorker.perform_async(
username: site.username,
email: site.email,
ip: site.ip,
classifier: params[:classifier]
)
end
end
flash[:success] = "#{ip_deleted_count + deleted_count} sites have been banned, including #{ip_deleted_count} matching IPs."
redirect '/admin'
end
post '/admin/mark_nsfw' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
site.is_nsfw = true
site.admin_nsfw = true
site.save_changes validate: false
flash[:success] = 'MISSION ACCOMPLISHED'
redirect '/admin'
end
post '/admin/feature' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
site.featured_at = Time.now
site.save_changes(validate: false)
flash[:success] = 'Site has been featured.'
redirect '/admin'
end
get '/admin/masquerade/:username' do
require_admin
site = Site[username: params[:username]]
not_found if site.nil?
session[:id] = site.id
redirect '/'
end
def require_admin
redirect '/' unless is_admin?
end
def is_admin?
signed_in? && current_site.is_admin
end

View file

@ -1,307 +0,0 @@
require 'base64'
get '/api' do
@title = 'Developers API'
erb :'api'
end
post '/api/upload_hash' do
require_api_credentials
res = {}
files = []
params.each do |k,v|
res[k] = current_site.sha1_hash_match? k, v
end
api_success files: res
end
get '/api/list' do
require_api_credentials
files = []
if params[:path].nil? || params[:path].empty?
file_list = current_site.site_files
else
file_list = current_site.file_list params[:path]
end
file_list.each do |file|
new_file = {}
new_file[:path] = file[:path]
new_file[:is_directory] = file[:is_directory]
new_file[:size] = file[:size] unless file[:is_directory]
new_file[:created_at] = file[:created_at].rfc2822
new_file[:updated_at] = file[:updated_at].rfc2822
new_file[:sha1_hash] = file[:sha1_hash] unless file[:is_directory]
files << new_file
end
files.each {|f| f[:path].sub!(/^\//, '')}
api_success files: files
end
def extract_files(params, files = [])
# Check if the entire input is directly an array of files
if params.is_a?(Array)
params.each do |item|
# Call extract_files on each item if it's an Array or Hash to handle nested structures
if item.is_a?(Array) || item.is_a?(Hash)
extract_files(item, files)
end
end
elsif params.is_a?(Hash)
params.each do |key, value|
# If the value is a Hash and contains a :tempfile key, it's considered an uploaded file.
if value.is_a?(Hash) && value.has_key?(:tempfile) && !value[:tempfile].nil?
files << {filename: value[:name], tempfile: value[:tempfile]}
elsif value.is_a?(Array)
value.each do |val|
if val.is_a?(Hash) && val.has_key?(:tempfile) && !val[:tempfile].nil?
# Directly add the file info if it's an uploaded file within an array
files << {filename: val[:name], tempfile: val[:tempfile]}
elsif val.is_a?(Hash) || val.is_a?(Array)
# Recursively search for more files if the element is a Hash or Array
extract_files(val, files)
end
end
elsif value.is_a?(Hash)
# Recursively search for more files if the value is a Hash
extract_files(value, files)
end
end
end
files
end
post '/api/upload' do
require_api_credentials
files = extract_files params
if !params[:username].blank?
site = Site[username: params[:username]]
if site.nil? || site.is_deleted
api_error 400, 'site_not_found', "could not find site"
end
if site.owned_by?(current_site)
@_site = site
else
api_error 400, 'site_not_allowed', "not allowed to change this site with your current logged in site"
end
end
api_error 400, 'missing_files', 'you must provide files to upload' if files.empty?
uploaded_size = files.collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }
if current_site.file_size_too_large? uploaded_size
api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files'
end
if current_site.too_many_files?(files.length)
api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)})"
end
files.each do |file|
file[:filename] = Rack::Utils.unescape file[:filename]
if !current_site.okay_to_upload?(file)
api_error 400, 'invalid_file_type', "#{file[:filename]} is not an allowed file type for free sites, supporter required"
end
if File.directory? file[:filename]
api_error 400, 'directory_exists', "#{file[:filename]} being used by a directory"
end
if current_site.file_size_too_large? file[:tempfile].size
api_error 400, 'file_too_large' "#{file[:filename]} is too large"
end
if SiteFile.path_too_long? file[:filename]
api_error 400, 'file_path_too_long', "#{file[:filename]} path is too long"
end
if SiteFile.name_too_long? file[:filename]
api_error 400, 'file_name_too_long', "#{file[:filename]} filename is too long (exceeds #{SiteFile::FILE_NAME_CHARACTER_LIMIT} characters)"
end
end
results = current_site.store_files files
api_success 'your file(s) have been successfully uploaded'
end
post '/api/rename' do
require_api_credentials
api_error 400, 'missing_arguments', 'you must provide path and new_path' if params[:path].blank? || params[:new_path].blank?
path = current_site.scrubbed_path params[:path]
new_path = current_site.scrubbed_path params[:new_path]
unless path.is_a?(String)
api_error 400, 'bad_path', "#{path} is not a valid path, cancelled renaming"
end
unless new_path.is_a?(String)
api_error 400, 'bad_new_path', "#{new_path} is not a valid new_path, cancelled renaming"
end
site_file = current_site.site_files.select {|sf| sf.path == path}.first
if site_file.nil?
api_error 400, 'missing_file', "could not find #{path}"
end
res = site_file.rename new_path
if res.first == true
api_success "#{path} has been renamed to #{new_path}"
else
api_error 400, 'rename_error', res.last
end
end
post '/api/delete' do
require_api_credentials
api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?
paths = []
params[:filenames].each do |path|
unless path.is_a?(String)
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
end
if current_site.files_path(path) == current_site.files_path
api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site'
end
if !current_site.file_exists?(path)
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
end
if path == 'index.html' || path == '/index.html'
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
end
paths << path
end
paths.each do |path|
current_site.delete_file(path)
end
api_success 'file(s) have been deleted'
end
get '/api/info' do
if params[:sitename]
site = Site[username: params[:sitename]]
api_error 400, 'site_not_found', "could not find site #{params[:sitename]}" if site.nil? || site.is_banned
api_success api_info_for(site)
else
init_api_credentials
api_success api_info_for(current_site)
end
end
get '/api/key' do
require_api_credentials
current_site.generate_api_key! if current_site.api_key.blank?
api_success api_key: current_site.api_key
end
def api_info_for(site)
{
info: {
sitename: site.username,
views: site.views,
hits: site.hits,
created_at: site.created_at.rfc2822,
last_updated: site.site_updated_at ? site.site_updated_at.rfc2822 : nil,
domain: site.domain,
tags: site.tags.collect {|t| t.name}
}
}
end
# Catch-all for missing api calls
get '/api/:name' do
api_not_found
end
post '/api/:name' do
api_not_found
end
def require_api_credentials
return true if current_site && csrf_safe?
if !request.env['HTTP_AUTHORIZATION'].nil?
init_api_credentials
else
api_error_invalid_auth
end
end
def init_api_credentials
auth = request.env['HTTP_AUTHORIZATION']
begin
if bearer_match = auth.match(/^Bearer (.+)/)
api_key = bearer_match.captures.first
api_error_invalid_auth if api_key.nil? || api_key.empty?
else
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
end
rescue
api_error_invalid_auth
end
if defined?(api_key) && !api_key.blank?
site = Site[api_key: api_key]
elsif defined?(user) && defined?(pass)
site = Site.get_site_from_login user, pass
else
api_error_invalid_auth
end
if site.nil? || site.is_banned || site.is_deleted || !(site.required_validations_met?)
api_error_invalid_auth
end
DB['update sites set api_calls=api_calls+1 where id=?', site.id].first
session[:id] = site.id
end
def api_success(message_or_obj)
output = {result: 'success'}
if message_or_obj.is_a?(String)
output[:message] = message_or_obj
else
output.merge! message_or_obj
end
api_response(200, output)
end
def api_response(status, output)
halt status, JSON.pretty_generate(output)+"\n"
end
def api_error(status, error_type, message)
api_response(status, result: 'error', error_type: error_type, message: message)
end
def api_error_invalid_auth
api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password (or your api key)'
end
def api_not_found
api_error 404, 'not_found', 'the requested api call does not exist'
end

View file

@ -1,7 +0,0 @@
get '/blog/?' do
redirect 'https://blog.neocities.org', 301
end
get '/blog/:article' do |article|
redirect "https://blog.neocities.org/#{article}.html", 301
end

View file

@ -1,189 +0,0 @@
get '/browse/?' do
@page = params[:page]
@page = 1 if @page.not_an_integer?
if params[:tag]
params[:tag] = params[:tag].gsub(Tag::INVALID_TAG_REGEX, '').gsub(/\s+/, '').slice(0, Tag::NAME_LENGTH_MAX)
@title = "Sites tagged #{params[:tag]}"
end
if is_education?
ds = education_sites_dataset
else
ds = browse_sites_dataset
end
ds = ds.paginate @page.to_i, Site::BROWSE_PAGINATION_LENGTH
@pagination_dataset = ds
@sites = ds.all
site_ids = @sites.collect {|s| s[:id]}
tags = DB['select site_id,name from tags join sites_tags on tags.id=sites_tags.tag_id where site_id IN ?', site_ids].all
@site_tags = {}
site_ids.each do |site_id|
@site_tags[site_id] = tags.select {|t| t[:site_id] == site_id}.collect {|t| t[:name]}
end
erb :browse
end
def education_sites_dataset
ds = Site.filter is_deleted: false
ds = ds.association_join(:tags).select_all(:sites)
params[:tag] = current_site.tags.first.name
ds = ds.where ['tags.name = ?', params[:tag]]
end
def browse_sites_dataset
ds = Site.browse_dataset
ds = ds.where is_education: false
if current_site
ds = ds.or sites__id: current_site.id
if !current_site.blocking_site_ids.empty?
ds = ds.where(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
end
if current_site.blocks_dataset.count
ds = ds.where(
Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocks_dataset.select(:actioning_site_id).all.collect {|s| s.actioning_site_id})
)
end
end
if current_site && current_site.is_admin && params[:sites]
ds = ds.where sites__username: params[:sites].split(',')
return ds
end
params[:sort_by] ||= 'special_sauce'
case params[:sort_by]
when 'special_sauce'
ds = ds.where{score > 1} unless params[:tag]
ds = ds.order :score.desc, :follow_count.desc, :views.desc, :site_updated_at.desc
when 'random'
ds = ds.where{score > 3} unless params[:tag]
ds = ds.order(Sequel.lit('RANDOM()'))
when 'most_followed'
ds = ds.where{views > Site::BROWSE_MINIMUM_FOLLOWER_VIEWS}
ds = ds.where{follow_count > Site::BROWSE_FOLLOWER_MINIMUM_FOLLOWS}
ds = ds.where{updated_at > Site::BROWSE_FOLLOWER_UPDATED_AT_CUTOFF.ago} unless params[:tag]
ds = ds.order :follow_count.desc, :score.desc, :updated_at.desc
when 'last_updated'
ds = ds.where{score > 3} unless params[:tag]
ds = ds.exclude site_updated_at: nil
ds = ds.order :site_updated_at.desc
when 'newest'
ds = ds.where{views > Site::BROWSE_MINIMUM_VIEWS} unless is_admin?
ds = ds.exclude site_updated_at: nil
ds = ds.order :created_at.desc, :views.desc
when 'oldest'
ds = ds.where{score > 0.4} unless params[:tag]
ds = ds.exclude site_updated_at: nil
ds = ds.order(:created_at, :views.desc)
when 'hits'
ds = ds.where{score > 1}
ds = ds.order(:hits.desc, :site_updated_at.desc)
when 'views'
ds = ds.where{score > 3}
ds = ds.order(:views.desc, :site_updated_at.desc)
when 'featured'
ds = ds.exclude featured_at: nil
ds = ds.order :featured_at.desc
when 'tipping_enabled'
ds = ds.where tipping_enabled: true
ds = ds.where("(tipping_paypal is not null and tipping_paypal != '') or (tipping_bitcoin is not null and tipping_bitcoin != '')")
ds = ds.where{score > 1} unless params[:tag]
ds = ds.group :sites__id
ds = ds.order :follow_count.desc, :views.desc, :updated_at.desc
when 'blocks'
require_admin
ds = ds.select{[sites.*, Sequel[count(site_id)].as(:total)]}
ds = ds.inner_join :blocks, :site_id => :id
ds = ds.group :sites__id
ds = ds.order :total.desc
end
ds = ds.where ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
if params[:tag]
ds = ds.select_all :sites
ds = ds.inner_join :sites_tags, :site_id => :id
ds = ds.inner_join :tags, :id => :sites_tags__tag_id
ds = ds.where ['tags.name = ?', params[:tag]]
ds = ds.where ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
end
ds
end
def daily_search_max?
query_count = $redis_cache.get('search_query_count').to_i
query_count >= $config['google_custom_search_query_limit']
end
get '/browse/search' do
@title = 'Site Search'
@daily_search_max_reached = daily_search_max?
if @daily_search_max_reached
params[:q] = nil
end
if !params[:q].blank?
created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400)
$redis_cache.incr('search_query_count') unless created
@start = params[:start].to_i
@start = 0 if @start < 0
@resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: {
key: $config['google_custom_search_key'],
cx: $config['google_custom_search_cx'],
safe: 'active',
start: @start,
q: Rack::Utils.escape(params[:q]) + ' -filetype:pdf -filetype:txt site:*.neocities.org'
})
@items = []
if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0"
@total_results = @resp['searchInformation']['totalResults'].to_i
@resp['items'].each do |item|
link = Addressable::URI.parse(item['link'])
unencoded_path = Rack::Utils.unescape(Rack::Utils.unescape(link.path)) # Yes, it needs to be decoded twice
item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path
item['link'] = link
next if link.host == 'neocities.org'
username = link.host.split('.').first
site = Site[username: username]
next if site.nil? || site.is_deleted || site.is_nsfw
screenshot_path = unencoded_path
screenshot_path << 'index' if screenshot_path[-1] == '/'
['.html', '.htm'].each do |ext|
if site.screenshot_exists?(screenshot_path + ext, '540x405')
screenshot_path += ext
break
end
end
item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405')
@items << item
end
end
else
@items = nil
@total_results = 0
end
erb :'search'
end

View file

@ -1,21 +0,0 @@
post '/comment/:comment_id/toggle_like' do |comment_id|
require_login
content_type :json
comment = Comment[id: comment_id]
return 403 if comment.event.site.is_blocking?(current_site) || current_site.is_blocking?(comment.event.site)
liked_response = comment.toggle_site_like(current_site) ? 'liked' : 'unliked'
{result: liked_response, comment_like_count: comment.comment_likes_dataset.count, liking_site_names: comment.liking_site_usernames}.to_json
end
post '/comment/:comment_id/delete' do |comment_id|
require_login
content_type :json
comment = Comment[id: comment_id]
if comment.event.site == current_site || comment.actioning_site == current_site
comment.delete
return {result: 'success'}.to_json
end
return {result: 'error'}.to_json
end

View file

@ -1,50 +0,0 @@
get '/contact' do
erb :'contact'
end
post '/contact' do
@errors = []
if params[:email].empty? || params[:subject].empty? || params[:body].empty?
@errors << 'Please fill out all fields'
end
if 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
if !@errors.empty?
erb :'contact'
else
body = params[:body]
if current_site
body = "current username: #{current_site.username}\n\n" + body
if parent_site != current_site
body = "parent username: #{parent_site.username}\n\n" + body
end
end
if current_site && current_site.supporter?
subject = "[Neocities Supporter Contact]: #{params[:subject]}"
else
subject = "[Neocities Contact]: #{params[:subject]}"
end
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: params[:email],
to: 'contact@neocities.org',
subject: subject,
body: body,
no_footer: true
})
flash[:success] = 'Your contact message has been sent.'
redirect '/'
end
end

View file

@ -1,154 +0,0 @@
CREATE_MATCH_REGEX = /^username$|^password$|^email$|^new_tags_string$|^is_education$/
def education_whitelist_required?
return true if params[:is_education] == 'true' && $config['education_tag_whitelist']
false
end
def education_whitelisted?
return true if education_whitelist_required? && !$config['education_tag_whitelist'].select {|t| params[:new_tags_string].match(t)}.empty?
false
end
post '/create_validate_all' do
content_type :json
fields = params.select {|p| p.match CREATE_MATCH_REGEX}
begin
site = Site.new fields
rescue ArgumentError => e
if e.message == 'input string invalid'
return {error: 'invalid input'}.to_json
else
raise e
end
end
if site.valid?
return [].to_json if education_whitelisted?
end
resp = site.errors.collect {|e| [e.first, e.last.first]}
resp << ['captcha', 'Please complete the captcha.'] if params[:'h-captcha-response'].empty? && !self.class.test?
resp.to_json
end
post '/create_validate' do
content_type :json
if !params[:field].match CREATE_MATCH_REGEX
return {error: 'not a valid field'}.to_json
end
begin
site = Site.new(params[:field] => params[:value])
rescue ArgumentError => e
if e.message == 'input string invalid'
return {error: 'invalid input'}.to_json
else
raise e
end
end
site.is_education = params[:is_education]
site.valid?
field_sym = params[:field].to_sym
if site.errors[field_sym]
return {error: site.errors[field_sym].first}.to_json
end
{result: 'ok'}.to_json
end
post '/create' do
content_type :json
dashboard_if_signed_in
@site = Site.new(
username: params[:username],
password: params[:password],
email: params[:email],
new_tags_string: params[:new_tags_string],
is_education: params[:is_education] == 'true' ? true : false,
ip: request.ip,
ga_adgroupid: session[:ga_adgroupid]
)
if education_whitelist_required?
if education_whitelisted?
@site.email_confirmed = true
else
flash[:error] = 'The class tag is invalid.'
return {result: 'error'}.to_json
end
else
if !hcaptcha_valid?
flash[:error] = 'The captcha was not valid, please try again.'
return {result: 'error'}.to_json
end
if Site.ip_create_limit?(request.ip)
flash[:error] = 'Your IP address has created too many sites, please try again later or contact support.'
return {result: 'error'}.to_json
end
if Site.disposable_mx_record?(@site.email)
flash[:error] = 'Cannot use a disposable email address.'
return {result: 'error'}.to_json
end
if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(@site, request)
flash[:error] = 'Site creation is not currently available from your location, please try again later.'
return {result: 'error'}.to_json
end
if defined?(BlackBox.tutorial_required?) && BlackBox.tutorial_required?(@site, request)
@site.tutorial_required = true
end
if !@site.valid?
flash[:error] = @site.errors.first.last.first
return {result: 'error'}.to_json
end
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
begin
@site.save
rescue Sequel::UniqueConstraintViolation => e
if e.message =~ /username.+already exists/
flash[:error] = 'Username already exists.'
return {result: 'error'}.to_json
end
raise e
end
unless education_whitelisted?
send_confirmation_email @site
@site.send_email(
subject: "[Neocities] Welcome to Neocities!",
body: Tilt.new('./views/templates/email/welcome.erb', pretty: true).render(self)
)
end
session[:id] = @site.id
{result: 'ok'}.to_json
end

View file

@ -1,40 +0,0 @@
get '/dashboard' do
require_login
dashboard_init
dont_browser_cache
unless current_site.dashboard_accessed
current_site.dashboard_accessed = true
current_site.save_changes validate: false
end
erb :'dashboard/index'
end
def dashboard_init
if params[:dir] && params[:dir][0] != '/'
params[:dir] = '/'+params[:dir]
end
if !File.directory?(current_site.files_path(params[:dir]))
if !File.directory?(current_site.files_path)
flash[:error] = 'Could not find your web site, please contact support.'
signout
redirect '/'
else
flash[:error] = 'Could not find the requested directory.'
redirect '/dashboard'
end
end
@dir = params[:dir]
@file_list = current_site.file_list @dir
end
get '/dashboard/files' do
require_login
dashboard_init
dont_browser_cache
erb :'dashboard/files', layout: false
end

View file

@ -1,38 +0,0 @@
get '/dmca' do
@title = 'DMCA'
erb :'dmca'
end
get '/dmca/contact_info' do
content_type :json
{data: erb(:'dmca/contact_info', layout: false)}.to_json
end
post '/dmca/contact' do
@title = 'DMCA'
@errors = []
if params[:email].empty? || params[:subject].empty? || params[:urls].empty? || params[:body].empty?
@errors << 'Please fill out all fields'
end
if !hcaptcha_valid?
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
end
if !@errors.empty?
erb :'dmca'
else
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: params[:email],
to: 'dmca@neocities.org',
subject: "[Neocities DMCA Notice]: #{params[:subject]}",
body: "#{params[:urls].to_s}\n#{params[:body].to_s}",
no_footer: true
})
flash[:success] = 'Your DMCA notification has been sent.'
redirect '/'
end
end

View file

@ -1,29 +0,0 @@
get '/domain/new' do
require_login
@title = 'Register a Domain'
erb :'domain/new'
end
post '/domain/check_availability.json' do
require_login
content_type :json
timer = Time.now.to_i
while true
if (Time.now.to_i - timer) > 60
api_error 200, :contact_fail, 'Error contacting domain server, please try again.'
end
begin
res = $gandi.domain.available([params[:domain]])[params[:domain]]
rescue => Gandi::DataError
api_error 200, :invalid_domain, 'Domain name was invalid, please try another.'
end
api_success res unless res == 'pending'
sleep 0.2
end
end

View file

@ -1,56 +0,0 @@
post '/event/:event_id/toggle_like' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
return 403 if event.site && event.site.is_blocking?(current_site)
return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site)
liked_response = event.toggle_site_like(current_site) ? 'liked' : 'unliked'
{result: liked_response, event_like_count: event.likes_dataset.count, liking_site_names: event.liking_site_usernames}.to_json
end
post '/event/:event_id/comment' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
return 403 if event.site && event.site.is_blocking?(current_site)
return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site)
site = event.site
if(site.is_blocking?(current_site) ||
site.profile_comments_enabled == false ||
current_site.commenting_allowed? == false ||
(current_site.is_a_jerk? && event.site_id != current_site.id && !site.is_following?(current_site)) ||
params[:message].length > Site::MAX_COMMENT_SIZE)
return {result: 'error'}.to_json
end
event.add_site_comment current_site, params[:message]
{result: 'success'}.to_json
end
post '/event/:event_id/update_profile_comment' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
return {result: 'error'}.to_json unless (current_site.id == event.profile_comment.actioning_site_id &&
params[:message].length <= Site::MAX_COMMENT_SIZE)
event.profile_comment.update message: params[:message]
return {result: 'success'}.to_json
end
post '/event/:event_id/delete' do |event_id|
require_login
content_type :json
event = Event[id: event_id]
if event.site_id == current_site.id || event.actioning_site_id == current_site.id
event.delete
return {result: 'success'}.to_json
end
return {result: 'error'}.to_json
end

View file

@ -1,182 +0,0 @@
get '/?' do
if current_site
require_login
redirect '/dashboard' if current_site.is_education
@page = params[:page]
@page = 1 if @page.not_an_integer?
if params[:activity] == 'mine'
events_dataset = current_site.latest_events(@page)
elsif params[:event_id]
event = Event.select(:id).where(id: params[:event_id]).first
not_found if event.nil?
not_found if event.is_deleted
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
else
events_dataset = current_site.news_feed(@page)
end
@pagination_dataset = events_dataset
@events = events_dataset.all
current_site.events_dataset.update notification_seen: true
halt erb :'home', locals: {site: current_site}
end
if SimpleCache.expired?(:sites_count)
@sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 4.hours
else
@sites_count = SimpleCache.get :sites_count
end
if SimpleCache.expired?(:total_hits_count)
@total_hits_count = SimpleCache.store :total_hits_count, DB['SELECT SUM(hits) AS hits FROM SITES'].first[:hits], 4.hours
else
@total_hits_count = SimpleCache.get :total_hits_count
end
@total_hits_count ||= 0
if SimpleCache.expired?(:total_views_count)
@total_views_count = SimpleCache.store :total_views_count, DB['SELECT SUM(views) AS views FROM SITES'].first[:views], 4.hours
else
@total_views_count = SimpleCache.get :total_views_count
end
@total_views_count ||= 0
if SimpleCache.expired?(:changed_count)
@changed_count = SimpleCache.store :changed_count, DB['SELECT SUM(changed_count) AS changed_count FROM SITES'].first[:changed_count], 4.hours
else
@changed_count = SimpleCache.get :changed_count
end
@changed_count ||= 0
=begin
if SimpleCache.expired?(:blog_feed_html)
@blog_feed_html = ''
begin
xml = HTTP.timeout(global: 2).get('https://blog.neocities.org/feed.xml').to_s
feed = Feedjira::Feed.parse xml
feed.entries[0..2].each do |entry|
@blog_feed_html += %{<a href="#{entry.url}">#{entry.title.split('.').first}</a> <span style="float: right">#{entry.published.strftime('%b %-d, %Y')}</span><br>}
end
rescue
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
end
@blog_feed_html = SimpleCache.store :blog_feed_html, @blog_feed_html, 8.hours
else
@blog_feed_html = SimpleCache.get :blog_feed_html
end
=end
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
if SimpleCache.expired?(:featured_sites)
@featured_sites = Site.order(:score.desc).exclude(is_nsfw: true).exclude(is_deleted: true).limit(1000).all.sample(12).collect {|s| {screenshot_url: s.screenshot_url('index.html', '540x405'), uri: s.uri, title: s.title}}
SimpleCache.store :featured_sites, @featured_sites, 1.hour
else
@featured_sites = SimpleCache.get :featured_sites
end
@create_disabled = false
erb :index, layout: :index_layout
end
get '/welcome' do
require_login
redirect '/' if current_site.supporter?
@title = 'Welcome!'
erb :'welcome', locals: {site: current_site}
end
get '/education' do
redirect '/' if signed_in?
erb :education, layout: :index_layout
end
get '/donate' do
erb :'donate'
end
get '/about' do
erb :'about'
end
get '/terms' do
erb :'terms'
end
get '/privacy' do
erb :'privacy'
end
get '/press' do
erb :'press'
end
get '/legal/?' do
@title = 'Legal Guide to Neocities'
erb :'legal'
end
get '/thankyou' do
@title = 'Thank you!'
erb :'thankyou'
end
get '/cli' do
@title = 'Command Line Interface'
erb :'cli'
end
get '/forgot_username' do
@title = 'Forgot Username'
erb :'forgot_username'
end
post '/forgot_username' do
if params[:email].blank?
flash[:error] = 'Cannot use an empty email address!'
redirect '/forgot_username'
end
begin
sites = Site.get_recovery_sites_with_email params[:email]
rescue ArgumentError
redirect '/forgot_username'
end
sites.each do |site|
body = <<-EOT
Hello! This is the Neocities cat, and I have received a username lookup request using this email address.
Your username is #{site.username}
If you didn't request this, you can ignore it. Or hide under a bed. Or take a nap. Your call.
Meow,
the Neocities Cat
EOT
body.strip!
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:email],
subject: '[Neocities] Username lookup',
body: body
})
end
flash[:success] = 'If your email was valid, the Neocities Cat will send an e-mail with your username in it.'
redirect '/'
end

View file

@ -1,37 +0,0 @@
=begin
get '/mockup/home' do
erb :'mockup/home'
end
get '/mockup/edit' do
erb :'mockup/edit'
end
get '/mockup/profile' do
require_login
erb :'mockup/profile', locals: {site: current_site}
end
get '/mockup/browse' do
erb :'mockup/browse'
end
get '/mockup/tips' do
erb :'mockup/tips'
end
get '/mockup/welcome' do
require_login
erb :'mockup/welcome', locals: {site: current_site}
end
get '/mockup/stats' do
require_login
erb :'mockup/stats', locals: {site: current_site}
end
get '/mockup/tutorial-c1p2' do
require_login
erb :'mockup/tutorial-c1p2', locals: {site: current_site}
end
=end

View file

@ -1,86 +0,0 @@
get '/password_reset' do
@title = 'Password Reset'
redirect '/' if signed_in?
erb :'password_reset'
end
post '/send_password_reset' do
if params[:email].blank?
flash[:error] = 'You must enter a valid email address.'
redirect '/password_reset'
end
sites = Site.get_recovery_sites_with_email params[:email]
if sites.length > 0
token = SecureRandom.uuid.gsub('-', '')+'-'+Time.now.to_i.to_s
sites.each do |site|
next unless site.parent?
site.password_reset_token = token
site.save_changes validate: false
body = <<-EOT
Hello! This is the Penelope the Neocities cat, and I have received a password reset request for your e-mail address.
Go to this URL to reset your password: https://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{Rack::Utils.escape(token)}
This link will expire in 24 hours.
If you didn't request this password reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
Meow,
Penelope
EOT
body.strip!
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:email],
subject: '[Neocities] Password Reset',
body: body
})
end
end
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
get '/password_reset_confirm' do
@title = 'Password Reset Confirm'
if params[:token].nil? || params[:token].strip.empty?
flash[:error] = 'Token cannot be empty.'
redirect '/'
end
reset_site = Site.where(username: params[:username], password_reset_token: params[:token]).first
if reset_site.nil?
flash[:error] = 'Could not find a site with this username and token.'
redirect '/'
end
timestamp = Time.at(reset_site.password_reset_token.split('-').last.to_i)
if Time.now.to_i - timestamp.to_i > Site::PASSWORD_RESET_EXPIRATION_TIME
flash[:error] = 'Token has expired.'
redirect '/'
end
if reset_site.is_deleted
unless reset_site.undelete!
flash[:error] = "Sorry, we cannot restore this account."
redirect '/'
end
end
reset_site.password_reset_confirmed = true
reset_site.save_changes
session[:id] = reset_site.id
redirect '/settings#password'
end

View file

@ -1,3 +0,0 @@
get '/plan/?' do
redirect '/supporter'
end

View file

@ -1,393 +0,0 @@
require 'socket'
require 'ipaddr'
get '/settings/?' do
require_login
@site = parent_site
erb :'settings/account'
end
def require_ownership_for_settings
@site = Site[username: params[:username]]
not_found if @site.nil?
unless @site.owned_by? parent_site
flash[:error] = 'Cannot edit this site, you do not have permission.'
redirect request.referrer
end
end
get '/settings/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?
require_login
require_ownership_for_settings
@title = "Site settings for #{username}"
erb :'settings/site'
end
post '/settings/:username/delete' do
require_login
require_ownership_for_settings
if params[:confirm_username] != @site.username
flash[:error] = 'Site user name and entered user name did not match.'
redirect "/settings/#{@site.username}#delete"
end
@site.deleted_reason = params[:deleted_reason]
@site.save validate: false
@site.destroy
flash[:success] = 'Site deleted.'
if @site.username == current_site.username
signout
redirect '/'
end
redirect '/settings#sites'
end
post '/settings/:username/profile' do
require_login
require_ownership_for_settings
@site.update(
profile_comments_enabled: params[:site][:profile_comments_enabled],
profile_enabled: params[:site][:profile_enabled]
)
flash[:success] = 'Profile settings changed.'
redirect "/settings/#{@site.username}#profile"
end
post '/settings/:username/change_name' do
require_login
require_ownership_for_settings
old_site = Site[username: @site.username]
old_username = @site.username
if params[:name] == nil || params[:name] == ''
flash[:error] = 'Name cannot be blank.'
redirect "/settings/#{@site.username}#username"
end
if old_username.downcase == params[:name].downcase
flash[:error] = 'You already have this name.'
redirect "/settings/#{@site.username}#username"
end
old_host = @site.host
old_site_file_paths = @site.site_files.collect {|site_file| site_file.path}
@site.username = params[:name]
if @site.valid?
DB.transaction {
@site.save_changes
@site.move_files_from old_username
}
old_site.delete_all_thumbnails_and_screenshots
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>"
redirect "/settings/#{@site.username}#username"
else
flash[:error] = @site.errors.first.last.first
redirect "/settings/#{old_username}#username"
end
end
post '/settings/:username/tipping' do
require_login
require_ownership_for_settings
current_site.tipping_enabled = params[:site][:tipping_enabled]
current_site.tipping_paypal = params[:site][:tipping_paypal]
current_site.tipping_bitcoin = params[:site][:tipping_bitcoin]
if current_site.valid?
current_site.save_changes
flash[:success] = "Tip settings have been updated."
else
flash[:error] = current_site.errors.first.last.first
end
redirect "/settings/#{current_site.username}#tipping"
end
post '/settings/:username/change_nsfw' do
require_login
require_ownership_for_settings
redirect "/settings/#{@site.username}" if @site.admin_nsfw == true
@site.is_nsfw = params[:is_nsfw]
@site.save_changes validate: false
flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+'
redirect "/settings/#{@site.username}#nsfw"
end
post '/settings/:username/custom_domain' do
require_login
require_ownership_for_settings
original_domain = @site.domain
@site.domain = params[:domain]
if params[:domain] =~ /^www\..+$/i
flash[:error] = 'Cannot begin with www - please only enter the domain name.'
redirect "/settings/#{@site.username}/#custom_domain"
end
begin
addr = IPAddr.new @site.values[:domain]
if addr.ipv4? || addr.ipv6?
flash[:error] = 'IP addresses are not allowed. Please enter a valid domain name.'
redirect "/settings/#{@site.username}#custom_domain"
end
rescue IPAddr::InvalidAddressError
end
begin
Socket.gethostbyname @site.values[:domain]
rescue SocketError, ResolutionError => e
flash[:error] = "The domain isn't setup to use Neocities yet, cannot add. Please make the A and CNAME record changes where you registered your domain."
redirect "/settings/#{@site.username}#custom_domain"
end
if @site.valid?
@site.save_changes
if @site.domain != original_domain
LetsEncryptWorker.perform_async @site.id
# Sometimes the www record isn't ready for some reason, so try a delay to fix that.
LetsEncryptWorker.perform_in 40.minutes, @site.id
end
flash[:success] = 'The domain has been successfully updated! Make sure your configuration with the domain registrar is correct. It could take a while for the changes to take effect (15-40 minutes), please be patient.'
redirect "/settings/#{@site.username}#custom_domain"
else
flash[:error] = @site.errors.first.last.first
redirect "/settings/#{@site.username}#custom_domain"
end
end
post '/settings/:username/bluesky_set_did' do
require_login
require_ownership_for_settings
# todo standards based validation
if params[:did].length > 50
flash[:error] = 'DID provided was too long'
elsif !params[:did].match(/^did:plc:([a-z|0-9)]+)$/)
flash[:error] = 'DID was invalid'
else
tmpfile = Tempfile.new 'atproto-did'
tmpfile.write params[:did]
tmpfile.close
@site.store_files [{filename: '.well-known/atproto-did', tempfile: tmpfile}]
$redis_proxy.hdel "dns-_atproto.#{@site.username}.neocities.org", 'TXT'
flash[:success] = 'DID set! You can now verify the handle on the Bluesky app.'
end
redirect "/settings/#{@site.username}#bluesky"
end
post '/settings/:username/generate_api_key' do
require_login
require_ownership_for_settings
is_new = @site.api_key.nil?
@site.generate_api_key!
msg = is_new ? "New API key has been generated." : "API key has been regenerated."
flash[:success] = msg
redirect "/settings/#{@site.username}#api_key"
end
post '/settings/change_password' do
require_login
if !current_site.password_reset_confirmed && !Site.valid_login?(parent_site.username, params[:current_password])
flash[:error] = 'Your provided password does not match the current one.'
redirect "/settings#password"
end
parent_site.password = params[:new_password]
parent_site.valid?
if params[:new_password] != params[:new_password_confirm]
parent_site.errors.add :password, 'New passwords do not match.'
end
parent_site.password_reset_token = nil
parent_site.password_reset_confirmed = false
if parent_site.errors.empty?
parent_site.save_changes
parent_site.send_email(
subject: "[Neocities] Your password has been changed",
body: Tilt.new('./views/templates/email/password_changed.erb', pretty: true).render(self)
)
flash[:success] = 'Successfully changed password.'
redirect "/settings#password"
else
flash[:error] = current_site.errors.first.last.first
redirect '/settings#password'
end
end
post '/settings/change_email' do
require_login
if params[:from_confirm]
redirect_url = "/site/#{parent_site.username}/confirm_email"
else
redirect_url = '/settings#email'
end
if params[:email] == parent_site.email
flash[:error] = 'You are already using this email address for this account.'
redirect redirect_url
end
previous_email = parent_site.email
parent_site.email = params[:email]
parent_site.email_confirmation_token = SecureRandom.hex 3
parent_site.email_confirmed = false
parent_site.password_reset_token = nil
if parent_site.valid?
parent_site.save_changes
send_confirmation_email
parent_site.send_email(
subject: "[Neocities] Your email address has been changed",
body: Tilt.new('./views/templates/email/email_changed.erb', pretty: true).render(self, site: parent_site, previous_email: previous_email)
)
if !parent_site.supporter?
session[:fromsettings] = true
redirect "/site/#{parent_site.email}/confirm_email"
else
flash[:success] = 'Email address changed.'
redirect '/settings#email'
end
end
flash[:error] = parent_site.errors.first.last.first
redirect redirect_url
end
post '/settings/change_email_notification' do
require_login
owner = current_site.owner
owner.send_emails = params[:send_emails]
owner.send_comment_emails = params[:send_comment_emails]
owner.send_follow_emails = params[:send_follow_emails]
owner.email_invoice = params[:email_invoice]
owner.save_changes validate: false
flash[:success] = 'Email notification settings have been updated.'
redirect '/settings#email'
end
post '/settings/change_editor_settings' do
require_login
owner = current_site.owner
owner.editor_autocomplete_enabled = params[:editor_autocomplete_enabled]
owner.editor_font_size = params[:editor_font_size]
owner.editor_keyboard_mode = params[:editor_keyboard_mode]
owner.editor_tab_width = params[:editor_tab_width]
owner.editor_help_tooltips = params[:editor_help_tooltips]
owner.save_changes validate: false
@filename = params[:path]
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
end
post '/settings/create_child' do
require_login
if !current_site.plan_feature(:unlimited_site_creation)
flash[:error] = 'Cannot create a new site with your current plan, please become a supporter.'
redirect '/settings#sites'
end
site = Site.new
site.parent_site_id = parent_site.id
site.username = params[:username]
if site.valid?
site.save
flash[:success] = 'Your new site has been created! To manage it, click your username in the top right and go to "Switch Site".'
redirect '/settings#sites'
else
flash[:error] = site.errors.first.last.first
redirect '/settings#sites'
end
end
get '/settings/unsubscribe_email/?' do
redirect "/settings/#email" if signed_in?
if params[:email] && params[:token] && params[:email] != '' && Site.valid_email_unsubscribe_token?(params[:email], params[:token])
Site.where(email: params[:email]).all.each do |site|
site.send_emails = false
site.save_changes validate: false
end
@message = "You have been successfully unsubscribed from future emails to #{params[:email]}. Our apologies for the inconvenience."
else
@message = 'There was an error unsubscribing your email address. Please contact support.'
end
erb :'settings/account/unsubscribe'
end
post '/settings/update_card' do
require_login
customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
old_card_ids = customer.sources.collect {|s| s.id}
begin
customer.sources.create source: params[:stripe_token]
rescue Stripe::InvalidRequestError, Stripe::CardError => e
if e.message.match /cannot use a.+token more than once/
flash[:error] = 'Card is already being used.'
redirect '/settings#billing'
elsif e.message.match /Your card was declined/
flash[:error] = 'The card was declined. Please contact your bank.'
else
raise e
end
end
old_card_ids.each do |card_id|
customer.sources.retrieve(card_id).delete
end
flash[:success] = 'Card information updated.'
redirect '/settings#billing'
end

View file

@ -1,80 +0,0 @@
get '/signin/?' do
dashboard_if_signed_in
@title = 'Sign In'
erb :'signin/index'
end
post '/signin' do
dashboard_if_signed_in
if Site.valid_login? params[:username], params[:password]
site = Site.get_with_identifier params[:username]
if site.is_banned
flash[:error] = 'Invalid login.'
flash[:username] = params[:username]
redirect '/signin'
end
if site.is_deleted
session[:deleted_site_id] = site.id
redirect '/signin/restore'
end
session[:id] = site.id
redirect '/'
else
flash[:error] = 'Invalid login.'
flash[:username] = params[:username]
redirect '/signin'
end
end
get '/signin/restore' do
redirect '/' unless session[:deleted_site_id]
@site = Site[session[:deleted_site_id]]
redirect '/' if @site.nil?
@title = 'Restore Deleted Site'
erb :'signin/restore'
end
get '/signin/cancel_restore' do
session[:deleted_site_id] = nil
flash[:success] = 'Site restore was cancelled.'
redirect '/'
end
post '/signin/restore' do
redirect '/' unless session[:deleted_site_id]
@site = Site[session[:deleted_site_id]]
session[:deleted_site_id] = nil
if @site.undelete!
session[:id] = @site.id
else
flash[:error] = "Sorry, we cannot restore this account."
end
redirect '/'
end
get '/signin/:username' do
require_login
@site = Site[username: params[:username]]
not_found if @site.nil?
if @site.owned_by? current_site
session[:id] = @site.id
redirect request.referrer
end
flash[:error] = 'You do not have permission to switch to this site.'
redirect request.referrer
end
post '/signout' do
require_login
signout
redirect '/'
end

View file

@ -1,388 +0,0 @@
get '/site/:username.rss' do |username|
site = Site[username: username]
halt 404 if site.nil? || (current_site && site.is_blocking?(current_site))
content_type :xml
site.to_rss
end
get '/site/:username/?' do |username|
site = Site[username: username]
# TODO: There should probably be a "this site was deleted" page.
not_found if site.nil? || site.is_banned || site.is_deleted || (current_site && site.is_blocking?(current_site))
redirect '/' if site.is_education
redirect site.uri unless site.profile_enabled
@title = site.title
@page = params[:page]
@page = 1 if @page.not_an_integer?
if params[:event_id]
not_found if params[:event_id].not_an_integer?
not_found if params[:event_id].to_i > 2_147_483_647 # max integer
event = Event.where(id: params[:event_id]).exclude(is_deleted: true).first
not_found if event.nil?
event_site = event.site
event_actioning_site = event.actioning_site
not_found if current_site && event_site && event_site.is_blocking?(current_site)
not_found if current_site && event_actioning_site && event_actioning_site.is_blocking?(current_site)
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
else
events_dataset = site.latest_events(@page, current_site)
end
@page_count = events_dataset.page_count || 1
@pagination_dataset = events_dataset
@latest_events = events_dataset.all
meta_robots 'noindex, follow'
erb :'site', locals: {site: site, is_current_site: site == current_site}
end
MAX_STAT_POINTS = 30
get '/site/:username/stats' do
@default_stat_points = 7
@site = Site[username: params[:username]]
not_found if @site.nil? || @site.is_banned || @site.is_deleted || (current_site && @site.is_blocking?(current_site))
@title = "Site stats for #{@site.host}"
@stats = {}
%i{referrers locations paths}.each do |stat|
@stats[stat] = @site.send("stat_#{stat}_dataset".to_sym).order(:views.desc).limit(100).all
end
@stats[:locations].collect! do |location|
location_name = ''
location_name += location.city_name if location.city_name
if location.region_name
# Some of the region names are numbers for some reason.
begin
Integer(location.region_name)
rescue
location_name += ', ' unless location_name == ''
location_name += location.region_name
end
end
if location.country_code2 && !$country_codes[location.country_code2].nil?
location_name += ', ' unless location_name == ''
location_name += $country_codes[location.country_code2]
end
location_hash = {name: location_name, views: location.views}
if location.latitude && location.longitude
location_hash.merge! latitude: location.latitude, longitude: location.longitude
end
location_hash
end
stats_dataset = @site.stats_dataset.order(:created_at.desc).exclude(created_at: Date.today)
if @site.supporter?
unless params[:days].to_s == 'sincethebigbang'
unless params[:days].not_an_integer?
stats_dataset = stats_dataset.limit params[:days]
else
params[:days] = @default_stat_points
stats_dataset = stats_dataset.limit @default_stat_points
end
end
else
stats_dataset = stats_dataset.limit @default_stat_points
end
stats = stats_dataset.all.reverse
if current_site && @site.owned_by?(current_site) && params[:format] == 'csv'
content_type 'application/csv'
attachment "#{current_site.username}-stats.csv"
return CSV.generate do |csv|
csv << ['day', 'hits', 'views', 'bandwidth']
stats.each do |s|
csv << [s[:created_at].to_s, s[:hits], s[:views], s[:bandwidth]]
end
end
end
if stats.length > MAX_STAT_POINTS
stats = stats.select.with_index {|a, i| (i % (stats.length / MAX_STAT_POINTS.to_f).round) == 0}
end
@stats[:stat_days] = stats
@multi_tooltip_template = "<%= datasetLabel %> - <%= value %>"
erb :'site/stats', locals: {site: @site}
end
post '/site/:username/set_editor_theme' do
require_login
current_site.editor_theme = params[:editor_theme]
current_site.save_changes validate: false
'ok'
end
get '/site/:username/follows' do |username|
@title = "Sites #{username} follows"
@site = Site[username: username]
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
params[:page] ||= "1"
@pagination_dataset = @site.followings_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
erb :'site/follows'
end
get '/site/:username/followers' do |username|
@title = "Sites that follow #{username}"
@site = Site[username: username]
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
params[:page] ||= "1"
@pagination_dataset = @site.follows_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
erb :'site/followers'
end
post '/site/:username/comment' do |username|
require_login
site = Site[username: username]
redirect request.referer if current_site && (site.is_blocking?(current_site) || current_site.is_blocking?(site))
last_comment = site.profile_comments_dataset.order(:created_at.desc).first
if last_comment && last_comment.message == params[:message] && last_comment.created_at > 2.hours.ago
redirect request.referer
end
if site.profile_comments_enabled == false ||
params[:message].empty? ||
params[:message].length > Site::MAX_COMMENT_SIZE ||
site.is_blocking?(current_site) ||
current_site.is_blocking?(site) ||
current_site.commenting_allowed? == false ||
(current_site.is_a_jerk? && site.id != current_site.id && !site.is_following?(current_site))
redirect request.referrer
end
site.add_profile_comment(
actioning_site_id: current_site.id,
message: params[:message]
)
redirect request.referrer
end
post '/site/:site_id/toggle_follow' do |site_id|
require_login
content_type :json
site = Site[id: site_id]
return 403 if site.is_blocking?(current_site)
{result: (current_site.toggle_follow(site) ? 'followed' : 'unfollowed')}.to_json
end
post '/site/create_directory' do
require_login
path = "#{params[:dir] || ''}/#{params[:name]}"
result = current_site.create_directory path
if result != true
flash[:error] = result
end
redirect "/dashboard?dir=#{Rack::Utils.escape params[:dir]}"
end
get '/site/:username/confirm_email/:token' do
@title = 'Confirm email'
if current_site && current_site.email_confirmed
return erb(:'site_email_confirmed')
end
site = Site[username: params[:username]]
if site.nil?
return erb(:'site_email_not_confirmed')
end
if site.email_confirmed
return erb(:'site_email_confirmed')
end
if site.email_confirmation_token == params[:token]
site.email_confirmation_token = nil
site.email_confirmation_count = 0
site.email_confirmed = true
site.save_changes
erb :'site_email_confirmed'
else
erb :'site_email_not_confirmed'
end
end
get '/site/:username/confirm_email' do
require_login
@title = 'Confirm your Email Address'
@fromsettings = session[:fromsettings]
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
erb :'site/confirm_email'
end
post '/site/:username/confirm_email' do
require_login
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
# Update email, resend token
if params[:email]
send_confirmation_email @site
end
if params[:token].blank?
flash[:error] = 'You must enter a valid token.'
redirect "/site/#{current_site.username}/confirm_email"
end
if current_site.email_confirmation_token == params[:token]
current_site.email_confirmation_token = nil
current_site.email_confirmation_count = 0
current_site.email_confirmed = true
current_site.save_changes
if session[:fromsettings]
session[:fromsettings] = nil
flash[:success] = 'Email address changed.'
redirect '/settings#email'
end
redirect '/tutorial'
else
flash[:error] = 'You must enter a valid token.'
redirect "/site/#{current_site.username}/confirm_email"
end
end
post '/site/:username/block' do |username|
require_login
site = Site[username: username]
redirect request.referer if current_site.id == site.id
current_site.block! site
if request.referer.match /\/site\/#{username}/i
redirect '/'
else
redirect request.referer
end
end
get '/site/:username/unblock' do |username|
require_login
site = Site[username: username]
if site.nil? || current_site.id == site.id
redirect request.referer
end
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

@ -1,270 +0,0 @@
get '/site_files/new_page' do
require_login
@title = 'New Page'
erb :'site_files/new_page'
end
# Redirect from original path
get '/site_files/new' do
require_login
redirect '/site_files/new_page'
end
post '/site_files/create' do
require_login
@errors = []
filename = params[:filename]
filename.gsub!(/[^a-zA-Z0-9_\-.]/, '')
redirect_uri = '/dashboard'
redirect_uri += "?dir=#{Rack::Utils.escape params[:dir]}" if params[:dir]
if filename.nil? || filename.strip.empty?
flash[:error] = 'You must provide a file name.'
redirect redirect_uri
end
name = "#{filename}"
name = "#{params[:dir]}/#{name}" if params[:dir]
name = current_site.scrubbed_path name
if current_site.file_exists?(name)
flash[:error] = %{Web page "#{Rack::Utils.escape_html name}" already exists! Choose another name.}
redirect redirect_uri
end
extname = File.extname name
unless extname.empty? || extname.match(/^\.#{Site::EDITABLE_FILE_EXT}/i)
flash[:error] = "Must be an editable text file type (#{Site::VALID_EDITABLE_EXTENSIONS.join(', ')})."
redirect redirect_uri
end
site_file = current_site.site_files_dataset.where(path: name).first
if site_file
flash[:error] = 'File already exists, cannot create.'
redirect redirect_uri
end
if extname.match(/^\.html|^\.htm/i)
begin
current_site.install_new_html_file name
rescue Sequel::UniqueConstraintViolation
end
else
file_path = current_site.files_path(name)
FileUtils.touch file_path
File.chmod 0640, file_path
site_file ||= SiteFile.new site_id: current_site.id, path: name
site_file.size = 0
site_file.set size: 0
site_file.set sha1_hash: Digest::SHA1.hexdigest('')
site_file.set updated_at: Time.now
site_file.save
end
escaped_name = Rack::Utils.escape_html name
flash[:success] = %{#{escaped_name} was created! <a style="color: #FFFFFF; text-decoration: underline" href="/site_files/text_editor/#{escaped_name}">Click here to edit it</a>.}
redirect redirect_uri
end
def file_upload_response(error=nil)
if error
flash[:error] = error
end
if params[:from_button]
query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : ''
redirect "/dashboard#{query_string}"
else
halt 406, error if error
halt 200, 'File(s) successfully uploaded.'
end
end
def require_login_file_upload_ajax
file_upload_response 'You are not signed in!' unless signed_in?
end
post '/site_files/delete' do
require_login
path = HTMLEntities.new.decode params[:filename]
begin
current_site.delete_file path
rescue Sequel::NoExistingObject
# the deed was presumably already done
end
flash[:success] = "Deleted #{Rack::Utils.escape_html params[:filename]}."
dirname = Pathname(path).dirname
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
redirect "/dashboard#{dir_query}"
end
post '/site_files/rename' do
require_login
path = HTMLEntities.new.decode params[:path]
new_path = HTMLEntities.new.decode params[:new_path]
site_file = current_site.site_files.select {|s| s.path == path}.first
escaped_path = Rack::Utils.escape_html path
escaped_new_path = Rack::Utils.escape_html new_path
if site_file.nil?
flash[:error] = "File #{escaped_path} does not exist."
else
res = site_file.rename new_path
if res.first == true
flash[:success] = "Renamed #{escaped_path} to #{escaped_new_path}"
else
flash[:error] = "Failed to rename #{escaped_path} to #{escaped_new_path}: #{Rack::Utils.escape_html res.last}"
end
end
dirname = Pathname(path).dirname
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
redirect "/dashboard#{dir_query}"
end
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|
ZipTricks::Streamer.open(out) do |zip|
Dir["#{directory_path}/**/*"].each do |file|
next if File.directory?(file)
zip_path = file.sub("#{directory_path}/", '')
zip.write_stored_file(zip_path) do |file_writer|
File.open(file, 'rb') do |file|
IO.copy_stream(file, file_writer)
end
end
end
end
end
end
get %r{\/site_files\/download\/(.+)} do
require_login
dont_browser_cache
not_found if params[:captures].nil? || params[:captures].length != 1
filename = params[:captures].first
attachment filename
send_file current_site.current_files_path(filename)
end
get %r{\/site_files\/text_editor\/(.+)} do
require_login
dont_browser_cache
@filename = params[:captures].first
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
end
get '/site_files/text_editor' do
require_login
dont_browser_cache
@filename = params[:filename]
extname = File.extname @filename
@ace_mode = case extname
when /htm|html/ then 'html'
when /js/ then 'javascript'
when /md/ then 'markdown'
when /css/ then 'css'
else
nil
end
file_path = current_site.current_files_path @filename
if File.directory? file_path
flash[:error] = 'Cannot edit a directory.'
redirect '/dashboard'
end
if !File.exist?(file_path)
flash[:error] = 'We could not find the requested file.'
redirect '/dashboard'
end
@title = "Editing #{@filename}"
erb :'site_files/text_editor'
end
get '/site_files/allowed_types' do
@title = 'Allowed File Types'
erb :'site_files/allowed_types'
end
get '/site_files/hotlinking' do
@title = 'Hotlinking Information'
erb :'site_files/hotlinking'
end
get '/site_files/mount_info' do
@title = 'Site Mount Information'
erb :'site_files/mount_info'
end
post '/site_files/chat' do
require_login
dont_browser_cache
headers 'X-Accel-Buffering' => 'no'
halt(403) unless parent_site.supporter?
# Ensure the request is treated as a stream
stream do |out|
url = 'https://api.anthropic.com/v1/messages'
headers = {
"anthropic-version" => "2023-06-01",
"anthropic-beta" => "messages-2023-12-15",
"content-type" => "application/json",
"x-api-key" => $config['anthropic_api_key']
}
body = {
model: "claude-3-haiku-20240307",
system: params[:system],
messages: JSON.parse(params[:messages]),
max_tokens: 4096,
temperature: 0.5,
stream: true
}.to_json
res = HTTP.headers(headers).post(url, body: body)
while(buffer = res.body.readpartial)
out << buffer
end
end
end

View file

@ -1,199 +0,0 @@
get '/supporter/?' do
@title = 'Become a Supporter'
erb :'welcome'
end
post '/supporter/end' do
require_login
redirect '/' unless parent_site.paying_supporter?
parent_site.end_supporter_membership!
flash[:success] = "Your supporter membership has been cancelled. We're sorry to see you go, but thanks again for your support! Remember, you can always become a supporter again in the future."
redirect '/supporter'
end
post '/supporter/update' do
require_login
plan_type = 'supporter'
if is_special_upgrade
require_admin
site = Site[username: params[:username]]
plan_type = 'special'
if site.nil?
flash[:error] = 'Cannot find the requested user.'
redirect '/admin'
end
end
site ||= parent_site
DB.transaction do
if site.stripe_customer_id
customer = Stripe::Customer.retrieve site.stripe_customer_id
customer.cards.each {|card| card.delete}
if !params[:stripe_token].blank?
customer.sources.create source: params[:stripe_token]
end
begin
subscription = customer.subscriptions.create plan: plan_type
rescue Stripe::CardError => e
flash[:error] = "Error: #{Rack::Utils.escape_html e.message}"
redirect '/supporter'
end
site.plan_ended = false
site.plan_type = plan_type
site.stripe_subscription_id = subscription.id
site.save_changes validate: false
else
begin
customer = Stripe::Customer.create(
source: params[:stripe_token],
description: "#{site.username} - #{site.id}",
email: site.email,
plan: plan_type
)
rescue Stripe::CardError => e
flash[:error] = "Error: #{Rack::Utils.escape_html e.message} This is likely caused by incorrect information, or an issue with your credit card. Please try again, or contact your bank."
redirect '/supporter'
end
site.stripe_customer_id = customer.id
site.stripe_subscription_id = customer.subscriptions.first.id
site.plan_ended = false
site.plan_type = plan_type
site.save_changes validate: false
end
end
if site.email
if is_special_upgrade
site.send_email(
subject: "[Neocities] Your site has been upgraded to supporter!",
body: Tilt.new('./views/templates/email/supporter_upgrade.erb', pretty: true).render(self)
)
redirect '/admin'
end
site.send_email(
subject: "[Neocities] You've become a supporter!",
body: Tilt.new('./views/templates/email/subscription.erb', pretty: true).render(
self, {
username: site.username,
plan_name: Site::PLAN_FEATURES[params[:plan_type].to_sym][:name],
plan_space: Site::PLAN_FEATURES[params[:plan_type].to_sym][:space].pretty,
plan_bw: Site::PLAN_FEATURES[params[:plan_type].to_sym][:bandwidth].pretty
})
)
end
if is_special_upgrade
flash[:success] = "#{site.username} has been upgraded to supporter."
redirect '/admin'
end
redirect '/supporter/thanks'
end
get '/supporter/thanks' do
@title = 'Supporter Confirmation'
require_login
erb :'supporter/thanks'
end
get '/supporter/bitcoin/?' do
@title = 'Bitcoin Supporter'
erb :'supporter/bitcoin'
end
get '/supporter/paypal' do
require_login
redirect '/supporter' if parent_site.supporter?
hash = paypal_recurring_authorization_hash
if parent_site.paypal_token
hash.merge! token: parent_site.paypal_token
end
ppr = PayPal::Recurring.new hash
paypal_response = ppr.checkout
if !paypal_response.valid?
flash[:error] = 'There was an issue connecting to Paypal, please contact support.'
redirect '/supporter'
end
redirect paypal_response.checkout_url
end
get '/supporter/paypal/return' do
require_login
if params[:token].nil? || params[:PayerID].nil?
flash[:error] = 'Unknown error, could not complete the request. Please contact Neocities support.'
end
ppr = PayPal::Recurring.new(paypal_recurring_hash.merge(
token: params[:token],
payer_id: params[:PayerID]
))
paypal_response = ppr.request_payment
unless paypal_response.approved? && paypal_response.completed?
flash[:error] = 'Unknown error, could not complete the request. Please contact Neocities support.'
redirect '/supporter'
end
site = current_site.parent || current_site
ppr = PayPal::Recurring.new(paypal_recurring_authorization_hash.merge(
frequency: 1,
token: params[:token],
period: :monthly,
reference: site.id.to_s,
payer_id: params[:PayerID],
start_at: 1.month.from_now,
failed: 3,
outstanding: :next_billing
))
paypal_response = ppr.create_recurring_profile
site.paypal_token = params[:token]
site.paypal_profile_id = paypal_response.profile_id
site.paypal_active = true
site.plan_type = 'supporter'
site.plan_ended = false
site.save_changes validate: false
redirect '/supporter/thanks'
end
def paypal_recurring_hash
{
ipn_url: "https://neocities.org/webhooks/paypal",
description: 'Neocities Supporter - Monthly',
amount: Site::PLAN_FEATURES[:supporter][:price].to_s,
currency: 'USD'
}
end
def paypal_recurring_authorization_hash
paypal_recurring_hash.merge(
return_url: "https://neocities.org/supporter/paypal/return",
cancel_url: "https://neocities.org/supporter",
ipn_url: "https://neocities.org/webhooks/paypal"
)
end
def is_special_upgrade
params[:username] && params[:plan_type] == 'special'
end

View file

@ -1,85 +0,0 @@
require 'base64'
require 'zlib'
require 'rubygems/package'
get '/sysops/proxy/map.txt' do
require_proxy_auth
domains = ''
Site.exclude(domain: nil).
exclude(domain: '').
select(:username,:domain).
all.
collect do |s|
domains << "#{s.domain} #{s.username};\n"
end
content_type :text
domains
end
get '/sysops/proxy/sslcerts.tar.gz' do
require_proxy_auth
sites = Site.ssl_sites
nginx_config = ''
tar = StringIO.new
Gem::Package::TarWriter.new(tar) do |writer|
writer.mkdir 'sslcerts', 0740
writer.mkdir 'sslcerts/certs', 0740
sites.each do |site|
writer.add_file "sslcerts/certs/#{site.username}.key", 0640 do |f|
f.write site.ssl_key
end
writer.add_file "sslcerts/certs/#{site.username}.crt", 0640 do |f|
f.write site.ssl_cert
end
nginx_config << %{
server {
listen 443 ssl;
server_name #{site.domain} *.#{site.domain};
ssl_certificate sslsites/certs/#{site.username}.crt;
ssl_certificate_key sslsites/certs/#{site.username}.key;
location / {
proxy_http_version 1.1;
proxy_set_header Host #{site.username}.neocities.org;
proxy_pass http://127.0.0.1$request_uri;
}
}
}.unindent
end
writer.add_file "sslcerts/sslsites.conf", 0640 do |f|
f.write nginx_config
end
end
tar.rewind
package = StringIO.new 'b'
package.set_encoding 'binary'
gzip = Zlib::GzipWriter.new package
gzip.write tar.read
tar.close
gzip.finish
package.rewind
attachment
package.read
end
class ProxyAccessViolation < StandardError; end
def require_proxy_auth
begin
auth = request.env['HTTP_AUTHORIZATION']
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
raise ProxyAccessViolation unless pass == $config['proxy_pass']
rescue
raise ProxyAccessViolation, "Violator: #{request.ip}" unless pass == $config['proxy_pass']
end
end

View file

@ -1,31 +0,0 @@
post '/tags/add' do
require_login
current_site.new_tags_string = params[:tags]
if current_site.valid?
current_site.save_tags
else
flash[:errors] = current_site.errors.first.last.first
end
redirect request.referer
end
post '/tags/remove' do
require_login
if params[:tags].is_a?(Array)
DB.transaction {
params[:tags].each do |tag|
tag_to_remove = current_site.tags.select {|t| t.name == tag}.first
current_site.remove_tag(tag_to_remove) if tag_to_remove
end
}
end
redirect request.referer
end
get '/tags/autocomplete/:name.json' do |name|
Tag.autocomplete(name).collect {|t| t[:name]}.to_json
end

View file

@ -1,52 +0,0 @@
def default_tutorial_html
<<-EOT.strip
<!DOCTYPE html>
<html>
<body>
Hello World!
</body>
</html>
EOT
end
get '/tutorials' do
erb :'tutorials'
end
get '/tutorial/?' do
require_login
erb :'tutorial/index'
end
get '/tutorial/:section/?' do
require_login
not_found unless %w{html}.include?(params[:section])
redirect "/tutorial/#{params[:section]}/1"
end
get '/tutorial/:section/:page/?' do
require_login
@page = params[:page]
not_found unless @page.match?(/\A[1-9]\z|\A10\z/)
not_found unless %w{html}.include?(params[:section])
@section = params[:section]
@title = "#{params[:section].upcase} Tutorial - #{@page}/10"
if @page == '9'
unless csrf_safe?
signout
redirect '/'
end
current_site.tutorial_required = false
current_site.save_changes validate: false
end
erb "tutorial/layout".to_sym
end

View file

@ -1,153 +0,0 @@
post '/webhooks/paypal' do
EmailWorker.perform_async({
from: 'web@neocities.org',
to: 'errors@neocities.org',
subject: "[Neocities Paypal Webhook] Received a Webhook from Paypal",
body: params.inspect,
no_footer: true
})
'ok'
end
def valid_paypal_webhook_source?
# https://www.paypal.com/us/smarthelp/article/what-are-the-ip-addresses-for-live-paypal-servers-ts1056
request_ip = IPAddress::IPv4.new request.ip
['127.0.0.1', '66.211.170.66', '173.0.81.0/24'].each do |ip|
return true if IPAddress::IPv4.new(ip).include? request_ip
end
false
end
post '/webhooks/paypal/tipping_notify' do
return 403 unless valid_paypal_webhook_source?
payload = JSON.parse Base64.strict_decode64(params[:custom]), symbolize_names: true
site = Site[payload[:site_id]]
@tip_hash = {
message: (params[:memo] ? params[:memo] : nil),
amount: params[:mc_gross],
currency: params[:mc_currency],
fee: params[:mc_fee],
actioning_site: (payload[:actioning_site_id] ? Site[payload[:actioning_site_id]] : nil),
paypal_payer_email: params[:payer_email],
paypal_receiver_email: params[:receiver_email],
paypal_txn_id: params[:txn_id],
created_at: DateTime.strptime(params[:payment_date], "%H:%M:%S %b %e, %Y %Z").to_time
}
@tip = site.add_tip @tip_hash
Event.create(
site_id: @tip.site.id,
actioning_site_id: (@tip.actioning_site ? @tip.actioning_site.id : nil),
tip_id: @tip.id
)
if @tip.actioning_site
subject = "You received a #{@tip.amount_string} tip from #{@tip.actioning_site.username}!"
else
subject = "You received a #{@tip.amount_string} tip!"
end
@tip.site.send_email(
subject: subject,
body: Tilt.new('./views/templates/email/tip_received.erb', pretty: true).render(self)
)
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:payer_email],
subject: "You sent a #{@tip.amount_string} tip!",
body: Tilt.new('./views/templates/email/tip_sent.erb', pretty: true).render(self)
})
end
post '/webhooks/stripe' do
event = JSON.parse request.body.read
if event['type'] == 'customer.created'
username = event['data']['object']['description'].split(' - ').first
email = event['data']['object']['email']
EmailWorker.perform_async({
from: 'web@neocities.org',
to: 'contact@neocities.org',
subject: "[Neocities] New customer: #{username}",
body: "#{username}\n#{email}\n#{Site[username: username].uri}",
no_footer: true
})
end
if event['type'] == 'charge.failed'
site = stripe_get_site_from_event event
EmailWorker.perform_async({
from: 'web@neocities.org',
to: site.email,
subject: "[Neocities] There was an issue charging your card",
body: Tilt.new('./views/templates/email/charge_failure.erb', pretty: true).render(self)
})
end
if event['type'] == 'customer.subscription.deleted'
site = stripe_get_site_from_event event
site.stripe_subscription_id = nil
site.plan_type = nil
site.plan_ended = true
site.save_changes validate: false
EmailWorker.perform_async({
from: 'web@neocities.org',
to: site.email,
subject: "[Neocities] Supporter plan has ended",
body: Tilt.new('./views/templates/email/supporter_ended.erb', pretty: true).render(self)
})
end
if event['type'] == 'invoice.payment_succeeded'
site = stripe_get_site_from_event event
if site.email_invoice && site.stripe_paying_supporter?
invoice_obj = event['data']['object']
EmailWorker.perform_async({
from: 'web@neocities.org',
to: site.email,
subject: "[Neocities] Invoice",
body: Tilt.new('./views/templates/email/invoice.erb', pretty: true).render(
self,
site: site,
amount: invoice_obj['amount_due'],
period_start: Time.at(invoice_obj['period_start']),
period_end: Time.at(invoice_obj['period_end']),
date: Time.at(invoice_obj['date'])
)
})
end
end
'ok'
end
def stripe_get_site_from_event(event)
customer_id = event['data']['object']['customer']
halt 'ok' if customer_id.nil? # Likely a fraudulent card report
customer = Stripe::Customer.retrieve customer_id
# Some old accounts only have a username for the desc
desc_split = customer.description.split(' - ')
if desc_split.length == 1
site_where = {username: desc_split.first}
end
if desc_split.last.not_an_integer?
site_where = {username: desc_split.first}
else
site_where = {id: desc_split.last}
end
Site.where(site_where).first
end

View file

@ -1,138 +0,0 @@
def dashboard_if_signed_in
redirect '/dashboard' if signed_in?
end
def csrf_safe?
csrf_token == params[:csrf_token] || csrf_token == request.env['HTTP_X_CSRF_TOKEN']
end
def csrf_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
def is_education?
current_site && current_site.is_education
end
def require_login
redirect '/' unless signed_in? && current_site
end
def signed_in?
return false if current_site.nil?
true
end
def signout
@_site = nil
@_parent_site = nil
session[:id] = nil
session.clear
response.delete_cookie 'neocities', path: '/'
request.env['rack.session.options'][:drop] = true
end
def current_site
return nil if session[:id].nil?
@_site ||= Site[id: session[:id]]
@_parent_site ||= @_site.parent
if @_site.is_banned || @_site.is_deleted || (@_parent_site && (@_parent_site.is_banned || @_parent_site.is_deleted))
signout
end
@_site
end
def parent_site
@_parent_site || current_site
end
def meta_robots(newtag=nil)
if newtag
@_meta_robots = newtag
end
@_meta_robots
end
def title
out = "Neocities"
return out if request.path == '/'
return "#{out} - #{@title}" if @title
"#{out} - #{request.path.gsub('/', '').capitalize}"
end
def encoding_fix(file)
begin
Rack::Utils.escape_html file
rescue ArgumentError => e
if e.message =~ /invalid byte sequence in UTF-8/ ||
e.message =~ /incompatible character encodings/
return Rack::Utils.escape_html(file.force_encoding('BINARY'))
end
fail
end
end
def send_confirmation_email(site=current_site)
if site.email_confirmation_count > Site::MAXIMUM_EMAIL_CONFIRMATIONS
flash[:error] = 'You sent too many email confirmation requests, cannot continue.'
redirect request.referrer
end
DB['UPDATE sites set email_confirmation_count=email_confirmation_count+1 WHERE id=?', site.id].first
EmailWorker.perform_async({
from: 'web@neocities.org',
reply_to: 'contact@neocities.org',
to: site.email,
subject: "[Neocities] Confirm your email address",
body: Tilt.new('./views/templates/email/confirm.erb', pretty: true).render(self, site: site)
})
end
def dont_browser_cache
headers['Cache-Control'] = 'private, no-store, max-age=0, no-cache, must-revalidate, post-check=0, pre-check=0'
headers['Pragma'] = 'no-cache'
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
@dont_browser_cache = true
end
def sanitize_comment(text)
Rinku.auto_link Sanitize.fragment(text), :all, 'target="_blank" rel="nofollow"'
end
def flash_display(opts={})
erb :'_flash', layout: false, locals: {opts: opts}
end
def hcaptcha_valid?
return true if ENV['RACK_ENV'] == 'test' || ENV['CI']
return false unless params[:'h-captcha-response']
resp = HTTP.get('https://hcaptcha.com/siteverify', params: {
secret: $config['hcaptcha_secret_key'],
response: params[:'h-captcha-response']
})
resp = JSON.parse resp
if resp['success'] == true
true
else
false
end
end
JS_ESCAPE_MAP = {"\\" => "\\\\", "</" => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'", "`" => "\\`", "$" => "\\$"}
def escape_javascript(javascript)
javascript = javascript.to_s
if javascript.empty?
result = ""
else
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
end
result
end

View file

@ -1,9 +0,0 @@
These guidelines apply to Neocities contributor communications, including
messages in code repositories, collaboration tools, outreach channels, etc.
* Language and actions must be free of personal attacks, harassment,
discrimination, and NSFW content.
Instances of unacceptable behavior can be reported by contacting the project
team at abuse@neocities.org. All complaints will be investigated and reporter
confidentiality will be maintained.

101
config.ru
View file

@ -1,104 +1,7 @@
require 'rubygems'
require './app.rb'
require 'sidekiq/web'
require 'airbrake/sidekiq'
use Airbrake::Rack::Middleware
map('/') do
run Sinatra::Application
end
map '/webdav' do
use Rack::Auth::Basic do |username, password|
@site = Site.get_site_from_login(username, password)
@site ? true : false
end
run lambda { |env|
request_method = env['REQUEST_METHOD']
path = env['PATH_INFO']
unless @site.owner.supporter?
return [
402,
{
'Content-Type' => 'application/xml',
'X-Upgrade-Required' => 'https://neocities.org/supporter'
},
[
<<~XML
<?xml version="1.0" encoding="utf-8"?>
<error xmlns="DAV:">
<message>WebDAV access requires a supporter account.</message>
</error>
XML
]
]
end
case request_method
when 'OPTIONS'
return [200, {'Allow' => 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE', 'DAV' => '1,2'}, ['']]
when 'PUT'
tmpfile = Tempfile.new('davfile', encoding: 'binary')
tmpfile.write(env['rack.input'].read)
tmpfile.close
return [507, {}, ['']] if @site.file_size_too_large?(tmpfile.size)
if @site.okay_to_upload?(filename: path, tempfile: tmpfile)
@site.store_files([{ filename: path, tempfile: tmpfile }])
return [201, {}, ['']]
else
return [415, {}, ['']]
end
when 'MKCOL'
@site.create_directory(path)
return [201, {}, ['']]
when 'MOVE'
destination = env['HTTP_DESTINATION'][/\/webdav(.+)$/i, 1]
return [400, {}, ['Bad Request']] unless destination
path.sub!(/^\//, '') # Remove leading slash if present
site_file = @site.site_files.find { |s| s.path == path }
return [404, {}, ['']] unless site_file
site_file.rename(destination)
return [201, {}, ['']]
when 'DELETE'
@site.delete_file(path)
return [201, {}, ['']]
else
unless ['PROPFIND', 'GET', 'HEAD'].include? request_method
return [501, {}, ['Not Implemented']]
end
env['PATH_INFO'] = "/#{@site.scrubbed_path(path)}" unless path.empty?
# Terrible hack to fix WebDAV for the VSC plugin
if env['CONTENT_LENGTH'] == "0"
env['rack.input'] = StringIO.new('<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:"><prop>
<getcontentlength xmlns="DAV:"/>
<getlastmodified xmlns="DAV:"/>
<resourcetype xmlns="DAV:"/>
</prop></propfind>')
env['CONTENT_LENGTH'] = env['rack.input'].length.to_s
end
DAV4Rack::Handler.new(
root: @site.files_path,
root_uri_path: '/webdav'
).call(env)
end
}
end
map('/') { run Sinatra::Application }
map '/sidekiq' do
use Rack::Auth::Basic, "Protected Area" do |username, password|
@ -106,7 +9,5 @@ map '/sidekiq' do
username == $config['sidekiq_user'] && password == $config['sidekiq_pass']
end
use Rack::Session::Cookie, key: 'sidekiq.session', secret: Base64.strict_decode64($config['session_secret'])
use Rack::Protection::AuthenticityToken
run Sidekiq::Web
end

View file

@ -1,31 +0,0 @@
database: 'postgres://postgres:citestpassword@localhost/ci_test'
database_pool: 1
session_secret: 'SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk='
email_unsubscribe_token: "somethingrandomderrrrp"
paypal_api_username: derp
paypal_api_password: ing
paypal_api_signature: tonz
logs_path: "/tmp/neocitiestestlogs"
letsencrypt_key: ./tests/files/letsencrypt.key
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
proxy_ips:
- 10.0.0.1
- 10.0.0.2
education_tag_whitelist:
- mrteacher
stop_forum_spam_api_key: testkey
screenshot_urls:
- http://screenshots:derp@screenshotssite.com
cache_control_ips:
- 1.2.3.4
- 4.5.6.7
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
twilio_account_sid: ACEDERPDERP
twilio_auth_token: derpderpderp
twilio_service_sid: VADERPDERPDERP
minfraud_account_id: 696969420
minfraud_license_key: DERPDERPDERP
google_custom_search_key: herpderp
google_custom_search_cx: herpderp
google_custom_search_query_limit: 69

View file

@ -1,70 +1,24 @@
development:
database: 'postgres://localhost/neocities'
database: 'postgres://neocities@127.0.0.1/neocities'
database_pool: 1
redis_url: "redis://localhost"
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
sidekiq_user: "ENTER USER HERE"
sidekiq_pass: "ENTER PASS HERE"
stripe_publishable_key: "ENTER KEY HERE"
stripe_api_key: "ENTER KEY HERE"
ip_hash_salt: "400$8$1$fc21863da5d531c1"
proxy_pass: 'somethinglongandrandom'
email_unsubscribe_token: 'somethingrandom'
logs_path: /path/to/nginx/logs
paypal_api_username: derp
paypal_api_password: ing
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
education_tag_whitelist:
- mrteacher
screenshot_urls:
- http://screenshots:derp@127.0.0.1:12345
stop_forum_spam_api_key: testkey
google_custom_search_key: herpderp
google_custom_search_cx: herpderp
google_custom_search_query_limit: 69
session_secret: SECRET GOES HERE
recaptcha_public_key: ENTER PUBLIC KEY HERE
recaptcha_private_key: ENTER PRIVATE KEY HERE
sidekiq_user: ENTER USER HERE
sidekiq_pass: ENTER PASS HERE
phantomjs_url:
- http://localhost:8910
stripe_publishable_key: fillout
stripe_api_key: fillout
test:
database: 'postgres://localhost/neocities_test'
database: 'postgres://neocities@127.0.0.1/neocities_test'
database_pool: 1
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
sidekiq_user: "ENTER USER HERE"
sidekiq_pass: "ENTER PASS HERE"
stripe_publishable_key: "ENTER KEY HERE"
stripe_api_key: "ENTER KEY HERE"
ip_hash_salt: "400$8$1$fc21863da5d531c1"
proxy_pass: 'somethinglongandrandom'
email_unsubscribe_token: 'somethingrandom'
paypal_api_username: derp
paypal_api_password: ing
paypal_api_signature: tonz
letsencrypt_key: ./tests/files/letsencrypt.key
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
proxy_ips:
- 10.0.0.1
- 10.0.0.2
education_tag_whitelist:
- mrteacher
stop_forum_spam_api_key: testkey
screenshot_urls:
- http://screenshots:derp@screenshotssite.com
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
google_custom_search_key: herpderp
google_custom_search_cx: herpderp
google_custom_search_query_limit: 69
session_secret: SECRET GOES HERE
recaptcha_public_key: ENTER PUBLIC KEY HERE
recaptcha_private_key: ENTER PRIVATE KEY HERE
sidekiq_user: ENTER USER HERE
sidekiq_pass: ENTER PASS HERE
phantomjs_url:
- http://localhost:8910
stripe_publishable_key: fillout
stripe_api_key: fillout

7
config.yml.travis Normal file
View file

@ -0,0 +1,7 @@
database: 'postgres://postgres@localhost/travis_ci_test'
database_pool: 1
session_secret: 's3cr3t'
recaptcha_public_key: '1234'
recaptcha_private_key: '5678'
phantomjs_url:
- http://localhost:8910

View file

@ -1,5 +1,3 @@
# frozen_string_literal: true
RubyVM::YJIT.enable
ENV['RACK_ENV'] ||= 'development'
ENV['TZ'] = 'UTC'
DIR_ROOT = File.expand_path File.dirname(__FILE__)
@ -9,25 +7,13 @@ Encoding.default_external = 'UTF-8'
require 'yaml'
require 'json'
require 'logger'
require 'zip'
Bundler.require
Bundler.require :development if ENV['RACK_ENV'] == 'development'
require 'tilt/erubi'
require 'active_support'
require 'active_support/time'
class File
def self.exists?(val)
self.exist?(val)
end
end
Dir['./ext/**/*.rb'].each {|f| require f}
# :nocov:
if ENV['CI']
$config = YAML.load_file File.join(DIR_ROOT, 'config.yml.ci')
if ENV['TRAVIS']
$config = YAML.load_file File.join(DIR_ROOT, 'config.yml.travis')
else
begin
$config = YAML.load_file(File.join(DIR_ROOT, 'config.yml'))[ENV['RACK_ENV']]
@ -36,163 +22,90 @@ else
exit
end
end
# :nocov:
DB = Sequel.connect $config['database'], sslmode: 'disable', max_connections: $config['database_pool']
DB.extension :pagination
DB.extension :auto_literal_strings
Sequel.split_symbols = true
Sidekiq.strict_args!(false)
require 'will_paginate/sequel'
Dir.glob('workers/*.rb').each {|w| require File.join(DIR_ROOT, "/#{w}") }
# :nocov:
=begin
if defined?(Pry)
Pry.commands.alias_command 'c', 'continue'
Pry.commands.alias_command 's', 'step'
Pry.commands.alias_command 'n', 'next'
Pry.commands.alias_command 'f', 'finish'
end
=end
# :nocov:
unless ENV['RACK_ENV'] == 'production'
Sidekiq.configure_server do |config|
config.logger = nil
end
end
Sidekiq::Logging.logger = nil unless ENV['RACK_ENV'] == 'production'
sidekiq_redis_config = {}
sidekiq_redis_config = {namespace: 'neocitiesworker'}
sidekiq_redis_config[:url] = $config['sidekiq_url'] if $config['sidekiq_url']
# :nocov:
Sidekiq.configure_server do |config|
config.redis = sidekiq_redis_config
end
# :nocov:
Sidekiq.configure_client do |config|
config.logger = nil
config.redis = sidekiq_redis_config
end
if ENV['RACK_ENV'] == 'test'
$redis = MockRedis.new
else
$redis = Redis.new url: $config['redis_url']
end
$redis_cache = Redis::Namespace.new :cache, redis: $redis
if ENV['RACK_ENV'] == 'test'
$redis_proxy = MockRedis.new
else
$redis_proxy = Redis.new url: $config['redis_proxy']
end
# :nocov:
if ENV['RACK_ENV'] == 'development'
# Run async jobs immediately in development.
=begin
module Sidekiq
module Worker
module ClassMethods
def perform_async(*args)
Thread.new {
self.new.perform *args
}
end
end
end
end
=end
end
# :nocov:
require File.join(DIR_ROOT, 'workers', 'thumbnail_worker.rb')
require File.join(DIR_ROOT, 'workers', 'screenshot_worker.rb')
require File.join(DIR_ROOT, 'workers', 'email_worker.rb')
Sequel.datetime_class = Time
Sequel.extension :core_extensions
Sequel.extension :migration
Sequel::Model.plugin :validation_helpers
Sequel::Model.plugin :force_encoding, 'UTF-8'
Sequel::Model.plugin :timestamps, create: :created_at, update: :updated_at
Sequel::Model.plugin :defaults_setter
Sequel::Model.plugin :create_timestamp
Sequel.default_timezone = :utc
Sequel.default_timezone = 'UTC'
Sequel::Migrator.apply DB, './migrations'
Stripe.api_key = $config['stripe_api_key']
Dir.glob('models/*.rb').each {|m| require File.join(DIR_ROOT, "#{m}") }
Dir.glob('workers/*.rb').each {|w| require File.join(DIR_ROOT, "/#{w}") }
DB.loggers << Logger.new(STDOUT) if ENV['RACK_ENV'] == 'development'
if ENV['RACK_ENV'] == 'development'
# If new, throw up a random Server for development.
if Server.count == 0
Server.create ip: '127.0.0.1', slots_available: 999999
end
end
Mail.defaults do
#options = { :address => "smtp.gmail.com",
# :port => 587,
# :domain => 'your.host.name',
# :user_name => '<username>',
# :password => '<password>',
# :authentication => 'plain',
# :enable_starttls_auto => true }
options = {}
delivery_method :sendmail, options
end
Sinatra::Application.set :erb, escape_html: true
require 'sass/plugin/rack'
Sinatra::Application.use Sass::Plugin::Rack
# Session fix for Internet Fucking Explorer https://github.com/rkh/rack-protection/issues/11
Sinatra::Application.set :protection, except: :session_hijacking
Sass::Plugin.options[:template_location] = 'sass'
Sass::Plugin.options[:css_location] = './public/css'
Sass::Plugin.options[:style] = :nested
if ENV['RACK_ENV'] != 'development'
Sass::Plugin.options[:style] = :compressed
# Sass::Plugin.options[:never_update] = true
Sass::Plugin.options[:full_exception] = false
class Sinatra::Base
alias_method :render_original, :render
def render(engine, data, options = {}, locals = {}, &block)
options.merge!(pretty: self.class.development?) if engine == :slim && options[:pretty].nil?
render_original engine, data, options, locals, &block
end
end
PayPal::Recurring.configure do |config|
config.sandbox = false
config.username = $config['paypal_api_username']
config.password = $config['paypal_api_password']
config.signature = $config['paypal_api_signature']
end
require 'csv'
$country_codes = {}
CSV.foreach("./files/country_codes.csv") do |row|
$country_codes[row.last] = row.first
end
gandi_opts = {}
gandi_opts[:env] = :test # unless ENV['RACK_ENV'] == 'production'
$gandi = Gandi::Session.new $config['gandi_api_key'], gandi_opts
$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
Airbrake.configure do |c|
c.project_id = $config['airbrake_project_id']
c.project_key = $config['airbrake_project_key']
end
Airbrake.add_filter do |notice|
if notice[:params][:password]
# Filter out password.
notice[:params][:password] = '[Filtered]'
class Numeric
def roundup(nearest=10)
self % nearest == 0 ? self : self + nearest - (self % nearest)
end
notice.ignore! if notice.stash[:exception].is_a?(Sinatra::NotFound)
def rounddown(nearest=10)
self % nearest == 0 ? self : self - (self % nearest)
end
end
Airbrake.add_filter Airbrake::Sidekiq::RetryableJobsFilter.new

View file

@ -1,13 +0,0 @@
class NilClass
def empty?
true
end
def blank?
true
end
def not_an_integer?
true
end
end

View file

@ -1,169 +0,0 @@
module Ago
module VERSION
MAJOR = 0
MINOR = 1
TINY = 5
class << self
def pretty
"#{MAJOR}.#{MINOR}.#{TINY}"
end
alias_method :print, :pretty
end
end
Ago::Order = [:year, :month, :week, :day, :hour, :minute, :second]
Ago::Units = {
:year => {
:basic => 60 * 60 * 24 * 365,
:gregorian => 86400 * 365.2425,
},
:month => {
:basic => 60 * 60 * 24 * 30,
:gregorian => 86400 * 30.436875,
},
:week => {
:basic => 60 * 60 * 24 * 7,
:gregorian => 86400 * 7.02389423076923,
},
:day => {
:basic => 60 * 60 * 24
},
:hour => {
:basic => 60 * 60
},
:minute => {
:basic => 60
},
:second => {
:basic => 1
}
}
def Ago.calendar_check(calendar)
error = ":calendar => value must be either :basic or :gregorian."
unless calendar == :basic || calendar == :gregorian
raise ArgumentError, error
end
end
module Ago::TimeAgo
# Generate List of valid unit :symbols
valids = ""
Ago::Order.each do |u|
unless u == :second
valids += ":#{u.to_s}, "
else
valids += "and :#{u.to_s}."
end
end
Valids = valids
def ago_in_words(opts={})
# Process options {hash}
focus = opts[:focus] ? opts[:focus] : 0
start_at = opts[:start_at] ? opts[:start_at] : :year
now = opts[:now] ? opts[:now] : Time.now
in_time = opts[:in_time] ? opts[:in_time] : :past
calendar = opts[:calendar] ? opts[:calendar] : :basic
# Filter out invalid arguments for :in_time
in_time_error = ":in_time => value must be either :past or :future, " \
+ "depending on whether the Time object is before or after Time.now."
unless in_time == :past || in_time == :future
raise ArgumentError, in_time_error
end
# Filter out invalid arguments for :calendar
Ago.calendar_check(calendar)
# Filter out invalid arguments for :start_at and :focus
base_error = " => value must either be a number " +
"between 0 and 6 (inclusive),\nor one of the following " +
"symbols: " + Valids
{:focus => focus, :start_at => start_at}.each do |key, opt|
opt_error = ":" + key.to_s + base_error
if opt.class == Integer
raise ArgumentError, opt_error unless opt >= 0 && opt <= 6
elsif opt.class == Symbol
raise ArgumentError, opt_error unless Ago::Units[opt]
else
raise ArgumentError, opt_error
end
end
# Create Variables necessary for processing.
frags = []
output = ""
count = 0
now = calendar == :basic ? now.to_i : now.to_f
my_time = calendar == :basic ? self.to_i : self.to_f
if now > my_time
diff = now - my_time
tail = " ago"
elsif my_time > now
diff = my_time - now
tail = " from now"
else
diff = 0
tail = "just now"
end
# Begin Ago.ago processing
Ago::Order.each do |u|
if calendar == :gregorian && Ago::Units[u][:gregorian]
value = Ago::Units[u][:gregorian]
else
value = Ago::Units[u][:basic]
end
count += 1
# Move further ahead in the Ago::Units array if start_at is farther back than
# the current point in the array.
if start_at.class == Integer
next if count <= start_at
elsif start_at.class == Symbol
next if Ago::Order.index(u) < Ago::Order.index(start_at)
end
n = (diff/value).floor
if n > 0
plural = n > 1 ? "s" : ""
frags << "#{n} #{u.to_s + plural}"
# If the argument passed into ago() is a symbol, focus the ago statement
# down to the level specified in the symbol
if focus.class == Symbol
break if u == focus || u == :second
elsif focus.class == Integer
if focus == 0 || u == :second
break
else
focus -= 1
end
end
diff -= n * value
end
end
# Der Kommissar
frags.size.times do |n|
output += frags[n]
output += ", " unless n == frags.size - 1
end
return output + "#{tail}"
end
def from_now_in_words(opts={})
ago_in_words(opts)
end
end
end
class Time
include Ago::TimeAgo
end

View file

@ -1,13 +0,0 @@
class Float
def round_to(x)
(self * 10**x).round.to_f / 10**x
end
def ceil_to(x)
(self * 10**x).ceil.to_f / 10**x
end
def floor_to(x)
(self * 10**x).floor.to_f / 10**x
end
end

View file

@ -1,5 +0,0 @@
class MockRedis
def publish(channel, message)
# TODO make actually useful
end
end

View file

@ -1,83 +0,0 @@
class Numeric
ONE_MEGABYTE = 1000000
def roundup(nearest=10)
self % nearest == 0 ? self : self + nearest - (self % nearest)
end
def to_mb
self/ONE_MEGABYTE.to_f
end
def to_bytes_pretty
computed = nil
unit = nil
{
'B' => 1000,
'KB' => 1000 * 1000,
'MB' => 1000 * 1000 * 1000,
'GB' => 1000 * 1000 * 1000 * 1000,
'TB' => 1000 * 1000 * 1000 * 1000 * 1000
}.each_pair { |e, s|
if self < s
computed = (self.to_f / (s / 1000)).round(2)
unit = e
break
end
}
computed = computed.to_i if computed.modulo(1) == 0.0
"#{computed} #{unit}"
end
def to_gigabytes_pretty
self.to_gigabytes.to_s + ' GB'
end
def to_gigabytes
self / (1000**3)
end
def to_comma_separated
self.to_i.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse
end
def format_large_number
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
if self > 9999
if self > 999999999
unit_char = 'B' #billion
unit_amount = 1000000000.0
elsif self > 999999
unit_char = 'M' #million
unit_amount = 1000000.0
elsif self > 9999
unit_char = 'K' #thousand
unit_amount = 1000.0
end
self_divided = self.to_f / unit_amount
self_rounded = self_divided.round(1)
if self_rounded.denominator == 1
return sprintf ("%.0f" + unit_char), self_divided
else
return sprintf ("%.1f" + unit_char), self_divided
end
else
if self > 999
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
else
return self
end
end
end
def to_space_pretty
to_bytes_pretty
end
def not_an_integer?
!self.integer?
end
end

View file

@ -1,8 +0,0 @@
module Sequel::Plugins::CreateTimestamp
module InstanceMethods
def before_create
self.created_at = Time.now if respond_to?(:created_at) && !self.created_at
super
end
end
end

View file

@ -1,33 +0,0 @@
module Sequel
module ParanoidDelete
def self.included(base)
base.extend(ClassMethods)
end
# Instead of actually deleting, we just set is_deleted to true,
# and look for it with our default dataset filter.
def delete
self.is_deleted = true
save :validate => false
true
end
module ClassMethods
# There's no hook for setting default filters after inheritance (that I'm aware of),
# so this adds the filter for the first time the class' dataset is accessed for the new model.
def dataset
if @_is_deleted_filter_set.nil?
#KD: I turned this off because I think it's easier to do this manually.
#@dataset.filter! is_deleted: false
@_is_deleted_filter_set = true
end
super
end
end
end
# Model.include ParanoidDelete
end

View file

@ -1,7 +0,0 @@
class Sinatra::Base
alias_method :render_original, :render
def render(engine, data, options = {}, locals = {}, &block)
options.merge!(pretty: self.class.development?) if engine == :slim && options[:pretty].nil?
render_original engine, data, options, locals, &block
end
end

View file

@ -1,5 +0,0 @@
class Sinatra::IndifferentHash
def not_an_integer?
true
end
end

View file

@ -1,26 +0,0 @@
class String
def shorten(length, usedots=true)
if usedots
return self if self.length < length
"#{self[0..length-3]}..."
else
self[0..length]
end
end
def unindent
gsub /^#{scan(/^\s*/).min_by{|l|l.length}}/, ""
end
def blank?
return true if self == ''
false
end
def not_an_integer?
Integer(self)
false
rescue ArgumentError
true
end
end

View file

@ -1,7 +0,0 @@
class Tempfile
alias_method :size_original, :size
def size
s = size_original
s.nil? ? 0 : s
end
end

View file

@ -1,9 +0,0 @@
class Time
def ago
self.ago_in_words
end
def from_now
self.from_now_in_words
end
end

Binary file not shown.

View file

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

View file

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

73
files/neocities Normal file
View file

@ -0,0 +1,73 @@
server {
listen 80;
listen 443 ssl;
ssl_certificate /etc/nginx/certs/neocities.org.crt;
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
server_name www.neocities.org;
rewrite ^(.*)$ $scheme://neocities.org$1 permanent;
}
server {
listen 80;
listen 443 ssl;
ssl_certificate /etc/nginx/certs/neocities.org.crt;
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
set $ssl off;
if ($scheme = https) {
set $ssl on;
}
root /home/web/neocities-web/public;
server_name neocities.org;
access_log /var/log/nginx/neocities-web.log;
error_page 500 = /gateway_error.html;
# location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
# expires 60s;
# log_not_found off;
# }
try_files $uri @neocities;
location @neocities {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Ssl $ssl;
proxy_max_temp_file_size 0;
# proxy_pass http://127.0.0.1:20000;
proxy_pass http://unix:/var/run/neocities/neocities.sock;
break;
}
}
server {
listen 80;
listen 443 ssl;
ssl_certificate /etc/nginx/certs/neocities.org.crt;
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
server_name ~^(?<subdomain>.+)\.neocities.org$;
access_log /var/log/nginx/neocities-sites.log neocities;
root /home/web/neocities-web/public/sites/$subdomain;
index /index.html;
error_page 404 = @notfound;
location @notfound {
try_files /not_found.html @notfound_root;
}
location @notfound_root {
root /home/web/neocities-web/public;
try_files /web_site_not_found.html =404;
}
location ~* \.(html|jpg|jpeg|png|gif|ico|css|js)$ {
# expires 20s;
log_not_found off;
}
}

View file

@ -0,0 +1,7 @@
/var/log/nginx/neocities-sites.log {
rotate 14
create 0640 www-data adm
postrotate
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
endscript
}

111
files/nginx.conf Normal file
View file

@ -0,0 +1,111 @@
user www-data www-data;
worker_processes 8;
pid /var/run/nginx.pid;
events {
worker_connections 4000;
multi_accept on;
use epoll;
}
http {
root /home/web/neocities-web/public;
error_page 404 = /not_found.html;
log_format neocities '$time_iso8601 $subdomain $bytes_sent $request_uri ';
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH;
ssl_prefer_server_ciphers on;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
reset_timedout_connection on;
charset utf-8;
client_max_body_size 20m;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
gzip_min_length 1024;
# gzip_vary on;
# gzip_proxied any;
gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
##
# nginx-naxsi config
##
# Uncomment it if you installed nginx-naxsi
##
#include /etc/nginx/naxsi_core.rules;
##
# nginx-passenger config
##
# Uncomment it if you installed nginx-passenger
##
#passenger_root /usr;
#passenger_ruby /usr/bin/ruby;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View file

@ -1,56 +0,0 @@
var page = require('webpage').create()
var system = require('system')
var maxTimeout = 15000
if (system.args.length === 1) {
console.log('required args: <siteURL> <outputFilePath>');
phantom.exit(1)
}
var address = system.args[1]
var outputPath = system.args[2]
page.viewportSize = { width: 1280, height: 960 }
page.clipRect = { top: 0, left: 0, width: 1280, height: 960}
/*
In development, not working yet.
page.settings.scriptTimeout = 1000
page.onLongRunningScript = function() {
page.stopJavaScript()
phantom.exit(4)
}
*/
var t = Date.now()
console.log('Loading ' + address)
setTimeout(function() {
console.log('timeout')
phantom.exit(62)
}, maxTimeout)
page.settings.resourceTimeout = maxTimeout
page.onResourceTimeout = function(e) {
console.log(e.errorCode)
console.log(e.errorString)
console.log(e.url)
phantom.exit(3)
}
page.open(address, function(status) {
if(status !== 'success') {
console.log('failed')
phantom.exit(2)
}
page.render(outputPath)
console.log('Loading time ' + (Date.now() - t) + ' msec');
phantom.exit()
})

View file

@ -1,24 +0,0 @@
1,weekly,https://neocities.org
0.9,daily,https://neocities.org/browse
0.9,monthly,https://neocities.org/signin
0.9,hourly,https://neocities.org/activity
0.9,monthly,https://neocities.org/tutorials
0.9,monthly,https://neocities.org/supporter
0.9,monthly,https://neocities.org/cli
0.9,monthly,https://neocities.org/about
0.9,monthly,https://neocities.org/donate
0.9,monthly,https://neocities.org/api
0.9,monthly,https://neocities.org/press
0.8,monthly,https://neocities.org/terms
0.8,weekly,https://blog.neocities.org
0.7,monthly,https://neocities.org/contact
0.6,daily,https://neocities.org/browse?sort_by=special_sauce&tag=
0.6,hourly,https://neocities.org/browse?sort_by=last_updated&tag=
0.6,daily,https://neocities.org/browse?sort_by=supporters&tag=
0.6,weekly,https://neocities.org/browse?sort_by=featured&tag=
0.6,daily,https://neocities.org/browse?sort_by=tipping_enabled&tag=
0.6,weekly,https://neocities.org/browse?sort_by=views&tag=
0.6,weekly,https://neocities.org/browse?sort_by=hits&tag=
0.6,hourly,https://neocities.org/browse?sort_by=newest&tag=
0.6,monthly,https://neocities.org/browse?sort_by=oldest&tag=
0.6,always,https://neocities.org/browse?sort_by=random&tag=

70
files/sysctl.conf Normal file
View file

@ -0,0 +1,70 @@
#
# /etc/sysctl.conf - Configuration file for setting system variables
# See /etc/sysctl.d/ for additional system variables
# See sysctl.conf (5) for information.
#
#kernel.domainname = example.com
# Uncomment the following to stop low-level messages on console
#kernel.printk = 3 4 1 3
##############################################################3
# Functions previously found in netbase
#
# Uncomment the next two lines to enable Spoof protection (reverse-path filter)
# Turn on Source Address Verification in all interfaces to
# prevent some spoofing attacks
#net.ipv4.conf.default.rp_filter=1
#net.ipv4.conf.all.rp_filter=1
# Uncomment the next line to enable TCP/IP SYN cookies
# See http://lwn.net/Articles/277146/
# Note: This may impact IPv6 TCP sessions too
#net.ipv4.tcp_syncookies=1
# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1
# Uncomment the next line to enable packet forwarding for IPv6
# Enabling this option disables Stateless Address Autoconfiguration
# based on Router Advertisements for this host
#net.ipv6.conf.all.forwarding=1
###################################################################
# Additional settings - these settings can improve the network
# security of the host and prevent against some network attacks
# including spoofing attacks and man in the middle attacks through
# redirection. Some network environments, however, require that these
# settings are disabled so review and enable them as needed.
#
# Do not accept ICMP redirects (prevent MITM attacks)
#net.ipv4.conf.all.accept_redirects = 0
#net.ipv6.conf.all.accept_redirects = 0
# _or_
# Accept ICMP redirects only for gateways listed in our default
# gateway list (enabled by default)
# net.ipv4.conf.all.secure_redirects = 1
#
# Do not send ICMP redirects (we are not a router)
#net.ipv4.conf.all.send_redirects = 0
#
# Do not accept IP source route packets (we are not a router)
#net.ipv4.conf.all.accept_source_route = 0
#net.ipv6.conf.all.accept_source_route = 0
#
# Log Martian Packets
#net.ipv4.conf.all.log_martians = 1
#
net.ipv4.tcp_max_syn_backlog = 3240000
net.core.somaxconn = 3240000
net.ipv4.tcp_max_tw_buckets = 1440000
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_congestion_control = cubic

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 KiB

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.rename_table :changes, :site_changes
}
down {
DB.rename_table :site_changes, :changes
}
end

View file

@ -1,19 +0,0 @@
Sequel.migration do
up {
DB.rename_column :events, :comment_id, :profile_comment_id
DB.create_table! :profile_comments do
primary_key :id
Integer :site_id
Integer :actioning_site_id
Text :message
DateTime :created_at
DateTime :updated_at
end
}
down {
DB.rename_column :events, :profile_comment_id, :comment_id
DB.drop_table :profile_comments
}
end

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
Sequel.migration do
up {
DB.create_table! :comment_likes do
primary_key :id
Integer :comment_id
Integer :site_id
Integer :actioning_site_id
DateTime :created_at
end
}
down {
DB.drop_table :comment_likes
}
end

View file

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

View file

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

View file

@ -1,11 +0,0 @@
Sequel.migration do
up {
DB.drop_column :events, :created_at
DB.add_column :events, :created_at, :timestamp, index: true
}
down {
DB.drop_column :events, :created_at
DB.add_column :events, :created_at, :integer, index: true
}
end

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.rename_column :events, :site_update_id, :site_change_id
}
down {
DB.rename_column :events, :site_change_id, :site_update_id
}
end

View file

@ -1,14 +0,0 @@
Sequel.migration do
up {
DB.create_table! :site_change_files do
primary_key :id
Integer :site_change_id
String :filename
DateTime :created_at
end
}
down {
DB.drop_table :site_change_files
}
end

View file

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

View file

@ -1,18 +0,0 @@
Sequel.migration do
up {
DB.create_table! :reports do
primary_key :id
Integer :site_id
Integer :reporting_site_id
String :type
Text :comments
Text :action_taken
String :ip
DateTime :created_at
end
}
down {
DB.drop_table :reports
}
end

View file

@ -1,10 +0,0 @@
Sequel.migration do
change do
#alter_table(:events) { add_index :created_at }
alter_table(:sites) { add_index :updated_at }
alter_table(:comment_likes) { add_index :comment_id }
alter_table(:comment_likes) { add_index :actioning_site_id }
alter_table(:sites_tags) { add_index :tag_id }
alter_table(:tags) { add_index :name }
end
end

View file

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

View file

@ -1,11 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :email_confirmation_token, :text
DB.add_column :sites, :email_confirmed, :boolean, default: false
}
down {
DB.drop_column :sites, :email_confirmation_token
DB.drop_column :sites, :email_confirmed
}
end

View file

@ -1,12 +0,0 @@
Sequel.migration do
up {
DB.create_table! :blocked_ips do
String :ip, primary_key: true
DateTime :created_at
end
}
down {
DB.drop_table :blocked_ips
}
end

View file

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

View file

@ -1,12 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :ssl_key, :text
DB.add_column :sites, :ssl_cert, :text
}
down {
DB.drop_column :sites, :ssl_key
DB.drop_column :sites, :ssl_cert
}
end

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
Sequel.migration do
up {
DB.drop_table :servers
}
down {
DB.create_table! :servers do
primary_key :id
String :ip
Integer :slots_available
DateTime :created_at
DateTime :updated_at
end
}
end

View file

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

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.add_column :tags, :is_nsfw, :boolean, default: false, index: true
}
down {
DB.drop_column :tags, :is_nsfw
}
end

View file

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

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :space_used, :bigint, default: 0, index: true
}
down {
DB.drop_column :sites, :space_used
}
end

View file

@ -1,17 +0,0 @@
Sequel.migration do
up {
DB.create_table! :site_files do
Integer :site_id, index: true
String :path
Bigint :size
String :sha1_hash
Boolean :is_directory, default: false
DateTime :created_at
DateTime :updated_at
end
}
down {
DB.drop_table :site_files
}
end

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :site_updated_at, DateTime, index: true
}
down {
DB.drop_column :sites, :site_updated_at
}
end

View file

@ -1,30 +0,0 @@
Sequel.migration do
up {
DB.drop_table :site_files
DB.create_table! :site_files do
Integer :site_id
String :path
Bigint :size
String :sha1_hash
Boolean :is_directory, default: false
DateTime :created_at
DateTime :updated_at
primary_key [:site_id, :path], :name => :site_files_pk
end
}
down {
DB.drop_table :site_files
DB.create_table! :site_files do
Integer :site_id, index: true
String :path
Bigint :size
String :sha1_hash
Boolean :is_directory, default: false
DateTime :created_at
DateTime :updated_at
end
}
end

View file

@ -1,13 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :send_emails, :boolean, default: true
DB.add_column :sites, :send_comment_emails, :boolean, default: true
DB.add_column :sites, :send_follow_emails, :boolean, default: true
}
down {
DB.drop_column :sites, :send_emails
DB.drop_column :sites, :send_comment_emails
DB.drop_column :sites, :send_follow_emails
}
end

View file

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

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :featured_at, DateTime, index: true
}
down {
DB.drop_column :sites, :featured_at
}
end

View file

@ -1,9 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :api_calls, Integer, default: 0, index: true
}
down {
DB.drop_column :sites, :api_calls
}
end

View file

@ -1,11 +0,0 @@
Sequel.migration do
up {
DB.add_column :sites, :paypal_profile_id, String
DB.add_column :sites, :paypal_token, String
}
down {
DB.drop_column :sites, :paypal_profile_id
DB.drop_column :sites, :paypal_token
}
end

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