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
This commit is contained in:
Sam 2017-08-22 11:46:15 -04:00
parent b00747fd49
commit bcf7dc38c2
11 changed files with 202 additions and 0 deletions

View File

@ -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

View File

@ -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<token.attrs.length; i++) {
if (token.attrs[i][1].indexOf('upload://') === 0) {
images.push([token, i]);
break;
}
}
}
}
function rule(state) {
let images = [];
for (let i = 0; i < state.tokens.length; i++) {
let blockToken = state.tokens[i];
if (blockToken.tag === 'img') {
addImage(images, blockToken);
}
if (!blockToken.children) {
continue;
}
for (let j = 0; j < blockToken.children.length; j++) {
let token = blockToken.children[j];
if (token.tag === 'img') {
addImage(images, token);
}
}
}
if (images.length > 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);
});
}

View File

@ -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,

View File

@ -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

35
lib/base62.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}

BIN
public/images/transparent.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -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
<p><img src="#{upload.url}" alt="upload"></p>
<ul>
<li>
<p><img src="#{upload.url}" alt="upload"></p>
</li>
<li>
<p>test</p>
<ul>
<li><img src="#{upload.url}" alt="upload"></li>
</ul>
</li>
</ul>
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
<p><img src="/images/transparent.png" alt="upload" data-orig-src="upload://abcABC.png"></p>
HTML
puts PrettyText.cook(raw)
expect(PrettyText.cook(raw)).to eq(cooked.strip)
end
end
end

View File

@ -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