2016-05-19 22:25:08 +10:00
require 'mini_racer'
2013-02-05 14:16:51 -05:00
require 'nokogiri'
2016-06-14 14:31:51 -04:00
require 'erb'
2015-01-29 22:53:48 +01:00
require_dependency 'url_helper'
2013-05-28 09:48:47 +10:00
require_dependency 'excerpt_parser'
2013-10-15 18:33:06 -04:00
require_dependency 'post'
2016-05-02 11:36:09 +10:00
require_dependency 'discourse_tagging'
2016-06-14 14:31:51 -04:00
require_dependency 'pretty_text/helpers'
2013-02-05 14:16:51 -05:00
module PrettyText
2016-06-14 14:31:51 -04:00
@mutex = Mutex . new
@ctx_init = Mutex . new
2013-02-05 14:16:51 -05:00
2016-06-14 14:31:51 -04:00
def self . app_root
Rails . root
end
2013-07-16 17:48:48 +10:00
2016-06-14 14:31:51 -04:00
def self . find_file ( root , filename )
return filename if File . file? ( " #{ root } #{ filename } " )
2015-03-12 15:51:28 -04:00
2016-06-14 14:31:51 -04:00
es6_name = " #{ filename } .js.es6 "
return es6_name if File . file? ( " #{ root } #{ es6_name } " )
2013-02-05 14:16:51 -05:00
2016-06-14 14:31:51 -04:00
js_name = " #{ filename } .js "
return js_name if File . file? ( " #{ root } #{ js_name } " )
2015-09-25 13:35:14 +10:00
2016-06-14 14:31:51 -04:00
erb_name = " #{ filename } .js.es6.erb "
return erb_name if File . file? ( " #{ root } #{ erb_name } " )
end
2015-12-28 14:28:16 +08:00
2016-06-14 14:31:51 -04:00
def self . apply_es6_file ( ctx , root_path , part_name )
filename = find_file ( root_path , part_name )
if filename
source = File . read ( " #{ root_path } #{ filename } " )
2016-04-25 15:55:15 -04:00
2016-06-14 14:31:51 -04:00
if filename =~ / \ .erb$ /
source = ERB . new ( source ) . result ( binding )
end
2016-04-25 15:55:15 -04:00
2016-06-14 14:31:51 -04:00
template = Tilt :: ES6ModuleTranspilerTemplate . new { }
transpiled = template . module_transpile ( source , " #{ Rails . root } /app/assets/javascripts/ " , part_name )
ctx . eval ( transpiled )
else
# Look for vendored stuff
vendor_root = " #{ Rails . root } /vendor/assets/javascripts/ "
filename = find_file ( vendor_root , part_name )
if filename
ctx . eval ( File . read ( " #{ vendor_root } #{ filename } " ) )
2016-04-25 15:55:15 -04:00
end
end
2013-02-05 14:16:51 -05:00
end
2016-06-14 14:31:51 -04:00
def self . create_es6_context
ctx = MiniRacer :: Context . new ( timeout : 15000 )
2013-02-05 14:16:51 -05:00
2016-06-14 14:31:51 -04:00
ctx . eval ( " window = {}; window.devicePixelRatio = 2; " ) # hack to make code think stuff is retina
2013-02-05 14:16:51 -05:00
2016-06-14 14:31:51 -04:00
if Rails . env . development? || Rails . env . test?
ctx . attach ( " console.log " , proc { | l | p l } )
end
2013-02-25 19:42:20 +03:00
2016-06-14 14:31:51 -04:00
ctx_load ( ctx , " vendor/assets/javascripts/loader.js " )
ctx_load ( ctx , " vendor/assets/javascripts/lodash.js " )
manifest = File . read ( " #{ Rails . root } /app/assets/javascripts/pretty-text-bundle.js " )
root_path = " #{ Rails . root } /app/assets/javascripts/ "
manifest . each_line do | l |
2016-08-25 17:15:29 +05:30
l = l . chomp
2016-06-14 14:31:51 -04:00
if l =~ / \/ \/ = require ( \ . \/ )?(.*)$ /
apply_es6_file ( ctx , root_path , Regexp . last_match [ 2 ] )
elsif l =~ / \/ \/ = require_tree ( \ . \/ )?(.*)$ /
path = Regexp . last_match [ 2 ]
2016-08-10 13:20:39 -04:00
Dir [ " #{ root_path } / #{ path } /** " ] . sort . each do | f |
2016-06-14 14:31:51 -04:00
apply_es6_file ( ctx , root_path , f . sub ( root_path , '' ) [ 1 .. - 1 ] . sub ( / \ .js.es6$ / , '' ) )
end
end
2016-05-19 22:25:08 +10:00
end
2013-02-05 14:16:51 -05:00
2016-06-14 14:31:51 -04:00
apply_es6_file ( ctx , root_path , " discourse/lib/utilities " )
PrettyText :: Helpers . instance_methods . each do | method |
ctx . attach ( " __helpers. #{ method } " , PrettyText :: Helpers . method ( method ) )
2013-08-08 18:14:12 -04:00
end
2016-06-14 14:31:51 -04:00
ctx . load ( " #{ Rails . root } /lib/pretty_text/shims.js " )
ctx . eval ( " __setUnicode( #{ Emoji . unicode_replacements_json } ) " )
2013-08-08 18:14:12 -04:00
2016-06-14 14:31:51 -04:00
to_load = [ ]
DiscoursePluginRegistry . each_globbed_asset do | a |
to_load << a if File . file? ( a ) && a =~ / discourse-markdown /
end
to_load . uniq . each do | f |
if f =~ / ^.+assets \/ javascripts \/ /
root = Regexp . last_match [ 0 ]
apply_es6_file ( ctx , root , f . sub ( root , '' ) . sub ( / \ .js \ .es6$ / , '' ) )
2013-02-05 14:16:51 -05:00
end
end
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
2016-06-14 14:31:51 -04:00
@ctx = create_es6_context
2013-08-15 18:12:10 -04:00
end
2014-04-14 22:55:57 +02:00
2013-02-05 14:16:51 -05:00
@ctx
end
2014-11-14 17:51:04 +11:00
def self . reset_context
@ctx_init . synchronize do
@ctx = nil
end
end
2016-07-07 15:52:56 +08:00
def self . markdown ( text , opts = { } )
2013-02-05 14:16:51 -05:00
# 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
2016-05-19 22:25:08 +10:00
text = text || " "
2013-02-05 14:16:51 -05:00
2014-02-04 11:12:53 +11:00
protect do
2013-08-16 13:03:47 +10:00
context = v8
2013-10-11 16:24:27 -04:00
2016-06-14 14:31:51 -04:00
paths = {
baseUri : Discourse :: base_uri ,
CDN : Rails . configuration . action_controller . asset_host ,
}
2013-10-11 16:24:27 -04:00
2016-06-14 14:31:51 -04:00
if SiteSetting . enable_s3_uploads?
if SiteSetting . s3_cdn_url . present?
paths [ :S3CDN ] = SiteSetting . s3_cdn_url
2013-10-11 16:24:27 -04:00
end
2016-06-14 14:31:51 -04:00
paths [ :S3BaseUrl ] = Discourse . store . absolute_base_url
2013-10-11 16:24:27 -04:00
end
2016-06-14 14:31:51 -04:00
context . eval ( " __optInput = {}; " )
context . eval ( " __optInput.siteSettings = #{ SiteSetting . client_settings_json } ; " )
context . eval ( " __paths = #{ paths . to_json } ; " )
2016-03-02 14:31:32 -05:00
2016-06-14 14:31:51 -04:00
if opts [ :topicId ]
context . eval ( " __optInput.topicId = #{ opts [ :topicId ] . to_i } ; " )
2015-01-29 17:35:52 +01:00
end
2016-06-14 14:31:51 -04:00
2016-07-07 15:52:56 +08:00
context . eval ( " __optInput.userId = #{ opts [ :user_id ] . to_i } ; " ) if opts [ :user_id ]
2016-06-14 14:31:51 -04:00
context . eval ( " __optInput.getURL = __getURL; " )
2016-07-07 15:52:56 +08:00
context . eval ( " __optInput.getCurrentUser = __getCurrentUser; " )
2016-06-14 14:31:51 -04:00
context . eval ( " __optInput.lookupAvatar = __lookupAvatar; " )
context . eval ( " __optInput.getTopicInfo = __getTopicInfo; " )
context . eval ( " __optInput.categoryHashtagLookup = __categoryLookup; " )
context . eval ( " __optInput.mentionLookup = __mentionLookup; " )
custom_emoji = { }
Emoji . custom . map { | e | custom_emoji [ e . name ] = e . url }
context . eval ( " __optInput.customEmoji = #{ custom_emoji . to_json } ; " )
2016-08-11 14:59:20 -04:00
context . eval ( '__textOptions = __buildOptions(__optInput);' )
# Be careful disabling sanitization. We allow for custom emails
if opts [ :sanitize ] == false
context . eval ( '__textOptions.sanitize = false;' )
end
opts = context . eval ( " __pt = new __PrettyText(__textOptions); " )
2016-06-14 14:31:51 -04:00
2016-01-29 22:59:15 +08:00
DiscourseEvent . trigger ( :markdown_context , context )
2016-06-14 14:31:51 -04:00
baked = context . eval ( " __pt.cook( #{ text . inspect } ) " )
2013-02-05 14:16:51 -05:00
end
2014-11-14 17:51:04 +11:00
if baked . blank? && ! ( opts || { } ) [ :skip_blank_test ]
# we may have a js engine issue
test = markdown ( " a " , skip_blank_test : true )
if test . blank?
Rails . logger . warn ( " Markdown engine appears to have crashed, resetting context " )
reset_context
opts || = { }
opts = opts . dup
opts [ :skip_blank_test ] = true
baked = markdown ( text , opts )
end
end
2013-02-05 14:16:51 -05:00
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
2016-06-14 14:31:51 -04:00
v8 . eval ( " __utils.avatarImg({size: #{ size . inspect } , avatarTemplate: #{ avatar_template . inspect } }, __getURL); " )
2013-02-05 14:16:51 -05:00
end
end
2015-10-15 09:59:29 +02:00
def self . unescape_emoji ( title )
2016-06-14 14:31:51 -04:00
return title unless SiteSetting . enable_emoji?
set = SiteSetting . emoji_set . inspect
2015-10-15 09:59:29 +02:00
protect do
2016-06-14 14:31:51 -04:00
v8 . eval ( " __performEmojiUnescape( #{ title . inspect } , { getURL: __getURL, emojiSet: #{ set } }) " )
2015-10-15 09:59:29 +02:00
end
end
2013-02-05 14:16:51 -05:00
def self . cook ( text , opts = { } )
2015-04-23 19:33:29 +02:00
options = opts . dup
2013-02-05 14:16:51 -05:00
# we have a minor inconsistency
2015-04-23 19:33:29 +02:00
options [ :topicId ] = opts [ :topic_id ]
2015-12-30 14:35:25 -05:00
working_text = text . dup
2015-12-29 16:27:56 -05:00
sanitized = markdown ( working_text , options )
2015-05-26 11:13:12 +10:00
doc = Nokogiri :: HTML . fragment ( sanitized )
if ! options [ :omit_nofollow ] && SiteSetting . add_rel_nofollow_to_user_content
add_rel_nofollow_to_user_content ( doc )
end
2016-06-27 22:08:49 +02:00
if SiteSetting . enable_s3_uploads && SiteSetting . s3_cdn_url . present?
2015-05-26 11:13:12 +10:00
add_s3_cdn ( doc )
end
doc . to_html
end
def self . add_s3_cdn ( doc )
doc . css ( " img " ) . each do | img |
2015-06-10 18:56:44 +10:00
next unless img [ " src " ]
2016-06-30 16:55:01 +02:00
img [ " src " ] = Discourse . store . cdn_url ( img [ " src " ] )
2015-05-26 11:13:12 +10:00
end
2013-02-11 11:43:07 +11:00
end
2013-02-25 19:42:20 +03:00
2015-05-26 11:13:12 +10:00
def self . add_rel_nofollow_to_user_content ( doc )
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 . 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? ||
2015-05-27 14:31:01 +10:00
uri . host == site_uri . host ||
uri . host . ends_with? ( " . " << site_uri . host ) ||
whitelist . any? { | u | uri . host == 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
2013-02-05 14:16:51 -05:00
end
2014-07-11 14:17:01 +10:00
class DetectedLink
attr_accessor :is_quote , :url
def initialize ( url , is_quote = false )
@url = url
@is_quote = is_quote
end
end
2013-02-05 14:16:51 -05:00
def self . extract_links ( html )
links = [ ]
2013-06-05 20:53:07 +02:00
doc = Nokogiri :: HTML . fragment ( html )
2016-03-16 22:35:08 +01:00
# remove href inside quotes & elided part
doc . css ( " aside.quote a, .elided a " ) . each { | l | l [ " href " ] = " " }
2014-07-11 14:17:01 +10:00
2013-06-05 20:53:07 +02:00
# extract all links from the post
2014-07-11 14:17:01 +10:00
doc . css ( " a " ) . each { | l |
2016-04-15 20:02:18 +02:00
unless l [ " href " ] . blank? || " # " . freeze == l [ " href " ] [ 0 ]
2014-07-11 14:17:01 +10:00
links << DetectedLink . new ( l [ " href " ] )
end
}
2013-06-05 20:53:07 +02:00
# extract links to quotes
2014-05-20 17:20:52 -04:00
doc . css ( " aside.quote[data-topic] " ) . 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
2014-07-11 14:17:01 +10:00
links << DetectedLink . new ( url , true )
2013-02-13 15:22:04 -05:00
end
2016-09-22 16:50:05 -04:00
# Extract Youtube links
doc . css ( " div[data-youtube-id] " ) . each do | d |
links << DetectedLink . new ( " https://www.youtube.com/watch?v= #{ d [ 'data-youtube-id' ] } " , false )
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 = { } )
2014-11-05 20:37:00 +01:00
# TODO: properly fix this HACK in ExcerptParser without introducing XSS
doc = Nokogiri :: HTML . fragment ( html )
strip_image_wrapping ( doc )
html = doc . to_html
2013-05-28 09:48:47 +10:00
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 )
2014-09-17 12:08:00 -04:00
fragment . css ( 'a' ) . each { | a | a . replace ( a . inner_html ) }
2013-06-05 15:28:10 -04:00
fragment . to_html
end
2016-06-21 11:12:30 -04:00
# Given a Nokogiri doc, convert all links to absolute
def self . make_all_links_absolute ( doc )
site_uri = nil
doc . css ( " a " ) . each do | link |
href = link [ " href " ] . to_s
begin
uri = URI ( href )
site_uri || = URI ( Discourse . base_url )
link [ " href " ] = " #{ site_uri } #{ link [ 'href' ] } " unless uri . host . present?
rescue URI :: InvalidURIError , URI :: InvalidComponentError
# leave it
end
end
end
2014-04-17 12:32:51 -04:00
def self . strip_image_wrapping ( doc )
doc . css ( " .lightbox-wrapper .meta " ) . remove
end
2016-06-21 11:12:30 -04:00
def self . format_for_email ( html , post = nil )
doc = Nokogiri :: HTML . fragment ( html )
DiscourseEvent . trigger ( :reduce_cooked , doc , post )
strip_image_wrapping ( doc )
make_all_links_absolute ( doc )
doc . to_html
2013-11-28 15:57:21 -05:00
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
2016-05-19 22:25:08 +10:00
rval = yield
2014-02-04 11:12:53 +11:00
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