mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:23:00 +08:00
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:
@ -14,3 +14,4 @@
|
|||||||
//= require ./pretty-text/engines/discourse-markdown/newline
|
//= require ./pretty-text/engines/discourse-markdown/newline
|
||||||
//= require ./pretty-text/engines/discourse-markdown/html-img
|
//= require ./pretty-text/engines/discourse-markdown/html-img
|
||||||
//= require ./pretty-text/engines/discourse-markdown/text-post-process
|
//= require ./pretty-text/engines/discourse-markdown/text-post-process
|
||||||
|
//= require ./pretty-text/engines/discourse-markdown/image-protocol
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
@ -21,6 +21,7 @@ export function buildOptions(state) {
|
|||||||
lookupAvatarByPostNumber,
|
lookupAvatarByPostNumber,
|
||||||
emojiUnicodeReplacer,
|
emojiUnicodeReplacer,
|
||||||
lookupInlineOnebox,
|
lookupInlineOnebox,
|
||||||
|
lookupImageUrls,
|
||||||
previewing,
|
previewing,
|
||||||
linkify,
|
linkify,
|
||||||
censoredWords
|
censoredWords
|
||||||
@ -58,6 +59,7 @@ export function buildOptions(state) {
|
|||||||
mentionLookup: state.mentionLookup,
|
mentionLookup: state.mentionLookup,
|
||||||
emojiUnicodeReplacer,
|
emojiUnicodeReplacer,
|
||||||
lookupInlineOnebox,
|
lookupInlineOnebox,
|
||||||
|
lookupImageUrls,
|
||||||
censoredWords,
|
censoredWords,
|
||||||
allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null,
|
allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null,
|
||||||
markdownIt: true,
|
markdownIt: true,
|
||||||
|
@ -4,6 +4,7 @@ require_dependency "url_helper"
|
|||||||
require_dependency "db_helper"
|
require_dependency "db_helper"
|
||||||
require_dependency "validators/upload_validator"
|
require_dependency "validators/upload_validator"
|
||||||
require_dependency "file_store/local_store"
|
require_dependency "file_store/local_store"
|
||||||
|
require_dependency "base62"
|
||||||
|
|
||||||
class Upload < ActiveRecord::Base
|
class Upload < ActiveRecord::Base
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
@ -53,6 +54,17 @@ class Upload < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
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)
|
def self.generate_digest(path)
|
||||||
Digest::SHA1.file(path).hexdigest
|
Digest::SHA1.file(path).hexdigest
|
||||||
end
|
end
|
||||||
|
35
lib/base62.rb
Normal file
35
lib/base62.rb
Normal 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
|
@ -165,6 +165,7 @@ module PrettyText
|
|||||||
__optInput.customEmoji = #{custom_emoji.to_json};
|
__optInput.customEmoji = #{custom_emoji.to_json};
|
||||||
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
|
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
|
||||||
__optInput.lookupInlineOnebox = __lookupInlineOnebox;
|
__optInput.lookupInlineOnebox = __lookupInlineOnebox;
|
||||||
|
__optInput.lookupImageUrls = __lookupImageUrls;
|
||||||
#{opts[:linkify] == false ? "__optInput.linkify = false;" : ""}
|
#{opts[:linkify] == false ? "__optInput.linkify = false;" : ""}
|
||||||
__optInput.censoredWords = #{WordWatcher.words_for_action(:censor).join('|').to_json};
|
__optInput.censoredWords = #{WordWatcher.words_for_action(:censor).join('|').to_json};
|
||||||
JS
|
JS
|
||||||
|
@ -45,6 +45,30 @@ module PrettyText
|
|||||||
end
|
end
|
||||||
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)
|
def lookup_inline_onebox(url)
|
||||||
InlineOneboxer.lookup(url)
|
InlineOneboxer.lookup(url)
|
||||||
end
|
end
|
||||||
|
@ -53,6 +53,10 @@ function __lookupInlineOnebox(url) {
|
|||||||
return __helpers.lookup_inline_onebox(url);
|
return __helpers.lookup_inline_onebox(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __lookupImageUrls(urls) {
|
||||||
|
return __helpers.lookup_image_urls(urls);
|
||||||
|
}
|
||||||
|
|
||||||
function __getTopicInfo(i) {
|
function __getTopicInfo(i) {
|
||||||
return __helpers.get_topic_info(i);
|
return __helpers.get_topic_info(i);
|
||||||
}
|
}
|
||||||
|
BIN
public/images/transparent.png
Executable file
BIN
public/images/transparent.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 68 B |
@ -1059,4 +1059,51 @@ HTML
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "image decoding" do
|
||||||
|
|
||||||
|
it "can decode upload:// for default setup" do
|
||||||
|
upload = Fabricate(:upload)
|
||||||
|
|
||||||
|
raw = <<~RAW
|
||||||
|

|
||||||
|
|
||||||
|
- 
|
||||||
|
|
||||||
|
- test
|
||||||
|
- 
|
||||||
|
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 = ""
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
@ -113,4 +113,21 @@ describe Upload do
|
|||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
Reference in New Issue
Block a user