FEATURE: Support `[description|attachment](upload://<short-sha>)` in MD take 2.
Previous attempt was missing `post_uploads` records.
This commit is contained in:
parent
63292cecd9
commit
f0620e7118
|
@ -36,7 +36,7 @@ import {
|
|||
import {
|
||||
cacheShortUploadUrl,
|
||||
resolveAllShortUrls
|
||||
} from "pretty-text/image-short-url";
|
||||
} from "pretty-text/upload-short-url";
|
||||
|
||||
import {
|
||||
INLINE_ONEBOX_LOADING_CSS_CLASS,
|
||||
|
|
|
@ -13,7 +13,7 @@ const CookText = Ember.Component.extend({
|
|||
// pretty text may only be loaded now
|
||||
Ember.run.next(() =>
|
||||
window
|
||||
.requireModule("pretty-text/image-short-url")
|
||||
.requireModule("pretty-text/upload-short-url")
|
||||
.resolveAllShortUrls(ajax)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -444,15 +444,9 @@ export function getUploadMarkdown(upload) {
|
|||
) {
|
||||
return uploadLocation(upload.url);
|
||||
} else {
|
||||
return (
|
||||
'<a class="attachment" href="' +
|
||||
upload.url +
|
||||
'">' +
|
||||
upload.original_filename +
|
||||
"</a> (" +
|
||||
I18n.toHumanSize(upload.filesize) +
|
||||
")\n"
|
||||
);
|
||||
return `[${upload.original_filename} (${I18n.toHumanSize(
|
||||
upload.filesize
|
||||
)})|attachment](${upload.short_url})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
//= 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
|
||||
//= require ./pretty-text/engines/discourse-markdown/upload-protocol
|
||||
//= require ./pretty-text/engines/discourse-markdown/inject-line-number
|
||||
//= require ./pretty-text/engines/discourse-markdown/d-wrap
|
||||
|
|
|
@ -11,4 +11,4 @@
|
|||
//= require ./pretty-text/sanitizer
|
||||
//= require ./pretty-text/oneboxer
|
||||
//= require ./pretty-text/inline-oneboxer
|
||||
//= require ./pretty-text/image-short-url
|
||||
//= require ./pretty-text/upload-short-url
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { default as WhiteLister } from "pretty-text/white-lister";
|
||||
import { sanitize } from "pretty-text/sanitizer";
|
||||
import guid from "pretty-text/guid";
|
||||
import { ATTACHMENT_CSS_CLASS } from "pretty-text/upload-short-url";
|
||||
|
||||
function deprecate(feature, name) {
|
||||
return function() {
|
||||
|
@ -187,6 +188,26 @@ function setupImageDimensions(md) {
|
|||
md.renderer.rules.image = renderImage;
|
||||
}
|
||||
|
||||
function renderAttachment(tokens, idx, options, env, slf) {
|
||||
const linkOpenToken = tokens[idx];
|
||||
const linkTextToken = tokens[idx + 1];
|
||||
const split = linkTextToken.content.split("|");
|
||||
const isValid = !linkOpenToken.attrs[
|
||||
linkOpenToken.attrIndex("data-orig-href")
|
||||
];
|
||||
|
||||
if (isValid && split.length === 2 && split[1] === ATTACHMENT_CSS_CLASS) {
|
||||
linkOpenToken.attrs.unshift(["class", split[1]]);
|
||||
linkTextToken.content = split[0];
|
||||
}
|
||||
|
||||
return slf.renderToken(tokens, idx, options);
|
||||
}
|
||||
|
||||
function setupAttachments(md) {
|
||||
md.renderer.rules.link_open = renderAttachment;
|
||||
}
|
||||
|
||||
let Helpers;
|
||||
|
||||
export function setup(opts, siteSettings, state) {
|
||||
|
@ -276,6 +297,7 @@ export function setup(opts, siteSettings, state) {
|
|||
setupUrlDecoding(opts.engine);
|
||||
setupHoister(opts.engine);
|
||||
setupImageDimensions(opts.engine);
|
||||
setupAttachments(opts.engine);
|
||||
setupBlockBBCode(opts.engine);
|
||||
setupInlineBBCode(opts.engine);
|
||||
setupTextPostProcessRuler(opts.engine);
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
// 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 lookup = state.md.options.discourse.lookupImageUrls;
|
||||
let longUrls = (lookup && lookup(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) {
|
||||
const opts = helper.getOptions();
|
||||
if (opts.previewing) helper.whiteList(["img.resizable"]);
|
||||
helper.whiteList(["img[data-orig-src]"]);
|
||||
helper.registerPlugin(md => {
|
||||
md.core.ruler.push("image-protocol", rule);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// add image to array if src has an upload
|
||||
function addImage(uploads, token) {
|
||||
if (token.attrs) {
|
||||
for (let i = 0; i < token.attrs.length; i++) {
|
||||
if (token.attrs[i][1].indexOf("upload://") === 0) {
|
||||
uploads.push([token, i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rule(state) {
|
||||
let uploads = [];
|
||||
|
||||
for (let i = 0; i < state.tokens.length; i++) {
|
||||
let blockToken = state.tokens[i];
|
||||
|
||||
if (blockToken.tag === "img" || blockToken.tag === "a") {
|
||||
addImage(uploads, blockToken);
|
||||
}
|
||||
|
||||
if (!blockToken.children) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < blockToken.children.length; j++) {
|
||||
let token = blockToken.children[j];
|
||||
if (token.tag === "img" || token.tag === "a") {
|
||||
addImage(uploads, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uploads.length > 0) {
|
||||
let srcList = uploads.map(([token, srcIndex]) => token.attrs[srcIndex][1]);
|
||||
let lookup = state.md.options.discourse.lookupUploadUrls;
|
||||
let longUrls = (lookup && lookup(srcList)) || {};
|
||||
|
||||
uploads.forEach(([token, srcIndex]) => {
|
||||
let origSrc = token.attrs[srcIndex][1];
|
||||
let mapped = longUrls[origSrc];
|
||||
|
||||
switch (token.tag) {
|
||||
case "img":
|
||||
if (mapped) {
|
||||
token.attrs[srcIndex][1] = mapped.url;
|
||||
} else {
|
||||
token.attrs[srcIndex][1] = state.md.options.discourse.getURL(
|
||||
"/images/transparent.png"
|
||||
);
|
||||
|
||||
token.attrs.push(["data-orig-src", origSrc]);
|
||||
}
|
||||
break;
|
||||
case "a":
|
||||
if (mapped) {
|
||||
token.attrs[srcIndex][1] = mapped.short_path;
|
||||
} else {
|
||||
token.attrs[srcIndex][1] = state.md.options.discourse.getURL(
|
||||
"/404"
|
||||
);
|
||||
|
||||
token.attrs.push(["data-orig-href", origSrc]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
const opts = helper.getOptions();
|
||||
if (opts.previewing) helper.whiteList(["img.resizable"]);
|
||||
helper.whiteList(["img[data-orig-src]", "a[data-orig-href]"]);
|
||||
|
||||
helper.registerPlugin(md => {
|
||||
md.core.ruler.push("upload-protocol", rule);
|
||||
});
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
let _cache = {};
|
||||
|
||||
export function lookupCachedUploadUrl(shortUrl) {
|
||||
return _cache[shortUrl];
|
||||
}
|
||||
|
||||
export function lookupUncachedUploadUrls(urls, ajax) {
|
||||
return ajax("/uploads/lookup-urls", {
|
||||
method: "POST",
|
||||
data: { short_urls: urls }
|
||||
}).then(uploads => {
|
||||
uploads.forEach(upload =>
|
||||
cacheShortUploadUrl(upload.short_url, upload.url)
|
||||
);
|
||||
urls.forEach(url =>
|
||||
cacheShortUploadUrl(url, lookupCachedUploadUrl(url) || "missing")
|
||||
);
|
||||
return uploads;
|
||||
});
|
||||
}
|
||||
|
||||
export function cacheShortUploadUrl(shortUrl, url) {
|
||||
_cache[shortUrl] = url;
|
||||
}
|
||||
|
||||
export function resetCache() {
|
||||
_cache = {};
|
||||
}
|
||||
|
||||
function _loadCachedShortUrls($images) {
|
||||
$images.each((idx, image) => {
|
||||
const $image = $(image);
|
||||
const url = lookupCachedUploadUrl($image.data("orig-src"));
|
||||
|
||||
if (url) {
|
||||
$image.removeAttr("data-orig-src");
|
||||
if (url !== "missing") {
|
||||
$image.attr("src", url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _loadShortUrls($images, ajax) {
|
||||
const urls = $images.toArray().map(img => $(img).data("orig-src"));
|
||||
return lookupUncachedUploadUrls(urls, ajax).then(() =>
|
||||
_loadCachedShortUrls($images)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveAllShortUrls(ajax) {
|
||||
let $shortUploadUrls = $("img[data-orig-src]");
|
||||
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
_loadCachedShortUrls($shortUploadUrls);
|
||||
|
||||
$shortUploadUrls = $("img[data-orig-src]");
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
// this is carefully batched so we can do a leading debounce (trigger right away)
|
||||
return Ember.run.debounce(
|
||||
null,
|
||||
() => _loadShortUrls($shortUploadUrls, ajax),
|
||||
450,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ export function buildOptions(state) {
|
|||
lookupPrimaryUserGroupByPostNumber,
|
||||
formatUsername,
|
||||
emojiUnicodeReplacer,
|
||||
lookupImageUrls,
|
||||
lookupUploadUrls,
|
||||
previewing,
|
||||
linkify,
|
||||
censoredWords
|
||||
|
@ -65,7 +65,7 @@ export function buildOptions(state) {
|
|||
lookupPrimaryUserGroupByPostNumber,
|
||||
formatUsername,
|
||||
emojiUnicodeReplacer,
|
||||
lookupImageUrls,
|
||||
lookupUploadUrls,
|
||||
censoredWords,
|
||||
allowedHrefSchemes: siteSettings.allowed_href_schemes
|
||||
? siteSettings.allowed_href_schemes.split("|")
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
let _cache = {};
|
||||
|
||||
export function lookupCachedUploadUrl(shortUrl) {
|
||||
return _cache[shortUrl] || {};
|
||||
}
|
||||
|
||||
const MISSING = "missing";
|
||||
|
||||
export function lookupUncachedUploadUrls(urls, ajax) {
|
||||
return ajax("/uploads/lookup-urls", {
|
||||
method: "POST",
|
||||
data: { short_urls: urls }
|
||||
}).then(uploads => {
|
||||
uploads.forEach(upload => {
|
||||
cacheShortUploadUrl(upload.short_url, {
|
||||
url: upload.url,
|
||||
short_path: upload.short_path
|
||||
});
|
||||
});
|
||||
|
||||
urls.forEach(url =>
|
||||
cacheShortUploadUrl(url, {
|
||||
url: lookupCachedUploadUrl(url).url || MISSING,
|
||||
short_path: lookupCachedUploadUrl(url).short_path || MISSING
|
||||
})
|
||||
);
|
||||
|
||||
return uploads;
|
||||
});
|
||||
}
|
||||
|
||||
export function cacheShortUploadUrl(shortUrl, value) {
|
||||
_cache[shortUrl] = value;
|
||||
}
|
||||
|
||||
export function resetCache() {
|
||||
_cache = {};
|
||||
}
|
||||
|
||||
export const ATTACHMENT_CSS_CLASS = "attachment";
|
||||
|
||||
function _loadCachedShortUrls($uploads) {
|
||||
$uploads.each((idx, upload) => {
|
||||
const $upload = $(upload);
|
||||
let url;
|
||||
|
||||
switch (upload.tagName) {
|
||||
case "A":
|
||||
url = lookupCachedUploadUrl($upload.data("orig-href")).short_path;
|
||||
|
||||
if (url) {
|
||||
$upload.removeAttr("data-orig-href");
|
||||
|
||||
if (url !== MISSING) {
|
||||
$upload.attr("href", url);
|
||||
const content = $upload.text().split("|");
|
||||
|
||||
if (content[1] === ATTACHMENT_CSS_CLASS) {
|
||||
$upload.addClass(ATTACHMENT_CSS_CLASS);
|
||||
$upload.text(content[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "IMG":
|
||||
url = lookupCachedUploadUrl($upload.data("orig-src")).url;
|
||||
|
||||
if (url) {
|
||||
$upload.removeAttr("data-orig-src");
|
||||
|
||||
if (url !== MISSING) {
|
||||
$upload.attr("src", url);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _loadShortUrls($uploads, ajax) {
|
||||
const urls = $uploads.toArray().map(upload => {
|
||||
const $upload = $(upload);
|
||||
return $upload.data("orig-src") || $upload.data("orig-href");
|
||||
});
|
||||
|
||||
return lookupUncachedUploadUrls(urls, ajax).then(() =>
|
||||
_loadCachedShortUrls($uploads)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveAllShortUrls(ajax) {
|
||||
const attributes = "img[data-orig-src], a[data-orig-href]";
|
||||
let $shortUploadUrls = $(attributes);
|
||||
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
_loadCachedShortUrls($shortUploadUrls);
|
||||
|
||||
$shortUploadUrls = $(attributes);
|
||||
if ($shortUploadUrls.length > 0) {
|
||||
// this is carefully batched so we can do a leading debounce (trigger right away)
|
||||
return Ember.run.debounce(
|
||||
null,
|
||||
() => _loadShortUrls($shortUploadUrls, ajax),
|
||||
450,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@ require_dependency 'upload_creator'
|
|||
require_dependency "file_store/local_store"
|
||||
|
||||
class UploadsController < ApplicationController
|
||||
requires_login except: [:show]
|
||||
requires_login except: [:show, :show_short]
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show]
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show, :show_short]
|
||||
|
||||
def create
|
||||
# capture current user for block later on
|
||||
|
@ -56,8 +56,12 @@ class UploadsController < ApplicationController
|
|||
uploads = []
|
||||
|
||||
if (params[:short_urls] && params[:short_urls].length > 0)
|
||||
PrettyText::Helpers.lookup_image_urls(params[:short_urls]).each do |short_url, url|
|
||||
uploads << { short_url: short_url, url: url }
|
||||
PrettyText::Helpers.lookup_upload_urls(params[:short_urls]).each do |short_url, paths|
|
||||
uploads << {
|
||||
short_url: short_url,
|
||||
url: paths[:url],
|
||||
short_path: paths[:short_path]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -76,23 +80,31 @@ class UploadsController < ApplicationController
|
|||
return render_404 unless local_store.has_been_uploaded?(upload.url)
|
||||
end
|
||||
|
||||
opts = {
|
||||
filename: upload.original_filename,
|
||||
content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type,
|
||||
}
|
||||
opts[:disposition] = "inline" if params[:inline]
|
||||
opts[:disposition] ||= "attachment" unless FileHelper.is_supported_image?(upload.original_filename)
|
||||
|
||||
file_path = Discourse.store.path_for(upload)
|
||||
return render_404 unless file_path
|
||||
|
||||
send_file(file_path, opts)
|
||||
send_file_local_upload(upload)
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show_short
|
||||
if SiteSetting.prevent_anons_from_downloading_files && current_user.nil?
|
||||
return render_404
|
||||
end
|
||||
|
||||
sha1 = Upload.sha1_from_base62_encoded(params[:base62])
|
||||
|
||||
if upload = Upload.find_by(sha1: sha1)
|
||||
if Discourse.store.internal?
|
||||
send_file_local_upload(upload)
|
||||
else
|
||||
redirect_to Discourse.store.path_for(upload)
|
||||
end
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
def metadata
|
||||
params.require(:url)
|
||||
upload = Upload.get_from_url(params[:url])
|
||||
|
@ -165,4 +177,24 @@ class UploadsController < ApplicationController
|
|||
tempfile&.close!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_file_local_upload(upload)
|
||||
opts = {
|
||||
filename: upload.original_filename,
|
||||
content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type
|
||||
}
|
||||
|
||||
if params[:inline]
|
||||
opts[:disposition] = "inline"
|
||||
elsif !FileHelper.is_supported_image?(upload.original_filename)
|
||||
opts[:disposition] = "attachment"
|
||||
end
|
||||
|
||||
file_path = Discourse.store.path_for(upload)
|
||||
return render_404 unless file_path
|
||||
|
||||
send_file(file_path, opts)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -889,8 +889,10 @@ class Post < ActiveRecord::Base
|
|||
upload_patterns = [
|
||||
/\/uploads\/#{RailsMultisite::ConnectionManagement.current_db}\//,
|
||||
/\/original\//,
|
||||
/\/optimized\//
|
||||
/\/optimized\//,
|
||||
/\/uploads\/short-url\/[a-zA-Z0-9]+\..*/
|
||||
]
|
||||
|
||||
fragments ||= Nokogiri::HTML::fragment(self.cooked)
|
||||
links = fragments.css("a/@href", "img/@src").map { |media| media.value }.uniq
|
||||
|
||||
|
@ -911,7 +913,7 @@ class Post < ActiveRecord::Base
|
|||
if path.include? "optimized"
|
||||
OptimizedImage.extract_sha1(path)
|
||||
else
|
||||
Upload.extract_sha1(path)
|
||||
Upload.extract_sha1(path) || Upload.sha1_from_short_path(path)
|
||||
end
|
||||
|
||||
yield(src, path, sha1)
|
||||
|
|
|
@ -117,7 +117,28 @@ class Upload < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def short_url
|
||||
"upload://#{Base62.encode(sha1.hex)}.#{extension}"
|
||||
"upload://#{short_url_basename}"
|
||||
end
|
||||
|
||||
def short_path
|
||||
self.class.short_path(sha1: self.sha1, extension: self.extension)
|
||||
end
|
||||
|
||||
def self.short_path(sha1:, extension:)
|
||||
@url_helpers ||= Rails.application.routes.url_helpers
|
||||
|
||||
@url_helpers.upload_short_path(
|
||||
base62: self.base62_sha1(sha1),
|
||||
extension: extension
|
||||
)
|
||||
end
|
||||
|
||||
def self.base62_sha1(sha1)
|
||||
Base62.encode(sha1.hex)
|
||||
end
|
||||
|
||||
def base62_sha1
|
||||
Upload.base62_sha1(upload.sha1)
|
||||
end
|
||||
|
||||
def local?
|
||||
|
@ -180,9 +201,20 @@ class Upload < ActiveRecord::Base
|
|||
get_dimension(:thumbnail_height)
|
||||
end
|
||||
|
||||
def self.sha1_from_short_path(path)
|
||||
if path =~ /(\/uploads\/short-url\/)([a-zA-Z0-9]+)(\..*)?/
|
||||
self.sha1_from_base62_encoded($2)
|
||||
end
|
||||
end
|
||||
|
||||
def self.sha1_from_short_url(url)
|
||||
if url =~ /(upload:\/\/)?([a-zA-Z0-9]+)(\..*)?/
|
||||
sha1 = Base62.decode($2).to_s(16)
|
||||
self.sha1_from_base62_encoded($2)
|
||||
end
|
||||
end
|
||||
|
||||
def self.sha1_from_base62_encoded(encoded_sha1)
|
||||
sha1 = Base62.decode(encoded_sha1).to_s(16)
|
||||
|
||||
if sha1.length > SHA1_LENGTH
|
||||
nil
|
||||
|
@ -190,7 +222,6 @@ class Upload < ActiveRecord::Base
|
|||
sha1.rjust(SHA1_LENGTH, '0')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.generate_digest(path)
|
||||
Digest::SHA1.file(path).hexdigest
|
||||
|
@ -322,6 +353,12 @@ class Upload < ActiveRecord::Base
|
|||
problems
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def short_url_basename
|
||||
"#{Upload.base62_sha1(sha1)}.#{extension}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -486,6 +486,7 @@ Discourse::Application.routes.draw do
|
|||
|
||||
# used to download original images
|
||||
get "uploads/:site/:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, sha: /\h{40}/, extension: /[a-z0-9\.]+/i }
|
||||
get "uploads/short-url/:base62(.:extension)" => "uploads#show_short", constraints: { site: /\w+/, base62: /[a-zA-Z0-9]+/, extension: /[a-z0-9\.]+/i }, as: :upload_short
|
||||
# used to download attachments
|
||||
get "uploads/:site/original/:tree:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, tree: /([a-z0-9]+\/)+/i, sha: /\h{40}/, extension: /[a-z0-9\.]+/i }
|
||||
# used to download attachments (old route)
|
||||
|
|
|
@ -101,8 +101,13 @@ module FileStore
|
|||
end
|
||||
|
||||
def path_for(upload)
|
||||
url = upload.try(:url)
|
||||
FileStore::LocalStore.new.path_for(upload) if url && url[/^\/[^\/]/]
|
||||
url = upload&.url
|
||||
|
||||
if url && url[/^\/[^\/]/]
|
||||
FileStore::LocalStore.new.path_for(upload)
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
|
||||
def cdn_url(url)
|
||||
|
|
|
@ -159,7 +159,7 @@ module PrettyText
|
|||
__optInput.categoryHashtagLookup = __categoryLookup;
|
||||
__optInput.customEmoji = #{custom_emoji.to_json};
|
||||
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
|
||||
__optInput.lookupImageUrls = __lookupImageUrls;
|
||||
__optInput.lookupUploadUrls = __lookupUploadUrls;
|
||||
__optInput.censoredWords = #{WordWatcher.words_for_action(:censor).join('|').to_json};
|
||||
JS
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ module PrettyText
|
|||
end
|
||||
end
|
||||
|
||||
def lookup_image_urls(urls)
|
||||
def lookup_upload_urls(urls)
|
||||
map = {}
|
||||
result = {}
|
||||
|
||||
|
@ -66,11 +66,16 @@ module PrettyText
|
|||
reverse_map[value] << key
|
||||
end
|
||||
|
||||
Upload.where(sha1: map.values).pluck(:sha1, :url).each do |row|
|
||||
sha1, url = row
|
||||
Upload.where(sha1: map.values).pluck(:sha1, :url, :extension).each do |row|
|
||||
sha1, url, extension = row
|
||||
|
||||
if short_urls = reverse_map[sha1]
|
||||
short_urls.each { |short_url| result[short_url] = url }
|
||||
short_urls.each do |short_url|
|
||||
result[short_url] = {
|
||||
url: url,
|
||||
short_path: Upload.short_path(sha1: sha1, extension: extension)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -65,8 +65,8 @@ function __getURL(url) {
|
|||
return url;
|
||||
}
|
||||
|
||||
function __lookupImageUrls(urls) {
|
||||
return __helpers.lookup_image_urls(urls);
|
||||
function __lookupUploadUrls(urls) {
|
||||
return __helpers.lookup_upload_urls(urls);
|
||||
}
|
||||
|
||||
function __getTopicInfo(i) {
|
||||
|
|
|
@ -303,21 +303,12 @@ describe FileStore::S3Store do
|
|||
end
|
||||
|
||||
describe ".path_for" do
|
||||
|
||||
def assert_path(path, expected)
|
||||
upload = Upload.new(url: path)
|
||||
|
||||
path = store.path_for(upload)
|
||||
expected = FileStore::LocalStore.new.path_for(upload) if expected
|
||||
|
||||
expect(path).to eq(expected)
|
||||
end
|
||||
|
||||
it "correctly falls back to local" do
|
||||
assert_path("/hello", "/hello")
|
||||
assert_path("//hello", nil)
|
||||
assert_path("http://hello", nil)
|
||||
assert_path("https://hello", nil)
|
||||
local_upload = Fabricate(:upload)
|
||||
s3_upload = Fabricate(:upload_s3)
|
||||
|
||||
expect(Discourse.store.path_for(local_upload)).to eq(local_upload.url)
|
||||
expect(Discourse.store.path_for(s3_upload)).to eq(s3_upload.url)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1260,9 +1260,11 @@ HTML
|
|||
|
||||
end
|
||||
|
||||
describe "image decoding" do
|
||||
describe "upload decoding" do
|
||||
|
||||
it "can decode upload:// for default setup" do
|
||||
set_cdn_url('https://cdn.com')
|
||||
|
||||
upload = Fabricate(:upload)
|
||||
|
||||
raw = <<~RAW
|
||||
|
@ -1274,6 +1276,12 @@ HTML
|
|||
- ![upload](#{upload.short_url})
|
||||
|
||||
![upload](#{upload.short_url.gsub(".png", "")})
|
||||
|
||||
[some attachment](#{upload.short_url})
|
||||
|
||||
[some attachment|attachment](#{upload.short_url})
|
||||
|
||||
[some attachment|random](#{upload.short_url})
|
||||
RAW
|
||||
|
||||
cooked = <<~HTML
|
||||
|
@ -1290,6 +1298,9 @@ HTML
|
|||
</li>
|
||||
</ul>
|
||||
<p><img src="#{upload.url}" alt="upload"></p>
|
||||
<p><a href="#{upload.short_path}">some attachment</a></p>
|
||||
<p><a class="attachment" href="#{upload.short_path}">some attachment</a></p>
|
||||
<p><a href="#{upload.short_path}">some attachment|random</a></p>
|
||||
HTML
|
||||
|
||||
expect(PrettyText.cook(raw)).to eq(cooked.strip)
|
||||
|
@ -1297,10 +1308,15 @@ HTML
|
|||
|
||||
it "can place a blank image if we can not find the upload" do
|
||||
|
||||
raw = "![upload](upload://abcABC.png)"
|
||||
raw = <<~MD
|
||||
![upload](upload://abcABC.png)
|
||||
|
||||
[some attachment|attachment](upload://abcdefg.png)
|
||||
MD
|
||||
|
||||
cooked = <<~HTML
|
||||
<p><img src="/images/transparent.png" alt="upload" data-orig-src="upload://abcABC.png"></p>
|
||||
<p><a href="/404" data-orig-href="upload://abcdefg.png">some attachment|attachment</a></p>
|
||||
HTML
|
||||
|
||||
expect(PrettyText.cook(raw)).to eq(cooked.strip)
|
||||
|
|
|
@ -1229,29 +1229,11 @@ describe Post do
|
|||
end
|
||||
|
||||
describe '#link_post_uploads' do
|
||||
fab!(:video_upload) do
|
||||
Fabricate(:upload,
|
||||
url: '/uploads/default/original/1X/1/1234567890123456.mp4'
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:image_upload) do
|
||||
Fabricate(:upload,
|
||||
url: '/uploads/default/original/1X/1/1234567890123456.jpg'
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:audio_upload) do
|
||||
Fabricate(:upload,
|
||||
url: '/uploads/default/original/1X/1/1234567890123456.ogg'
|
||||
)
|
||||
end
|
||||
|
||||
fab!(:attachment_upload) do
|
||||
Fabricate(:upload,
|
||||
url: '/uploads/default/original/1X/1/1234567890123456.csv'
|
||||
)
|
||||
end
|
||||
fab!(:video_upload) { Fabricate(:upload, extension: "mp4") }
|
||||
fab!(:image_upload) { Fabricate(:upload) }
|
||||
fab!(:audio_upload) { Fabricate(:upload, extension: "ogg") }
|
||||
fab!(:attachment_upload) { Fabricate(:upload, extension: "csv") }
|
||||
fab!(:attachment_upload_2) { Fabricate(:upload) }
|
||||
|
||||
let(:base_url) { "#{Discourse.base_url_no_prefix}#{Discourse.base_uri}" }
|
||||
let(:video_url) { "#{base_url}#{video_upload.url}" }
|
||||
|
@ -1260,6 +1242,7 @@ describe Post do
|
|||
let(:raw) do
|
||||
<<~RAW
|
||||
<a href="#{attachment_upload.url}">Link</a>
|
||||
[test|attachment](#{attachment_upload_2.short_url})
|
||||
<img src="#{image_upload.url}">
|
||||
|
||||
<video width="100%" height="100%" controls>
|
||||
|
@ -1285,7 +1268,11 @@ describe Post do
|
|||
post.link_post_uploads
|
||||
|
||||
expect(PostUpload.where(post: post).pluck(:upload_id)).to contain_exactly(
|
||||
video_upload.id, image_upload.id, audio_upload.id, attachment_upload.id
|
||||
video_upload.id,
|
||||
image_upload.id,
|
||||
audio_upload.id,
|
||||
attachment_upload.id,
|
||||
attachment_upload_2.id
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -253,6 +253,16 @@ describe Upload do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.sha1_from_short_path' do
|
||||
it "should be able to lookup sha1" do
|
||||
path = "/uploads/short-url/3UjQ4jHoyeoQndk5y3qHzm3QVTQ.png"
|
||||
sha1 = "1b6453892473a467d07372d45eb05abc2031647a"
|
||||
|
||||
expect(Upload.sha1_from_short_path(path)).to eq(sha1)
|
||||
expect(Upload.sha1_from_short_path(path.sub(".png", ""))).to eq(sha1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_s' do
|
||||
it 'should return the right value' do
|
||||
expect(upload.to_s).to eq(upload.url)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UploadsController do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
describe '#create' do
|
||||
it 'requires you to be logged in' do
|
||||
post "/uploads.json"
|
||||
|
@ -10,7 +12,9 @@ describe UploadsController do
|
|||
end
|
||||
|
||||
context 'logged in' do
|
||||
let!(:user) { sign_in(Fabricate(:user)) }
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
let(:logo) do
|
||||
Rack::Test::UploadedFile.new(file_from_fixtures("logo.png"))
|
||||
|
@ -195,11 +199,6 @@ describe UploadsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
let(:site) { "default" }
|
||||
let(:sha) { Digest::SHA1.hexdigest("discourse") }
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
def upload_file(file, folder = "images")
|
||||
fake_logo = Rack::Test::UploadedFile.new(file_from_fixtures(file, folder))
|
||||
SiteSetting.authorized_extensions = "*"
|
||||
|
@ -213,10 +212,14 @@ describe UploadsController do
|
|||
expect(response.status).to eq(200)
|
||||
|
||||
url = JSON.parse(response.body)["url"]
|
||||
upload = Upload.where(url: url).first
|
||||
upload = Upload.get_from_url(url)
|
||||
upload
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
let(:site) { "default" }
|
||||
let(:sha) { Digest::SHA1.hexdigest("discourse") }
|
||||
|
||||
context "when using external storage" do
|
||||
fab!(:upload) { upload_file("small.pdf", "pdf") }
|
||||
|
||||
|
@ -284,7 +287,7 @@ describe UploadsController do
|
|||
it "returns 404 when an anonymous user tries to download a file" do
|
||||
skip("this only works when nginx/apache is asset server") if Discourse::Application.config.public_file_server.enabled
|
||||
upload = upload_file("small.pdf", "pdf")
|
||||
delete "/session/#{user.username}.json" # upload a file, then sign out
|
||||
delete "/session/#{user.username}.json"
|
||||
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
get upload.url
|
||||
|
@ -293,9 +296,66 @@ describe UploadsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#show_short" do
|
||||
describe "local store" do
|
||||
fab!(:image_upload) { upload_file("smallest.png") }
|
||||
|
||||
it "returns the right response" do
|
||||
get image_upload.short_path
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(response.headers["Content-Disposition"])
|
||||
.to include("attachment; filename=\"#{image_upload.original_filename}\"")
|
||||
end
|
||||
|
||||
it "returns the right response when `inline` param is given" do
|
||||
get "#{image_upload.short_path}?inline=1"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(response.headers["Content-Disposition"])
|
||||
.to include("inline; filename=\"#{image_upload.original_filename}\"")
|
||||
end
|
||||
|
||||
it "returns the right response when base62 param is invalid " do
|
||||
get "/uploads/short-url/12345.png"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns the right response when anon tries to download a file " \
|
||||
"when prevent_anons_from_downloading_files is true" do
|
||||
|
||||
delete "/session/#{user.username}.json"
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
|
||||
get image_upload.short_path
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "s3 store" do
|
||||
let(:upload) { Fabricate(:upload_s3) }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.s3_access_key_id = "fakeid7974664"
|
||||
SiteSetting.s3_secret_access_key = "fakesecretid7974664"
|
||||
end
|
||||
|
||||
it "should redirect to the s3 URL" do
|
||||
get upload.short_path
|
||||
|
||||
expect(response).to redirect_to(upload.url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#lookup_urls' do
|
||||
it 'can look up long urls' do
|
||||
sign_in(Fabricate(:user))
|
||||
sign_in(user)
|
||||
upload = Fabricate(:upload)
|
||||
|
||||
post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] }
|
||||
|
@ -303,6 +363,7 @@ describe UploadsController do
|
|||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result[0]["url"]).to eq(upload.url)
|
||||
expect(result[0]["short_path"]).to eq(upload.short_path)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -327,7 +388,7 @@ describe UploadsController do
|
|||
|
||||
describe 'when signed in' do
|
||||
before do
|
||||
sign_in(Fabricate(:user))
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'when url is invalid' do
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
|
||||
acceptance("Composer Attachment", {
|
||||
loggedIn: true,
|
||||
pretend(server, helper) {
|
||||
server.post("/uploads/lookup-urls", () => {
|
||||
return helper.response([
|
||||
{
|
||||
short_url: "upload://asdsad.png",
|
||||
url: "/uploads/default/3X/1/asjdiasjdiasida.png",
|
||||
short_path: "/uploads/short-url/asdsad.png"
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("attachments are cooked properly", async assert => {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
|
||||
await fillIn(".d-editor-input", "[test](upload://abcdefg.png)");
|
||||
|
||||
assert.equal(
|
||||
find(".d-editor-preview:visible")
|
||||
.html()
|
||||
.trim(),
|
||||
'<p><a href="/404">test</a></p>'
|
||||
);
|
||||
|
||||
await fillIn(".d-editor-input", "[test|attachment](upload://asdsad.png)");
|
||||
|
||||
assert.equal(
|
||||
find(".d-editor-preview:visible")
|
||||
.html()
|
||||
.trim(),
|
||||
'<p><a href="/uploads/short-url/asdsad.png" class="attachment">test</a></p>'
|
||||
);
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
import {
|
||||
lookupCachedUploadUrl,
|
||||
resolveAllShortUrls,
|
||||
resetCache
|
||||
} from "pretty-text/image-short-url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
QUnit.module("lib:pretty-text/image-short-url", {
|
||||
beforeEach() {
|
||||
const response = object => {
|
||||
return [200, { "Content-Type": "application/json" }, object];
|
||||
};
|
||||
|
||||
const srcs = [
|
||||
{
|
||||
short_url: "upload://a.jpeg",
|
||||
url: "/uploads/default/original/3X/c/b/1.jpeg"
|
||||
},
|
||||
{
|
||||
short_url: "upload://b.jpeg",
|
||||
url: "/uploads/default/original/3X/c/b/2.jpeg"
|
||||
}
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
server.post("/uploads/lookup-urls", () => { //eslint-disable-line
|
||||
return response(srcs);
|
||||
});
|
||||
|
||||
fixture().html(
|
||||
srcs.map(src => `<img data-orig-src="${src.url}">`).join("")
|
||||
);
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
resetCache();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("resolveAllShortUrls", async assert => {
|
||||
let lookup;
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://a.jpeg");
|
||||
assert.notOk(lookup);
|
||||
|
||||
await resolveAllShortUrls(ajax);
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://a.jpeg");
|
||||
assert.equal(lookup, "/uploads/default/original/3X/c/b/1.jpeg");
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://b.jpeg");
|
||||
assert.equal(lookup, "/uploads/default/original/3X/c/b/2.jpeg");
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://c.jpeg");
|
||||
assert.notOk(lookup);
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
lookupCachedUploadUrl,
|
||||
resolveAllShortUrls,
|
||||
resetCache
|
||||
} from "pretty-text/upload-short-url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
QUnit.module("lib:pretty-text/upload-short-url", {
|
||||
beforeEach() {
|
||||
const response = object => {
|
||||
return [200, { "Content-Type": "application/json" }, object];
|
||||
};
|
||||
|
||||
const imageSrcs = [
|
||||
{
|
||||
short_url: "upload://a.jpeg",
|
||||
url: "/uploads/default/original/3X/c/b/1.jpeg",
|
||||
short_path: "/uploads/short-url/a.jpeg"
|
||||
},
|
||||
{
|
||||
short_url: "upload://b.jpeg",
|
||||
url: "/uploads/default/original/3X/c/b/2.jpeg",
|
||||
short_path: "/uploads/short-url/b.jpeg"
|
||||
}
|
||||
];
|
||||
|
||||
const attachmentSrcs = [
|
||||
{
|
||||
short_url: "upload://c.pdf",
|
||||
url: "/uploads/default/original/3X/c/b/3.pdf",
|
||||
short_path: "/uploads/short-url/c.pdf"
|
||||
}
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
server.post("/uploads/lookup-urls", () => { //eslint-disable-line
|
||||
return response(imageSrcs.concat(attachmentSrcs));
|
||||
});
|
||||
|
||||
fixture().html(
|
||||
imageSrcs.map(src => `<img data-orig-src="${src.url}">`).join("") +
|
||||
attachmentSrcs.map(src => `<a data-orig-href="${src.url}">`).join("")
|
||||
);
|
||||
},
|
||||
|
||||
afterEach() {
|
||||
resetCache();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("resolveAllShortUrls", async assert => {
|
||||
let lookup;
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://a.jpeg");
|
||||
assert.deepEqual(lookup, {});
|
||||
|
||||
await resolveAllShortUrls(ajax);
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://a.jpeg");
|
||||
|
||||
assert.deepEqual(lookup, {
|
||||
url: "/uploads/default/original/3X/c/b/1.jpeg",
|
||||
short_path: "/uploads/short-url/a.jpeg"
|
||||
});
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://b.jpeg");
|
||||
|
||||
assert.deepEqual(lookup, {
|
||||
url: "/uploads/default/original/3X/c/b/2.jpeg",
|
||||
short_path: "/uploads/short-url/b.jpeg"
|
||||
});
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://c.jpeg");
|
||||
assert.deepEqual(lookup, {});
|
||||
|
||||
lookup = lookupCachedUploadUrl("upload://c.pdf");
|
||||
assert.deepEqual(lookup, {
|
||||
url: "/uploads/default/original/3X/c/b/3.pdf",
|
||||
short_path: "/uploads/short-url/c.pdf"
|
||||
});
|
||||
});
|
|
@ -165,14 +165,19 @@ QUnit.test("allows valid uploads to go through", assert => {
|
|||
assert.not(bootbox.alert.calledOnce);
|
||||
});
|
||||
|
||||
var testUploadMarkdown = function(filename) {
|
||||
return getUploadMarkdown({
|
||||
var testUploadMarkdown = function(filename, opts = {}) {
|
||||
return getUploadMarkdown(
|
||||
Object.assign(
|
||||
{
|
||||
original_filename: filename,
|
||||
filesize: 42,
|
||||
thumbnail_width: 100,
|
||||
thumbnail_height: 200,
|
||||
url: "/uploads/123/abcdef.ext"
|
||||
});
|
||||
},
|
||||
opts
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
QUnit.test("getUploadMarkdown", assert => {
|
||||
|
@ -184,9 +189,12 @@ QUnit.test("getUploadMarkdown", assert => {
|
|||
testUploadMarkdown("[foo|bar].png"),
|
||||
"![%5Bfoo%7Cbar%5D|100x200](/uploads/123/abcdef.ext)"
|
||||
);
|
||||
assert.ok(
|
||||
testUploadMarkdown("important.txt") ===
|
||||
'<a class="attachment" href="/uploads/123/abcdef.ext">important.txt</a> (42 Bytes)\n'
|
||||
|
||||
const short_url = "uploads://asdaasd.ext";
|
||||
|
||||
assert.equal(
|
||||
testUploadMarkdown("important.txt", { short_url }),
|
||||
`[important.txt (42 Bytes)|attachment](${short_url})`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue