2013-02-05 13:16:51 -06:00
|
|
|
require 'v8'
|
|
|
|
require 'nokogiri'
|
2013-05-27 18:48:47 -05:00
|
|
|
require_dependency 'excerpt_parser'
|
2013-10-15 17:33:06 -05:00
|
|
|
require_dependency 'post'
|
2013-02-05 13:16:51 -06:00
|
|
|
|
|
|
|
module PrettyText
|
|
|
|
|
|
|
|
class Helpers
|
2013-07-16 02:48:48 -05:00
|
|
|
|
|
|
|
def t(key, opts)
|
|
|
|
str = I18n.t("js." + key)
|
|
|
|
if opts
|
|
|
|
# TODO: server localisation has no parity with client
|
|
|
|
# should be fixed
|
|
|
|
opts.each do |k,v|
|
|
|
|
str.gsub!("{{#{k}}}", v)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
str
|
|
|
|
end
|
|
|
|
|
2013-02-25 10:42:20 -06:00
|
|
|
# function here are available to v8
|
2013-02-05 13:16:51 -06:00
|
|
|
def avatar_template(username)
|
|
|
|
return "" unless username
|
|
|
|
|
|
|
|
user = User.where(username_lower: username.downcase).first
|
2013-08-13 15:08:29 -05:00
|
|
|
if user.present?
|
2013-02-05 13:16:51 -06:00
|
|
|
user.avatar_template
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def is_username_valid(username)
|
|
|
|
return false unless username
|
|
|
|
username = username.downcase
|
|
|
|
return User.exec_sql('select 1 from users where username_lower = ?', username).values.length == 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@mutex = Mutex.new
|
2013-08-15 17:12:10 -05:00
|
|
|
@ctx_init = Mutex.new
|
2013-02-05 13:16:51 -06:00
|
|
|
|
|
|
|
def self.mention_matcher
|
2013-03-23 19:57:00 -05:00
|
|
|
Regexp.new("(\@[a-zA-Z0-9_]{#{User.username_length.begin},#{User.username_length.end}})")
|
2013-02-25 10:42:20 -06:00
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
|
|
|
|
def self.app_root
|
|
|
|
Rails.root
|
|
|
|
end
|
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
def self.create_new_context
|
|
|
|
ctx = V8::Context.new
|
2013-02-25 10:42:20 -06:00
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx["helpers"] = Helpers.new
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx_load(ctx,
|
2013-09-16 13:08:55 -05:00
|
|
|
"vendor/assets/javascripts/md5.js",
|
|
|
|
"vendor/assets/javascripts/lodash.js",
|
|
|
|
"vendor/assets/javascripts/Markdown.Converter.js",
|
2013-05-27 18:48:47 -05:00
|
|
|
"lib/headless-ember.js",
|
2013-09-16 13:08:55 -05:00
|
|
|
"vendor/assets/javascripts/rsvp.js",
|
2013-06-10 15:48:50 -05:00
|
|
|
Rails.configuration.ember.handlebars_location)
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx.eval("var Discourse = {}; Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
|
|
|
ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina
|
|
|
|
ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-08-29 12:11:12 -05:00
|
|
|
decorate_context(ctx)
|
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx_load(ctx,
|
2013-09-16 13:08:55 -05:00
|
|
|
"vendor/assets/javascripts/better_markdown.js",
|
2013-10-11 15:24:27 -05:00
|
|
|
"app/assets/javascripts/defer/html-sanitizer-bundle.js",
|
2013-08-08 17:14:12 -05:00
|
|
|
"app/assets/javascripts/discourse/dialects/dialect.js",
|
2013-10-24 11:36:46 -05:00
|
|
|
"app/assets/javascripts/discourse/lib/utilities.js",
|
|
|
|
"app/assets/javascripts/discourse/lib/markdown.js")
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-08-08 17:14:12 -05:00
|
|
|
Dir["#{Rails.root}/app/assets/javascripts/discourse/dialects/**.js"].each do |dialect|
|
|
|
|
unless dialect =~ /\/dialect\.js$/
|
|
|
|
ctx.load(dialect)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
# Load server side javascripts
|
|
|
|
if DiscoursePluginRegistry.server_side_javascripts.present?
|
|
|
|
DiscoursePluginRegistry.server_side_javascripts.each do |ssjs|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx.load(ssjs)
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx['quoteTemplate'] = File.open(app_root + 'app/assets/javascripts/discourse/templates/quote.js.shbrs') {|f| f.read}
|
|
|
|
ctx['quoteEmailTemplate'] = File.open(app_root + 'lib/assets/quote_email.js.shbrs') {|f| f.read}
|
|
|
|
ctx.eval("HANDLEBARS_TEMPLATES = {
|
2013-02-05 13:16:51 -06:00
|
|
|
'quote': Handlebars.compile(quoteTemplate),
|
|
|
|
'quote_email': Handlebars.compile(quoteEmailTemplate),
|
|
|
|
};")
|
2013-08-15 17:12:10 -05:00
|
|
|
|
|
|
|
ctx
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.v8
|
2013-08-15 22:03:47 -05:00
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
return @ctx if @ctx
|
|
|
|
|
|
|
|
# ensure we only init one of these
|
|
|
|
@ctx_init.synchronize do
|
|
|
|
return @ctx if @ctx
|
|
|
|
@ctx = create_new_context
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
@ctx
|
|
|
|
end
|
|
|
|
|
2013-08-29 12:11:12 -05:00
|
|
|
def self.decorate_context(context)
|
|
|
|
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
|
|
|
context.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
|
|
|
|
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
|
|
|
|
context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
|
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
def self.markdown(text, opts=nil)
|
|
|
|
# we use the exact same markdown converter as the client
|
2013-02-25 10:42:20 -06:00
|
|
|
# TODO: use the same extensions on both client and server (in particular the template for mentions)
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
baked = nil
|
|
|
|
|
2013-02-25 10:42:20 -06:00
|
|
|
@mutex.synchronize do
|
2013-08-15 22:03:47 -05:00
|
|
|
context = v8
|
2013-02-05 13:16:51 -06:00
|
|
|
# we need to do this to work in a multi site environment, many sites, many settings
|
2013-08-29 12:11:12 -05:00
|
|
|
decorate_context(context)
|
2013-10-11 15:24:27 -05:00
|
|
|
|
|
|
|
context_opts = opts || {}
|
|
|
|
context_opts[:sanitize] ||= true
|
|
|
|
context['opts'] = context_opts
|
|
|
|
|
2013-08-15 22:03:47 -05:00
|
|
|
context['raw'] = text
|
2013-10-11 15:24:27 -05:00
|
|
|
|
|
|
|
if Post.white_listed_image_classes.present?
|
|
|
|
Post.white_listed_image_classes.each do |klass|
|
|
|
|
context.eval("Discourse.Markdown.whiteListClass('#{klass}')")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-15 22:03:47 -05:00
|
|
|
context.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}')
|
|
|
|
context.eval('opts["lookupAvatar"] = function(p){return Discourse.Utilities.avatarImg({size: "tiny", avatarTemplate: helpers.avatar_template(p)});}')
|
|
|
|
baked = context.eval('Discourse.Markdown.markdownConverter(opts).makeHtml(raw)')
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
# we need some minimal server side stuff, apply CDN and TODO filter disallowed markup
|
2013-02-25 10:42:20 -06:00
|
|
|
baked = apply_cdn(baked, Rails.configuration.action_controller.asset_host)
|
2013-02-05 13:16:51 -06:00
|
|
|
baked
|
|
|
|
end
|
|
|
|
|
|
|
|
# leaving this here, cause it invokes v8, don't want to implement twice
|
2013-08-13 15:08:29 -05:00
|
|
|
def self.avatar_img(avatar_template, size)
|
2013-02-05 13:16:51 -06:00
|
|
|
r = nil
|
2013-02-25 10:42:20 -06:00
|
|
|
@mutex.synchronize do
|
2013-08-13 15:08:29 -05:00
|
|
|
v8['avatarTemplate'] = avatar_template
|
2013-02-05 13:16:51 -06:00
|
|
|
v8['size'] = size
|
2013-08-29 12:11:12 -05:00
|
|
|
decorate_context(v8)
|
2013-08-13 15:08:29 -05:00
|
|
|
r = v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });")
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
r
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.apply_cdn(html, url)
|
|
|
|
return html unless url
|
|
|
|
|
2013-10-24 04:29:35 -05:00
|
|
|
image = /\.(png|jpg|jpeg|gif|bmp|tif|tiff)$/i
|
2013-07-16 15:16:33 -05:00
|
|
|
relative = /^\/[^\/]/
|
2013-02-05 13:16:51 -06:00
|
|
|
|
|
|
|
doc = Nokogiri::HTML.fragment(html)
|
2013-06-15 05:29:20 -05:00
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
doc.css("a").each do |l|
|
2013-06-15 05:29:20 -05:00
|
|
|
href = l["href"].to_s
|
2013-07-16 15:16:33 -05:00
|
|
|
l["href"] = url + href if href =~ relative && href =~ image
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
2013-06-15 05:29:20 -05:00
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
doc.css("img").each do |l|
|
2013-06-15 05:29:20 -05:00
|
|
|
src = l["src"].to_s
|
2013-07-16 15:16:33 -05:00
|
|
|
l["src"] = url + src if src =~ relative
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
doc.to_s
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.cook(text, opts={})
|
|
|
|
cloned = opts.dup
|
|
|
|
# we have a minor inconsistency
|
|
|
|
cloned[:topicId] = opts[:topic_id]
|
2013-09-11 14:52:37 -05:00
|
|
|
sanitized = markdown(text.dup, cloned)
|
2013-02-10 18:43:07 -06:00
|
|
|
if SiteSetting.add_rel_nofollow_to_user_content
|
2013-02-25 10:42:20 -06:00
|
|
|
sanitized = add_rel_nofollow_to_user_content(sanitized)
|
2013-02-10 18:43:07 -06:00
|
|
|
end
|
|
|
|
sanitized
|
|
|
|
end
|
2013-02-25 10:42:20 -06:00
|
|
|
|
2013-02-10 18:43:07 -06:00
|
|
|
def self.add_rel_nofollow_to_user_content(html)
|
2013-02-11 01:58:19 -06:00
|
|
|
whitelist = []
|
2013-02-11 02:01:33 -06:00
|
|
|
|
2013-02-11 01:58:19 -06:00
|
|
|
l = SiteSetting.exclude_rel_nofollow_domains
|
|
|
|
if l.present?
|
2013-02-25 10:42:20 -06:00
|
|
|
whitelist = l.split(",")
|
2013-02-11 01:58:19 -06:00
|
|
|
end
|
2013-02-11 02:01:33 -06:00
|
|
|
|
2013-02-10 18:43:07 -06:00
|
|
|
site_uri = nil
|
|
|
|
doc = Nokogiri::HTML.fragment(html)
|
|
|
|
doc.css("a").each do |l|
|
|
|
|
href = l["href"].to_s
|
2013-02-25 10:42:20 -06:00
|
|
|
begin
|
2013-02-10 18:43:07 -06:00
|
|
|
uri = URI(href)
|
|
|
|
site_uri ||= URI(Discourse.base_url)
|
2013-02-25 10:42:20 -06:00
|
|
|
|
|
|
|
if !uri.host.present? ||
|
|
|
|
uri.host.ends_with?(site_uri.host) ||
|
2013-02-11 02:01:33 -06:00
|
|
|
whitelist.any?{|u| uri.host.ends_with?(u)}
|
2013-02-10 18:43:07 -06:00
|
|
|
# we are good no need for nofollow
|
|
|
|
else
|
|
|
|
l["rel"] = "nofollow"
|
|
|
|
end
|
|
|
|
rescue URI::InvalidURIError
|
2013-02-25 10:42:20 -06:00
|
|
|
# add a nofollow anyway
|
2013-02-10 18:43:07 -06:00
|
|
|
l["rel"] = "nofollow"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
doc.to_html
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.extract_links(html)
|
|
|
|
links = []
|
2013-06-05 13:53:07 -05:00
|
|
|
doc = Nokogiri::HTML.fragment(html)
|
|
|
|
# remove href inside quotes
|
|
|
|
doc.css("aside.quote a").each { |l| l["href"] = "" }
|
|
|
|
# extract all links from the post
|
2013-08-01 01:00:17 -05:00
|
|
|
doc.css("a").each { |l| links << l["href"] unless l["href"].blank? }
|
2013-06-05 13:53:07 -05:00
|
|
|
# extract links to quotes
|
2013-02-13 14:22:04 -06:00
|
|
|
doc.css("aside.quote").each do |a|
|
2013-06-05 13:53:07 -05:00
|
|
|
topic_id = a['data-topic']
|
2013-02-25 10:42:20 -06:00
|
|
|
|
2013-02-13 14:22:04 -06:00
|
|
|
url = "/t/topic/#{topic_id}"
|
2013-06-05 13:53:07 -05:00
|
|
|
if post_number = a['data-post']
|
2013-02-13 14:22:04 -06:00
|
|
|
url << "/#{post_number}"
|
|
|
|
end
|
|
|
|
|
|
|
|
links << url
|
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
links
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-05-27 18:48:47 -05:00
|
|
|
def self.excerpt(html, max_length, options={})
|
|
|
|
ExcerptParser.get_excerpt(html, max_length, options)
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-06-05 14:28:10 -05:00
|
|
|
def self.strip_links(string)
|
|
|
|
return string if string.blank?
|
|
|
|
|
|
|
|
# If the user is not basic, strip links from their bio
|
|
|
|
fragment = Nokogiri::HTML.fragment(string)
|
|
|
|
fragment.css('a').each {|a| a.replace(a.text) }
|
|
|
|
fragment.to_html
|
|
|
|
end
|
|
|
|
|
2013-05-27 18:48:47 -05:00
|
|
|
protected
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2013-08-15 17:12:10 -05:00
|
|
|
def self.ctx_load(ctx, *files)
|
2013-05-27 18:48:47 -05:00
|
|
|
files.each do |file|
|
2013-08-15 17:12:10 -05:00
|
|
|
ctx.load(app_root + file)
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|