diff --git a/Gemfile b/Gemfile index f2c47ca..64562fc 100644 --- a/Gemfile +++ b/Gemfile @@ -57,6 +57,10 @@ gem 'jekyll', git: 'https://0xacab.org/sutty/jekyll/jekyll.git', branch: 'master' gem 'jekyll-data', require: 'jekyll-data', git: 'https://0xacab.org/sutty/jekyll/jekyll-data.git' +gem 'jekyll-commonmark' +gem 'jekyll-images' +gem 'jekyll-include-cache' +gem 'sutty-liquid' gem 'lockbox' gem 'mini_magick' gem 'mobility' diff --git a/Gemfile.lock b/Gemfile.lock index cf68a83..4da3a62 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -236,6 +236,9 @@ GEM nokogiri (>= 1.6) jbuilder (2.10.1) activesupport (>= 5.0.0) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) jekyll-feed (0.15.1) jekyll (>= 3.7, < 5.0) jekyll-images (0.2.7) @@ -590,7 +593,10 @@ DEPENDENCIES inline_svg jbuilder (~> 2.5) jekyll! + jekyll-commonmark jekyll-data! + jekyll-images + jekyll-include-cache letter_opener listen (>= 3.0.5, < 3.2) lockbox @@ -625,6 +631,7 @@ DEPENDENCIES sucker_punch sutty-donaciones-jekyll-theme sutty-jekyll-theme + sutty-liquid sutty-minima symbol-fstring terminal-table diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index abf1229..2f09cea 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base around_action :set_locale rescue_from ActionController::RoutingError, with: :page_not_found + rescue_from ActionController::ParameterMissing, with: :page_not_found before_action do Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl')) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index ecf5af8..f727da6 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -52,6 +52,18 @@ class PostsController < ApplicationController fresh_when @post end + # Genera una previsualización del artículo. + # + # TODO: No todos los artículos tienen previsualización! + def preview + @site = find_site + @post = @site.posts(lang: locale).find params[:post_id] + + authorize @post + + render html: @post.render + end + def new authorize Post @site = find_site diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 33d76b7..d6fa37c 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -106,6 +106,27 @@ class SitesController < ApplicationController redirect_to sites_path end + # Obtiene y streamea archivos estáticos desde el repositorio mismo, + # pero sólo los públicos (es decir los archivos subidos desde Sutty). + def static_file + authorize site + + file = params.require(:file) + '.' + params.require(:format) + + raise ActionController::RoutingError unless file.start_with? 'public/' + + path = site.relative_path file + + raise ActionController::RoutingError unless File.exist? path + + # TODO: Hacer esto usa recursos, pero menos que generar el sitio + # cada vez. Para poder usar X-Accel tendríamos que montar los + # repositorios en el servidor web, cosa que no queremos, o hacer + # links simbólicos desde todos los public, o usar un servidor web + # local que soporte sendfile mejor que Rails (nghttpd?) + send_file path + end + private def site diff --git a/app/lib/jekyll/tags/base.rb b/app/lib/jekyll/tags/base.rb new file mode 100644 index 0000000..c4fb234 --- /dev/null +++ b/app/lib/jekyll/tags/base.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Jekyll + module Tags + class Base < Liquid::Tag + def render(context) + context.registers[:site].config['url'] + end + end + end +end diff --git a/app/lib/jekyll/tags/empty.rb b/app/lib/jekyll/tags/empty.rb new file mode 100644 index 0000000..82625a7 --- /dev/null +++ b/app/lib/jekyll/tags/empty.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Jekyll + module Tags + # Genera un tag vacío que sirve para reemplazar tags provistos por + # complementos que no vamos a cargar dentro del panel (seo, feed, + # etc.) Lo correcto sería modificar Liquid::Document para que + # ignore los tags desconocidos, pero en nuestras pruebas los toma + # como el comienzo de un bloque e ignora HTML adyacente, así que + # preferimos avanzar con una lista predeterminada. + # + # @see config/initializers/core_extensions.rb + class Empty < Liquid::Tag + def render(_) + '' + end + end + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 4a80cab..c238ece 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -75,6 +75,51 @@ class Post < OpenStruct "#" end + # Renderiza el artículo para poder previsualizarlo. Leemos solo la + # información básica, con lo que no van a funcionar artículos + # relacionados y otras cuestiones. + # + # @see app/lib/jekyll/tags/base.rb + def render + Dir.chdir site.path do + # Compatibilidad con jekyll-locales, necesario para el filtro + # date_local + # + # TODO: Cambiar el locale en otro lado + site.jekyll.config['lang'] = lang.value + site.jekyll.config['locale'] = lang.value + + # Payload básico con traducciones. + document.renderer.payload = { + 'site' => { + 'data' => site.data, + 'i18n' => site.data[lang.value], + 'lang' => lang.value, + 'locale' => lang.value + }, + 'page' => document.to_liquid + } + + # Renderizar lo estrictamente necesario y convertir a HTML para + # poder reemplazar valores. + html = Nokogiri::HTML document.renderer.render_document + # Las imágenes se cargan directamente desde el repositorio, porque + # no son públicas hasta que se publica el artículo. + html.css('img').each do |img| + next if %r{\Ahttps?://} =~ img.attributes['src'] + + img.attributes['src'].value = Rails.application.routes.url_helpers.site_static_file_url(site, file: img.attributes['src'].value) + end + + # Notificar a les usuaries que están viendo una previsualización + # XXX: Asume que estamos usando Bootstrap :B + html.at_css('body').first_element_child.before("
#{I18n.t('posts.preview_message')}
") + + # Cacofonía + html.to_html.html_safe + end + end + # Devuelve una llave para poder guardar el post en una cache def cache_key 'posts/' + uuid.value diff --git a/app/models/site.rb b/app/models/site.rb index 4a6dfbf..e6530a9 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -146,6 +146,13 @@ class Site < ApplicationRecord File.join(Site.site_path, name_was) end + # Limpiar la ruta y unirla con el separador de directorios del + # sistema operativo. Como si algún día fuera a cambiar o + # soportáramos Windows :P + def relative_path(suspicious_path) + File.join(path, *suspicious_path.gsub('..', '/').gsub('./', '').squeeze('/').split('/')) + end + # Obtiene la lista de traducciones actuales # # Siempre tiene que tener algo porque las traducciones están diff --git a/app/policies/post_policy.rb b/app/policies/post_policy.rb index e249d9f..c22202a 100644 --- a/app/policies/post_policy.rb +++ b/app/policies/post_policy.rb @@ -18,6 +18,10 @@ class PostPolicy post.site.usuarie?(usuarie) || post.usuaries.include?(usuarie) end + def preview? + show? + end + def new? create? end diff --git a/app/policies/site_policy.rb b/app/policies/site_policy.rb index 33bdb2a..2ca9625 100644 --- a/app/policies/site_policy.rb +++ b/app/policies/site_policy.rb @@ -24,6 +24,11 @@ class SitePolicy !current_role.temporal end + # Todes pueden ver los archivos + def static_file? + true + end + # Todes pueden crear nuevos sitios def new? true diff --git a/app/views/posts/show.haml b/app/views/posts/show.haml index 405b9c7..77b38d4 100644 --- a/app/views/posts/show.haml +++ b/app/views/posts/show.haml @@ -11,6 +11,9 @@ = link_to t('posts.edit'), edit_site_post_path(@site, @post.id), class: 'btn btn-block' + = link_to t('posts.preview'), + site_post_preview_path(@site, @post.id), + class: 'btn btn-block' %table.table.table-condensed %thead diff --git a/config/environments/development.rb b/config/environments/development.rb index a292fd6..ac601d4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -68,6 +68,9 @@ Rails.application.configure do config.action_mailer.perform_caching = false config.action_mailer.delivery_method = :letter_opener config.action_mailer.perform_deliveries = true - config.action_mailer.default_url_options = { host: 'localhost', - port: 3000 } + config.action_mailer.default_url_options = { host: 'panel.sutty.local', port: 3000, protocol: 'https' } + + Rails.application.routes.default_url_options[:host] = 'panel.sutty.local' + Rails.application.routes.default_url_options[:port] = 3000 + Rails.application.routes.default_url_options[:protocol] = 'https' end diff --git a/config/environments/production.rb b/config/environments/production.rb index 54eae4d..6b3db4a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -126,7 +126,8 @@ Rails.application.configure do # Recibir por mail notificaciones de excepciones config.action_mailer.default_url_options = { - host: "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}" + host: "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}", + protocol: 'https' } config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true @@ -144,4 +145,7 @@ Rails.application.configure do sender_address: ENV['DEFAULT_FROM'], exception_recipients: ENV['EXCEPTION_TO'] } + + Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}" + Rails.application.routes.default_url_options[:protocol] = 'https' end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 05997dd..146de0f 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -9,10 +9,10 @@ Rails.application.config.content_security_policy do |policy| policy.default_src :self # XXX: Varios scripts generan estilos en línea - policy.style_src :self, :unsafe_inline + policy.style_src :self, :unsafe_inline, :https # Repetimos la default para poder saber cuál es la política en falta policy.script_src :self - policy.font_src :self + policy.font_src :self, :https # XXX: Los íconos de Trix se cargan vía data: policy.img_src :self, :data, :https # Ya no usamos applets! diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 724b760..239f386 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -3,6 +3,14 @@ String.include CoreExtensions::String::StripTags Jekyll::Document.include CoreExtensions::Jekyll::Document::Path +# Definir tags de Liquid que provienen de complementos para que siempre +# devuelvan contenido vacío. +%w[seo feed_meta turbolinks].each do |tag| + Liquid::Template.register_tag(tag, Jekyll::Tags::Empty) +end + +Liquid::Template.register_tag('base', Jekyll::Tags::Base) + module ActionDispatch # Redefinir el formateo de URLs de Rails para eliminar parámetros # selectivamente @@ -28,12 +36,6 @@ end # # TODO: Aplicar monkey patches en otro lado... module Jekyll - Site.class_eval do - def setup - ensure_not_in_dest - end - end - Reader.class_eval do def retrieve_posts(_); end diff --git a/config/locales/en.yml b/config/locales/en.yml index f5b254e..2b15ea1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -483,6 +483,8 @@ en: categories: 'Everything' index: 'Posts' edit: 'Edit' + preview: 'Preliminary version' + preview_message: 'This is a preliminary version, use the Publish changes button back on the panel to publish the article on your site.' open: 'Tip: You can add new options by typing them and pressing Enter' private: '🔒 The values of this field will remain private' select: diff --git a/config/locales/es.yml b/config/locales/es.yml index b68c6a8..b4e445d 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -492,6 +492,8 @@ es: new: 'Agregar:' index: 'Artículos' edit: 'Editar' + preview: 'Versión preliminar' + preview_message: 'Esta es una versión preliminar, para que el artículo aparezca en tu sitio utiliza el botón Publicar cambios en el panel' open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar' private: '🔒 Los valores de este campo serán privados' select: diff --git a/config/routes.rb b/config/routes.rb index 6448431..7bfc0da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,6 +30,8 @@ Rails.application.routes.draw do # alias en nginx sin tener que usar expresiones regulares para # detectar el nombre del sitio. get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} } + # Obtener archivos estáticos desde el directorio público + get '/sites/:site_id/static_file/(*file)', to: 'sites#static_file', as: 'site_static_file', constraints: { site_id: %r{[^/]+} } resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do # Gestionar actualizaciones del sitio @@ -53,7 +55,9 @@ Rails.application.routes.draw do nested do scope '(:locale)' do post :'posts/reorder', to: 'posts#reorder' - resources :posts + resources :posts do + get :preview, to: 'posts#preview' + end end end