diff --git a/.gitignore b/.gitignore index c44ce044..0abd85ad 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ files/sslsites.zip .vagrant public/banned_sites public/deleted_sites +tests/stat_logs/* diff --git a/Gemfile b/Gemfile index b5a3e08c..dcaf606a 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,10 @@ gem 'thread' gem 'scrypt' gem 'rack-cache' gem 'rest-client' +gem 'geoip' +gem 'io-extra', require: 'io/extra' +gem 'rye' +gem 'dnsruby' platform :mri, :rbx do gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic @@ -72,8 +76,10 @@ group :test do gem 'rack_session_access', require: nil gem 'webmock', require: nil gem 'stripe-ruby-mock', '~> 2.0.1', require: 'stripe_mock' + gem 'timecop' platform :mri, :rbx do gem 'simplecov', require: nil + gem 'm' end end diff --git a/Gemfile.lock b/Gemfile.lock index e726b123..8ec16f93 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,6 +9,8 @@ GEM tzinfo (~> 1.1) addressable (2.3.7) ago (0.1.5) + annoy (0.5.6) + highline (>= 1.5.0) ansi (1.4.3) autoparse (0.3.3) addressable (>= 2.3.1) @@ -20,15 +22,15 @@ GEM byebug (2.7.0) columnize (~> 0.3) debugger-linecache (~> 1.2) - capybara (2.4.1) + capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara_minitest_spec (1.0.1) + capybara_minitest_spec (1.0.5) capybara (>= 2) - minitest (>= 2) + minitest (>= 4) celluloid (0.15.2) timers (~> 1.1.0) climate_control (0.0.3) @@ -47,9 +49,11 @@ GEM 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) unf (>= 0.0.5, < 1.0.0) + drydock (0.6.9) erubis (2.7.0) extlib (0.9.16) fabrication (2.11.0) @@ -62,6 +66,7 @@ GEM 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) @@ -74,10 +79,12 @@ GEM signet (>= 0.5.0) uuidtools (>= 2.1.0) hashie (2.0.5) + highline (1.7.2) hiredis (0.5.0) http-cookie (1.0.2) domain_name (~> 0.5) i18n (0.6.9) + io-extra (1.2.8) jimson-temp (0.9.5) blankslate (>= 3.1.2) multi_json (~> 1.0) @@ -89,6 +96,9 @@ GEM kgio (2.9.2) launchy (2.4.2) addressable (~> 2.3) + m (1.3.4) + method_source (>= 0.6.7) + rake (>= 0.9.2.2) magic (0.2.6) ffi (>= 0.6.3) mail (2.5.4) @@ -97,8 +107,8 @@ GEM metaclass (0.0.4) method_source (0.8.2) mime-types (1.25.1) - mini_portile (0.6.0) - minitest (5.3.1) + mini_portile (0.6.2) + minitest (5.6.1) minitest-reporters (1.0.2) ansi builder @@ -106,14 +116,17 @@ GEM powerbar mocha (1.0.0) metaclass (~> 0.0.1) - multi_json (1.10.1) + multi_json (1.11.0) multipart-post (2.0.0) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (2.9.2) netrc (0.10.3) - nokogiri (1.6.3.1) - mini_portile (= 0.6.0) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) pg (0.17.1) phantomjs (1.9.7.1) - poltergeist (1.5.1) + poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -131,14 +144,14 @@ GEM pry (~> 0.9.12) puma (2.8.1) rack (>= 1.1, < 2.0) - rack (1.5.2) + rack (1.6.0) rack-cache (1.2) rack (>= 0.4) rack-protection (1.5.2) rack rack-recaptcha (0.6.6) json - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) rack_session_access (0.1.1) builder (>= 2.0.0) @@ -157,7 +170,14 @@ GEM mime-types (>= 1.16, < 3.0) netrc (~> 0.7) retriable (1.4.1) - rmagick (2.13.3) + rmagick (2.15.0) + rye (0.9.13) + annoy + docile (>= 1.0.1) + highline (>= 1.5.1) + net-scp (>= 1.0.2) + net-ssh (>= 2.0.13) + sysinfo (>= 0.8.1) safe_yaml (1.0.4) sass (3.3.8) screencap (0.1.1) @@ -195,7 +215,8 @@ GEM sinatra (>= 1.0.0) sinatra-xsendfile (0.4.2) sinatra (>= 0.9.1) - slop (3.5.0) + slop (3.6.0) + storable (0.8.9) stripe (1.15.0) json (~> 1.8.1) mime-types (>= 1.25, < 3.0) @@ -204,9 +225,13 @@ GEM dante (>= 0.2.0) jimson-temp stripe (>= 1.15.0) + 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 @@ -224,7 +249,9 @@ GEM webmock (1.17.4) addressable (>= 2.2.7) crack (>= 0.3.2) - websocket-driver (0.3.4) + websocket-driver (0.5.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) xpath (2.0.0) nokogiri (~> 1.3) zipruby (0.3.6) @@ -238,15 +265,19 @@ DEPENDENCIES capybara_minitest_spec cocaine dav4rack + dnsruby erubis fabrication faker filesize + geoip google-api-client hiredis + io-extra jdbc-postgres jruby-openssl json + m magic mail minitest @@ -267,6 +298,7 @@ DEPENDENCIES rest-client rmagick ruby-debug + rye sass screencap scrypt @@ -282,5 +314,9 @@ DEPENDENCIES stripe-ruby-mock (~> 2.0.1) thread tilt + timecop webmock zipruby + +BUNDLED WITH + 1.10.2 diff --git a/Rakefile b/Rakefile index ecbb5742..4ac5badd 100644 --- a/Rakefile +++ b/Rakefile @@ -31,40 +31,11 @@ end desc "parse logs" task :parse_logs => [:environment] do - Dir[File.join($config['logs_path'], '*.log')].each do |log_path| - hits = {} - visits = {} - visit_ips = {} - - logfile = File.open log_path, 'r' - - while hit = logfile.gets - time, username, size, path, ip = hit.split ' ' - - hits[username] ||= 0 - hits[username] += 1 - - visit_ips[username] = [] if !visit_ips[username] - - unless visit_ips[username].include?(ip) - visits[username] ||= 0 - visits[username] += 1 - visit_ips[username] << ip - end - end - - logfile.close - - hits.each do |username,hitcount| - DB['update sites set hits=hits+? where username=?', hitcount, username].first - end - - visits.each do |username,visitcount| - DB['update sites set views=views+? where username=?', visitcount, username].first - end - - FileUtils.rm log_path - end + Stat.prune! + StatLocation.prune! + StatReferrer.prune! + StatPath.prune! + Stat.parse_logfiles $config['logs_path'] end desc 'Update banned IPs list' @@ -223,7 +194,7 @@ end desc 'prime_space_used' task :prime_space_used => [:environment] do Site.select(:id,:username,:space_used).all.each do |s| - s.space_used += s.actual_space_used + s.space_used = s.actual_space_used s.save_changes validate: false end end diff --git a/app.rb b/app.rb index bedf3554..27a7034b 100644 --- a/app.rb +++ b/app.rb @@ -36,6 +36,7 @@ before do end not_found do + @title = 'Not Found' erb :'not_found' end @@ -44,7 +45,7 @@ error do from: 'web@neocities.org', to: 'errors@neocities.org', subject: "[Neocities Error] #{env['sinatra.error'].class}: #{env['sinatra.error'].message}", - body: erb(:'views/templates/email/error'), + body: erb(:'templates/email/error', layout: false), no_footer: true }) diff --git a/app/admin.rb b/app/admin.rb index 287945b0..5f2303af 100644 --- a/app/admin.rb +++ b/app/admin.rb @@ -5,6 +5,56 @@ get '/admin' do erb :'admin' end +get '/admin/reports' do + require_admin + @reports = Report.order(:created_at.desc).all + erb :'admin/reports' +end + +get '/admin/email' do + require_admin + erb :'admin/email' +end + +post '/admin/email' do + require_admin + + %i{subject body}.each do |k| + if params[k].nil? || params[k].empty? + flash[:error] = "#{k.capitalize} is missing." + redirect '/admin/email' + end + end + + sites = Site.newsletter_sites + + day = 0 + + until sites.empty? + seconds = 0.0 + queued_sites = [] + Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times { + break if sites.empty? + queued_sites << sites.pop + } + + queued_sites.each do |site| + EmailWorker.perform_at((day.days.from_now + seconds), { + from: 'Kyle from Neocities ', + to: site.email, + subject: params[:subject], + body: params[:body] + }) + seconds += 0.5 + end + + day += 1 + end + + flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day." + redirect '/' +end + post '/admin/banip' do require_admin site = Site[username: params[:username]] @@ -18,7 +68,7 @@ post '/admin/banip' do flash[:error] = 'IP is blank, cannot continue' redirect '/admin' end - sites = Site.filter(ip: Site.hash_ip(site.ip), is_banned: false).all + sites = Site.filter(ip: site.ip, is_banned: false).all sites.each {|s| s.ban!} flash[:error] = "#{sites.length} sites have been banned." redirect '/admin' diff --git a/app/api.rb b/app/api.rb index b2907260..1387c659 100644 --- a/app/api.rb +++ b/app/api.rb @@ -7,6 +7,7 @@ end post '/api/upload' do require_api_credentials + files = [] params.each do |k,v| next unless v.is_a?(Hash) && v[:tempfile] @@ -22,6 +23,10 @@ post '/api/upload' do api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files' end + if current_site.too_many_files?(files.length) + api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)}), #{current_site.supporter? ? 'please contact support' : 'please upgrade to a supporter account'}" + end + files.each do |file| if !current_site.okay_to_upload?(file) api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content) for this site, files have not been uploaded" @@ -32,13 +37,7 @@ post '/api/upload' do end end - results = [] - files.each do |file| - results << current_site.store_file(file[:filename], file[:tempfile]) - end - - current_site.increment_changed_count if results.include?(true) - + results = current_site.store_files files api_success 'your file(s) have been successfully uploaded' end @@ -53,6 +52,10 @@ post '/api/delete' do api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting" end + if current_site.files_path(path) == current_site.files_path + api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site' + end + if !current_site.file_exists?(path) api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting" end diff --git a/app/browse.rb b/app/browse.rb index f3f64097..ba0c2704 100644 --- a/app/browse.rb +++ b/app/browse.rb @@ -1,17 +1,33 @@ get '/browse/?' do - params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty? - site_dataset = browse_sites_dataset - site_dataset = site_dataset.paginate @current_page, Site::BROWSE_PAGINATION_LENGTH - @page_count = site_dataset.page_count || 1 - @sites = site_dataset.all - erb :browse -end - -def browse_sites_dataset @current_page = params[:current_page] @current_page = @current_page.to_i @current_page = 1 if @current_page == 0 + params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty? + + if is_education? + site_dataset = education_sites_dataset + else + site_dataset = browse_sites_dataset + end + + site_dataset = site_dataset.paginate @current_page, Site::BROWSE_PAGINATION_LENGTH + @page_count = site_dataset.page_count || 1 + @sites = site_dataset.all + if params[:tag] + @title = "Sites tagged #{params[:tag]}" + end + erb :browse +end + +def education_sites_dataset + site_dataset = Site.filter is_deleted: false + site_dataset = site_dataset.association_join(:tags).select_all(:sites) + params[:tag] = current_site.tags.first.name + site_dataset.where! ['tags.name = ?', params[:tag]] +end + +def browse_sites_dataset site_dataset = Site.filter(is_deleted: false, is_banned: false, is_crashing: false).filter(site_changed: true) if current_site @@ -27,6 +43,19 @@ def browse_sites_dataset end case params[:sort_by] + when 'followers' + site_dataset = site_dataset.association_left_join :follows + site_dataset.select_all! :sites + site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count") + site_dataset.group! :sites__id + site_dataset.order! :follow_count.desc, :updated_at.desc + when 'supporters' + site_dataset.exclude! plan_type: nil + site_dataset.exclude! plan_type: 'free' + site_dataset.order! :views.desc, :site_updated_at.desc + when 'featured' + site_dataset.exclude! featured_at: nil + site_dataset.order! :featured_at.desc when 'hits' site_dataset.where!{views > 100} site_dataset.order!(:hits.desc, :site_updated_at.desc) @@ -50,16 +79,19 @@ def browse_sites_dataset params[:sort_by] = 'views' site_dataset.order!(:views.desc, :site_updated_at.desc) else - params[:sort_by] = 'last_updated' - site_dataset.where!{views > 100} - site_dataset.order!(:site_updated_at.desc, :views.desc) + site_dataset = site_dataset.association_left_join :follows + site_dataset.select_all! :sites + site_dataset.select_append! Sequel.lit("count(follows.site_id) AS follow_count") + site_dataset.group! :sites__id + site_dataset.order! :follow_count.desc, :updated_at.desc end end site_dataset.where! ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)] if params[:tag] - site_dataset = site_dataset.association_join(:tags).select_all(:sites) + site_dataset.inner_join! :sites_tags, :site_id => :id + site_dataset.inner_join! :tags, :id => :sites_tags__tag_id site_dataset.where! ['tags.name = ?', params[:tag]] site_dataset.where! ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)] end diff --git a/app/create.rb b/app/create.rb index 05403947..d0af94c0 100644 --- a/app/create.rb +++ b/app/create.rb @@ -16,9 +16,11 @@ def new_recaptcha_valid? end end +CREATE_MATCH_REGEX = /^username$|^password$|^email$|^new_tags_string$|^is_education$/ + post '/create_validate_all' do content_type :json - fields = params.select {|p| p.match /^username$|^password$|^email$|^new_tags_string$/} + fields = params.select {|p| p.match CREATE_MATCH_REGEX} site = Site.new fields @@ -33,11 +35,12 @@ end post '/create_validate' do content_type :json - if !params[:field].match /^username$|^password$|^email$|^new_tags_string$/ + if !params[:field].match CREATE_MATCH_REGEX return {error: 'not a valid field'}.to_json - end + end site = Site.new(params[:field] => params[:value]) + site.is_education = params[:is_education] site.valid? field_sym = params[:field].to_sym @@ -51,14 +54,23 @@ end post '/create' do content_type :json - require_unbanned_ip + + if banned?(true) + signout + session[:banned] = true if !session[:banned] + + flash[:error] = 'There was an error, please contact support to log in.' + redirect '/' + end + dashboard_if_signed_in @site = Site.new( username: params[:username], password: params[:password], email: params[:email], - new_tags_string: params[:tags], + new_tags_string: params[:new_tags_string], + is_education: params[:is_education] == 'true' ? true : false, ip: request.ip ) @@ -85,4 +97,4 @@ post '/create' do session[:id] = @site.id {result: 'ok'}.to_json -end \ No newline at end of file +end diff --git a/app/index.rb b/app/index.rb index 0ad58bce..17e2ee85 100644 --- a/app/index.rb +++ b/app/index.rb @@ -2,6 +2,8 @@ get '/?' do if current_site require_login + redirect '/dashboard' if current_site.is_education + @suggestions = current_site.suggestions @current_page = params[:current_page].to_i @@ -34,7 +36,7 @@ get '/?' do @sites_count = SimpleCache.get :sites_count end - erb :index, layout: false + erb :index, layout: :index_layout end get '/welcome' do @@ -43,6 +45,11 @@ get '/welcome' do erb :'welcome', locals: {site: current_site} end +get '/education' do + redirect '/' if signed_in? + erb :education, layout: :index_layout +end + get '/tutorials' do erb :'tutorials' end @@ -68,5 +75,10 @@ get '/press' do end get '/legal/?' do + @title = 'Legal Guide to Neocities' erb :'legal' end + +get '/permanent-web' do + erb :'permanent_web' +end diff --git a/app/mockup.rb b/app/mockup.rb index eceef7f7..3017a565 100644 --- a/app/mockup.rb +++ b/app/mockup.rb @@ -24,4 +24,9 @@ get '/welcome_mockup' do require_login erb :'welcome_mockup', locals: {site: current_site} end + +get '/stats_mockup' do + require_login + erb :'stats_mockup', locals: {site: current_site} +end # :nocov: \ No newline at end of file diff --git a/app/signin.rb b/app/signin.rb index 2f265f69..a2da5d98 100644 --- a/app/signin.rb +++ b/app/signin.rb @@ -1,5 +1,6 @@ get '/signin/?' do dashboard_if_signed_in + @title = 'Sign In' erb :'signin' end @@ -47,4 +48,4 @@ end def signout session[:id] = nil -end \ No newline at end of file +end diff --git a/app/site.rb b/app/site.rb index 4f019533..d716ec56 100644 --- a/app/site.rb +++ b/app/site.rb @@ -9,6 +9,8 @@ get '/site/:username/?' do |username| # TODO: There should probably be a "this site was deleted" page. not_found if site.nil? || site.is_banned || site.is_deleted + redirect '/' if site.is_education + @title = site.title @current_page = params[:current_page] @@ -16,6 +18,7 @@ get '/site/:username/?' do |username| @current_page = 1 if @current_page == 0 if params[:event_id] + not_found unless params[:event_id].is_integer? event = Event.select(:id).where(id: params[:event_id]).first not_found if event.nil? events_dataset = Event.where(id: params[:event_id]).paginate(1, 1) @@ -29,6 +32,76 @@ get '/site/:username/?' do |username| erb :'site', locals: {site: site, is_current_site: site == current_site} end +get '/site/:username/archives' do + require_login + @site = Site[username: params[:username]] + not_found if @site.nil? + redirect request.referrer unless current_site.id == @site.id + + @archives = @site.archives_dataset.limit(300).order(:updated_at.desc).all + + erb :'site/archives' +end + +get '/site/:username/stats' do + @site = Site[username: params[:username]] + not_found if @site.nil? + + @title = "Site stats for #{@site.host}" + + @stats = {} + + %i{referrers locations paths}.each do |stat| + @stats[stat] = @site.send("stat_#{stat}_dataset".to_sym).order(:views.desc).limit(100).all + end + + @stats[:locations].collect! do |location| + location_name = '' + + location_name += location.city_name if location.city_name + + if location.region_name + # Some of the region names are numbers for some reason. + begin + Integer(location.region_name) + rescue + location_name += ', ' unless location_name == '' + location_name += location.region_name + end + end + + if location.country_code2 && !$country_codes[location.country_code2].nil? + location_name += ', ' unless location_name == '' + location_name += $country_codes[location.country_code2] + end + + location_hash = {name: location_name, views: location.views} + if location.latitude && location.longitude + location_hash.merge! latitude: location.latitude, longitude: location.longitude + end + location_hash + end + + stats_dataset = @site.stats_dataset.order(:created_at.desc).exclude(created_at: Date.today) + + if @site.supporter? + unless params[:days].to_s == 'sincethebigbang' + if params[:days] + stats_dataset.limit! params[:days] + else + stats_dataset.limit! 7 + end + end + else + stats_dataset.limit! 7 + end + + @stats[:stat_days] = stats_dataset.all.reverse + @multi_tooltip_template = "<%= datasetLabel %> - <%= value %>" + + erb :'site/stats', locals: {site: @site} +end + post '/site/:username/set_editor_theme' do require_login current_site.editor_theme = params[:editor_theme] diff --git a/app/site_files.rb b/app/site_files.rb index 4a3a8349..aa7107dc 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -9,32 +9,67 @@ get '/site_files/new' do redirect '/site_files/new_page' end -post '/site_files/create_page' do +post '/site_files/create' do require_login @errors = [] - params[:pagefilename].gsub!(/[^a-zA-Z0-9_\-.]/, '') - params[:pagefilename].gsub!(/\.html$/i, '') + filename = params[:pagefilename] || params[:filename] - if params[:pagefilename].nil? || params[:pagefilename].strip.empty? - @errors << 'You must provide a file name.' - halt erb(:'site_files/new_page') + filename.gsub!(/[^a-zA-Z0-9_\-.]/, '') + + redirect_uri = '/dashboard' + redirect_uri += "?dir=#{Rack::Utils.escape params[:dir]}" if params[:dir] + + if filename.nil? || filename.strip.empty? + flash[:error] = 'You must provide a file name.' + redirect redirect_uri end - name = "#{params[:pagefilename]}.html" + name = "#{filename}" name = "#{params[:dir]}/#{name}" if params[:dir] + name = current_site.scrubbed_path name + if current_site.file_exists?(name) - @errors << %{Web page "#{name}" already exists! Choose another name.} - halt erb(:'site_files/new_page') + flash[:error] = %{Web page "#{name}" already exists! Choose another name.} + redirect redirect_uri end - current_site.install_new_html_file name + extname = File.extname name + + unless extname.match /^\.#{Site::EDITABLE_FILE_EXT}/i + flash[:error] = "Must be an text editable file type (#{Site::VALID_EDITABLE_EXTENSIONS.join(', ')})." + redirect redirect_uri + end + + site_file = current_site.site_files_dataset.where(path: name).first + + if site_file + flash[:error] = 'File already exists, cannot create.' + redirect redirect_uri + end + + if extname.match(/^\.html|^\.htm/i) + current_site.install_new_html_file name + else + file_path = current_site.files_path(name) + FileUtils.touch file_path + File.chmod 0640, file_path + + site_file ||= SiteFile.new site_id: current_site.id, path: name + + site_file.set_all( + size: 0, + sha1_hash: Digest::SHA1.hexdigest(''), + updated_at: Time.now + ) + site_file.save + end flash[:success] = %{#{name} was created! Click here to edit it.} - redirect params[:dir] ? "/dashboard?dir=#{Rack::Utils.escape params[:dir]}" : '/dashboard' + redirect redirect_uri end def file_upload_response(error=nil) @@ -59,8 +94,22 @@ post '/site_files/upload' do file_upload_response "Uploaded files were not seen by the server, cancelled. We don't know what's causing this yet. Please contact us so we can help fix it. Thanks!" end - params[:files].each do |file| - file[:filename] = "#{params[:dir]}/#{file[:filename]}" if params[:dir] + params[:files].each_with_index do |file,i| + dir_name = '' + dir_name = params[:dir] if params[:dir] + + unless params[:file_paths].nil? || params[:file_paths].empty? || params[:file_paths].length == 0 + + file_path = params[:file_paths].select {|file_path| + file[:filename] == Pathname(file_path).basename.to_s + }.first + + unless file_path.nil? + dir_name += '/' + Pathname(file_path).dirname.to_s + end + end + + file[:filename] = "#{dir_name}/#{file[:filename]}" if current_site.file_size_too_large? file[:tempfile].size file_upload_response "#{file[:filename]} is too large, upload cancelled." end @@ -75,21 +124,23 @@ post '/site_files/upload' do file_upload_response "File(s) do not fit in your available space, upload cancelled." end - results = [] - params[:files].each do |file| - results << current_site.store_file(file[:filename], file[:tempfile]) + if current_site.too_many_files? params[:files].length + file_upload_response "Too many files, cannot upload" end - current_site.increment_changed_count if results.include?(true) + results = current_site.store_files params[:files] file_upload_response end post '/site_files/delete' do require_login current_site.delete_file params[:filename] - flash[:success] = "Deleted #{params[:filename]}." - redirect '/dashboard' + + dirname = Pathname(params[:filename]).dirname + dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}" + + redirect "/dashboard#{dir_query}" end get '/site_files/:username.zip' do |username| @@ -147,7 +198,7 @@ post %r{\/site_files\/save\/(.+)} do halt 'File is too large to fit in your space, it has NOT been saved. You will need to reduce the size or upgrade to a new plan.' end - current_site.store_file filename, tempfile + current_site.store_files [{filename: filename, tempfile: tempfile}] 'ok' end diff --git a/app/stats.rb b/app/stats.rb index f3868a8b..02101bd9 100644 --- a/app/stats.rb +++ b/app/stats.rb @@ -2,6 +2,8 @@ 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, diff --git a/app/stripe_webhook.rb b/app/stripe_webhook.rb index f46b7934..eefcac1b 100644 --- a/app/stripe_webhook.rb +++ b/app/stripe_webhook.rb @@ -1,3 +1,23 @@ +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 + post '/stripe_webhook' do event = JSON.parse request.body.read if event['type'] == 'customer.created' @@ -14,8 +34,7 @@ post '/stripe_webhook' do end if event['type'] == 'charge.failed' - site_id = event['data']['object']['description'].split(' - ').last - site = Site[site_id] + site = stripe_get_site_from_event event EmailWorker.perform_async({ from: 'web@neocities.org', @@ -26,8 +45,7 @@ post '/stripe_webhook' do end if event['type'] == 'customer.subscription.deleted' - site_id = event['data']['object']['description'].split(' - ').last - site = Site[site_id] + site = stripe_get_site_from_event event site.stripe_subscription_id = nil site.plan_type = nil site.save_changes validate: false diff --git a/app/surf.rb b/app/surf.rb index f4639c5c..95d819db 100644 --- a/app/surf.rb +++ b/app/surf.rb @@ -1,10 +1,12 @@ get '/surf/?' do + @current_page = params[:current_page].to_i || 1 params.delete 'tag' if params[:tag].nil? || params[:tag].strip.empty? site_dataset = browse_sites_dataset site_dataset = site_dataset.paginate @current_page, 1 @page_count = site_dataset.page_count || 1 @site = site_dataset.first redirect "/browse?#{Rack::Utils.build_query params}" if @site.nil? + @title = "Surf Mode - #{@site.title}" erb :'surf', layout: false end diff --git a/app_helpers.rb b/app_helpers.rb index 0723f111..7c9e5e5b 100644 --- a/app_helpers.rb +++ b/app_helpers.rb @@ -1,10 +1,20 @@ +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 def require_login_ajax halt 'You are not logged in!' unless signed_in? - halt 'You are banned.' if current_site.is_banned? || parent_site.is_banned? + halt 'Please contact support.' if banned? end def csrf_safe? @@ -15,13 +25,13 @@ def csrf_token session[:_csrf_token] ||= SecureRandom.base64(32) end +def is_education? + current_site && current_site.is_education +end + def require_login redirect '/' unless signed_in? - if session[:banned] || current_site.is_banned || parent_site.is_banned - signout - session[:banned] = true - redirect '/' - end + enforce_ban if banned? end def signed_in? @@ -38,14 +48,18 @@ def parent_site current_site.parent? ? current_site : current_site.parent end -def require_unbanned_ip - if session[:banned] || Site.banned_ip?(request.ip) - signout - session[:banned] = true - flash[:error] = 'Site creation has been banned due to ToS violation/spam. '+ - 'If you believe this to be in error, contact the site admin.' - return {result: 'error'}.to_json - end +def banned?(ip_check=false) + return true if session[:banned] + return true if current_site && (current_site.is_banned || parent_site.is_banned) + + return true if ip_check && Site.banned_ip?(request.ip) + false +end + +def enforce_ban + signout + session[:banned] = true + redirect '/' end def title @@ -76,3 +90,31 @@ def send_confirmation_email(site=current_site) body: Tilt.new('./views/templates/email_confirm.erb', pretty: true).render(self, site: site) }) end + +def plan_pricing_button(plan_type) + plan_type = plan_type.to_s + + if !parent_site + %{Sign Up} + elsif parent_site && parent_site.plan_type == plan_type + if request.path.match /\/welcome/ + %{Get Started} + else + %{
Current Plan
} + end + else + #if plan_type == 'supporter' + # plan_price = "$#{Site::PLAN_FEATURES[plan_type.to_sym][:price]*12}, once per year" + #else + plan_price = "$#{Site::PLAN_FEATURES[plan_type.to_sym][:price]}, monthly" + #end + + if request.path.match /\/welcome/ + button_title = 'Get Started' + else + button_title = parent_site.plan_type == 'free' ? 'Upgrade' : 'Change' + end + + %{#{button_title}} + end +end diff --git a/config.yml.travis b/config.yml.travis index 627467e9..876179cf 100644 --- a/config.yml.travis +++ b/config.yml.travis @@ -6,4 +6,5 @@ recaptcha_private_key: '5678' phantomjs_url: - http://localhost:8910 ip_hash_salt: "400$8$1$fc21863da5d531c1" -email_unsubscribe_token: "somethingrandomderrrrp" \ No newline at end of file +email_unsubscribe_token: "somethingrandomderrrrp" +logs_path: "/tmp/neocitiestestlogs" diff --git a/environment.rb b/environment.rb index ccff05f4..6d9911a9 100644 --- a/environment.rb +++ b/environment.rb @@ -61,6 +61,7 @@ end # :nocov: if ENV['RACK_ENV'] == 'development' # Run async jobs immediately in development. +=begin module Sidekiq module Worker module ClassMethods @@ -72,18 +73,7 @@ if ENV['RACK_ENV'] == 'development' end end end -end -# :nocov: - -# :nocov: -if $config['pubsub_url'] - $pubsub_pool = ConnectionPool.new(size: 10, timeout: 5) { - Redis.new url: $config['pubsub_url'] - } -end - -if $config['pubsub_url'].nil? && ENV['RACK_ENV'] == 'production' - raise 'pubsub_url is missing from config' +=end end # :nocov: @@ -131,3 +121,11 @@ if ENV['RACK_ENV'] != 'development' # Sass::Plugin.options[:never_update] = true Sass::Plugin.options[:full_exception] = false end + +require 'csv' + +$country_codes = {} + +CSV.foreach("./files/country_codes.csv") do |row| + $country_codes[row.last] = row.first +end diff --git a/ext/numeric.rb b/ext/numeric.rb index 347c7178..3f385fa2 100644 --- a/ext/numeric.rb +++ b/ext/numeric.rb @@ -10,15 +10,28 @@ class Numeric end def to_bytes_pretty - space = (self.to_f / ONE_MEGABYTE).round(2) - space = space.to_i if space.denominator == 1 -# if space >= 1000000 -# "#{space/1000000} TB" - if space >= 1000 - "#{space/1000} GB" - else - "#{space} MB" - end + computed = nil + unit = nil + { + 'B' => 1000, + 'KB' => 1000 * 1000, + 'MB' => 1000 * 1000 * 1000, + 'GB' => 1000 * 1000 * 1000 * 1000, + 'TB' => 1000 * 1000 * 1000 * 1000 * 1000 + }.each_pair { |e, s| + if self < s + computed = (self.to_f / (s / 1000)).round(2) + unit = e + break + end + } + computed = computed.to_i if computed.modulo(1) == 0.0 + + "#{computed} #{unit}" + end + + def to_comma_separated + self.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse end def format_large_number @@ -33,7 +46,7 @@ class Numeric unit_char = 'K' #thousand unit_amount = 1000.0 end - + self_divided = self.to_f / unit_amount self_rounded = self_divided.round(1) diff --git a/ext/string.rb b/ext/string.rb index 12ec0af1..d4954f6f 100644 --- a/ext/string.rb +++ b/ext/string.rb @@ -11,4 +11,8 @@ class String def unindent gsub /^#{scan(/^\s*/).min_by{|l|l.length}}/, "" end + + def is_integer? + true if Integer(self) rescue false + end end diff --git a/files/GeoLiteCity.dat b/files/GeoLiteCity.dat new file mode 100644 index 00000000..b9841afb Binary files /dev/null and b/files/GeoLiteCity.dat differ diff --git a/files/country_codes.csv b/files/country_codes.csv new file mode 100644 index 00000000..62976d2d --- /dev/null +++ b/files/country_codes.csv @@ -0,0 +1,250 @@ +Afghanistan,AF +Åland Islands,AX +Albania,AL +Algeria,DZ +American Samoa,AS +Andorra,AD +Angola,AO +Anguilla,AI +Antarctica,AQ +Antigua and Barbuda,AG +Argentina,AR +Armenia,AM +Aruba,AW +Australia,AU +Austria,AT +Azerbaijan,AZ +Bahamas,BS +Bahrain,BH +Bangladesh,BD +Barbados,BB +Belarus,BY +Belgium,BE +Belize,BZ +Benin,BJ +Bermuda,BM +Bhutan,BT +"Bolivia, Plurinational State of",BO +"Bonaire, Sint Eustatius and Saba",BQ +Bosnia and Herzegovina,BA +Botswana,BW +Bouvet Island,BV +Brazil,BR +British Indian Ocean Territory,IO +Brunei Darussalam,BN +Bulgaria,BG +Burkina Faso,BF +Burundi,BI +Cambodia,KH +Cameroon,CM +Canada,CA +Cape Verde,CV +Cayman Islands,KY +Central African Republic,CF +Chad,TD +Chile,CL +China,CN +Christmas Island,CX +Cocos (Keeling) Islands,CC +Colombia,CO +Comoros,KM +Congo,CG +"Congo, the Democratic Republic of the",CD +Cook Islands,CK +Costa Rica,CR +Côte d'Ivoire,CI +Croatia,HR +Cuba,CU +Curaçao,CW +Cyprus,CY +Czech Republic,CZ +Denmark,DK +Djibouti,DJ +Dominica,DM +Dominican Republic,DO +Ecuador,EC +Egypt,EG +El Salvador,SV +Equatorial Guinea,GQ +Eritrea,ER +Estonia,EE +Ethiopia,ET +Falkland Islands (Malvinas),FK +Faroe Islands,FO +Fiji,FJ +Finland,FI +France,FR +French Guiana,GF +French Polynesia,PF +French Southern Territories,TF +Gabon,GA +Gambia,GM +Georgia,GE +Germany,DE +Ghana,GH +Gibraltar,GI +Greece,GR +Greenland,GL +Grenada,GD +Guadeloupe,GP +Guam,GU +Guatemala,GT +Guernsey,GG +Guinea,GN +Guinea-Bissau,GW +Guyana,GY +Haiti,HT +Heard Island and McDonald Mcdonald Islands,HM +Holy See (Vatican City State),VA +Honduras,HN +Hong Kong,HK +Hungary,HU +Iceland,IS +India,IN +Indonesia,ID +"Iran, Islamic Republic of",IR +Iraq,IQ +Ireland,IE +Isle of Man,IM +Israel,IL +Italy,IT +Jamaica,JM +Japan,JP +Jersey,JE +Jordan,JO +Kazakhstan,KZ +Kenya,KE +Kiribati,KI +"Korea, Democratic People's Republic of",KP +"Korea, Republic of",KR +Kuwait,KW +Kyrgyzstan,KG +Lao People's Democratic Republic,LA +Latvia,LV +Lebanon,LB +Lesotho,LS +Liberia,LR +Libya,LY +Liechtenstein,LI +Lithuania,LT +Luxembourg,LU +Macao,MO +"Macedonia, the Former Yugoslav Republic of",MK +Madagascar,MG +Malawi,MW +Malaysia,MY +Maldives,MV +Mali,ML +Malta,MT +Marshall Islands,MH +Martinique,MQ +Mauritania,MR +Mauritius,MU +Mayotte,YT +Mexico,MX +"Micronesia, Federated States of",FM +"Moldova, Republic of",MD +Monaco,MC +Mongolia,MN +Montenegro,ME +Montserrat,MS +Morocco,MA +Mozambique,MZ +Myanmar,MM +Namibia,NA +Nauru,NR +Nepal,NP +Netherlands,NL +New Caledonia,NC +New Zealand,NZ +Nicaragua,NI +Niger,NE +Nigeria,NG +Niue,NU +Norfolk Island,NF +Northern Mariana Islands,MP +Norway,NO +Oman,OM +Pakistan,PK +Palau,PW +"Palestine, State of",PS +Panama,PA +Papua New Guinea,PG +Paraguay,PY +Peru,PE +Philippines,PH +Pitcairn,PN +Poland,PL +Portugal,PT +Puerto Rico,PR +Qatar,QA +Réunion,RE +Romania,RO +Russian Federation,RU +Rwanda,RW +Saint Barthélemy,BL +"Saint Helena, Ascension and Tristan da Cunha",SH +Saint Kitts and Nevis,KN +Saint Lucia,LC +Saint Martin (French part),MF +Saint Pierre and Miquelon,PM +Saint Vincent and the Grenadines,VC +Samoa,WS +San Marino,SM +Sao Tome and Principe,ST +Saudi Arabia,SA +Senegal,SN +Serbia,RS +Seychelles,SC +Sierra Leone,SL +Singapore,SG +Sint Maarten (Dutch part),SX +Slovakia,SK +Slovenia,SI +Solomon Islands,SB +Somalia,SO +South Africa,ZA +South Georgia and the South Sandwich Islands,GS +South Sudan,SS +Spain,ES +Sri Lanka,LK +Sudan,SD +Suriname,SR +Svalbard and Jan Mayen,SJ +Swaziland,SZ +Sweden,SE +Switzerland,CH +Syrian Arab Republic,SY +"Taiwan, Province of China",TW +Tajikistan,TJ +"Tanzania, United Republic of",TZ +Thailand,TH +Timor-Leste,TL +Togo,TG +Tokelau,TK +Tonga,TO +Trinidad and Tobago,TT +Tunisia,TN +Turkey,TR +Turkmenistan,TM +Turks and Caicos Islands,TC +Tuvalu,TV +Uganda,UG +Ukraine,UA +United Arab Emirates,AE +United Kingdom,GB +United States,US +United States Minor Outlying Islands,UM +Uruguay,UY +Uzbekistan,UZ +Vanuatu,VU +"Venezuela, Bolivarian Republic of",VE +Viet Nam,VN +"Virgin Islands, British",VG +"Virgin Islands, U.S.",VI +Wallis and Futuna,WF +Western Sahara,EH +Yemen,YE +Zambia,ZM +Zimbabwe,ZW +European Union, EU diff --git a/files/fullhitsmigration.rb b/files/fullhitsmigration.rb new file mode 100644 index 00000000..e9e346ff --- /dev/null +++ b/files/fullhitsmigration.rb @@ -0,0 +1,85 @@ +raise 'nope' + +Sequel.migration do + up { + raise 'derp' + DB.drop_table :stats + DB.drop_table :stat_referrers + DB.drop_table :stat_paths + DB.drop_table :stat_locations + + DB.create_table! :hits do + primary_key :id + Integer :site_id, index: true + Integer :hit_referrer_id + Integer :hit_path_id + Integer :hit_location_id + Bignum :bandwidth + Time :accessed_at, index: true + end + + DB.create_table! :hit_referrers do + primary_key :id + String :uri, index: {unique: true} + end + + DB.create_table! :hit_locations do + primary_key :id + String :country_code2 + String :region_name + String :city_name + Float :latitude + Float :longitude + end + + DB.create_table! :hit_paths do + primary_key :id + String :path, index: {unique: true} + end + } + + down { + raise 'No.' if ENV['RACK_ENV'] == 'production' + + %i{hits hit_referrers hit_locations hit_paths}.each do |t| + DB.drop_table t + end + + DB.create_table! :stats do + primary_key :id + Integer :site_id + Date :created_at + Integer :hits + Integer :views + Integer :comments + Integer :follows + Integer :site_updates + end + + DB.create_table! :stat_referrers do + primary_key :id + Integer :stat_id + String :url + String :views + end + + DB.create_table! :stat_locations do + primary_key :id + Integer :stat_id + String :country_code2 + String :region_name + String :city_name + Decimal :latitude + Decimal :longitude + Integer :views + end + + DB.create_table :stat_paths do + primary_key :id + Integer :stat_id + String :name + Integer :views + end + } +end + diff --git a/migrations/057_add_paypal_data.rb b/migrations/057_add_paypal_data.rb new file mode 100644 index 00000000..684d637a --- /dev/null +++ b/migrations/057_add_paypal_data.rb @@ -0,0 +1,11 @@ +Sequel.migration do + up { + DB.add_column :sites, :paypal_profile_id, String + DB.add_column :sites, :paypal_token, String + } + + down { + DB.drop_column :sites, :paypal_profile_id + DB.drop_column :sites, :paypal_token + } +end diff --git a/migrations/058_add_paypal_status.rb b/migrations/058_add_paypal_status.rb new file mode 100644 index 00000000..12391a22 --- /dev/null +++ b/migrations/058_add_paypal_status.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :paypal_active, :boolean, default: false + } + + down { + DB.drop_column :sites, :paypal_active + } +end diff --git a/migrations/059_refactor_stats.rb b/migrations/059_refactor_stats.rb new file mode 100644 index 00000000..f9ead2af --- /dev/null +++ b/migrations/059_refactor_stats.rb @@ -0,0 +1,55 @@ +Sequel.migration do + up { + DB.drop_table :stats + DB.create_table! :stats do + primary_key :id + Integer :site_id, index: true + Date :created_at, index: true + Integer :hits, default: 0 + Integer :views, default: 0 + Integer :comments, default: 0 + Integer :follows, default: 0 + Integer :site_updates, default: 0 + end + + DB.create_table! :stat_referrers do + primary_key :id + Integer :stat_id, index: true + String :url + Integer :views, default: 0 + end + + DB.create_table! :stat_locations do + primary_key :id + Integer :stat_id, index: true + String :country_code2 + String :region_name + String :city_name + Decimal :latitude + Decimal :longitude + Integer :views, default: 0 + end + + DB.create_table! :stat_paths do + primary_key :id + Integer :stat_id, index: true + String :name + Integer :views, default: 0 + end + } + + down { + DB.drop_table :stats + DB.create_table! :stats do + primary_key :id + Integer :site_id, index: true + Integer :hits, default: 0 + Integer :views, default: 0 + DateTime :created_at, index: true + end + + DB.drop_table :stat_referrers + DB.drop_table :stat_locations + DB.drop_table :stat_paths + } +end diff --git a/migrations/060_separate_stat_timestamps.rb b/migrations/060_separate_stat_timestamps.rb new file mode 100644 index 00000000..1243c6b3 --- /dev/null +++ b/migrations/060_separate_stat_timestamps.rb @@ -0,0 +1,18 @@ +# This migration detaches stat_referrers, stat_locations and stat_paths +# from stats. Instead of stat_id, we'll add a created_at timestamp and remove +# after 7 days for both free and supporter plans (for now). +Sequel.migration do + up { + [:stat_referrers, :stat_paths, :stat_locations].each do |stat_table| + drop_column stat_table, :stat_id + add_column stat_table, :created_at, :date, index: true + end + } + + down { + [:stat_referrers, :stat_paths, :stat_locations].each do |stat_table| + drop_column stat_table, :created_at + add_column stat_table, :stat_id, :integer, index: true + end + } +end diff --git a/migrations/061_add_site_ids.rb b/migrations/061_add_site_ids.rb new file mode 100644 index 00000000..c9bf8741 --- /dev/null +++ b/migrations/061_add_site_ids.rb @@ -0,0 +1,16 @@ +# This migration detaches stat_referrers, stat_locations and stat_paths +# from stats. Instead of stat_id, we'll add a created_at timestamp and remove +# after 7 days for both free and supporter plans (for now). +Sequel.migration do + up { + [:stat_referrers, :stat_paths, :stat_locations].each do |stat_table| + add_column stat_table, :site_id, :integer, index: true + end + } + + down { + [:stat_referrers, :stat_paths, :stat_locations].each do |stat_table| + drop_column stat_table, :site_id + end + } +end diff --git a/migrations/062_fix_latlng.rb b/migrations/062_fix_latlng.rb new file mode 100644 index 00000000..6bdbce48 --- /dev/null +++ b/migrations/062_fix_latlng.rb @@ -0,0 +1,12 @@ +Sequel.migration do + up { + drop_column :stat_locations, :latitude + drop_column :stat_locations, :longitude + add_column :stat_locations, :latitude, :float + add_column :stat_locations, :longitude, :float + } + + down { + # meh. + } +end diff --git a/migrations/063_add_bandwidth_to_stats.rb b/migrations/063_add_bandwidth_to_stats.rb new file mode 100644 index 00000000..f3877a5d --- /dev/null +++ b/migrations/063_add_bandwidth_to_stats.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + add_column :stats, :bandwidth, :bigint, default: 0 + } + + down { + drop_column :stats, :bandwidth + } +end diff --git a/migrations/064_add_education_to_sites.rb b/migrations/064_add_education_to_sites.rb new file mode 100644 index 00000000..f5c3fb48 --- /dev/null +++ b/migrations/064_add_education_to_sites.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + add_column :sites, :is_education, :boolean, default: false + } + + down { + drop_column :sites, :is_education + } +end diff --git a/migrations/065_add_ipfs.rb b/migrations/065_add_ipfs.rb new file mode 100644 index 00000000..68c285d2 --- /dev/null +++ b/migrations/065_add_ipfs.rb @@ -0,0 +1,14 @@ +Sequel.migration do + up { + DB.create_table! :archives do + Integer :site_id, index: true + String :ipfs_hash + DateTime :updated_at, index: true + unique [:site_id, :ipfs_hash] + end + } + + down { + DB.drop_table :archives + } +end diff --git a/migrations/066_add_username_index_to_sites.rb b/migrations/066_add_username_index_to_sites.rb new file mode 100644 index 00000000..9d45f023 --- /dev/null +++ b/migrations/066_add_username_index_to_sites.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_index :sites, :username + } + + down { + DB.drop_index :sites, :username + } +end diff --git a/migrations/067_add_missing_stat_indexes.rb b/migrations/067_add_missing_stat_indexes.rb new file mode 100644 index 00000000..df7ffca9 --- /dev/null +++ b/migrations/067_add_missing_stat_indexes.rb @@ -0,0 +1,13 @@ +Sequel.migration do + up { + DB['create index stat_referrers_hash_multi on stat_referrers (site_id, md5(url))'].first + DB.add_index :stat_locations, :site_id + DB.add_index :stat_paths, :site_id + } + + down { + DB['drop index stat_referrers_hash_multi'].first + DB.drop_index :stat_locations, :site_id + DB.drop_index :stat_paths, :site_id + } +end diff --git a/migrations/068_add_stat_referrer_site_id_index.rb b/migrations/068_add_stat_referrer_site_id_index.rb new file mode 100644 index 00000000..8bd91a7b --- /dev/null +++ b/migrations/068_add_stat_referrer_site_id_index.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_index :stat_referrers, :site_id + } + + down { + DB.drop_index :stat_referrers, :site_id + } +end diff --git a/migrations/069_add_stat_created_indexes.rb b/migrations/069_add_stat_created_indexes.rb new file mode 100644 index 00000000..f041c183 --- /dev/null +++ b/migrations/069_add_stat_created_indexes.rb @@ -0,0 +1,13 @@ +Sequel.migration do + up { + %i{stat_referrers stat_locations stat_paths}.each do |t| + DB.add_index t, :created_at + end + } + + down { + %i{stat_referrers stat_locations stat_paths}.each do |t| + DB.drop_index t, :created_at + end + } +end diff --git a/models/archive.rb b/models/archive.rb new file mode 100644 index 00000000..53731056 --- /dev/null +++ b/models/archive.rb @@ -0,0 +1,9 @@ +class Archive < Sequel::Model + many_to_one :site + set_primary_key [:site_id, :ipfs_hash] + unrestrict_primary_key + + def url + "https://#{ipfs_hash}.ipfs.neocities.org" + end +end diff --git a/models/site.rb b/models/site.rb index a91ab51c..a9e5aeb4 100644 --- a/models/site.rb +++ b/models/site.rb @@ -36,6 +36,10 @@ class Site < Sequel::Model html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp } + VALID_EDITABLE_EXTENSIONS = %w{ + html htm txt js css md manifest + } + MINIMUM_PASSWORD_LENGTH = 5 BAD_USERNAME_REGEX = /[^\w-]/i VALID_HOSTNAME = /^[a-z0-9][a-z0-9-]+?[a-z0-9]$/i # http://tools.ietf.org/html/rfc1123 @@ -73,7 +77,7 @@ class Site < Sequel::Model PHISHING_FORM_REGEX = /www.formbuddy.com\/cgi-bin\/form.pl/i SPAM_MATCH_REGEX = ENV['RACK_ENV'] == 'test' ? /pillz/ : /#{$config['spam_smart_filter'].join('|')}/i EMAIL_SANITY_REGEX = /.+@.+\..+/i - EDITABLE_FILE_EXT = /html|htm|txt|js|css|md|manifest/i + EDITABLE_FILE_EXT = /#{VALID_EDITABLE_EXTENSIONS.join('|')}/i BANNED_TIME = 2592000 # 30 days in seconds TITLE_MAX = 100 @@ -98,20 +102,35 @@ class Site < Sequel::Model unlimited_site_creation: true, custom_ssl_certificates: true, no_file_restrictions: true, - custom_domains: true + custom_domains: true, + maximum_site_files: 25000 } PLAN_FEATURES[:free] = PLAN_FEATURES[:supporter].merge( name: 'Free', - space: Filesize.from('50MB').to_i, + space: Filesize.from('100MB').to_i, bandwidth: Filesize.from('50GB').to_i, price: 0, unlimited_site_creation: false, custom_ssl_certificates: false, no_file_restrictions: false, - custom_domains: false + custom_domains: false, + maximum_site_files: 1000 ) + def self.newsletter_sites + Site.select(:email). + exclude(email: 'nil').exclude(is_banned: true). + where{updated_at > EMAIL_BLAST_MAXIMUM_AGE}. + where{changed_count > 0}. + order(:updated_at.desc). + all + end + + def too_many_files?(file_count=0) + (site_files_dataset.count + file_count) > plan_feature(:maximum_site_files) + end + def plan_feature(key) PLAN_FEATURES[plan_type.to_sym][key.to_sym] end @@ -128,7 +147,15 @@ class Site < Sequel::Model plan_five: 5 } - BROWSE_PAGINATION_LENGTH = 300 + BROWSE_PAGINATION_LENGTH = 100 + + EMAIL_BLAST_MAXIMUM_AGE = 6.months.ago + + if ENV['RACK_ENV'] == 'test' + EMAIL_BLAST_MAXIMUM_PER_DAY = 2 + else + EMAIL_BLAST_MAXIMUM_PER_DAY = 1000 + end many_to_many :tags @@ -161,6 +188,13 @@ class Site < Sequel::Model one_to_many :site_files + one_to_many :stats + one_to_many :stat_referrers + one_to_many :stat_locations + one_to_many :stat_paths + + one_to_many :archives + def account_sites_dataset Site.where(Sequel.|({id: owner.id}, {parent_site_id: owner.id})).order(:parent_site_id.desc, :username) end @@ -260,6 +294,7 @@ class Site < Sequel::Model end def banned_ip?(ip) + return false if ENV['RACK_ENV'] == 'production' && ip == '127.0.0.1' return true if Site.where(is_banned: true). where(ip: hash_ip(ip)). where(['updated_at > ?', Time.now-BANNED_TIME]). @@ -360,25 +395,26 @@ class Site < Sequel::Model def install_new_files FileUtils.mkdir_p files_path + files = [] + %w{index not_found}.each do |name| tmpfile = Tempfile.new "newinstall-#{name}" tmpfile.write render_template("#{name}.erb") tmpfile.close - - store_file "#{name}.html", tmpfile, new_install: true - purge_cache "/#{name}.html" - ScreenshotWorker.perform_async values[:username], "#{name}.html" + files << {filename: "#{name}.html", tempfile: tmpfile} end tmpfile = Tempfile.new 'style.css' tmpfile.close FileUtils.cp template_file_path('style.css'), tmpfile.path - store_file 'style.css', tmpfile, new_install: true + files << {filename: 'style.css', tempfile: tmpfile} tmpfile = Tempfile.new 'cat.png' tmpfile.close FileUtils.cp template_file_path('cat.png'), tmpfile.path - store_file 'cat.png', tmpfile, new_install: true + files << {filename: 'cat.png', tempfile: tmpfile} + + store_files files, new_install: true end def get_file(path) @@ -539,77 +575,57 @@ class Site < Sequel::Model end def purge_cache(path) - relative_path = path.gsub(base_files_path, '') - payload = {site: username, path: relative_path} - payload[:domain] = domain if !domain.empty? - PurgeCacheWorker.perform_async payload + relative_path = path.gsub base_files_path, '' + + # We gotta flush the dirname too if it's an index file. + if relative_path != '' && relative_path.match(/\/$|index\.html?$/i) + PurgeCacheOrderWorker.perform_async username, relative_path + PurgeCacheOrderWorker.perform_async username, Pathname(relative_path).dirname.to_s + else + PurgeCacheOrderWorker.perform_async username, relative_path + end end - def store_file(path, uploaded, opts={}) - relative_path = scrubbed_path path - path = files_path path - pathname = Pathname(path) + Rye::Cmd.add_command :ipfs, nil, 'add', :r - site_file = site_files_dataset.where(path: relative_path).first - - uploaded_sha1 = Digest::SHA1.file(uploaded.path).hexdigest - - if site_file && site_file.sha1_hash == uploaded_sha1 - return false - end - - if relative_path == 'index.html' && opts[:new_install] != true + def add_to_ipfs + # Not ideal. An SoA version is in progress. + if $config['ipfs_ssh_host'] && $config['ipfs_ssh_user'] + rbox = Rye::Box.new $config['ipfs_ssh_host'], :user => $config['ipfs_ssh_user'] begin - new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text - rescue NoMethodError => e - else - if new_title.length < TITLE_MAX - self.title = new_title - end + response = rbox.ipfs "sites/#{self.username.gsub(/\/|\.\./, '')}" + output_array = response + ensure + rbox.disconnect end - - self.site_changed = true - self.site_updated_at = Time.now - self.updated_at = Time.now - - save_changes(validate: false) + else + line = Cocaine::CommandLine.new('ipfs', 'add -r :path') + response = line.run path: files_path + output_array = response.to_s.split("\n") end - if pathname.extname.match HTML_REGEX - # SPAM and phishing checking code goes here + output_array.last.split(' ')[1] + end + + def archive! + #if ENV["RACK_ENV"] == 'test' + # ipfs_hash = "QmcKi2ae3uGb1kBg1yBpsuwoVqfmcByNdMiZ2pukxyLWD8" + #else + #end + + ipfs_hash = add_to_ipfs + + archive = archives_dataset.where(ipfs_hash: ipfs_hash).first + if archive + archive.updated_at = Time.now + archive.save_changes + else + add_archive ipfs_hash: ipfs_hash, updated_at: Time.now end + end - dirname = pathname.dirname.to_s - - if !File.exists? dirname - FileUtils.mkdir_p dirname - end - - uploaded_size = uploaded.size - - FileUtils.cp uploaded.path, path - File.chmod 0640, path - - site_file ||= SiteFile.new site_id: self.id, path: relative_path - - site_file.set_all( - size: uploaded_size, - sha1_hash: uploaded_sha1, - updated_at: Time.now - ) - site_file.save - - purge_cache path - - if pathname.extname.match HTML_REGEX - ScreenshotWorker.perform_async values[:username], relative_path - elsif pathname.extname.match IMAGE_REGEX - ThumbnailWorker.perform_async values[:username], relative_path - end - - SiteChange.record self, relative_path unless opts[:new_install] - - true + def latest_archive + @latest_archive ||= archives_dataset.order(:updated_at.desc).first end def is_directory?(path) @@ -626,11 +642,6 @@ class Site < Sequel::Model true end - def increment_changed_count - self.changed_count += 1 - save_changes(validate: false) - end - def files_zip zip_name = "neocities-#{username}" @@ -654,41 +665,17 @@ class Site < Sequel::Model tmpfile.path end - def delete_file(path) - begin - FileUtils.rm files_path(path) - rescue Errno::EISDIR - site_files.each do |site_file| - if site_file.path.match /^#{path}\// - site_file.destroy - end - end - FileUtils.remove_dir files_path(path), true - rescue Errno::ENOENT - end - - purge_cache path - - ext = File.extname(path).gsub(/^./, '') - - screenshots_delete(path) if ext.match HTML_REGEX - thumbnails_delete(path) if ext.match IMAGE_REGEX - - path = path[1..path.length] if path[0] == '/' - - site_files_dataset.where(path: path).delete - SiteChangeFile.filter(site_id: self.id, filename: path).delete - - true - end - def move_files_from(oldusername) FileUtils.mv base_files_path(oldusername), base_files_path end def install_new_html_file(path) - File.write files_path(path), render_template('index.erb') + tmpfile = Tempfile.new 'neocities_html_template' + tmpfile.write render_template('index.erb') + tmpfile.close + store_files [{filename: path, tempfile: tmpfile}] purge_cache path + tmpfile.unlink end def file_exists?(path) @@ -830,6 +817,18 @@ class Site < Sequel::Model new_tags.compact! @new_filtered_tags = [] + if values[:is_education] == true + if new? + if @new_tags_string.nil? || @new_tags_string.empty? + errors.add :new_tags_string, 'A Class Tag is required.' + end + + if new_tags.length > 1 + errors.add :new_tags_string, 'Must only have one tag' + end + end + end + if ((new? ? 0 : tags_dataset.count) + new_tags.length > 5) errors.add :new_tags_string, 'Cannot have more than 5 tags for your site.' end @@ -856,7 +855,7 @@ class Site < Sequel::Model break end - next if tags.collect {|t| t.name}.include? tag + next if !new? && tags.collect {|t| t.name}.include?(tag) @new_filtered_tags << tag @new_filtered_tags.uniq! @@ -906,9 +905,17 @@ class Site < Sequel::Model is_root_index: file_path == "#{base_files_path}/index.html" } + site_file = site_files_dataset.where(path: file_path.gsub(base_files_path, '').sub(/^\//, '')).first + + if site_file + file[:size] = site_file.size unless file[:is_directory] + file[:updated_at] = site_file.updated_at + end + file[:is_html] = !(file[:ext].match HTML_REGEX).nil? file[:is_image] = !(file[:ext].match IMAGE_REGEX).nil? file[:is_editable] = !(file[:ext].match EDITABLE_FILE_EXT).nil? + file end @@ -945,6 +952,7 @@ class Site < Sequel::Model ((total_space_used.to_f / maximum_space) * 100).round(1) end + # Note: Change Stat#prune! if you change this business logic. def supporter? owner.plan_type != 'free' end @@ -966,6 +974,7 @@ class Site < Sequel::Model !values[:plan_type].match(/plan_/).nil? end + # Note: Change Stat#prune! if you change this business logic. def plan_type return 'free' if owner.values[:plan_type].nil? return 'supporter' if owner.values[:plan_type].match /^plan_/ @@ -1089,4 +1098,149 @@ class Site < Sequel::Model end end end + + # array of hashes: filename, tempfile, opts. + def store_files(files, opts={}) + results = [] + new_size = 0 + html_uploaded = false + + if too_many_files?(files.length) + results << false + return results + end + + files.each do |file| + html_uploaded = true if file[:filename].match HTML_REGEX + + existing_size = 0 + site_file = site_files_dataset.where(path: scrubbed_path(file[:filename])).first + if site_file + existing_size = site_file.size + end + + res = store_file(file[:filename], file[:tempfile], file[:opts] || opts) + if res == true + new_size -= existing_size + new_size += file[:tempfile].size + end + results << res + end + + if results.include? true && opts[:new_install] != true + time = Time.now + sql = DB["update sites set site_changed=?, site_updated_at=?, updated_at=?, changed_count=changed_count+1, space_used=space_used#{new_size < 0 ? new_size.to_s : '+'+new_size.to_s} where id=?", + true, + time, + time, + self.id + ] + sql.first + reload + + #SiteChange.record self, relative_path unless opts[:new_install] + ArchiveWorker.perform_async self.id + end + + results + end + + def delete_file(path) + return false if files_path(path) == files_path + begin + FileUtils.rm files_path(path) + rescue Errno::EISDIR + site_files.each do |site_file| + if site_file.path.match /^#{path}\// + site_file.destroy + end + end + FileUtils.remove_dir files_path(path), true + rescue Errno::ENOENT + end + + purge_cache path + + ext = File.extname(path).gsub(/^./, '') + + screenshots_delete(path) if ext.match HTML_REGEX + thumbnails_delete(path) if ext.match IMAGE_REGEX + + path = path[1..path.length] if path[0] == '/' + + DB.transaction do + site_file = site_files_dataset.where(path: path).first + if site_file + DB['update sites set space_used=space_used-? where id=?', site_file.size, self.id].first + site_file.delete + end + SiteChangeFile.filter(site_id: self.id, filename: path).delete + end + + true + end + + private + + def store_file(path, uploaded, opts={}) + relative_path = scrubbed_path path + path = files_path path + pathname = Pathname(path) + + site_file = site_files_dataset.where(path: relative_path).first + + uploaded_sha1 = Digest::SHA1.file(uploaded.path).hexdigest + + if site_file && site_file.sha1_hash == uploaded_sha1 + return false + end + + if relative_path == 'index.html' + begin + new_title = Nokogiri::HTML(File.read(uploaded.path)).css('title').first.text + rescue NoMethodError => e + else + if new_title.length < TITLE_MAX + self.title = new_title + save_changes validate: false + end + end + end + + if pathname.extname.match HTML_REGEX + # SPAM and phishing checking code goes here + end + + dirname = pathname.dirname.to_s + + if !File.exists? dirname + FileUtils.mkdir_p dirname + end + + uploaded_size = uploaded.size + + FileUtils.cp uploaded.path, path + File.chmod 0640, path + + SiteChange.record self, relative_path unless opts[:new_install] + + site_file ||= SiteFile.new site_id: self.id, path: relative_path + + site_file.set_all( + size: uploaded_size, + sha1_hash: uploaded_sha1, + updated_at: Time.now + ) + site_file.save + + purge_cache path + + if pathname.extname.match HTML_REGEX + ScreenshotWorker.perform_async values[:username], relative_path + elsif pathname.extname.match IMAGE_REGEX + ThumbnailWorker.perform_async values[:username], relative_path + end + true + end + end diff --git a/models/site_file.rb b/models/site_file.rb index 7e714808..fa5db8d6 100644 --- a/models/site_file.rb +++ b/models/site_file.rb @@ -1,6 +1,5 @@ class SiteFile < Sequel::Model - unrestrict_primary_key plugin :update_primary_key many_to_one :site -end \ No newline at end of file +end diff --git a/models/stat.rb b/models/stat.rb index 007cf7eb..687997ee 100644 --- a/models/stat.rb +++ b/models/stat.rb @@ -1,3 +1,244 @@ class Stat < Sequel::Model + FREE_RETAINMENT_DAYS = 30 + many_to_one :site -end \ No newline at end of file + one_to_many :stat_referrers + one_to_many :stat_locations + one_to_many :stat_paths + + class << self + def prune! + DB[ + "DELETE FROM stats WHERE created_at < ? AND site_id NOT IN (SELECT id FROM sites WHERE plan_type IS NOT NULL OR plan_type != 'free')", + (FREE_RETAINMENT_DAYS-1).days.ago.to_date.to_s + ].first + end + + def parse_logfiles(path) + Dir["#{path}/*.log"].each do |log_path| + site_logs = {} + logfile = File.open log_path, 'r' + + while hit = logfile.gets + hit_array = hit.strip.split "\t" + + raise ArgumentError, hit.inspect if hit_array.length > 6 + + time, username, size, path, ip, referrer = hit_array + + next if !referrer.nil? && referrer.match(/bot/i) + + site_logs[username] = { + hits: 0, + views: 0, + bandwidth: 0, + view_ips: [], + ips: [], + referrers: {}, + paths: {} + } unless site_logs[username] + + site_logs[username][:hits] += 1 + site_logs[username][:bandwidth] += size.to_i + + unless site_logs[username][:view_ips].include?(ip) + site_logs[username][:views] += 1 + site_logs[username][:view_ips] << ip + + if referrer != '-' && !referrer.nil? + site_logs[username][:referrers][referrer] ||= 0 + site_logs[username][:referrers][referrer] += 1 + end + end + + site_logs[username][:paths][path] ||= 0 + site_logs[username][:paths][path] += 1 + end + + logfile.close + + current_time = Time.now.utc + current_day_string = current_time.to_date.to_s + + Site.select(:id, :username).where(username: site_logs.keys).all.each do |site| + site_logs[site.username][:id] = site.id + end + + DB.transaction do + site_logs.each do |username, site_log| + DB['update sites set hits=hits+?, views=views+? where username=?', + site_log[:hits], + site_log[:views], + username + ].first + + opts = {site_id: site_log[:id], created_at: current_day_string} + + stat = Stat.select(:id).where(opts).first + DB[:stats].lock('EXCLUSIVE') { stat = Stat.create opts } if stat.nil? + + DB[ + 'update stats set hits=hits+?, views=views+?, bandwidth=bandwidth+? where id=?', + site_log[:hits], + site_log[:views], + site_log[:bandwidth], + stat.id + ].first + +=begin + site_log[:referrers].each do |referrer, views| + stat_referrer = StatReferrer.create_or_get site_log[:id], referrer + DB['update stat_referrers set views=views+? where site_id=?', views, site_log[:id]].first + end + + site_log[:view_ips].each do |ip| + site_location = StatLocation.create_or_get site_log[:id], ip + next if site_location.nil? + DB['update stat_locations set views=views+1 where id=?', site_location.id].first + end + + site_log[:paths].each do |path, views| + site_path = StatPath.create_or_get site_log[:id], path + next if site_path.nil? + DB['update stat_paths set views=views+? where id=?', views, site_path.id].first + end +=end + end + end + + FileUtils.rm log_path + end + end + end +end + +=begin +require 'io/extra' +require 'geoip' + +# Note: This isn't really a class right now. +module Stat + + + class << self + def parse_logfiles(path) + Dir["#{path}/*.log"].each do |logfile_path| + parse_logfile logfile_path + FileUtils.rm logfile_path + end + end + + def parse_logfile(path) + geoip = GeoIP.new GEOCITY_PATH + logfile = File.open path, 'r' + + hits = [] + + while hit = logfile.gets + time, username, size, path, ip, referrer = hit.split ' ' + + site = Site.select(:id).where(username: username).first + next unless site + + paths_dataset = StatsDB[:paths] + path_record = paths_dataset[name: path] + path_id = path_record ? path_record[:id] : paths_dataset.insert(name: path) + + referrers_dataset = StatsDB[:referrers] + referrer_record = referrers_dataset[name: referrer] + referrer_id = referrer_record ? referrer_record[:id] : referrers_dataset.insert(name: referrer) + + location_id = nil + + if city = geoip.city(ip) + locations_dataset = StatsDB[:locations].select(:id) + location_hash = {country_code2: city.country_code2, region_name: city.region_name, city_name: city.city_name} + + location = locations_dataset.where(location_hash).first + location_id = location ? location[:id] : locations_dataset.insert(location_hash) + end + + hits << [site.id, referrer_id, path_id, location_id, size, time] + end + + StatsDB[:hits].import( + [:site_id, :referrer_id, :path_id, :location_id, :bytes_sent, :logged_at], + hits + ) + end + end +end + + + + +=begin + def parse_logfile(path) + hits = {} + visits = {} + visit_ips = {} + + logfile = File.open path, 'r' + + while hit = logfile.gets + time, username, size, path, ip, referrer = hit.split ' ' + + hits[username] ||= 0 + hits[username] += 1 + visit_ips[username] = [] if !visit_ips[username] + + unless visit_ips[username].include? ip + visits[username] ||= 0 + visits[username] += 1 + visit_ips[username] << ip + end + end + + logfile.close + + + hits.each do |username,hitcount| + DB['update sites set hits=hits+? where username=?', hitcount, username].first + end + + visits.each do |username,visitcount| + DB['update sites set views=views+? where username=?', visitcount, username].first + end + end + end +=end + +=begin + def self.parse(logfile_path) + hits = {} + visits = {} + visit_ips = {} + + logfile = File.open logfile_path, 'r' + + while hit = logfile.gets + time, username, size, path, ip = hit.split ' ' + + hits[username] ||= 0 + hits[username] += 1 + + visit_ips[username] = [] if !visit_ips[username] + + unless visit_ips[username].include?(ip) + visits[username] ||= 0 + visits[username] += 1 + visit_ips[username] << ip + end + end + + logfile.close + + hits.each do |username,hitcount| + DB['update sites set hits=hits+? where username=?', hitcount, username].first + end + + visits.each do |username,visitcount| + DB['update sites set views=views+? where username=?', visitcount, username].first + end + end +=end diff --git a/models/stat_location.rb b/models/stat_location.rb new file mode 100644 index 00000000..f1a8e863 --- /dev/null +++ b/models/stat_location.rb @@ -0,0 +1,27 @@ +require 'geoip' + +class StatLocation < Sequel::Model + GEOCITY_PATH = './files/GeoLiteCity.dat' + RETAINMENT_DAYS = 7 + + many_to_one :site + + def self.prune! + where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete + end + + def self.create_or_get(site_id, ip) + geoip = GeoIP.new GEOCITY_PATH + city = geoip.city ip + + return nil if city.nil? + + opts = {site_id: site_id, country_code2: city.country_code2, region_name: city.region_name, city_name: city.city_name} + stat_location = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first + DB[table_name].lock('EXCLUSIVE') { + stat_location = create opts.merge(latitude: city.latitude, longitude: city.longitude, created_at: Date.today) + } if stat_location.nil? + + stat_location + end +end diff --git a/models/stat_path.rb b/models/stat_path.rb new file mode 100644 index 00000000..8dcd6308 --- /dev/null +++ b/models/stat_path.rb @@ -0,0 +1,19 @@ +class StatPath < Sequel::Model + RETAINMENT_DAYS = 7 + + many_to_one :site + + def self.prune! + where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete + end + + def self.create_or_get(site_id, name) + opts = {site_id: site_id, name: name} + stat_path = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first + DB[table_name].lock('EXCLUSIVE') { + stat_path = create opts.merge created_at: Date.today + } if stat_path.nil? + + stat_path + end +end diff --git a/models/stat_referrer.rb b/models/stat_referrer.rb new file mode 100644 index 00000000..edeb52c8 --- /dev/null +++ b/models/stat_referrer.rb @@ -0,0 +1,19 @@ +class StatReferrer < Sequel::Model + many_to_one :site + RETAINMENT_DAYS = 7 + + def self.prune! + where{created_at < (RETAINMENT_DAYS-2).days.ago.to_date}.delete + end + + def self.create_or_get(site_id, url) + opts = {site_id: site_id, url: url} + stat_referrer = where(opts).where{created_at > RETAINMENT_DAYS.days.ago}.first + + DB[table_name].lock('EXCLUSIVE') { + stat_referrer = create opts.merge(created_at: Date.today) + } if stat_referrer.nil? + + stat_referrer + end +end diff --git a/public/img/kickstarterlogo.png b/public/img/kickstarterlogo.png new file mode 100644 index 00000000..1d1622b7 Binary files /dev/null and b/public/img/kickstarterlogo.png differ diff --git a/public/img/kickstarterthumbnail.jpg b/public/img/kickstarterthumbnail.jpg new file mode 100644 index 00000000..509ac5e2 Binary files /dev/null and b/public/img/kickstarterthumbnail.jpg differ diff --git a/public/img/neocities-ipfs-large.png b/public/img/neocities-ipfs-large.png new file mode 100644 index 00000000..24499f1b Binary files /dev/null and b/public/img/neocities-ipfs-large.png differ diff --git a/public/img/neocities-ipfs.jpg b/public/img/neocities-ipfs.jpg new file mode 100644 index 00000000..8d4e6f45 Binary files /dev/null and b/public/img/neocities-ipfs.jpg differ diff --git a/public/img/neocities-logo-education.png b/public/img/neocities-logo-education.png new file mode 100644 index 00000000..85898db9 Binary files /dev/null and b/public/img/neocities-logo-education.png differ diff --git a/public/img/tutorialthumbnail.png b/public/img/tutorialthumbnail.png new file mode 100644 index 00000000..b155646b Binary files /dev/null and b/public/img/tutorialthumbnail.png differ diff --git a/public/js/Chart.min.js b/public/js/Chart.min.js new file mode 100644 index 00000000..3a0a2c87 --- /dev/null +++ b/public/js/Chart.min.js @@ -0,0 +1,11 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ +(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/public/js/news/comment.js b/public/js/news/comment.js index 278c8d61..ce534e10 100644 --- a/public/js/news/comment.js +++ b/public/js/news/comment.js @@ -3,16 +3,13 @@ var Comment = { var form = $(form) var comment = form.find('[name="comment"]').val() form.remove() - $.post('/event/'+eventId+'/comment', {csrf_token: csrfToken, message: comment}, function(res) { - console.log(res) location.reload() }) }, delete: function(commentId, csrfToken) { $.post('/comment/'+commentId+'/delete', {csrf_token: csrfToken}, function(res) { - console.log(res) location.reload() }) }, diff --git a/sass/_project-sass/_project-Base.scss b/sass/_project-sass/_project-Base.scss index 71b8a3c7..0d82fa65 100644 --- a/sass/_project-sass/_project-Base.scss +++ b/sass/_project-sass/_project-Base.scss @@ -59,7 +59,7 @@ a{ @import 'project-Main'; // Project Specific Main Content Area Styling @import 'project-Footer'; // Project Specific Footer Styling -.alert{ +.alert-error { background-color:#F5BA00; color:#fff; } diff --git a/sass/_project-sass/_project-Header.scss b/sass/_project-sass/_project-Header.scss index 2d5edbc3..28e64804 100644 --- a/sass/_project-sass/_project-Header.scss +++ b/sass/_project-sass/_project-Header.scss @@ -83,6 +83,23 @@ } } } +.hp.education { + .col.intro { + padding-top: 20px; + img { + @include vendor(transform, scaleX(-1)); + width: 100px; + margin-right: 25px; + + &.float-Right { + margin-left: 10px; + } + } + .intro-text { + font-size: 1.1em; + } + } +} .intro-List{ @extend %kill-List; @@ -490,4 +507,4 @@ } } -.interior .constant-Nav{margin:0} +.interior .constant-Nav, .hp.education .constant-Nav{margin:0} diff --git a/sass/_project-sass/_project-Main.scss b/sass/_project-sass/_project-Main.scss index 4e0ac182..c21d8777 100644 --- a/sass/_project-sass/_project-Main.scss +++ b/sass/_project-sass/_project-Main.scss @@ -7,18 +7,18 @@ background:#f6f0e6; min-height:500px; padding-bottom:50px; - + h1, h2, h3, h4, h5, h6{ color:#e93250 } } .content, .footer-Content { padding: 20px 3%; - + @media (max-device-width:480px), screen and (max-width:800px) { padding: 20px 7%; } - + @media screen and (min-width:1300px){ padding: 20px 0; } @@ -34,7 +34,7 @@ .interior .header-Outro { padding-top: 30px; overflow: hidden; - + @media (max-device-width:480px), screen and (max-width:800px) { padding-top: 15px; } @@ -42,18 +42,22 @@ .interior .header-Outro h1 { font-size: 2.5em; margin-top: 15px; + + a { + color: white; + } } .news-site-info { @media (max-device-width:480px), screen and (max-width:800px) { float: right; width: 35%; - + .col-50 { width:100%; margin-bottom: 1em!important; } } - + } .site-url { font-size:18px; @@ -78,11 +82,11 @@ -webkit-box-shadow: 0px 1px 1px 1px rgba(0,0,0,0.10); box-shadow: 0px 1px 1px 1px rgba(0,0,0,0.10); padding: 25px 3% 40px 3%; - + @media (max-device-width:480px), screen and (max-width:800px) { padding: 22px 7% 40px 7%; } - + h1, h2, h3, h4, h5, h6 { margin-top: 1em; &:first-child { @@ -122,19 +126,34 @@ } .site-url { margin-top: -9px; - } + } .btn-Action { font-weight: normal; font-size: 12px; padding: 7px 15px; - - @media (max-device-width:480px), screen and (max-width:800px) { - margin: 6px 0px 8px; - } } .row.content { margin-left: 6%; - margin-right: 6%; + margin-right: 6%; + } + @media (max-device-width:480px), screen and (max-width:800px) { + .signup-Area { + display: none; + } + ul { + display: none; + } + .row.content.wide { + margin: 0; + padding-top: 13px; + padding-bottom: 10px; + } + .btn-Action { + margin: 6px 4px 8px; + } + .site-url { + margin-top: -13px; + } } } .interior .header-Outro a {color:#E93250} @@ -151,7 +170,7 @@ -webkit-box-shadow: 1px 2px 5px 2px rgba(0,0,0,0.10); box-shadow: 1px 2px 5px 2px rgba(0,0,0,0.10); border: 4px solid white; - + @media (max-device-width:480px), screen and (max-width:800px) { margin: 0 auto; } @@ -165,7 +184,7 @@ -webkit-background-size:cover; background-size:cover; background-position: center top; - + @media (max-device-width:480px), screen and (max-width:800px) { width: 100%; height: 300px; @@ -203,10 +222,10 @@ } .interior .header-Outro.dashboard .col-50:nth-of-type(2) { float: left; - + @media (max-device-width: 480px), screen and (max-width: 800px) { - margin-left: 22px; - margin-top: 18px; + margin-left: 0px; + margin-top: 16px; } } .welcome { @@ -215,7 +234,7 @@ background-size: 77px 81px; padding: 20px 20px 20px 111px; margin-bottom: 13px; - + @media (max-device-width: 480px), screen and (max-width: 800px) { padding: 111px 20px 20px 20px; background-position: center 20px; @@ -233,10 +252,55 @@ } .files { float:left; - background: #E4D8CB; + background: #EAE1D5; 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); + } + .btn { + padding: 7px 11px; + margin-top: 1px; + background: #77ABB8; + @include box-shadow(0 0 5px rgba(0, 0, 0, 0.2)); + + &:hover { + background: #83B3C0; + } + &:focus, &.active { + outline: 0; + background: #4F727B; + } + } + } +} +.files { + .btn.iconview-button { + background: #4F727B; + } + .btn.listview-button { + background: #77ABB8; + } +} +.files.list-view { + .btn.listview-button { + background: #4F727B; + } + .btn.iconview-button { + background: #77ABB8; + } } .files .header { background: #5E95A1; @@ -259,12 +323,22 @@ } .files .actions { float: right; + + @media (max-device-width:480px), screen and (max-width:800px) { + float: left; + margin-top: 7px; + + .fa { + display: none; + } + } } .files .btn-Action { margin-left: 8px; - + @media (max-device-width:480px), screen and (max-width:800px) { - margin: 4px 8px 4px 0; + margin: 4px 3px 4px 0; + padding: 8px 17px; } } .files .list { @@ -278,12 +352,12 @@ padding: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; - border-radius: 8px; + border-radius: 6px; min-height: 300px; } .files .list .upload-Boundary.with-instruction { background: url(/img/drag-drop.png) no-repeat center center; - + @media (max-device-width:480px), screen and (max-width:800px) { background: 0; } @@ -312,7 +386,7 @@ margin-bottom: 2px; } .files .progress-bar { - background:#CCCCCC; + background:#CCCCCC; -webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; @@ -360,6 +434,7 @@ white-space: nowrap; overflow: hidden; display: block; + text-overflow: ellipsis; } .html-thumbnail { font-size: 11px; @@ -448,6 +523,113 @@ .html-thumbnail.misc.fileimagehover .overlay { margin: 1px 0 0 2px; } +@mixin dashboard-list-view { + padding: 0; + + .upload-Boundary { + padding: 0; + border: 0; + margin: 0; + } + .file { + padding: 10px 20px; + margin: 0; + width: 100%; + + &:nth-child(even) { + background: #EFE8DC; + } + .title { + margin: 0; + margin-left: 7px; + 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%; + } + } + input[type='checkbox'] { + display: block; + float: left; + margin-top: 5px; + margin-right: 6px; + } + } + .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; + } + } + .misc-icon { + padding-top: 8px; + } + .folder-icon { + background-position: 0 4px; + background-size: 23px; + height: 23px; + } + .file > .overlay { + padding-top: 11px; + margin-left: 20px; + text-align: right; + background-color: transparent; + display: block; + width: 94%; + + a { + color: #e93250; + display: inline; + margin-right: 5px; + } + .link-overlay { + width: 30%; + } + + @media (max-device-width:480px), screen and (max-width:800px) { + width: 84%; + } + } + .html-thumbnail > .overlay { + display: none; + } + .column { + float: left; + width: 13%; + font-size: 13px; + display: block; + padding-top: 4px; + + @media (max-device-width:480px), screen and (max-width:800px) { + display: none; + } + } +} +@media (max-device-width:480px), screen and (max-width:800px) { + .files .list { + @include dashboard-list-view; + } + .files .btn-group { + display: none; + } +} +.files.list-view .list { + @include dashboard-list-view; +} .site-actions { float: left; margin-top: 20px; @@ -472,7 +654,7 @@ position: relative; clear: both; width: 100%; - + h2:first-of-type, h3:first-of-type { margin-top: 0; } @@ -516,7 +698,7 @@ width: 66.95%; left: 33.05%; background: white; - + @media (max-device-width:480px), screen and (max-width:800px) { width: 100%; border-bottom: 1px solid #ddd; @@ -526,14 +708,14 @@ .site-profile .content.misc-page.columns .col-66 { padding: 0; min-height: 38em; - + @media (max-device-width:480px), screen and (max-width:800px) { min-height: 0px; } } .news-feed .content.misc-page.columns .col-66 { min-height: 56em; - + @media (max-device-width:480px), screen and (max-width:800px) { min-height: 0; } @@ -542,14 +724,14 @@ width: 33%; left: 33%; float: right; - + @media (max-device-width:480px), screen and (max-width:800px) { width: 100%; } } .interior .header-Outro.with-columns { padding-top: 22px; -} +} .interior .header-Outro.with-columns h3 { float: left; margin-bottom: 0; @@ -560,14 +742,14 @@ } .interior .header-Outro.with-columns .col { padding: 25px 0 8px 30px; - + @media (max-device-width:480px), screen and (max-width:800px) { padding: 10px 0 0 27px; } } .interior .header-Outro.with-columns .col-32 { width: 33%; - + h3 { @media (max-device-width:480px), screen and (max-width:800px) { display: none; @@ -583,10 +765,10 @@ .interior .header-Outro.with-columns .col-66 { width: 67%; border-right: 1px solid #0B0F11; - + @media (max-device-width:480px), screen and (max-width:800px) { border-right: 0; - padding: 15px 0 27px 30px; + padding: 15px 0 27px 30px; } } .interior .header-Outro.with-columns .col-32 .edit { @@ -600,7 +782,7 @@ margin-top: 1.4em; font-size: 0.8em; margin-left: 1.5em; - + @media (max-device-width:480px), screen and (max-width:800px) { margin: 0; clear: left; @@ -635,7 +817,7 @@ background-size: cover; float: left; margin-bottom: 20px; - + @media (max-device-width:480px), screen and (max-width:800px) { width: 60%; } @@ -661,11 +843,11 @@ clear: both; margin: 20px auto 2em auto; text-align: center; - + .btn-Action:last-child { margin-left: 10px; } - + @media (max-device-width:480px), screen and (max-width:800px) { text-align: left; } @@ -751,6 +933,16 @@ a.tag:hover { } .news-item.comment .icon { background: #DAEEA5; +} +.news-item.comment.for-me .icon, .news-item.tip.for-me .icon { + background-size: 62px 62px; + width: 82px; + height: 62px; + background-position: right top; + background-repeat: no-repeat; +} +.news-item .icon-mini { + } .news-item.update .icon { background: #E93250; @@ -809,7 +1001,7 @@ a.tag:hover { float: left; width: 100%; } -.news-item .file a:hover { +.news-item .file a:hover { text-decoration: none; } .news-item .html-thumbnail { @@ -874,7 +1066,7 @@ a.tag:hover { .signup-Area.large { width: 418px; height: 236px; - + @media (max-device-width:480px), screen and (max-width:800px) { height: auto; } @@ -905,14 +1097,14 @@ a.tag:hover { a:first-of-type { margin-left: 0; } - + } .interior .header-Outro .stats { margin-bottom: 1.3em; float: left; width: 100%; margin-top: 1.9em; - + @media (max-device-width:480px), screen and (max-width:800px) { float: none; width: 270px; @@ -936,7 +1128,7 @@ a.tag:hover { text-align: center; margin-right: 28px; color: #84997E; - + @media (max-device-width:480px), screen and (max-width:800px) { margin: 0; width: 90px; @@ -952,21 +1144,21 @@ a.tag:hover { .following { display: none; } - .unfollow { + .unfollow { display: none; } } #followLink.is-following { - - + + .follow { display: none; } - .following { + .following { display: block; width: 5.9em; } - .unfollow { + .unfollow { display: none; } } @@ -974,10 +1166,10 @@ a.tag:hover { .follow { display: none; } - .following { + .following { display: none; } - .unfollow { + .unfollow { display: block; width: 5.9em; } @@ -1008,7 +1200,7 @@ a.tag:hover { border: 3px solid white; -webkit-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3); -moz-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3); - box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3); + box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.3); width: 72px; } .archives img { @@ -1025,11 +1217,11 @@ a.tag:hover { width: 100%; padding: 20px 0 15px 30px; margin: 0 0 3px 0; - + input { width: 86%; float: left; - + @media (max-width:950px) { width: 82%; } @@ -1042,7 +1234,7 @@ a.tag:hover { .comment-policy { font-style: italic; font-size: .8em; - margin-right: 45px; + margin-right: 45px; clear: both; margin-top: 2em; } @@ -1107,11 +1299,11 @@ a.tag:hover { .row { margin: 5.5em 10%; padding: 0; - + @media (max-device-width:480px), screen and (max-width:800px){ margin: 4em 5%; } - + &:first-of-type { margin-top: 3.5em; } @@ -1125,7 +1317,7 @@ a.tag:hover { padding-top: 2em; float: left; } - + @media (max-device-width:480px), screen and (max-width:800px){ width: 100%; padding-right: 0px; @@ -1140,7 +1332,7 @@ a.tag:hover { background-repeat: no-repeat; border: 13px solid #4f7d88; @include box-shadow(1px 1px 2px 0px rgba(0, 0, 0, 0.18)); - + } &:nth-child(3) .screenshot { border: 13px solid $c-Brand-1; @@ -1171,7 +1363,7 @@ a.tag:hover { border-bottom: 1px solid #801629; padding: 30px 0; position: relative; - + h2.delta { color: white; margin: 0; @@ -1204,17 +1396,45 @@ a.tag:hover { background-image:url(/img/hpgallery-next.png); right: 0; } +.section.instructor-quotes { + background: #971D31; + + h2, h3, p { + color: #fff; + text-align: left; + } + h2 { + margin-left: 20px; + } + p { + font-size: 1em; + font-style: italic; + margin-right: 30px; + margin-top: 20px; + clear: both; + } + .image { + float: left; + margin-right: 22px; + } +} .section.features { background: #4F7E89; text-align: center; - + .row:first-of-type { margin-bottom: 0; } } +.section.support { + padding-bottom: 100px; + p { + font-size: 1.1em; + } +} .section.features .col { padding-left: 40px; - + @media (max-device-width:480px), screen and (max-width:800px){ padding-left: 20px; width: 100%; @@ -1248,7 +1468,7 @@ a.tag:hover { clear: both; margin-top: 30px; margin-bottom: 0px; - + @media (max-device-width:480px), screen and (max-width:800px){ margin-top: 0; } @@ -1258,10 +1478,10 @@ a.tag:hover { background-repeat: no-repeat; margin-right: 30px; background-size: 100%; - + @media (max-width:1170px) and (min-width:900px){ margin-right: 25px!important; - + &:last-child { margin-right: 0!important; } @@ -1275,11 +1495,11 @@ a.tag:hover { } .section .logo.wired { width: 211px; - height: 44px; + height: 44px; background-image: url(/img/wired-logo.png); margin-top: 6px; margin-right: 38px; - + @media (max-width:1170px) and (min-width:900px){ width: 170px; } @@ -1289,7 +1509,7 @@ a.tag:hover { } .section .logo.fastco { width: 262px; - height: 39px; + height: 39px; background-image: url(/img/fastcompany-logo.png); margin-top: 7px; @media (max-width:1170px) and (min-width:900px){ @@ -1298,7 +1518,7 @@ a.tag:hover { } .section .logo.vice { width: 160px; - height: 50px; + height: 50px; background-image: url(/img/vice-logo.png); @media (max-width:1170px) and (min-width:900px){ width: 120px; @@ -1306,7 +1526,7 @@ a.tag:hover { } } .section .logo.ars { - width: 187px; + width: 187px; height: 62px; background-image: url(/img/ars-logo.png); margin-top: -4px; @@ -1324,7 +1544,7 @@ a.tag:hover { .section .quote { width: 70%; margin: 0 auto .5em auto; - + h3 { margin-top: .5em; text-align: center; @@ -1334,9 +1554,9 @@ a.tag:hover { font-family: $times; color: #afcbd1; } - a { - color: #afcbd1; - } +} +.section.press .quote a { + color: #afcbd1; } .section.plans { @media(max-device-width:480px), screen and (max-width:550px) { @@ -1481,12 +1701,12 @@ a.tag:hover { border: 0; background: 0; padding-right: 15px; - + span { border-bottom: 1px dotted #bbb; cursor: help; } - + @media(max-device-width:480px), screen and (max-width:800px) { width: 26%; } @@ -1503,8 +1723,8 @@ a.tag:hover { } } .section.plans.welcome { - padding: 63px 3%; - + padding: 63px 3% 0 3%; + h3 { color: #5e95a1; } @@ -1512,7 +1732,7 @@ a.tag:hover { width: 68%; margin-left: auto; margin-right: auto; - + @media(max-device-width:480px), screen and (max-width:550px) { width: 100%; } @@ -1522,18 +1742,18 @@ a.tag:hover { margin-right: auto; max-width: 990px; min-width: 900px; - + @media(max-device-width:480px), screen and (max-width:550px) { min-width: 0; } } >.row >.col { padding-left: 40px; - + &:first-child { padding-left: 0; } - + @media(max-device-width:480px), screen and (max-width:550px) { padding-left: 0; width: 100%; @@ -1551,10 +1771,10 @@ a.tag:hover { float: left; background: 0; padding-bottom: 0; - + ul { clear: both; - + &.main-features { font-size: 140%; margin-bottom: .7em; @@ -1590,7 +1810,7 @@ a.tag:hover { border-right-width: 0; margin-top: 15px; margin-bottom: 15px; - + .main-features { margin-bottom: 76px; } @@ -1601,7 +1821,7 @@ a.tag:hover { border: 1px solid #E0E0E0; @include box-shadow(-1px 1px 5px 0px rgba(0,0,0,0.1)); margin-bottom: 1.2em!important; - + .col:first-child { width: 38%; } @@ -1625,7 +1845,7 @@ a.tag:hover { margin-right: 3px; } } - @media(max-device-width:480px), screen and (max-width:550px) { + @media(max-device-width:480px), screen and (max-width:550px) { .col.free, .col.supporter, .col.supporter .col { width: 100%!important; } @@ -1644,11 +1864,11 @@ a.tag:hover { .section.bottom-signup { // padding-top: 1em; // padding-bottom: 6.5em; - + padding-top: 0; background: #4F7E89; padding-bottom: 7em; - + a { color: white; text-decoration: underline; @@ -1713,8 +1933,20 @@ a.tag:hover { .browse-page h1 { margin-top: 0; } -.browse-page .tags { - padding: 0 30px 40px 30px; +.browse-page .row.content.misc { + form { + padding: 0; + } + input { + margin-top: 0; + margin-bottom: 0; + } + &:last-child { + padding-bottom: 68px; + } + p { + margin-top: 1em; + } } .misc-page .pagination { width: 100%; @@ -1754,7 +1986,7 @@ a.tag:hover { width: 100%; margin-top: 22px; font-size: 15px; - + a, a:visited { color: white; } @@ -1764,6 +1996,12 @@ a.tag:hover { .filename { font-weight: bold; } + @media (max-device-width:480px), screen and (max-width:800px) { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } } .row.content { padding: 15px 20px; @@ -1773,17 +2011,17 @@ a.tag:hover { .tools { color: #8099A7; float: right; - + .theme { font-size: .9em; display: inline; - + select#theme { vertical-align: baseline; background: #25333c; color: #8099A7; - } - + } + @media (max-device-width:480px), screen and (max-width:800px) { display: none; } @@ -1791,7 +2029,7 @@ a.tag:hover { #saveButton { margin-top: 0; } - .tooltip { + .tooltip { &.bottom .tooltip-arrow { border-bottom-color: #971D31; } @@ -1831,7 +2069,7 @@ a.tag:hover { background: rgba(228, 228, 228, 0.42); width: 100%; text-align: center; - padding: 26px 0 5px; + padding: 26px 0 5px; p { margin-top: 10px; } @@ -1863,4 +2101,79 @@ a.tag:hover { @media (max-device-width:480px), screen and (max-width:800px) { display: inline; } -} \ No newline at end of file +} +table#latest-visitors { + width: 100%; + color: #777; + font-size: .8em; + + td { + white-space: nowrap; + max-width: 0; + } + span { + display: block; + } + .location { + color: #2f4149; + font-weight: bold; + font-size: 1.2em; + } + .paths { + overflow: hidden; + text-overflow: ellipsis; + } +} +#earth_div { + width: 100%; + height: 400px; +} +.content.misc-page.columns .col.globe { + padding-right: 0; + padding-top: 4px; +} +.news-feed .content.misc-page .col-50 { + @media (max-device-width:480px), screen and (max-width:800px) { + width: 100%; + } +} +.intro-List.kickstarter .col { + padding-top: 1em; + padding-bottom: .8em; + margin-left: 0; + + &:first-child{ + padding-left: 2px; + } + + .title { + margin-top: 1%; + } + .title a { + color: white; + font-weight: bold; + text-decoration: none; + } + p { + margin-top: 15px; + } +} +.welcome.kickstarter { + background: #daeea5 url(/img/tutorialthumbnail.png) no-repeat; + background-position: right center; + background-size: auto 100%; + padding: 15px 100px 4px 23px; + margin-bottom: 13px; + font-size: 95%; + @media (max-device-width:480px), screen and (max-width:800px) { + background-size: 32%; + background-position: right top; + } + + h4 { + margin-bottom: .2em; + a { + color: #2c3e50!important; + } + } +} diff --git a/sass/_project-sass/_project-Website-Gallery.scss b/sass/_project-sass/_project-Website-Gallery.scss index 45306414..8653cb88 100644 --- a/sass/_project-sass/_project-Website-Gallery.scss +++ b/sass/_project-sass/_project-Website-Gallery.scss @@ -11,7 +11,7 @@ float:left; margin-bottom:$spacing*2; color: #666; - @include box-shadow(1px 1px 2px 0px rgba(0, 0, 0, 0.18)); + @include box-shadow(0px 1px 3px 0px rgba(0, 0, 0, 0.18)); @media (max-device-width:480px), screen and (max-width:800px){ width:50% diff --git a/tests/acceptance/admin_tests.rb b/tests/acceptance/admin_tests.rb index 3f6923c7..32698e2d 100644 --- a/tests/acceptance/admin_tests.rb +++ b/tests/acceptance/admin_tests.rb @@ -51,4 +51,4 @@ describe '/admin' do end end -end \ No newline at end of file +end diff --git a/tests/acceptance/dashboard_tests.rb b/tests/acceptance/dashboard_tests.rb index cf4b60a6..d0f1641d 100644 --- a/tests/acceptance/dashboard_tests.rb +++ b/tests/acceptance/dashboard_tests.rb @@ -17,10 +17,22 @@ describe 'dashboard' do visit '/dashboard' click_link 'New Folder' fill_in 'name', with: 'testimages' - click_button 'Create' + #click_button 'Create' + all('#createDir button[type=submit]').first.click page.must_have_content /testimages/ File.directory?(@site.files_path('testimages')).must_equal true end + + it 'creates a new file' do + random = SecureRandom.uuid.gsub('-', '') + visit '/dashboard' + click_link 'New File' + fill_in 'filename', with: "#{random}.html" + #click_button 'Create' + all('#createFile button[type=submit]').first.click + page.must_have_content /#{random}\.html/ + File.exist?(@site.files_path("#{random}.html")).must_equal true + end end end -end \ No newline at end of file +end diff --git a/tests/acceptance/education_tests.rb b/tests/acceptance/education_tests.rb new file mode 100644 index 00000000..27f5276b --- /dev/null +++ b/tests/acceptance/education_tests.rb @@ -0,0 +1,61 @@ +require_relative './environment.rb' + +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: false) +end + +describe 'signup' do + include Capybara::DSL + + def fill_in_valid + @site = Fabricate.attributes_for(:site) + @class_tag = SecureRandom.uuid.gsub('-', '')[0..Tag::NAME_LENGTH_MAX-1] + fill_in 'username', with: @site[:username] + fill_in 'password', with: @site[:password] + fill_in 'email', with: @site[:email] + fill_in 'new_tags_string', with: @class_tag + end + + before do + Capybara.default_driver = :poltergeist + Capybara.reset_sessions! + visit '/education' + page.must_have_content 'Neocities' # Used to force load wait + end + + after do + Capybara.default_driver = :rack_test + end + + it 'succeeds with valid data' do + fill_in_valid + click_button 'Create My Site' + page.must_have_content /Welcome to your new site/ + + index_file_path = File.join Site::SITE_FILES_ROOT, @site[:username], 'index.html' + File.exist?(index_file_path).must_equal true + + site = Site[username: @site[:username]] + site.site_files.length.must_equal 4 + site.site_changed.must_equal false + site.site_updated_at.must_equal nil + site.is_education.must_equal true + site.tags.length.must_equal 1 + site.tags.first.name.must_equal @class_tag + end + + it 'fails to create for existing site' do + @existing_site = Fabricate :site + fill_in_valid + fill_in :username, with: @existing_site.username + click_button 'Create My Site' + page.must_have_content 'already taken' + end + + it 'fails for multiple tags' do + fill_in_valid + fill_in :new_tags_string, with: 'derp, ie' + click_button 'Create My Site' + page.must_have_content 'Must only have one tag' + end +end diff --git a/tests/acceptance/environment.rb b/tests/acceptance/environment.rb index b0f3379b..352aeafd 100644 --- a/tests/acceptance/environment.rb +++ b/tests/acceptance/environment.rb @@ -4,4 +4,6 @@ Capybara.app = Sinatra::Application def teardown Capybara.reset_sessions! -end \ No newline at end of file +end + +Capybara.default_wait_time = 5 diff --git a/tests/acceptance/index_tests.rb b/tests/acceptance/index_tests.rb index d23cf98c..ff8aee1b 100644 --- a/tests/acceptance/index_tests.rb +++ b/tests/acceptance/index_tests.rb @@ -11,8 +11,8 @@ describe '/' do it 'loads the news feed with welcome' do visit '/' - page.body.must_match /Neocities news feed/i - page.body.must_match /You aren’t following any websites yet/i + page.body.must_match /Thanks for joining the Neocities community/i + page.body.wont_match /You aren’t following any websites yet/i end it 'displays a follow and an unrelated follow' do diff --git a/tests/acceptance/signup_tests.rb b/tests/acceptance/signup_tests.rb index 6e2a6719..32e1b70d 100644 --- a/tests/acceptance/signup_tests.rb +++ b/tests/acceptance/signup_tests.rb @@ -30,16 +30,19 @@ describe 'signup' do Capybara.default_driver = :poltergeist Capybara.reset_sessions! visit_signup + page.must_have_content 'Neocities' # Used to force load wait end after do Capybara.default_driver = :rack_test + BlockedIp.where(ip: '127.0.0.1').delete + DB[:sites].where(is_banned: true).delete end it 'succeeds with valid data' do fill_in_valid click_signup_button - site_created?.must_equal true + site_created? index_file_path = File.join Site::SITE_FILES_ROOT, @site[:username], 'index.html' File.exist?(index_file_path).must_equal true @@ -48,19 +51,36 @@ describe 'signup' do site.site_files.length.must_equal 4 site.site_changed.must_equal false site.site_updated_at.must_equal nil + site.is_education.must_equal false site.ip.must_equal Site.hash_ip('127.0.0.1') end - it 'fails to create for existing site' do + it 'fails if site with same ip has been banned' do + @banned_site = Fabricate :site + @banned_site.is_banned = true + @banned_site.save_changes + fill_in_valid click_signup_button - page.must_have_content 'Welcome to Neocities' - Capybara.reset_sessions! - visit_signup - sleep 0.3 - fill_in 'username', with: @site[:username] - fill_in 'password', with: @site[:password] + Site[username: @site[:username]].must_be_nil + current_path.must_equal '/' + page.wont_have_content 'Welcome to Neocities' + end + + it 'fails if IP is banned from blocked ips list' do + DB[:blocked_ips].insert(ip: '127.0.0.1', created_at: Time.now) + fill_in_valid + click_signup_button + Site[username: @site[:username]].must_be_nil + current_path.must_equal '/' + page.wont_have_content 'Welcome to Neocities' + end + + it 'fails to create for existing site' do + @existing_site = Fabricate :site + fill_in_valid + fill_in 'username', with: @existing_site.username click_signup_button page.must_have_content 'already taken' end @@ -113,9 +133,6 @@ describe 'signup' do page.must_have_content /email.+exists/ end -puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS" - -=begin it 'succeeds with no tags' do fill_in_valid fill_in 'new_tags_string', with: '' @@ -139,7 +156,7 @@ puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS" Site.last.tags.collect {|t| t.name}.must_equal ['derpie', 'shoujo'] end - it 'fails with invalid tag chars' do + it 'fails with invalid tag chars' do fill_in_valid fill_in 'new_tags_string', with: '$POLICE OFFICER$$$$$, derp' click_signup_button @@ -179,9 +196,10 @@ puts "$$$$$$$$$$$$$$$$$$$$$$ TODO FIX TAGS TESTS" fill_in 'new_tags_string', with: 'one, one' click_signup_button - site = Site.last + page.must_have_content /Welcome to Neocities/ + + site = Site[username: @site[:username]] site.tags.length.must_equal 1 site.tags.first.name.must_equal 'one' end -=end end diff --git a/tests/acceptance/site_tests.rb b/tests/acceptance/site_tests.rb index fd517412..60a75261 100644 --- a/tests/acceptance/site_tests.rb +++ b/tests/acceptance/site_tests.rb @@ -21,7 +21,7 @@ describe 'site page' do click_button 'Post' @site.profile_comments.count.must_equal 1 profile_comment = @site.profile_comments.first - profile_comment.actioning_site.must_equal @commenting_site + profile_comment.actioning_site.id.must_equal @commenting_site.id profile_comment.message.must_equal 'I love your site!' end diff --git a/tests/admin_tests.rb b/tests/admin_tests.rb new file mode 100644 index 00000000..fff3ee02 --- /dev/null +++ b/tests/admin_tests.rb @@ -0,0 +1,56 @@ +require_relative './environment.rb' +require 'rack/test' + +include Rack::Test::Methods + +def app + Sinatra::Application +end + +describe 'email blasting' do + before do + EmailWorker.jobs.clear + @admin_site = Fabricate :site, is_admin: true + end + + it 'works' do + DB['update sites set changed_count=?', 0].first + relevant_emails = [] + + sites_emailed_count = Site::EMAIL_BLAST_MAXIMUM_PER_DAY*2 + + sites_emailed_count.times { + site = Fabricate :site, updated_at: Time.now, changed_count: 1 + relevant_emails << site.email + } + + EmailWorker.jobs.clear + + time = Time.now + + Timecop.freeze(time) do + post '/admin/email', { + :csrf_token => 'abcd', + :subject => 'Subject Test', + :body => 'Body Test'}, { + 'rack.session' => { 'id' => @admin_site.id, '_csrf_token' => 'abcd' } + } + + relevant_jobs = EmailWorker.jobs.select{|j| relevant_emails.include?(j['args'].first['to']) } + relevant_jobs.length.must_equal sites_emailed_count + + relevant_jobs.each do |job| + args = job['args'].first + args['from'].must_equal 'Kyle from Neocities ' + args['subject'].must_equal 'Subject Test' + args['body'].must_equal 'Body Test' + end + + relevant_jobs.select {|j| j['at'].nil? || j['at'] == Time.now.to_f}.length.must_equal 1 + relevant_jobs.select {|j| j['at'] == (Time.now + 0.5).to_f}.length.must_equal 1 + + relevant_jobs.select {|j| j['at'] == (time+1.day.to_i).to_f}.length.must_equal 1 + relevant_jobs.select {|j| j['at'] == (time+1.day.to_i+0.5).to_f}.length.must_equal 1 + end + end +end diff --git a/tests/api_tests.rb b/tests/api_tests.rb index 5f94665a..668589f8 100644 --- a/tests/api_tests.rb +++ b/tests/api_tests.rb @@ -89,7 +89,7 @@ describe 'api delete' do it 'succeeds with weird filenames' do create_site basic_authorize @user, @pass - @site.store_file 't$st.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + @site.store_files [{filename: 't$st.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}] post '/api/delete', filenames: ['t$st.jpg'] res[:result].must_equal 'success' @@ -102,16 +102,37 @@ describe 'api delete' do it 'fails with missing files' do create_site basic_authorize @user, @pass - @site.store_file 'test.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + @site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}] post '/api/delete', filenames: ['doesntexist.jpg'] res[:error_type].must_equal 'missing_files' end + it 'fails to delete site directory' do + create_site + basic_authorize @user, @pass + post '/api/delete', filenames: ['/'] + res[:error_type].must_equal 'cannot_delete_site_directory' + File.exist?(@site.files_path).must_equal true + end + + it 'fails to delete other directories' do + create_site + @other_site = @site + create_site + basic_authorize @user, @pass + post '/api/delete', filenames: ["../#{@other_site.username}"] + File.exist?(@other_site.base_files_path).must_equal true + res[:error_type].must_equal 'missing_files' + post '/api/delete', filenames: ["../#{@other_site.username}/index.html"] + File.exist?(@other_site.base_files_path+'/index.html').must_equal true + res[:error_type].must_equal 'missing_files' + end + it 'succeeds with valid filenames' do create_site basic_authorize @user, @pass - @site.store_file 'test.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - @site.store_file 'test2.jpg', Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + @site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}] + @site.store_files [{filename: 'test2.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}] post '/api/delete', filenames: ['test.jpg', 'test2.jpg'] res[:result].must_equal 'success' site_file_exists?('test.jpg').must_equal false @@ -139,6 +160,19 @@ describe 'api upload' do res[:error_type].must_equal 'missing_files' end + it 'fails with too many files' do + create_site + basic_authorize @user, @pass + @site.plan_feature(:maximum_site_files).times { + uuid = SecureRandom.uuid.gsub('-', '')+'.html' + @site.add_site_file path: uuid + } + post '/api/upload', { + '/lol.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + res[:error_type].must_equal 'too_many_files' + end + it 'resists directory traversal attack' do create_site basic_authorize @user, @pass @@ -167,14 +201,6 @@ describe 'api upload' do '/' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') } res[:error_type].must_equal 'invalid_file_type' - - create_site - basic_authorize @user, @pass - post '/api/upload', { - '' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') - } - - res[:error_type].must_equal 'missing_files' end it 'fails for file with no extension' do diff --git a/tests/environment.rb b/tests/environment.rb index 4cd4a7ac..3247bc9f 100644 --- a/tests/environment.rb +++ b/tests/environment.rb @@ -50,4 +50,4 @@ I18n.enforce_available_locales = true Mail.defaults do delivery_method :test -end \ No newline at end of file +end diff --git a/tests/site_file_tests.rb b/tests/site_file_tests.rb index 625cbc5c..f2aee5ee 100644 --- a/tests/site_file_tests.rb +++ b/tests/site_file_tests.rb @@ -18,21 +18,25 @@ describe 'site_files' do before do @site = Fabricate :site ThumbnailWorker.jobs.clear + PurgeCacheOrderWorker.jobs.clear PurgeCacheWorker.jobs.clear ScreenshotWorker.jobs.clear end describe 'delete' do it 'works' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file + @site.reload.space_used.must_equal uploaded_file.size file_path = @site.files_path 'test.jpg' File.exists?(file_path).must_equal true delete_file filename: 'test.jpg' File.exists?(file_path).must_equal false SiteFile[site_id: @site.id, path: 'test.jpg'].must_be_nil + @site.reload.space_used.must_equal 0 end - it 'deletes all files in a directory' do + it 'deletes a directory and all files in it' do upload( 'dir' => 'test', 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') @@ -45,6 +49,21 @@ describe 'site_files' do @site.site_files.select {|f| f.path =~ /^test\//}.length.must_equal 0 @site.site_files.select {|f| f.path =~ /^test/}.length.must_equal 1 end + + it 'goes back to deleting directory' do + upload( + 'dir' => 'test', + 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + ) + delete_file filename: 'test/test.jpg' + last_response.headers['Location'].must_equal "http://example.org/dashboard?dir=test" + + upload( + 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + ) + delete_file filename: 'test.jpg' + last_response.headers['Location'].must_equal "http://example.org/dashboard" + end end describe 'upload' do @@ -82,10 +101,31 @@ describe 'site_files' do args = ScreenshotWorker.jobs.first['args'] args.first.must_equal @site.username args.last.must_equal 'index.html' - @site.title.must_equal "#{@site.username}.neocities.org" + @site.title.must_equal "The web site of #{@site.username}" @site.reload @site.site_changed.must_equal true @site.title.must_equal 'Hello?' + + # Purge cache needs to flush / and index.html for either scenario. + PurgeCacheOrderWorker.jobs.length.must_equal 2 + first_purge = PurgeCacheOrderWorker.jobs.first + dirname_purge = PurgeCacheOrderWorker.jobs.last + + username, pathname = first_purge['args'] + username.must_equal @site.username + pathname.must_equal '/index.html' + username, pathame = nil + username, pathname = dirname_purge['args'] + username.must_equal @site.username + pathname.must_equal '/' + end + + it 'provides the correct space used after overwriting an existing file' do + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file + second_uploaded_file = Rack::Test::UploadedFile.new('./tests/files/img/test.jpg', 'image/jpeg') + upload 'files[]' => second_uploaded_file + @site.reload.space_used.must_equal second_uploaded_file.size end it 'does not change title for subdir index.html' do @@ -97,15 +137,19 @@ describe 'site_files' do @site.reload.title.must_equal title end - it 'succeeds with valid file' do - upload 'files[]' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + uploaded_file = Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + upload 'files[]' => uploaded_file last_response.body.must_match /successfully uploaded/i File.exists?(@site.files_path('test.jpg')).must_equal true - queue_args = PurgeCacheWorker.jobs.first['args'].first - queue_args['site'].must_equal @site.username - queue_args['path'].must_equal '/test.jpg' + username, path = PurgeCacheOrderWorker.jobs.first['args'] + username.must_equal @site.username + path.must_equal '/test.jpg' + + @site.reload + @site.space_used.wont_equal 0 + @site.space_used.must_equal uploaded_file.size ThumbnailWorker.jobs.length.must_equal 1 ThumbnailWorker.drain @@ -114,7 +158,7 @@ describe 'site_files' do File.exists?(@site.thumbnail_path('test.jpg', resolution)).must_equal true end - @site.site_changed.must_equal false + @site.site_changed.must_equal true end it 'fails with unsupported file' do @@ -153,9 +197,18 @@ describe 'site_files' do last_response.body.must_match /successfully uploaded/i File.exists?(@site.files_path('derpie/derptest/test.jpg')).must_equal true - PurgeCacheWorker.jobs.length.must_equal 1 - queue_args = PurgeCacheWorker.jobs.first['args'].first - queue_args['path'].must_equal '/derpie/derptest/test.jpg' + PurgeCacheOrderWorker.jobs.length.must_equal 1 + username, path = PurgeCacheOrderWorker.jobs.first['args'] + path.must_equal '/derpie/derptest/test.jpg' + + PurgeCacheOrderWorker.drain + + PurgeCacheWorker.jobs.length.must_equal 2 + ip, username, path = PurgeCacheWorker.jobs.first['args'] + ip.must_equal '10.0.0.1' + username.must_equal @site.username + path.must_equal '/derpie/derptest/test.jpg' + PurgeCacheWorker.jobs.last['args'].first.must_equal '10.0.0.2' ThumbnailWorker.jobs.length.must_equal 1 ThumbnailWorker.drain diff --git a/tests/site_tests.rb b/tests/site_tests.rb index c42d1d49..e72a685e 100644 --- a/tests/site_tests.rb +++ b/tests/site_tests.rb @@ -53,10 +53,12 @@ describe Site do end it 'should match plan_type' do - %w{supporter neko catbus fatcat}.each do |plan_type| + %w{supporter free}.each do |plan_type| site = Fabricate :site, plan_type: plan_type site.plan_type.must_equal plan_type end + site = Fabricate :site, plan_type: nil + site.plan_type.must_equal 'free' end end @@ -77,4 +79,4 @@ describe Site do site.suggestions.length.must_equal Site::SUGGESTIONS_LIMIT end end -end \ No newline at end of file +end diff --git a/tests/stat_tests.rb b/tests/stat_tests.rb new file mode 100644 index 00000000..4216d39e --- /dev/null +++ b/tests/stat_tests.rb @@ -0,0 +1,153 @@ +require_relative './environment.rb' + +STAT_LOGS_PATH = 'tests/stat_logs' +STAT_LOGS_DIR_MATCH = "#{STAT_LOGS_PATH}/*.log" + +describe 'stats' do + before do + Dir[STAT_LOGS_DIR_MATCH].each {|f| FileUtils.rm f} + @site_one = Fabricate :site + @site_two = Fabricate :site + + @time = Time.now + @time_iso8601 = @time.iso8601 + + log = [ + "#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t67.180.75.140\thttp://example.com", + "#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t67.180.75.140\thttp://example.com", + "#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t172.56.16.152\thttp://example.com", + "#{@time_iso8601}\t#{@site_one.username}\t5000\t/\t172.56.16.152\t-", + "#{@time_iso8601}\t#{@site_two.username}\t5000\t/\t67.180.75.140\thttp://example.com", + "#{@time_iso8601}\t#{@site_two.username}\t5000\t/\t127.0.0.1\t-", + "#{@time_iso8601}\t#{@site_two.username}\t5000\t/derp.html\t127.0.0.2\thttps://example.com" + ] + + File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file| + file.write log.join("\n") + end + end + + it 'deals with spaces in paths' do + @site = Fabricate :site + File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file| + file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com\n" + file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com\n" + end + + Stat.parse_logfiles STAT_LOGS_PATH + @site.stats.first.bandwidth.must_equal 612917*2 + #@site.stat_referrers.first.url.must_equal 'http://derp.com' + #@site.stat_locations.first.city_name.must_equal 'Menlo Park' + end + + it 'deals with spaces in referrer' do + @site = Fabricate :site + File.open("tests/stat_logs/#{SecureRandom.uuid}.log", 'w') do |file| + file.write "2015-05-02T21:16:35+00:00\t#{@site.username}\t612917\t/images/derpie space.png\t67.180.75.140\thttp://derp.com?q=what the lump\n" + end + + Stat.parse_logfiles STAT_LOGS_PATH + end + + it 'prunes logs for free sites' do + @free_site = Fabricate :site + @supporter_site = Fabricate :site, plan_type: 'supporter' + + day = Date.today + (Stat::FREE_RETAINMENT_DAYS+1).times do |i| + [@free_site, @supporter_site].each do |site| + Stat.create site_id: site.id, created_at: day + end + day = day - 1 + end + + count_site_ids = [@free_site.id, @supporter_site.id] + expected_stat_count = (Stat::FREE_RETAINMENT_DAYS+1)*2 + + Stat.where(site_id: count_site_ids).count.must_equal expected_stat_count + Stat.prune! + Stat.where(site_id: count_site_ids).count.must_equal expected_stat_count-1 + Stat.where(site_id: @supporter_site.id).count.must_equal expected_stat_count/2 + end + + it 'prunes referrers' do + stat_referrer_now = @site_one.add_stat_referrer created_at: Date.today, url: 'http://example.com/now' + stat_referrer = @site_one.add_stat_referrer created_at: (StatReferrer::RETAINMENT_DAYS-1).days.ago, url: 'http://example.com' + StatReferrer[stat_referrer.id].wont_be_nil + @site_one.stat_referrers_dataset.count.must_equal 2 + StatReferrer.prune! + @site_one.stat_referrers_dataset.count.must_equal 1 + StatReferrer[stat_referrer.id].must_be_nil + end + + it 'prunes locations' do + stat_location = @site_one.add_stat_location( + created_at: (StatLocation::RETAINMENT_DAYS-1).days.ago, + country_code2: 'US', + region_name: 'Minnesota', + city_name: 'Minneapolis' + ) + StatLocation[stat_location.id].wont_be_nil + StatLocation.prune! + StatLocation[stat_location.id].must_be_nil + end + + it 'prunes paths' do + stat_path = @site_one.add_stat_path( + created_at: (StatPath::RETAINMENT_DAYS-1).days.ago, + name: '/derpie.html' + ) + StatPath[stat_path.id].wont_be_nil + StatPath.prune! + StatPath[stat_path.id].must_be_nil + end + + it 'parses logfile' do + Stat.parse_logfiles STAT_LOGS_PATH + + @site_one.reload + @site_one.hits.must_equal 4 + @site_one.views.must_equal 2 + stat = @site_one.stats.first + stat.hits.must_equal 4 + stat.views.must_equal 2 + stat.bandwidth.must_equal 20_000 + + #@site_one.stat_referrers.count.must_equal 1 + #stat_referrer = @site_one.stat_referrers.first + #stat_referrer.url.must_equal 'http://example.com' + #stat_referrer.created_at.must_equal @time.to_date + #stat_referrer.views.must_equal 2 + + #@site_one.stat_paths.length.must_equal 1 + #stat_path = @site_one.stat_paths.first + #stat_path.name.must_equal '/' + #stat_path.views.must_equal 4 + + #@site_one.stat_locations.length.must_equal 2 + #stat_location = @site_one.stat_locations.first + #stat_location.country_code2.must_equal 'US' + #stat_location.region_name.must_equal 'CA' + #stat_location.city_name.must_equal 'Menlo Park' + #stat_location.views.must_equal 1 + + @site_two.reload + @site_two.hits.must_equal 3 + @site_two.views.must_equal 3 + stat = @site_two.stats.first + stat.hits.must_equal 3 + stat.views.must_equal 3 + stat.bandwidth.must_equal 15_000 + #@site_two.stat_referrers.count.must_equal 2 + #stat_referrer = @site_two.stat_referrers.first + #stat_referrer.url.must_equal 'http://example.com' + #stat_referrer.views.must_equal 2 + + #stat_paths = @site_two.stat_paths + #stat_paths.length.must_equal 2 + #stat_paths.first.name.must_equal '/' + #stat_paths.last.name.must_equal '/derp.html' + + # [geoip.city('67.180.75.140'), geoip.city('172.56.16.152')] + end +end diff --git a/tests/workers/archive_worker_tests.rb b/tests/workers/archive_worker_tests.rb new file mode 100644 index 00000000..90a748e2 --- /dev/null +++ b/tests/workers/archive_worker_tests.rb @@ -0,0 +1,28 @@ +require_relative '../environment.rb' + +describe ArchiveWorker do + it 'stores an IPFS archive' do + return if ENV['TRAVIS'] + site = Fabricate :site + ipfs_hash = site.add_to_ipfs + ArchiveWorker.new.perform site.id + site.archives.length.must_equal 1 + archive_one = site.archives.first + archive_one.ipfs_hash.must_equal ipfs_hash + archive_one.updated_at.wont_be_nil + + new_updated_at = Time.now - 500 + archive_one.update updated_at: new_updated_at + + ArchiveWorker.new.perform site.id + archive_one.reload.updated_at.wont_equal new_updated_at + + site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}] + ArchiveWorker.new.perform site.id + + site.reload + site.archives.length.must_equal 2 + archive_two = site.archives_dataset.exclude(ipfs_hash: archive_one.ipfs_hash).first + archive_two.ipfs_hash.wont_be_nil + end +end diff --git a/tests/workers/purge_cache_order_worker_tests.rb b/tests/workers/purge_cache_order_worker_tests.rb new file mode 100644 index 00000000..39582f71 --- /dev/null +++ b/tests/workers/purge_cache_order_worker_tests.rb @@ -0,0 +1,21 @@ +require_relative '../environment.rb' + +describe PurgeCacheWorker do + before do + PurgeCacheOrderWorker.jobs.clear + PurgeCacheWorker.jobs.clear + end + + it 'queues up purges' do + PurgeCacheOrderWorker.new.perform('kyledrake', '/test.jpg') + + job_one_args = PurgeCacheWorker.jobs.first['args'] + job_two_args = PurgeCacheWorker.jobs.last['args'] + job_one_args[0].must_equal '10.0.0.1' + job_one_args[1].must_equal 'kyledrake' + job_one_args[2].must_equal '/test.jpg' + job_two_args[0].must_equal '10.0.0.2' + job_two_args[1].must_equal 'kyledrake' + job_two_args[2].must_equal '/test.jpg' + end +end diff --git a/tests/workers/purge_cache_worker_tests.rb b/tests/workers/purge_cache_worker_tests.rb new file mode 100644 index 00000000..90be0da8 --- /dev/null +++ b/tests/workers/purge_cache_worker_tests.rb @@ -0,0 +1,64 @@ +require_relative '../environment.rb' + +describe PurgeCacheWorker do + before do + @test_ip = '10.0.0.1' + end + + it 'throws exception without 200 or 404 http status' do + stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg"). + with(headers: {'Host' => 'kyledrake.neocities.org'}) + .to_return(status: 503) + + worker = PurgeCacheWorker.new + + proc { + worker.perform @test_ip, 'kyledrake', '/test.jpg' + }.must_raise RestClient::ServiceUnavailable + end + + it 'handles 404 without exception' do + stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg"). + with(headers: {'Host' => 'kyledrake.neocities.org'}) + .to_return(status: 404) + + worker = PurgeCacheWorker.new + worker.perform @test_ip, 'kyledrake', '/test.jpg' + end + + it 'sends a purge request' do + stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg"). + with(headers: {'Host' => 'kyledrake.neocities.org'}) + .to_return(status: 200) + + worker = PurgeCacheWorker.new + worker.perform @test_ip, 'kyledrake', '/test.jpg' + + assert_requested :get, "http://#{@test_ip}/:cache/purge/test.jpg" + end + + it 'handles spaces correctly' do + stub_request(:get, "http://#{@test_ip}/:cache/purge/te st.jpg"). + with(headers: {'Host' => 'kyledrake.neocities.org'}) + .to_return(status: 200) + + url = Addressable::URI.encode_component( + "http://#{@test_ip}/:cache/purge/te st.jpg", + Addressable::URI::CharacterClasses::QUERY + ) + + worker = PurgeCacheWorker.new + worker.perform @test_ip, 'kyledrake', '/te st.jpg' + + assert_requested :get, url + end + + it 'works without forward slash' do + stub_request(:get, "http://#{@test_ip}/:cache/purge/test.jpg"). + with(headers: {'Host' => 'kyledrake.neocities.org'}) + .to_return(status: 200) + + worker = PurgeCacheWorker.new + worker.perform @test_ip, 'kyledrake', 'test.jpg' + end +end diff --git a/views/_footer.erb b/views/_footer.erb index d3409177..cd67fe04 100644 --- a/views/_footer.erb +++ b/views/_footer.erb @@ -10,9 +10,11 @@
    • About
    • Donate
    • -
    • Blog
    • -
    • API
    • -
    • Press
    • + <% unless is_education? %> +
    • Blog
    • +
    • API
    • +
    • Press
    • + <% end %>
    • Terms
    • Privacy
    • Contact
    • diff --git a/views/_header.erb b/views/_header.erb index 58c43454..189adc6e 100644 --- a/views/_header.erb +++ b/views/_header.erb @@ -30,17 +30,19 @@ diff --git a/views/site_files/new_page.erb b/views/site_files/new_page.erb index 28fa090d..31839b31 100644 --- a/views/site_files/new_page.erb +++ b/views/site_files/new_page.erb @@ -17,11 +17,11 @@ <% end %>
      -
      + <%== csrf_token_input_html %>

      What's the name of your page?

      -

      .html

      +

      .html

      Note: We will automatically scrub any characters not matching: a-z A-Z 0-9 _ - .

      @@ -29,4 +29,4 @@

      If you want to make this the index page (and an index page doesn't exist), name it index.html.

      - \ No newline at end of file + diff --git a/views/site_files/text_editor.erb b/views/site_files/text_editor.erb index 304933a7..f178ed55 100644 --- a/views/site_files/text_editor.erb +++ b/views/site_files/text_editor.erb @@ -27,7 +27,12 @@
      @@ -69,9 +74,11 @@
      - Save - View - '>Share + Save + + View + '>Share + + +

      Last 7 Days

      +

      (Upgrade to see up to see stats for all time)

      + + + + +
      +
      +

      Top Paths

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      PathVisits
      /130
      /contact110
      /art101
      /about99
      /links33
      + +

      Top Locations

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      CityVisits
      Portland, OR, USA22
      Portland, OR, USA22
      Portland, OR, USA22
      Portland, OR, USA22
      Portland, OR, USA22
      +
      + +
      + +

      Top Referrers

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      ReferrerVisits
      Google search22
      Google search22
      Google search22
      Google search22
      Google search22
      +
      +
      +
      + +
      +
      +

      <%= site.title %>

      +
      +
      + <% if site.updated_at %> + Last updated
      <%= site.updated_at.ago %> + <% else %> + Your new website!
      Start Building + <% end %> +
      +
      +
      <%= site.views.format_large_number %> views
      + <% follows_count = site.follows_dataset.count %> +
      <%= follows_count.format_large_number %> follower<%= follows_count == 1 ? '' : 's' %>
      +
      +
      +
      + + + + + + <%== erb :'_follows', layout: false, locals: {site: site, is_current_site: site == current_site} %> + + <%== erb :'_tags', layout: false, locals: {site: site, is_current_site: site == current_site} %> +
      +
      +
      + + + + + \ No newline at end of file diff --git a/views/surf.erb b/views/surf.erb index 041e19f1..6776a450 100644 --- a/views/surf.erb +++ b/views/surf.erb @@ -2,11 +2,7 @@ - <% if @title %> - <%= @title %> - Neocities - <% else %> - Neocities - <% end %> + <%= title %>