# frozen_string_literal: true # Representa la plantilla de un campo en los metadatos del artículo # # TODO: Validar el tipo de valor pasado a value= según el :type # MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, :value, :help, :required, :errors, :post, :layout, keyword_init: true) do include Metadata::FrontMatterConcern def inspect "#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>" end # Queremos que los artículos nuevos siempre cacheen, si usamos el UUID # siempre vamos a obtener un item nuevo. def cache_key return "#{layout.value}/#{name}" if post.new? @cache_key ||= "post/#{post.uuid.value}/#{name}" end # Genera una versión de caché en base a la fecha de modificación del # Post, el valor actual y los valores posibles, de forma que cualquier # cambio permita renovar la caché. # # @return [String] def cache_version post.cache_version + value.hash.to_s + values.hash.to_s end # @return [String] def cache_key_with_version "#{cache_key}-#{cache_version}" end # XXX: Deberíamos sanitizar durante la asignación? def value=(new_value) @value_was = value self[:value] = new_value end # Siempre obtener el valor actual y solo obtenerlo del documento una # vez. def value_was return @value_was if instance_variable_defined? '@value_was' @value_was = document_value end def changed? value_was != value end # Obtiene el valor del JekyllDocument def document_value document.data[name.to_s] end # Trae el idioma actual del sitio o del panel # @return [String] def lang @lang ||= post&.lang&.value || I18n.locale end # El valor por defecto def default_value layout.metadata.dig(name, 'default', lang.to_s) end # Valores posibles, busca todos los valores actuales en otros # artículos del mismo sitio # # @return [Array] def values site.indexed_posts.everything_of(name) end # Valor actual o por defecto. Al memoizarlo podemos modificarlo # usando otros métodos que el de asignación. def value self[:value] ||= if (data = document_value).present? private? ? decrypt(data) : data else default_value end end # Detecta si el valor está vacío def empty? value.blank? end # Comprueba si el metadato es válido def valid? validate end def validate self.errors = [] errors << I18n.t('metadata.cant_be_empty') unless can_be_empty? errors.empty? end # Usa el valor por defecto para generar el formato de StrongParam # necesario. # # @return [Symbol,Hash] def to_param case default_value when Hash then { name => default_value.keys.map(&:to_sym) } when Array then { name => [] } else name end end def to_s value.to_s end def array? type == 'array' end # En caso de que algún campo necesite realizar acciones antes de ser # guardado def save if !changed? self[:value] = document_value if private? return true end self[:value] = sanitize value self[:value] = encrypt(value) if private? true end def related_posts? false end def related_methods @related_methods ||= [].freeze end # Determina si el campo es privado y debería ser cifrado def private? layout.metadata.dig(name, 'private').present? end # Determina si el campo debería estar deshabilitado def disabled? layout.metadata.dig(name, 'disabled') || !writable? end # Determina si el campo es de solo lectura # # once => el campo solo se puede modificar si estaba vacío def writable? case layout.metadata.dig(name, 'writable') when 'once' then value.blank? else true end end private # Si es obligatorio no puede estar vacío def can_be_empty? true unless required && empty? end # No usamos sanitize_action_text_content porque espera un ActionText # # Ver ActionText::ContentHelper#sanitize_action_text_content def sanitize(string) return if string.nil? return string unless string.is_a? String sanitizer .sanitize(string.tr("\r", '').unicode_normalize, tags: allowed_tags, attributes: allowed_attributes) .strip .html_safe end def sanitizer @sanitizer ||= Rails::Html::Sanitizer.safe_list_sanitizer.new end def allowed_attributes @allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id name].freeze end def allowed_tags @allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote figcaption a sub sup small].freeze end # Decifra el valor # # XXX: Otros tipos de valores necesitan implementar su propio método # de decifrado (Array). def decrypt(value) return value if value.blank? box.decrypt_str value.to_s rescue Lockbox::DecryptionError => e if value.to_s.include? ' ' value else ExceptionNotifier.notify_exception(e, data: { site: site.name, post: post.path.absolute, name: name }) I18n.t('lockbox.help.decryption_error') end end # Cifra el valor. # # XXX: Otros tipos de valores necesitan implementar su propio método # de cifrado (Array). def encrypt(value) box.encrypt value.to_s end # Genera una lockbox a partir de la llave privada del sitio # # @return [Lockbox] def box @box ||= Lockbox.new key: site.private_key, padding: true, encode: true end end