FIX: Replace deprecated URI.encode, URI.escape, URI.unescape and URI.unencode (#8528)
The following methods have long been deprecated in ruby due to flaws in their implementation per http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-core/29293?29179-31097: URI.escape URI.unescape URI.encode URI.unencode escape/encode are just aliases for one another. This PR uses the Addressable gem to replace these methods with its own encode, unencode, and encode_component methods where appropriate. I have put all references to Addressable::URI here into the UrlHelper to keep them corralled in one place to make changes to this implementation easier. Addressable is now also an explicit gem dependency.
This commit is contained in:
parent
b6acfb7847
commit
edbc356593
1
Gemfile
1
Gemfile
|
@ -134,6 +134,7 @@ gem 'highline', '~> 1.7.0', require: false
|
||||||
gem 'rack-protection' # security
|
gem 'rack-protection' # security
|
||||||
gem 'cbor', require: false
|
gem 'cbor', require: false
|
||||||
gem 'cose', require: false
|
gem 'cose', require: false
|
||||||
|
gem 'addressable', '~> 2.7.0'
|
||||||
|
|
||||||
# Gems used only for assets and not required in production environments by default.
|
# Gems used only for assets and not required in production environments by default.
|
||||||
# Allow everywhere for now cause we are allowing asset debugging in production
|
# Allow everywhere for now cause we are allowing asset debugging in production
|
||||||
|
|
|
@ -437,6 +437,7 @@ DEPENDENCIES
|
||||||
activemodel (= 6.0.1)
|
activemodel (= 6.0.1)
|
||||||
activerecord (= 6.0.1)
|
activerecord (= 6.0.1)
|
||||||
activesupport (= 6.0.1)
|
activesupport (= 6.0.1)
|
||||||
|
addressable (~> 2.7.0)
|
||||||
annotate
|
annotate
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
aws-sdk-sns
|
aws-sdk-sns
|
||||||
|
|
|
@ -304,7 +304,7 @@ class ListController < ApplicationController
|
||||||
(slug_path + [@category.id.to_s]).join("/")
|
(slug_path + [@category.id.to_s]).join("/")
|
||||||
end
|
end
|
||||||
|
|
||||||
route_params[:username] = UrlHelper.escape_uri(params[:username]) if params[:username].present?
|
route_params[:username] = UrlHelper.encode_component(params[:username]) if params[:username].present?
|
||||||
route_params
|
route_params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -374,7 +374,7 @@ class ListController < ApplicationController
|
||||||
|
|
||||||
opts = opts.dup
|
opts = opts.dup
|
||||||
if SiteSetting.unicode_usernames && opts[:group_name]
|
if SiteSetting.unicode_usernames && opts[:group_name]
|
||||||
opts[:group_name] = URI.encode(opts[:group_name])
|
opts[:group_name] = UrlHelper.encode_component(opts[:group_name])
|
||||||
end
|
end
|
||||||
opts.delete(:category) if page_params.include?(:category_slug_path_with_id)
|
opts.delete(:category) if page_params.include?(:category_slug_path_with_id)
|
||||||
|
|
||||||
|
|
|
@ -383,8 +383,7 @@ module ApplicationHelper
|
||||||
|
|
||||||
def topic_featured_link_domain(link)
|
def topic_featured_link_domain(link)
|
||||||
begin
|
begin
|
||||||
uri = URI.encode(link)
|
uri = UrlHelper.encode_and_parse(link)
|
||||||
uri = URI.parse(uri)
|
|
||||||
uri = URI.parse("http://#{uri}") if uri.scheme.nil?
|
uri = URI.parse("http://#{uri}") if uri.scheme.nil?
|
||||||
host = uri.host.downcase
|
host = uri.host.downcase
|
||||||
host.start_with?('www.') ? host[4..-1] : host
|
host.start_with?('www.') ? host[4..-1] : host
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Jobs
|
||||||
raise Discourse::InvalidParameters.new(:backup_file_path) if backup_file_path.blank?
|
raise Discourse::InvalidParameters.new(:backup_file_path) if backup_file_path.blank?
|
||||||
|
|
||||||
backup_file_path = URI(backup_file_path)
|
backup_file_path = URI(backup_file_path)
|
||||||
backup_file_path.query = URI.encode_www_form(token: EmailBackupToken.set(user.id))
|
backup_file_path.query = { token: EmailBackupToken.set(user.id) }.to_param
|
||||||
|
|
||||||
message = DownloadBackupMailer.send_email(user.email, backup_file_path.to_s)
|
message = DownloadBackupMailer.send_email(user.email, backup_file_path.to_s)
|
||||||
Email::Sender.new(message, :download_backup_message).send
|
Email::Sender.new(message, :download_backup_message).send
|
||||||
|
|
|
@ -27,7 +27,7 @@ module Jobs
|
||||||
|
|
||||||
cooked_username = PrettyText::Helpers.format_username(@old_username)
|
cooked_username = PrettyText::Helpers.format_username(@old_username)
|
||||||
@cooked_mention_username_regex = /^@#{cooked_username}$/i
|
@cooked_mention_username_regex = /^@#{cooked_username}$/i
|
||||||
@cooked_mention_user_path_regex = /^\/u(?:sers)?\/#{CGI.escape(cooked_username)}$/i
|
@cooked_mention_user_path_regex = /^\/u(?:sers)?\/#{UrlHelper.encode_component(cooked_username)}$/i
|
||||||
@cooked_quote_username_regex = /(?<=\s)#{cooked_username}(?=:)/i
|
@cooked_quote_username_regex = /(?<=\s)#{cooked_username}(?=:)/i
|
||||||
|
|
||||||
update_posts
|
update_posts
|
||||||
|
|
|
@ -624,7 +624,7 @@ class UserNotifications < ActionMailer::Base
|
||||||
|
|
||||||
email_opts = {
|
email_opts = {
|
||||||
topic_title: Emoji.gsub_emoji_to_unicode(title),
|
topic_title: Emoji.gsub_emoji_to_unicode(title),
|
||||||
topic_title_url_encoded: title ? URI.encode(title) : title,
|
topic_title_url_encoded: title ? UrlHelper.encode_component(title) : title,
|
||||||
message: message,
|
message: message,
|
||||||
url: post.url(without_slug: SiteSetting.private_email?),
|
url: post.url(without_slug: SiteSetting.private_email?),
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
|
@ -649,7 +649,7 @@ class UserNotifications < ActionMailer::Base
|
||||||
use_topic_title_subject: use_topic_title_subject,
|
use_topic_title_subject: use_topic_title_subject,
|
||||||
site_description: SiteSetting.site_description,
|
site_description: SiteSetting.site_description,
|
||||||
site_title: SiteSetting.title,
|
site_title: SiteSetting.title,
|
||||||
site_title_url_encoded: URI.encode(SiteSetting.title),
|
site_title_url_encoded: UrlHelper.encode_component(SiteSetting.title),
|
||||||
locale: locale
|
locale: locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ module HasUrl
|
||||||
return if url.blank?
|
return if url.blank?
|
||||||
|
|
||||||
uri = begin
|
uri = begin
|
||||||
URI(URI.unescape(url))
|
URI(UrlHelper.unencode(url))
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class EmbeddableHost < ActiveRecord::Base
|
||||||
return eh if eh.path_whitelist.blank?
|
return eh if eh.path_whitelist.blank?
|
||||||
|
|
||||||
path_regexp = Regexp.new(eh.path_whitelist)
|
path_regexp = Regexp.new(eh.path_whitelist)
|
||||||
return eh if path_regexp.match(path) || path_regexp.match(URI.unescape(path))
|
return eh if path_regexp.match(path) || path_regexp.match(UrlHelper.unencode(path))
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -971,7 +971,7 @@ class Post < ActiveRecord::Base
|
||||||
next unless Discourse.store.has_been_uploaded?(src) || (include_local_upload && src =~ /\A\/[^\/]/i)
|
next unless Discourse.store.has_been_uploaded?(src) || (include_local_upload && src =~ /\A\/[^\/]/i)
|
||||||
|
|
||||||
path = begin
|
path = begin
|
||||||
URI(URI.unescape(GlobalSetting.cdn_url ? src.sub(GlobalSetting.cdn_url, "") : src))&.path
|
URI(UrlHelper.unencode(GlobalSetting.cdn_url ? src.sub(GlobalSetting.cdn_url, "") : src))&.path
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1354,7 +1354,7 @@ class Topic < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def featured_link_root_domain
|
def featured_link_root_domain
|
||||||
MiniSuffix.domain(URI.parse(URI.encode(self.featured_link)).hostname)
|
MiniSuffix.domain(UrlHelper.encode_and_parse(self.featured_link).hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.private_message_topics_count_per_day(start_date, end_date, topic_subtype)
|
def self.private_message_topics_count_per_day(start_date, end_date, topic_subtype)
|
||||||
|
|
|
@ -768,8 +768,8 @@ class User < ActiveRecord::Base
|
||||||
url = SiteSetting.external_system_avatars_url.dup
|
url = SiteSetting.external_system_avatars_url.dup
|
||||||
url = +"#{Discourse::base_uri}#{url}" unless url =~ /^https?:\/\//
|
url = +"#{Discourse::base_uri}#{url}" unless url =~ /^https?:\/\//
|
||||||
url.gsub! "{color}", letter_avatar_color(normalized_username)
|
url.gsub! "{color}", letter_avatar_color(normalized_username)
|
||||||
url.gsub! "{username}", CGI.escape(username)
|
url.gsub! "{username}", UrlHelper.encode_component(username)
|
||||||
url.gsub! "{first_letter}", CGI.escape(normalized_username.grapheme_clusters.first)
|
url.gsub! "{first_letter}", UrlHelper.encode_component(normalized_username.grapheme_clusters.first)
|
||||||
url.gsub! "{hostname}", Discourse.current_hostname
|
url.gsub! "{hostname}", Discourse.current_hostname
|
||||||
url
|
url
|
||||||
else
|
else
|
||||||
|
|
|
@ -315,10 +315,7 @@ class FinalDestination
|
||||||
end
|
end
|
||||||
|
|
||||||
def escape_url
|
def escape_url
|
||||||
UrlHelper.escape_uri(
|
UrlHelper.escape_uri(@url)
|
||||||
CGI.unescapeHTML(@url),
|
|
||||||
Regexp.new("[^#{URI::PATTERN::UNRESERVED}#{URI::PATTERN::RESERVED}#]")
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def private_ranges
|
def private_ranges
|
||||||
|
|
|
@ -10,13 +10,31 @@ class UrlHelper
|
||||||
url, fragment = url.split("#", 2)
|
url, fragment = url.split("#", 2)
|
||||||
uri = URI.parse(url)
|
uri = URI.parse(url)
|
||||||
if uri
|
if uri
|
||||||
fragment = URI.escape(fragment) if fragment&.include?('#')
|
# Addressable::URI::CharacterClasses::UNRESERVED is used here because without it
|
||||||
|
# the # in the fragment is not encoded
|
||||||
|
fragment = Addressable::URI.encode_component(fragment, Addressable::URI::CharacterClasses::UNRESERVED) if fragment&.include?('#')
|
||||||
uri.fragment = fragment
|
uri.fragment = fragment
|
||||||
uri
|
uri
|
||||||
end
|
end
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.encode_and_parse(url)
|
||||||
|
URI.parse(Addressable::URI.encode(url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.encode(url)
|
||||||
|
Addressable::URI.encode(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.unencode(url)
|
||||||
|
Addressable::URI.unencode(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.encode_component(url_component)
|
||||||
|
Addressable::URI.encode_component(url_component)
|
||||||
|
end
|
||||||
|
|
||||||
def self.is_local(url)
|
def self.is_local(url)
|
||||||
url.present? && (
|
url.present? && (
|
||||||
Discourse.store.has_been_uploaded?(url) ||
|
Discourse.store.has_been_uploaded?(url) ||
|
||||||
|
@ -43,14 +61,10 @@ class UrlHelper
|
||||||
self.absolute(url, nil)
|
self.absolute(url, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
DOUBLE_ESCAPED_REGEXP ||= /%25([0-9a-f]{2})/i
|
|
||||||
|
|
||||||
# Prevents double URL encode
|
# Prevents double URL encode
|
||||||
# https://stackoverflow.com/a/37599235
|
# https://stackoverflow.com/a/37599235
|
||||||
def self.escape_uri(uri, pattern = URI::UNSAFE)
|
def self.escape_uri(uri)
|
||||||
encoded = URI.encode(uri, pattern)
|
UrlHelper.encode_component(CGI.unescapeHTML(UrlHelper.unencode(uri)))
|
||||||
encoded.gsub!(DOUBLE_ESCAPED_REGEXP, '%\1')
|
|
||||||
encoded
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cook_url(url, secure: false)
|
def self.cook_url(url, secure: false)
|
||||||
|
|
|
@ -9,7 +9,7 @@ class UrlValidator < ActiveModel::EachValidator
|
||||||
uri.is_a?(URI::HTTP) && !uri.host.nil? && uri.host.include?(".")
|
uri.is_a?(URI::HTTP) && !uri.host.nil? && uri.host.include?(".")
|
||||||
rescue URI::Error => e
|
rescue URI::Error => e
|
||||||
if (e.message =~ /URI must be ascii only/)
|
if (e.message =~ /URI must be ascii only/)
|
||||||
value = URI.encode(value)
|
value = UrlHelper.encode(value)
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -972,7 +972,7 @@ EOM
|
||||||
User.find_each do |u|
|
User.find_each do |u|
|
||||||
ucf = u.custom_fields
|
ucf = u.custom_fields
|
||||||
if ucf && ucf["import_id"] && ucf["import_username"]
|
if ucf && ucf["import_id"] && ucf["import_username"]
|
||||||
username = URI.escape(ucf["import_username"])
|
username = UrlHelper.encode_component(ucf["import_username"])
|
||||||
Permalink.create(url: "#{USERDIR}/#{ucf['import_id']}-#{username}", external_url: "/users/#{u.username}") rescue nil
|
Permalink.create(url: "#{USERDIR}/#{ucf['import_id']}-#{username}", external_url: "/users/#{u.username}") rescue nil
|
||||||
print '.'
|
print '.'
|
||||||
end
|
end
|
||||||
|
|
|
@ -181,7 +181,7 @@ RSpec.describe ListController do
|
||||||
unicode_group = Fabricate(:group, name: '群群组')
|
unicode_group = Fabricate(:group, name: '群群组')
|
||||||
unicode_group.add(user)
|
unicode_group.add(user)
|
||||||
topic = Fabricate(:private_message_topic, allowed_groups: [unicode_group])
|
topic = Fabricate(:private_message_topic, allowed_groups: [unicode_group])
|
||||||
get "/topics/private-messages-group/#{user.username}/#{URI.escape(unicode_group.name)}.json"
|
get "/topics/private-messages-group/#{user.username}/#{UrlHelper.encode_component(unicode_group.name)}.json"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
expect(JSON.parse(response.body)["topic_list"]["topics"].first["id"])
|
expect(JSON.parse(response.body)["topic_list"]["topics"].first["id"])
|
||||||
|
|
Loading…
Reference in New Issue