From 8fcbc2280f6c4717b49d7a84bf2d7a1bbab723a1 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 17 Mar 2020 15:05:08 -0300 Subject: [PATCH 01/19] actualizacion de seguridad --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f8e3fd74..da3f78d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -228,7 +228,7 @@ GEM request_store (~> 1.0) netaddr (2.0.4) nio4r (2.5.2) - nokogiri (1.10.7) + nokogiri (1.10.9) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) parallel (1.19.1) @@ -242,7 +242,7 @@ GEM coderay (~> 1.1.0) method_source (~> 0.9.0) public_suffix (4.0.2) - puma (4.3.1) + puma (4.3.3) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) From 26247a95440620ac897a26a88367f38791978511 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 19 Mar 2020 15:31:29 -0300 Subject: [PATCH 02/19] permitir cambiar de idioma --- app/controllers/posts_controller.rb | 21 +++++++++++++++------ app/services/post_service.rb | 13 ++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 4d999d9a..fbe9036d 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -11,21 +11,21 @@ class PostsController < ApplicationController @category = session[:category] = params.dig(:category) @layout = params.dig(:layout).try :to_sym # TODO: Aplicar policy_scope - @posts = @site.posts(lang: I18n.locale) + @posts = @site.posts(lang: lang) @posts.sort_by!(:order, :date).reverse! @usuarie = @site.usuarie? current_usuarie end def show @site = find_site - @post = @site.posts.find params[:id] + @post = @site.posts(lang: lang).find params[:id] authorize @post end def new authorize Post @site = find_site - @post = @site.posts.build(lang: I18n.locale, layout: params[:layout]) + @post = @site.posts.build(lang: lang, layout: params[:layout]) end def create @@ -44,14 +44,14 @@ class PostsController < ApplicationController def edit @site = find_site - @post = @site.posts.find params[:id] + @post = @site.posts(lang: lang).find params[:id] authorize @post end def update @site = find_site - @post = @site.posts.find params[:id] + @post = @site.posts(lang: lang).find params[:id] authorize @post @@ -70,7 +70,7 @@ class PostsController < ApplicationController # Eliminar artículos def destroy @site = find_site - @post = @site.posts.find params[:id] + @post = @site.posts(lang: lang).find params[:id] authorize @post @@ -96,4 +96,13 @@ class PostsController < ApplicationController service.reorder redirect_to site_posts_path(@site) end + + # Devuelve el idioma solicitado a través de un parámetro, validando + # que el sitio soporte ese idioma + def lang + return unless params[:lang] + return unless @site.try(:locales).try(:include?, params[:lang]) + + params[:lang] + end end diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 125d4eeb..4dfdba07 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -102,8 +102,19 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end end + # Devuelve el idioma solicitado a través de un parámetro, validando + # que el sitio soporte ese idioma + # + # TODO: DRY def lang - params[:post][:lang] || I18n.locale + return unless params[:lang] + return unless site.try(:locales).try(:include?, params[:lang]) + + params[:lang] + end + + def layout + params.dig(:post, :layout) || params[:layout] end end # rubocop:enable Metrics/BlockLength From 534dbaed282f77b254ff42f275cb36c82e11c42b Mon Sep 17 00:00:00 2001 From: f Date: Thu, 19 Mar 2020 15:46:33 -0300 Subject: [PATCH 03/19] actualizacion de seguridad --- Gemfile.lock | 186 ++++++++++++++++++++++++++------------------------- 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index da3f78d6..99234830 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,56 +16,56 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.0.2.1) - actionpack (= 6.0.2.1) + actioncable (6.0.2.2) + actionpack (= 6.0.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.2.1) - actionpack (= 6.0.2.1) - activejob (= 6.0.2.1) - activerecord (= 6.0.2.1) - activestorage (= 6.0.2.1) - activesupport (= 6.0.2.1) + actionmailbox (6.0.2.2) + actionpack (= 6.0.2.2) + activejob (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) mail (>= 2.7.1) - actionmailer (6.0.2.1) - actionpack (= 6.0.2.1) - actionview (= 6.0.2.1) - activejob (= 6.0.2.1) + actionmailer (6.0.2.2) + actionpack (= 6.0.2.2) + actionview (= 6.0.2.2) + activejob (= 6.0.2.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.2.1) - actionview (= 6.0.2.1) - activesupport (= 6.0.2.1) + actionpack (6.0.2.2) + actionview (= 6.0.2.2) + activesupport (= 6.0.2.2) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.2.1) - actionpack (= 6.0.2.1) - activerecord (= 6.0.2.1) - activestorage (= 6.0.2.1) - activesupport (= 6.0.2.1) + actiontext (6.0.2.2) + actionpack (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) nokogiri (>= 1.8.5) - actionview (6.0.2.1) - activesupport (= 6.0.2.1) + actionview (6.0.2.2) + activesupport (= 6.0.2.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.2.1) - activesupport (= 6.0.2.1) + activejob (6.0.2.2) + activesupport (= 6.0.2.2) globalid (>= 0.3.6) - activemodel (6.0.2.1) - activesupport (= 6.0.2.1) - activerecord (6.0.2.1) - activemodel (= 6.0.2.1) - activesupport (= 6.0.2.1) - activestorage (6.0.2.1) - actionpack (= 6.0.2.1) - activejob (= 6.0.2.1) - activerecord (= 6.0.2.1) + activemodel (6.0.2.2) + activesupport (= 6.0.2.2) + activerecord (6.0.2.2) + activemodel (= 6.0.2.2) + activesupport (= 6.0.2.2) + activestorage (6.0.2.2) + actionpack (= 6.0.2.2) + activejob (= 6.0.2.2) + activerecord (= 6.0.2.2) marcel (~> 0.3.1) - activesupport (6.0.2.1) + activesupport (6.0.2.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -74,7 +74,7 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) ast (2.4.0) - autoprefixer-rails (9.7.3) + autoprefixer-rails (9.7.4) execjs bcrypt (3.1.13) bcrypt_pbkdf (1.0.1) @@ -88,7 +88,7 @@ GEM autoprefixer-rails (>= 9.1.0) popper_js (>= 1.14.3, < 2) sassc-rails (>= 2.0.0) - brakeman (4.7.2) + brakeman (4.8.0) builder (3.2.4) capybara (2.18.0) addressable @@ -101,11 +101,11 @@ GEM childprocess (3.0.0) coderay (1.1.2) colorator (1.1.0) - commonmarker (0.20.2) + commonmarker (0.21.0) ruby-enum (~> 0.5) - concurrent-ruby (1.1.5) - crass (1.0.5) - database_cleaner (1.7.0) + concurrent-ruby (1.1.6) + crass (1.0.6) + database_cleaner (1.8.3) devise (4.7.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -125,7 +125,7 @@ GEM em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) - email_address (0.1.12) + email_address (0.1.15) netaddr (>= 2.0.4, < 3) simpleidn errbase (0.2.0) @@ -140,7 +140,7 @@ GEM factory_bot_rails (5.1.1) factory_bot (~> 5.1.0) railties (>= 4.2.0) - ffi (1.11.3) + ffi (1.12.2) forwardable-extended (2.6.0) friendly_id (5.3.0) activerecord (>= 4.0.0) @@ -151,7 +151,7 @@ GEM tilt haml-lint (0.999.999) haml_lint - haml_lint (0.34.1) + haml_lint (0.35.0) haml (>= 4.0, < 5.2) rainbow rubocop (>= 0.50.0) @@ -167,17 +167,17 @@ GEM railties (>= 4.0.1) hiredis (0.6.3) http_parser.rb (0.6.0) - i18n (1.7.0) + i18n (1.8.2) concurrent-ruby (~> 1.0) - image_processing (1.10.0) + image_processing (1.10.3) mini_magick (>= 4.9.5, < 5) - ruby-vips (>= 2.0.13, < 3) - inline_svg (1.6.0) + ruby-vips (>= 2.0.17, < 3) + inline_svg (1.7.1) activesupport (>= 3.0) nokogiri (>= 1.6) jaro_winkler (1.5.4) - jbuilder (2.9.1) - activesupport (>= 4.2.0) + jbuilder (2.10.0) + activesupport (>= 5.0.0) jekyll (4.0.0) addressable (~> 2.4) colorator (~> 1.0) @@ -193,15 +193,15 @@ GEM rouge (~> 3.0) safe_yaml (~> 1.0) terminal-table (~> 1.8) - jekyll-sass-converter (2.0.1) + jekyll-sass-converter (2.1.0) sassc (> 2.0.1, < 3.0) jekyll-watch (2.2.1) listen (~> 3.0) kramdown (2.1.0) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - launchy (2.4.3) - addressable (~> 2.3) + launchy (2.5.0) + addressable (~> 2.7) letter_opener (1.7.0) launchy (~> 2.2) liquid (4.0.3) @@ -218,12 +218,12 @@ GEM mimemagic (~> 0.3.2) mercenary (0.3.6) method_source (0.9.2) - mimemagic (0.3.3) - mini_magick (4.9.5) + mimemagic (0.3.4) + mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.13.0) - mobility (0.8.9) + minitest (5.14.0) + mobility (0.8.10) i18n (>= 0.6.10, < 2) request_store (~> 1.0) netaddr (2.0.4) @@ -232,16 +232,16 @@ GEM mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) parallel (1.19.1) - parser (2.7.0.1) + parser (2.7.0.4) ast (~> 2.4.0) pathutil (0.16.2) forwardable-extended (~> 2.6) - pg (1.2.0) - popper_js (1.14.5) + pg (1.2.3) + popper_js (1.16.0) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - public_suffix (4.0.2) + public_suffix (4.0.3) puma (4.3.3) nio4r (~> 2.0) pundit (2.1.0) @@ -251,20 +251,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.2.1) - actioncable (= 6.0.2.1) - actionmailbox (= 6.0.2.1) - actionmailer (= 6.0.2.1) - actionpack (= 6.0.2.1) - actiontext (= 6.0.2.1) - actionview (= 6.0.2.1) - activejob (= 6.0.2.1) - activemodel (= 6.0.2.1) - activerecord (= 6.0.2.1) - activestorage (= 6.0.2.1) - activesupport (= 6.0.2.1) + rails (6.0.2.2) + actioncable (= 6.0.2.2) + actionmailbox (= 6.0.2.2) + actionmailer (= 6.0.2.2) + actionpack (= 6.0.2.2) + actiontext (= 6.0.2.2) + actionview (= 6.0.2.2) + activejob (= 6.0.2.2) + activemodel (= 6.0.2.2) + activerecord (= 6.0.2.2) + activestorage (= 6.0.2.2) + activesupport (= 6.0.2.2) bundler (>= 1.3.0) - railties (= 6.0.2.1) + railties (= 6.0.2.2) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -276,9 +276,9 @@ GEM railties (>= 6.0.0, < 7) rails_warden (0.6.0) warden (>= 1.2.0) - railties (6.0.2.1) - actionpack (= 6.0.2.1) - activesupport (= 6.0.2.1) + railties (6.0.2.2) + actionpack (= 6.0.2.2) + activesupport (= 6.0.2.2) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -290,46 +290,48 @@ GEM rbnacl (4.0.2) ffi redis (4.1.3) - redis-actionpack (5.1.0) - actionpack (>= 4.0, < 7) - redis-rack (>= 1, < 3) + redis-actionpack (5.2.0) + actionpack (>= 5, < 7) + redis-rack (>= 2.1.0, < 3) redis-store (>= 1.1.0, < 2) redis-activesupport (5.2.0) activesupport (>= 3, < 7) redis-store (>= 1.3, < 2) - redis-rack (2.0.6) - rack (>= 1.5, < 3) + redis-rack (2.1.2) + rack (>= 2.0.8, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) redis-actionpack (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6) redis-store (>= 1.2, < 2) - redis-store (1.8.1) + redis-store (1.8.2) redis (>= 4, < 5) request_store (1.5.0) rack (>= 1.4) responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) - rouge (3.14.0) - rubocop (0.78.0) + rexml (3.2.4) + rouge (3.17.0) + rubocop (0.80.1) jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.6) + parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + rexml ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.4.1) + rubocop-rails (2.4.2) rack (>= 1.1) rubocop (>= 0.72.0) ruby-enum (0.7.2) i18n ruby-progressbar (1.10.1) - ruby-vips (2.0.16) + ruby-vips (2.0.17) ffi (~> 1.9) ruby_dep (1.5.0) - rubyzip (2.0.0) - rugged (0.28.4.1) + rubyzip (2.3.0) + rugged (0.99.0) safe_yaml (1.0.5) safely_block (0.3.0) errbase (>= 0.1.1) @@ -378,8 +380,8 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.6) - unicode-display_width (1.6.0) - validates_hostname (1.0.8) + unicode-display_width (1.6.1) + validates_hostname (1.0.10) activerecord (>= 3.0) activesupport (>= 3.0) warden (1.2.8) @@ -398,7 +400,7 @@ GEM websocket-extensions (0.1.4) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.2.2) + zeitwerk (2.3.0) PLATFORMS ruby From 6aa50520e8ca65fb2906cc635b92810777c4c449 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 23 Mar 2020 17:46:19 -0300 Subject: [PATCH 04/19] formulario de contacto, closes #120 --- app/controllers/api/v1/base_controller.rb | 6 ++ app/controllers/api/v1/contact_controller.rb | 93 +++++++++++++++++++ app/jobs/contact_job.rb | 8 ++ app/mailers/contact_mailer.rb | 33 +++++++ app/models/site.rb | 13 ++- .../contact_mailer/notify_usuaries.html.haml | 11 +++ .../contact_mailer/notify_usuaries.text.haml | 11 +++ config/environments/development.rb | 1 + config/environments/test.rb | 1 + config/locales/en.yml | 5 + config/locales/es.yml | 5 + config/routes.rb | 1 + .../api/v1/contact_controller_test.rb | 89 ++++++++++++++++++ 13 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 app/controllers/api/v1/contact_controller.rb create mode 100644 app/jobs/contact_job.rb create mode 100644 app/mailers/contact_mailer.rb create mode 100644 app/views/contact_mailer/notify_usuaries.html.haml create mode 100644 app/views/contact_mailer/notify_usuaries.text.haml create mode 100644 test/controllers/api/v1/contact_controller_test.rb diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 6f76e51a..25c10e65 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -6,6 +6,12 @@ module Api class BaseController < ActionController::Base protect_from_forgery with: :null_session respond_to :json + + private + + def origin + request.headers['Origin'] + end end end end diff --git a/app/controllers/api/v1/contact_controller.rb b/app/controllers/api/v1/contact_controller.rb new file mode 100644 index 00000000..429136fd --- /dev/null +++ b/app/controllers/api/v1/contact_controller.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Api + module V1 + # API para formulario de contacto + class ContactController < BaseController + # Aplicar algunos chequeos básicos. Deberíamos registrar de + # alguna forma los errores pero tampoco queremos que nos usen + # recursos. + # + # TODO: Agregar los mismos chequeos que en PostsController + before_action :site_exists?, unless: :performed? + before_action :site_is_origin?, unless: :performed? + before_action :from_is_address?, unless: :performed? + before_action :gave_consent?, unless: :performed? + + # Recibe un mensaje a través del formulario de contacto y lo envía + # a les usuaries del sitio. + # + # Tenemos que verificar que el sitio exista y que algunos campos + # estén llenos para detener spambots o DDOS. También nos vamos a + # estar apoyando en la limitación de peticiones en el servidor web. + def receive + # No hacer nada si no se pasaron los chequeos + return if performed? + + # Si todo salió bien, enviar los correos y redirigir al sitio. + # El sitio nos dice a dónde tenemos que ir. + ContactJob.perform_async site_id: site.id, + **contact_params.to_h.symbolize_keys + + redirect_to contact_params[:redirect] || site.url + end + + private + + # Comprueba que el sitio existe + # + # TODO: Responder con una zip bomb! + def site_exists? + render html: body(:site_exists), status: status unless site + end + + # Comprueba que el mensaje vino fue enviado desde el sitio + def site_is_origin? + return if origin.to_s == site.url + + render html: body(:site_is_origin), status: status + end + + # Detecta si la dirección de contacto es válida. Además es + # opcional. + def from_is_address? + return unless contact_params[:from] + return if EmailAddress.valid? contact_params[:from] + + render html: body(:from_is_address), status: status + end + + # No aceptar nada si no dió su consentimiento + def gave_consent? + return if contact_params[:gdpr].present? + + render html: body(:gave_consent), status: status + end + + # Encuentra el sitio + def site + @site ||= Site.find_by(name: params[:site_id]) + end + + # Parámetros limpios + def contact_params + @contact_params ||= params.permit(:gdpr, :name, :pronouns, :from, + :contact, :body, :redirect) + end + + # Para poder testear, enviamos un mensaje en el cuerpo de la + # respuesta + # + # @param [Any] el mensaje + def body(message) + return message.to_s if Rails.env.test? + end + + # No queremos informar nada a los spammers, pero en testeo + # queremos saber por qué. :no_content oculta el cuerpo. + def status + Rails.env.test? ? :unprocessable_entity : :no_content + end + end + end +end diff --git a/app/jobs/contact_job.rb b/app/jobs/contact_job.rb new file mode 100644 index 00000000..9425b512 --- /dev/null +++ b/app/jobs/contact_job.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Envía los mensajes de contacto +class ContactJob < ApplicationJob + def perform(**args) + ContactMailer.with(**args).notify_usuaries.deliver_now + end +end diff --git a/app/mailers/contact_mailer.rb b/app/mailers/contact_mailer.rb new file mode 100644 index 00000000..b372c531 --- /dev/null +++ b/app/mailers/contact_mailer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Formulario de contacto +class ContactMailer < ApplicationMailer + # Enviar el formulario de contacto a todes les usuaries + def notify_usuaries + # Enviar de a 10 usuaries para minimizar el riesgo que nos + # consideren spammers. + # + # TODO: #i18n. Agrupar usuaries por su idioma + usuaries.each_slice(10) do |u| + mail to: u, + reply_to: params[:from], + subject: I18n.t('contact_mailer.subject', site: site.title) + end + end + + private + + def site + @site ||= Site.find params[:site_id] + end + + # Trae solo les usuaries definitives para eliminar un vector de ataque + # donde alguien crea un sitio, agrega a muches usuaries y les envía + # correos. + # + # TODO: Mover a Site#usuaries + def usuaries + site.roles.where(rol: 'usuarie', temporal: false).includes(:usuarie) + .pluck(:email) + end +end diff --git a/app/models/site.rb b/app/models/site.rb index ffdb4473..d6e87b39 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -68,12 +68,14 @@ class Site < ApplicationRecord end def hostname + return @hostname if @hostname + sub = name || I18n.t('deploys.deploy_local.ejemplo') - if sub.ends_with? '.' - sub.gsub(/\.\Z/, '') - else - "#{sub}.#{Site.domain}" - end + @hostname = if sub.ends_with? '.' + sub.gsub(/\.\Z/, '') + else + "#{sub}.#{Site.domain}" + end end def url @@ -340,6 +342,7 @@ class Site < ApplicationRecord config.description = description config.title = title config.url = url + config.hostname = hostname end # Migra los archivos a Sutty diff --git a/app/views/contact_mailer/notify_usuaries.html.haml b/app/views/contact_mailer/notify_usuaries.html.haml new file mode 100644 index 00000000..02a74624 --- /dev/null +++ b/app/views/contact_mailer/notify_usuaries.html.haml @@ -0,0 +1,11 @@ +-# + Solo enviamos versión de texto para no aceptar HTML en el formulario + de contacto + +%p + %strong= I18n.t('contact_mailer.name', name: sanitize(@params[:name]), + pronouns: sanitize(@params[:pronouns])) + %strong= I18n.t('contact_mailer.contact', contact: sanitize(@params[:contact])) + %strong= I18n.t('contact_mailer.gdpr', gdpr: sanitize(@params[:gdpr])) + +%div= sanitize @params[:body] diff --git a/app/views/contact_mailer/notify_usuaries.text.haml b/app/views/contact_mailer/notify_usuaries.text.haml new file mode 100644 index 00000000..393ddd62 --- /dev/null +++ b/app/views/contact_mailer/notify_usuaries.text.haml @@ -0,0 +1,11 @@ +-# + Solo enviamos versión de texto para no aceptar HTML en el formulario + de contacto + += I18n.t('contact_mailer.name', + name: sanitize(@params[:name]), + pronouns: sanitize(@params[:pronouns])) += I18n.t('contact_mailer.contact', contact: sanitize(@params[:contact])) += I18n.t('contact_mailer.gdpr', gdpr: sanitize(@params[:gdpr])) +\ += sanitize @params[:body] diff --git a/config/environments/development.rb b/config/environments/development.rb index 5a47ac0d..ac21523e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -64,6 +64,7 @@ Rails.application.configure do # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] } config.action_mailer.perform_caching = false config.action_mailer.delivery_method = :letter_opener config.action_mailer.perform_deliveries = true diff --git a/config/environments/test.rb b/config/environments/test.rb index f3334615..2f5a5f1f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -44,6 +44,7 @@ Rails.application.configure do # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] } config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } diff --git a/config/locales/en.yml b/config/locales/en.yml index 00e591b9..06b0fd0f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,6 +36,11 @@ en: es: Castillian Spanish en: English seconds: '%{seconds} seconds' + contact_mailer: + subject: '[%site] Contact form' + name: 'Name: %{name} (%{pronouns})' + contact: 'Contact: %{contact}' + gdpr: 'Consent: %{gdpr}' deploy_mailer: deployed: subject: "[Sutty] The site %{site} has been built" diff --git a/config/locales/es.yml b/config/locales/es.yml index cd0764a8..4d310aeb 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -38,6 +38,11 @@ es: es: Castellano en: Inglés seconds: '%{seconds} segundos' + contact_mailer: + subject: '[%{site}] Formulario de contacto' + name: 'Nombre: %{name} (%{pronouns})' + contact: 'Contacto: %{contact}' + gdpr: 'Consentimiento: %{gdpr}' deploy_mailer: deployed: subject: "[Sutty] El sitio %{site} ha sido generado" diff --git a/config/routes.rb b/config/routes.rb index 9e6c0660..f89bc537 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ Rails.application.routes.draw do resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-\.]+/, id: /[a-z0-9\-\.]+/ } do get 'invitades/cookie', to: 'invitades#cookie' resources :posts, only: %i[create] + post :contact, to: 'contact#receive' end end end diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb new file mode 100644 index 00000000..815bb6c1 --- /dev/null +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Api + module V1 + class ContactControllerTest < ActionDispatch::IntegrationTest + setup do + @rol = create :rol + @site = @rol.site + @usuarie = @rol.usuarie + end + + teardown do + @site&.destroy + end + + test 'el sitio tiene que existir' do + @site.destroy + + post v1_site_contact_url(@site), + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex, + gdpr: true + } + + assert_response :unprocessable_entity, response.status + assert_equal 'site_exists', response.body + end + + test 'hay que enviar desde el sitio principal' do + post v1_site_contact_url(@site), + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex, + gdpr: true + } + + assert_response :unprocessable_entity, response.status + assert_equal 'site_is_origin', response.body + end + + test 'hay que dar consentimiento' do + post v1_site_contact_url(@site), + headers: { + Origin: @site.url + }, + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex + } + + assert_response :unprocessable_entity, response.status + assert_equal 'gave_consent', response.body + end + + test 'enviar un mensaje genera correos' do + 10.times do + create :rol, site: @site + end + + post v1_site_contact_url(@site), + headers: { + Origin: @site.url + }, + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex, + gdpr: true + } + + assert_equal 2, ActionMailer::Base.deliveries.size + end + end + end +end From be708940208f94ed47c5203e7269cf2c908d57d9 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 23 Mar 2020 17:46:59 -0300 Subject: [PATCH 05/19] mostrar las plantillas en una grilla --- app/views/sites/_form.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index eaa400ff..b46f51a5 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -39,7 +39,7 @@ .row -# Demasiado complejo para un f.collection_radio_buttons - Design.all.each do |design| - .col + .col-md-4 %h3 = f.radio_button :design_id, design.id, checked: design.id == site.design_id, From dcf8ada83f233f036a5a1076af1c4d60d099248d Mon Sep 17 00:00:00 2001 From: f Date: Mon, 23 Mar 2020 17:50:15 -0300 Subject: [PATCH 06/19] no aceptar path en los params --- app/models/metadata_path.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/metadata_path.rb b/app/models/metadata_path.rb index 474ddee7..7f386928 100644 --- a/app/models/metadata_path.rb +++ b/app/models/metadata_path.rb @@ -21,6 +21,9 @@ class MetadataPath < MetadataTemplate File.basename(value, ext) end + # No lo aceptamos en los parámetros + def to_param; end + private def ext From c1d69c6db47a8dcf60b434b70e26aeccb857b133 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 24 Mar 2020 14:06:33 -0300 Subject: [PATCH 07/19] permitir plantillas en forma de gemas y obtener sus layouts --- Gemfile | 8 +++++ Gemfile.lock | 44 +++++++++++++++++++++++++++ app/models/site.rb | 29 ++++++++++++++++-- db/seeds/designs.yml | 7 +++++ test/models/site_test.rb | 66 +++++++++++++++++++++++++--------------- 5 files changed, 127 insertions(+), 27 deletions(-) diff --git a/Gemfile b/Gemfile index 56de71d2..b7fca068 100644 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,8 @@ gem 'hiredis' gem 'image_processing' gem 'inline_svg' gem 'jekyll' +gem 'jekyll-data', require: 'jekyll-data', + git: 'https://0xacab.org/sutty/jekyll/jekyll-data.git' gem 'mini_magick' gem 'mobility' gem 'pg' @@ -69,6 +71,12 @@ gem 'validates_hostname' gem 'webpacker' gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git' +group :themes do + gem 'editorial-autogestiva-jekyll-theme', require: false + gem 'minima', require: false + gem 'sutty-jekyll-theme', require: false +end + group :development, :test do gem 'pry' # Adds support for Capybara system testing and selenium driver diff --git a/Gemfile.lock b/Gemfile.lock index 99234830..676949fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://0xacab.org/sutty/jekyll/jekyll-data.git + revision: 1ad9c175be6bbb31ae6d19cbb8dde18828af90d9 + specs: + jekyll-data (1.1.0) + jekyll (>= 3.3, < 5.0.0) + GIT remote: https://0xacab.org/sutty/reverse_markdown.git revision: 5c243096669aa77e0dc173dec8006b4c5fe07683 @@ -122,6 +129,15 @@ GEM dotenv (= 2.7.5) railties (>= 3.2, < 6.1) ed25519 (1.2.4) + editorial-autogestiva-jekyll-theme (0.1.0) + jekyll (~> 4.0) + jekyll-data (~> 1.1) + jekyll-feed (~> 0.9) + jekyll-images (~> 0.2) + jekyll-include-cache (~> 0) + jekyll-locales (~> 0.1) + jekyll-relative-urls (~> 0.0) + jekyll-seo-tag (~> 2.1) em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) @@ -193,8 +209,20 @@ GEM rouge (~> 3.0) safe_yaml (~> 1.0) terminal-table (~> 1.8) + jekyll-feed (0.13.0) + jekyll (>= 3.7, < 5.0) + jekyll-images (0.2.3) + ruby-filemagic (~> 0.7) + ruby-vips (~> 2) + jekyll-include-cache (0.2.0) + jekyll (>= 3.7, < 5.0) + jekyll-locales (0.1.7) + jekyll-relative-urls (0.0.5) + jekyll (>= 3.8, < 5) jekyll-sass-converter (2.1.0) sassc (> 2.0.1, < 3.0) + jekyll-seo-tag (2.6.1) + jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) kramdown (2.1.0) @@ -222,6 +250,10 @@ GEM mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) minitest (5.14.0) mobility (0.8.10) i18n (>= 0.6.10, < 2) @@ -326,6 +358,7 @@ GEM rubocop (>= 0.72.0) ruby-enum (0.7.2) i18n + ruby-filemagic (0.7.2) ruby-progressbar (1.10.1) ruby-vips (2.0.17) ffi (~> 1.9) @@ -362,6 +395,13 @@ GEM sqlite3 (1.4.2) sucker_punch (2.1.2) concurrent-ruby (~> 1.0) + sutty-jekyll-theme (0.1.0) + jekyll (~> 4.0) + jekyll-feed (~> 0.9) + jekyll-images (~> 0.2) + jekyll-include-cache (~> 0) + jekyll-relative-urls (~> 0.0) + jekyll-seo-tag (~> 2.1) sysexits (1.2.0) temple (0.8.2) terminal-table (1.8.0) @@ -419,6 +459,7 @@ DEPENDENCIES devise_invitable dotenv-rails ed25519 + editorial-autogestiva-jekyll-theme email_address exception_notification factory_bot_rails @@ -430,9 +471,11 @@ DEPENDENCIES inline_svg jbuilder (~> 2.5) jekyll + jekyll-data! letter_opener listen (>= 3.0.5, < 3.2) mini_magick + minima mobility pg pry @@ -454,6 +497,7 @@ DEPENDENCIES spring-watcher-listen (~> 2.0.0) sqlite3 sucker_punch + sutty-jekyll-theme terminal-table timecop turbolinks (~> 5) diff --git a/app/models/site.rb b/app/models/site.rb index d6e87b39..8a1b17cd 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -68,7 +68,7 @@ class Site < ApplicationRecord end def hostname - return @hostname if @hostname + return @hostname unless name_changed? || @hostname.blank? sub = name || I18n.t('deploys.deploy_local.ejemplo') @hostname = if sub.ends_with? '.' @@ -261,6 +261,8 @@ class Site < ApplicationRecord end def reload_jekyll! + reset + Dir.chdir(path) do @jekyll = Jekyll::Site.new(jekyll_config) end @@ -287,10 +289,13 @@ class Site < ApplicationRecord 'quiet' => true, 'excerpt_separator' => '') # No necesitamos cargar plugins en este momento - %w[plugins gems theme].each do |unneeded| + %w[plugins gems].each do |unneeded| configuration[unneeded] = [] if configuration.key? unneeded end + # Eliminar el theme si no es una gema válida + configuration.delete 'theme' unless theme_available? + # Si estamos usando nuestro propio plugin de i18n, los posts están # en "colecciones" locales.each do |i| @@ -300,6 +305,20 @@ class Site < ApplicationRecord configuration end + # Lista los nombres de las plantillas disponibles como gemas, + # tomándolas dinámicamente de las que agreguemos en el grupo :themes + # del Gemfile. + def available_themes + @available_themes ||= Bundler.load.current_dependencies.select do |gem| + gem.groups.include? :themes + end.map(&:name) + end + + # Detecta si el tema actual es una gema + def theme_available? + available_themes.include? design.gem + end + # Devuelve el dominio actual def self.domain ENV.fetch('SUTTY', 'sutty.nl') @@ -310,6 +329,12 @@ class Site < ApplicationRecord File.join(Rails.root, '_sites') end + def reset + @read = false + @layouts = nil + @layouts_struct = nil + end + private # Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada diff --git a/db/seeds/designs.yml b/db/seeds/designs.yml index 836f0990..047ef960 100644 --- a/db/seeds/designs.yml +++ b/db/seeds/designs.yml @@ -27,6 +27,13 @@ description_en: "A design with Sutty's look & feel" description_es: 'El diseño de Sutty' license: 'https://0xacab.org/sutty/jekyll/sutty-jekyll-theme/-/blob/master/LICENSE.txt' +- name_en: 'Self-managed Book Publisher' + name_es: 'Editorial Autogestiva' + gem: 'editorial-autogestiva-jekyll-theme' + url: 'https://subelamarea.sutty.nl/' + description_en: "A theme for self-managed book publishers." + description_es: 'Una plantilla para catálogos de editoriales autogestivas.' + license: 'https://0xacab.org/sutty/jekyll/editorial-autogestiva-jekyll-theme/-/blob/master/LICENSE.txt' - name_en: 'Other themes' name_es: 'Mi propio diseño' gem: 'sutty-theme-own' diff --git a/test/models/site_test.rb b/test/models/site_test.rb index bb6a4777..c50bfd20 100644 --- a/test/models/site_test.rb +++ b/test/models/site_test.rb @@ -3,14 +3,16 @@ require 'test_helper' class SiteTest < ActiveSupport::TestCase + def site + @site ||= create :site + end + # Asegurarse que el sitio se destruye al terminar de usarlo teardown do - @site&.destroy + site&.destroy end test 'se puede crear un sitio' do - site = create :site - assert site.valid? # TODO: Mover a la validación del sitio o hacer algo similar assert File.directory?(site.path) @@ -19,78 +21,92 @@ class SiteTest < ActiveSupport::TestCase end test 'el nombre tiene que ser único' do - @site = create :site - site2 = build :site, name: @site.name + site2 = build :site, name: site.name assert_not site2.valid? end test 'el nombre del sitio puede contener subdominios' do - site = build :site, name: 'hola.chau' + @site = build :site, name: 'hola.chau' site.validate assert_not site.errors.messages[:name].present? end test 'el nombre del sitio puede terminar con punto' do - site = build :site, name: 'hola.chau.' + @site = build :site, name: 'hola.chau.' site.validate assert_not site.errors.messages[:name].present? end test 'el nombre del sitio no puede contener wildcard' do - site = build :site, name: '*.chau' + @site = build :site, name: '*.chau' site.validate assert site.errors.messages[:name].present? end test 'el nombre del sitio solo tiene letras, numeros y guiones' do - site = build :site, name: 'A_Z!' + @site = build :site, name: 'A_Z!' site.validate assert site.errors.messages[:name].present? end test 'al destruir un sitio se eliminan los archivos' do - site = create :site + @site = create :site assert site.destroy assert !File.directory?(site.path) end test 'se puede leer un sitio' do - site = create :site - assert site.valid? assert !site.posts.empty? end test 'se pueden renombrar' do - @site = create :site - path = @site.path + path = site.path - @site.update_attribute :name, SecureRandom.hex + site.update_attribute :name, SecureRandom.hex - assert_not_equal path, @site.path - assert File.directory?(@site.path) + assert_not_equal path, site.path + assert File.directory?(site.path) assert_not File.directory?(path) end test 'no se puede guardar html en title y description' do - site = build :site - site.description = "hola" - site.title = "hola" + _site = build :site + _site.description = "hola" + _site.title = "hola" - assert_equal 'hola', site.description - assert_equal 'hola', site.title + assert_equal 'hola', _site.description + assert_equal 'hola', _site.title end test 'el sitio tiene artículos en distintos idiomas' do - @site = create :site - I18n.available_locales.each do |locale| - assert @site.posts(lang: locale).size.positive? + assert site.posts(lang: locale).size.positive? end end + + test 'tienen un hostname que puede cambiar' do + assert_equal "#{site.name}.#{Site.domain}", site.hostname + + site.name = name = SecureRandom.hex + + assert_equal "#{name}.#{Site.domain}", site.hostname + end + + test 'se pueden traer los datos de una plantilla' do + @site = create :site, design: Design.find_by(gem: 'editorial-autogestiva-jekyll-theme') + + assert_equal %i[post], site.layouts.to_h.keys + + site.config.write + site.reload + + assert_equal %w[book editorial post], site.data['layouts'].keys + assert_equal %i[book editorial post], site.layouts.to_h.keys + end end From bdedaf67cc998b8d443231010a04cbe2e63a2da9 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 24 Mar 2020 14:14:55 -0300 Subject: [PATCH 08/19] solo notificar a les usuaries confirmades --- app/jobs/deploy_job.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb index 85411cb0..a115720a 100644 --- a/app/jobs/deploy_job.rb +++ b/app/jobs/deploy_job.rb @@ -45,9 +45,8 @@ class DeployJob < ApplicationJob end def notify_usuaries - # TODO: existe site.usuaries_ids? - @site.usuaries.find_each do |usuarie| - DeployMailer.with(usuarie: usuarie.id, site: @site.id) + @site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuaries_id).each do |usuarie| + DeployMailer.with(usuarie: usuarie, site: @site.id) .deployed(@deployed) .deliver_now end From abae3b5141599661bc78b97bf75f774b9ff8d3c5 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 24 Mar 2020 19:11:30 -0300 Subject: [PATCH 09/19] campos numericos --- app/models/metadata_number.rb | 15 +++++++++++++++ app/views/posts/attribute_ro/_number.haml | 3 +++ app/views/posts/attributes/_number.haml | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 app/models/metadata_number.rb create mode 100644 app/views/posts/attribute_ro/_number.haml create mode 100644 app/views/posts/attributes/_number.haml diff --git a/app/models/metadata_number.rb b/app/models/metadata_number.rb new file mode 100644 index 00000000..9422ed16 --- /dev/null +++ b/app/models/metadata_number.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Un campo numérico +class MetadataNumber < MetadataTemplate + # Nada + def default_value + nil + end + + def save + self[:value] = value.to_i + + true + end +end diff --git a/app/views/posts/attribute_ro/_number.haml b/app/views/posts/attribute_ro/_number.haml new file mode 100644 index 00000000..31dd8f0d --- /dev/null +++ b/app/views/posts/attribute_ro/_number.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td= metadata.value diff --git a/app/views/posts/attributes/_number.haml b/app/views/posts/attributes/_number.haml new file mode 100644 index 00000000..ec2adb53 --- /dev/null +++ b/app/views/posts/attributes/_number.haml @@ -0,0 +1,6 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + = number_field 'post', attribute, value: metadata.value, + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata From eb2bdacd5c5df089e0ce4e9c04991acafa11d425 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 25 Mar 2020 15:36:06 -0300 Subject: [PATCH 10/19] usar el nombre del sitio y no el hostname --- app/controllers/api/v1/contact_controller.rb | 6 +++++- test/controllers/api/v1/contact_controller_test.rb | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/contact_controller.rb b/app/controllers/api/v1/contact_controller.rb index 429136fd..52ce52df 100644 --- a/app/controllers/api/v1/contact_controller.rb +++ b/app/controllers/api/v1/contact_controller.rb @@ -64,9 +64,13 @@ module Api render html: body(:gave_consent), status: status end + def site_id + params[:site_id].gsub(/\.#{Site.domain}\z/, '') + end + # Encuentra el sitio def site - @site ||= Site.find_by(name: params[:site_id]) + @site ||= Site.find_by(name: site_id) end # Parámetros limpios diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb index 815bb6c1..1a525bcb 100644 --- a/test/controllers/api/v1/contact_controller_test.rb +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -65,11 +65,15 @@ module Api end test 'enviar un mensaje genera correos' do + ActionMailer::Base.deliveries.clear + + redirect = "#{@site.url}/?thanks" + 10.times do create :rol, site: @site end - post v1_site_contact_url(@site), + post v1_site_contact_url(site_id: @site.hostname), headers: { Origin: @site.url }, @@ -79,9 +83,11 @@ module Api contact: SecureRandom.hex, from: "#{SecureRandom.hex}@sutty.nl", body: SecureRandom.hex, - gdpr: true + gdpr: true, + redirect: redirect } + assert_equal redirect, response.headers['Location'] assert_equal 2, ActionMailer::Base.deliveries.size end end From 29947b4ed5ae998064a000adc0e9117a6114ee91 Mon Sep 17 00:00:00 2001 From: f Date: Wed, 25 Mar 2020 15:49:36 -0300 Subject: [PATCH 11/19] hostname en los formularios de contacto --- app/controllers/api/v1/contact_controller.rb | 7 +++- app/models/site.rb | 13 ++++--- .../api/v1/contact_controller_test.rb | 35 +++++++++++++++++-- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/contact_controller.rb b/app/controllers/api/v1/contact_controller.rb index 52ce52df..1b247250 100644 --- a/app/controllers/api/v1/contact_controller.rb +++ b/app/controllers/api/v1/contact_controller.rb @@ -64,8 +64,13 @@ module Api render html: body(:gave_consent), status: status end + # Realiza la inversa de Site#hostname def site_id - params[:site_id].gsub(/\.#{Site.domain}\z/, '') + @site_id ||= if params[:site_id].end_with? Site.domain + params[:site_id].gsub(/\.#{Site.domain}\z/, '') + else + "#{params[:site_id]}." + end end # Encuentra el sitio diff --git a/app/models/site.rb b/app/models/site.rb index 8a1b17cd..8a89f641 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -68,14 +68,13 @@ class Site < ApplicationRecord end def hostname - return @hostname unless name_changed? || @hostname.blank? - sub = name || I18n.t('deploys.deploy_local.ejemplo') - @hostname = if sub.ends_with? '.' - sub.gsub(/\.\Z/, '') - else - "#{sub}.#{Site.domain}" - end + + if sub.ends_with? '.' + sub.gsub(/\.\Z/, '') + else + "#{sub}.#{Site.domain}" + end end def url diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb index 1a525bcb..e2aabfd3 100644 --- a/test/controllers/api/v1/contact_controller_test.rb +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -18,7 +18,7 @@ module Api test 'el sitio tiene que existir' do @site.destroy - post v1_site_contact_url(@site), + post v1_site_contact_url(site_id: @site.hostname), params: { name: SecureRandom.hex, pronouns: SecureRandom.hex, @@ -33,7 +33,7 @@ module Api end test 'hay que enviar desde el sitio principal' do - post v1_site_contact_url(@site), + post v1_site_contact_url(site_id: @site.hostname), params: { name: SecureRandom.hex, pronouns: SecureRandom.hex, @@ -48,7 +48,7 @@ module Api end test 'hay que dar consentimiento' do - post v1_site_contact_url(@site), + post v1_site_contact_url(site_id: @site.hostname), headers: { Origin: @site.url }, @@ -90,6 +90,35 @@ module Api assert_equal redirect, response.headers['Location'] assert_equal 2, ActionMailer::Base.deliveries.size end + + test 'se puede enviar mensajes a dominios propios' do + ActionMailer::Base.deliveries.clear + + @site.update name: 'example.org.' + + redirect = "#{@site.url}?thanks" + + 10.times do + create :rol, site: @site + end + + post v1_site_contact_url(site_id: @site.hostname), + headers: { + Origin: @site.url + }, + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex, + gdpr: true, + redirect: redirect + } + + assert_equal redirect, response.headers['Location'] + assert_equal 2, ActionMailer::Base.deliveries.size + end end end end From b0c2896f5f360ad3f6499928f328eea63eaa04ec Mon Sep 17 00:00:00 2001 From: f Date: Wed, 25 Mar 2020 15:50:07 -0300 Subject: [PATCH 12/19] realmente enviar multiples correos --- app/jobs/contact_job.rb | 28 +++++++++++++++++++++++++++- app/mailers/contact_mailer.rb | 30 ++++-------------------------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/app/jobs/contact_job.rb b/app/jobs/contact_job.rb index 9425b512..849c5079 100644 --- a/app/jobs/contact_job.rb +++ b/app/jobs/contact_job.rb @@ -3,6 +3,32 @@ # Envía los mensajes de contacto class ContactJob < ApplicationJob def perform(**args) - ContactMailer.with(**args).notify_usuaries.deliver_now + @params = args + + # Enviar de a 10 usuaries para minimizar el riesgo que nos + # consideren spammers. + # + # TODO: #i18n. Agrupar usuaries por su idioma + + usuaries.each_slice(10) do |u| + ContactMailer.with(**args.merge(usuaries: u, title: site.title)) + .notify_usuaries.deliver_now + end + end + + private + + def site + @site ||= Site.find @params[:site_id] + end + + # Trae solo les usuaries definitives para eliminar un vector de ataque + # donde alguien crea un sitio, agrega a muches usuaries y les envía + # correos. + # + # TODO: Mover a Site#usuaries + def usuaries + site.roles.where(rol: 'usuarie', temporal: false).includes(:usuarie) + .pluck(:email) end end diff --git a/app/mailers/contact_mailer.rb b/app/mailers/contact_mailer.rb index b372c531..058608e6 100644 --- a/app/mailers/contact_mailer.rb +++ b/app/mailers/contact_mailer.rb @@ -2,32 +2,10 @@ # Formulario de contacto class ContactMailer < ApplicationMailer - # Enviar el formulario de contacto a todes les usuaries + # Enviar el formulario de contacto a les usuaries def notify_usuaries - # Enviar de a 10 usuaries para minimizar el riesgo que nos - # consideren spammers. - # - # TODO: #i18n. Agrupar usuaries por su idioma - usuaries.each_slice(10) do |u| - mail to: u, - reply_to: params[:from], - subject: I18n.t('contact_mailer.subject', site: site.title) - end - end - - private - - def site - @site ||= Site.find params[:site_id] - end - - # Trae solo les usuaries definitives para eliminar un vector de ataque - # donde alguien crea un sitio, agrega a muches usuaries y les envía - # correos. - # - # TODO: Mover a Site#usuaries - def usuaries - site.roles.where(rol: 'usuarie', temporal: false).includes(:usuarie) - .pluck(:email) + mail to: params[:usuaries], + reply_to: params[:from], + subject: I18n.t('contact_mailer.subject', site: params[:title]) end end From 0485774507c040cef8eb558221e3b8de9b6541ff Mon Sep 17 00:00:00 2001 From: f Date: Wed, 25 Mar 2020 16:46:16 -0300 Subject: [PATCH 13/19] poder elegir valores de una lista predefinida --- Gemfile.lock | 2 +- app/models/metadata_predefined_array.rb | 10 ++++++++++ app/models/metadata_related_posts.rb | 2 +- .../posts/attribute_ro/_predefined_array.haml | 5 +++++ .../posts/attributes/_predefined_array.haml | 19 +++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 app/models/metadata_predefined_array.rb create mode 100644 app/views/posts/attribute_ro/_predefined_array.haml create mode 100644 app/views/posts/attributes/_predefined_array.haml diff --git a/Gemfile.lock b/Gemfile.lock index 676949fd..ce9198f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,7 +129,7 @@ GEM dotenv (= 2.7.5) railties (>= 3.2, < 6.1) ed25519 (1.2.4) - editorial-autogestiva-jekyll-theme (0.1.0) + editorial-autogestiva-jekyll-theme (0.2.2) jekyll (~> 4.0) jekyll-data (~> 1.1) jekyll-feed (~> 0.9) diff --git a/app/models/metadata_predefined_array.rb b/app/models/metadata_predefined_array.rb new file mode 100644 index 00000000..a6e2105f --- /dev/null +++ b/app/models/metadata_predefined_array.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Una lista de valores predefinidos +class MetadataPredefinedArray < MetadataArray + def values + @values ||= layout[:metadata][name]['values'].map do |k, v| + { v[I18n.locale.to_s] => k } + end.inject(&:merge) + end +end diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb index 7d2273a0..0ebc8a02 100644 --- a/app/models/metadata_related_posts.rb +++ b/app/models/metadata_related_posts.rb @@ -5,7 +5,7 @@ class MetadataRelatedPosts < MetadataArray # Genera un Hash de { title | slug => uuid } def values - site.posts(lang: lang).map do |p| + @values ||= site.posts(lang: lang).map do |p| { title(p) => p.uuid.value } end.inject(:merge) end diff --git a/app/views/posts/attribute_ro/_predefined_array.haml b/app/views/posts/attribute_ro/_predefined_array.haml new file mode 100644 index 00000000..d5d13b2c --- /dev/null +++ b/app/views/posts/attribute_ro/_predefined_array.haml @@ -0,0 +1,5 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + - metadata.value.each do |v| + %span.badge.badge-primary= metadata.values.key v diff --git a/app/views/posts/attributes/_predefined_array.haml b/app/views/posts/attributes/_predefined_array.haml new file mode 100644 index 00000000..8984cb4e --- /dev/null +++ b/app/views/posts/attributes/_predefined_array.haml @@ -0,0 +1,19 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + + .mapable{ data: { values: metadata.value.to_json, + 'default-values': metadata.values.to_json, + name: "post[#{attribute}][]", list: id_for_datalist(attribute), + remove: 'false', legend: post_label_t(attribute, post: post), + described: id_for_help(attribute) } } + + = text_field(*field_name_for('post', attribute, '[]'), + value: metadata.value.join(', '), + **field_options(attribute, metadata)) + + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata + + %datalist{ id: id_for_datalist(attribute) } + - metadata.values.keys.each do |value| + %option{ value: value } From cb738963bbcc2fce10fe2d34875a809ac48b9af9 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 30 Mar 2020 17:37:04 -0300 Subject: [PATCH 14/19] longitud --- app/views/posts/attributes/_geo.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/attributes/_geo.haml b/app/views/posts/attributes/_geo.haml index 839393d8..e3b3ab46 100644 --- a/app/views/posts/attributes/_geo.haml +++ b/app/views/posts/attributes/_geo.haml @@ -16,4 +16,4 @@ value: metadata.value['lng'], **field_options(attribute, metadata)) = render 'posts/attribute_feedback', - post: post, attribute: [attribute, :lat], metadata: metadata + post: post, attribute: [attribute, :lng], metadata: metadata From 21827da354a83d9cf895a7d72bcda7d7efe8802d Mon Sep 17 00:00:00 2001 From: f Date: Tue, 31 Mar 2020 17:20:34 -0300 Subject: [PATCH 15/19] =?UTF-8?q?las=20plantillas=20pueden=20informar=20el?= =?UTF-8?q?=20nombre=20del=20tipo=20de=20art=C3=ADculo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/site.rb | 5 +++++ app/views/posts/index.haml | 2 +- config/locales/en.yml | 2 +- config/locales/es.yml | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/site.rb b/app/models/site.rb index 8a89f641..058e16e6 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -124,6 +124,11 @@ class Site < ApplicationRecord config.fetch('locales', I18n.available_locales.map(&:to_s)) end + # Similar a site.i18n en jekyll-locales + def i18n + data[I18n.locale.to_s] + end + # Devuelve el idioma por defecto del sitio, el primero de la lista. def default_locale locales.first diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index cded824e..ee8355a6 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -14,7 +14,7 @@ %h3= t('posts.new') %ul - @site.layouts.to_h.keys.each do |layout| - %li= link_to layout.to_s.humanize, + %li= link_to @site.i18n.dig('layouts', layout.to_s) || layout.to_s.humanize, new_site_post_path(@site, layout: layout) - if policy(@site).edit? diff --git a/config/locales/en.yml b/config/locales/en.yml index 06b0fd0f..54022396 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -387,7 +387,7 @@ en: date: 'date' order: 'Order' content: 'Text' - new: 'New post as' + new: 'Add:' dropdown: 'Toggle dropdown' categories: 'Everything' index: 'Posts' diff --git a/config/locales/es.yml b/config/locales/es.yml index 4d310aeb..816f5b0b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -397,7 +397,7 @@ es: content: 'Cuerpo del artículo' categories: 'Todos' dropdown: 'Desplegar el menú' - new: 'Crear artículo en:' + new: 'Agregar:' index: 'Artículos' edit: 'Editar' open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar' From e5aaed952451bcca1d5ef039eb4b2c46c5a0e983 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 31 Mar 2020 17:52:41 -0300 Subject: [PATCH 16/19] guardar los valores falsos closes #141 --- app/models/metadata_boolean.rb | 12 ++++++------ app/views/posts/attributes/_boolean.haml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/metadata_boolean.rb b/app/models/metadata_boolean.rb index f0c64c4f..113b9d5f 100644 --- a/app/models/metadata_boolean.rb +++ b/app/models/metadata_boolean.rb @@ -15,14 +15,14 @@ class MetadataBoolean < MetadataTemplate # En este caso, queremos priorizar el dato enviado por le usuarie # antes que el generado internamente. def value - return false if false? + return false if self[:value] == '0' + return self[:value] unless self[:value].nil? - self[:value].present? || document.data.fetch(name.to_s, default_value) + document.data.fetch(name.to_s, default_value) end - private - - def false? - self[:value] == '0' + def save + self[:value] = !%w[0 false].include?(self[:value]) + true end end diff --git a/app/views/posts/attributes/_boolean.haml b/app/views/posts/attributes/_boolean.haml index 7814ebbe..46ab667a 100644 --- a/app/views/posts/attributes/_boolean.haml +++ b/app/views/posts/attributes/_boolean.haml @@ -1,6 +1,6 @@ .form-check = hidden_field_tag "post[#{attribute}]", '0' - = check_box_tag "post[#{attribute}]", metadata.value, metadata.value, + = check_box_tag "post[#{attribute}]", '1', metadata.value, class: "form-check-input #{invalid(post, attribute)}", aria: { describedby: id_for_help(attribute) }, autofocus: autofocus From b0ae8e66e7ffc750ef6860fe8b640fc6338806bd Mon Sep 17 00:00:00 2001 From: f Date: Tue, 31 Mar 2020 18:30:09 -0300 Subject: [PATCH 17/19] =?UTF-8?q?grilla=20de=20dise=C3=B1os=20closes=20#13?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.scss | 10 ++++++++++ app/views/sites/_form.haml | 18 ++++++++++-------- config/locales/en.yml | 2 +- config/locales/es.yml | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index bfe5e24c..633aa3b6 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -262,3 +262,13 @@ svg { height: 1rem; } } + +.custom-control-label { + font-weight: bold; +} + +.designs { + .design { + margin-top: 1rem; + } +} diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index b46f51a5..eedcd7cd 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -36,18 +36,20 @@ .form-group %h2= t('.design.title') %p.lead= t('.help.design') - .row + .row.designs -# Demasiado complejo para un f.collection_radio_buttons - - Design.all.each do |design| - .col-md-4 - %h3 + - Design.all.find_each do |design| + .design.col-md-4.d-flex.flex-column + .custom-control.custom-radio = f.radio_button :design_id, design.id, checked: design.id == site.design_id, disabled: design.disabled, - required: true - = f.label "design_id_#{design.id}", design.name - = sanitize_markdown design.description, - tags: %w[p a strong em] + required: true, class: 'custom-control-input' + = f.label "design_id_#{design.id}", design.name, + class: 'custom-control-label' + .flex-fill + = sanitize_markdown design.description, + tags: %w[p a strong em] .btn-group{ role: 'group', 'aria-label': t('.design.actions') } - if design.url diff --git a/config/locales/en.yml b/config/locales/en.yml index 54022396..20b6aaf6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -317,7 +317,7 @@ en: title: 'Design' actions: 'Information about this design' url: 'Demo' - licencia: 'Read the license' + licencia: 'License' licencia: title: 'License for the site and everything published on it' url: 'Read the license' diff --git a/config/locales/es.yml b/config/locales/es.yml index 816f5b0b..7b7cdace 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -325,7 +325,7 @@ es: title: 'Diseño' actions: 'Información sobre este diseño' url: 'Demostración' - license: 'Leer la licencia' + license: 'Licencia' licencia: title: 'Licencia del sitio y todo lo publicado' url: 'Leer la licencia' From af956e284d5af9d2b0dc9f45b08965f1641625a7 Mon Sep 17 00:00:00 2001 From: f Date: Tue, 31 Mar 2020 18:40:21 -0300 Subject: [PATCH 18/19] listado de licencias --- app/views/sites/_form.haml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index eedcd7cd..4bb98cfe 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -60,25 +60,26 @@ target: '_blank', class: 'btn' %hr/ - .form-group + .form-group.licenses %h2= t('.licencia.title') %p.lead= t('.help.licencia') - - Licencia.all.each do |licencia| - .row + - Licencia.all.find_each do |licencia| + .row.license .col - %h3 - = f.radio_button :licencia_id, licencia.id, - checked: licencia.id == site.licencia_id, - required: true - = f.label "licencia_id_#{licencia.id}" do - = image_tag licencia.icons, alt: licencia.name - = licencia.name - = sanitize_markdown licencia.description, - tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6] + .media.mt-1 + = image_tag licencia.icons, alt: licencia.name, class: 'mr-3 mt-4' + .media-body + .custom-control.custom-radio + = f.radio_button :licencia_id, licencia.id, + checked: licencia.id == site.licencia_id, + required: true, class: 'custom-control-input' + = f.label "licencia_id_#{licencia.id}", class: 'custom-control-label' do + = licencia.name + = sanitize_markdown licencia.description, + tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6] - .btn-group{ role: 'group', 'aria-label': t('.licencia.actions') } - = link_to t('.licencia.url'), licencia.url, - target: '_blank', class: 'btn' + = link_to t('.licencia.url'), licencia.url, + target: '_blank', class: 'btn' %hr/ From 4e51ccd8612843e554ea0032d1edd65bbfc8f34d Mon Sep 17 00:00:00 2001 From: f Date: Tue, 31 Mar 2020 18:58:25 -0300 Subject: [PATCH 19/19] eliminar reverse markdown --- Gemfile | 1 - Gemfile.lock | 8 -------- 2 files changed, 9 deletions(-) diff --git a/Gemfile b/Gemfile index b7fca068..fbde9705 100644 --- a/Gemfile +++ b/Gemfile @@ -62,7 +62,6 @@ gem 'rails-i18n' gem 'rails_warden' gem 'redis', require: %w[redis redis/connection/hiredis] gem 'redis-rails' -gem 'reverse_markdown', git: 'https://0xacab.org/sutty/reverse_markdown.git' gem 'rubyzip' gem 'rugged' gem 'sucker_punch' diff --git a/Gemfile.lock b/Gemfile.lock index ce9198f0..5e41526d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,13 +5,6 @@ GIT jekyll-data (1.1.0) jekyll (>= 3.3, < 5.0.0) -GIT - remote: https://0xacab.org/sutty/reverse_markdown.git - revision: 5c243096669aa77e0dc173dec8006b4c5fe07683 - specs: - reverse_markdown (1.2.0) - nokogiri - GIT remote: https://0xacab.org/sutty/yaml_db.git revision: 40e44c29ce4290dfe4013ff4fce1be5a936fedf4 @@ -487,7 +480,6 @@ DEPENDENCIES rbnacl (< 5.0) redis redis-rails - reverse_markdown! rubocop-rails rubyzip rugged