finish merge on cleaned up news feed

This commit is contained in:
Kyle Drake 2016-07-29 14:10:29 -07:00
commit 4ca2e3f443
205 changed files with 6131 additions and 2295 deletions

25
.gitignore vendored
View file

@ -1,24 +1,9 @@
*.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/site_thumbnails
public/sites
public/site_screenshots
@ -26,12 +11,16 @@ 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
./black_box.rb
.vagrant
public/banned_sites
public/deleted_sites
tests/stat_logs/*
files/disposable_email_blacklist.conf
files/letsencrypt.key
files/tor.txt
.bundle

View file

@ -1,7 +1,9 @@
language: ruby
rvm:
- "2.1.1"
- "2.3.0"
addons:
postgresql: "9.3"
before_script:
- psql -c 'create database travis_ci_test;' -U postgres
sudo: false
bundler_args: --jobs=1

38
Gemfile
View file

@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'sinatra'
gem 'redis'
gem 'redis-namespace'
gem 'sequel', '4.8.0'
gem 'bcrypt'
gem 'sinatra-flash', require: 'sinatra/flash'
@ -9,14 +10,13 @@ gem 'sinatra-xsendfile', require: 'sinatra/xsendfile'
gem 'puma', require: nil
gem 'rack-recaptcha', require: 'rack/recaptcha'
gem 'rmagick', require: nil
gem 'sidekiq'
gem 'sidekiq', '~> 4.1.2'
gem 'ago'
gem 'mail'
gem 'google-api-client', require: 'google/api_client'
gem 'tilt'
gem 'erubis'
gem 'stripe' #, source: 'https://code.stripe.com/'
gem 'screencap'
gem 'stripe', '1.15.0' #, source: 'https://code.stripe.com/'
#gem 'screencap', '~> 0.1.4'
gem 'cocaine'
gem 'zipruby'
gem 'sass', require: nil
@ -25,18 +25,37 @@ gem 'filesize'
gem 'thread'
gem 'scrypt'
gem 'rack-cache'
gem 'rest-client'
gem 'rest-client', require: 'rest_client'
gem 'addressable', require: 'addressable/uri'
gem 'paypal-recurring', require: 'paypal/recurring'
gem 'geoip'
gem 'io-extra', require: 'io/extra'
gem 'rye'
gem 'dnsruby'
gem 'base32'
gem 'coveralls', require: false
gem 'sanitize'
gem 'will_paginate'
gem 'simpleidn'
gem 'gandi'
gem 'hoe', '3.14.2', require: nil
gem 'msgpack'
gem 'json-jwt', {
git: 'https://github.com/neocities/json-jwt.git',
branch: 'drop_activesupport'
}
gem 'acme-client', {
git: 'https://github.com/jhass/acme-client.git',
branch: 'no_activesupport'
}
gem 'http'
gem 'htmlentities'
platform :mri, :rbx 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
gem 'posix-spawn'
group :development, :test do
gem 'pry'
@ -61,6 +80,7 @@ end
group :development do
gem 'shotgun', require: nil
gem 'certified'
end
group :test do
@ -73,10 +93,12 @@ group :test do
gem 'rake', require: nil
gem 'poltergeist'
gem 'capybara_minitest_spec'
gem 'capybara', '2.6.2', require: nil
gem 'rack_session_access', require: nil
gem 'webmock', require: nil
gem 'stripe-ruby-mock', '~> 2.0.1', require: 'stripe_mock'
gem 'stripe-ruby-mock', '2.0.1', require: 'stripe_mock'
gem 'timecop'
gem 'mock_redis'
platform :mri, :rbx do
gem 'simplecov', require: nil

View file

@ -1,28 +1,46 @@
GIT
remote: https://github.com/jhass/acme-client.git
revision: d0ced992bfe42908bb1fc25ac549ae3318386c97
branch: no_activesupport
specs:
acme-client (0.2.2)
faraday (~> 0.9, >= 0.9.1)
json-jwt (~> 1.2, >= 1.2.3)
GIT
remote: https://github.com/neocities/json-jwt.git
revision: 00d9c3d34e6bfbab866a4a0405897182a4ed3833
branch: drop_activesupport
specs:
json-jwt (1.5.2)
bindata
hashery (~> 2.0)
multi_json (>= 1.3)
securecompare
url_safe_base64
GEM
remote: https://rubygems.org/
specs:
activesupport (4.1.4)
i18n (~> 0.6, >= 0.6.9)
activesupport (4.2.6)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.3.7)
addressable (2.4.0)
ago (0.1.5)
annoy (0.5.6)
highline (>= 1.5.0)
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)
ansi (1.5.0)
base32 (0.3.2)
bcrypt (3.1.11)
bindata (2.3.1)
blankslate (3.1.3)
builder (3.2.2)
byebug (2.7.0)
columnize (~> 0.3)
debugger-linecache (~> 1.2)
capybara (2.4.4)
byebug (8.2.4)
capybara (2.6.2)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
@ -31,123 +49,123 @@ GEM
capybara_minitest_spec (1.0.5)
capybara (>= 2)
minitest (>= 4)
celluloid (0.15.2)
timers (~> 1.1.0)
certified (1.0.0)
climate_control (0.0.3)
activesupport (>= 3.0)
cliver (0.3.2)
cocaine (0.5.4)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.0)
columnize (0.8.9)
connection_pool (2.0.0)
crack (0.4.2)
coderay (1.1.1)
concurrent-ruby (1.0.2)
connection_pool (2.2.0)
coveralls (0.8.13)
json (~> 1.8)
simplecov (~> 0.11.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
dante (0.2.0)
dav4rack (0.3.0)
nokogiri (>= 1.4.2)
rack (>= 1.1.0)
uuidtools (~> 2.1.1)
debugger-linecache (1.2.0)
dnsruby (1.58.0)
docile (1.1.3)
domain_name (0.5.23)
docile (1.1.5)
domain_name (0.5.20160310)
unf (>= 0.0.5, < 1.0.0)
drydock (0.6.9)
erubis (2.7.0)
extlib (0.9.16)
fabrication (2.11.0)
faker (1.3.0)
fabrication (2.15.0)
faker (1.6.3)
i18n (~> 0.5)
faraday (0.9.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
ffi (1.9.6)
ffi (1.9.10)
ffi-compiler (0.1.3)
ffi (>= 1.0.0)
rake
filesize (0.0.3)
geoip (1.5.0)
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)
highline (1.7.2)
hiredis (0.5.0)
filesize (0.1.1)
gandi (2.1.3)
hashie
geoip (1.6.1)
hashdiff (0.3.0)
hashery (2.1.2)
hashie (3.4.3)
highline (1.7.8)
hiredis (0.6.1)
hoe (3.14.2)
rake (>= 0.8, < 11.0)
htmlentities (4.3.4)
http (2.0.1)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 1.0.1)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.2)
domain_name (~> 0.5)
i18n (0.6.9)
http-form_data (1.0.1)
http_parser.rb (0.6.0)
i18n (0.7.0)
io-extra (1.2.8)
jimson-temp (0.9.5)
blankslate (>= 3.1.2)
multi_json (~> 1.0)
rack (~> 1.4)
rest-client (~> 1.0)
json (1.8.1)
jwt (0.1.11)
multi_json (>= 1.5)
kgio (2.9.2)
launchy (2.4.2)
addressable (~> 2.3)
m (1.3.4)
json (1.8.3)
m (1.4.2)
method_source (>= 0.6.7)
rake (>= 0.9.2.2)
magic (0.2.6)
magic (0.2.9)
ffi (>= 0.6.3)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mail (2.6.4)
mime-types (>= 1.16, < 4)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.2)
minitest (5.6.1)
minitest-reporters (1.0.2)
mime-types (2.99.1)
mini_portile2 (2.0.0)
minitest (5.8.4)
minitest-reporters (1.1.8)
ansi
builder
minitest (>= 5.0)
powerbar
mocha (1.0.0)
ruby-progressbar
mocha (1.1.0)
metaclass (~> 0.0.1)
multi_json (1.11.0)
mock_redis (0.16.1)
msgpack (0.7.5)
multi_json (1.11.2)
multipart-post (2.0.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (2.9.2)
netrc (0.10.3)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
pg (0.17.1)
phantomjs (1.9.7.1)
poltergeist (1.6.0)
net-ssh (3.1.1)
netrc (0.11.0)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
nokogumbo (1.4.7)
nokogiri
paypal-recurring (1.1.0)
pg (0.18.4)
poltergeist (1.9.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)
posix-spawn (0.3.11)
pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (1.3.2)
byebug (~> 2.7)
pry (~> 0.9.12)
puma (2.8.1)
rack (>= 1.1, < 2.0)
rack (1.6.0)
rack-cache (1.2)
pry-byebug (3.3.0)
byebug (~> 8.0)
pry (~> 0.10)
puma (3.4.0)
rack (1.6.4)
rack-cache (1.6.1)
rack (>= 0.4)
rack-protection (1.5.2)
rack-protection (1.5.3)
rack
rack-recaptcha (0.6.6)
json
@ -156,21 +174,16 @@ GEM
rack_session_access (0.1.1)
builder (>= 2.0.0)
rack (>= 1.0.0)
rainbows (4.6.1)
kgio (~> 2.5)
rack (~> 1.1)
unicorn (~> 4.8)
raindrops (0.13.0)
rake (10.3.2)
redis (3.0.7)
redis-namespace (1.4.1)
redis (~> 3.0.4)
rake (10.5.0)
redis (3.2.2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
retriable (1.4.1)
rmagick (2.15.0)
rmagick (2.15.4)
ruby-progressbar (1.7.5)
rye (0.9.13)
annoy
docile (>= 1.0.1)
@ -179,38 +192,35 @@ GEM
net-ssh (>= 2.0.13)
sysinfo (>= 0.8.1)
safe_yaml (1.0.4)
sass (3.3.8)
screencap (0.1.1)
phantomjs
scrypt (2.0.0)
sanitize (4.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.22)
scrypt (2.1.1)
ffi-compiler (>= 0.0.2)
rake
securecompare (1.0.0)
sequel (4.8.0)
sequel_pg (1.6.9)
sequel_pg (1.6.16)
pg (>= 0.8.0)
sequel (>= 3.39.0)
shotgun (0.9)
sequel (>= 4.0.0)
shotgun (0.9.1)
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)
sidekiq (4.1.2)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
redis (~> 3.2, >= 3.2.1)
simplecov (0.11.2)
docile (~> 1.1.0)
multi_json
simplecov-html (~> 0.8.0)
simplecov-html (0.8.0)
sinatra (1.4.4)
rack (~> 1.4)
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
simpleidn (0.0.6)
sinatra (1.4.7)
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
tilt (>= 1.3, < 3)
sinatra-flash (0.3.0)
sinatra (>= 1.0.0)
sinatra-xsendfile (0.4.2)
@ -228,30 +238,29 @@ GEM
sysinfo (0.8.1)
drydock
storable
thread (0.1.4)
thread_safe (0.3.4)
tilt (1.4.1)
timecop (0.7.4)
timers (1.1.0)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
term-ansicolor (1.3.2)
tins (~> 1.0)
thor (0.19.1)
thread (0.2.2)
thread_safe (0.3.5)
tilt (2.0.2)
timecop (0.8.1)
tins (1.6.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.6)
unicorn (4.8.2)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
uuidtools (2.1.4)
webmock (1.17.4)
addressable (>= 2.2.7)
unf_ext (0.0.7.2)
url_safe_base64 (0.2.2)
uuidtools (2.1.5)
webmock (1.24.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
websocket-driver (0.5.4)
hashdiff
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
will_paginate (3.1.0)
xpath (2.0.0)
nokogiri (~> 1.3)
zipruby (0.3.6)
@ -260,31 +269,44 @@ PLATFORMS
ruby
DEPENDENCIES
acme-client!
addressable
ago
base32
bcrypt
capybara (= 2.6.2)
capybara_minitest_spec
certified
cocaine
coveralls
dav4rack
dnsruby
erubis
fabrication
faker
filesize
gandi
geoip
google-api-client
hiredis
hoe (= 3.14.2)
htmlentities
http
io-extra
jdbc-postgres
jruby-openssl
json
json-jwt!
m
magic
mail
minitest
minitest-reporters
mocha
mock_redis
msgpack
paypal-recurring
pg
poltergeist
posix-spawn
pry
pry-byebug
puma
@ -292,31 +314,33 @@ DEPENDENCIES
rack-recaptcha
rack-test
rack_session_access
rainbows
rake
redis
redis-namespace
rest-client
rmagick
ruby-debug
rye
sanitize
sass
screencap
scrypt
sequel (= 4.8.0)
sequel_pg
shotgun
sidekiq
sidekiq (~> 4.1.2)
simplecov
simpleidn
sinatra
sinatra-flash
sinatra-xsendfile
stripe
stripe-ruby-mock (~> 2.0.1)
stripe (= 1.15.0)
stripe-ruby-mock (= 2.0.1)
thread
tilt
timecop
webmock
will_paginate
zipruby
BUNDLED WITH
1.10.2
1.12.1

View file

@ -1,6 +1,7 @@
# Neocities.org
[![Build Status](https://travis-ci.org/neocities/neocities.png?branch=master)](https://travis-ci.org/neocities/neocities)
[![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!

191
Rakefile
View file

@ -38,10 +38,17 @@ task :parse_logs => [:environment] do
Stat.parse_logfiles $config['logs_path']
end
desc 'Update disposable email blacklist'
task :update_disposable_email_blacklist => [:environment] do
uri = URI.parse('https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blacklist.conf')
File.write(Site::DISPOSABLE_EMAIL_BLACKLIST_PATH, Net::HTTP.get(uri))
end
desc 'Update banned IPs list'
task :update_blocked_ips => [:environment] do
uri = URI.parse('http://www.stopforumspam.com/downloads/listed_ip_90.zip')
blocked_ips_zip = Tempfile.new('blockedipszip', Dir.tmpdir, 'wb')
blocked_ips_zip = Tempfile.new('blockedipszip', Dir.tmpdir)
blocked_ips_zip.binmode
Net::HTTP.start(uri.host, uri.port) do |http|
@ -65,13 +72,65 @@ task :update_blocked_ips => [:environment] do
end
end
desc 'Compile domain map for nginx'
task :compile_domain_map => [:environment] do
File.open('./files/map.txt', 'w') do |file|
Site.exclude(domain: nil).exclude(domain: '').select(:username,:domain).all.collect do |site|
file.write ".#{site.domain} #{site.username};\n"
desc 'parse tor exits'
task :parse_tor_exits => [:environment] do
exit_ips = Net::HTTP.get(URI.parse('https://check.torproject.org/exit-addresses'))
exit_ips.split("\n").collect {|line|
line.match(/ExitAddress (\d+\.\d+\.\d+\.\d+)/)&.captures&.first
}.compact
# ^^ Array of ip addresses of known exit nodes
end
desc 'Compile nginx mapfiles'
task :compile_nginx_mapfiles => [:environment] do
FileUtils.mkdir_p './files/maps'
File.open('./files/maps/domains.txt', 'w') do |file|
Site.exclude(domain: nil).exclude(domain: '').select(:username,:domain).all.each do |site|
file.write ".#{site.values[:domain]} #{site.username};\n"
end
end
File.open('./files/maps/supporters.txt', 'w') do |file|
Site.select(:username, :domain).exclude(plan_type: 'free').exclude(plan_type: nil).all.each do |parent_site|
sites = [parent_site] + parent_site.children
sites.each do |site|
file.write "#{site.username}.neocities.org 1;\n"
unless site.host.match(/\.neocities\.org$/)
file.write ".#{site.values[:domain]} 1;\n"
end
end
end
end
File.open('./files/maps/sandboxed.txt', 'w') do |file|
usernames = DB["select username from sites where created_at > ? and (plan_type is null or plan_type='free')", 1.week.ago].all.collect {|s| s[:username]}.each {|username| file.write "#{username} 1;\n"}
end
# Compile letsencrypt ssl keys
sites = DB[%{select username,ssl_key,ssl_cert,domain from sites where ssl_cert is not null and ssl_key is not null and (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all
ssl_path = './files/maps/ssl'
FileUtils.mkdir_p ssl_path
sites.each do |site|
[site[:domain], "www.#{site[:domain]}"].each do |domain|
begin
key = OpenSSL::PKey::RSA.new site[:ssl_key]
crt = OpenSSL::X509::Certificate.new site[:ssl_cert]
rescue => e
puts "SSL ERROR: #{e.class} #{e.inspect}"
next
end
File.open(File.join(ssl_path, "#{domain}.key"), 'wb') {|f| f.write key.to_der}
File.open(File.join(ssl_path, "#{domain}.crt"), 'wb') {|f| f.write site[:ssl_cert]}
end
end
end
desc 'Produce SSL config package for proxy'
@ -216,6 +275,73 @@ task :hash_ips => [:environment] do
end
end
desc 'prime_site_files'
task :prime_site_files => [:environment] do
Site.where(is_banned: false).where(is_deleted: false).select(:id, :username).all.each do |site|
Dir.glob(File.join(site.files_path, '**/*')).each do |file|
path = file.gsub(site.base_files_path, '').sub(/^\//, '')
site_file = site.site_files_dataset[path: path]
if site_file.nil?
mtime = File.mtime file
site_file_opts = {
path: path,
updated_at: mtime,
created_at: mtime
}
if File.directory? file
site_file_opts.merge! is_directory: true
else
site_file_opts.merge!(
size: File.size(file),
sha1_hash: Digest::SHA1.file(file).hexdigest
)
end
site.add_site_file site_file_opts
end
end
end
end
desc 'dedupe_follows'
task :dedupe_follows => [:environment] do
follows = Follow.all
deduped_follows = Follow.all.uniq {|f| "#{f.site_id}_#{f.actioning_site_id}"}
follows.each do |follow|
unless deduped_follows.include?(follow)
puts "deleting dedupe: #{follow.inspect}"
follow.delete
end
end
end
desc 'flush_empty_index_sites'
task :flush_empty_index_sites => [:environment] do
sites = Site.select(:id).all
counter = 0
sites.each do |site|
if site.empty_index?
counter += 1
site.site_changed = false
site.save_changes validate: false
end
end
puts "#{counter} sites set to not changed."
end
desc 'compute_scores'
task :compute_scores => [:environment] do
Site.compute_scores
end
=begin
desc 'Update screenshots'
task :update_screenshots => [:environment] do
@ -224,3 +350,56 @@ task :update_screenshots => [:environment] do
}
end
=end
desc 'prime_classifier'
task :prime_classifier => [:environment] do
Site.select(:id, :username).where(is_banned: false, is_deleted: false).all.each do |site|
next if site.site_files_dataset.where(classifier: 'spam').count > 0
html_files = site.site_files_dataset.where(path: /\.html$/).all
html_files.each do |html_file|
print "training #{site.username}/#{html_file.path}..."
site.train html_file.path
print "done.\n"
end
end
end
desc 'train_spam'
task :train_spam => [:environment] do
paths = File.read('./spam.txt')
paths.split("\n").each do |path|
username, site_file_path = path.match(/^([a-zA-Z0-9_\-]+)\/(.+)$/i).captures
site = Site[username: username]
next if site.nil?
site_file = site.site_files_dataset.where(path: site_file_path).first
next if site_file.nil?
site.train site_file_path, :spam
site.ban!
puts "Deleted #{site_file_path}, banned #{site.username}"
end
end
desc 'regenerate_ssl_certs'
task :regenerate_ssl_certs => [:environment] do
sites = DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all
seconds = 2
sites.each do |site|
LetsEncryptWorker.perform_in seconds, site[:id]
seconds += 10
end
puts "#{sites.length.to_s} records are primed"
end
desc 'renew_ssl_certs'
task :renew_ssl_certs => [:environment] do
delay = 0
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

6
app.rb
View file

@ -27,8 +27,10 @@ before do
if request.path.match /^\/api\//i
@api = true
content_type :json
elsif request.path.match /^\/stripe_webhook$/
# Skips the CSRF check for stripe web hooks
elsif request.path.match /^\/webhooks\//
# Skips the CSRF/validation check for stripe web hooks
elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/plan/)
redirect "/site/#{current_site.username}/confirm_email"
else
content_type :html, 'charset' => 'utf-8'
redirect '/' if request.post? && !csrf_safe?

View file

@ -11,11 +11,166 @@ get '/admin/reports' do
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
# expires 14400, :public, :must_revalidate if self.class.production? # 4 hours
@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
@ -55,43 +210,43 @@ post '/admin/email' do
redirect '/'
end
post '/admin/banip' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
redirect '/admin'
end
if site.ip.nil? || site.ip.empty?
flash[:error] = 'IP is blank, cannot continue'
redirect '/admin'
end
sites = Site.filter(ip: site.ip, is_banned: false).all
sites.each {|s| s.ban!}
flash[:error] = "#{sites.length} sites have been banned."
redirect '/admin'
end
post '/admin/banhammer' do
require_admin
site = Site[username: params[:username]]
if site.nil?
flash[:error] = 'User not found'
if params[:usernames].empty?
flash[:error] = 'no usernames provided'
redirect '/admin'
end
if site.is_banned
flash[:error] = 'User is already banned'
redirect '/admin'
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? && !site.ip.empty?
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
end
site.ban!
flash[:success] = 'MISSION ACCOMPLISHED'
flash[:success] = "#{ip_deleted_count + deleted_count} sites have been banned, including #{ip_deleted_count} matching IPs."
redirect '/admin'
end
@ -105,6 +260,7 @@ post '/admin/mark_nsfw' do
end
site.is_nsfw = true
site.admin_nsfw = true
site.save_changes validate: false
flash[:success] = 'MISSION ACCOMPLISHED'
@ -126,6 +282,14 @@ post '/admin/feature' do
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 signed_in? && current_site.is_admin
end

View file

@ -5,6 +5,31 @@ get '/api' do
erb :'api'
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[:updated_at] = file[:updated_at].rfc2822
files << new_file
end
files.each {|f| f[:path].sub!(/^\//, '')}
api_success files: files
end
post '/api/upload' do
require_api_credentials
@ -77,7 +102,6 @@ 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
@ -95,7 +119,8 @@ def api_info_for(site)
created_at: site.created_at.rfc2822,
last_updated: site.site_updated_at ? site.site_updated_at.rfc2822 : nil,
domain: site.domain,
tags: site.tags.collect {|t| t.name}
tags: site.tags.collect {|t| t.name},
latest_ipfs_hash: site.latest_archive ? site.latest_archive.ipfs_hash : nil
}
}
end
@ -113,6 +138,7 @@ end
def require_api_credentials
if !request.env['HTTP_AUTHORIZATION'].nil?
init_api_credentials
api_error(403, 'email_not_validated', 'you need to validate your email address before using the API') if email_not_validated?
else
api_error_invalid_auth
end

View file

@ -1,22 +1,7 @@
require 'net/http'
require 'uri'
get '/blog/?' do
expires 60, :public, :must_revalidate
return Net::HTTP.get_response(URI('http://blog.neocities.org')).body
redirect 'https://blog.neocities.org', 301
end
get '/blog/:article' do |article|
expires 60, :public, :must_revalidate
attempted = false
begin
return Net::HTTP.get_response(URI("http://blog.neocities.org/#{article}.html")).body
rescue => e
raise e if attempted
attempted = true
article = article.match(/^[a-zA-Z0-9-]+/).to_s
retry
end
redirect "https://blog.neocities.org/#{article}.html", 301
end

View file

@ -1,7 +1,7 @@
get '/browse/?' do
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
@surfmode = false
@page = params[:page].to_i
@page = 1 if @page == 0
params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty?
@ -11,12 +11,14 @@ get '/browse/?' do
site_dataset = browse_sites_dataset
end
site_dataset = site_dataset.paginate @current_page, Site::BROWSE_PAGINATION_LENGTH
@page_count = site_dataset.page_count || 1
site_dataset = site_dataset.paginate @page, Site::BROWSE_PAGINATION_LENGTH
@pagination_dataset = site_dataset
@sites = site_dataset.all
if params[:tag]
@title = "Sites tagged #{params[:tag]}"
end
erb :browse
end
@ -28,9 +30,12 @@ def education_sites_dataset
end
def browse_sites_dataset
site_dataset = Site.filter(is_deleted: false, is_banned: false, is_crashing: false).filter(site_changed: true)
site_dataset = Site.browse_dataset
if current_site
site_dataset.or! sites__id: current_site.id
if !current_site.blocking_site_ids.empty?
site_dataset.where!(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
end
@ -43,6 +48,9 @@ def browse_sites_dataset
end
case params[:sort_by]
when 'special_sauce'
site_dataset.exclude! score: nil
site_dataset.order! :score.desc
when 'followers'
site_dataset = site_dataset.association_left_join :follows
site_dataset.select_all! :sites
@ -75,16 +83,12 @@ def browse_sites_dataset
params[:sort_by] = 'last_updated'
site_dataset.order!(:site_updated_at.desc, :views.desc)
else
if params[:tag]
params[:sort_by] = 'views'
site_dataset.order!(:views.desc, :site_updated_at.desc)
else
site_dataset = site_dataset.association_left_join :follows
site_dataset.select_all! :sites
site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count")
site_dataset.group! :sites__id
site_dataset.order! :follow_count.desc, :updated_at.desc
end
params[:sort_by] = 'followers'
site_dataset = site_dataset.association_left_join :follows
site_dataset.select_all! :sites
site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count")
site_dataset.group! :sites__id
site_dataset.order! :follow_count.desc, :views.desc, :updated_at.desc
end
site_dataset.where! ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]

View file

@ -1,5 +1,5 @@
def new_recaptcha_valid?
return session[:captcha_valid] = true if ENV['RACK_ENV'] == 'test'
return session[:captcha_valid] = true if ENV['RACK_ENV'] == 'test' || ENV['TRAVIS']
resp = Net::HTTP.get URI(
'https://www.google.com/recaptcha/api/siteverify?'+
Rack::Utils.build_query(

View file

@ -1,6 +1,13 @@
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'
end
@ -22,4 +29,4 @@ def dashboard_init
@dir = params[:dir]
@file_list = current_site.file_list @dir
end
end

View file

@ -30,7 +30,7 @@ post '/dmca/contact' do
no_footer: true
})
flash[:success] = 'Your DCMA notification has been sent.'
flash[:success] = 'Your DMCA notification has been sent.'
redirect '/'
end
end

29
app/domain.rb Normal file
View file

@ -0,0 +1,29 @@
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

@ -6,23 +6,23 @@ get '/?' do
@suggestions = current_site.suggestions
@current_page = params[:current_page].to_i
@current_page = 1 if @current_page == 0
@page = params[:page].to_i
@page = 1 if @page == 0
if params[:activity] == 'mine'
events_dataset = current_site.latest_events(@current_page, 10)
events_dataset = current_site.latest_events(@page, 10)
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)
elsif params[:activity] == 'global'
events_dataset = Event.global_dataset @current_page
events_dataset = Event.global_dataset @page
else
events_dataset = current_site.news_feed(@current_page, 10)
events_dataset = current_site.news_feed(@page, 10)
end
@page_count = events_dataset.page_count || 1
@pagination_dataset = events_dataset
@events = events_dataset.all
current_site.events_dataset.update notification_seen: true
@ -50,10 +50,6 @@ get '/education' do
erb :education, layout: :index_layout
end
get '/tutorials' do
erb :'tutorials'
end
get '/donate' do
erb :'donate'
end
@ -82,3 +78,46 @@ end
get '/permanent-web' do
erb :'permanent_web'
end
get '/thankyou' do
erb :'thankyou'
end
get '/forgot_username' do
erb :'forgot_username'
end
post '/forgot_username' do
if params[:email].blank?
flash[:error] = 'Cannot use an empty email address!'
redirect '/forgot_username'
end
sites = Site.where(email: params[:email]).all
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

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

View file

@ -1,39 +1,43 @@
get '/password_reset' do
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.filter(email: params[:email]).all
if sites.length > 0
token = SecureRandom.uuid.gsub('-', '')
sites.each do |site|
next unless site.parent?
site.update password_reset_token: token
end
body = <<-EOT
Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. Purrrr.
body = <<-EOT
Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address.
Go to this URL to reset your password: http://neocities.org/password_reset_confirm?token=#{token}
Go to this URL to reset your password: http://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{token}
After clicking on this link, your password for all the sites registered to this email address will be changed to this token.
Token: #{token}
If you didn't request this reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
If you didn't request this password reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
Meow,
the Neocities Cat
EOT
body.strip!
body.strip!
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:email],
subject: '[Neocities] Password Reset',
body: body
})
EmailWorker.perform_async({
from: 'web@neocities.org',
to: params[:email],
subject: '[Neocities] Password Reset',
body: body
})
end
end
flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.'
@ -42,29 +46,22 @@ end
get '/password_reset_confirm' do
if params[:token].nil? || params[:token].strip.empty?
flash[:error] = 'Could not find a site with this token.'
flash[:error] = 'Token cannot be empty.'
redirect '/'
end
reset_site = Site[password_reset_token: params[:token]]
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 token.'
flash[:error] = 'Could not find a site with this username and token.'
redirect '/'
end
sites = Site.filter(email: reset_site.email).all
reset_site.password_reset_token = nil
reset_site.password_reset_confirmed = true
reset_site.save_changes
if sites.length > 0
sites.each do |site|
site.password = reset_site.password_reset_token
site.save_changes
end
session[:id] = reset_site.id
flash[:success] = 'Your password for all sites with your email address has been changed to the token sent in your e-mail. Please login and change your password as soon as possible.'
else
flash[:error] = 'Could not find a site with this token.'
end
redirect '/'
redirect '/settings#password'
end

View file

@ -100,11 +100,103 @@ get '/plan/thanks' do
erb :'plan/thanks'
end
get '/plan/bitcoin/?' do
erb :'plan/bitcoin'
end
get '/plan/alternate/?' do
redirect '/plan/bitcoin'
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/plan/paypal/return",
cancel_url: "https://neocities.org/plan",
ipn_url: "https://neocities.org/webhooks/paypal"
)
end
get '/plan/paypal' do
require_login
redirect '/plan' if parent_site.supporter?
hash = paypal_recurring_authorization_hash
if current_site.paypal_token
hash.merge! token: current_site.paypal_token
end
ppr = PayPal::Recurring.new hash
paypal_response = ppr.checkout
redirect paypal_response.checkout_url if paypal_response.valid?
end
get '/plan/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 '/plan'
end
ppr = PayPal::Recurring.new(paypal_recurring_authorization_hash.merge(
frequency: 1,
token: params[:token],
period: :monthly,
reference: current_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
current_site.paypal_token = params[:token]
current_site.paypal_profile_id = paypal_response.profile_id
current_site.paypal_active = true
current_site.plan_type = 'supporter'
current_site.save_changes validate: false
redirect '/plan/thanks-paypal'
end
get '/plan/thanks-paypal' do
require_login
erb :'plan/thanks-paypal'
end
get '/plan/alternate/?' do
erb :'/plan/alternate'
get '/plan/paypal/cancel' do
require_login
redirect '/plan' unless parent_site.paypal_active
ppr = PayPal::Recurring.new profile_id: parent_site.paypal_profile_id
ppr.cancel
parent_site.plan_type = nil
parent_site.paypal_active = false
parent_site.paypal_profile_id = nil
parent_site.paypal_token = nil
parent_site.save_changes validate: false
redirect '/plan'
end

View file

@ -66,6 +66,7 @@ post '/settings/:username/profile' do
redirect "/settings/#{@site.username}#profile"
end
=begin
post '/settings/:username/ssl' do
require_login
require_ownership_for_settings
@ -167,6 +168,7 @@ post '/settings/:username/ssl' do
flash[:success] = 'Updated SSL key/certificate.'
redirect "/settings/#{@site.username}#custom_domain"
end
=end
post '/settings/:username/change_name' do
require_login
@ -179,13 +181,13 @@ post '/settings/:username/change_name' do
redirect "/settings/#{@site.username}#username"
end
if old_username == params[:name]
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_file_paths = @site.file_list.collect {|f| f[:path]}
old_site_file_paths = @site.site_files.collect {|site_file| site_file.path}
@site.username = params[:name]
@ -195,11 +197,11 @@ post '/settings/:username/change_name' do
@site.move_files_from old_username
}
old_file_paths.each do |file_path|
@site.purge_cache file_path
old_site_file_paths.each do |site_file_path|
@site.delete_cache site_file_path
end
flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it</b>."
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
@ -211,6 +213,8 @@ 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+'
@ -221,10 +225,30 @@ post '/settings/:username/custom_domain' do
require_login
require_ownership_for_settings
original_domain = @site.domain
@site.domain = params[:domain]
begin
Socket.gethostbyname @site.values[:domain]
rescue SocketError => e
if e.message =~ /name or service not known/i
flash[:error] = 'Domain needs to be valid and already registered.'
redirect "/settings/#{@site.username}#custom_domain"
elsif e.message =~ /No address associated with hostname/i
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
raise e
end
if @site.valid?
@site.save_changes
if @site.domain != original_domain
LetsEncryptWorker.perform_async @site.id
end
flash[:success] = 'The domain has been successfully updated.'
redirect "/settings/#{@site.username}#custom_domain"
else
@ -236,7 +260,7 @@ end
post '/settings/change_password' do
require_login
if !Site.valid_login?(parent_site.username, params[:current_password])
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
@ -248,6 +272,9 @@ post '/settings/change_password' do
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
flash[:success] = 'Successfully changed password.'
@ -260,10 +287,16 @@ 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 '/settings#email'
redirect redirect_url
end
parent_site.email = params[:email]
@ -273,12 +306,17 @@ post '/settings/change_email' do
if parent_site.valid?
parent_site.save_changes
send_confirmation_email
flash[:success] = 'Successfully changed email. We have sent a confirmation email, please use it to confirm your email address.'
redirect '/settings#email'
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 '/settings#email'
redirect redirect_url
end
post '/settings/change_email_notification' do
@ -331,4 +369,30 @@ get '/settings/unsubscribe_email/?' do
@message = 'There was an error unsubscribing your email address. Please contact support.'
end
erb :'settings/account/unsubscribe'
end
end
post '/settings/update_card' do
require_login
customer = Stripe::Customer.retrieve current_site.stripe_customer_id
old_card_ids = customer.sources.collect {|s| s.id}
begin
customer.sources.create source: params[:stripe_token]
rescue Stripe::InvalidRequestError => e
if e.message.match /cannot use a.+token more than once/
flash[:error] = 'Card is already being used.'
redirect '/settings#billing'
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

@ -13,9 +13,9 @@ get '/site/:username/?' do |username|
@title = site.title
@current_page = params[:current_page]
@current_page = @current_page.to_i
@current_page = 1 if @current_page == 0
@page = params[:page]
@page = @page.to_i
@page = 1 if @page == 0
if params[:event_id]
not_found unless params[:event_id].is_integer?
@ -23,10 +23,11 @@ get '/site/:username/?' do |username|
not_found if event.nil?
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
else
events_dataset = site.latest_events(@current_page, 10)
events_dataset = site.latest_events(@page, 10)
end
@page_count = events_dataset.page_count || 1
@pagination_dataset = events_dataset
@latest_events = events_dataset.all
erb :'site', locals: {site: site, is_current_site: site == current_site}
@ -161,7 +162,6 @@ post '/site/create_directory' do
require_login
path = "#{params[:dir] || ''}/#{params[:name]}"
result = current_site.create_directory path
if result != true
@ -172,8 +172,23 @@ post '/site/create_directory' do
end
get '/site/:username/confirm_email/:token' do
if current_site && current_site.email_confirmed
return erb(:'site_email_confirmed')
end
site = Site[username: params[:username]]
if !site.nil? && site.email_confirmation_token == params[:token]
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
@ -183,6 +198,47 @@ get '/site/:username/confirm_email/:token' do
end
end
get '/site/:username/confirm_email' do
require_login
@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/report' do |username|
site = Site[username: username]

View file

@ -134,10 +134,11 @@ end
post '/site_files/delete' do
require_login
current_site.delete_file params[:filename]
flash[:success] = "Deleted #{params[:filename]}."
path = HTMLEntities.new.decode params[:filename]
current_site.delete_file path
flash[:success] = "Deleted #{params[:filename]}. Please note it can take up to 30 minutes for deleted files to stop being viewable on your site."
dirname = Pathname(params[:filename]).dirname
dirname = Pathname(path).dirname
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
redirect "/dashboard#{dir_query}"
@ -151,15 +152,18 @@ get '/site_files/:username.zip' do |username|
send_file zipfile_path
end
get '/site_files/download/:filename' do |filename|
get %r{\/site_files\/download\/(.+)} do
require_login
content_type 'application/octet-stream'
not_found if params[:captures].nil? || params[:captures].length != 1
filename = params[:captures].first
attachment filename
current_site.get_file 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
extname = File.extname @filename
@ace_mode = case extname
@ -171,15 +175,20 @@ get %r{\/site_files\/text_editor\/(.+)} do
nil
end
begin
@file_data = current_site.get_file @filename
rescue Errno::ENOENT
flash[:error] = 'We could not find the requested file.'
redirect '/dashboard'
rescue Errno::EISDIR
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
@ -207,6 +216,10 @@ get '/site_files/allowed_types' do
erb :'site_files/allowed_types'
end
get '/site_files/hotlinking' do
erb :'site_files/hotlinking'
end
get '/site_files/mount_info' do
erb :'site_files/mount_info'
end

View file

@ -1,100 +0,0 @@
get '/stats/?' do
# expires 14400, :public, :must_revalidate if self.class.production? # 4 hours
@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_sites: Site.count,
total_unbanned_sites: Site.where(is_banned: false).count,
total_banned_sites: Site.where(is_banned: true).count,
total_nsfw_sites: Site.where(is_nsfw: true).count,
total_unbanned_nsfw_sites: Site.where(is_banned: false, is_nsfw: true).count,
total_banned_nsfw_sites: Site.where(is_banned: true, is_nsfw: true).count
}
# Start with the date of the first created site
start = Site.select(:created_at).
exclude(created_at: nil).
order(:created_at).
first[:created_at].to_date
runner = start
monthly_stats = []
now = 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 :'stats'
end

View file

@ -1,8 +1,8 @@
get '/surf/?' do
@current_page = params[:current_page].to_i || 1
@page = params[:page].to_i || 1
params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty?
site_dataset = browse_sites_dataset
site_dataset = site_dataset.paginate @current_page, 1
site_dataset = site_dataset.paginate @page, 1
@page_count = site_dataset.page_count || 1
@site = site_dataset.first
redirect "/browse?#{Rack::Utils.build_query params}" if @site.nil?

View file

@ -16,7 +16,10 @@ post '/tags/remove' do
if params[:tags].is_a?(Array)
DB.transaction {
params[:tags].each {|tag| current_site.remove_tag Tag[name: tag]}
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

41
app/tutorial.rb Normal file
View file

@ -0,0 +1,41 @@
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
redirect "/tutorial/#{params[:section]}/1"
end
get '/tutorial/:section/:page/?' do
require_login
@page = params[:page]
not_found if @page.to_i == 0
not_found unless %w{html css js}.include?(params[:section])
@section = params[:section]
@title = "#{params[:section].upcase} Tutorial - #{@page}/10"
erb "tutorial/layout".to_sym
end

View file

@ -1,24 +1,16 @@
def stripe_get_site_from_event(event)
customer_id = event['data']['object']['customer']
customer = Stripe::Customer.retrieve customer_id
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
})
# Some old accounts only have a username for the desc
desc_split = customer.description.split(' - ')
if desc_split.length == 1
site_where = {username: desc_split.first}
end
if desc_split.last.to_i == 0
site_where = {username: desc_split.first}
else
site_where = {id: desc_split.last}
end
Site.where(site_where).first
'ok'
end
post '/stripe_webhook' do
post '/webhooks/stripe' do
event = JSON.parse request.body.read
if event['type'] == 'customer.created'
username = event['data']['object']['description'].split(' - ').first
@ -60,3 +52,23 @@ post '/stripe_webhook' do
'ok'
end
def stripe_get_site_from_event(event)
customer_id = event['data']['object']['customer']
customer = Stripe::Customer.retrieve customer_id
# Some old accounts only have a username for the desc
desc_split = customer.description.split(' - ')
if desc_split.length == 1
site_where = {username: desc_split.first}
end
if desc_split.last.to_i == 0
site_where = {username: desc_split.first}
else
site_where = {id: desc_split.last}
end
Site.where(site_where).first
end

View file

@ -1,13 +1,3 @@
def kickstarter_days_remaining
ending = Time.parse('Sat, Jul 25 2015 3:05 PM PDT')
today = Time.now
remaining = ending - today
return 0 if remaining < 0
((ending - today) / 86400).to_i
end
def dashboard_if_signed_in
redirect '/dashboard' if signed_in?
end
@ -82,12 +72,19 @@ def encoding_fix(file)
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)
body: Tilt.new('./views/templates/email/confirm.erb', pretty: true).render(self, site: site)
})
end
@ -115,6 +112,20 @@ def plan_pricing_button(plan_type)
button_title = parent_site.plan_type == 'free' ? 'Upgrade' : 'Change'
end
if button_title == 'Change' && parent_site && parent_site.paypal_active
return %{<a href="/plan/paypal/cancel" onclick="return confirm('This will end your supporter plan.')" class="btn-Action">Change</a>}
end
%{<a data-plan_name="#{Site::PLAN_FEATURES[plan_type.to_sym][:name]}" data-plan_type="#{plan_type}" data-plan_price="#{plan_price}" onclick="card = new Skeuocard($('#skeuocard')); return false" class="btn-Action planPricingButton">#{button_title}</a>}
end
end
def dont_browser_cache
@dont_browser_cache = true
end
def email_not_validated?
return false if current_site && current_site.created_at < Site::EMAIL_VALIDATION_CUTOFF_DATE
current_site && current_site.parent? && !current_site.is_education && !current_site.email_confirmed && !current_site.supporter?
end

9
code-of-conduct.txt Normal file
View file

@ -0,0 +1,9 @@
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.

View file

@ -1,11 +1,6 @@
require 'rubygems'
require './app.rb'
require 'sidekiq/web'
require 'unicorn/preread_input'
if defined?(Unicorn)
use Unicorn::PrereadInput
end
map('/') do
use(Rack::Cache,
@ -35,13 +30,18 @@ map '/webdav' do
end
if Site.valid_file_type?(filename: path, tempfile: tmpfile)
site.store_file path, tmpfile
site.store_files [{filename: path, tempfile: tmpfile}]
return [201, {}, ['']]
else
return [415, {}, ['']]
end
end
if env['REQUEST_METHOD'] == 'MKCOL'
site.create_directory env['PATH_INFO']
return [201, {}, ['']]
end
if env['REQUEST_METHOD'] == 'MOVE'
tmpfile = Tempfile.new 'moved_file'
tmpfile.close
@ -51,7 +51,7 @@ map '/webdav' do
FileUtils.cp site.files_path(env['PATH_INFO']), tmpfile.path
DB.transaction do
site.store_file destination, tmpfile
site.store_files [{filename: destination, tempfile: tmpfile}]
site.delete_file env['PATH_INFO']
end

View file

@ -12,6 +12,11 @@ development:
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/
test:
database: 'postgres://neocities@localhost/neocities_test'
database_pool: 1
@ -25,3 +30,8 @@ test:
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/

View file

@ -7,4 +7,9 @@ phantomjs_url:
- http://localhost:8910
ip_hash_salt: "400$8$1$fc21863da5d531c1"
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/

View file

@ -11,6 +11,8 @@ require 'logger'
Bundler.require
Bundler.require :development if ENV['RACK_ENV'] == 'development'
require 'tilt/erubis'
Dir['./ext/**/*.rb'].each {|f| require f}
# :nocov:
@ -31,6 +33,8 @@ raise 'hash_ip_salt is required' unless $config['ip_hash_salt']
DB = Sequel.connect $config['database'], sslmode: 'disable', max_connections: $config['database_pool']
DB.extension :pagination
require 'will_paginate/sequel'
# :nocov:
=begin
if defined?(Pry)
@ -58,6 +62,14 @@ Sidekiq.configure_client do |config|
config.redis = sidekiq_redis_config
end
if ENV['RACK_ENV'] == 'test'
$redis = MockRedis.new
else
$redis = Redis.new
end
$redis_cache = Redis::Namespace.new :cache, redis: $redis
# :nocov:
if ENV['RACK_ENV'] == 'development'
# Run async jobs immediately in development.
@ -122,6 +134,13 @@ if ENV['RACK_ENV'] != 'development'
Sass::Plugin.options[:full_exception] = false
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 = {}
@ -129,3 +148,7 @@ $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

View file

@ -2,4 +2,8 @@ class NilClass
def empty?
true
end
def blank?
true
end
end

41
ext/base58.rb Normal file
View file

@ -0,0 +1,41 @@
module Base58
class << self
def int_to_base58(int_val, leading_zero_bytes=0)
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
base58_val, base = '', alpha.size
while int_val > 0
int_val, remainder = int_val.divmod(base)
base58_val = alpha[remainder] + base58_val
end
base58_val
end
def base58_to_int(base58_val)
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
int_val, base = 0, alpha.size
base58_val.reverse.each_char.with_index do |char,index|
raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = alpha.index(char)
int_val += char_index*(base**index)
end
int_val
end
def base58_to_bytestring(base58_val)
[Base58.decode_base58(base58_val)].pack('H*')
end
def encode_base58(hex)
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
("1"*leading_zero_bytes) + int_to_base58( hex.to_i(16) )
end
def decode_base58(base58_val)
s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
s = '' if s == '00'
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
s
end
alias_method :base58_to_hex, :decode_base58
end
end

View file

@ -31,7 +31,7 @@ class Numeric
end
def to_comma_separated
self.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse
self.to_i.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse
end
def format_large_number

View file

@ -15,4 +15,9 @@ class String
def is_integer?
true if Integer(self) rescue false
end
def blank?
return true if self == ''
false
end
end

View file

@ -0,0 +1,56 @@
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

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

View file

@ -0,0 +1,12 @@
Sequel.migration do
up {
DB.create_table! :banned_referrers do
primary_key :id
String :name
end
}
down {
DB.drop_table :banned_referrers
}
end

View file

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

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB['alter table follows add constraint one_follow_per_site unique (site_id, actioning_site_id)'].first
}
down {
DB['alter table follows drop constraint one_follow_per_site'].first
}
end

View file

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

View file

@ -0,0 +1,11 @@
# IT'S MADE OUT OF FUCKING PEOPLE
Sequel.migration do
up {
DB.add_column :sites, :score, :integer
}
down {
DB.drop_column :sites, :score
}
end

View file

@ -0,0 +1,13 @@
# IT'S MADE OUT OF FUCKING DECIMAL PEOPLE
Sequel.migration do
up {
DB.drop_column :sites, :score
DB.add_column :sites, :score, :decimal, default: 0
}
down {
DB.drop_column :sites, :score
DB.add_column :sites, :score, :integer
}
end

View file

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

View file

@ -0,0 +1,17 @@
Sequel.migration do
up {
DB.create_table! :daily_site_stats do
primary_key :id
Date :created_at, index: true
Integer :hits, default: 0
Integer :views, default: 0
Integer :comments, default: 0
Integer :follows, default: 0
Integer :site_updates, default: 0
end
}
down {
DB.drop_table :daily_site_stats
}
end

View file

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

View file

@ -0,0 +1,11 @@
Sequel.migration do
up {
DB.drop_column :daily_site_stats, :bandwidth
DB.add_column :daily_site_stats, :bandwidth, :integer, default: 0
}
down {
DB.drop_column :daily_site_stats, :bandwidth
DB.add_column :daily_site_stats, :bandwidth, :integer
}
end

View file

@ -0,0 +1,15 @@
Sequel.migration do
up {
alter_table(:stats) do
set_column_type :hits, Bignum
set_column_type :views, Bignum
end
}
down {
alter_table(:stats) do
set_column_type :hits, Integer
set_column_type :views, Integer
end
}
end

View file

@ -0,0 +1,19 @@
Sequel.migration do
up {
alter_table(:daily_site_stats) do
set_column_type :hits, Bignum
set_column_type :views, Bignum
set_column_type :bandwidth, Bignum
set_column_type :site_updates, Bignum
end
}
down {
alter_table(:daily_site_stats) do
set_column_type :hits, Integer
set_column_type :views, Integer
set_column_type :bandwidth, Integer
set_column_type :site_updates, Integer
end
}
end

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.add_column :site_files, :classifier, :text, default: nil, index: true
}
down {
DB.drop_column :site_files, :classifier
}
end

View file

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

View file

@ -0,0 +1,14 @@
Sequel.migration do
up {
DB.add_column :sites, :gandi_handle, :text, index: true
# This is not as horrible as it looks.
# It basically serves as a temp password when account is released from reseller account.
DB.add_column :sites, :gandi_password, :text
}
down {
DB.drop_column :sites, :gandi_handle
DB.drop_column :sites, :gandi_password
}
end

View file

@ -0,0 +1,23 @@
Sequel.migration do
up {
DB.drop_column :sites, :gandi_handle
DB.drop_column :sites, :gandi_password
DB.create_table! :domains do
primary_key :id
Integer :site_id, index: true
String :gandi_handle
String :gandi_password
String :gandi_domain_id
String :name
DateTime :created_at
DateTime :released_at
end
}
down {
DB.drop_table :domains
DB.add_column :sites, :gandi_handle, :text, index: true
DB.add_column :sites, :gandi_password, :text
}
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,47 @@
require 'base32'
class Archive < Sequel::Model
many_to_one :site
set_primary_key [:site_id, :ipfs_hash]
unrestrict_primary_key
MAXIMUM_ARCHIVES_PER_SITE = 100
ARCHIVE_WAIT_TIME = 1.hour
def before_destroy
unpin
super
end
def self.base58_to_hshca(base58)
Base32.encode(Base58.base58_to_bytestring(base58)).gsub('=', '').downcase
end
def hshca_hash
self.class.base58_to_hshca ipfs_hash
end
def unpin
# Not ideal. An SoA version is in progress.
if ENV['RACK_ENV'] == 'production' && $config['ipfs_ssh_host'] && $config['ipfs_ssh_user']
rbox = Rye::Box.new $config['ipfs_ssh_host'], :user => $config['ipfs_ssh_user']
rbox.disable_safe_mode
begin
response = rbox.execute "ipfs pin rm #{ipfs_hash}"
output_array = response
rescue => e
return true if e.message =~ /indirect pins cannot be removed directly/
ensure
rbox.disconnect
end
else
line = Cocaine::CommandLine.new('ipfs', 'pin rm :ipfs_hash')
binding.pry
response = line.run ipfs_hash: ipfs_hash
output_array = response.to_s.split("\n")
end
end
def url
"https://#{ipfs_hash}.ipfs.neocities.org"
"http://#{hshca_hash}.ipfs.neocitiesops.net"
end
end

View file

@ -0,0 +1,2 @@
class DailySiteStat < Sequel::Model
end

View file

@ -8,7 +8,7 @@ class Event < Sequel::Model
many_to_one :site_change
many_to_one :profile_comment
one_to_many :likes
one_to_many :comments
one_to_many :comments, order: :created_at
many_to_one :site
many_to_one :actioning_site, key: :actioning_site_id, class: :Site

View file

@ -30,14 +30,15 @@ class Site < Sequel::Model
application/xml
audio/midi
text/cache-manifest
application/rss+xml
}
VALID_EXTENSIONS = %w{
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp less sass rss kml dae obj mtl
}
VALID_EDITABLE_EXTENSIONS = %w{
html htm txt js css md manifest
html htm txt js css md manifest less
}
MINIMUM_PASSWORD_LENGTH = 5
@ -74,6 +75,8 @@ class Site < Sequel::Model
/PHP\.Hide/
]
EMPTY_FILE_HASH = Digest::SHA1.hexdigest ''
PHISHING_FORM_REGEX = /www.formbuddy.com\/cgi-bin\/form.pl/i
SPAM_MATCH_REGEX = ENV['RACK_ENV'] == 'test' ? /pillz/ : /#{$config['spam_smart_filter'].join('|')}/i
EMAIL_SANITY_REGEX = /.+@.+\..+/i
@ -85,7 +88,7 @@ class Site < Sequel::Model
SUGGESTIONS_LIMIT = 30
SUGGESTIONS_VIEWS_MIN = 500
CHILD_SITES_MAX = 100
CHILD_SITES_MAX = 30
IP_CREATE_LIMIT = 1000
TOTAL_IP_CREATE_LIMIT = 10000
@ -103,7 +106,7 @@ class Site < Sequel::Model
custom_ssl_certificates: true,
no_file_restrictions: true,
custom_domains: true,
maximum_site_files: 25000
maximum_site_files: 50000
}
PLAN_FEATURES[:free] = PLAN_FEATURES[:supporter].merge(
@ -115,9 +118,12 @@ class Site < Sequel::Model
custom_ssl_certificates: false,
no_file_restrictions: false,
custom_domains: false,
maximum_site_files: 1000
maximum_site_files: 2000
)
EMAIL_VALIDATION_CUTOFF_DATE = Time.parse('May 16, 2016')
DISPOSABLE_EMAIL_BLACKLIST_PATH = File.join(DIR_ROOT, 'files', 'disposable_email_blacklist.conf')
def self.newsletter_sites
Site.select(:email).
exclude(email: 'nil').exclude(is_banned: true).
@ -157,6 +163,8 @@ class Site < Sequel::Model
EMAIL_BLAST_MAXIMUM_PER_DAY = 1000
end
MAXIMUM_EMAIL_CONFIRMATIONS = 20
many_to_many :tags
one_to_many :profile_comments
@ -319,7 +327,7 @@ class Site < Sequel::Model
end
def is_following?(site)
followings_dataset.select(:id).filter(site_id: site.id).first ? true : false
followings_dataset.select(:follows__id).filter(site_id: site.id).first ? true : false
end
def toggle_follow(site)
@ -409,16 +417,16 @@ class Site < Sequel::Model
FileUtils.cp template_file_path('style.css'), tmpfile.path
files << {filename: 'style.css', tempfile: tmpfile}
tmpfile = Tempfile.new 'cat.png'
tmpfile = Tempfile.new 'neocities.png'
tmpfile.close
FileUtils.cp template_file_path('cat.png'), tmpfile.path
files << {filename: 'cat.png', tempfile: tmpfile}
FileUtils.cp template_file_path('neocities.png'), tmpfile.path
files << {filename: 'neocities.png', tempfile: tmpfile}
store_files files, new_install: true
end
def get_file(path)
File.read files_path(path)
File.read current_files_path(path)
end
def before_destroy
@ -447,7 +455,7 @@ class Site < Sequel::Model
DB.transaction {
self.is_banned = true
self.updated_at = Time.now
self.banned_at = Time.now
save(validate: false)
if !Dir.exist? BANNED_SITES_ROOT
@ -457,8 +465,8 @@ class Site < Sequel::Model
FileUtils.mv files_path, File.join(BANNED_SITES_ROOT, username)
}
file_list.each do |path|
purge_cache path
site_files.each do |site_file|
delete_cache site_file.path
end
end
@ -468,6 +476,16 @@ class Site < Sequel::Model
}
end
# Who this site follows
def followings_dataset
super.select_all(:follows).inner_join(:sites, :id=>:site_id).exclude(:sites__is_deleted => true).exclude(:sites__is_banned => true)
end
# Who this site is following
def follows_dataset
super.select_all(:follows).inner_join(:sites, :id=>:actioning_site_id).exclude(:sites__is_deleted => true).exclude(:sites__is_banned => true)
end
=begin
def follows_dataset
super.where(Sequel.~(site_id: blocking_site_ids))
@ -486,6 +504,7 @@ class Site < Sequel::Model
=end
def commenting_allowed?
return false if owner.commenting_banned == true
return true if owner.commenting_allowed
if owner.supporter?
@ -522,6 +541,21 @@ class Site < Sequel::Model
!username.empty? && username.match(/^[a-zA-Z0-9_\-]+$/i)
end
def self.disposable_email?(email)
return false unless File.exist?(DISPOSABLE_EMAIL_BLACKLIST_PATH)
return false if email.blank?
email.strip!
disposable_email_domains = File.readlines DISPOSABLE_EMAIL_BLACKLIST_PATH
disposable_email_domains.each do |disposable_email_domain|
return true if email.match disposable_email_domain.strip
end
false
end
def okay_to_upload?(uploaded_file)
return true if [:supporter].include?(plan_type.to_sym)
return false if self.class.possible_phishing?(uploaded_file)
@ -580,16 +614,41 @@ class Site < Sequel::Model
# We gotta flush the dirname too if it's an index file.
if relative_path != '' && relative_path.match(/\/$|index\.html?$/i)
PurgeCacheOrderWorker.perform_async username, relative_path
PurgeCacheOrderWorker.perform_async username, Pathname(relative_path).dirname.to_s
purge_file_path = Pathname(relative_path).dirname.to_s
PurgeCacheOrderWorker.perform_async username, '/?surf=1' if purge_file_path == '/'
PurgeCacheOrderWorker.perform_async username, purge_file_path
else
PurgeCacheOrderWorker.perform_async username, relative_path
end
end
# TODO DRY this up
def delete_cache(path)
relative_path = path.gsub base_files_path, ''
DeleteCacheOrderWorker.perform_async username, relative_path
# We gotta flush the dirname too if it's an index file.
if relative_path != '' && relative_path.match(/\/$|index\.html?$/i)
purge_file_path = Pathname(relative_path).dirname.to_s
DeleteCacheOrderWorker.perform_async username, '/?surf=1' if purge_file_path == '/'
DeleteCacheOrderWorker.perform_async username, purge_file_path
end
end
Rye::Cmd.add_command :ipfs, nil, 'add', :r
def add_to_ipfs
# Not ideal. An SoA version is in progress.
if archives_dataset.count > Archive::MAXIMUM_ARCHIVES_PER_SITE
archives_dataset.order(:updated_at).first.destroy
end
if $config['ipfs_ssh_host'] && $config['ipfs_ssh_user']
rbox = Rye::Box.new $config['ipfs_ssh_host'], :user => $config['ipfs_ssh_user']
begin
@ -607,6 +666,12 @@ class Site < Sequel::Model
output_array.last.split(' ')[1]
end
def purge_old_archives
archives_dataset.order(:updated_at).offset(Archive::MAXIMUM_ARCHIVES_PER_SITE).all.each do |archive|
archive.destroy
end
end
def archive!
#if ENV["RACK_ENV"] == 'test'
# ipfs_hash = "QmcKi2ae3uGb1kBg1yBpsuwoVqfmcByNdMiZ2pukxyLWD8"
@ -638,6 +703,32 @@ class Site < Sequel::Model
return 'Directory (or file) already exists.'
end
path_dirs = path.to_s.split('/').select {|p| ![nil, '.', ''].include?(p) }
path_site_file = ''
until path_dirs.empty?
if path_site_file == ''
path_site_file += path_dirs.shift
else
path_site_file += '/' + path_dirs.shift
end
raise ArgumentError, 'directory name cannot be empty' if path_site_file == ''
site_file = SiteFile.where(site_id: self.id, path: path_site_file).first
if site_file.nil?
SiteFile.create(
site_id: self.id,
path: path_site_file,
is_directory: true,
created_at: Time.now,
updated_at: Time.now
)
end
end
FileUtils.mkdir_p relative_path
true
end
@ -650,10 +741,12 @@ class Site < Sequel::Model
Zip::Archive.open(tmpfile.path, Zip::CREATE) do |ar|
ar.add_dir(zip_name)
end
Dir.glob("#{base_files_path}/**/*").each do |path|
relative_path = path.gsub(base_files_path+'/', '')
Dir.glob("#{base_files_path}/**/*").each do |path|
relative_path = path.gsub(base_files_path+'/', '')
Zip::Archive.open(tmpfile.path, Zip::CREATE) do |ar|
if File.directory?(path)
ar.add_dir(zip_name+'/'+relative_path)
else
@ -741,6 +834,14 @@ class Site < Sequel::Model
# super
# end
def domain=(domain)
super SimpleIDN.to_ascii(domain)
end
def domain
SimpleIDN.to_unicode values[:domain]
end
def validate
super
@ -764,6 +865,14 @@ class Site < Sequel::Model
errors.add :email, 'An email address is required.'
end
if parent? && values[:email] =~ /@neocities.org/
errors.add :email, 'Cannot use this email address.'
end
if parent? && new? && self.class.disposable_email?(values[:email])
errors.add :email, 'Cannot use a disposable email address.'
end
# Check for existing email if new or changing email.
if new? || @original_email
email_check = self.class.select(:id).filter(email: values[:email])
@ -793,15 +902,10 @@ class Site < Sequel::Model
end
if !values[:domain].nil? && !values[:domain].empty?
if values[:domain] =~ /neocities\.org/ || values[:domain] =~ /neocitiesops\.net/
errors.add :domain, "Domain is already being used.. by Neocities."
end
if !(values[:domain] =~ /^[a-zA-Z0-9.-]+\.[a-zA-Z0-9]+$/i) || values[:domain].length > 90
errors.add :domain, "Domain provided is not valid. Must take the form of domain.com"
end
site = Site[domain: values[:domain]]
if !site.nil? && site.id != self.id
errors.add :domain, "Domain provided is already being used by another site, please choose another."
@ -871,6 +975,12 @@ class Site < Sequel::Model
File.join TEMPLATE_ROOT, name
end
def current_base_files_path(name=username)
raise 'username missing' if name.nil? || name.empty?
return File.join BANNED_SITES_ROOT, name if is_banned
base_files_path name
end
def base_files_path(name=username)
raise 'username missing' if name.nil? || name.empty?
File.join SITE_FILES_ROOT, name
@ -881,7 +991,7 @@ class Site < Sequel::Model
path ||= ''
clean = []
parts = path.split '/'
parts = path.to_s.split '/'
parts.each do |part|
next if part.empty? || part == '.'
@ -891,6 +1001,10 @@ class Site < Sequel::Model
clean.join '/'
end
def current_files_path(path='')
File.join current_base_files_path, scrubbed_path(path)
end
def files_path(path='')
File.join base_files_path, scrubbed_path(path)
end
@ -929,8 +1043,16 @@ class Site < Sequel::Model
end
def actual_space_used
space = Dir.glob(File.join(files_path, '*')).collect {|p| File.size(p)}.inject {|sum,x| sum += x}
space.nil? ? 0 : space
space = 0
files = Dir.glob File.join(files_path, '**', '*')
files.each do |file|
next if File.directory? file
space += File.size file
end
space
end
def total_space_used
@ -945,14 +1067,18 @@ class Site < Sequel::Model
end
def maximum_space
PLAN_FEATURES[(parent? ? self : parent).plan_type.to_sym][:space]
plan_space = PLAN_FEATURES[(parent? ? self : parent).plan_type.to_sym][:space]
return custom_max_space if custom_max_space > plan_space
plan_space
end
def space_percentage_used
((total_space_used.to_f / maximum_space) * 100).round(1)
end
# Note: Change Stat#prune! if you change this business logic.
# Note: Change Stat#prune! and the nginx map compiler if you change this business logic.
def supporter?
owner.plan_type != 'free'
end
@ -965,6 +1091,10 @@ class Site < Sequel::Model
PLAN_FEATURES[plan_type.to_sym][:name]
end
def stripe_paying_supporter?
stripe_customer_id && !plan_ended && values[:plan_type].match(/free|special/).nil?
end
def unconverted_legacy_supporter?
stripe_customer_id && !plan_ended && values[:plan_type].nil? && stripe_subscription_id.nil?
end
@ -974,8 +1104,9 @@ class Site < Sequel::Model
!values[:plan_type].match(/plan_/).nil?
end
# Note: Change Stat#prune! if you change this business logic.
# Note: Change Stat#prune! and the nginx map compiler if you change this business logic.
def plan_type
return 'supporter' if owner.values[:paypal_active] == true
return 'free' if owner.values[:plan_type].nil?
return 'supporter' if owner.values[:plan_type].match /^plan_/
return 'supporter' if owner.values[:plan_type] == 'special'
@ -1037,13 +1168,70 @@ class Site < Sequel::Model
end
end
def self.compute_scores
select(:id, :username, :created_at, :updated_at, :views, :featured_at, :changed_count, :api_calls).exclude(is_banned: true).exclude(is_crashing: true).exclude(is_nsfw: true).exclude(updated_at: nil).where(site_changed: true).all.each do |s|
s.score = s.compute_score
s.save_changes validate: false
end
end
SCORE_GRAVITY = 1.8
def compute_score
points = 0
points += follows_dataset.count * 30
points += profile_comments_dataset.count * 1
points += views / 1000
points += 20 if !featured_at.nil?
# penalties
points = 0 if changed_count < 2
points = 0 if api_calls && api_calls > 1000
(points / ((Time.now - updated_at) / 7.days)**SCORE_GRAVITY).round(4)
end
=begin
def compute_score
score = 0
score += (Time.now - created_at) / 1.day
score -= ((Time.now - updated_at) / 1.day) * 2
score += 500 if (updated_at > 1.week.ago)
score -= 1000 if
follow_count = follows_dataset.count
score -= 1000 if follow_count == 0
score += follow_count * 100
score += profile_comments_dataset.count * 5
score += profile_commentings_dataset.count
score.to_i
end
=end
def self.browse_dataset
dataset.where is_deleted: false, is_banned: false, is_crashing: false, site_changed: true
end
def suggestions(limit=SUGGESTIONS_LIMIT, offset=0)
suggestions_dataset = Site.exclude(id: id).exclude(is_banned: true).exclude(is_nsfw: true).order(:views.desc, :updated_at.desc)
suggestions = suggestions_dataset.where(tags: tags).limit(limit, offset).all
return suggestions if suggestions.length == limit
suggestions += suggestions_dataset.where("views >= #{SUGGESTIONS_VIEWS_MIN}").limit(limit-suggestions.length).order(Sequel.lit('RANDOM()')).all
# Old.
#suggestions += suggestions_dataset.where("views >= #{SUGGESTIONS_VIEWS_MIN}").limit(limit-suggestions.length).order(Sequel.lit('RANDOM()')).all
# New:
site_dataset = self.class.browse_dataset.association_left_join :follows
site_dataset.select_all! :sites
site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count")
site_dataset.group! :sites__id
site_dataset.order! :follow_count.desc, :updated_at.desc
site_dataset.where! "views >= #{SUGGESTIONS_VIEWS_MIN}"
site_dataset.limit! limit-suggestions.length
#site_dataset.order! Sequel.lit('RANDOM()')
suggestions += site_dataset.all
end
def screenshot_path(path, resolution)
@ -1099,11 +1287,53 @@ class Site < Sequel::Model
end
end
def empty_index?
!site_files_dataset.where(path: /^\/?index.html$/).where(sha1_hash: EMPTY_FILE_HASH).first.nil?
end
def classify(path)
return nil unless classification_allowed? path
#$classifier.classify process_for_classification(path)
end
def classification_scores(path)
return nil unless classification_allowed? path
#$classifier.classification_scores process_for_classification(path)
end
def train(path, category='ham')
return nil unless classification_allowed? path
# $trainer.train(category, process_for_classification(path))
site_file = site_files_dataset.where(path: path).first
site_file.classifier = category
site_file.save_changes validate: false
end
def untrain(path, category='ham')
return nil unless classification_allowed? path
# $trainer.untrain(category, process_for_classification(path))
site_file = site_files_dataset.where(path: path).first
site_file.classifier = category
site_file.save_changes validate: false
end
def classification_allowed?(path)
site_file = site_files_dataset.where(path: path).first
return false if site_file.is_directory
return false if site_file.size > SiteFile::CLASSIFIER_LIMIT
return false if !path.match(/\.html$/)
true
end
def process_for_classification(path)
sanitized = Sanitize.fragment get_file(path)
sanitized.gsub(/(http|https):\/\//, '').gsub(/[^\w\s]/, '').downcase.split.uniq.select{|v| v.length < SiteFile::CLASSIFIER_WORD_LIMIT}.join(' ')
end
# array of hashes: filename, tempfile, opts.
def store_files(files, opts={})
results = []
new_size = 0
html_uploaded = false
if too_many_files?(files.length)
results << false
@ -1111,35 +1341,53 @@ class Site < Sequel::Model
end
files.each do |file|
html_uploaded = true if file[:filename].match HTML_REGEX
existing_size = 0
site_file = site_files_dataset.where(path: scrubbed_path(file[:filename])).first
if site_file
existing_size = site_file.size
end
res = store_file(file[:filename], file[:tempfile], file[:opts] || opts)
if res == true
new_size -= existing_size
new_size += file[:tempfile].size
end
results << res
end
if results.include? true && opts[:new_install] != true
time = Time.now
sql = DB["update sites set site_changed=?, site_updated_at=?, updated_at=?, changed_count=changed_count+1, space_used=space_used#{new_size < 0 ? new_size.to_s : '+'+new_size.to_s} where id=?",
true,
time,
time,
self.id
]
sql.first
if results.include? true
DB["update sites set space_used=space_used#{new_size < 0 ? new_size.to_s : '+'+new_size.to_s} where id=?", self.id].first
if opts[:new_install] != true
if files.select {|f| f[:filename] =~ /^\/?index.html$/}.length > 0 || site_changed == true
index_changed = true
else
index_changed = false
end
index_changed = false if empty_index?
time = Time.now
sql = DB["update sites set site_changed=?, site_updated_at=?, updated_at=?, changed_count=changed_count+1 where id=?",
index_changed,
time,
time,
self.id
]
sql.first
ArchiveWorker.perform_in Archive::ARCHIVE_WAIT_TIME, self.id
end
reload
#SiteChange.record self, relative_path unless opts[:new_install]
ArchiveWorker.perform_async self.id
end
results
@ -1147,36 +1395,9 @@ class Site < Sequel::Model
def delete_file(path)
return false if files_path(path) == files_path
begin
FileUtils.rm files_path(path)
rescue Errno::EISDIR
site_files.each do |site_file|
if site_file.path.match /^#{path}\//
site_file.destroy
end
end
FileUtils.remove_dir files_path(path), true
rescue Errno::ENOENT
end
purge_cache path
ext = File.extname(path).gsub(/^./, '')
screenshots_delete(path) if ext.match HTML_REGEX
thumbnails_delete(path) if ext.match IMAGE_REGEX
path = path[1..path.length] if path[0] == '/'
DB.transaction do
site_file = site_files_dataset.where(path: path).first
if site_file
DB['update sites set space_used=space_used-? where id=?', site_file.size, self.id].first
site_file.delete
end
SiteChangeFile.filter(site_id: self.id, filename: path).delete
end
path = scrubbed_path path
site_file = site_files_dataset.where(path: path).first
site_file.destroy if site_file
true
end
@ -1195,6 +1416,15 @@ class Site < Sequel::Model
return false
end
if pathname.extname.match HTML_REGEX
# SPAM and phishing checking code goes here
end
relative_path_dir = Pathname(relative_path).dirname
create_directory relative_path_dir unless relative_path_dir == '.'
uploaded_size = uploaded.size
if relative_path == 'index.html'
begin
new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text
@ -1207,18 +1437,6 @@ class Site < Sequel::Model
end
end
if pathname.extname.match HTML_REGEX
# SPAM and phishing checking code goes here
end
dirname = pathname.dirname.to_s
if !File.exists? dirname
FileUtils.mkdir_p dirname
end
uploaded_size = uploaded.size
FileUtils.cp uploaded.path, path
File.chmod 0640, path
@ -1236,11 +1454,11 @@ class Site < Sequel::Model
purge_cache path
if pathname.extname.match HTML_REGEX
ScreenshotWorker.perform_async values[:username], relative_path
ScreenshotWorker.perform_in 1.minute, values[:username], relative_path
elsif pathname.extname.match IMAGE_REGEX
ThumbnailWorker.perform_async values[:username], relative_path
end
true
end
end

View file

@ -1,5 +1,52 @@
require 'sanitize'
class SiteFile < Sequel::Model
CLASSIFIER_LIMIT = 1_000_000.freeze
CLASSIFIER_WORD_LIMIT = 25.freeze
unrestrict_primary_key
plugin :update_primary_key
many_to_one :site
def before_destroy
if is_directory
site.site_files_dataset.where(path: /^#{Regexp.quote path}\//, is_directory: true).all.each do |site_file|
begin
site_file.destroy
rescue Sequel::NoExistingObject
end
end
site.site_files_dataset.where(path: /^#{Regexp.quote path}\//, is_directory: false).all.each do |site_file|
site_file.destroy
end
begin
FileUtils.remove_dir site.files_path(path)
rescue Errno::ENOENT
end
else
begin
FileUtils.rm site.files_path(path)
rescue Errno::ENOENT
end
ext = File.extname(path).gsub(/^./, '')
site.screenshots_delete(path) if ext.match Site::HTML_REGEX
site.thumbnails_delete(path) if ext.match Site::IMAGE_REGEX
end
super
end
def after_destroy
super
unless is_directory
DB['update sites set space_used=space_used-? where id=?', size, site_id].first
end
site.delete_cache site.files_path(path)
SiteChangeFile.filter(site_id: site_id, filename: path).delete
end
end

View file

@ -15,8 +15,11 @@ class Stat < Sequel::Model
end
def parse_logfiles(path)
total_site_stats = {}
Dir["#{path}/*.log"].each do |log_path|
site_logs = {}
logfile = File.open log_path, 'r'
while hit = logfile.gets
@ -26,9 +29,13 @@ class Stat < Sequel::Model
time, username, size, path, ip, referrer = hit_array
log_time = Time.parse time
next if !referrer.nil? && referrer.match(/bot/i)
site_logs[username] = {
site_logs[log_time] = {} unless site_logs[log_time]
site_logs[log_time][username] = {
hits: 0,
views: 0,
bandwidth: 0,
@ -36,78 +43,111 @@ class Stat < Sequel::Model
ips: [],
referrers: {},
paths: {}
} unless site_logs[username]
} unless site_logs[log_time][username]
site_logs[username][:hits] += 1
site_logs[username][:bandwidth] += size.to_i
total_site_stats[log_time] = {
hits: 0,
views: 0,
bandwidth: 0
} unless total_site_stats[log_time]
unless site_logs[username][:view_ips].include?(ip)
site_logs[username][:views] += 1
site_logs[username][:view_ips] << ip
site_logs[log_time][username][:hits] += 1
site_logs[log_time][username][:bandwidth] += size.to_i
total_site_stats[log_time][:hits] += 1
total_site_stats[log_time][:bandwidth] += size.to_i
unless site_logs[log_time][username][:view_ips].include?(ip)
site_logs[log_time][username][:views] += 1
total_site_stats[log_time][:views] += 1
site_logs[log_time][username][:view_ips] << ip
if referrer != '-' && !referrer.nil?
site_logs[username][:referrers][referrer] ||= 0
site_logs[username][:referrers][referrer] += 1
site_logs[log_time][username][:referrers][referrer] ||= 0
site_logs[log_time][username][:referrers][referrer] += 1
end
end
site_logs[username][:paths][path] ||= 0
site_logs[username][:paths][path] += 1
site_logs[log_time][username][:paths][path] ||= 0
site_logs[log_time][username][:paths][path] += 1
end
logfile.close
current_time = Time.now.utc
current_day_string = current_time.to_date.to_s
Site.select(:id, :username).where(username: site_logs.keys).all.each do |site|
site_logs[site.username][:id] = site.id
end
DB.transaction do
site_logs.each do |username, site_log|
DB['update sites set hits=hits+?, views=views+? where username=?',
site_log[:hits],
site_log[:views],
username
site_logs.each do |log_time, usernames|
Site.select(:id, :username).where(username: usernames.keys).all.each do |site|
site_logs[log_time][site.username][:id] = site.id
end
usernames.each do |username, site_log|
DB['update sites set hits=hits+?, views=views+? where username=?',
site_log[:hits],
site_log[:views],
username
].first
opts = {site_id: site_log[:id], created_at: current_day_string}
opts = {site_id: site_log[:id], created_at: log_time.to_date.to_s}
stat = Stat.select(:id).where(opts).first
DB[:stats].lock('EXCLUSIVE') { stat = Stat.create opts } if stat.nil?
stat = nil
DB[
'update stats set hits=hits+?, views=views+?, bandwidth=bandwidth+? where id=?',
site_log[:hits],
site_log[:views],
site_log[:bandwidth],
stat.id
].first
DB[:stats].lock('EXCLUSIVE') {
stat = Stat.select(:id).where(opts).first
stat = Stat.create opts if stat.nil?
}
DB[
'update stats set hits=hits+?, views=views+?, bandwidth=bandwidth+? where id=?',
site_log[:hits],
site_log[:views],
site_log[:bandwidth],
stat.id
].first
=begin
site_log[:referrers].each do |referrer, views|
stat_referrer = StatReferrer.create_or_get site_log[:id], referrer
DB['update stat_referrers set views=views+? where site_id=?', views, site_log[:id]].first
end
site_log[:referrers].each do |referrer, views|
stat_referrer = StatReferrer.create_or_get site_log[:id], referrer
DB['update stat_referrers set views=views+? where site_id=?', views, site_log[:id]].first
end
site_log[:view_ips].each do |ip|
site_location = StatLocation.create_or_get site_log[:id], ip
next if site_location.nil?
DB['update stat_locations set views=views+1 where id=?', site_location.id].first
end
site_log[:view_ips].each do |ip|
site_location = StatLocation.create_or_get site_log[:id], ip
next if site_location.nil?
DB['update stat_locations set views=views+1 where id=?', site_location.id].first
end
site_log[:paths].each do |path, views|
site_path = StatPath.create_or_get site_log[:id], path
next if site_path.nil?
DB['update stat_paths set views=views+? where id=?', views, site_path.id].first
end
site_log[:paths].each do |path, views|
site_path = StatPath.create_or_get site_log[:id], path
next if site_path.nil?
DB['update stat_paths set views=views+? where id=?', views, site_path.id].first
end
=end
end
end
end
FileUtils.rm log_path
end
total_site_stats.each do |time, stats|
opts = {created_at: time.to_date.to_s}
DB[:stats].lock('EXCLUSIVE') {
stat = DailySiteStat.select(:id).where(opts).first
stat = DailySiteStat.create opts if stat.nil?
}
DB[
'update daily_site_stats set hits=hits+?, views=views+?, bandwidth=bandwidth+? where created_at=?',
stats[:hits],
stats[:views],
stats[:bandwidth],
time.to_date
].first
end
end
end
end

View file

@ -23,6 +23,14 @@ class Tag < Sequel::Model
end
def self.popular_names(limit=10)
DB["select tags.name,count(*) as c from sites_tags inner join tags on tags.id=sites_tags.tag_id where tags.name != '' and tags.is_nsfw='f' group by tags.name having count(*) > 1 order by c desc LIMIT ?", limit].all
cache = $redis_cache['tag_popular_names']
if cache.nil?
res = DB["select tags.name,count(*) as c from sites_tags inner join tags on tags.id=sites_tags.tag_id where tags.name != '' and tags.is_nsfw='f' group by tags.name having count(*) > 1 order by c desc LIMIT ?", limit].all
$redis_cache.set :tag_popular_names, res.to_msgpack
$redis_cache.expire :tag_popular_names, 86400 # 24 hours
else
res = MessagePack.unpack cache, symbolize_keys: true
end
res
end
end

BIN
public/cat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,102 @@
/*
Sunburst-like style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #000;
color: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #aeaeae;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #e28964;
}
.hljs-string {
color: #65b042;
}
.hljs-subst {
color: #daefa3;
}
.hljs-regexp,
.hljs-link {
color: #e9c062;
}
.hljs-title,
.hljs-section,
.hljs-tag,
.hljs-name {
color: #89bdff;
}
.hljs-class .hljs-title,
.hljs-doctag {
text-decoration: underline;
}
.hljs-symbol,
.hljs-bullet,
.hljs-number {
color: #3387cc;
}
.hljs-params,
.hljs-variable,
.hljs-template-variable {
color: #3e87e3;
}
.hljs-attribute {
color: #cda869;
}
.hljs-meta {
color: #8996a8;
}
.hljs-formula {
background-color: #0e2231;
color: #f8f8f8;
font-style: italic;
}
.hljs-addition {
background-color: #253b22;
color: #f8f8f8;
}
.hljs-deletion {
background-color: #420e09;
color: #f8f8f8;
}
.hljs-selector-class {
color: #9b703f;
}
.hljs-selector-id {
color: #8b98ab;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View file

@ -0,0 +1,75 @@
/* Tomorrow Night Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #969896;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #cc6666;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #de935f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #f0c674;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #b5bd68;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #81a2be;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #b294bb;
}
.hljs {
display: block;
overflow-x: auto;
background: #1d1f21;
color: #c5c8c6;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

0
public/fonts/dlfonts.sh Executable file → Normal file
View file

0
public/fonts/ocra-webfont.eot Executable file → Normal file
View file

0
public/fonts/ocra-webfont.svg Executable file → Normal file
View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

0
public/fonts/ocra-webfont.ttf Executable file → Normal file
View file

0
public/fonts/ocra-webfont.woff Executable file → Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
public/img/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
public/img/usedcarad.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
public/img/welcomingcat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,961 +0,0 @@
/**
* Title: KeyboardJS
* Version: v0.4.1
* Description: KeyboardJS is a flexible and easy to use keyboard binding
* library.
* Author: Robert Hurst.
*
* Copyright 2011, Robert William Hurst
* Licenced under the BSD License.
* See https://raw.github.com/RobertWHurst/KeyboardJS/master/license.txt
*/
(function(context, factory) {
//INDEXOF POLLYFILL
[].indexOf||(Array.prototype.indexOf=function(a,b,c){for(c=this.length,b=(c+~~b)%c;b<c&&(!(b in this)||this[b]!==a);b++);return b^c?b:-1;});
//AMD
if(typeof define === 'function' && define.amd) { define(constructAMD); }
//CommonJS
else if(typeof module !== 'undefined') {constructCommonJS()}
//GLOBAL
else { constructGlobal(); }
/**
* Construct AMD version of the library
*/
function constructAMD() {
//create a library instance
return init(context);
//spawns a library instance
function init(context) {
var library;
library = factory(context, 'amd');
library.fork = init;
return library;
}
}
/**
* Construct CommonJS version of the library
*/
function constructCommonJS() {
//create a library instance
module.exports = init(context);
return;
//spawns a library instance
function init(context) {
var library;
library = factory(context, 'CommonJS');
library.fork = init;
return library;
}
}
/**
* Construct a Global version of the library
*/
function constructGlobal() {
var library;
//create a library instance
library = init(context);
//spawns a library instance
function init(context) {
var library, namespaces = [], previousValues = {};
library = factory(context, 'global');
library.fork = init;
library.noConflict = noConflict;
library.noConflict('KeyboardJS', 'k');
return library;
//sets library namespaces
function noConflict( ) {
var args, nI, newNamespaces;
newNamespaces = Array.prototype.slice.apply(arguments);
for(nI = 0; nI < namespaces.length; nI += 1) {
if(typeof previousValues[namespaces[nI]] === 'undefined') {
delete context[namespaces[nI]];
} else {
context[namespaces[nI]] = previousValues[namespaces[nI]];
}
}
previousValues = {};
for(nI = 0; nI < newNamespaces.length; nI += 1) {
if(typeof newNamespaces[nI] !== 'string') {
throw new Error('Cannot replace namespaces. All new namespaces must be strings.');
}
previousValues[newNamespaces[nI]] = context[newNamespaces[nI]];
context[newNamespaces[nI]] = library;
}
namespaces = newNamespaces;
return namespaces;
}
}
}
})(this, function(targetWindow, env) {
var KeyboardJS = {}, locales = {}, locale, map, macros, activeKeys = [], bindings = [], activeBindings = [],
activeMacros = [], aI, usLocale;
targetWindow = targetWindow || window;
///////////////////////
// DEFAULT US LOCALE //
///////////////////////
//define US locale
//If you create a new locale please submit it as a pull request or post
// it in the issue tracker at
// http://github.com/RobertWhurst/KeyboardJS/issues/
usLocale = {
"map": {
//general
"3": ["cancel"],
"8": ["backspace"],
"9": ["tab"],
"12": ["clear"],
"13": ["enter"],
"16": ["shift"],
"17": ["ctrl"],
"18": ["alt", "menu"],
"19": ["pause", "break"],
"20": ["capslock"],
"27": ["escape", "esc"],
"32": ["space", "spacebar"],
"33": ["pageup"],
"34": ["pagedown"],
"35": ["end"],
"36": ["home"],
"37": ["left"],
"38": ["up"],
"39": ["right"],
"40": ["down"],
"41": ["select"],
"42": ["printscreen"],
"43": ["execute"],
"44": ["snapshot"],
"45": ["insert", "ins"],
"46": ["delete", "del"],
"47": ["help"],
"91": ["command", "windows", "win", "super", "leftcommand", "leftwindows", "leftwin", "leftsuper"],
"92": ["command", "windows", "win", "super", "rightcommand", "rightwindows", "rightwin", "rightsuper"],
"145": ["scrolllock", "scroll"],
"186": ["semicolon", ";"],
"187": ["equal", "equalsign", "="],
"188": ["comma", ","],
"189": ["dash", "-"],
"190": ["period", "."],
"191": ["slash", "forwardslash", "/"],
"192": ["graveaccent", "`"],
"219": ["openbracket", "["],
"220": ["backslash", "\\"],
"221": ["closebracket", "]"],
"222": ["apostrophe", "'"],
//0-9
"48": ["zero", "0"],
"49": ["one", "1"],
"50": ["two", "2"],
"51": ["three", "3"],
"52": ["four", "4"],
"53": ["five", "5"],
"54": ["six", "6"],
"55": ["seven", "7"],
"56": ["eight", "8"],
"57": ["nine", "9"],
//numpad
"96": ["numzero", "num0"],
"97": ["numone", "num1"],
"98": ["numtwo", "num2"],
"99": ["numthree", "num3"],
"100": ["numfour", "num4"],
"101": ["numfive", "num5"],
"102": ["numsix", "num6"],
"103": ["numseven", "num7"],
"104": ["numeight", "num8"],
"105": ["numnine", "num9"],
"106": ["nummultiply", "num*"],
"107": ["numadd", "num+"],
"108": ["numenter"],
"109": ["numsubtract", "num-"],
"110": ["numdecimal", "num."],
"111": ["numdivide", "num/"],
"144": ["numlock", "num"],
//function keys
"112": ["f1"],
"113": ["f2"],
"114": ["f3"],
"115": ["f4"],
"116": ["f5"],
"117": ["f6"],
"118": ["f7"],
"119": ["f8"],
"120": ["f9"],
"121": ["f10"],
"122": ["f11"],
"123": ["f12"]
},
"macros": [
//secondary key symbols
['shift + `', ["tilde", "~"]],
['shift + 1', ["exclamation", "exclamationpoint", "!"]],
['shift + 2', ["at", "@"]],
['shift + 3', ["number", "#"]],
['shift + 4', ["dollar", "dollars", "dollarsign", "$"]],
['shift + 5', ["percent", "%"]],
['shift + 6', ["caret", "^"]],
['shift + 7', ["ampersand", "and", "&"]],
['shift + 8', ["asterisk", "*"]],
['shift + 9', ["openparen", "("]],
['shift + 0', ["closeparen", ")"]],
['shift + -', ["underscore", "_"]],
['shift + =', ["plus", "+"]],
['shift + (', ["opencurlybrace", "opencurlybracket", "{"]],
['shift + )', ["closecurlybrace", "closecurlybracket", "}"]],
['shift + \\', ["verticalbar", "|"]],
['shift + ;', ["colon", ":"]],
['shift + \'', ["quotationmark", "\""]],
['shift + !,', ["openanglebracket", "<"]],
['shift + .', ["closeanglebracket", ">"]],
['shift + /', ["questionmark", "?"]]
]
};
//a-z and A-Z
for (aI = 65; aI <= 90; aI += 1) {
usLocale.map[aI] = String.fromCharCode(aI + 32);
usLocale.macros.push(['shift + ' + String.fromCharCode(aI + 32) + ', capslock + ' + String.fromCharCode(aI + 32), [String.fromCharCode(aI)]]);
}
registerLocale('us', usLocale);
getSetLocale('us');
//////////
// INIT //
//////////
//enable the library
enable();
/////////
// API //
/////////
//assemble the library and return it
KeyboardJS.enable = enable;
KeyboardJS.disable = disable;
KeyboardJS.activeKeys = getActiveKeys;
KeyboardJS.releaseKey = removeActiveKey;
KeyboardJS.pressKey = addActiveKey;
KeyboardJS.on = createBinding;
KeyboardJS.clear = removeBindingByKeyCombo;
KeyboardJS.clear.key = removeBindingByKeyName;
KeyboardJS.locale = getSetLocale;
KeyboardJS.locale.register = registerLocale;
KeyboardJS.macro = createMacro;
KeyboardJS.macro.remove = removeMacro;
KeyboardJS.key = {};
KeyboardJS.key.name = getKeyName;
KeyboardJS.key.code = getKeyCode;
KeyboardJS.combo = {};
KeyboardJS.combo.active = isSatisfiedCombo;
KeyboardJS.combo.parse = parseKeyCombo;
KeyboardJS.combo.stringify = stringifyKeyCombo;
return KeyboardJS;
//////////////////////
// INSTANCE METHODS //
//////////////////////
/**
* Enables KeyboardJS
*/
function enable() {
if(targetWindow.addEventListener) {
targetWindow.document.addEventListener('keydown', keydown, false);
targetWindow.document.addEventListener('keyup', keyup, false);
targetWindow.addEventListener('blur', reset, false);
targetWindow.addEventListener('webkitfullscreenchange', reset, false);
targetWindow.addEventListener('mozfullscreenchange', reset, false);
} else if(targetWindow.attachEvent) {
targetWindow.document.attachEvent('onkeydown', keydown);
targetWindow.document.attachEvent('onkeyup', keyup);
targetWindow.attachEvent('onblur', reset);
}
}
/**
* Exits all active bindings and disables KeyboardJS
*/
function disable() {
reset();
if(targetWindow.removeEventListener) {
targetWindow.document.removeEventListener('keydown', keydown, false);
targetWindow.document.removeEventListener('keyup', keyup, false);
targetWindow.removeEventListener('blur', reset, false);
targetWindow.removeEventListener('webkitfullscreenchange', reset, false);
targetWindow.removeEventListener('mozfullscreenchange', reset, false);
} else if(targetWindow.detachEvent) {
targetWindow.document.detachEvent('onkeydown', keydown);
targetWindow.document.detachEvent('onkeyup', keyup);
targetWindow.detachEvent('onblur', reset);
}
}
////////////////////
// EVENT HANDLERS //
////////////////////
/**
* Exits all active bindings. Optionally passes an event to all binding
* handlers.
* @param {KeyboardEvent} event [Optional]
*/
function reset(event) {
activeKeys = [];
pruneMacros();
pruneBindings(event);
}
/**
* Key down event handler.
* @param {KeyboardEvent} event
*/
function keydown(event) {
var keyNames, keyName, kI;
keyNames = getKeyName(event.keyCode);
if(keyNames.length < 1) { return; }
event.isRepeat = false;
for(kI = 0; kI < keyNames.length; kI += 1) {
keyName = keyNames[kI];
if (getActiveKeys().indexOf(keyName) != -1)
event.isRepeat = true;
addActiveKey(keyName);
}
executeMacros();
executeBindings(event);
}
/**
* Key up event handler.
* @param {KeyboardEvent} event
*/
function keyup(event) {
var keyNames, kI;
keyNames = getKeyName(event.keyCode);
if(keyNames.length < 1) { return; }
for(kI = 0; kI < keyNames.length; kI += 1) {
removeActiveKey(keyNames[kI]);
}
pruneMacros();
pruneBindings(event);
}
/**
* Accepts a key code and returns the key names defined by the current
* locale.
* @param {Number} keyCode
* @return {Array} keyNames An array of key names defined for the key
* code as defined by the current locale.
*/
function getKeyName(keyCode) {
return map[keyCode] || [];
}
/**
* Accepts a key name and returns the key code defined by the current
* locale.
* @param {Number} keyName
* @return {Number|false}
*/
function getKeyCode(keyName) {
var keyCode;
for(keyCode in map) {
if(!map.hasOwnProperty(keyCode)) { continue; }
if(map[keyCode].indexOf(keyName) > -1) { return keyCode; }
}
return false;
}
////////////
// MACROS //
////////////
/**
* Accepts a key combo and an array of key names to inject once the key
* combo is satisfied.
* @param {String} combo
* @param {Array} injectedKeys
*/
function createMacro(combo, injectedKeys) {
if(typeof combo !== 'string' && (typeof combo !== 'object' || typeof combo.push !== 'function')) {
throw new Error("Cannot create macro. The combo must be a string or array.");
}
if(typeof injectedKeys !== 'object' || typeof injectedKeys.push !== 'function') {
throw new Error("Cannot create macro. The injectedKeys must be an array.");
}
macros.push([combo, injectedKeys]);
}
/**
* Accepts a key combo and clears any and all macros bound to that key
* combo.
* @param {String} combo
*/
function removeMacro(combo) {
var macro;
if(typeof combo !== 'string' && (typeof combo !== 'object' || typeof combo.push !== 'function')) { throw new Error("Cannot remove macro. The combo must be a string or array."); }
for(mI = 0; mI < macros.length; mI += 1) {
macro = macros[mI];
if(compareCombos(combo, macro[0])) {
removeActiveKey(macro[1]);
macros.splice(mI, 1);
break;
}
}
}
/**
* Executes macros against the active keys. Each macro's key combo is
* checked and if found to be satisfied, the macro's key names are injected
* into active keys.
*/
function executeMacros() {
var mI, combo, kI;
for(mI = 0; mI < macros.length; mI += 1) {
combo = parseKeyCombo(macros[mI][0]);
if(activeMacros.indexOf(macros[mI]) === -1 && isSatisfiedCombo(combo)) {
activeMacros.push(macros[mI]);
for(kI = 0; kI < macros[mI][1].length; kI += 1) {
addActiveKey(macros[mI][1][kI]);
}
}
}
}
/**
* Prunes active macros. Checks each active macro's key combo and if found
* to no longer to be satisfied, each of the macro's key names are removed
* from active keys.
*/
function pruneMacros() {
var mI, combo, kI;
for(mI = 0; mI < activeMacros.length; mI += 1) {
combo = parseKeyCombo(activeMacros[mI][0]);
if(isSatisfiedCombo(combo) === false) {
for(kI = 0; kI < activeMacros[mI][1].length; kI += 1) {
removeActiveKey(activeMacros[mI][1][kI]);
}
activeMacros.splice(mI, 1);
mI -= 1;
}
}
}
//////////////
// BINDINGS //
//////////////
/**
* Creates a binding object, and, if provided, binds a key down hander and
* a key up handler. Returns a binding object that emits keyup and
* keydown events.
* @param {String} keyCombo
* @param {Function} keyDownCallback [Optional]
* @param {Function} keyUpCallback [Optional]
* @return {Object} binding
*/
function createBinding(keyCombo, keyDownCallback, keyUpCallback) {
var api = {}, binding, subBindings = [], bindingApi = {}, kI,
subCombo;
//break the combo down into a combo array
if(typeof keyCombo === 'string') {
keyCombo = parseKeyCombo(keyCombo);
}
//bind each sub combo contained within the combo string
for(kI = 0; kI < keyCombo.length; kI += 1) {
binding = {};
//stringify the combo again
subCombo = stringifyKeyCombo([keyCombo[kI]]);
//validate the sub combo
if(typeof subCombo !== 'string') { throw new Error('Failed to bind key combo. The key combo must be string.'); }
//create the binding
binding.keyCombo = subCombo;
binding.keyDownCallback = [];
binding.keyUpCallback = [];
//inject the key down and key up callbacks if given
if(keyDownCallback) { binding.keyDownCallback.push(keyDownCallback); }
if(keyUpCallback) { binding.keyUpCallback.push(keyUpCallback); }
//stash the new binding
bindings.push(binding);
subBindings.push(binding);
}
//build the binding api
api.clear = clear;
api.on = on;
return api;
/**
* Clears the binding
*/
function clear() {
var bI;
for(bI = 0; bI < subBindings.length; bI += 1) {
bindings.splice(bindings.indexOf(subBindings[bI]), 1);
}
}
/**
* Accepts an event name. and any number of callbacks. When the event is
* emitted, all callbacks are executed. Available events are key up and
* key down.
* @param {String} eventName
* @return {Object} subBinding
*/
function on(eventName ) {
var api = {}, callbacks, cI, bI;
//validate event name
if(typeof eventName !== 'string') { throw new Error('Cannot bind callback. The event name must be a string.'); }
if(eventName !== 'keyup' && eventName !== 'keydown') { throw new Error('Cannot bind callback. The event name must be a "keyup" or "keydown".'); }
//gather the callbacks
callbacks = Array.prototype.slice.apply(arguments, [1]);
//stash each the new binding
for(cI = 0; cI < callbacks.length; cI += 1) {
if(typeof callbacks[cI] === 'function') {
if(eventName === 'keyup') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyUpCallback.push(callbacks[cI]);
}
} else if(eventName === 'keydown') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyDownCallback.push(callbacks[cI]);
}
}
}
}
//construct and return the sub binding api
api.clear = clear;
return api;
/**
* Clears the binding
*/
function clear() {
var cI, bI;
for(cI = 0; cI < callbacks.length; cI += 1) {
if(typeof callbacks[cI] === 'function') {
if(eventName === 'keyup') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyUpCallback.splice(subBindings[bI].keyUpCallback.indexOf(callbacks[cI]), 1);
}
} else {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyDownCallback.splice(subBindings[bI].keyDownCallback.indexOf(callbacks[cI]), 1);
}
}
}
}
}
}
}
/**
* Clears all binding attached to a given key combo. Key name order does not
* matter as long as the key combos equate.
* @param {String} keyCombo
*/
function removeBindingByKeyCombo(keyCombo) {
var bI, binding, keyName;
for(bI = 0; bI < bindings.length; bI += 1) {
binding = bindings[bI];
if(compareCombos(keyCombo, binding.keyCombo)) {
bindings.splice(bI, 1); bI -= 1;
}
}
}
/**
* Clears all binding attached to key combos containing a given key name.
* @param {String} keyName
*/
function removeBindingByKeyName(keyName) {
var bI, kI, binding;
if(keyName) {
for(bI = 0; bI < bindings.length; bI += 1) {
binding = bindings[bI];
for(kI = 0; kI < binding.keyCombo.length; kI += 1) {
if(binding.keyCombo[kI].indexOf(keyName) > -1) {
bindings.splice(bI, 1); bI -= 1;
break;
}
}
}
} else {
bindings = [];
}
}
/**
* Executes bindings that are active. Only allows the keys to be used once
* as to prevent binding overlap.
* @param {KeyboardEvent} event The keyboard event.
*/
function executeBindings(event) {
var bI, sBI, binding, bindingKeys, remainingKeys, cI, killEventBubble, kI, bindingKeysSatisfied,
index, sortedBindings = [], bindingWeight;
remainingKeys = [].concat(activeKeys);
for(bI = 0; bI < bindings.length; bI += 1) {
bindingWeight = extractComboKeys(bindings[bI].keyCombo).length;
if(!sortedBindings[bindingWeight]) { sortedBindings[bindingWeight] = []; }
sortedBindings[bindingWeight].push(bindings[bI]);
}
for(sBI = sortedBindings.length - 1; sBI >= 0; sBI -= 1) {
if(!sortedBindings[sBI]) { continue; }
for(bI = 0; bI < sortedBindings[sBI].length; bI += 1) {
binding = sortedBindings[sBI][bI];
bindingKeys = extractComboKeys(binding.keyCombo);
bindingKeysSatisfied = true;
for(kI = 0; kI < bindingKeys.length; kI += 1) {
if(remainingKeys.indexOf(bindingKeys[kI]) === -1) {
bindingKeysSatisfied = false;
break;
}
}
if(bindingKeysSatisfied && isSatisfiedCombo(binding.keyCombo)) {
activeBindings.push(binding);
for(kI = 0; kI < bindingKeys.length; kI += 1) {
index = remainingKeys.indexOf(bindingKeys[kI]);
if(index > -1) {
remainingKeys.splice(index, 1);
kI -= 1;
}
}
for(cI = 0; cI < binding.keyDownCallback.length; cI += 1) {
if (binding.keyDownCallback[cI](event, getActiveKeys(), binding.keyCombo) === false) {
killEventBubble = true;
}
}
if(killEventBubble === true) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
/**
* Removes bindings that are no longer satisfied by the active keys. Also
* fires the key up callbacks.
* @param {KeyboardEvent} event
*/
function pruneBindings(event) {
var bI, cI, binding, killEventBubble;
for(bI = 0; bI < activeBindings.length; bI += 1) {
binding = activeBindings[bI];
if(isSatisfiedCombo(binding.keyCombo) === false) {
for(cI = 0; cI < binding.keyUpCallback.length; cI += 1) {
if (binding.keyUpCallback[cI](event, getActiveKeys(), binding.keyCombo) === false) {
killEventBubble = true;
}
}
if(killEventBubble === true) {
event.preventDefault();
event.stopPropagation();
}
activeBindings.splice(bI, 1);
bI -= 1;
}
}
}
///////////////////
// COMBO STRINGS //
///////////////////
/**
* Compares two key combos returning true when they are functionally
* equivalent.
* @param {String} keyComboArrayA keyCombo A key combo string or array.
* @param {String} keyComboArrayB keyCombo A key combo string or array.
* @return {Boolean}
*/
function compareCombos(keyComboArrayA, keyComboArrayB) {
var cI, sI, kI;
keyComboArrayA = parseKeyCombo(keyComboArrayA);
keyComboArrayB = parseKeyCombo(keyComboArrayB);
if(keyComboArrayA.length !== keyComboArrayB.length) { return false; }
for(cI = 0; cI < keyComboArrayA.length; cI += 1) {
if(keyComboArrayA[cI].length !== keyComboArrayB[cI].length) { return false; }
for(sI = 0; sI < keyComboArrayA[cI].length; sI += 1) {
if(keyComboArrayA[cI][sI].length !== keyComboArrayB[cI][sI].length) { return false; }
for(kI = 0; kI < keyComboArrayA[cI][sI].length; kI += 1) {
if(keyComboArrayB[cI][sI].indexOf(keyComboArrayA[cI][sI][kI]) === -1) { return false; }
}
}
}
return true;
}
/**
* Checks to see if a key combo string or key array is satisfied by the
* currently active keys. It does not take into account spent keys.
* @param {String} keyCombo A key combo string or array.
* @return {Boolean}
*/
function isSatisfiedCombo(keyCombo) {
var cI, sI, stage, kI, stageOffset = 0, index, comboMatches;
keyCombo = parseKeyCombo(keyCombo);
for(cI = 0; cI < keyCombo.length; cI += 1) {
comboMatches = true;
stageOffset = 0;
for(sI = 0; sI < keyCombo[cI].length; sI += 1) {
stage = [].concat(keyCombo[cI][sI]);
for(kI = stageOffset; kI < activeKeys.length; kI += 1) {
index = stage.indexOf(activeKeys[kI]);
if(index > -1) {
stage.splice(index, 1);
stageOffset = kI;
}
}
if(stage.length !== 0) { comboMatches = false; break; }
}
if(comboMatches) { return true; }
}
return false;
}
/**
* Accepts a key combo array or string and returns a flat array containing all keys referenced by
* the key combo.
* @param {String} keyCombo A key combo string or array.
* @return {Array}
*/
function extractComboKeys(keyCombo) {
var cI, sI, kI, keys = [];
keyCombo = parseKeyCombo(keyCombo);
for(cI = 0; cI < keyCombo.length; cI += 1) {
for(sI = 0; sI < keyCombo[cI].length; sI += 1) {
keys = keys.concat(keyCombo[cI][sI]);
}
}
return keys;
}
/**
* Parses a key combo string into a 3 dimensional array.
* - Level 1 - sub combos.
* - Level 2 - combo stages. A stage is a set of key name pairs that must
* be satisfied in the order they are defined.
* - Level 3 - each key name to the stage.
* @param {String|Array} keyCombo A key combo string.
* @return {Array}
*/
function parseKeyCombo(keyCombo) {
var s = keyCombo, i = 0, op = 0, ws = false, nc = false, combos = [], combo = [], stage = [], key = '';
if(typeof keyCombo === 'object' && typeof keyCombo.push === 'function') { return keyCombo; }
if(typeof keyCombo !== 'string') { throw new Error('Cannot parse "keyCombo" because its type is "' + (typeof keyCombo) + '". It must be a "string".'); }
//remove leading whitespace
while(s.charAt(i) === ' ') { i += 1; }
while(true) {
if(s.charAt(i) === ' ') {
//white space & next combo op
while(s.charAt(i) === ' ') { i += 1; }
ws = true;
} else if(s.charAt(i) === ',') {
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected , at character index ' + i + '.'); }
nc = true;
i += 1;
} else if(s.charAt(i) === '+') {
//next key
if(key.length) { stage.push(key); key = ''; }
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected + at character index ' + i + '.'); }
op = true;
i += 1;
} else if(s.charAt(i) === '>') {
//next stage op
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected > at character index ' + i + '.'); }
op = true;
i += 1;
} else if(i < s.length - 1 && s.charAt(i) === '!' && (s.charAt(i + 1) === '>' || s.charAt(i + 1) === ',' || s.charAt(i + 1) === '+')) {
key += s.charAt(i + 1);
op = false;
ws = false;
nc = false;
i += 2;
} else if(i < s.length && s.charAt(i) !== '+' && s.charAt(i) !== '>' && s.charAt(i) !== ',' && s.charAt(i) !== ' ') {
//end combo
if(op === false && ws === true || nc === true) {
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(combo.length) { combos.push(combo); combo = []; }
}
op = false;
ws = false;
nc = false;
//key
while(i < s.length && s.charAt(i) !== '+' && s.charAt(i) !== '>' && s.charAt(i) !== ',' && s.charAt(i) !== ' ') {
key += s.charAt(i);
i += 1;
}
} else {
//unknown char
i += 1;
continue;
}
//end of combos string
if(i >= s.length) {
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(combo.length) { combos.push(combo); combo = []; }
break;
}
}
return combos;
}
/**
* Stringifys a key combo.
* @param {Array|String} keyComboArray A key combo array. If a key
* combo string is given it will be returned.
* @return {String}
*/
function stringifyKeyCombo(keyComboArray) {
var cI, ccI, output = [];
if(typeof keyComboArray === 'string') { return keyComboArray; }
if(typeof keyComboArray !== 'object' || typeof keyComboArray.push !== 'function') { throw new Error('Cannot stringify key combo.'); }
for(cI = 0; cI < keyComboArray.length; cI += 1) {
output[cI] = [];
for(ccI = 0; ccI < keyComboArray[cI].length; ccI += 1) {
output[cI][ccI] = keyComboArray[cI][ccI].join(' + ');
}
output[cI] = output[cI].join(' > ');
}
return output.join(' ');
}
/////////////////
// ACTIVE KEYS //
/////////////////
/**
* Returns the a copy of the active keys array.
* @return {Array}
*/
function getActiveKeys() {
return [].concat(activeKeys);
}
/**
* Adds a key to the active keys array, but only if it has not already been
* added.
* @param {String} keyName The key name string.
*/
function addActiveKey(keyName) {
if(keyName.match(/\s/)) { throw new Error('Cannot add key name ' + keyName + ' to active keys because it contains whitespace.'); }
if(activeKeys.indexOf(keyName) > -1) { return; }
activeKeys.push(keyName);
}
/**
* Removes a key from the active keys array.
* @param {String} keyNames The key name string.
*/
function removeActiveKey(keyName) {
var keyCode = getKeyCode(keyName);
if(keyCode === '91' || keyCode === '92') { activeKeys = []; } //remove all key on release of super.
else { activeKeys.splice(activeKeys.indexOf(keyName), 1); }
}
/////////////
// LOCALES //
/////////////
/**
* Registers a new locale. This is useful if you would like to add support for a new keyboard layout. It could also be useful for
* alternative key names. For example if you program games you could create a locale for your key mappings. Instead of key 65 mapped
* to 'a' you could map it to 'jump'.
* @param {String} localeName The name of the new locale.
* @param {Object} localeMap The locale map.
*/
function registerLocale(localeName, localeMap) {
//validate arguments
if(typeof localeName !== 'string') { throw new Error('Cannot register new locale. The locale name must be a string.'); }
if(typeof localeMap !== 'object') { throw new Error('Cannot register ' + localeName + ' locale. The locale map must be an object.'); }
if(typeof localeMap.map !== 'object') { throw new Error('Cannot register ' + localeName + ' locale. The locale map is invalid.'); }
//stash the locale
if(!localeMap.macros) { localeMap.macros = []; }
locales[localeName] = localeMap;
}
/**
* Swaps the current locale.
* @param {String} localeName The locale to activate.
* @return {Object}
*/
function getSetLocale(localeName) {
//if a new locale is given then set it
if(localeName) {
if(typeof localeName !== 'string') { throw new Error('Cannot set locale. The locale name must be a string.'); }
if(!locales[localeName]) { throw new Error('Cannot set locale to ' + localeName + ' because it does not exist. If you would like to submit a ' + localeName + ' locale map for KeyboardJS please submit it at https://github.com/RobertWHurst/KeyboardJS/issues.'); }
//set the current map and macros
map = locales[localeName].map;
macros = locales[localeName].macros;
//set the current locale
locale = localeName;
}
//return the current locale
return locale;
}
});

2
public/js/xregexp-min.js vendored Normal file

File diff suppressed because one or more lines are too long

28
puma_config.rb Normal file
View file

@ -0,0 +1,28 @@
def processor_count
case RbConfig::CONFIG['host_os']
when /darwin9/
`hwprefs cpu_count`.to_i
when /darwin/
((`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`).to_i
when /linux/
`cat /proc/cpuinfo | grep processor | wc -l`.to_i
when /freebsd/
`sysctl -n hw.ncpu`.to_i
when /mswin|mingw/
require 'win32ole'
wmi = WIN32OLE.connect("winmgmts://")
cpu = wmi.ExecQuery("select NumberOfCores from Win32_Processor") # TODO count hyper-threaded in this
cpu.to_enum.first.NumberOfCores
end
end
environment 'production'
daemonize
pidfile '/var/run/neocities/neocities.pid'
stdout_redirect '/var/log/neocities/neocities.log', '/var/log/neocities/neocities-errors.log', true
quiet
workers processor_count
worker_timeout 600
preload_app!
on_worker_boot { DB.disconnect }
bind 'unix:/var/run/neocities/neocities.sock?backlog=2048'

View file

@ -14,7 +14,7 @@
}
.row.header-Content.content {
padding-bottom:27px;
padding-bottom: 31px;
}
.header-Content.content{
@ -28,7 +28,7 @@
background:url(/img/neocity.png) 95% bottom no-repeat;
background-size: 734px;
min-height:214px;
@media (max-device-width:480px), screen and (max-width:800px){
@include vendor(background-size, cover);
min-height:2px;
@ -64,7 +64,7 @@
color: #B8D375;
font-size: .9em;
margin-bottom: 1.8em;
a {
color: #B8D375;
border-bottom: 1px solid rgba(184, 211, 117, 0.5);
@ -90,7 +90,7 @@
@include vendor(transform, scaleX(-1));
width: 100px;
margin-right: 25px;
&.float-Right {
margin-left: 10px;
}
@ -107,19 +107,19 @@
margin-bottom:20px;
padding:4px 20px 4px 14px;
}
li{
padding-left:$spacing*9;
padding-right:$spacing*3;
margin-bottom:20px;
margin-left: 8px;
}
h3 {
margin-bottom: 0px;
font-size: 1.7em;
}
p {
color:#B2BCC1;
line-height: 170%;
@ -139,14 +139,14 @@
.intro-Tools{position:relative}
.intro-Question{
position:relative;
.intro-Icon{
background-position:0 -40px;
}
}
}
.intro-Social {
position:relative;
.intro-Icon {
background-position: 0 -80px;
}
@ -166,7 +166,7 @@
position:absolute;
top: -40px;
margin-bottom: 15px;
@media (max-device-width:480px), screen and (max-width:800px){
height:auto;
margin:0;
@ -175,35 +175,35 @@
position:static;
width:100%;
}
h2{
margin-bottom:0;
text-shadow:0 1px 1px rgba(0,0,0,.5);
font-size: 1.8em;
}
hr{
border-bottom:1px solid #4a6677;
border-top:1px solid #1d282d;
margin: 4px 0 21px;
}
fieldset{
padding: 25px 33px;
background:url(/img/sign-up-bg.png) repeat-x center top;
background:url(/img/sign-up-bg.png) repeat-x center top;
border-radius: 4px 4px 0 0;
}
label{
color:#81b8c6;
}
label#domain-name {
display: inline;
vertical-align: 8px;
color: #C2CFD4;
}
.input-Area{
background:#29383f;
border:0 solid black;
@ -213,7 +213,7 @@
margin-right:$spacing;
padding: 10px 10px 7px 10px;
width:100%;
// &:focus{color:#eee}
}
.input-Area#create-Input {
@ -223,7 +223,7 @@
margin-left: 0;
position: static;
}
.tooltip {
.tooltip {
&.left .tooltip-arrow {
border-left-color: #971D31;
}
@ -249,41 +249,41 @@
.header-Nav{
background:#5e95a1;
border-bottom:1px solid #92B4BD;
@include vendor(transition, all 0.35s);
@media (max-device-width:480px), screen and (max-width:800px){
position:fixed;
top:-900px!important;
}
&.show-Nav{
top:0!important;
padding-bottom: 2px;
}
a, a:visited{
color:#fff;
padding:$spacing*2 $spacing*3;
color:#fff;
padding:$spacing*2 $spacing*3;
text-decoration:none;
@media (max-device-width:480px), screen and (max-width:800px){
display:block;
}
>.fa-heart {
vertical-align: .5em;
margin-left: .3em;
font-size: 9px;
position: relative;
position: relative;
color: rgba(255, 255, 255, 0.4);
display: inline-block;
>.fa-heart {
display:none;
}
}
&:hover{
background:#528995; text-decoration:underline;
>.fa-heart {
@ -300,7 +300,7 @@
}
&.selected, &:active{background:#528995; text-decoration:underline}
}
a.small-Nav{
background: #65a0ad;
display:none;
@ -311,28 +311,28 @@
height:36px;
z-index:9999;
padding: 5px 12px 0px 12px;
@media (max-device-width:480px), screen and (max-width:800px){
display:block;
}
}
.notification-value {
background: $c-Brand-1;
padding: 2px 5px;
@include border-radius(4px);
}
.dropdown {
height: 2.6em; //not sure why I need this :/
a {
width:100%;
}
.info {
float:right;
margin-left: 10px;
.fa-caret-down {
margin-left:10px;
}
@ -345,10 +345,10 @@
min-width: 13em;
@include border-radius(0px 0px 6px 6px);
@include box-shadow(0 2px 7px rgba(0,0,0,0.2));
li {
width:100%;
a {
float:left;
}
@ -367,21 +367,21 @@
}
.dropdown-submenu {
float: left;
&:hover > a, &:focus > a {
background:#3C6670;
}
.dropdown-menu {
width: 100%;
margin-left: -1px;
margin-top: -1px;
li {
overflow: hidden;
}
}
>a:after {
border-left-color: #fff;
}
@ -390,27 +390,27 @@
.constant-Nav{
float:left;
position:relative;
@media (max-device-width:480px), screen and (max-width:800px){
float:none;
li{float:none;}
}
}
.status-Nav{
float:right;
@media (max-device-width:480px), screen and (max-width:800px){
float:none;
}
li{
float:left;
@media (max-device-width:480px), screen and (max-width:800px){
float:none
}
}
}
}
// hp CSS
@ -429,12 +429,12 @@
left:-90px;
position:fixed;
@include vendor(transition, all 0.35s);
&.in-View{
left:0!important;
z-index:99;
@media (max-device-width:480px), screen and (max-width:800px){
left:-90px!important;
}
@ -456,11 +456,11 @@
@media (max-device-width:480px), screen and (max-width:800px){
margin-left:0;
}
&.in-View{
margin-left:0;
padding-left:70px;
@media (max-device-width:480px), screen and (max-width:800px){
padding-left:0
}
@ -504,7 +504,7 @@
padding-left:74px;
@media (max-device-width:480px), screen and (max-width:800px){
width:100%;
}
}
}
.interior .constant-Nav, .hp.education .constant-Nav{margin:0}

View file

@ -187,7 +187,7 @@
@media (max-device-width:480px), screen and (max-width:800px) {
width: 100%;
height: 300px;
height: 200px;
}
}
.interior .header-Outro .screenshot.dashboard {
@ -256,16 +256,16 @@
width: 100%;
position: relative;
margin-top: 7px;
.column, input[type='checkbox'] {
display: none;
}
.btn-group {
float: left;
margin-right: 15px;
margin-left: -3px;
>.btn+.btn {
margin-left: 0px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
@ -276,9 +276,6 @@
background: #77ABB8;
@include box-shadow(0 0 5px rgba(0, 0, 0, 0.2));
&:hover {
background: #83B3C0;
}
&:focus, &.active {
outline: 0;
background: #4F727B;
@ -323,11 +320,11 @@
}
.files .actions {
float: right;
@media (max-device-width:480px), screen and (max-width:800px) {
float: left;
margin-top: 7px;
.fa {
display: none;
}
@ -525,7 +522,7 @@
}
@mixin dashboard-list-view {
padding: 0;
.upload-Boundary {
padding: 0;
border: 0;
@ -535,20 +532,20 @@
padding: 10px 20px;
margin: 0;
width: 100%;
&:nth-child(even) {
background: #EFE8DC;
}
.title {
margin: 0;
margin-left: 7px;
margin-left: 9px;
margin-top: 2px;
float: left;
font-size: 14px;
text-align: left;
width: 30%;
text-overflow: ellipsis;
@media (max-device-width:480px), screen and (max-width:800px) {
width: 33%;
}
@ -562,14 +559,13 @@
}
.html-thumbnail, .misc-icon {
margin: 0;
margin-left: 4px;
float: left;
width: 23px;
height: 23px;
background-size: 23px;
padding: 0;
font-size: 8px;
img {
max-width: 23px;
max-height: 23px;
@ -590,7 +586,7 @@
background-color: transparent;
display: block;
width: 94%;
a {
color: #e93250;
display: inline;
@ -599,7 +595,7 @@
.link-overlay {
width: 30%;
}
@media (max-device-width:480px), screen and (max-width:800px) {
width: 84%;
}
@ -613,7 +609,7 @@
font-size: 13px;
display: block;
padding-top: 4px;
@media (max-device-width:480px), screen and (max-width:800px) {
display: none;
}
@ -756,10 +752,9 @@
}
}
@media (max-device-width:480px), screen and (max-width:800px) {
width: 10em!important;
float: right;
padding: 0 0 18px 0;
margin-top: -77px;
position: absolute;
top: 46px;
right: -8px;
}
}
.interior .header-Outro.with-columns .col-66 {
@ -820,6 +815,7 @@
@media (max-device-width:480px), screen and (max-width:800px) {
width: 60%;
height: 160px;
}
}
.site-portrait {
@ -958,7 +954,7 @@ a.tag:hover {
position: relative;
width: 100%;
float: left;
.text {
float: left;
margin-top: .45em;
@ -984,17 +980,17 @@ a.tag:hover {
padding: 0;
margin-left: 0;
margin-right: 17px;
.html-thumbnail {
width: 102px;
}
&:first-child .html-thumbnail.html {
width: 322px;
height: 100px;
width: 540px;
height: 405px;
}
&:first-child .html-thumbnail.html img {
width: 322px;
height: 200px;
width: 540px;
height: 405px;
}
}
.news-item .file .image-container {
@ -1020,6 +1016,7 @@ a.tag:hover {
.news-item .comments .actions, .news-item .comments p {
margin-left: 47px;
}
.news-item .comments p {
margin-bottom: .4em;
margin-top: .15em;
@ -1867,7 +1864,7 @@ a.tag:hover {
padding-top: 0;
background: #4F7E89;
padding-bottom: 7em;
padding-bottom: 5em;
a {
color: white;
@ -2137,43 +2134,71 @@ table#latest-visitors {
width: 100%;
}
}
.intro-List.kickstarter .col {
padding-top: 1em;
padding-bottom: .8em;
margin-left: 0;
&:first-child{
padding-left: 2px;
.section.thankyou {
text-align: center;
color: #4F7E89;
padding: 6.5em 8% 7em;
a {
color: #4F7E89;
text-decoration: underline;
}
.title {
margin-top: 1%;
}
.title a {
color: white;
font-weight: bold;
text-decoration: none;
img {
margin-bottom: 1em;
}
p {
margin-top: 15px;
font-size: 1em;
margin-bottom: .5em;
margin-top: .5em;
}
}
.welcome.kickstarter {
background: #daeea5 url(/img/tutorialthumbnail.png) no-repeat;
background-position: right center;
background-size: auto 100%;
padding: 15px 100px 4px 23px;
margin-bottom: 13px;
font-size: 95%;
@media (max-device-width:480px), screen and (max-width:800px) {
background-size: 32%;
background-position: right top;
}
h4 {
p:first-child {
font-size: 120%;
margin-bottom: .2em;
a {
color: #2c3e50!important;
}
}
}
ul.thankyou {
list-style: none;
margin-top: 1.5em;
clear: both;
li {
display: inline-block;
width: 32%;
}
}
pre, code {
background: #1d1f21;
color: #FFFFFF;
}
.welcoming-cat {
width: 200px;
float: right;
margin-top: -40px;
}
.section.tutorial-welcome {
padding: 80px 18%;
.option {
margin: 3.5em 0;
}
h3 {
margin-top: 0em;
font-size: 1.3em;
}
p {
font-size: 1.1em;
}
}
.tagcloud .tag10 { font-size: 0.6em; font-weight: 90; }
.tagcloud .tag9 { font-size: 0.7em; font-weight: 100; }
.tagcloud .tag8 { font-size: 0.8em; font-weight: 200; }
.tagcloud .tag7 { font-size: 0.9em; font-weight: 300; }
.tagcloud .tag6 { font-size: 1.0em; font-weight: 400; }
.tagcloud .tag5 { font-size: 1.2em; font-weight: 500; }
.tagcloud .tag4 { font-size: 1.4em; font-weight: 600; }
.tagcloud .tag3 { font-size: 1.6em; font-weight: 700; }
.tagcloud .tag2 { font-size: 1.8em; font-weight: 800; }
.tagcloud .tag1 { font-size: 2.2em; font-weight: 900; }
.tagcloud .tag0 { font-size: 2.5em; font-weight: 900; }

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