diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index 91aa8962..43e3994f 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -37,7 +37,7 @@ def create @bookmark.node = Node.new(user_id: current_user.id, cc_licensed: false) @bookmark.node.preview_tags = params[:tags] @bookmark.valid? - flash.now[:alert] = "Votre lien semble invalide. Le confimez‑vous ?" unless @bookmark.link =~ /\A#{URI::regexp(['http', 'https'])}\z/ + flash.now[:alert] = "Votre lien est invalide, veuillez le corriger." unless @bookmark.errors[:link].empty? render :new end end diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 75e47a37..e5fcff6d 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -26,22 +26,15 @@ class Bookmark < Content validates :title, presence: { message: "Le titre est obligatoire" }, length: { maximum: 100, message: "Le titre est trop long" } validates :link, presence: { message: "Vous ne pouvez pas poster un lien vide" }, - http_url: { message: "Le lien n'est pas valide" }, + uri: { protocols: ["http", "https"], message: "Le lien n'est pas valide" }, length: { maximum: 255, message: "Le lien est trop long" } - def link=(raw) - raw.strip! - return write_attribute :url, nil if raw.blank? - uri = URI.parse(raw) - # Default to HTTP link if neither scheme nor host is found - if uri.scheme.blank? && uri.host.blank? - raw = "http://#{raw}" - uri = URI.parse(raw) - end - write_attribute :link, uri.to_s - # Let raw value if error when parsed, HttpUrlValidator will manage it - rescue URI::InvalidURIError - write_attribute :link, raw + before_validation do |bookmark| + bookmark.link = UriValidator.before_validation(bookmark.link); + end + + after_validation do |bookmark| + bookmark.link = UriValidator.after_validation(bookmark.link); end def create_node(attrs={}) diff --git a/app/models/link.rb b/app/models/link.rb index 52fde9b4..4f044e08 100644 --- a/app/models/link.rb +++ b/app/models/link.rb @@ -26,22 +26,16 @@ class Link < ActiveRecord::Base validates :title, presence: { message: "Un lien doit obligatoirement avoir un titre" }, length: { maximum: 100, message: "Le titre est trop long" } - validates :url, http_url: { protocols: PROTOCOLS, message: "L'adresse n'est pas valide" }, + validates :url, uri: { protocols: PROTOCOLS, message: "L'adresse n'est pas valide" }, presence: { message: "Un lien doit obligatoirement avoir une adresse" }, length: { maximum: 255, message: "L’adresse est trop longue" } - def url=(raw) - raw.strip! - return write_attribute :url, nil if raw.blank? - uri = URI.parse(raw) - if uri.scheme.blank? && uri.host.blank? - raw = "http://#{raw}" - uri = URI.parse(raw) - end - write_attribute :url, uri.to_s - # Let raw value if error when parsed, HttpUrlValidator will manage it - rescue URI::InvalidURIError - write_attribute :url, raw + before_validation do |link| + link.url = UriValidator.before_validation(link.url); + end + + after_validation do |link| + link.url = UriValidator.after_validation(link.url); end ### Behaviour ### diff --git a/app/models/user.rb b/app/models/user.rb index de72766e..7a65c9d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -30,15 +30,25 @@ class User < ActiveRecord::Base has_many :taggings, -> { includes(:tag) }, dependent: :destroy has_many :tags, -> { distinct }, through: :taggings - validates_format_of :homesite, message: "L’adresse du site Web personnel n’est pas valide", with: URI::regexp(%w(http https)), allow_blank: true - validates :homesite, length: { maximum: 100, message: "L’adresse du site Web personnel est trop longue" } + validates :homesite, uri: { protocols: ["http", "https"], message: "L’adresse du site Web personnel n’est pas valide" }, + length: { maximum: 100, message: "L’adresse du site Web personnel est trop longue" } validates :name, length: { maximum: 40, message: "Le nom affiché est trop long" } validates :jabber_id, length: { maximum: 32, message: "L’adresse XMPP est trop longue" } - validates :mastodon_url, http_url: { protocols: ["https"], message: "L’adresse du compte Mastodon n’est pas valide" }, + validates :mastodon_url, uri: { protocols: ["https"], message: "L’adresse du compte Mastodon n’est pas valide" }, length: { maximum: 255, message: "L’adresse du compte Mastodon est trop longue" } validates :signature, length: { maximum: 255, message: "La signature est trop longue" } validates :custom_avatar_url, length: { maximum: 255, message: "L’adresse de l’avatar est trop longue" } + before_validation do |user| + user.homesite = UriValidator.before_validation(user.homesite) + user.mastodon_url = UriValidator.before_validation(user.mastodon_url) + end + + after_validation do |user| + user.homesite = UriValidator.after_validation(user.homesite) + user.mastodon_url = UriValidator.after_validation(user.mastodon_url) + end + def self.collective find_by(name: "Collectif") end diff --git a/app/validators/http_url_validator.rb b/app/validators/http_url_validator.rb deleted file mode 100644 index d104a1a9..00000000 --- a/app/validators/http_url_validator.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Validate if a value is a HTTP url -class HttpUrlValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - if value.present? && not(valid?(value, options)) - record.errors.add attribute, (options[:message] || "n'est pas une URL HTTP valide") - end - end - - private - - def valid?(value, options) - # Valid links can be parsed by URI - uri = URI.parse(value) - # Authorize protocol - if options.has_key?(:protocols) - return options[:protocols].include?(uri.scheme) - end - # Links starting with "//MY_DOMAIN" are current scheme dependent and are valid - return true if uri.scheme.nil? && uri.host == MY_DOMAIN - # All other links are valid only if scheme and host exists - return uri.scheme.present? && uri.host.present? - rescue URI::InvalidURIError - false - end -end diff --git a/app/validators/uri_validator.rb b/app/validators/uri_validator.rb new file mode 100644 index 00000000..65c1e174 --- /dev/null +++ b/app/validators/uri_validator.rb @@ -0,0 +1,68 @@ +# Validate if a value is a URI +class UriValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if value.present? && not(valid?(value, options)) + record.errors.add attribute, (options[:message] || "n'est pas un lien valide") + end + end + + private + + def valid?(value, options) + # Valid links can be parsed by URI + uri = URI.parse(value) + + # Authorize only given protocol + if options.has_key?(:protocols) + return options[:protocols].include?(uri.scheme) + end + + # Links starting with "//MY_DOMAIN" are current scheme dependent and are valid + return true if uri.scheme.nil? && uri.host == MY_DOMAIN + + # All other links are valid only if scheme and host exists + return uri.scheme.present? && uri.host.present? + rescue URI::InvalidURIError + false + end + + def self.before_validation(raw, default_scheme='http://') + raw.strip! + return nil if raw.blank? + + # Automatically encodes sharp signs (#) found in URI fragment: + # RFC 3986 uses sharp to define URI fragment and requires other sharps + # to be percent encoded + fragments = raw.split("#") + if (fragments.length > 2) + raw = fragments[0] + '#' + fragments[1..-1].join('%23') + end + + uri = URI.parse(raw) + + # If user forgot to add scheme, add default + if default_scheme.present? && uri.scheme.blank? && uri.host.blank? + raw = "#{default_scheme}#{raw}" + uri = URI.parse(raw) + end + + return uri.to_s + + # Let raw value if error occured when we tried to parse it, because + # the UriValidator will manage it itself on validation + rescue URI::InvalidURIError + return raw + end + + def self.after_validation(raw) + # Decodes sharp signs (#) found in URI fragment to keep visual match with + # the user input + fragments = raw.split("#") + if (fragments.length == 2) + raw = fragments[0] + '#' + fragments[1].gsub('%23', '#') + end + + return raw + end + +end