From bcf7dc38c21be2741b9ebfb6e1982f36bab66be8 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 22 Aug 2017 11:46:15 -0400 Subject: [PATCH] FEATURE: server side support for upload:// markdown This allows uploads to be specified using short sha1 hash instead of full URL Client side change is pending --- app/assets/javascripts/markdown-it-bundle.js | 1 + .../discourse-markdown/image-protocol.js.es6 | 59 ++++++++++++++++++ .../pretty-text/pretty-text.js.es6 | 2 + app/models/upload.rb | 12 ++++ lib/base62.rb | 35 +++++++++++ lib/pretty_text.rb | 1 + lib/pretty_text/helpers.rb | 24 +++++++ lib/pretty_text/shims.js | 4 ++ public/images/transparent.png | Bin 0 -> 68 bytes spec/components/pretty_text_spec.rb | 47 ++++++++++++++ spec/models/upload_spec.rb | 17 +++++ 11 files changed, 202 insertions(+) create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6 create mode 100644 lib/base62.rb create mode 100755 public/images/transparent.png diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js index 81c05f719fd..3cd7a69f2fc 100644 --- a/app/assets/javascripts/markdown-it-bundle.js +++ b/app/assets/javascripts/markdown-it-bundle.js @@ -14,3 +14,4 @@ //= require ./pretty-text/engines/discourse-markdown/newline //= require ./pretty-text/engines/discourse-markdown/html-img //= require ./pretty-text/engines/discourse-markdown/text-post-process +//= require ./pretty-text/engines/discourse-markdown/image-protocol diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6 new file mode 100644 index 00000000000..ee86ccc24bf --- /dev/null +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-protocol.js.es6 @@ -0,0 +1,59 @@ +// add image to array if src has an upload +function addImage(images, token) { + if (token.attrs) { + for(let i=0; i 0) { + let srcList = images.map(([token, srcIndex]) => token.attrs[srcIndex][1]); + let longUrls = state.md.options.discourse.lookupImageUrls(srcList); + + images.forEach(([token, srcIndex]) => { + let origSrc = token.attrs[srcIndex][1]; + let mapped = longUrls[origSrc]; + if (mapped) { + token.attrs[srcIndex][1] = mapped; + } else { + token.attrs[srcIndex][1] = state.md.options.discourse.getURL('/images/transparent.png'); + token.attrs.push(['data-orig-src', origSrc]); + } + }); + } + +} + +export function setup(helper) { + helper.whiteList(['img[data-orig-src]']); + helper.registerPlugin(md => { + md.core.ruler.push('image-protocol', rule); + }); +} diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 68fd360dc66..094ad9a5e82 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -21,6 +21,7 @@ export function buildOptions(state) { lookupAvatarByPostNumber, emojiUnicodeReplacer, lookupInlineOnebox, + lookupImageUrls, previewing, linkify, censoredWords @@ -58,6 +59,7 @@ export function buildOptions(state) { mentionLookup: state.mentionLookup, emojiUnicodeReplacer, lookupInlineOnebox, + lookupImageUrls, censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, markdownIt: true, diff --git a/app/models/upload.rb b/app/models/upload.rb index 79429b1e94a..ace1e3485f0 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -4,6 +4,7 @@ require_dependency "url_helper" require_dependency "db_helper" require_dependency "validators/upload_validator" require_dependency "file_store/local_store" +require_dependency "base62" class Upload < ActiveRecord::Base belongs_to :user @@ -53,6 +54,17 @@ class Upload < ActiveRecord::Base end end + def short_url + "upload://#{Base62.encode(sha1.hex)}.#{extension}" + end + + def self.sha1_from_short_url(url) + if url =~ /(upload:\/\/)?([a-zA-Z0-9]+)(\..*)?/ + sha1 = Base62.decode($2).to_s(16) + sha1.length == 40 ? sha1 : nil + end + end + def self.generate_digest(path) Digest::SHA1.file(path).hexdigest end diff --git a/lib/base62.rb b/lib/base62.rb new file mode 100644 index 00000000000..84f9fbf23c8 --- /dev/null +++ b/lib/base62.rb @@ -0,0 +1,35 @@ +# Modified version of: https://github.com/steventen/base62-rb + +module Base62 + KEYS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze + KEYS_HASH = KEYS.each_char.with_index.inject({}) { |h, (k, v)| h[k] = v; h } + BASE = KEYS.length + + # Encodes base10 (decimal) number to base62 string. + def self.encode(num) + return "0" if num == 0 + return nil if num < 0 + + str = "" + while num > 0 + # prepend base62 charaters + str = KEYS[num % BASE] + str + num = num / BASE + end + str + end + + # Decodes base62 string to a base10 (decimal) number. + def self.decode(str) + num = 0 + i = 0 + len = str.length - 1 + # while loop is faster than each_char or other 'idiomatic' way + while i < str.length + pow = BASE**(len - i) + num += KEYS_HASH[str[i]] * pow + i += 1 + end + num + end +end diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index cfe191f5051..56a8da4352e 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -165,6 +165,7 @@ module PrettyText __optInput.customEmoji = #{custom_emoji.to_json}; __optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer; __optInput.lookupInlineOnebox = __lookupInlineOnebox; + __optInput.lookupImageUrls = __lookupImageUrls; #{opts[:linkify] == false ? "__optInput.linkify = false;" : ""} __optInput.censoredWords = #{WordWatcher.words_for_action(:censor).join('|').to_json}; JS diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 38abc290922..fcf66188f05 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -45,6 +45,30 @@ module PrettyText end end + def lookup_image_urls(urls) + map = {} + result = {} + + urls.each do |url| + sha1 = Upload.sha1_from_short_url(url) + map[url] = sha1 if sha1 + end + + if map.length > 0 + reverse_map = map.invert + + Upload.where(sha1: map.values).pluck(:sha1, :url).each do |row| + sha1, url = row + + if short_url = reverse_map[sha1] + result[short_url] = url + end + end + end + + result + end + def lookup_inline_onebox(url) InlineOneboxer.lookup(url) end diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index dc0ede53de7..26741932a75 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -53,6 +53,10 @@ function __lookupInlineOnebox(url) { return __helpers.lookup_inline_onebox(url); } +function __lookupImageUrls(urls) { + return __helpers.lookup_image_urls(urls); +} + function __getTopicInfo(i) { return __helpers.get_topic_info(i); } diff --git a/public/images/transparent.png b/public/images/transparent.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 73d3a168b38..f2d4225179a 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1059,4 +1059,51 @@ HTML end end + describe "image decoding" do + + it "can decode upload:// for default setup" do + upload = Fabricate(:upload) + + raw = <<~RAW + ![upload](#{upload.short_url}) + + - ![upload](#{upload.short_url}) + + - test + - ![upload](#{upload.short_url}) + RAW + + cooked = <<~HTML +

upload

+ + HTML + + expect(PrettyText.cook(raw)).to eq(cooked.strip) + end + + it "can place a blank image if we can not find the upload" do + + raw = "![upload](upload://abcABC.png)" + + cooked = <<~HTML +

upload

+ HTML + + puts PrettyText.cook(raw) + + expect(PrettyText.cook(raw)).to eq(cooked.strip) + end + + end + end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 6625cc5fd89..53a98da03f5 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -113,4 +113,21 @@ describe Upload do end end + describe '.short_url' do + it "should generate a correct short url" do + upload = Upload.new(sha1: 'bda2c513e1da04f7b4e99230851ea2aafeb8cc4e', extension: 'png') + expect(upload.short_url).to eq('upload://r3AYqESanERjladb4vBB7VsMBm6.png') + end + end + + describe '.sha1_from_short_url' do + it "should be able to look up sha1" do + sha1 = 'bda2c513e1da04f7b4e99230851ea2aafeb8cc4e' + + expect(Upload.sha1_from_short_url('upload://r3AYqESanERjladb4vBB7VsMBm6.png')).to eq(sha1) + expect(Upload.sha1_from_short_url('upload://r3AYqESanERjladb4vBB7VsMBm6')).to eq(sha1) + expect(Upload.sha1_from_short_url('r3AYqESanERjladb4vBB7VsMBm6')).to eq(sha1) + end + end + end