2013-02-05 14:16:51 -05:00
require 'v8'
require 'nokogiri'
2013-05-28 09:48:47 +10:00
require_dependency 'excerpt_parser'
2013-10-15 18:33:06 -04:00
require_dependency 'post'
2013-02-05 14:16:51 -05:00
module PrettyText
class Helpers
2013-07-16 17:48:48 +10:00
def t ( key , opts )
str = I18n . t ( " js. " + key )
if opts
2014-04-14 22:55:57 +02:00
# TODO: server localisation has no parity with client should be fixed
2014-02-18 11:14:35 +11:00
str = str . dup
2013-07-16 17:48:48 +10:00
opts . each do | k , v |
str . gsub! ( " {{ #{ k } }} " , v )
end
end
str
end
2013-02-25 19:42:20 +03:00
# function here are available to v8
2013-02-05 14:16:51 -05:00
def avatar_template ( username )
return " " unless username
2014-05-06 14:41:59 +01:00
user = User . find_by ( username_lower : username . downcase )
2013-11-05 19:04:47 +01:00
user . avatar_template if user . present?
2013-02-05 14:16:51 -05:00
end
def is_username_valid ( username )
return false unless username
username = username . downcase
2014-04-14 22:55:57 +02:00
return User . exec_sql ( 'SELECT 1 FROM users WHERE username_lower = ?' , username ) . values . length == 1
2013-02-05 14:16:51 -05:00
end
end
@mutex = Mutex . new
2013-08-15 18:12:10 -04:00
@ctx_init = Mutex . new
2013-02-05 14:16:51 -05:00
def self . mention_matcher
2013-03-24 01:57:00 +01:00
Regexp . new ( " ( \ @[a-zA-Z0-9_]{ #{ User . username_length . begin } , #{ User . username_length . end } }) " )
2013-02-25 19:42:20 +03:00
end
2013-02-05 14:16:51 -05:00
def self . app_root
Rails . root
end
2013-08-15 18:12:10 -04:00
def self . create_new_context
2013-12-09 12:43:48 +11:00
# timeout any eval that takes longer that 5 seconds
ctx = V8 :: Context . new ( timeout : 5000 )
2013-02-25 19:42:20 +03:00
2013-08-15 18:12:10 -04:00
ctx [ " helpers " ] = Helpers . new
2013-02-05 14:16:51 -05:00
2013-08-15 18:12:10 -04:00
ctx_load ( ctx ,
2014-04-14 22:55:57 +02:00
" vendor/assets/javascripts/md5.js " ,
" vendor/assets/javascripts/lodash.js " ,
" vendor/assets/javascripts/Markdown.Converter.js " ,
" lib/headless-ember.js " ,
" vendor/assets/javascripts/rsvp.js " ,
Rails . configuration . ember . handlebars_location
)
2013-02-05 14:16:51 -05:00
2014-01-06 16:50:04 +11:00
ctx . eval ( " var Discourse = {}; Discourse.SiteSettings = {}; " )
2013-08-15 18:12:10 -04:00
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 14:16:51 -05:00
2013-08-29 13:11:12 -04:00
decorate_context ( ctx )
2013-08-15 18:12:10 -04:00
ctx_load ( ctx ,
2014-04-14 22:55:57 +02:00
" vendor/assets/javascripts/better_markdown.js " ,
" app/assets/javascripts/defer/html-sanitizer-bundle.js " ,
" app/assets/javascripts/discourse/dialects/dialect.js " ,
" app/assets/javascripts/discourse/lib/utilities.js " ,
" app/assets/javascripts/discourse/lib/html.js " ,
" app/assets/javascripts/discourse/lib/markdown.js "
)
2013-02-05 14:16:51 -05:00
2013-08-08 18:14:12 -04: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 14:16:51 -05:00
# Load server side javascripts
if DiscoursePluginRegistry . server_side_javascripts . present?
DiscoursePluginRegistry . server_side_javascripts . each do | ssjs |
2013-11-20 14:38:21 +11:00
if ( ssjs =~ / \ .erb / )
erb = ERB . new ( File . read ( ssjs ) )
erb . filename = ssjs
ctx . eval ( erb . result )
else
ctx . load ( ssjs )
end
2013-02-05 14:16:51 -05:00
end
end
2013-12-24 00:50:36 +01:00
ctx [ 'quoteTemplate' ] = File . open ( app_root + 'app/assets/javascripts/discourse/templates/quote.js.handlebars' ) { | f | f . read }
ctx [ 'quoteEmailTemplate' ] = File . open ( app_root + 'lib/assets/quote_email.js.handlebars' ) { | f | f . read }
2013-08-15 18:12:10 -04:00
ctx . eval ( " HANDLEBARS_TEMPLATES = {
2013-02-05 14:16:51 -05:00
'quote' : Handlebars . compile ( quoteTemplate ) ,
'quote_email' : Handlebars . compile ( quoteEmailTemplate ) ,
} ; " )
2013-08-15 18:12:10 -04:00
ctx
end
def self . v8
return @ctx if @ctx
# ensure we only init one of these
@ctx_init . synchronize do
return @ctx if @ctx
@ctx = create_new_context
end
2014-04-14 22:55:57 +02:00
2013-02-05 14:16:51 -05:00
@ctx
end
2013-08-29 13:11:12 -04: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 14:16:51 -05:00
def self . markdown ( text , opts = nil )
# we use the exact same markdown converter as the client
2013-02-25 19:42:20 +03:00
# TODO: use the same extensions on both client and server (in particular the template for mentions)
2013-02-05 14:16:51 -05:00
baked = nil
2014-02-04 11:12:53 +11:00
protect do
2013-08-16 13:03:47 +10:00
context = v8
2013-02-05 14:16:51 -05:00
# we need to do this to work in a multi site environment, many sites, many settings
2013-08-29 13:11:12 -04:00
decorate_context ( context )
2013-10-11 16:24:27 -04:00
context_opts = opts || { }
context_opts [ :sanitize ] || = true
context [ 'opts' ] = context_opts
2013-08-16 13:03:47 +10:00
context [ 'raw' ] = text
2013-10-11 16:24:27 -04: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-16 13:03:47 +10: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 14:16:51 -05:00
end
baked
end
# leaving this here, cause it invokes v8, don't want to implement twice
2013-08-13 22:08:29 +02:00
def self . avatar_img ( avatar_template , size )
2014-02-04 11:12:53 +11:00
protect do
2013-08-13 22:08:29 +02:00
v8 [ 'avatarTemplate' ] = avatar_template
2013-02-05 14:16:51 -05:00
v8 [ 'size' ] = size
2013-08-29 13:11:12 -04:00
decorate_context ( v8 )
2014-02-04 11:12:53 +11:00
v8 . eval ( " Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size }); " )
2013-02-05 14:16:51 -05:00
end
end
def self . cook ( text , opts = { } )
cloned = opts . dup
# we have a minor inconsistency
cloned [ :topicId ] = opts [ :topic_id ]
2013-09-11 15:52:37 -04:00
sanitized = markdown ( text . dup , cloned )
2014-01-15 11:34:17 -05:00
sanitized = add_rel_nofollow_to_user_content ( sanitized ) if ! cloned [ :omit_nofollow ] && SiteSetting . add_rel_nofollow_to_user_content
2013-02-11 11:43:07 +11:00
sanitized
end
2013-02-25 19:42:20 +03:00
2013-02-11 11:43:07 +11:00
def self . add_rel_nofollow_to_user_content ( html )
2013-02-11 18:58:19 +11:00
whitelist = [ ]
2013-02-11 19:01:33 +11:00
2013-11-20 14:38:21 +11:00
domains = SiteSetting . exclude_rel_nofollow_domains
2014-03-29 16:50:44 -07:00
whitelist = domains . split ( '|' ) if domains . present?
2013-02-11 19:01:33 +11:00
2013-02-11 11:43:07 +11:00
site_uri = nil
doc = Nokogiri :: HTML . fragment ( html )
doc . css ( " a " ) . each do | l |
href = l [ " href " ] . to_s
2013-02-25 19:42:20 +03:00
begin
2013-02-11 11:43:07 +11:00
uri = URI ( href )
site_uri || = URI ( Discourse . base_url )
2013-02-25 19:42:20 +03:00
2013-11-05 19:04:47 +01:00
if ! uri . host . present? ||
uri . host . ends_with? ( site_uri . host ) ||
whitelist . any? { | u | uri . host . ends_with? ( u ) }
2013-02-11 11:43:07 +11:00
# we are good no need for nofollow
else
l [ " rel " ] = " nofollow "
end
2014-03-07 10:44:04 +01:00
rescue URI :: InvalidURIError , URI :: InvalidComponentError
2013-02-25 19:42:20 +03:00
# add a nofollow anyway
2013-02-11 11:43:07 +11:00
l [ " rel " ] = " nofollow "
end
end
doc . to_html
2013-02-05 14:16:51 -05:00
end
def self . extract_links ( html )
links = [ ]
2013-06-05 20:53:07 +02: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 16:00:17 +10:00
doc . css ( " a " ) . each { | l | links << l [ " href " ] unless l [ " href " ] . blank? }
2013-06-05 20:53:07 +02:00
# extract links to quotes
2013-02-13 15:22:04 -05:00
doc . css ( " aside.quote " ) . each do | a |
2013-06-05 20:53:07 +02:00
topic_id = a [ 'data-topic' ]
2013-02-25 19:42:20 +03:00
2013-02-13 15:22:04 -05:00
url = " /t/topic/ #{ topic_id } "
2013-06-05 20:53:07 +02:00
if post_number = a [ 'data-post' ]
2013-02-13 15:22:04 -05:00
url << " / #{ post_number } "
end
links << url
end
2013-02-05 14:16:51 -05:00
links
end
2013-05-28 09:48:47 +10:00
def self . excerpt ( html , max_length , options = { } )
ExcerptParser . get_excerpt ( html , max_length , options )
end
2013-02-05 14:16:51 -05:00
2013-06-05 15:28:10 -04: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
2014-04-17 12:32:51 -04:00
# Given a Nokogiri doc, convert all links to absolute
def self . make_all_links_absolute ( doc )
2013-11-28 15:57:21 -05:00
site_uri = nil
2014-02-04 12:57:16 +11:00
doc . css ( " a " ) . each do | link |
href = link [ " href " ] . to_s
2013-11-28 15:57:21 -05:00
begin
uri = URI ( href )
site_uri || = URI ( Discourse . base_url )
2014-02-04 12:57:16 +11:00
link [ " href " ] = " #{ site_uri } #{ link [ 'href' ] } " unless uri . host . present?
2013-11-28 15:57:21 -05:00
rescue URI :: InvalidURIError
# leave it
end
end
2014-04-17 12:32:51 -04:00
end
def self . strip_image_wrapping ( doc )
doc . css ( " .lightbox-wrapper .meta " ) . remove
end
def self . format_for_email ( html )
doc = Nokogiri :: HTML . fragment ( html )
make_all_links_absolute ( doc )
strip_image_wrapping ( doc )
2013-11-28 15:57:21 -05:00
doc . to_html
end
2013-05-28 09:48:47 +10:00
protected
2013-02-05 14:16:51 -05:00
2014-02-04 11:12:53 +11:00
class JavaScriptError < StandardError
attr_accessor :message , :backtrace
def initialize ( message , backtrace )
@message = message
@backtrace = backtrace
end
end
def self . protect
rval = nil
@mutex . synchronize do
begin
rval = yield
# This may seem a bit odd, but we don't want to leak out
# objects that require locks on the v8 vm, to get a backtrace
# you need a lock, if this happens in the wrong spot you can
# deadlock a process
rescue V8 :: Error = > e
raise JavaScriptError . new ( e . message , e . backtrace )
end
end
rval
end
2013-08-15 18:12:10 -04:00
def self . ctx_load ( ctx , * files )
2013-05-28 09:48:47 +10:00
files . each do | file |
2013-08-15 18:12:10 -04:00
ctx . load ( app_root + file )
2013-02-05 14:16:51 -05:00
end
end
end