From ab76ee76c4d41db26e6cf85ecda0f5e9c70b6e10 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Mon, 8 May 2017 19:17:25 +0200 Subject: [PATCH 001/106] ignore files in upstream-patches --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b17eeb8..c0b1883 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ vendor/bundle public/assets +upstream-patches # templates to be created per-site app/views/home/privacy.* From cabe52b31daf4b15d295e3ce776bd0da6f8f36b0 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Mon, 8 May 2017 19:18:37 +0200 Subject: [PATCH 002/106] Story: look through self.taggings since it's eager loaded in list views --- app/models/story.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/story.rb b/app/models/story.rb index 76d2475..edfc999 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -227,7 +227,7 @@ class Story < ActiveRecord::Base return false end - if self.tags.select{|t| t.privileged? }.any? + if self.taggings.select{|t| t.tag && t.tag.privileged? }.any? return false end From 70f16e4dd3519e3155b298b59f3ed8fb9f126f02 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Mon, 8 May 2017 19:20:35 +0200 Subject: [PATCH 003/106] mail_new_activity: use updated_at, to give time after initial --- script/mail_new_activity | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/mail_new_activity b/script/mail_new_activity index 1214263..a084264 100755 --- a/script/mail_new_activity +++ b/script/mail_new_activity @@ -140,7 +140,7 @@ last_comment_id = (Keystore.value_for(LAST_COMMENT_KEY) || Comment.where("id > ? AND (is_deleted = ? AND is_moderated = ?)", last_comment_id, false, false).order(:id).each do |c| # allow some time for newer comments to be edited before sending them out - if (Time.now - c.created_at) < 2.minutes + if (Time.now - (c.updated_at || c.created_at)) < 2.minutes break end From 05743959cf0b3b99ba40ab93f8b4f8739ce423aa Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 11 May 2017 12:35:33 +0200 Subject: [PATCH 004/106] layout: link to hats (for all) when no hat requests present --- app/views/layouts/application.html.erb | 2 ++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + 3 files changed, 4 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f266060..cece3ac 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -126,6 +126,8 @@ <% if @user && @user.is_moderator? && (hrc = HatRequest.count) > 0 %> <%= t('.hatrequestlink') %> (<%= hrc %>) + <% else %> + <%= t('.hatslink') %> <% end %> <%= t('.privacylink') %> <%= t('.aboutlink') %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d1f558..1e33da8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -64,6 +64,7 @@ en: invitationqueuelink: "Invitation Queue" chatlink: "Chat" hatrequestlink: "Hat Requests" + hatslink: "Hats" privacylink: "Privacy" aboutlink: "About" blog: "Blog" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index bde94fa..b2e45ca 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -76,6 +76,7 @@ fr: invitationqueuelink: "File d'invitation" chatlink: "Chat" hatrequestlink: "Porter le chapeau" + hatslink: "Chapeaux" privacylink: "Confidentialité" aboutlink: "À propos" blog: "Blog" From e8d11e28d733d4f2cd69ee321d0b3da519ccffc3 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 11 May 2017 12:37:38 +0200 Subject: [PATCH 005/106] Comment: cap downvotes to -5, only collapse when it reaches --- app/assets/stylesheets/application.css | 4 ++-- app/models/comment.rb | 5 ++++- app/models/story.rb | 3 +-- app/models/user.rb | 4 ++-- app/views/comments/_comment.html.erb | 7 ++++--- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 0b0868d..46f162b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -476,10 +476,10 @@ li div.details { opacity: 0.7; } .negative_3 { - opacity: 0.5; + opacity: 0.6; } .negative_5 { - opacity: 0.2; + opacity: 0.5; } .comment.highlighted { diff --git a/app/models/comment.rb b/app/models/comment.rb index 92f646f..a1eec60 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -26,6 +26,9 @@ class Comment < ActiveRecord::Base DOWNVOTABLE_DAYS = 7 + # the lowest a score can go, which makes it collapsed by default + DOWNVOTABLE_MIN_SCORE = -5 + # after this many minutes old, a comment cannot be edited MAX_EDIT_MINS = (60 * 6) @@ -321,7 +324,7 @@ class Comment < ActiveRecord::Base end def is_downvotable? - if self.created_at + if self.created_at && self.score >= DOWNVOTABLE_MIN_SCORE Time.now - self.created_at <= DOWNVOTABLE_DAYS.days else false diff --git a/app/models/story.rb b/app/models/story.rb index edfc999..3a85cf7 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -341,8 +341,7 @@ class Story < ActiveRecord::Base end def is_downvotable? - return true - if self.created_at + if self.created_at && self.score >= -5 Time.now - self.created_at <= DOWNVOTABLE_DAYS.days else false diff --git a/app/models/user.rb b/app/models/user.rb index 6fc1a4d..1aa8c02 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,7 +65,7 @@ class User < ActiveRecord::Base MIN_KARMA_TO_SUGGEST = 10 # minimum karma required to be able to downvote comments - MIN_KARMA_TO_DOWNVOTE = 100 + MIN_KARMA_TO_DOWNVOTE = 50 # minimum karma required to be able to submit new stories MIN_KARMA_TO_SUBMIT_STORIES = -4 @@ -176,7 +176,7 @@ class User < ActiveRecord::Base # user can unvote return true end - elsif obj.is_a?(Comment) + elsif obj.is_a?(Comment) && obj.is_downvotable? return !self.is_new? && (self.karma >= MIN_KARMA_TO_DOWNVOTE) end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 49937f7..ee3aee1 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,13 +1,14 @@ > + <%= comment.score <= Comment::DOWNVOTABLE_MIN_SCORE ? "checked" : "" %>>
<%= comment.highlighted ? "highlighted" : "" %> - <%= comment.score <= 0 ? "negative" : "" %> - <%= comment.score <= -1 ? "negative_1" : "" %>"> + <%= comment.score <= -1 ? "negative_1" : "" %> + <%= comment.score <= -3 ? "negative_3" : "" %> + <%= comment.score <= -5 ? "negative_5" : "" %>"> <% if !comment.is_gone? %>
<% if @user %> From b36bc149805c2d6d86acac46f542cd80eca07d2a Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 18:34:22 +0200 Subject: [PATCH 006/106] merged de41a9546514344c3c9415c9e340daf868963134 with i18n --- app/views/users/show.html.erb | 2 +- config/locales/en.yml | 2 +- config/locales/fr.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 025cec0..10ca8ee 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -49,7 +49,7 @@ <%= distance_of_time_in_words(@showing_user.created_at, Time.now) %> <% if @showing_user.invited_by_user %> - <%= t('.byinvitationfrom') %> + <%= raw(t('.byinvitationfrom', :user => @showing_user.username)) %> <%= link_to @showing_user.invited_by_user.try(:username), @showing_user.invited_by_user %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 1e33da8..f153d30 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -362,7 +362,7 @@ en: storysubmissions: "with story submissions" disabled: "disabled" joined: "Joined:" - byinvitationfrom: "by invitation from" + byinvitationfrom: "by invitation from" banneduser: "Banned:" bannedby: "by" hats: "Hats:" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b2e45ca..1695e00 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -372,7 +372,7 @@ fr: storysubmissions: "avec propositions d'infos" disabled: "désactivé" joined: "Inscrit :" - byinvitationfrom: "par invitation de" + byinvitationfrom: "par invitation de" banneduser: "Banni :" bannedby: "par" hats: "Chapeaux :" From 84e125c3285dafc3345e6e9898589fc5ee621288 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 18:38:51 +0200 Subject: [PATCH 007/106] Comment: fix an off-by-one --- app/models/comment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index a1eec60..efc083e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -324,7 +324,7 @@ class Comment < ActiveRecord::Base end def is_downvotable? - if self.created_at && self.score >= DOWNVOTABLE_MIN_SCORE + if self.created_at && self.score > DOWNVOTABLE_MIN_SCORE Time.now - self.created_at <= DOWNVOTABLE_DAYS.days else false From d2a873a4e9c9b98946c5914f1c32ca3ead81ca52 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 18:41:52 +0200 Subject: [PATCH 008/106] when highlighting a comment, force it and its parents to show --- app/controllers/stories_controller.rb | 3 +++ app/views/comments/_comment.html.erb | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0b9a39a..060304a 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -137,10 +137,13 @@ class StoriesController < ApplicationController @comments = @story.merged_comments.includes(:user, :story, :hat).arrange_for_user(@user) + @force_show_thread_id = nil + if params[:comment_short_id] @comments.each do |c,x| if c.short_id == params[:comment_short_id] c.highlighted = true + @force_show_thread_id = c.thread_id break end end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index ee3aee1..38b9055 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,6 +1,8 @@ > + <%= comment.score <= Comment::DOWNVOTABLE_MIN_SCORE && + !(@force_show_thread_id && @force_show_thread_id == comment.thread_id) ? + "checked" : "" %>>
Date: Tue, 16 May 2017 18:58:36 +0200 Subject: [PATCH 010/106] merged 734e28776ga19c8ac9c7c176f857237788d33c71 with i18n --- app/views/stories/_listdetail.html.erb | 4 ++-- extras/story_cacher.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/stories/_listdetail.html.erb b/app/views/stories/_listdetail.html.erb index e62f0b0..ab95ed2 100644 --- a/app/views/stories/_listdetail.html.erb +++ b/app/views/stories/_listdetail.html.erb @@ -163,8 +163,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %> <% end %> <% if story.url.present? %> | - <%= t('.cached') %> + <%= t('.cached') %> <% end %> <% if !story.is_gone? %> diff --git a/extras/story_cacher.rb b/extras/story_cacher.rb index a1f344e..fcacb2e 100644 --- a/extras/story_cacher.rb +++ b/extras/story_cacher.rb @@ -44,7 +44,7 @@ class StoryCacher begin s = Sponge.new s.timeout = 45 - s.fetch("https://archive.is/#{db_url}") + s.fetch("https://web.archive.org/save/#{db_url}") rescue => e Rails.logger.error "error caching #{db_url}: #{e.message}" end From 462080150ff3fcb74b261ba660af2d291bfdb06a Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:30:48 +0200 Subject: [PATCH 011/106] search: catch sphinx errors gracefully and show an error - merged with i18n --- app/controllers/search_controller.rb | 6 +++++- app/models/search.rb | 16 ++++++++-------- config/locales/en.yml | 1 + config/locales/fr.yml | 1 + 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index e29ee00..b515352 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -19,7 +19,11 @@ class SearchController < ApplicationController end if @search.valid? - @search.search_for_user!(@user) + begin + @search.search_for_user!(@user) + rescue ThinkingSph::ConnectionError + flash[:error] = I18n.t 'controllers.search_controller.flasherrorsearchcontroller' + end end end diff --git a/app/models/search.rb b/app/models/search.rb index 3a10a6a..809d568 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -96,14 +96,9 @@ class Search query = Riddle.escape(words) # go go gadget search - self.results = [] - self.total_results = 0 - begin - self.results = ThinkingSphinx.search query, opts - self.total_results = self.results.total_entries - rescue => e - Rails.logger.info "Error from Sphinx: #{e.inspect}" - end + self.total_results = -1 + self.results = ThinkingSphinx.search query, opts + self.total_results = self.results.total_entries if self.page > self.page_count self.page = self.page_count @@ -132,5 +127,10 @@ class Search end end end + + rescue ThinkingSphinx::ConnectionError => e + self.results = [] + self.total_results = -1 + raise e end end diff --git a/config/locales/en.yml b/config/locales/en.yml index f153d30..e3be902 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -439,6 +439,7 @@ en: flashdeletedmessage: "Deleted message." search_controller: searchtitle: "Search" + flasherrorsearchcontroller: "Sorry, but the search engine is currently out of order" stories_controller: submitstorytitle: "Submit Story" editstorytitle: "Edit Story" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 1695e00..3d6c6e9 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -449,6 +449,7 @@ fr: flashdeletedmessage: "Message effacé." search_controller: searchtitle: "Rechercher" + flasherrorsearchcontroller: "Désolé mais le moteur de recherche est actuellement cassé" stories_controller: submitstorytitle: "Soumettre une info" editstorytitle: "Éditer une info" From c7cf9591efa32f36ccaf593a13302e3dab6a08d6 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:33:17 +0200 Subject: [PATCH 012/106] User: sort BANNED_USERNAMES --- app/models/user.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1aa8c02..3bff609 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,9 +54,9 @@ class User < ActiveRecord::Base self.create_mailing_list_token end - BANNED_USERNAMES = [ "admin", "administrator", "hostmaster", "mailer-daemon", - "postmaster", "root", "security", "support", "webmaster", "moderator", - "moderators", "help", "contact", "fraud", "guest", "nobody", ] + BANNED_USERNAMES = [ "admin", "administrator", "contact", "fraud", "guest", + "help", "hostmaster", "mailer-daemon", "moderator", "moderators", "nobody", + "postmaster", "root", "security", "support", "sysop", "webmaster" ] # days old accounts are considered new for NEW_USER_DAYS = 7 From 921efbabe92585f3bc7d4f46b41a5816df65d9c2 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:35:07 +0200 Subject: [PATCH 013/106] Tiny refactor for blue lobsters (#333) --- app/controllers/application_controller.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 26a18ea..77ee039 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -50,13 +50,9 @@ class ApplicationController < ActionController::Base Rails.logger.info " Traffic level: #{@traffic.to_i}" end - if rand(2000000) == 1 - @traffic_color = sprintf("%02x%02x%02x", - 0, 0, [ 255, (@traffic * 7).floor + 50.0 ].min) - else - @traffic_color = sprintf("%02x%02x%02x", - [ 255, (@traffic * 7).floor + 50.0 ].min, 0, 0) - end + intensity = (@traffic * 7).floor + 50.0 + color = rand(2000000) == 1 ? "0000%02x" : "%02x0000" + @traffic_color = sprintf(color, intensity > 255 ? 255 : intensity) true end From 432977c087d52492abb570b94486ee6f1e12d6e3 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:36:52 +0200 Subject: [PATCH 014/106] comments: when showing user threads, always load votes --- app/controllers/comments_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 27fc2a3..144a3f2 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -274,7 +274,7 @@ class CommentsController < ApplicationController comments_by_thread_id = comments.group_by(&:thread_id) @threads = comments_by_thread_id.values_at(*thread_ids).compact - if @user && (@showing_user.id == @user.id) + if @user @votes = Vote.comment_votes_by_user_for_story_hash(@user.id, comments.map(&:story_id).uniq) From 60ec1d625091539b9966ddd65feed1b0422a1787 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:48:01 +0200 Subject: [PATCH 015/106] users: do dumb caching of these big user trees --- app/controllers/users_controller.rb | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4371d25..4bcf869 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -16,11 +16,16 @@ class UsersController < ApplicationController def tree @title = I18n.t 'controllers.users_controller.usertitle' + newest_user = User.last.id + if params[:by].to_s == "karma" - @users = User.order("karma DESC, id ASC").to_a - @user_count = @users.length - @title << " By Karma" - render :action => "list" + render :text => Rails.cache.fetch("users_by_karma_#{newest_user}", + :expires_in => (60 * 60 * 24)) { + @users = User.order("karma DESC, id ASC").to_a + @user_count = @users.length + @title << " By Karma" + render_to_string :action => "list" + } elsif params[:moderators] @users = User.where("is_admin = ? OR is_moderator = ?", true, true). order("id ASC").to_a @@ -28,10 +33,14 @@ class UsersController < ApplicationController @title = "Moderators and Administrators" render :action => "list" else - users = User.order("id DESC").to_a - @user_count = users.length - @users_by_parent = users.group_by(&:invited_by_user_id) - @newest = User.order("id DESC").limit(10) + render :text => Rails.cache.fetch("users_tree_#{newest_user}", + :expires_in => (60 * 60 * 24)) { + users = User.order("id DESC").to_a + @user_count = users.length + @users_by_parent = users.group_by(&:invited_by_user_id) + @newest = User.order("id DESC").limit(10) + render_to_string :action => "tree" + } end end From 29f0b6a944f09599cdb86bf12cbe81414a586af0 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 19:50:36 +0200 Subject: [PATCH 016/106] let spiders crawl all over us --- app/views/layouts/application.html.erb | 1 - public/robots.txt | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cece3ac..1e8c220 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -8,7 +8,6 @@ - <% if @meta_tags %> diff --git a/public/robots.txt b/public/robots.txt index aa94cad..87a97c0 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,19 +1,2 @@ -# block all spiders by default -User-agent: * -Disallow: / - -# but allow major ones -User-agent: Googlebot -Allow: / - -User-agent: Slurp -Allow: / - -User-Agent: msnbot -Disallow: - -User-agent: Baiduspider -Disallow: / - User-agent: * Disallow: /search From 255b8e40b15a6e16a398c81acddebeb16cb3286b Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 23:32:07 +0200 Subject: [PATCH 017/106] revent comment textarea from exceeding width of container - merged with i18n --- app/assets/stylesheets/application.css | 5 +++++ app/views/comments/_commentbox.html.erb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 46f162b..52fef5f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -837,6 +837,11 @@ div.comment_form_container form { max-width: 700px; } +div.comment_form_container textarea { + box-sizing: border-box; + width: 100%; +} + /* trees */ diff --git a/app/views/comments/_commentbox.html.erb b/app/views/comments/_commentbox.html.erb index 65614ab..eb57a42 100644 --- a/app/views/comments/_commentbox.html.erb +++ b/app/views/comments/_commentbox.html.erb @@ -15,7 +15,7 @@ data-shortid="<%= comment.short_id if comment.persisted? %>">
<%= text_area_tag "comment", comment.comment, :rows => 5, - :style => "width: 100%;", :autocomplete => "off", :disabled => !@user, + :autocomplete => "off", :disabled => !@user, :placeholder => (@user ? "" : t('.mustbelogged')) %> From 9050b8e10b65286afbe83ad6b3a53b4080a19109 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 16 May 2017 23:34:41 +0200 Subject: [PATCH 018/106] users: don't cache full page, just inner content --- app/controllers/users_controller.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4bcf869..a20d97b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -19,13 +19,14 @@ class UsersController < ApplicationController newest_user = User.last.id if params[:by].to_s == "karma" - render :text => Rails.cache.fetch("users_by_karma_#{newest_user}", + content = Rails.cache.fetch("users_by_karma_#{newest_user}", :expires_in => (60 * 60 * 24)) { @users = User.order("karma DESC, id ASC").to_a @user_count = @users.length @title << " By Karma" - render_to_string :action => "list" + render_to_string :action => "list", :layout => nil } + render :text => content, :layout => "application" elsif params[:moderators] @users = User.where("is_admin = ? OR is_moderator = ?", true, true). order("id ASC").to_a @@ -33,14 +34,15 @@ class UsersController < ApplicationController @title = "Moderators and Administrators" render :action => "list" else - render :text => Rails.cache.fetch("users_tree_#{newest_user}", + content = Rails.cache.fetch("users_tree_#{newest_user}", :expires_in => (60 * 60 * 24)) { users = User.order("id DESC").to_a @user_count = users.length @users_by_parent = users.group_by(&:invited_by_user_id) @newest = User.order("id DESC").limit(10) - render_to_string :action => "tree" + render_to_string :action => "tree", :layout => nil } + render :text => content, :layout => "application" end end From 82a8a7155a9b92d17c43a297a2d124a8565dc730 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 10:23:25 +0200 Subject: [PATCH 019/106] here be dragons - merged with i18n --- Gemfile | 2 + app/assets/javascripts/application.js.erb | 48 +++++++++++---- app/assets/stylesheets/application.css | 17 ++++++ app/controllers/comments_controller.rb | 20 +++++++ app/controllers/settings_controller.rb | 2 +- app/models/comment.rb | 34 ++++++++++- app/models/user.rb | 14 +++++ app/views/comments/_comment.html.erb | 9 +++ app/views/settings/index.html.erb | 4 ++ app/views/stories/show.html.erb | 18 ++++++ config/locales/en.yml | 4 ++ config/locales/fr.yml | 4 ++ config/routes.rb | 3 + .../20170119172852_move_user_settings.rb | 58 +++++++++++++++++++ db/migrate/20170119192703_add_dragons.rb | 5 ++ db/schema.rb | 54 ++++++++--------- 16 files changed, 257 insertions(+), 39 deletions(-) create mode 100644 db/migrate/20170119172852_move_user_settings.rb create mode 100644 db/migrate/20170119192703_add_dragons.rb diff --git a/Gemfile b/Gemfile index e548957..01b401c 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,8 @@ gem "nokogiri", "= 1.6.1" gem "htmlentities" gem "rdiscount" +gem "activerecord-typedstore" + # for twitter-posting bot gem "oauth" diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 622bab4..2d77117 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -338,6 +338,16 @@ $(document).ready(function() { return Lobsters.moderateStory(this); }), + $(".toggle_dragons").click(function(a) { + var c = $(".dragon_threads").first(); + if (c) { + if (c.hasClass("hidden")) + c.removeClass("hidden"); + else + c.addClass("hidden"); + } + }), + $(document).on("click", "a.comment_replier", function() { if (!Lobsters.curUser) { Lobsters.bounceToLogin(); @@ -408,6 +418,34 @@ $(document).ready(function() { }); } }); + $(document).on("click", "a.comment_undeletor", function() { + if (confirm("Êtes-vous sûr de vouloir restaurer ce commentaire ?")) { + var li = $(this).closest(".comment"); + $.post("/comments/" + $(li).attr("data-shortid") + "/undelete", + function(d) { + $(li).replaceWith(d); + }); + } + }); + + $(document).on("click", "a.comment_dragon", function() { + if (confirm("Êtes-vous sûr de vouloir marquer ce fil de discussion ?")) { + var li = $(this).closest(".comment"); + $.post("/comments/" + $(li).attr("data-shortid") + "/dragon", + function(d) { + window.location.reload(); + }); + } + }); + $(document).on("click", "a.comment_undragon", function() { + if (confirm("Êtes-vous sûr de vouloir supprimer la marque de ce fil de discussion ?")) { + var li = $(this).closest(".comment"); + $.post("/comments/" + $(li).attr("data-shortid") + "/undragon", + function(d) { + window.location.reload(); + }); + } + }); $(document).on("click", "a.comment_moderator", function() { var reason = prompt("Raison de la modération :"); @@ -421,16 +459,6 @@ $(document).ready(function() { }); }); - $(document).on("click", "a.comment_undeletor", function() { - if (confirm("Êtes-vous sûr de vouloir dé-supprimer ce commentaire ?")) { - var li = $(this).closest(".comment"); - $.post("/comments/" + $(li).attr("data-shortid") + "/undelete", - function(d) { - $(li).replaceWith(d); - }); - } - }); - Lobsters.runSelect2(); $(document).on("click", "div.markdown_help_toggler .markdown_help_label", diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 52fef5f..6810ee4 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -737,6 +737,23 @@ div.comment_actions a { text-decoration: none; } +div.dragons { +} +.dragon_text { + background-color: #f8f8f8; + color: gray; + font-style: italic; + margin: 1em; + padding: 2em 0 2.5em 0; + text-align: center; +} +.dragon_text a { + color: gray; +} +.dragon_threads.hidden { + display: none; +} + a.pagelink { border: 1px solid #ddd; background-color: #fbfbfb; diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 144a3f2..67b8a7e 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -128,6 +128,26 @@ class CommentsController < ApplicationController :content_type => "text/html", :locals => { :comment => comment } end + def dragon + if !((comment = find_comment) && @user.is_moderator?) + return render :text => "can't find comment", :status => 400 + end + + comment.become_dragon_for_user(@user) + + render :text => "ok" + end + + def undragon + if !((comment = find_comment) && @user.is_moderator?) + return render :text => "can't find comment", :status => 400 + end + + comment.remove_dragon_for_user(@user) + + render :text => "ok" + end + def update if !((comment = find_comment) && comment.is_editable_by_user?(@user)) return render :text => "can't find comment", :status => 400 diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 317c607..533b255 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -81,7 +81,7 @@ private :email_replies, :email_messages, :email_mentions, :pushover_replies, :pushover_messages, :pushover_mentions, :mailing_list_mode, :show_avatars, :show_story_previews, - :show_submitted_story_threads + :show_submitted_story_threads, :hide_dragons ) end end diff --git a/app/models/comment.rb b/app/models/comment.rb index b18d956..f838d24 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -50,8 +50,8 @@ class Comment < ActiveRecord::Base end def self.arrange_for_user(user) - parents = self.order("(upvotes - downvotes) < 0 ASC, confidence DESC"). - group_by(&:parent_comment_id) + parents = self.order("is_dragon ASC, (upvotes - downvotes) < 0 ASC, " << + "confidence DESC").group_by(&:parent_comment_id) # top-down list of comments, regardless of indent level ordered = [] @@ -168,6 +168,36 @@ class Comment < ActiveRecord::Base end end + def become_dragon_for_user(user) + Comment.record_timestamps = false + + self.is_dragon = true + + m = Moderation.new + m.comment_id = self.id + m.moderator_user_id = user.id + m.action = "turned into a dragon" + m.save + + self.save(:validate => false) + Comment.record_timestamps = true + end + + def remove_dragon_for_user(user) + Comment.record_timestamps = false + + self.is_dragon = false + + m = Moderation.new + m.comment_id = self.id + m.moderator_user_id = user.id + m.action = "slayed dragon" + m.save + + self.save(:validate => false) + Comment.record_timestamps = true + end + # http://evanmiller.org/how-not-to-sort-by-average-rating.html # https://github.com/reddit/reddit/blob/master/r2/r2/lib/db/_sorts.pyx def calculated_confidence diff --git a/app/models/user.rb b/app/models/user.rb index 3bff609..115f365 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -33,6 +33,20 @@ class User < ActiveRecord::Base has_secure_password + typed_store :settings do |s| + s.boolean :email_notifications, :default => false + s.boolean :email_replies, :default => false + s.boolean :pushover_replies, :default => false + s.string :pushover_user_key + s.boolean :email_messages, :default => false + s.boolean :pushover_messages, :default => false + s.boolean :email_mentions, :default => false + s.boolean :show_avatars, :default => true + s.boolean :show_story_previews, :default => false + s.boolean :show_submitted_story_threads, :default => false + s.boolean :hide_dragons, :default => false + end + validates :email, :format => { :with => /\A[^@ ]+@[^@ ]+\.[^@ ]+\Z/ }, :uniqueness => { :case_sensitive => false } diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 38b9055..c4be4ce 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -83,6 +83,15 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <% end %> <% end %> + <% if !comment.parent_comment_id && @user && @user.is_moderator? %> + | + <% if comment.is_dragon? %> + <%= t('.undragon') %> + <% else %> + <%= t('.dragon') %> + <% end %> + <% end %> + <% if @user && !comment.story.is_gone? && !comment.is_gone? %> | <%= t('.reply') %> diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 73d800a..d4ab006 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -193,6 +193,10 @@ <%= f.check_box :show_avatars %>
+
+ <%= f.label :hide_dragons, "Hide Dragons:", :class => "required" %> + <%= f.check_box :hide_dragons %> +

<%= f.submit t('.saveallsettings') %> diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb index d88eb91..efe6449 100644 --- a/app/views/stories/show.html.erb +++ b/app/views/stories/show.html.erb @@ -45,9 +45,22 @@ <% comments_by_parent = @comments.group_by(&:parent_comment_id) %> <% subtree = comments_by_parent[nil] %> <% ancestors = [] %> + <% dragons = false %> <% while subtree %> <% if (comment = subtree.shift) %> + <% if comment.is_dragon? && !dragons %> +
+ +
"> + <% dragons = true %> + <% end %> +
  • <%= render "comments/comment", :comment => comment, :show_story => (comment.story_id != @story.id), @@ -64,5 +77,10 @@
  • <% end %> <% end %> + + <% if dragons %> +
    +
    + <% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e3be902..09632f3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -101,6 +101,8 @@ en: delete: "delete" reply: "reply" about: "on:" + dragon: "dragon" + undragon: "undragon" email_message: notification.text: replyat: "Reply at " @@ -345,6 +347,8 @@ en: preview: "Preview" submit: "Submit a Story" submitbutton: "Submit" + show: + toggledragons: "— here be dragons —" users: list: administrator: "administrator" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3d6c6e9..9e8262b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -103,6 +103,8 @@ fr: delete: "supprimer" reply: "répondre" about: "sur :" + dragon: "dragon" + undragon: "non-dragon" global: markdownhelp: emphasizedtext: "italique" @@ -355,6 +357,8 @@ fr: preview: "Aperçu" submit: "Soumettre une info" submitbutton: "Soumettre" + show: + toggledragons: "— voici les dragons —" users: list: administrator: "administrateur" diff --git a/config/routes.rb b/config/routes.rb index e2320d3..65c07f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,6 +72,9 @@ Lobsters::Application.routes.draw do post "delete" post "undelete" + + post "dragon" + post "undragon" end end get "/comments/page/:page" => "comments#index" diff --git a/db/migrate/20170119172852_move_user_settings.rb b/db/migrate/20170119172852_move_user_settings.rb new file mode 100644 index 0000000..a0a19eb --- /dev/null +++ b/db/migrate/20170119172852_move_user_settings.rb @@ -0,0 +1,58 @@ +class MoveUserSettings < ActiveRecord::Migration + def up + add_column :users, :settings, :text + + [ + :email_notifications, + :email_replies, + :pushover_replies, + :pushover_user_key, + :email_messages, + :pushover_messages, + :email_mentions, + :show_avatars, + :show_story_previews, + :show_submitted_story_threads, + ].each do |col| + rename_column :users, col, "old_#{col}" + end + + User.find_each do |u| + [ + :email_notifications, + :email_replies, + :pushover_replies, + :pushover_user_key, + :email_messages, + :pushover_messages, + :email_mentions, + :show_avatars, + :show_story_previews, + :show_submitted_story_threads, + ].each do |k| + u.settings[k] = u.send("old_#{k}") + end + + u.save(:validate => false) + end + end + + def down + remove_column :users, :settings + + [ + :email_notifications, + :email_replies, + :pushover_replies, + :pushover_user_key, + :email_messages, + :pushover_messages, + :email_mentions, + :show_avatars, + :show_story_previews, + :show_submitted_story_threads, + ].each do |col| + rename_column :users, "old#{col}", col + end + end +end diff --git a/db/migrate/20170119192703_add_dragons.rb b/db/migrate/20170119192703_add_dragons.rb new file mode 100644 index 0000000..ed03dd0 --- /dev/null +++ b/db/migrate/20170119192703_add_dragons.rb @@ -0,0 +1,5 @@ +class AddDragons < ActiveRecord::Migration + def change + add_column :comments, :is_dragon, :boolean, :default => false + end +end diff --git a/db/schema.rb b/db/schema.rb index fe2d5ef..823e889 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160704022756) do +ActiveRecord::Schema.define(version: 20170119192703) do create_table "comments", force: true do |t| t.datetime "created_at", null: false @@ -30,6 +30,7 @@ ActiveRecord::Schema.define(version: 20160704022756) do t.boolean "is_moderated", default: false t.boolean "is_from_email", default: false t.integer "hat_id" + t.boolean "is_dragon", default: false end add_index "comments", ["confidence"], name: "confidence_idx", using: :btree @@ -183,38 +184,39 @@ ActiveRecord::Schema.define(version: 20160704022756) do add_index "tags", ["tag"], name: "tag", unique: true, using: :btree create_table "users", force: true do |t| - t.string "username", limit: 50 - t.string "email", limit: 100 - t.string "password_digest", limit: 75 + t.string "username", limit: 50 + t.string "email", limit: 100 + t.string "password_digest", limit: 75 t.datetime "created_at" - t.boolean "email_notifications", default: false - t.boolean "is_admin", default: false - t.string "password_reset_token", limit: 75 - t.string "session_token", limit: 75, default: "", null: false - t.text "about", limit: 16777215 + t.boolean "old_email_notifications", default: false + t.boolean "is_admin", default: false + t.string "password_reset_token", limit: 75 + t.string "session_token", limit: 75, default: "", null: false + t.text "about", limit: 16777215 t.integer "invited_by_user_id" - t.boolean "email_replies", default: false - t.boolean "pushover_replies", default: false - t.string "pushover_user_key" - t.boolean "email_messages", default: true - t.boolean "pushover_messages", default: true - t.boolean "is_moderator", default: false - t.boolean "email_mentions", default: false - t.boolean "pushover_mentions", default: false - t.string "rss_token", limit: 75 - t.string "mailing_list_token", limit: 75 - t.integer "mailing_list_mode", default: 0 - t.integer "karma", default: 0, null: false + t.boolean "old_email_replies", default: false + t.boolean "old_pushover_replies", default: false + t.string "old_pushover_user_key" + t.boolean "old_email_messages", default: true + t.boolean "old_pushover_messages", default: true + t.boolean "is_moderator", default: false + t.boolean "old_email_mentions", default: false + t.boolean "pushover_mentions", default: false + t.string "rss_token", limit: 75 + t.string "mailing_list_token", limit: 75 + t.integer "mailing_list_mode", default: 0 + t.integer "karma", default: 0, null: false t.datetime "banned_at" t.integer "banned_by_user_id" - t.string "banned_reason", limit: 200 + t.string "banned_reason", limit: 200 t.datetime "deleted_at" - t.boolean "show_avatars", default: false - t.boolean "show_story_previews", default: false - t.boolean "show_submitted_story_threads", default: true + t.boolean "old_show_avatars", default: false + t.boolean "old_show_story_previews", default: false + t.boolean "old_show_submitted_story_threads", default: true t.datetime "disabled_invite_at" t.integer "disabled_invite_by_user_id" - t.string "disabled_invite_reason", limit: 200 + t.string "disabled_invite_reason", limit: 200 + t.text "settings" end add_index "users", ["mailing_list_mode"], name: "mailing_list_enabled", using: :btree From 42a2df9d251d8064bf8e45408078a6b2c27ea415 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 10:47:08 +0200 Subject: [PATCH 020/106] Submit comment with ctrl+enter or cmd+enter --- app/assets/javascripts/application.js.erb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 2d77117..f017dc0 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -512,4 +512,11 @@ $(document).ready(function() { $("#story_guidelines").toggle(); return false; }); + + + $('textarea#comment').keydown(function (e) { + if ((e.metaKey || e.ctrlKey) && e.keyCode == 13) { + $("button.comment-post").click(); + } + }); }); From 0f7a901a55a9209f115f49dd2bd1724a3cedeeab Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 10:56:24 +0200 Subject: [PATCH 021/106] moderations: shorter date/time, don't wrap --- app/views/moderations/index.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/moderations/index.html.erb b/app/views/moderations/index.html.erb index 002384d..83adc4e 100644 --- a/app/views/moderations/index.html.erb +++ b/app/views/moderations/index.html.erb @@ -5,14 +5,14 @@ - + <% bit = 0 %> <% @moderations.each do |mod| %> - +
    <%= t('.datecolumn') %><%= t('.datecolumn') %> <%= t('.moderatorcolumn') %> <%= t('.reasoncolumn') %>
    <%= l mod.created_at %><%= mod.created_at.strftime("%Y-%m-%d %H:%M %z") %> <% if mod.moderator %> <%= mod.moderator.try(:username) %> From 1ac81cbe2a86e565680fc59b1159a5c91483d818 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 11:02:32 +0200 Subject: [PATCH 022/106] missing i18n for dragons --- app/models/comment.rb | 4 ++-- config/locales/en.yml | 2 ++ config/locales/fr.yml | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index f838d24..22110cb 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -176,7 +176,7 @@ class Comment < ActiveRecord::Base m = Moderation.new m.comment_id = self.id m.moderator_user_id = user.id - m.action = "turned into a dragon" + m.action = I18n.t('models.comment.turnedintodragon') m.save self.save(:validate => false) @@ -191,7 +191,7 @@ class Comment < ActiveRecord::Base m = Moderation.new m.comment_id = self.id m.moderator_user_id = user.id - m.action = "slayed dragon" + m.action = I18n.t('models.comment.slayeddragon') m.save self.save(:validate => false) diff --git a/config/locales/en.yml b/config/locales/en.yml index 09632f3..23104f4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,6 +20,8 @@ en: storyidcannotbeblank: "A story ID cannot be blank." deletedcomment: "deleted comment" threadremovedby: "Thread removed by moderator %{modoname} : %{modreason}" + turnedintodragon: "turned into a dragon" + slayeddragon: "slayed dragon" moderation: storyeditedby: "Your story has been edited by " usersuggestions: "user suggestions" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9e8262b..8d551c2 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -32,6 +32,8 @@ fr: storyidcannotbeblank: "Un ID d'info ne peut pas être vide." deletedcomment: "commentaire supprimé" threadremovedby: "Fil supprimé par le modérateur %{modoname} : %{modreason}" + turnedintodragon: "transformé en dragon" + slayeddragon: "le dragon a été terrassé" moderation: storyeditedby: "Votre info a été éditée par " usersuggestions: "suggestions d'utilisateur" From fd1148dc6bd9f2a990ed2484b2c6925433c7742a Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 11:29:54 +0200 Subject: [PATCH 023/106] add TOTP-based two-factor authentication option --- Gemfile | 2 + app/controllers/login_controller.rb | 37 ++++++++++- app/controllers/settings_controller.rb | 84 ++++++++++++++++++++++++ app/models/user.rb | 15 +++++ app/views/login/twofa.html.erb | 23 +++++++ app/views/settings/index.html.erb | 19 ++++++ app/views/settings/twofa.html.erb | 32 +++++++++ app/views/settings/twofa_enroll.html.erb | 25 +++++++ app/views/settings/twofa_verify.html.erb | 24 +++++++ config/routes.rb | 10 +++ 10 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 app/views/login/twofa.html.erb create mode 100644 app/views/settings/twofa.html.erb create mode 100644 app/views/settings/twofa_enroll.html.erb create mode 100644 app/views/settings/twofa_verify.html.erb diff --git a/Gemfile b/Gemfile index 01b401c..a214e92 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,8 @@ gem "dynamic_form" gem "exception_notification" gem "bcrypt", "~> 3.1.2" +gem "rotp" +gem "rqrcode" gem "nokogiri", "= 1.6.1" gem "htmlentities" diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 2ee683f..1e2a9d4 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -41,13 +41,18 @@ class LoginController < ApplicationController "unmoderated comments have been undeleted." end - session[:u] = user.session_token - if !user.password_digest.to_s.match(/^\$2a\$#{BCrypt::Engine::DEFAULT_COST}\$/) user.password = user.password_confirmation = params[:password].to_s user.save! end + if user.has_2fa? + session[:twofa_u] = user.session_token + return redirect_to "/login/2fa" + end + + session[:u] = user.session_token + if (rd = session[:redirect_to]).present? session.delete(:redirect_to) return redirect_to rd @@ -126,4 +131,32 @@ class LoginController < ApplicationController return redirect_to forgot_password_path end end + + def twofa + if tmpu = find_twofa_user + Rails.logger.info " Authenticated as user #{tmpu.id} " << + "(#{tmpu.username}), verifying TOTP" + else + reset_session + return redirect_to "/login" + end + end + + def twofa_verify + if (tmpu = find_twofa_user) && tmpu.authenticate_totp(params[:totp_code]) + session[:u] = tmpu.session_token + session.delete(:twofa_u) + return redirect_to "/" + else + flash[:error] = "Your TOTP code did not match. Please try again." + return redirect_to "/login/2fa" + end + end + +private + def find_twofa_user + if session[:twofa_u].present? + return User.where(:session_token => session[:twofa_u]).first + end + end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 533b255..c7726c7 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -1,6 +1,8 @@ class SettingsController < ApplicationController before_filter :require_logged_in_user + TOTP_SESSION_TIMEOUT = (60 * 15) + def index @title = t('.accountsettings') @@ -73,6 +75,88 @@ class SettingsController < ApplicationController render :action => "index" end + def twofa + @title = "Two-Factor Authentication" + end + + def twofa_auth + if @user.authenticate(params[:user][:password].to_s) + session[:last_authed] = Time.now.to_i + session.delete(:totp_secret) + + if @user.has_2fa? + @user.disable_2fa! + flash[:success] = "Two-Factor Authentication has been disabled." + return redirect_to "/settings" + else + return redirect_to twofa_enroll_url + end + else + flash[:error] = "Your password was not correct." + return redirect_to twofa_url + end + end + + def twofa_enroll + @title = "Two-Factor Authentication" + + if (Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT + flash[:error] = "Your enrollment period timed out." + return redirect_to twofa_url + end + + if !session[:totp_secret] + session[:totp_secret] = ROTP::Base32.random_base32 + end + + totp = ROTP::TOTP.new(session[:totp_secret], + :issuer => Rails.application.name) + totp_url = totp.provisioning_uri(@user.email) + + # no option for inline svg, so just strip off leading tag + qrcode = RQRCode::QRCode.new(totp_url) + qr = qrcode.as_svg(:offset => 0, color: "000", :module_size => 5, + :shape_rendering => "crispEdges").gsub(/^<\?xml.*>/, "") + + @qr_svg = "#{qr}" + end + + def twofa_verify + @title = "Two-Factor Authentication" + + if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) || + !session[:totp_secret] + flash[:error] = "Your enrollment period timed out." + return redirect_to twofa_url + end + end + + def twofa_update + if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) || + !session[:totp_secret] + flash[:error] = "Your enrollment period timed out." + return redirect_to twofa_url + end + + @user.totp_secret = session[:totp_secret] + if @user.authenticate_totp(params[:totp_code]) + # re-roll, just in case + @user.session_token = nil + @user.save! + + session[:u] = @user.session_token + + flash[:success] = "Two-Factor Authentication has been enabled on " << + "your account." + session.delete(:totp_secret) + return redirect_to "/settings" + else + flash[:error] = "Your TOTP code was invalid, please verify the " << + "current code in your TOTP application." + return redirect_to twofa_verify_url + end + end + private def user_params diff --git a/app/models/user.rb b/app/models/user.rb index 115f365..6d1d348 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,6 +45,7 @@ class User < ActiveRecord::Base s.boolean :show_story_previews, :default => false s.boolean :show_submitted_story_threads, :default => false s.boolean :hide_dragons, :default => false + s.string :totp_secret end validates :email, :format => { :with => /\A[^@ ]+@[^@ ]+\.[^@ ]+\Z/ }, @@ -116,6 +117,11 @@ class User < ActiveRecord::Base h end + def authenticate_totp(code) + totp = ROTP::TOTP.new(self.totp_secret) + totp.verify(code) + end + def avatar_url(size = 100) "https://secure.gravatar.com/avatar/" + Digest::MD5.hexdigest(self.email.strip.downcase) + @@ -276,6 +282,11 @@ class User < ActiveRecord::Base end end + def disable_2fa! + self.totp_secret = nil + self.save! + end + def grant_moderatorship_by_user!(user) User.transaction do self.is_moderator = true @@ -304,6 +315,10 @@ class User < ActiveRecord::Base PasswordReset.password_reset_link(self, ip).deliver end + def has_2fa? + self.totp_secret.present? + end + def is_active? !(deleted_at? || is_banned?) end diff --git a/app/views/login/twofa.html.erb b/app/views/login/twofa.html.erb new file mode 100644 index 0000000..c069579 --- /dev/null +++ b/app/views/login/twofa.html.erb @@ -0,0 +1,23 @@ +
    +
    + Login - Two Factor Authentication +
    + + <%= form_tag twofa_login_url do %> +

    + Enter the current TOTP code from your TOTP application: +

    + +

    + <%= label_tag :totp_code, "TOTP Code:" %> + <%= text_field_tag :totp_code, "", :size => 10, :type => "number", + :autofocus => "autofocus" %> +
    +

    + +

    + <%= submit_tag "Login" %> +

    + <% end %> +
    + diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index d4ab006..d12c181 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -62,6 +62,25 @@
    +
    + Security Settings +
    + +
    + <%= f.label :twofa, "Two-Factor Auth:", :class => "required" %> + + <% if @edit_user.totp_secret.present? %> + + Enabled + (Disable) + <% else %> + Disabled (Enroll) + <% end %> + +
    + +
    +
    <%= t('.notificationsettings') %>
    diff --git a/app/views/settings/twofa.html.erb b/app/views/settings/twofa.html.erb new file mode 100644 index 0000000..66fb958 --- /dev/null +++ b/app/views/settings/twofa.html.erb @@ -0,0 +1,32 @@ +
    + +
    + <%= @title %> +
    + + <%= form_for @user, :url => twofa_auth_url, :method => :post do |f| %> +

    + <% if @user.has_2fa? %> + To turn off two-factor authentication for your account, enter your + current password: + <% else %> + To begin the two-factor authentication enrollment for your account, + enter your current password: + <% end %> +

    + +
    + <%= f.label :password, "Current Password:", :class => "required" %> + <%= f.password_field :password, :size => 40, :autocomplete => "off" %> +
    + +

    + <% if @user.has_2fa? %> + <%= submit_tag "Disable Two-Factor Authentication" %> + <% else %> + <%= submit_tag "Continue" %> + <% end %> + <% end %> +

    diff --git a/app/views/settings/twofa_enroll.html.erb b/app/views/settings/twofa_enroll.html.erb new file mode 100644 index 0000000..b505190 --- /dev/null +++ b/app/views/settings/twofa_enroll.html.erb @@ -0,0 +1,25 @@ +
    + +
    + <%= @title %> +
    + +

    + Scan the QR code below or click on it to open in your TOTP application of choice: +

    + + <%= raw @qr_svg %> + +

    + Once you have finished registering with your TOTP application, proceed to + the next screen to verify your current TOTP code and actually enable + Two-Factor Authentication on your account. +

    + +

    + <%= button_to "Verify and Enable", twofa_verify_url, :method => :get %> +

    diff --git a/app/views/settings/twofa_verify.html.erb b/app/views/settings/twofa_verify.html.erb new file mode 100644 index 0000000..db6ae10 --- /dev/null +++ b/app/views/settings/twofa_verify.html.erb @@ -0,0 +1,24 @@ +
    + +
    + <%= @title %> +
    + + <%= form_tag twofa_update_url do %> +

    + To enable Two-Factor Authentication on your account using your new TOTP + secret, enter the six-digit code from your TOTP application: +

    + +
    + <%= label_tag :totp_code, "TOTP Code:", :class => "required" %> + <%= text_field_tag :totp_code, "", :size => 10, :autocomplete => "off", + :type => "number", :autofocus => "autofocus" %> +
    + +

    + <%= submit_tag "Verify and Enable" %> + <% end %> +

    diff --git a/config/routes.rb b/config/routes.rb index 65c07f0..972e4d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,8 @@ Lobsters::Application.routes.draw do get "/login" => "login#index" post "/login" => "login#login" post "/logout" => "login#logout" + get "/login/2fa" => "login#twofa" + post "/login/2fa_verify" => "login#twofa_verify", :as => "twofa_login" get "/signup" => "signup#index" post "/signup" => "signup#signup" @@ -107,6 +109,14 @@ Lobsters::Application.routes.draw do get "/settings/pushover_callback" => "settings#pushover_callback" post "/settings/delete_account" => "settings#delete_account", :as => "delete_account" + get "/settings/2fa" => "settings#twofa", :as => "twofa" + post "/settings/2fa_auth" => "settings#twofa_auth", :as => "twofa_auth" + get "/settings/2fa_enroll" => "settings#twofa_enroll", + :as => "twofa_enroll" + get "/settings/2fa_verify" => "settings#twofa_verify", + :as => "twofa_verify" + post "/settings/2fa_update" => "settings#twofa_update", + :as => "twofa_update" get "/filters" => "filters#index" post "/filters" => "filters#update" From 71acdd69cceb3390f73ecd7e33501ff6607e3a33 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 16:43:49 +0200 Subject: [PATCH 024/106] i18n of 2fa settings --- app/views/settings/index.html.erb | 10 +++++----- config/locales/en.yml | 6 ++++++ config/locales/fr.yml | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index d12c181..e009387 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -63,18 +63,18 @@
    - Security Settings + <%= t('.securitysettings') %>
    - <%= f.label :twofa, "Two-Factor Auth:", :class => "required" %> + <%= f.label :twofa, t('.twofactorauth'), :class => "required" %> <% if @edit_user.totp_secret.present? %> - Enabled - (Disable) + <%= t('.enrolled2fa') %> + (<%= t('.disable2fa') %>) <% else %> - Disabled (Enroll) + <%= t('.disabled2fa') %> (<%= t('.enroll2fa') %>) <% end %>
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 23104f4..d5862ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -279,6 +279,12 @@ en: deleteaccounttext: "To permanently delete your account, verify your current password below. Your account will be put into a deleted state, your comments will be marked as deleted and no longer readable by any other users, and your private messages will be deleted. Your submitted stories will not be deleted. Your username will remain reserved and will not be available to be used on any other account." verifypassword: "Verify Password:" deleteaccountconfirmation: "Yes, Delete My Account" + securitysettings: "Security Settings" + twofactorauth: "Two-Factor Auth:" + disable2fa: "Disable" + enroll2fa: "Enroll" + disabled2fa: "Disabled" + enabled2fa: "Enabled" pushover: pushovernotconfigured: "This site is not configured for Pushover" pushover_callback: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 8d551c2..f2ba831 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -289,6 +289,12 @@ fr: deleteaccounttext: "Pour supprimer définitivement votre compte, vérifiez votre mot de passe actuel ci-dessous. Votre compte sera passé à l'état supprimé, vos commentaires seront marqués comme supprimés et ne seront plus lisibles par les autres utilisateurs, vos messages privés seront également supprimés. Vos infos proposées ne seront pas supprimées. Votre nom d'utilisateur restera réservé et ne sera pas disponible pour un autre compte." verifypassword: "Vérification du mot de passe :" deleteaccountconfirmation: "Oui, supprimez mon compte" + securitysettings: "Paramètres de sécurité" + twofactorauth: "Authentification à deux facteurs :" + disable2fa: "Désactiver" + enroll2fa: "Activer" + disabled2fa: "Désactivé" + enabled2fa: "Activé" pushover: pushovernotconfigured: "Ce site n'est pas configuré pour le Pushover" pushover_callback: From 787a82c88bfe1337a57e8fad8cd9cbc028639aa1 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 18:13:24 +0200 Subject: [PATCH 025/106] i18n of 2FA process --- app/controllers/settings_controller.rb | 22 ++++++++--------- app/views/settings/index.html.erb | 2 +- app/views/settings/twofa.html.erb | 14 +++++------ app/views/settings/twofa_enroll.html.erb | 12 +++------ app/views/settings/twofa_verify.html.erb | 5 ++-- config/locales/en.yml | 27 +++++++++++++++++++++ config/locales/fr.yml | 31 ++++++++++++++++++++++-- 7 files changed, 79 insertions(+), 34 deletions(-) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index c7726c7..95c37e9 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -76,7 +76,7 @@ class SettingsController < ApplicationController end def twofa - @title = "Two-Factor Authentication" + @title = t('.title') end def twofa_auth @@ -86,22 +86,22 @@ class SettingsController < ApplicationController if @user.has_2fa? @user.disable_2fa! - flash[:success] = "Two-Factor Authentication has been disabled." + flash[:success] = t('.2fahasbeendisabled') return redirect_to "/settings" else return redirect_to twofa_enroll_url end else - flash[:error] = "Your password was not correct." + flash[:error] = t('.2fapassnotcorrect') return redirect_to twofa_url end end def twofa_enroll - @title = "Two-Factor Authentication" + @title = t('.title') if (Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT - flash[:error] = "Your enrollment period timed out." + flash[:error] = t('.enrollmenttimeout') return redirect_to twofa_url end @@ -122,11 +122,11 @@ class SettingsController < ApplicationController end def twofa_verify - @title = "Two-Factor Authentication" + @title = t('.title') if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) || !session[:totp_secret] - flash[:error] = "Your enrollment period timed out." + flash[:error] = t('.enrollmenttimeout') return redirect_to twofa_url end end @@ -134,7 +134,7 @@ class SettingsController < ApplicationController def twofa_update if ((Time.now.to_i - session[:last_authed].to_i) > TOTP_SESSION_TIMEOUT) || !session[:totp_secret] - flash[:error] = "Your enrollment period timed out." + flash[:error] = t('.enrollmenttimeout') return redirect_to twofa_url end @@ -146,13 +146,11 @@ class SettingsController < ApplicationController session[:u] = @user.session_token - flash[:success] = "Two-Factor Authentication has been enabled on " << - "your account." + flash[:success] = t('.2fahasbeenenabled') session.delete(:totp_secret) return redirect_to "/settings" else - flash[:error] = "Your TOTP code was invalid, please verify the " << - "current code in your TOTP application." + flash[:error] = t('.totpinvalid') return redirect_to twofa_verify_url end end diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index e009387..c907ffd 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -71,7 +71,7 @@ <% if @edit_user.totp_secret.present? %> - <%= t('.enrolled2fa') %> + <%= t('.enabled2fa') %> (<%= t('.disable2fa') %>) <% else %> <%= t('.disabled2fa') %> (<%= t('.enroll2fa') %>) diff --git a/app/views/settings/twofa.html.erb b/app/views/settings/twofa.html.erb index 66fb958..f687429 100644 --- a/app/views/settings/twofa.html.erb +++ b/app/views/settings/twofa.html.erb @@ -1,6 +1,6 @@
    <%= @title %> @@ -9,24 +9,22 @@ <%= form_for @user, :url => twofa_auth_url, :method => :post do |f| %>

    <% if @user.has_2fa? %> - To turn off two-factor authentication for your account, enter your - current password: + <%= t('.turnoff') %> <% else %> - To begin the two-factor authentication enrollment for your account, - enter your current password: + <%= t('.turnon') %> <% end %>

    - <%= f.label :password, "Current Password:", :class => "required" %> + <%= f.label :password, t('.currentpassword'), :class => "required" %> <%= f.password_field :password, :size => 40, :autocomplete => "off" %>

    <% if @user.has_2fa? %> - <%= submit_tag "Disable Two-Factor Authentication" %> + <%= submit_tag t('.disable2fa') %> <% else %> - <%= submit_tag "Continue" %> + <%= submit_tag t('.continue') %> <% end %> <% end %>

    diff --git a/app/views/settings/twofa_enroll.html.erb b/app/views/settings/twofa_enroll.html.erb index b505190..2a49355 100644 --- a/app/views/settings/twofa_enroll.html.erb +++ b/app/views/settings/twofa_enroll.html.erb @@ -1,25 +1,21 @@
    <%= @title %>

    - Scan the QR code below or click on it to open in your TOTP application of choice: + <%= raw(t('.scanqrcode')) %>

    <%= raw @qr_svg %>

    - Once you have finished registering with your TOTP application, proceed to - the next screen to verify your current TOTP code and actually enable - Two-Factor Authentication on your account. + <%= t('.registring') %>

    - <%= button_to "Verify and Enable", twofa_verify_url, :method => :get %> + <%= button_to t('.verifyenable'), twofa_verify_url, :method => :get %>

    diff --git a/app/views/settings/twofa_verify.html.erb b/app/views/settings/twofa_verify.html.erb index db6ae10..4e45f17 100644 --- a/app/views/settings/twofa_verify.html.erb +++ b/app/views/settings/twofa_verify.html.erb @@ -8,8 +8,7 @@ <%= form_tag twofa_update_url do %>

    - To enable Two-Factor Authentication on your account using your new TOTP - secret, enter the six-digit code from your TOTP application: + <%= t('.enablecode') %>

    @@ -19,6 +18,6 @@

    - <%= submit_tag "Verify and Enable" %> + <%= submit_tag t('.verifyenable') %> <% end %>

    diff --git a/config/locales/en.yml b/config/locales/en.yml index d5862ee..91c0e32 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -292,6 +292,33 @@ en: pushovernorandomtokenurl: "No random token present in URL" accountsetuppushover: "Your account is now setup for Pushover notifications." accountnolongersetuppushover: "Your account is no longer setup for Pushover notifications." + twofa: + title: "Two-Factor Authentication" + backtosettings: "Back to Settings" + disable2fa: "Disable Two-Factor Authentication" + continue: "Continue" + currentpassword: "Current Password:" + turnoff: "To turn off two-factor authentication for your account, enter your current password:" + turnon: "To begin the two-factor authentication enrollment for your account, enter your current password:" + twofa_auth: + 2fahasbeendisabled: "Two-Factor Authentication has been disabled on your account." + 2fapassnotcorrect: "Your password was not correct." + twofa_enroll: + title: "Two-Factor Authentication" + backtosettings: "Back to Settings" + enrollmenttimeout: "Your enrollment period timed out." + scanqrcode: "Scan the QR code below or click on it to open in your TOTP application of choice:" + registring: "Once you have finished registering with your TOTP application, proceed to the next screen to verify your current TOTP code and actually enable Two-Factor Authentication on your account." + verifyenable: "Verify and Enable" + twofa_update: + enrollmenttimeout: "Your enrollment period timed out." + 2fahasbeenenabled: "Two-Factor Authentication has been enabled on your account." + totpinvalid: "Your TOTP code was invalid, please verify the current code in your TOTP application." + twofa_verify: + title: "Two-Factor Authentication" + enrollmenttimeout: "Your enrollment period timed out." + enablecode: "To enable Two-Factor Authentication on your account using your new TOTP secret, enter the six-digit code from your TOTP application:" + verifyenable: "Verify and Enable" update: updatesettingsflash: "Successfully updated settings." stories: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f2ba831..f755aac 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -293,8 +293,8 @@ fr: twofactorauth: "Authentification à deux facteurs :" disable2fa: "Désactiver" enroll2fa: "Activer" - disabled2fa: "Désactivé" - enabled2fa: "Activé" + disabled2fa: "Désactivée" + enabled2fa: "Activée" pushover: pushovernotconfigured: "Ce site n'est pas configuré pour le Pushover" pushover_callback: @@ -302,6 +302,33 @@ fr: pushovernorandomtokenurl: "Pas de jeton alétoire présent dans l'url" accountsetuppushover: "Votre compte est maintenant configuré pour les notifications Pushover." accountnolongersetuppushover: "Votre compte n'est plus configuré pour les notifications Pushover." + twofa: + title: "Authentification à deux facteurs" + backtosettings: "Retour aux paramètres" + disable2fa: "Désactiver l'authentification à deux facteurs" + continue: "Continuer" + currentpassword: "Mot de passe actuel :" + turnoff: "Pour désactiver l'authentification à deux facteurs, entrez votre mot de passe actuel :" + turnon: "Pour activer l'authentification à deux facteurs pour votre compte, entrez votre mot de passe actuel :" + twofa_auth: + 2fahasbeendisabled: "L'authentification à deux facteurs a été désactivée sur votre compte" + 2fapassnotcorrect: "Mot de passe incorrect" + twofa_enroll: + title: "Authentification à deux facteurs" + backtosettings: "Retour aux paramètres" + enrollmenttimeout: "Le délai d'attente est dépassé" + scanqrcode: "Scannez le QR code ci-dessous ou cliquez dessus pour l'ouvrir dans l'application TOTP de votre choix :" + registring: "Une fois le processus d'enregistrement de vos applications TOTP achevé, passez à l'écran suivant pour vérifier et activer l'authentification à deux facteurs pour votre compte." + verifyenable: "Vérifier et Activer" + twofa_update: + enrollmenttimeout: "Le délai d'attente est dépassé" + 2fahasbeenenabled: "L'authentification à deux facteurs a été activée sur votre compte" + totpinvalid: "Votre code TOTP est invalide, merci de vérifier le code actuellement affiché par votre application TOTP" + twofa_verify: + title: "Authentification à deux facteurs" + enrollmenttimeout: "Le délai d'attente est dépassé" + enablecode: "Afin d'activer l'authentification à deux facteurs pour votre compte en utilisant votre nouveau secret TOTP, entrez le code à 6 chiffres de votre application :" + verifyenable: "Vérifier et Activer" update: updatesettingsflash: "Paramètres mis à jour avec succès." stories: From ae60d827c69dfd491e333caf494887adcf32d21f Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 18:34:19 +0200 Subject: [PATCH 026/106] i18n of 2FA login --- app/controllers/login_controller.rb | 2 +- app/views/login/twofa.html.erb | 8 ++++---- config/locales/en.yml | 7 +++++++ config/locales/fr.yml | 7 +++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 1e2a9d4..ec4f6f9 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -148,7 +148,7 @@ class LoginController < ApplicationController session.delete(:twofa_u) return redirect_to "/" else - flash[:error] = "Your TOTP code did not match. Please try again." + flash[:error] = t('.totpcodenotmatch') return redirect_to "/login/2fa" end end diff --git a/app/views/login/twofa.html.erb b/app/views/login/twofa.html.erb index c069579..07cc840 100644 --- a/app/views/login/twofa.html.erb +++ b/app/views/login/twofa.html.erb @@ -1,22 +1,22 @@
    - Login - Two Factor Authentication + <%= t('.login2fa') %>
    <%= form_tag twofa_login_url do %>

    - Enter the current TOTP code from your TOTP application: + <%= t('.logintotpcode') %>

    - <%= label_tag :totp_code, "TOTP Code:" %> + <%= label_tag :totp_code, t('.totpcode') %> <%= text_field_tag :totp_code, "", :size => 10, :type => "number", :autofocus => "autofocus" %>

    - <%= submit_tag "Login" %> + <%= submit_tag t('.loginbutton') %>

    <% end %>
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 91c0e32..afeb79d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -174,6 +174,13 @@ en: password: "New Password:" again: "(Again):" setpassbutton: "Set New Password" + twofa: + login2fa: "Login - Two Factor Authentication" + logintotpcode: "Enter the current TOTP code from your TOTP application:" + loginbutton: "Login" + totpcode: "TOTP Code:" + twofa_verify: + totpcodenotmatch: "Your TOTP code did not match. Please try again." messages: index: viewreceived: "View Received" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f755aac..2866e08 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -174,6 +174,13 @@ fr: password: "Mot de passe :" again: "(encore):" setpassbutton: "Changer le mot de passe" + twofa: + login2fa: "Identification par authentification à deux facteurs" + logintotpcode: "Entrez le code TOTP affiché par votre application :" + loginbutton: "S'identifier" + totpcode: "Code TOTP :" + twofa_verify: + totpcodenotmatch: "Code TOTP incorrect. Merci de ré-essayer." filters: index: filteredtags: "Marques filtrées" From de768ed620ad9023c0c00e95e104fc24e8a9df0e Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 18:35:45 +0200 Subject: [PATCH 027/106] word-wrap: break-word on comments and story text --- app/assets/stylesheets/application.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6810ee4..67747c6 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -675,6 +675,7 @@ div.story_text { margin-bottom: 1.5em; max-width: 700px; line-height: 1.4em; + word-wrap: break-word; } div.story_text p { margin: 0.75em 0; @@ -693,6 +694,7 @@ a#story_text_expander { div.comment_text { max-width: 700px; + word-wrap: break-word; } div.comment_text blockquote, From 8579be4d398031c65c2274c6901e9341182e6342 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 18:49:24 +0200 Subject: [PATCH 028/106] Hide number input spinner buttons for TOTP inputs - merged with i18n --- app/assets/stylesheets/application.css | 6 ++++++ app/views/login/twofa.html.erb | 4 ++-- app/views/settings/twofa_verify.html.erb | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 67747c6..6fa27eb 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -232,6 +232,12 @@ button:disabled { color: gray; } +.totp_code::-webkit-inner-spin-button, +.totp_code::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + /* outliners */ diff --git a/app/views/login/twofa.html.erb b/app/views/login/twofa.html.erb index 07cc840..4161258 100644 --- a/app/views/login/twofa.html.erb +++ b/app/views/login/twofa.html.erb @@ -10,8 +10,8 @@

    <%= label_tag :totp_code, t('.totpcode') %> - <%= text_field_tag :totp_code, "", :size => 10, :type => "number", - :autofocus => "autofocus" %> + <%= number_field_tag :totp_code, "", :size => 10, :autocomplete => "off", + :autofocus => true, :class => "totp_code" %>

    diff --git a/app/views/settings/twofa_verify.html.erb b/app/views/settings/twofa_verify.html.erb index 4e45f17..d281e3e 100644 --- a/app/views/settings/twofa_verify.html.erb +++ b/app/views/settings/twofa_verify.html.erb @@ -13,8 +13,8 @@
    <%= label_tag :totp_code, "TOTP Code:", :class => "required" %> - <%= text_field_tag :totp_code, "", :size => 10, :autocomplete => "off", - :type => "number", :autofocus => "autofocus" %> + <%= number_field_tag :totp_code, "", :size => 10, :autocomplete => "off", + :autofocus => true, :class => "totp_code" %>

    From 7975da374f2ba489298b565ae811e00735a10f07 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 19:11:54 +0200 Subject: [PATCH 029/106] step two of settings migration: delete old columns --- .../20170225201811_delete_old_settings.rb | 18 ++++++++ db/schema.rb | 42 +++++++------------ 2 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20170225201811_delete_old_settings.rb diff --git a/db/migrate/20170225201811_delete_old_settings.rb b/db/migrate/20170225201811_delete_old_settings.rb new file mode 100644 index 0000000..05d51ef --- /dev/null +++ b/db/migrate/20170225201811_delete_old_settings.rb @@ -0,0 +1,18 @@ +class DeleteOldSettings < ActiveRecord::Migration + def change + [ + :email_notifications, + :email_replies, + :pushover_replies, + :pushover_user_key, + :email_messages, + :pushover_messages, + :email_mentions, + :show_avatars, + :show_story_previews, + :show_submitted_story_threads, + ].each do |col| + remove_column :users, "old_#{col}" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 823e889..279296e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170119192703) do +ActiveRecord::Schema.define(version: 20170225201811) do create_table "comments", force: true do |t| t.datetime "created_at", null: false @@ -184,38 +184,28 @@ ActiveRecord::Schema.define(version: 20170119192703) do add_index "tags", ["tag"], name: "tag", unique: true, using: :btree create_table "users", force: true do |t| - t.string "username", limit: 50 - t.string "email", limit: 100 - t.string "password_digest", limit: 75 + t.string "username", limit: 50 + t.string "email", limit: 100 + t.string "password_digest", limit: 75 t.datetime "created_at" - t.boolean "old_email_notifications", default: false - t.boolean "is_admin", default: false - t.string "password_reset_token", limit: 75 - t.string "session_token", limit: 75, default: "", null: false - t.text "about", limit: 16777215 + t.boolean "is_admin", default: false + t.string "password_reset_token", limit: 75 + t.string "session_token", limit: 75, default: "", null: false + t.text "about", limit: 16777215 t.integer "invited_by_user_id" - t.boolean "old_email_replies", default: false - t.boolean "old_pushover_replies", default: false - t.string "old_pushover_user_key" - t.boolean "old_email_messages", default: true - t.boolean "old_pushover_messages", default: true - t.boolean "is_moderator", default: false - t.boolean "old_email_mentions", default: false - t.boolean "pushover_mentions", default: false - t.string "rss_token", limit: 75 - t.string "mailing_list_token", limit: 75 - t.integer "mailing_list_mode", default: 0 - t.integer "karma", default: 0, null: false + t.boolean "is_moderator", default: false + t.boolean "pushover_mentions", default: false + t.string "rss_token", limit: 75 + t.string "mailing_list_token", limit: 75 + t.integer "mailing_list_mode", default: 0 + t.integer "karma", default: 0, null: false t.datetime "banned_at" t.integer "banned_by_user_id" - t.string "banned_reason", limit: 200 + t.string "banned_reason", limit: 200 t.datetime "deleted_at" - t.boolean "old_show_avatars", default: false - t.boolean "old_show_story_previews", default: false - t.boolean "old_show_submitted_story_threads", default: true t.datetime "disabled_invite_at" t.integer "disabled_invite_by_user_id" - t.string "disabled_invite_reason", limit: 200 + t.string "disabled_invite_reason", limit: 200 t.text "settings" end From 79964100bb8fae1122541c9b5a7e0939928348cf Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 19:14:01 +0200 Subject: [PATCH 030/106] Story: when calculating hotness, always penalize downvotes --- app/models/story.rb | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/models/story.rb b/app/models/story.rb index 3a85cf7..2d3879c 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -187,15 +187,20 @@ class Story < ActiveRecord::Base def calculated_hotness base = self.tags.map{|t| t.hotness_mod }.sum - # give a story's comment votes some weight (unless the hotness mod is - # negative), but ignore the story submitter's own comments - if base < 0 - cpoints = 0 - else - cpoints = self.comments.where("user_id <> ?", self.user_id). - select(:upvotes, :downvotes).map{|c| c.upvotes + 1 - c.downvotes }. - inject(&:+).to_f * 0.5 - end + # give a story's comment votes some weight, ignoring submitter's comments + cpoints = self.comments. + where("user_id <> ?", self.user_id). + select(:upvotes, :downvotes). + map{|c| + if base < 0 + # in stories already starting out with a bad hotness mod, only look + # at the downvotes to find out if this tire fire needs to be put out + c.downvotes * -0.5 + else + c.upvotes + 1 - c.downvotes + end + }. + inject(&:+).to_f * 0.5 # mix in any stories this one cannibalized cpoints += self.merged_stories.map{|s| s.score }.inject(&:+).to_f From 6b20a3a913c76cd08a795e6f46dad4464448fa06 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 19:21:23 +0200 Subject: [PATCH 031/106] point archive links back to archive.is - merged with i18n --- app/views/stories/_listdetail.html.erb | 4 ++-- extras/story_cacher.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/stories/_listdetail.html.erb b/app/views/stories/_listdetail.html.erb index ab95ed2..e62f0b0 100644 --- a/app/views/stories/_listdetail.html.erb +++ b/app/views/stories/_listdetail.html.erb @@ -163,8 +163,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %> <% end %> <% if story.url.present? %> | - <%= t('.cached') %> + <%= t('.cached') %> <% end %> <% if !story.is_gone? %> diff --git a/extras/story_cacher.rb b/extras/story_cacher.rb index fcacb2e..a1f344e 100644 --- a/extras/story_cacher.rb +++ b/extras/story_cacher.rb @@ -44,7 +44,7 @@ class StoryCacher begin s = Sponge.new s.timeout = 45 - s.fetch("https://web.archive.org/save/#{db_url}") + s.fetch("https://archive.is/#{db_url}") rescue => e Rails.logger.error "error caching #{db_url}: #{e.message}" end From a823a31688e389232a9a752e7ca32c286ce23638 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Wed, 17 May 2017 19:36:41 +0200 Subject: [PATCH 032/106] Require current password on password change - merged with i18n --- app/controllers/settings_controller.rb | 11 ++++++++--- app/views/settings/index.html.erb | 6 ++++++ config/locales/en.yml | 2 ++ config/locales/fr.yml | 2 ++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 95c37e9..0cf0fa1 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -67,9 +67,14 @@ class SettingsController < ApplicationController def update @edit_user = @user.clone - if @edit_user.update_attributes(user_params) - flash.now[:success] = t('.updatesettingsflash') - @user = @edit_user + if params[:user][:password].empty? || + @user.authenticate(params[:current_password].to_s) + if @edit_user.update_attributes(user_params) + flash.now[:success] = t('.updatesettingsflash') + @user = @edit_user + end + else + flash[:error] = t('.passwordnotcorrect') end render :action => "index" diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index c907ffd..eeb426d 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -18,6 +18,12 @@ +

    + <%= label_tag :current_password, t('.currentpassword'), + :class => "required" %> + <%= password_field_tag :current_password, nil, :size => 40 %> +
    +
    <%= f.label :password, t('.password'), :class => "required" %> <%= f.password_field :password, :size => 40, :autocomplete => "off" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index afeb79d..d703d2a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -250,6 +250,7 @@ en: accountsettings: "Account Settings" username: "Username:" password: "New Password:" + currentpassword: "Current Password:" confirmpassword: "Confirm Password:" emailaddress: "E-mail Address:" gravatarized: "Gravatar'ized" @@ -328,6 +329,7 @@ en: verifyenable: "Verify and Enable" update: updatesettingsflash: "Successfully updated settings." + passwordnotcorrect: "Your password was not correct." stories: edit: edit: "Edit Story" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2866e08..a614b76 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -260,6 +260,7 @@ fr: accountsettings: "Paramètres du compte" username: "Utilisateur :" password: "Nouveau mot de passe :" + currentpassword: "Mot de passe actuel :" confirmpassword: "Confirmer le mot de passe :" emailaddress: "Adresse e-mail :" gravatarized: "Gravatarisé" @@ -338,6 +339,7 @@ fr: verifyenable: "Vérifier et Activer" update: updatesettingsflash: "Paramètres mis à jour avec succès." + passwordnotcorrect: "Le mot de passe est incorrect." stories: edit: edit: "Éditer l'info" From 830d0e65866def98cbdfe7d5b147dc46a1b1d029 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 18 May 2017 18:54:28 +0200 Subject: [PATCH 033/106] home: protect against cache accesses and expirations --- app/controllers/home_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 941cb4f..244266d 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -239,7 +239,12 @@ private else key = opts.merge(page: page).sort.map{|k,v| "#{k}=#{v.to_param}" }.join(" ") - Rails.cache.fetch("stories #{key}", :expires_in => 45, &block) + begin + Rails.cache.fetch("stories #{key}", :expires_in => 45, &block) + rescue Errno::ENOENT => e + Rails.logger.error "error fetching stories #{key}: #{e}" + yield + end end end end From a702fae66af2c933ebfd9d0a36853b4eaa79c21a Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 18 May 2017 19:02:21 +0200 Subject: [PATCH 034/106] highlight stories to their authors that have suggestions - merged with i18n --- app/assets/stylesheets/application.css | 3 +++ app/models/story.rb | 4 ++++ app/views/stories/_listdetail.html.erb | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6fa27eb..6ae2772 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -607,6 +607,9 @@ span.user_is_author, a.user_is_author, li .byline a.user_is_author { color: #6081bd; } +li .byline a.story_has_suggestions { + color: #6081bd; +} li.story.hidden { opacity: 0.25; diff --git a/app/models/story.rb b/app/models/story.rb index 2d3879c..d08d64b 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -328,6 +328,10 @@ class Story < ActiveRecord::Base "hotness = '#{self.calculated_hotness}' WHERE id = #{self.id.to_i}") end + def has_suggestions? + self.suggested_taggings.any? || self.suggested_titles.any? + end + def hider_count @hider_count ||= HiddenStory.where(:story_id => self.id).count end diff --git a/app/views/stories/_listdetail.html.erb b/app/views/stories/_listdetail.html.erb index e62f0b0..4267d67 100644 --- a/app/views/stories/_listdetail.html.erb +++ b/app/views/stories/_listdetail.html.erb @@ -119,7 +119,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %> <% if story.is_editable_by_user?(@user) %> | - <%= t('.edit') %> + "><%= t('.edit') %> <% if story.is_gone? && story.is_undeletable_by_user?(@user) %> | From 7f1b18f7bf9f7b28aeb35c1c1e609491c151aa34 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 18 May 2017 19:05:48 +0200 Subject: [PATCH 035/106] tweak error message - merged with i18n --- config/locales/en.yml | 2 +- config/locales/fr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index d703d2a..bc4832c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -329,7 +329,7 @@ en: verifyenable: "Verify and Enable" update: updatesettingsflash: "Successfully updated settings." - passwordnotcorrect: "Your password was not correct." + passwordnotcorrect: "Your current password was not entered correctly." stories: edit: edit: "Edit Story" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a614b76..7aed813 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -339,7 +339,7 @@ fr: verifyenable: "Vérifier et Activer" update: updatesettingsflash: "Paramètres mis à jour avec succès." - passwordnotcorrect: "Le mot de passe est incorrect." + passwordnotcorrect: "Le mot de passe courant n'a pas été entré correctement." stories: edit: edit: "Éditer l'info" From f4b9fe9ff4b7818f80acd490af85ac050cde75ed Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 18 May 2017 19:14:01 +0200 Subject: [PATCH 036/106] Update views with email entry to use email_field - merged with i18n --- app/views/invitations/build.html.erb | 2 +- app/views/login/forgot_password.html.erb | 2 +- app/views/login/index.html.erb | 2 +- app/views/settings/index.html.erb | 2 +- app/views/users/_invitationform.html.erb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/invitations/build.html.erb b/app/views/invitations/build.html.erb index 5d068de..ce03086 100644 --- a/app/views/invitations/build.html.erb +++ b/app/views/invitations/build.html.erb @@ -13,7 +13,7 @@
    <%= f.label :email, t('.buildinvemail') %> - <%= f.text_field :email, :size => 30 %> + <%= f.email_field :email, :size => 30 %>
    <%= f.label :memo, t('.buildinvurl') %> diff --git a/app/views/login/forgot_password.html.erb b/app/views/login/forgot_password.html.erb index 517e8b7..172a703 100644 --- a/app/views/login/forgot_password.html.erb +++ b/app/views/login/forgot_password.html.erb @@ -9,7 +9,7 @@ <%= form_tag reset_password_path do %> <%= label_tag :email, t('.email') %> - <%= text_field_tag :email, "", :size => 30 %> + <%= email_field_tag :email, "", :size => 30 %>

    diff --git a/app/views/login/index.html.erb b/app/views/login/index.html.erb index 4549f64..96d68ca 100644 --- a/app/views/login/index.html.erb +++ b/app/views/login/index.html.erb @@ -6,7 +6,7 @@ <%= form_tag login_path do %>

    <%= label_tag :email, t('.email') %> - <%= text_field_tag :email, "", :size => 30, :autofocus => "autofocus" %> + <%= email_field_tag :email, "", :size => 30, :autofocus => "autofocus" %>
    <%= label_tag :password, t('.password') %> diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index eeb426d..0559ab4 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -38,7 +38,7 @@

    <%= f.label :email, t('.emailaddress'), :class => "required" %> - <%= f.text_field :email, :size => 40 %> + <%= f.email_field :email, :size => 40 %> <%= raw(t('.gravatarized')) %> diff --git a/app/views/users/_invitationform.html.erb b/app/views/users/_invitationform.html.erb index 3e78ec7..5869b96 100644 --- a/app/views/users/_invitationform.html.erb +++ b/app/views/users/_invitationform.html.erb @@ -9,7 +9,7 @@
    <%= label_tag :email, t(:emailaddress), :class => "required" %> - <%= text_field_tag :email, "", :size => 30, :autocomplete => "off" %> + <%= email_field_tag :email, "", :size => 30, :autocomplete => "off" %>
    From bed62ec2457ea10617551f1492cbb603fdf50774 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Thu, 18 May 2017 19:16:56 +0200 Subject: [PATCH 037/106] login can also take a username, switch back to text_field_tag - merged with i18n --- app/views/login/forgot_password.html.erb | 2 +- app/views/login/index.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/login/forgot_password.html.erb b/app/views/login/forgot_password.html.erb index 172a703..517e8b7 100644 --- a/app/views/login/forgot_password.html.erb +++ b/app/views/login/forgot_password.html.erb @@ -9,7 +9,7 @@ <%= form_tag reset_password_path do %> <%= label_tag :email, t('.email') %> - <%= email_field_tag :email, "", :size => 30 %> + <%= text_field_tag :email, "", :size => 30 %>

    diff --git a/app/views/login/index.html.erb b/app/views/login/index.html.erb index 96d68ca..4549f64 100644 --- a/app/views/login/index.html.erb +++ b/app/views/login/index.html.erb @@ -6,7 +6,7 @@ <%= form_tag login_path do %>

    <%= label_tag :email, t('.email') %> - <%= email_field_tag :email, "", :size => 30, :autofocus => "autofocus" %> + <%= text_field_tag :email, "", :size => 30, :autofocus => "autofocus" %>
    <%= label_tag :password, t('.password') %> From fd25aed94b29921976647f28ab375e0233c28f91 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 10:29:12 +0200 Subject: [PATCH 038/106] update to rails 4.2.8 - merged with i18n --- Gemfile | 4 +- app/models/comment.rb | 4 +- app/models/invitation.rb | 2 +- app/models/invitation_request.rb | 4 +- app/models/message.rb | 2 +- app/models/user.rb | 2 +- config/application.rb | 8 +- config/environments/production.rb | 2 +- config/environments/test.rb | 6 +- db/schema.rb | 153 +++++++++++++++--------------- 10 files changed, 97 insertions(+), 90 deletions(-) diff --git a/Gemfile b/Gemfile index a214e92..e75d1c5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem "rails", "4.1.12" +gem "rails", "4.2.8" gem "unicorn" @@ -37,7 +37,7 @@ gem "oauth" gem "mail" group :test, :development do - gem "rspec-rails", "~> 2.6" + gem "rspec-rails", "~> 3.5", ">= 3.5.2" gem "machinist" gem "sqlite3" gem "faker" diff --git a/app/models/comment.rb b/app/models/comment.rb index 22110cb..4d1029e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -257,7 +257,7 @@ class Comment < ActiveRecord::Base if u.email_mentions? begin - EmailReply.mention(self, u).deliver + EmailReply.mention(self, u).deliver_now rescue => e Rails.logger.error "error e-mailing #{u.email}: #{e}" end @@ -281,7 +281,7 @@ class Comment < ActiveRecord::Base u.id != self.user.id if u.email_replies? begin - EmailReply.reply(self, u).deliver + EmailReply.reply(self, u).deliver_now rescue => e Rails.logger.error "error e-mailing #{u.email}: #{e}" end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 1977364..a246d64 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -24,6 +24,6 @@ class Invitation < ActiveRecord::Base end def send_email - InvitationMailer.invitation(self).deliver + InvitationMailer.invitation(self).deliver_now end end diff --git a/app/models/invitation_request.rb b/app/models/invitation_request.rb index 5abe529..2678a76 100644 --- a/app/models/invitation_request.rb +++ b/app/models/invitation_request.rb @@ -26,8 +26,8 @@ class InvitationRequest < ActiveRecord::Base def markeddown_memo Markdowner.to_html(self.memo) end - + def send_email - InvitationRequestMailer.invitation_request(self).deliver + InvitationRequestMailer.invitation_request(self).deliver_now end end diff --git a/app/models/message.rb b/app/models/message.rb index 1797de7..e71c127 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -49,7 +49,7 @@ class Message < ActiveRecord::Base if self.recipient.email_messages? begin - EmailMessage.notify(self, self.recipient).deliver + EmailMessage.notify(self, self.recipient).deliver_now rescue => e Rails.logger.error "error e-mailing #{self.recipient.email}: #{e}" end diff --git a/app/models/user.rb b/app/models/user.rb index 6d1d348..3a42787 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -312,7 +312,7 @@ class User < ActiveRecord::Base self.password_reset_token = "#{Time.now.to_i}-#{Utils.random_str(30)}" self.save! - PasswordReset.password_reset_link(self, ip).deliver + PasswordReset.password_reset_link(self, ip).deliver_now end def has_2fa? diff --git a/config/application.rb b/config/application.rb index e0b69f0..3c08f65 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,7 +26,13 @@ module Lobsters # Raise an exception when using mass assignment with unpermitted attributes config.action_controller.action_on_unpermitted_parameters = :raise + config.active_record.raise_in_transactional_callbacks = true + config.cache_store = :file_store, "#{config.root}/tmp/cache/" + + config.after_initialize do + require "#{Rails.root}/lib/monkey.rb" + end end end @@ -68,5 +74,3 @@ class << Rails.application true end end - -require "#{Rails.root}/lib/monkey" diff --git a/config/environments/production.rb b/config/environments/production.rb index b4ee2c7..a301912 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -20,7 +20,7 @@ Lobsters::Application.configure do # config.action_dispatch.rack_cache = true # Disable Rails's static asset server (Apache or nginx will already do this). - config.serve_static_assets = false + config.serve_static_files = false # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier diff --git a/config/environments/test.rb b/config/environments/test.rb index e21872a..635a19d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -13,7 +13,7 @@ Lobsters::Application.configure do config.eager_load = false # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_assets = true + config.serve_static_files = true config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. @@ -35,5 +35,7 @@ Lobsters::Application.configure do config.active_support.deprecation = :stderr # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + config.action_view.raise_on_missing_translations = true end + +RSpec::Expectations.configuration.on_potential_false_positives = :nothing diff --git a/db/schema.rb b/db/schema.rb index 279296e..4c45c69 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,23 +13,23 @@ ActiveRecord::Schema.define(version: 20170225201811) do - create_table "comments", force: true do |t| + create_table "comments", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at" t.string "short_id", limit: 10, default: "", null: false - t.integer "story_id", null: false - t.integer "user_id", null: false - t.integer "parent_comment_id" - t.integer "thread_id" + t.integer "story_id", limit: 4, null: false + t.integer "user_id", limit: 4, null: false + t.integer "parent_comment_id", limit: 4 + t.integer "thread_id", limit: 4 t.text "comment", limit: 16777215, null: false - t.integer "upvotes", default: 0, null: false - t.integer "downvotes", default: 0, null: false + t.integer "upvotes", limit: 4, default: 0, null: false + t.integer "downvotes", limit: 4, default: 0, null: false t.decimal "confidence", precision: 20, scale: 19, default: 0.0, null: false t.text "markeddown_comment", limit: 16777215 t.boolean "is_deleted", default: false t.boolean "is_moderated", default: false t.boolean "is_from_email", default: false - t.integer "hat_id" + t.integer "hat_id", limit: 4 t.boolean "is_dragon", default: false end @@ -38,100 +38,101 @@ ActiveRecord::Schema.define(version: 20170225201811) do add_index "comments", ["story_id", "short_id"], name: "story_id_short_id", using: :btree add_index "comments", ["thread_id"], name: "thread_id", using: :btree - create_table "hat_requests", force: true do |t| + create_table "hat_requests", force: :cascade do |t| t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" - t.string "hat" - t.string "link" - t.text "comment" + t.integer "user_id", limit: 4 + t.string "hat", limit: 255 + t.string "link", limit: 255 + t.text "comment", limit: 65535 end - create_table "hats", force: true do |t| + create_table "hats", force: :cascade do |t| t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" - t.integer "granted_by_user_id" - t.string "hat" - t.string "link" + t.integer "user_id", limit: 4 + t.integer "granted_by_user_id", limit: 4 + t.string "hat", limit: 255 + t.string "link", limit: 255 end - create_table "hidden_stories", force: true do |t| - t.integer "user_id" - t.integer "story_id" + create_table "hidden_stories", force: :cascade do |t| + t.integer "user_id", limit: 4 + t.integer "story_id", limit: 4 end add_index "hidden_stories", ["user_id", "story_id"], name: "index_hidden_stories_on_user_id_and_story_id", unique: true, using: :btree - create_table "invitation_requests", force: true do |t| - t.string "code" - t.boolean "is_verified", default: false - t.string "email" - t.string "name" - t.text "memo" - t.string "ip_address" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + create_table "invitation_requests", force: :cascade do |t| + t.string "code", limit: 255 + t.boolean "is_verified", default: false + t.string "email", limit: 255 + t.string "name", limit: 255 + t.text "memo", limit: 65535 + t.string "ip_address", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table "invitations", force: true do |t| - t.integer "user_id" - t.string "email" - t.string "code" + create_table "invitations", force: :cascade do |t| + t.integer "user_id", limit: 4 + t.string "email", limit: 255 + t.string "code", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "memo", limit: 16777215 end - create_table "keystores", id: false, force: true do |t| + create_table "keystores", id: false, force: :cascade do |t| t.string "key", limit: 50, default: "", null: false t.integer "value", limit: 8 end add_index "keystores", ["key"], name: "key", unique: true, using: :btree - create_table "messages", force: true do |t| + create_table "messages", force: :cascade do |t| t.datetime "created_at" - t.integer "author_user_id" - t.integer "recipient_user_id" + t.integer "author_user_id", limit: 4 + t.integer "recipient_user_id", limit: 4 t.boolean "has_been_read", default: false t.string "subject", limit: 100 t.text "body", limit: 16777215 t.string "short_id", limit: 30 t.boolean "deleted_by_author", default: false t.boolean "deleted_by_recipient", default: false + t.integer "hat_id", limit: 4 end add_index "messages", ["short_id"], name: "random_hash", unique: true, using: :btree - create_table "moderations", force: true do |t| + create_table "moderations", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "moderator_user_id" - t.integer "story_id" - t.integer "comment_id" - t.integer "user_id" + t.integer "moderator_user_id", limit: 4 + t.integer "story_id", limit: 4 + t.integer "comment_id", limit: 4 + t.integer "user_id", limit: 4 t.text "action", limit: 16777215 t.text "reason", limit: 16777215 t.boolean "is_from_suggestions", default: false end - create_table "stories", force: true do |t| + create_table "stories", force: :cascade do |t| t.datetime "created_at" - t.integer "user_id" + t.integer "user_id", limit: 4 t.string "url", limit: 250, default: "" t.string "title", limit: 150, default: "", null: false t.text "description", limit: 16777215 t.string "short_id", limit: 6, default: "", null: false t.boolean "is_expired", default: false, null: false - t.integer "upvotes", default: 0, null: false - t.integer "downvotes", default: 0, null: false + t.integer "upvotes", limit: 4, default: 0, null: false + t.integer "downvotes", limit: 4, default: 0, null: false t.boolean "is_moderated", default: false, null: false t.decimal "hotness", precision: 20, scale: 10, default: 0.0, null: false t.text "markeddown_description", limit: 16777215 t.text "story_cache", limit: 16777215 - t.integer "comments_count", default: 0, null: false - t.integer "merged_story_id" + t.integer "comments_count", limit: 4, default: 0, null: false + t.integer "merged_story_id", limit: 4 t.datetime "unavailable_at" t.string "twitter_id", limit: 20 t.boolean "user_is_author", default: false @@ -144,35 +145,35 @@ ActiveRecord::Schema.define(version: 20170225201811) do add_index "stories", ["twitter_id"], name: "index_stories_on_twitter_id", using: :btree add_index "stories", ["url"], name: "url", length: {"url"=>191}, using: :btree - create_table "suggested_taggings", force: true do |t| - t.integer "story_id" - t.integer "tag_id" - t.integer "user_id" + create_table "suggested_taggings", force: :cascade do |t| + t.integer "story_id", limit: 4 + t.integer "tag_id", limit: 4 + t.integer "user_id", limit: 4 end - create_table "suggested_titles", force: true do |t| - t.integer "story_id" - t.integer "user_id" + create_table "suggested_titles", force: :cascade do |t| + t.integer "story_id", limit: 4 + t.integer "user_id", limit: 4 t.string "title", limit: 150, null: false end - create_table "tag_filters", force: true do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "user_id" - t.integer "tag_id" + create_table "tag_filters", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id", limit: 4 + t.integer "tag_id", limit: 4 end add_index "tag_filters", ["user_id", "tag_id"], name: "user_tag_idx", using: :btree - create_table "taggings", force: true do |t| - t.integer "story_id", null: false - t.integer "tag_id", null: false + create_table "taggings", force: :cascade do |t| + t.integer "story_id", limit: 4, null: false + t.integer "tag_id", limit: 4, null: false end add_index "taggings", ["story_id", "tag_id"], name: "story_id_tag_id", unique: true, using: :btree - create_table "tags", force: true do |t| + create_table "tags", force: :cascade do |t| t.string "tag", limit: 25, default: "", null: false t.string "description", limit: 100 t.boolean "privileged", default: false @@ -183,7 +184,7 @@ ActiveRecord::Schema.define(version: 20170225201811) do add_index "tags", ["tag"], name: "tag", unique: true, using: :btree - create_table "users", force: true do |t| + create_table "users", force: :cascade do |t| t.string "username", limit: 50 t.string "email", limit: 100 t.string "password_digest", limit: 75 @@ -192,21 +193,21 @@ ActiveRecord::Schema.define(version: 20170225201811) do t.string "password_reset_token", limit: 75 t.string "session_token", limit: 75, default: "", null: false t.text "about", limit: 16777215 - t.integer "invited_by_user_id" + t.integer "invited_by_user_id", limit: 4 t.boolean "is_moderator", default: false t.boolean "pushover_mentions", default: false t.string "rss_token", limit: 75 t.string "mailing_list_token", limit: 75 - t.integer "mailing_list_mode", default: 0 - t.integer "karma", default: 0, null: false + t.integer "mailing_list_mode", limit: 4, default: 0 + t.integer "karma", limit: 4, default: 0, null: false t.datetime "banned_at" - t.integer "banned_by_user_id" + t.integer "banned_by_user_id", limit: 4 t.string "banned_reason", limit: 200 t.datetime "deleted_at" t.datetime "disabled_invite_at" - t.integer "disabled_invite_by_user_id" + t.integer "disabled_invite_by_user_id", limit: 4 t.string "disabled_invite_reason", limit: 200 - t.text "settings" + t.text "settings", limit: 65535 end add_index "users", ["mailing_list_mode"], name: "mailing_list_enabled", using: :btree @@ -216,10 +217,10 @@ ActiveRecord::Schema.define(version: 20170225201811) do add_index "users", ["session_token"], name: "session_hash", unique: true, using: :btree add_index "users", ["username"], name: "username", unique: true, using: :btree - create_table "votes", force: true do |t| - t.integer "user_id", null: false - t.integer "story_id", null: false - t.integer "comment_id" + create_table "votes", force: :cascade do |t| + t.integer "user_id", limit: 4, null: false + t.integer "story_id", limit: 4, null: false + t.integer "comment_id", limit: 4 t.integer "vote", limit: 1, null: false t.string "reason", limit: 1 end From b347a5f453d080440964bd93ffc681ec70720edc Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 10:30:29 +0200 Subject: [PATCH 039/106] how did these gitkeep files get here... --- app/mailers/.gitkeep | 0 app/models/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/mailers/.gitkeep delete mode 100644 app/models/.gitkeep diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/.gitkeep b/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 From b74b9e3b946891ed4adf3fb8723382eaf47b3c89 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 10:33:56 +0200 Subject: [PATCH 040/106] show more text on hat requests --- app/views/hats/requests_index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/hats/requests_index.html.erb b/app/views/hats/requests_index.html.erb index 155b590..4375382 100644 --- a/app/views/hats/requests_index.html.erb +++ b/app/views/hats/requests_index.html.erb @@ -26,7 +26,7 @@

    <%= f.label :link, "Link:", :class => "required" %> - <%= f.text_field "link", :size => 25 %> + <%= f.text_field "link", :size => 75 %>
    From 2aba955a6834e2192a39677a02678d0d4bfcf09b Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:32:21 +0200 Subject: [PATCH 041/106] i18n of hats --- app/controllers/hats_controller.rb | 6 ++-- app/views/hats/build_request.html.erb | 49 +++++--------------------- app/views/hats/index.html.erb | 15 ++++---- app/views/hats/requests_index.html.erb | 20 +++++------ config/locales/en.yml | 35 ++++++++++++++++++ config/locales/fr.yml | 35 ++++++++++++++++++ 6 files changed, 98 insertions(+), 62 deletions(-) diff --git a/app/controllers/hats_controller.rb b/app/controllers/hats_controller.rb index dd5bd24..d53de81 100644 --- a/app/controllers/hats_controller.rb +++ b/app/controllers/hats_controller.rb @@ -28,7 +28,7 @@ class HatsController < ApplicationController @hat_request.comment = params[:hat_request][:comment] if @hat_request.save - flash[:success] = "Successfully submitted hat request." + flash[:success] = t('.submittedhatrequest') return redirect_to "/hats" end @@ -47,7 +47,7 @@ class HatsController < ApplicationController permit(:hat, :link)) @hat_request.approve_by_user!(@user) - flash[:success] = "Successfully approved hat request." + flash[:success] = t('.approvedhatrequest') return redirect_to "/hats/requests" end @@ -57,7 +57,7 @@ class HatsController < ApplicationController @hat_request.reject_by_user_for_reason!(@user, params[:hat_request][:rejection_comment]) - flash[:success] = "Successfully rejected hat request." + flash[:success] = t('.rejectedhatrequest') return redirect_to "/hats/requests" end diff --git a/app/views/hats/build_request.html.erb b/app/views/hats/build_request.html.erb index 5c5b8a8..10e6273 100644 --- a/app/views/hats/build_request.html.erb +++ b/app/views/hats/build_request.html.erb @@ -1,60 +1,29 @@
    - Request a Hat + <%= t('.title') %>
    -

    - A hat is a formal, verified, way of posting a comment while speaking for a - project, organization, or company. Each user may have multiple hats, one of - which may be worn at any time when posting a comment or sending a private - message. -

    -

    - Hats are intended for core project developers and company employees that are - authorized to make comments on behalf of those organizations, and are usually - reserved for notable projects and organizations. Hats are not intended to - list every contribution a person has made to any software project. -

    -

    - Hats will not be authorized for occasional contributors to projects, - developers of projects which are not widely known, past employees of - companies, or other situations where it cannot be verified that one is - authorized to represent that organization. In general, unless one has an - e-mail address at a compay domain or have commit access to a project, a hat - will not be authorized. -

    -

    - To request a hat for your account, provide a short description of the hat - (e.g., "OpenBSD Developer"), a public link that will be shown when hovering - over the hat that users can see, such as your e-mail address at that project - or company, or a link to a company website showing your employment, and - private comments that will be seen only by moderators during approval. -

    -

    - Once your hat is requested, a moderator will verify your request by e-mailing - the address you submitted as the link, or doing some other manual - verification of project association. -

    + <%= raw(t('.description')) %> <%= form_for @hat_request, :url => create_hat_request_path do |f| %>

    - <%= f.label :hat, "Hat:" %> + <%= f.label :hat, t('.hat') %> <%= f.text_field :hat, :size => 20, - :placeholder => "XYZ Project Member" %> + :placeholder => t('.hatplaceholder') %>
    - <%= f.label :link, "Link:" %> + <%= f.label :link, t('.link') %> <%= f.text_field :link, :size => 50, - :placeholder => "user@project.org, or a URL to an employment page" %> + :placeholder => t('.linkplaceholder') %>
    - <%= f.label :comment, "Comment:" %> + <%= f.label :comment, t('.comment') %> <%= f.text_area :comment, :rows => 4, - :placeholder => "Will only be shown to moderators during approval" %> + :placeholder => t('.commentplaceholder') %>

    - <%= submit_tag "Request Hat" %> + <%= submit_tag t('.requesthatbutton') %>

    <% end %>
    diff --git a/app/views/hats/index.html.erb b/app/views/hats/index.html.erb index 08a33d8..24e18ac 100644 --- a/app/views/hats/index.html.erb +++ b/app/views/hats/index.html.erb @@ -1,25 +1,22 @@
    <% if @user %> <% end %>
    - Hats + <%= t('.title') %>

    - A hat is a formal, verified, way of posting a comment while speaking for a - project, organization, or company. Each user may have multiple hats, one of - which may be selected to be worn when posting a comment or sending a private - message. + <%= t('.description') %>

    - - - + + + <% bit = 0 %> <% @hat_groups.keys.sort_by{|a| a.downcase }.each do |hg| %> diff --git a/app/views/hats/requests_index.html.erb b/app/views/hats/requests_index.html.erb index 4375382..d0c1a56 100644 --- a/app/views/hats/requests_index.html.erb +++ b/app/views/hats/requests_index.html.erb @@ -1,10 +1,10 @@
    - Requested Hats + <%= t('.title') %>
    <% if @hat_requests.count == 0 %> - No hat requests. + <%= t('.nohatrequests') %> <% else %> <% @hat_requests.each_with_index do |hr,x| %> <% if x > 0 %> @@ -15,42 +15,42 @@ :method => :post do |f| %>

    - <%= f.label :user_id, "User:", :class => "required" %> + <%= f.label :user_id, t('.user'), :class => "required" %> <%= hr.user.username %>
    - <%= f.label :hat, "Hat:", :class => "required" %> + <%= f.label :hat, t('.hat'), :class => "required" %> <%= f.text_field "hat", :size => 25 %>
    - <%= f.label :link, "Link:", :class => "required" %> + <%= f.label :link, t('.link'), :class => "required" %> <%= f.text_field "link", :size => 75 %>
    - <%= f.label :link, "Comment:", :class => "required" %> + <%= f.label :link, t('.comment'), :class => "required" %>
    <%= raw(h(hr.comment.to_s).gsub(/\n/, "
    ")) %>

    - <%= submit_tag "Approve Hat Request" %> + <%= submit_tag t('.approve') %>

    <% end %>

    - or + <%= t('.hator') %>

    <%= form_for hr, :url => reject_hat_request_url(:id => hr), :method => :post do |f| %>
    - <%= f.label :link, "Reason:", :class => "required" %> + <%= f.label :link, t('.reason'), :class => "required" %> <%= f.text_area :rejection_comment, :rows => 4 %>

    - <%= submit_tag "Reject Hat Request" %> + <%= submit_tag t('.reject') %>

    <% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index bc4832c..80e8c2b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -123,6 +123,41 @@ en: pretextdesc: "prefix text with at least    4 spaces" inlineimage: "(inline image)" inlineimagedesc: "![alt text](http://example.com/image.jpg) (only allowed in story text)" + hats: + build_request: + title: "Request a Hat" + description: "

    A hat is a formal, verified, way of posting a comment while speaking for a project, organization, or company. Each user may have multiple hats, one of which may be worn at any time when posting a comment or sending a private message.

    Hats are intended for core project developers and company employees that are authorized to make comments on behalf of those organizations, and are usually reserved for notable projects and organizations. Hats are not intended to list every contribution a person has made to any software project.

    Hats will not be authorized for occasional contributors to projects, developers of projects which are not widely known, past employees of companies, or other situations where it cannot be verified that one is authorized to represent that organization. In general, unless one has an e-mail address at a compay domain or have commit access to a project, a hat will not be authorized.

    To request a hat for your account, provide a short description of the hat (e.g., \"OpenBSD Developer\"), a public link that will be shown when hovering over the hat that users can see, such as your e-mail address at that project or company, or a link to a company website showing your employment, and private comments that will be seen only by moderators during approval.

    Once your hat is requested, a moderator will verify your request by e-mailing the address you submitted as the link, or doing some other manual verification of project association.

    " + hat: "Hat:" + hatplaceholder: "XYZ Project Member" + link: "Link:" + linkplaceholder: "user@project.org, or a URL to an employment page" + comment: "Comment:" + commentplaceholder: "Will only be shown to moderators during approval" + requesthatbutton: "Request Hat" + create_request: + submittedhatrequest: "Successfully submitted hat request." + index: + title: "Hats" + description: "A hat is a formal, verified, way of posting a comment while speaking for a project, organization, or company. Each user may have multiple hats, one of which may be selected to be worn when posting a comment or sending a private message." + request: "Request Hat" + user: "User" + hat: "Hat" + link: "Link" + reject_request: + requestedhatrequest: "Successfully rejected hat request." + requests_index: + title: "Requested Hats" + nohatrequests: "No hat requests." + user: "User:" + hat: "Hat:" + link: "Link:" + comment: "Comment:" + approve: Approve Hat Request" + reason: "Reason:" + reject: "Reject Hat Request" + hator: "or" + approve_request: + approvedhatrequest: "Successfully approved hat request." home: index: homerecentsdesc: "The newest stories with a random sampling of recently submitted stories that have not yet reached the front page." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7aed813..1b6e1d3 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -123,6 +123,41 @@ fr: pretextdesc: "précéder le texte avec au moins     4 espaces" inlineimage: "(image en ligne)" inlineimagedesc: "![texte de substitution](http://example.com/image.jpg) (autorisé seulement dans les articles)" + hats: + build_request: + title: "Demander un chapeau" + description: "

    Un chapeau est un moyen formel, vérifié de poster un commentaire au nom d'un projet, d'une organisation ou d'une entreprise. Chaque utilisateur peut avoir plusieurs chapeaux, l'un d'eux pouvant être choisi d'être porté au moment de poster un commentaire ou d'envoyer un message privé.

    Les chapeaux sont destinés aux développeurs principaux d'un projet et les employés d'entreprise qui sont autorisés à faire des commentaires au nom de leurs organisations, et sont habituellement réservés pour les projets notables et les organisations. Les chapeaux ne sont pas destinés à lister toutes les contributions d'une personne à des projets logiciels.

    Les chapeaux ne vont pas être autorisés pour les contributeurs occasionnels d'un projet, développeurs d'un projet qui n'est pas largement connu, pas non plus pour les anciens employés d'une entreprise ou autres situations où il n'est pas possible de vérifier que quelqu'un est autorisé à représenter une organisation. En général à moins d'avoir un e-mail avec le nom de domaine de l'entreprise ou du projet ou les droits d'accès aux sources d'un projet, un chapeau ne sera pas autorisé.

    Pour demander un chapeau pour votre compte, merci de fournir une courte description du chapeau (exemple \"développeur OpenBSD\"), un lien public qui sera affiché quand on passe la souris au-dessus du chapeau que les utilisateurs peuvent voir, comme votre adresse e-mail du projet ou de l'entreprise ou un lien vers le site web de l'entreprise puis une description privée qui sera seulement vue par les modérateurs durant le processus d'approbation.

    Une fois qu'un chapeau a été demandé, un modérateur va vérifier votre demande en envoyant un e-mail à l'adresse soumise comme lien et/ou en effectuant d'autres vérifications manuelles de votre association au projet.

    " + hat: "Chapeau:" + hatplaceholder: "Membre du projet XYZ" + link: "Lien :" + linkplaceholder: "membre@projet.org, ou l'URL de la page de l'employeur" + comment: "Commentaire :" + commentplaceholder: "Sera seulement montré aux modérateurs devant le processus d'approbation" + requesthatbutton: "Demander un chapeau" + create_request: + submittedhatrequest: "Demande de chapeau transmise avec succès." + index: + title: "Chapeaux" + description: "Un chapeau est un moyen formel, vérifié de poster un commentaire au nom d'un projet, d'une organisation ou d'une entreprise. Chaque utilisateur peut avoir plusieurs chapeaux, l'un d'eux pouvant être choisi d'être porté au moment de poster un commentaire ou d'envoyer un message privé." + request: "Demander un chapeau" + user: "Utilisateur" + hat: "Chapeau" + link: "Lien" + reject_request: + rejectedhatrequest: "Demande de chapeau rejetée avec succès." + requests_index: + title: "Chapeaux demandés" + nohatrequests: "Aucune demande de chapeau." + user: "Utilisateur :" + hat: "Chapeau :" + link: "Lien :" + comment: "Commentaire :" + approve: "Approuver la demande de chapeau" + reason: "Raison :" + reject: "Rejeter la demande de chapeau" + hator: "ou" + approve_request: + approvedhatrequest: "La demande de chapeau a été approuvée." home: index: homerecentsdesc: "Les plus récentes infos avec un panaché aléatoire des infos récentes soumises qui n'ont pas encore atteint la page principale." From ffd5611a220e14e3893751d1d8987056bc7e32c1 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:34:10 +0200 Subject: [PATCH 042/106] show more text on hat requests - merged with i18n --- app/views/hats/requests_index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/hats/requests_index.html.erb b/app/views/hats/requests_index.html.erb index d0c1a56..7161072 100644 --- a/app/views/hats/requests_index.html.erb +++ b/app/views/hats/requests_index.html.erb @@ -21,7 +21,7 @@
    <%= f.label :hat, t('.hat'), :class => "required" %> - <%= f.text_field "hat", :size => 25 %> + <%= f.text_field "hat", :size => 75 %>
    From d3e90291b3a5ab09b8995d9e51b3b7c59cc62377 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:35:33 +0200 Subject: [PATCH 043/106] css: style input type=number like others --- app/assets/stylesheets/application.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6ae2772..f9043cf 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -128,6 +128,7 @@ input[type="text"], input[type="search"], input[type="password"], input[type="email"], +input[type="number"], textarea { border: 1px solid #ccc; } From bf5967b93ea901b1f184bc9ccb33b213f9b3e953 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:52:20 +0200 Subject: [PATCH 044/106] add a /login.json endpoint, just returns success or failure for now - merged with i18n --- app/controllers/login_controller.rb | 69 ++++++++++++++++++++--------- config/routes.rb | 2 +- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index ec4f6f9..b6db000 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -47,33 +47,62 @@ class LoginController < ApplicationController end if user.has_2fa? - session[:twofa_u] = user.session_token - return redirect_to "/login/2fa" - end - - session[:u] = user.session_token - - if (rd = session[:redirect_to]).present? - session.delete(:redirect_to) - return redirect_to rd - elsif params[:referer].present? - begin - ru = URI.parse(params[:referer]) - if ru.host == Rails.application.domain - return redirect_to ru.to_s + if params[:totp].present? + if !user.authenticate_totp(params[:totp]) + raise "invalid TOTP code" + end + else + return respond_to do |format| + format.html { + session[:twofa_u] = user.session_token + redirect_to "/login/2fa" + } + format.json { + render :json => { :status => 0, + :error => "must supply totp parameter" } + } end - rescue => e - Rails.logger.error "error parsing referer: #{e}" end end - return redirect_to "/" + return respond_to do |format| + format.html { + session[:u] = user.session_token + + if (rd = session[:redirect_to]).present? + session.delete(:redirect_to) + return redirect_to rd + elsif params[:referer].present? + begin + ru = URI.parse(params[:referer]) + if ru.host == Rails.application.domain + return redirect_to ru.to_s + end + rescue => e + Rails.logger.error "error parsing referer: #{e}" + end + end + + redirect_to "/" + } + format.json { + render :json => { :status => 1, :username => user.username } + } + end rescue end - flash.now[:error] = I18n.t 'controllers.login_controller.flashlogininvalid' - @referer = params[:referer] - index + respond_to do |format| + format.html { + flash.now[:error] = I18n.t 'controllers.login_controller.flashlogininvalid' + @referer = params[:referer] + index + } + format.json { + render :json => { :status => 0, + :error => "invalid 'email' and/or 'password' parameter" } + } + end end def forgot_password diff --git a/config/routes.rb b/config/routes.rb index 972e4d4..c29a765 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,7 +30,7 @@ Lobsters::Application.routes.draw do get "/threads/:user" => "comments#threads" get "/login" => "login#index" - post "/login" => "login#login" + post "/login" => "login#login", :format => /html|json/ post "/logout" => "login#logout" get "/login/2fa" => "login#twofa" post "/login/2fa_verify" => "login#twofa_verify", :as => "twofa_login" From 31184f3c710932a530ef239127a0c774b4e64763 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:55:21 +0200 Subject: [PATCH 045/106] css: unify on a slightly taller line-height --- app/assets/stylesheets/application.css | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f9043cf..7df4bd6 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -21,6 +21,7 @@ body, textarea, input, button { body { background-color: #fefefe; + line-height: 1.4em; } a { @@ -117,11 +118,9 @@ select, textarea { color: #555; background-color: white; - line-height: 1.2em; padding: 3px 5px; } textarea { - line-height: 1.35em; resize: vertical; } input[type="text"], @@ -373,7 +372,6 @@ ol.comments { margin-left: 20px; margin-bottom: 0em; padding-left: 6px; - line-height: 1.35em; } ol.comments1 { margin-left: 0; @@ -638,7 +636,6 @@ div.story_content { ol.stories.list div.story_content { color: #777; max-height: 2.6em; - line-height: 1.3em; margin: 0.25em 40px 0.25em 0; overflow: hidden; text-overflow: clip; @@ -684,12 +681,10 @@ div.page_link_buttons span { div.story_text { margin-bottom: 1.5em; max-width: 700px; - line-height: 1.4em; word-wrap: break-word; } div.story_text p { margin: 0.75em 0; - line-height: 1.4em; } div.story_text img { max-width: 100%; @@ -878,7 +873,6 @@ div.comment_form_container textarea { .tree ul { margin: 0 0 0 0.5em; padding: 0; - line-height: 1.5em; list-style: none; position: relative; } @@ -1235,7 +1229,6 @@ div#flasher { div#flasher div.flash-error, div#flasher div.flash-notice, div#flasher div.flash-success { - line-height: 1.5em; display: inline-block; padding-top: 25px; margin-left: auto; From 80bea81bfc8e0727d927316656934f20125aff92 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:58:22 +0200 Subject: [PATCH 046/106] css: make fonts slightly bigger now that we have more space --- app/assets/stylesheets/application.css | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 7df4bd6..62fdc54 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -498,7 +498,7 @@ li .link { } li .link a { - font-size: 11pt; + font-size: 11.5pt; text-decoration: none; } @@ -562,7 +562,7 @@ li .comment_folder_button:checked ~ ol.comments li { li .byline { color: #888; - font-size: 9pt; + font-size: 9.5pt; } li .byline > img.avatar { margin-bottom: -5px; @@ -698,6 +698,7 @@ a#story_text_expander { } div.comment_text { + font-size: 10.5pt; max-width: 700px; word-wrap: break-word; } @@ -737,13 +738,6 @@ div.comment_text code { line-height: 1.2em; } -div.comment_actions a { - color: #888; - font-weight: bold; - font-size: 8.5pt; - text-decoration: none; -} - div.dragons { } .dragon_text { From 2fcacd5074e5ad54c531bddd50c00dad04253cf3 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 13:59:58 +0200 Subject: [PATCH 047/106] login: allow password to contain totp code --- app/controllers/login_controller.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index b6db000..468d471 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -27,8 +27,17 @@ class LoginController < ApplicationController raise "no user" end - if !user.try(:authenticate, params[:password].to_s) - raise "authentication failed" + if !user.authenticate(params[:password].to_s) + # if the user has 2fa enabled and the password looks like it has a totp + # code attached, separate them + if user.has_2fa? && + (m = params[:password].to_s.match(/\A(.+):(\d+)\z/)) && + user.authenticate(m[1]) + params[:password] = m[1] + params[:totp] = m[2] + else + raise "authentication failed" + end end if user.is_banned? @@ -48,7 +57,9 @@ class LoginController < ApplicationController if user.has_2fa? if params[:totp].present? - if !user.authenticate_totp(params[:totp]) + if user.authenticate_totp(params[:totp]) + # ok, fall through + else raise "invalid TOTP code" end else From 3faf62c53c8273ec34991db16b36b0ac2095d46b Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 14:01:10 +0200 Subject: [PATCH 048/106] Search: try to compile regexp first before sending to SQL --- app/models/search.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/search.rb b/app/models/search.rb index 809d568..5a758e3 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -68,8 +68,14 @@ class Search if domain.present? self.what = "stories" - story_ids = Story.select(:id).where("`url` REGEXP '//([^/]*\.)?" + - ActiveRecord::Base.connection.quote_string(domain) + "/'"). + begin + reg = Regexp.new("//([^/]*\.)?#{domain}/") + rescue RegexpError + return false + end + + story_ids = Story.select(:id).where("`url` REGEXP '" + + ActiveRecord::Base.connection.quote_string(reg.source) + "'"). collect(&:id) if story_ids.any? From fb421febce29b44754bc3e3c4a492ac194184145 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 14:04:12 +0200 Subject: [PATCH 049/106] css: more line-height tweaks --- app/assets/stylesheets/application.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 62fdc54..f164efe 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -21,7 +21,7 @@ body, textarea, input, button { body { background-color: #fefefe; - line-height: 1.4em; + line-height: 1.45em; } a { @@ -454,8 +454,8 @@ li.story { clear: both; } ol.stories li.story div.story_liner { - padding-top: 0.3em; - padding-bottom: 0.3em; + padding-top: 0.25em; + padding-bottom: 0.25em; } .comment { clear: both; From 9ae80b1e9eaf5302a0098ac3cbd7be468af6668a Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Fri, 19 May 2017 14:06:09 +0200 Subject: [PATCH 050/106] factor out avatar code to show 2x version everywhere --- app/helpers/application_helper.rb | 9 +++++++++ app/views/comments/_comment.html.erb | 4 ++-- app/views/stories/_listdetail.html.erb | 12 ++++-------- app/views/users/show.html.erb | 4 +--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 69869f7..f294671 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,15 @@ module ApplicationHelper MAX_PAGES = 15 + def avatar_img(user, size) + image_tag(user.avatar_url(size), { + :srcset => "#{user.avatar_url(size)} 1x, " << + "#{user.avatar_url(size * 2)} 2x", + :class => "avatar", + :size => "#{size}x#{size}", + :alt => "#{user.username} avatar" }) + end + def break_long_words(str, len = 30) safe_join(str.split(" ").map{|w| if w.length > len diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index c4be4ce..51d3337 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -38,8 +38,8 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <% end %> <% if (@user && @user.show_avatars?) || !@user %> - + <%= + avatar_img(comment.user, 16) %> <% end %> <% end %> + <%= + avatar_img(ms.user, 16) %> <% end %> <% if story.user_is_author? %> <%= t('.authoredby') %> @@ -92,10 +90,8 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %>
    - From 0c1516bb14e5adf3fdfa610a627f4408b65304d3 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:33:55 +0200 Subject: [PATCH 077/106] application.js: always replace comment form in submit/preview --- app/assets/javascripts/application.js.erb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index f017dc0..f83cd03 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -174,12 +174,7 @@ var _Lobsters = Class.extend({ if ($(form).find('#parent_comment_short_id').length) { $(form).closest('.comment').replaceWith($.parseHTML(data)); } else { - if ($(form).attr("id").match(/^edit_comment_.+$/)) { - $(form).parent(".comment").replaceWith($.parseHTML(data)); - } else { - $(form).closest('.comment').after($.parseHTML(data)); - $(form).find('textarea').val(''); - } + $(form).parent(".comment").replaceWith($.parseHTML(data)); } }); }, From b4f0f57df90ccc638bd9e5dfc11ed92cb08d0fc2 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:35:14 +0200 Subject: [PATCH 078/106] User: username regex is a const now --- app/models/user.rb | 6 ++---- app/views/settings/index.html.erb | 2 +- app/views/signup/invited.html.erb | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 070125d..9be77e0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -95,10 +95,8 @@ class User < ActiveRecord::Base end end - def self.username_regex - User.validators_on(:username).select{|v| - v.class == ActiveModel::Validations::FormatValidator }.first. - options[:with].inspect + def self.username_regex_s + "/^" + VALID_USERNAME.to_s.gsub(/(\?-mix:|\(|\))/, "") + "$/" end def as_json(options = {}) diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 1ac928d..b6c55ee 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -14,7 +14,7 @@ <%= f.label :username, t('.username'), :class => "required" %> <%= f.text_field :username, :size => 15 %> - <%= User.username_regex %> + <%= User.username_regex_s %> diff --git a/app/views/signup/invited.html.erb b/app/views/signup/invited.html.erb index f23f9d2..8ccb84c 100644 --- a/app/views/signup/invited.html.erb +++ b/app/views/signup/invited.html.erb @@ -27,7 +27,7 @@ <%= f.label :username, "Username:", :class => "required" %> <%= f.text_field :username, :size => 30 %> - <%= User.username_regex %> + <%= User.username_regex_s %>
    From c9b5ec10e367eaa1f136dcc6862186d128e901c6 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:41:09 +0200 Subject: [PATCH 079/106] add twitter oauth linking --- app/controllers/settings_controller.rb | 41 +++++++++++++++++++++++++- app/models/user.rb | 7 +++++ app/views/settings/index.html.erb | 15 ++++++++++ config/routes.rb | 3 ++ extras/twitter.rb | 31 +++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index fa0c967..a597080 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -171,7 +171,7 @@ class SettingsController < ApplicationController def github_callback if !session[:github_state].present? || !params[:code].present? || (params[:state].to_s != session[:github_state].to_s) - flash[:error] = "No OAuth state" + flash[:error] = "Invalid OAuth state" return redirect_to "/settings" end @@ -199,6 +199,45 @@ class SettingsController < ApplicationController return redirect_to "/settings" end + def twitter_auth + session[:twitter_state] = SecureRandom.hex + return redirect_to Twitter.oauth_auth_url(session[:twitter_state]) + end + + def twitter_callback + if !session[:twitter_state].present? || + (params[:state].to_s != session[:twitter_state].to_s) + flash[:error] = "Invalid OAuth state" + return redirect_to "/settings" + end + + session.delete(:twitter_state) + + tok, sec, username = Twitter.token_secret_and_user_from_token_and_verifier( + params[:oauth_token], params[:oauth_verifier]) + if tok.present? && username.present? + @user.twitter_oauth_token = tok + @user.twitter_oauth_token_secret = sec + @user.twitter_username = username + @user.save! + flash[:success] = "Your account has been linked to Twitter user @" << + "#{username}." + else + return twitter_disconnect + end + + return redirect_to "/settings" + end + + def twitter_disconnect + @user.twitter_oauth_token = nil + @user.twitter_username = nil + @user.twitter_oauth_token_secret = nil + @user.save! + flash[:success] = "Your Twitter association has been removed." + return redirect_to "/settings" + end + private def user_params diff --git a/app/models/user.rb b/app/models/user.rb index 9be77e0..5e32b1d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -48,6 +48,9 @@ class User < ActiveRecord::Base s.string :totp_secret s.string :github_oauth_token s.string :github_username + s.string :twitter_oauth_token + s.string :twitter_oauth_token_secret + s.string :twitter_username end validates :email, :format => { :with => /\A[^@ ]+@[^@ ]+\.[^@ ]+\Z/ }, @@ -121,6 +124,10 @@ class User < ActiveRecord::Base h[:github_username] = self.github_username end + if self.twitter_username.present? + h[:twitter_username] = self.twitter_username + end + h end diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index b6c55ee..9618683 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -246,6 +246,21 @@ <% end %> + <% if Twitter.enabled? %> +
    + <%= label_tag :twitter_username, "Twitter:", :class => "required" %> + <% if @edit_user.twitter_username.present? %> + Linked to + @<%= h(@edit_user.twitter_username) %> + (<%= link_to "Disconnect", "/settings/twitter_disconnect", + :method => :post %>) + <% else %> + Connect + <% end %> +
    + <% end %> +

    diff --git a/config/routes.rb b/config/routes.rb index 6135757..4f8bda7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -121,6 +121,9 @@ Lobsters::Application.routes.draw do get "/settings/github_auth" => "settings#github_auth" get "/settings/github_callback" => "settings#github_callback" post "/settings/github_disconnect" => "settings#github_disconnect" + get "/settings/twitter_auth" => "settings#twitter_auth" + get "/settings/twitter_callback" => "settings#twitter_callback" + post "/settings/twitter_disconnect" => "settings#twitter_disconnect" get "/filters" => "filters#index" post "/filters" => "filters#update" diff --git a/extras/twitter.rb b/extras/twitter.rb index 6a7f5db..8650b07 100644 --- a/extras/twitter.rb +++ b/extras/twitter.rb @@ -4,6 +4,9 @@ class Twitter # these need to be overridden in config/initializers/production.rb @@CONSUMER_KEY = nil @@CONSUMER_SECRET = nil + + # these are set for the account used to post updates in + # script/post_to_twitter (needs read/write access) @@AUTH_TOKEN = nil @@AUTH_SECRET = nil @@ -12,6 +15,10 @@ class Twitter # https://t.co/eyW1U2HLtP TCO_LEN = 23 + def self.enabled? + self.CONSUMER_KEY.present? + end + def self.oauth_consumer OAuth::Consumer.new(self.CONSUMER_KEY, self.CONSUMER_SECRET, { :site => "https://api.twitter.com" }) @@ -47,4 +54,28 @@ class Twitter end end end + + def self.token_secret_and_user_from_token_and_verifier(tok, verifier) + rt = OAuth::RequestToken.from_hash(self.oauth_consumer, + { :oauth_token => tok }) + at = rt.get_access_token({ :oauth_verifier => verifier }) + + res = at.get("/1.1/account/verify_credentials.json") + js = JSON.parse(res.body) + + if !js["screen_name"].present? + return nil + end + + [ at.token, at.secret, js["screen_name"] ] + end + + def self.oauth_request_token(state) + self.oauth_consumer.get_request_token(:oauth_callback => + Rails.application.root_url + "settings/twitter_callback?state=#{state}") + end + + def self.oauth_auth_url(state) + self.oauth_request_token(state).authorize_url + end end From 04c94e6f194f2ef324334bda2f593e051b25fc55 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:42:49 +0200 Subject: [PATCH 080/106] post_to_twitter: clarify MIN_STORY_SCORE is a minimum --- script/post_to_twitter | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/post_to_twitter b/script/post_to_twitter index aea3583..751c207 100755 --- a/script/post_to_twitter +++ b/script/post_to_twitter @@ -7,9 +7,9 @@ require File.expand_path('../../config/boot', __FILE__) require APP_PATH Rails.application.require_environment! -MIN_STORY_SCORE = 2 +MIN_STORY_SCORE = 3 -Story.where("is_expired = ? AND #{Story.score_sql} > ? AND " << +Story.where("is_expired = ? AND #{Story.score_sql} >= ? AND " << "twitter_id IS NULL AND created_at >= ?", false, MIN_STORY_SCORE, Time.now - 1.days).order(:id).each_with_index do |s,x| if s.tags.map(&:tag).include?("meta") From 3f6646d87b11ec83b2dd24f0075d9bebf1cb9675 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:44:20 +0200 Subject: [PATCH 081/106] if a user has a linked twitter account, give them attribution --- app/controllers/stories_controller.rb | 4 ++++ script/post_to_twitter | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 2783824..9c509f2 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -163,6 +163,10 @@ class StoriesController < ApplicationController "apple-touch-icon-144.png", } + if @story.user.twitter_username.present? + @meta_tags["twitter:creator"] = "@" + @story.user.twitter_username + end + load_user_votes render :action => "show" diff --git a/script/post_to_twitter b/script/post_to_twitter index 751c207..d59f495 100755 --- a/script/post_to_twitter +++ b/script/post_to_twitter @@ -25,12 +25,18 @@ Time.now - 1.days).order(:id).each_with_index do |s,x| tags += ' #' + tagging.tag.tag end - tco_status = "\n" + + via = "" + if s.user.twitter_username.present? + via = "\n" + (s.user_is_author? ? "by" : "via") + + " @#{s.user.twitter_username}" + end + + tco_status = via + "\n" + (s.url.present?? ("X" * Twitter::TCO_LEN) + "\n" : "") + ("X" * Twitter::TCO_LEN) + tags - status = "\n" + + status = via + "\n" + (s.url.present?? s.url + "\n" : "") + s.short_id_url + tags From ec93040614422135a3fedfbef3438fd04589f09e Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:46:33 +0200 Subject: [PATCH 082/106] user profile: show twitter info here too --- app/views/users/show.html.erb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 9d91ec3..373c65a 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -119,8 +119,19 @@ - https://github.com/<%= h(@showing_user.github_username) %> + https://github.com/<%= h(@showing_user.github_username) + %> + +
    + <% end %> + + <% if @showing_user.twitter_username.present? %> + + + + @<%= h(@showing_user.twitter_username) %>
    <% end %> From 1b79d6dd4519f171df0be4836c599fb47ce221e4 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:48:08 +0200 Subject: [PATCH 083/106] css: make tags slightly more rectangular --- app/assets/stylesheets/application.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f164efe..a867e62 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -48,11 +48,11 @@ div.clear { a.tag { background-color: #fffcd7; border: 1px solid #d5d458; - border-radius: 10px; + border-radius: 5px; color: #555; font-size: 8pt; margin-left: 0.25em; - padding: 0px 0.5em 1px 0.5em; + padding: 0px 0.4em 1px 0.4em; text-decoration: none; vertical-align: text-top; } From f9d46c666bfbc258c483f9d41e52484d5aae05af Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 10:50:37 +0200 Subject: [PATCH 084/106] stories: if the user can't see the story, just return a 404 --- app/controllers/stories_controller.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 9c509f2..204ff7c 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -126,12 +126,11 @@ class StoriesController < ApplicationController return redirect_to @story.merged_into_story.comments_path end - if @story.can_be_seen_by_user?(@user) - @title = @story.title - else - @title = "[Story removed]" + if !@story.can_be_seen_by_user?(@user) + raise ActionController::RoutingError.new("story gone") end + @title = @story.title @short_url = @story.short_id_url @comments = @story.merged_comments.includes(:user, :story, :hat, From 722189b6ac2fbe96c555b2318e7cde2ea5557e3f Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 12:26:49 +0200 Subject: [PATCH 085/106] add a dynamic 404 page --- .gitignore | 1 + app/controllers/home_controller.rb | 20 +++++++++++++++++--- config/application.rb | 2 ++ config/routes.rb | 2 ++ public/404.html | 9 --------- 5 files changed, 22 insertions(+), 12 deletions(-) delete mode 100644 public/404.html diff --git a/.gitignore b/.gitignore index c0b1883..4db7818 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ upstream-patches app/views/home/privacy.* app/views/home/about.* app/views/home/chat.* +app/views/home/404.* app/assets/stylesheets/local/* public/favicon.ico public/apple-touch-icon* diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 244266d..7a444d3 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -4,11 +4,24 @@ class HomeController < ApplicationController before_filter { @page = page } before_filter :require_logged_in_user, :only => [ :upvoted ] + def four_oh_four + begin + @title = "Resource Not Found" + render :action => "404", :status => 404 + rescue ActionView::MissingTemplate + render :text => "
    " << + "
    404
    " << + "Resource not found" << + "
    ", :layout => "application" + end + end + + def about begin @title = I18n.t 'controllers.home_controller.abouttitle' render :action => "about" - rescue + rescue ActionView::MissingTemplate render :text => I18n.t('controllers.home_controller.abouttext'), :layout => "application" end end @@ -17,8 +30,9 @@ class HomeController < ApplicationController begin @title = I18n.t 'controllers.home_controller.chattitle' render :action => "chat" - rescue + rescue ActionView::MissingTemplate render :text => "
    " << + "
    Chat
    " << "Keep it on-site" << "
    ", :layout => "application" end @@ -28,7 +42,7 @@ class HomeController < ApplicationController begin @title = I18n.t 'controllers.home_controller.privacytitle' render :action => "privacy" - rescue + rescue ActionView::MissingTemplate render :text => I18n.t('controllers.home_controller.licensetext'), :layout => "application" end end diff --git a/config/application.rb b/config/application.rb index 3c08f65..3401516 100644 --- a/config/application.rb +++ b/config/application.rb @@ -30,6 +30,8 @@ module Lobsters config.cache_store = :file_store, "#{config.root}/tmp/cache/" + config.exceptions_app = self.routes + config.after_initialize do require "#{Rails.root}/lib/monkey.rb" end diff --git a/config/routes.rb b/config/routes.rb index 4f8bda7..c0e2994 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,8 @@ Lobsters::Application.routes.draw do :protocol => (Rails.application.config.force_ssl ? "https://" : "http://"), :as => "root" + get "/404" => "home#four_oh_four", :via => :all + get "/rss" => "home#index", :format => "rss" get "/hottest" => "home#index", :format => "json" diff --git a/public/404.html b/public/404.html deleted file mode 100644 index 86724f7..0000000 --- a/public/404.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - 404 - - -

    gone fishin'

    - - From 24909604b58fbc5cb7ba5175af21b3151ca19adc Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 12:42:48 +0200 Subject: [PATCH 086/106] login: when resetting a password, if user has 2fa, make them login again - merged with i18n --- app/controllers/login_controller.rb | 14 +++++++++----- config/locales/en.yml | 3 +++ config/locales/fr.yml | 3 +++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 468d471..3f23d1a 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -159,15 +159,19 @@ class LoginController < ApplicationController end if @reset_user.save && @reset_user.is_active? - session[:u] = @reset_user.session_token - return redirect_to "/" + if @reset_user.has_2fa? + flash[:success] = t('.passwordreset') + return redirect_to "/login" + else + session[:u] = @reset_user.session_token + return redirect_to "/" + end else - flash[:error] = "Could not reset password." + flash[:error] = t('.couldnotresetpassword') end end else - flash[:error] = "Invalid reset token. It may have already been " << - "used or you may have copied it incorrectly." + flash[:error] = t(.invalidresettoken') return redirect_to forgot_password_path end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b76870..a80f44b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -210,6 +210,9 @@ en: password: "New Password:" again: "(Again):" setpassbutton: "Set New Password" + passwordreset: "Your password has been reset." + couldnotresetpassword: "Could not reset password." + invalidresettoken: "Invalid reset token. It may have already been used or you may have copied it incorrectly." twofa: login2fa: "Login - Two Factor Authentication" logintotpcode: "Enter the current TOTP code from your TOTP application:" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7d9b431..a491e4a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -210,6 +210,9 @@ fr: password: "Mot de passe :" again: "(encore):" setpassbutton: "Changer le mot de passe" + passwordreset: "Votre mot de passe a été changé" + couldnotresetpassword: "Le mot de passe n'a pas pu être changé." + invalidresettoken: "Jeton de changement invalide. Il a pu déjà être utilisé ou mal copié." twofa: login2fa: "Identification par authentification à deux facteurs" logintotpcode: "Entrez le code TOTP affiché par votre application :" From e14d9194c33e312188a15ad8e545dc0c1ab10a59 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 12:46:46 +0200 Subject: [PATCH 087/106] users/show: wrap hats in a div, for fancy people with many - merged with i18n --- app/views/users/show.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 373c65a..4b9cf2c 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -69,12 +69,12 @@ <% if @showing_user.hats.any? %> - +
    <% @showing_user.hats.each do |hat| %> <%= hat.to_html_label %> <% end %> - -
    +
    +
    <% end %> <% if @showing_user.deleted_at? %> From 3d54a8ea836f80fd2324eaec769c01a1a46a3322 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 12:48:36 +0200 Subject: [PATCH 088/106] users: re-enable username '@' linking in user profiles --- app/models/user.rb | 4 +--- extras/markdowner.rb | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 5e32b1d..5833cc1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -346,9 +346,7 @@ class User < ActiveRecord::Base end def linkified_about - # most users are probably mentioning "@username" to mean a twitter url, not - # a link to a profile on this site - Markdowner.to_html(self.about, { :disable_profile_links => true }) + Markdowner.to_html(self.about) end def most_common_story_tag diff --git a/extras/markdowner.rb b/extras/markdowner.rb index ca67d06..1be448a 100644 --- a/extras/markdowner.rb +++ b/extras/markdowner.rb @@ -1,6 +1,5 @@ class Markdowner # opts[:allow_images] allows tags - # opts[:disable_profile_links] disables @username -> /u/username links def self.to_html(text, opts = {}) if text.blank? @@ -10,9 +9,7 @@ class Markdowner exts = [:tagfilter, :autolink, :strikethrough] root = CommonMarker.render_doc(text.to_s, [:SMART], exts) - unless opts[:disable_profile_links] - walk_text_nodes(root) {|n| postprocess_text_node(n)} - end + walk_text_nodes(root){|n| postprocess_text_node(n) } ng = Nokogiri::HTML(root.to_html([:SAFE], exts)) From 5a3ac372b8298cd6ee79b375772f938f7a21d9f1 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 12:55:45 +0200 Subject: [PATCH 089/106] user tree: users with no parents don't descend from anything --- app/assets/stylesheets/application.css | 6 ++++++ app/views/users/tree.html.erb | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index a867e62..ba1e58d 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -912,6 +912,12 @@ div.comment_form_container textarea { height: auto; } +li.noparent:before, +ul.noparent:before { + border-top: 0 !important; + border-left: 0 !important; +} + ul.user_tree { color: #888; } diff --git a/app/views/users/tree.html.erb b/app/views/users/tree.html.erb index 17843cf..556da31 100644 --- a/app/views/users/tree.html.erb +++ b/app/views/users/tree.html.erb @@ -3,21 +3,23 @@ <%= @title %> (<%= @user_count %>)

    -

    - <%= t('.newestusers') %> - <%= raw @newest.map{|u| "#{u.username} " << - "(#{u.karma})" }.join(", ") %> -

    + <% if @newest %> +

    + <%= t('.newestusers') %> + <%= raw @newest.map{|u| "#{u.username} " << + "(#{u.karma})" }.join(", ") %> +

    + <% end %> -
      +
        <% subtree = @users_by_parent[nil] %> <% ancestors = [] %> <% while subtree %> <% if (user = subtree.pop) %> -
      • +
      • "> class="inactive_user" From 4537718dce21895eff749a69937aeb06d424813b Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:01:21 +0200 Subject: [PATCH 090/106] user trees: hide moderator karma, as done on profiles - merged with i18n --- app/views/users/list.html.erb | 5 ++--- app/views/users/tree.html.erb | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/users/list.html.erb b/app/views/users/list.html.erb index 3815cbe..d4e051e 100644 --- a/app/views/users/list.html.erb +++ b/app/views/users/list.html.erb @@ -15,11 +15,10 @@ ><%= user.username %> <% if user.is_admin? %> <%= t('.moderator') %> + <% elsif user.is_moderator? %> + <%= t('.moderator') %> <% else %> (<%= user.karma %>) - <% if user.is_moderator? %> - <%= t('.administrator') %> - <% end %> <% end %>
      • <% end %> diff --git a/app/views/users/tree.html.erb b/app/views/users/tree.html.erb index 556da31..99625cf 100644 --- a/app/views/users/tree.html.erb +++ b/app/views/users/tree.html.erb @@ -29,11 +29,10 @@ ><%= user.username %> <% if user.is_admin? %> <%= t('.administrator') %> + <% elsif user.is_moderator? %> + <%= t('.moderator') %> <% else %> (<%= user.karma %>) - <% if user.is_moderator? %> - <%= t('.moderator') %>(moderator) - <% end %> <% end %> <% if (children = @users_by_parent[user.id]) %> <% # drill down deeper in the tree %> From 6668b1a992be1791f5c14d33101c10c6139c4e2d Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:10:38 +0200 Subject: [PATCH 091/106] users: move hats down to other personal things - merged with i18n --- app/views/users/show.html.erb | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 4b9cf2c..19f3679 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -67,16 +67,6 @@
        <% end %> - <% if @showing_user.hats.any? %> - -
        - <% @showing_user.hats.each do |hat| %> - <%= hat.to_html_label %> - <% end %> -
        -
        - <% end %> - <% if @showing_user.deleted_at? %> @@ -115,6 +105,17 @@
        + <% if @showing_user.hats.any? %> + +
        + <% @showing_user.hats.each do |hat| %> + <%= hat.to_html_label %> + <% end %> +
        +
        + <% end %> + + <% if @showing_user.github_username.present? %> From 6a336b7cee960c1eea2d939db4de6e9bb732595f Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:28:25 +0200 Subject: [PATCH 092/106] comments: simplify highlighting, use css selector - merged with i18n --- app/assets/stylesheets/application.css | 8 +++++++- app/controllers/stories_controller.rb | 12 ------------ app/models/comment.rb | 8 ++------ app/views/comments/_comment.html.erb | 9 +++------ config/routes.rb | 8 +++++--- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index ba1e58d..274939f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -487,7 +487,7 @@ li div.details { opacity: 0.5; } -.comment.highlighted { +.comment:target { background-color: #fffcd7; border-radius: 20px; } @@ -560,6 +560,12 @@ li .comment_folder_button:checked ~ ol.comments li { display: none; } +/* try to force a highlighted comment to stay visible */ +li .comment_folder_button:checked ~ div.comment div.comment_text, +li .comment_folder_button:checked ~ div.comment div.voters { + display: block; +} + li .byline { color: #888; font-size: 9.5pt; diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 204ff7c..2ba3169 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -136,18 +136,6 @@ class StoriesController < ApplicationController @comments = @story.merged_comments.includes(:user, :story, :hat, :votes => :user).arrange_for_user(@user) - @force_show_thread_id = nil - - if params[:comment_short_id] - @comments.each do |c,x| - if c.short_id == params[:comment_short_id] - c.highlighted = true - @force_show_thread_id = c.thread_id - break - end - end - end - respond_to do |format| format.html { @comment = @story.comments.build diff --git a/app/models/comment.rb b/app/models/comment.rb index a23f399..80b0fda 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -10,7 +10,7 @@ class Comment < ActiveRecord::Base :class_name => "Moderation" belongs_to :hat - attr_accessor :current_vote, :previewing, :indent_level, :highlighted + attr_accessor :current_vote, :previewing, :indent_level before_validation :on => :create do self.assign_short_id_and_upvote @@ -429,10 +429,6 @@ class Comment < ActiveRecord::Base self.upvotes - self.downvotes end - def short_id_path - self.story.short_id_path + "/c/#{self.short_id}" - end - def short_id_url Rails.application.root_url + "c/#{self.short_id}" end @@ -446,7 +442,7 @@ class Comment < ActiveRecord::Base end def url - self.story.comments_url + "/comments/#{self.short_id}#c_#{self.short_id}" + self.story.comments_path + "#c_#{self.short_id}" end def vote_summary_for_user(u) diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 51d3337..e5957a7 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,13 +1,10 @@ > -
        > +
        - <%= comment.highlighted ? "highlighted" : "" %> <%= comment.score <= -1 ? "negative_1" : "" %> <%= comment.score <= -3 ? "negative_3" : "" %> <%= comment.score <= -5 ? "negative_5" : "" %>"> @@ -64,7 +61,7 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <% if !comment.previewing %> | - <%= t('.link') %> + <%= t('.link') %> <% if comment.is_editable_by_user?(@user) %> | diff --git a/config/routes.rb b/config/routes.rb index c0e2994..5d47fe2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,12 +91,14 @@ Lobsters::Application.routes.draw do post "keep_as_new" end - get "/s/:id/:title/comments/:comment_short_id" => "stories#show" - get "/s/:id/(:title)" => "stories#show", :format => /html|json/ - get "/c/:id" => "comments#redirect_from_short_id" get "/c/:id.json" => "comments#show_short_id", :format => "json" + # deprecated + get "/s/:story_id/:title/comments/:id" => "comments#redirect_from_short_id" + + get "/s/:id/(:title)" => "stories#show", :format => /html|json/ + get "/u" => "users#tree" get "/u/:username" => "users#show", :as => "user", :format => /html|json/ From a3071c22b5ad6dacd630e153dfb399ad80ba3986 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:30:56 +0200 Subject: [PATCH 093/106] Story: try to remove meta[property='og:site_name'] from fetched title --- app/models/story.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/models/story.rb b/app/models/story.rb index fb640be..59ccb8c 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -801,6 +801,24 @@ class Story < ActiveRecord::Base title = parsed.at_css("title").try(:text).to_s end + # see if the site name is available, so we can strip it out in case it was + # present in the fetched title + begin + site_name = parsed.at_css("meta[property='og:site_name']"). + attributes["content"].text + + if site_name.present? && site_name.length < title.length && + title[-(site_name.length), site_name.length] == site_name + title = title[0, title.length - site_name.length] + + # remove title/site name separator + if title.match(/ [ \-\|\u2013] $/) + title = title[0, title.length - 3] + end + end + rescue + end + @fetched_attributes[:title] = title # now get canonical version of url (though some cms software puts incorrect From e255915723be871c0e91abf7adae953d5c3f16f5 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:32:28 +0200 Subject: [PATCH 094/106] Comment: #url should use story's url, not path --- app/models/comment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 80b0fda..49c4cd2 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -442,7 +442,7 @@ class Comment < ActiveRecord::Base end def url - self.story.comments_path + "#c_#{self.short_id}" + self.story.comments_url + "#c_#{self.short_id}" end def vote_summary_for_user(u) From 517f62c8863b77163ed98c1f6af6b55311043c44 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:42:02 +0200 Subject: [PATCH 095/106] stories: show edit link for merged stories - merged with i18nx --- app/views/stories/_listdetail.html.erb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/stories/_listdetail.html.erb b/app/views/stories/_listdetail.html.erb index 42da9d9..c88bc8b 100644 --- a/app/views/stories/_listdetail.html.erb +++ b/app/views/stories/_listdetail.html.erb @@ -75,6 +75,10 @@ class="story <%= story.vote && story.vote[:vote] == 1 ? "upvoted" : "" %> ms.html_class_for_user %>"><%= ms.user.username %> <%= distance_of_time_in_words(ms.created_at, Time.now, :strip_about => true) %> + <% if ms.is_editable_by_user?(@user) %> + | + <%= t('.edit') %> + <% end %> <% end %> <% end %> From 8fe01e7e2cfe165bd157c5bcdb7fbe34541520a4 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:43:36 +0200 Subject: [PATCH 096/106] User: add active scope --- app/models/user.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 5833cc1..b0a0166 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -69,6 +69,8 @@ class User < ActiveRecord::Base end end + scope :active, -> { where(:banned_at => nil, :deleted_at => nil) } + before_save :check_session_token before_validation :on => :create do self.create_rss_token From a05ab9a01e2f9ef3342cd8ba9fd36b8bde6879ad Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 13:44:54 +0200 Subject: [PATCH 097/106] Story#recalculate_hotness: cap comment points at story votes --- app/models/story.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/models/story.rb b/app/models/story.rb index 59ccb8c..f90c021 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -123,7 +123,11 @@ class Story < ActiveRecord::Base end def self.recalculate_all_hotnesses! - Story.all.order("id DESC").each do |s| + # do the front page first, since find_each can't take an order + Story.order("id DESC").limit(100).each do |s| + s.recalculate_hotness! + end + Story.find_each do |s| s.recalculate_hotness! end true @@ -189,6 +193,8 @@ class Story < ActiveRecord::Base end def calculated_hotness + # take each tag's hotness modifier into effect, and give a slight bump to + # stories submitted by the author base = self.tags.map{|t| t.hotness_mod }.sum + (self.user_is_author ? 0.25 : 0.0) @@ -210,6 +216,12 @@ class Story < ActiveRecord::Base # mix in any stories this one cannibalized cpoints += self.merged_stories.map{|s| s.score }.inject(&:+).to_f + # if a story has many comments but few votes, it's probably a bad story, so + # cap the comment points at the number of upvotes + if cpoints > self.upvotes + cpoints = self.upvotes + end + # don't immediately kill stories at 0 by bumping up score by one order = Math.log([ (score + 1).abs + cpoints, 1 ].max, 10) if score > 0 From b6ceb95e13e40aa181a07747a8afc2793ad6af74 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:01:05 +0200 Subject: [PATCH 098/106] layout: if layouts/_footer exists, use it instead - merged with i18n --- .gitignore | 1 + app/views/layouts/application.html.erb | 34 ++++++++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 4db7818..b59a649 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ app/views/home/privacy.* app/views/home/about.* app/views/home/chat.* app/views/home/404.* +app/views/layouts/_footer.* app/assets/stylesheets/local/* public/favicon.ico public/apple-touch-icon* diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1e8c220..81c733d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -117,23 +117,27 @@ <%= yield %>
        From c14c2599354f31bccaa08de5e628f4f046dd4868 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:02:42 +0200 Subject: [PATCH 099/106] mobile css: remove left padding from footer --- app/assets/stylesheets/mobile.css | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/mobile.css b/app/assets/stylesheets/mobile.css index 909febb..228eda1 100644 --- a/app/assets/stylesheets/mobile.css +++ b/app/assets/stylesheets/mobile.css @@ -242,6 +242,5 @@ div#footer { text-align: center; float: none; - padding-left: 10px; } } From 31e06e4740585ad597598eda5af91737889af3ae Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:09:58 +0200 Subject: [PATCH 100/106] move logout link to settings - merged with i18n --- app/views/layouts/application.html.erb | 3 --- app/views/settings/index.html.erb | 4 ++++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 81c733d..716a706 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -95,9 +95,6 @@ raw("class=\"cur_url\"") : "" %>><%= @user.username %> (<%= @user.karma %>) - <%= link_to t('.logoutlink'), { :controller => "login", :action => "logout" }, - :data => { :confirm => t('.confirmlogoutlink') }, - :method => "post" %> <% else %> <%= t('.loginlink') %> <% end %> diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 9618683..3d6146a 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -1,6 +1,10 @@
        <%= t('.viewprofile') %> + | + <%= link_to t('.logoutlink'), { :controller => "login", :action => "logout" }, + :data => { :confirm => t('.confirmlogoutlink') }, + :method => "post" %>
        <%= t('.accountsettings') %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a80f44b..e679951 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -285,6 +285,7 @@ en: deleteaccountflash: "Your account has been deleted." verifypasswordflash: "Your password could not be verified." index: + logoutlink: "Logout" viewprofile: "View Profile" accountsettings: "Account Settings" username: "Username:" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a491e4a..70f9a3f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -295,6 +295,7 @@ fr: deleteaccountflash: "Votre compte a été supprimé." verifypasswordflash: "Votre mot de passe n'a pas pu être vérifié." index: + logoutlink: "Se déconnecter" viewprofile: "Voir le profil" accountsettings: "Paramètres du compte" username: "Utilisateur :" From 653d8e64a99b97ac2dd69240b0a7c637c0ed32a5 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:13:12 +0200 Subject: [PATCH 101/106] experiment with hiding fresh but boring comment scores --- app/assets/javascripts/application.js.erb | 10 ++++++++-- app/assets/stylesheets/application.css | 4 ++++ app/models/comment.rb | 12 ++++++++++++ app/views/comments/_comment.html.erb | 6 ++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index f83cd03..7d907c5 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -116,7 +116,12 @@ var _Lobsters = Class.extend({ var li = $(voterEl).closest(".story, .comment"); var scoreDiv = li.find("div.score").get(0); - var score = parseInt(scoreDiv.innerHTML); + var score = 0; + var showScore = true; + if (scoreDiv.innerHTML == "-") + showScore = false; + else + score = parseInt(scoreDiv.innerHTML); var action = ""; if (li.hasClass("upvoted") && point > 0) { @@ -150,7 +155,8 @@ var _Lobsters = Class.extend({ action = "downvote"; } - scoreDiv.innerHTML = score; + if (showScore) + scoreDiv.innerHTML = score; if (action == "upvote" || action == "unvote") { li.find(".reason").html(""); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 274939f..e6ecc52 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -487,6 +487,10 @@ li div.details { opacity: 0.5; } +.comment.bad { + opacity: 0.7; +} + .comment:target { background-color: #fffcd7; border-radius: 20px; diff --git a/app/models/comment.rb b/app/models/comment.rb index 49c4cd2..0308875 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -32,6 +32,8 @@ class Comment < ActiveRecord::Base # after this many minutes old, a comment cannot be edited MAX_EDIT_MINS = (60 * 6) + SCORE_RANGE_TO_HIDE = (-2 .. 4) + validate do self.comment.to_s.strip == "" && errors.add(" ", I18n.t( 'models.comment.commentcannotbeblank')) @@ -429,6 +431,16 @@ class Comment < ActiveRecord::Base self.upvotes - self.downvotes end + def score_for_user(user) + if (user && user.is_moderator?) || + (self.created_at && self.created_at < 24.hours.ago) || + !SCORE_RANGE_TO_HIDE.include?(score) + score + else + "-" + end + end + def short_id_url Rails.application.root_url + "c/#{self.short_id}" end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index e5957a7..6b47be9 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -5,9 +5,7 @@ data-shortid="<%= comment.short_id if comment.persisted? %>" class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? "upvoted" : "downvoted") : "" %> - <%= comment.score <= -1 ? "negative_1" : "" %> - <%= comment.score <= -3 ? "negative_3" : "" %> - <%= comment.score <= -5 ? "negative_5" : "" %>"> + <%= comment.score < Comment::SCORE_RANGE_TO_HIDE.first ? "bad" : "" %>"> <% if !comment.is_gone? %>
        <% if @user %> @@ -15,7 +13,7 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <% else %> <%= link_to "", login_path, :class => "upvoter" %> <% end %> -
        <%= comment.score %>
        +
        <%= comment.score_for_user(@user) %>
        <% if @user && @user.can_downvote?(comment) %> <% else %> From 7855b2c3c47332c58ddbede5dbcf23f579445e14 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:14:47 +0200 Subject: [PATCH 102/106] Story: move minimum score to a constant, like Comment has --- app/models/story.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/story.rb b/app/models/story.rb index f90c021..d390474 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -33,6 +33,9 @@ class Story < ActiveRecord::Base DOWNVOTABLE_DAYS = 14 + # the lowest a score can go + DOWNVOTABLE_MIN_SCORE = -5 + # after this many minutes old, a story cannot be edited MAX_EDIT_MINS = (60 * 6) @@ -381,7 +384,7 @@ class Story < ActiveRecord::Base end def is_downvotable? - if self.created_at && self.score >= -5 + if self.created_at && self.score >= DOWNVOTABLE_MIN_SCORE Time.now - self.created_at <= DOWNVOTABLE_DAYS.days else false From 49dd502de2bd969515a90646443a3c29b259a1eb Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:16:32 +0200 Subject: [PATCH 103/106] Comment: don't show downvote summary if we're not showing --- app/models/comment.rb | 12 ++++++++---- app/views/comments/_comment.html.erb | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 0308875..90fc4f8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -431,10 +431,8 @@ class Comment < ActiveRecord::Base self.upvotes - self.downvotes end - def score_for_user(user) - if (user && user.is_moderator?) || - (self.created_at && self.created_at < 24.hours.ago) || - !SCORE_RANGE_TO_HIDE.include?(score) + def score_for_user(u) + if self.showing_downvotes_for_user?(u) score else "-" @@ -445,6 +443,12 @@ class Comment < ActiveRecord::Base Rails.application.root_url + "c/#{self.short_id}" end + def showing_downvotes_for_user?(u) + return (u && u.is_moderator?) || + (self.created_at && self.created_at < 36.hours.ago) || + !SCORE_RANGE_TO_HIDE.include?(self.score) + end + def to_param self.short_id end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 6b47be9..bd506ff 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -100,8 +100,8 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? <% if comment.downvotes > 0 && - ((comment.score <= 0 && comment.user_id == @user.try(:id)) || - @user.try("is_moderator?")) %> + comment.showing_downvotes_for_user?(@user) && + (comment.user_id == @user.try(:id) || @user.try("is_moderator?")) %> | <%= comment.vote_summary_for_user(@user).downcase %> <% elsif comment.current_vote && comment.current_vote[:vote] == -1 %> | -1 From 44774bae6d1dc06cce327b849f3c4a59e3cb753c Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:18:03 +0200 Subject: [PATCH 104/106] log when blue logo shows up --- app/controllers/application_controller.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f62d64a..f3db4cb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -54,7 +54,10 @@ class ApplicationController < ActionController::Base end intensity = (@traffic * 7).floor + 50.0 - color = rand(2000000) == 1 ? "0000%02x" : "%02x0000" + if (blue = (rand(2000000) == 1)) && @user + Rails.logger.info " User #{@user.id} (#{@user.username}) saw blue logo" + end + color = (blue ? "0000%02x" : "%02x0000") @traffic_color = sprintf(color, intensity > 255 ? 255 : intensity) true From ecc97fb30aa951568e46c493e780c52ad7b6c324 Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Tue, 23 May 2017 14:33:59 +0200 Subject: [PATCH 105/106] login: provide specific error messages for certain failures - merged with i18n --- app/controllers/login_controller.rb | 35 +++++++++++++++++++---------- config/locales/en.yml | 5 ++++- config/locales/fr.yml | 5 +++++ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 3f23d1a..3ef5ff7 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,3 +1,8 @@ +class LoginBannedError < StandardError; end +class LoginDeletedError < StandardError; end +class LoginTOTPFailedError < StandardError; end +class LoginFailedError < StandardError; end + class LoginController < ApplicationController before_filter :authenticate_user @@ -22,9 +27,11 @@ class LoginController < ApplicationController user = User.where(:username => params[:email]).first end + fail_reason = nil + begin if !user - raise "no user" + raise LoginFailedError end if !user.authenticate(params[:password].to_s) @@ -36,18 +43,16 @@ class LoginController < ApplicationController params[:password] = m[1] params[:totp] = m[2] else - raise "authentication failed" + raise LoginFailedError end end if user.is_banned? - raise "user is banned" + raise LoginBannedError end if !user.is_active? - user.undelete! - flash[:success] = "Your account has been reactivated and your " << - "unmoderated comments have been undeleted." + raise LoginDeletedError end if !user.password_digest.to_s.match(/^\$2a\$#{BCrypt::Engine::DEFAULT_COST}\$/) @@ -60,7 +65,7 @@ class LoginController < ApplicationController if user.authenticate_totp(params[:totp]) # ok, fall through else - raise "invalid TOTP code" + raise LoginTOTPFailedError end else return respond_to do |format| @@ -100,18 +105,24 @@ class LoginController < ApplicationController render :json => { :status => 1, :username => user.username } } end - rescue + rescue LoginBannedError + fail_reason = I18n.t 'controllers.login_controller.bannedaccount' + rescue LoginDeletedError + fail_reason = I18n.t 'controllers.login_controller.deletedaccount' + rescue LoginTOTPFailedError + fail_reason = I18n.t 'controllers.login_controller.totpinvalid' + rescue LoginFailedError + fail_reason = I18n.t 'controllers.login_controller.flashlogininvalid' end respond_to do |format| format.html { - flash.now[:error] = I18n.t 'controllers.login_controller.flashlogininvalid' + flash.now[:error] = fail_reason @referer = params[:referer] index } format.json { - render :json => { :status => 0, - :error => "invalid 'email' and/or 'password' parameter" } + render :json => { :status => 0, :error => fail_reason } } end end @@ -171,7 +182,7 @@ class LoginController < ApplicationController end end else - flash[:error] = t(.invalidresettoken') + flash[:error] = t('.invalidresettoken') return redirect_to forgot_password_path end end diff --git a/config/locales/en.yml b/config/locales/en.yml index e679951..8c77136 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,7 +62,6 @@ en: messageslink: "Messages" loginlink: "Login" logoutlink: "Logout" - confirmlogoutlink: "Are you sure you want to logout?" moderationloglink: "Moderation Log" invitationqueuelink: "Invitation Queue" chatlink: "Chat" @@ -286,6 +285,7 @@ en: verifypasswordflash: "Your password could not be verified." index: logoutlink: "Logout" + confirmlogoutlink: "Are you sure you want to logout?" viewprofile: "View Profile" accountsettings: "Account Settings" username: "Username:" @@ -517,6 +517,9 @@ en: flashsuccessdeleteinvit: "Successfully deleted invitation request from %{name}" login_controller: flashlogininvalid: "Invalid e-mail address and/or password." + totpinvalid: "Your TOTP code was invalid." + deletedaccount: "Your account has been deleted." + bannedaccount: "Your account has been banned." messages_controller: messagestitle: "Messages" messagessenttitle: "Messages Sent" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 70f9a3f..2e0fb7e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -296,6 +296,7 @@ fr: verifypasswordflash: "Votre mot de passe n'a pas pu être vérifié." index: logoutlink: "Se déconnecter" + confirmlogoutlink: "Êtes-vous sûr de vouloir vous déconnecter?" viewprofile: "Voir le profil" accountsettings: "Paramètres du compte" username: "Utilisateur :" @@ -527,6 +528,10 @@ fr: flashsuccessdeleteinvit: "Demande d'invitation de %{name} supprimée avec succès" login_controller: flashlogininvalid: "Adresse e-mail et/ou mot de passe invalide." + flashlogininvalid: "Adresse e-mail et/ou mot de passe invalide." + totpinvalid: "Votre code TOTP est invalide." + deletedaccount: "Votre compte a été supprimé." + bannedaccount: "Votre compte a été banni." messages_controller: messagestitle: "Messages" messagessenttitle: "Messages envoyés" From 7d75f5aee9931a730d9d52cbf010c8d36e15bcea Mon Sep 17 00:00:00 2001 From: Carl Chenet Date: Sat, 8 Jul 2017 13:03:30 +0200 Subject: [PATCH 106/106] i18 of hide dragons message in settins --- app/views/settings/index.html.erb | 2 +- config/locales/en.yml | 1 + config/locales/fr.yml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 3d6146a..e9665a4 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -205,7 +205,7 @@
        - <%= f.label :hide_dragons, "Hide Dragons:", :class => "required" %> + <%= f.label :hide_dragons, t('.hidedragons'), :class => "required" %> <%= f.check_box :hide_dragons %>
        diff --git a/config/locales/en.yml b/config/locales/en.yml index 8c77136..6c03f07 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -320,6 +320,7 @@ en: miscsettings: "Miscellaneous Settings" storypreview: "Show Story Previews:" useravatars: "Show User Avatars:" + hidedragons: "Hide Dragons:" saveallsettings: "Save All Settings" inviteuser: "Invite a New User" cannotsendinvitations: "You cannot send invitations." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2e0fb7e..27a9847 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -331,6 +331,7 @@ fr: miscsettings: "Paramètres variés" storypreview: "Montrer les aperçus des infos: " useravatars: "Montrer les avatars des utilisateurs :" + hidedragons: "Cacher les dragons :" saveallsettings: "Sauver tous les paramètres" inviteuser: "Inviter un nouvel utilisateur" cannotsendinvitations: "Vous ne pouvez pas envoyer d'invitations."
    UserHatLink<%= t('.user') %><%= t('.hat') %><%= t('.link') %>
    <%= t('.strongtext') %> <%= raw t('.strongtextdesc') %>
    <%= t('.fixedwidth') %> <%= raw t('.fixedwidthdesc') %>