diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 8f2528cdd38..4a5de6d738b 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -13,6 +13,9 @@ import { tinyAvatar, displayErrorForUpload, getUploadMarkdown, validateUploadedFiles } from 'discourse/lib/utilities'; +import { lookupCachedUploadUrl, + lookupUncachedUploadUrls, + cacheShortUploadUrl } from 'pretty-text/image-short-url'; export default Ember.Component.extend({ classNames: ['wmd-controls'], @@ -191,6 +194,24 @@ export default Ember.Component.extend({ $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id)); }, + _loadShortUrls($images) { + const urls = _.map($images, img => $(img).data('orig-src')); + lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images)); + }, + + _loadCachedShortUrls($images) { + $images.each((idx, image) => { + let $image = $(image); + let url = lookupCachedUploadUrl($image.data('orig-src')); + if (url) { + $image.removeAttr('data-orig-src'); + if (url !== "missing") { + $image.attr('src', url); + } + } + }); + }, + _warnMentionedGroups($preview) { Ember.run.scheduleOnce('afterRender', () => { var found = this.get('warnedGroupMentions') || []; @@ -312,6 +333,7 @@ export default Ember.Component.extend({ if (upload && upload.url) { if (!this._xhr || !this._xhr._userCancelled) { const markdown = getUploadMarkdown(upload); + cacheShortUploadUrl(upload.short_url, upload.url); this.appEvents.trigger('composer:replace-text', uploadPlaceholder, markdown); this._resetUpload(false); } else { @@ -579,6 +601,19 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450); } + // Short upload urls + let $shortUploadUrls = $('img[data-orig-src]'); + + if ($shortUploadUrls.length > 0) { + this._loadCachedShortUrls($shortUploadUrls); + + $shortUploadUrls = $('img[data-orig-src]'); + if ($shortUploadUrls.length > 0) { + // this is carefully batched so we can do an leading debounce (trigger right away) + Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true); + } + } + let inline = {}; $('a.inline-onebox-loading', $preview).each(function(index, link) { let $link = $(link); diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 3a139c99223..a1e55234e27 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -298,7 +298,7 @@ export function getUploadMarkdown(upload) { if (isAnImage(upload.original_filename)) { const split = upload.original_filename.split('.'); const name = split[split.length-2]; - return `![${name}|${upload.width}x${upload.height}](${upload.url})`; + return `![${name}|${upload.width}x${upload.height}](${upload.short_url || upload.url})`; } else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) { return uploadLocation(upload.url); } else { diff --git a/app/assets/javascripts/pretty-text-bundle.js b/app/assets/javascripts/pretty-text-bundle.js index a572344df81..41324ba31e5 100644 --- a/app/assets/javascripts/pretty-text-bundle.js +++ b/app/assets/javascripts/pretty-text-bundle.js @@ -10,3 +10,4 @@ //= require ./pretty-text/sanitizer //= require ./pretty-text/oneboxer //= require ./pretty-text/inline-oneboxer +//= require ./pretty-text/image-short-url 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 index ee86ccc24bf..2211413f445 100644 --- 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 @@ -35,7 +35,8 @@ function rule(state) { if (images.length > 0) { let srcList = images.map(([token, srcIndex]) => token.attrs[srcIndex][1]); - let longUrls = state.md.options.discourse.lookupImageUrls(srcList); + let lookup = state.md.options.discourse.lookupImageUrls; + let longUrls = (lookup && lookup(srcList)) || {}; images.forEach(([token, srcIndex]) => { let origSrc = token.attrs[srcIndex][1]; diff --git a/app/assets/javascripts/pretty-text/image-short-url.js.es6 b/app/assets/javascripts/pretty-text/image-short-url.js.es6 new file mode 100644 index 00000000000..d815b46696c --- /dev/null +++ b/app/assets/javascripts/pretty-text/image-short-url.js.es6 @@ -0,0 +1,18 @@ +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 => _cache[upload.short_url] = upload.url); + urls.forEach(url => _cache[url] = _cache[url] || "missing"); + return uploads; + }); +} + +export function cacheShortUploadUrl(shortUrl, url) { + _cache[shortUrl] = url; +} diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index c19fa695342..da6fd016306 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -20,19 +20,32 @@ class UploadsController < ApplicationController if params[:synchronous] && (current_user.staff? || is_api?) data = create_upload(file, url, type, for_private_message, pasted) - render json: data.as_json + render json: serialize_upload(data) else Scheduler::Defer.later("Create Upload") do begin data = create_upload(file, url, type, for_private_message, pasted) ensure - MessageBus.publish("/uploads/#{type}", (data || {}).as_json, client_ids: [params[:client_id]]) + MessageBus.publish("/uploads/#{type}", serialize_upload(data), client_ids: [params[:client_id]]) end end render json: success_json end end + def lookup_urls + params.permit(short_urls: []) + 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 } + end + end + + render json: uploads.to_json + end + def show return render_404 if !RailsMultisite::ConnectionManagement.has_db?(params[:site]) @@ -57,6 +70,13 @@ class UploadsController < ApplicationController protected + def serialize_upload(data) + # as_json.as_json is not a typo... as_json in AM serializer returns keys as symbols, we need them + # as strings here + serialized = UploadSerializer.new(data, root: nil).as_json.as_json if Upload === data + serialized ||= (data || {}).as_json + end + def render_404 raise Discourse::NotFound end diff --git a/app/serializers/upload_serializer.rb b/app/serializers/upload_serializer.rb index 9e0d866248d..a02fbef9b09 100644 --- a/app/serializers/upload_serializer.rb +++ b/app/serializers/upload_serializer.rb @@ -1,5 +1,11 @@ class UploadSerializer < ApplicationSerializer - - attributes :id, :url, :original_filename, :filesize, :width, :height - + attributes :id, + :url, + :original_filename, + :filesize, + :width, + :height, + :extension, + :short_url, + :retain_hours end diff --git a/config/routes.rb b/config/routes.rb index 553bbc9e5b2..64fe59a0cbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -414,6 +414,7 @@ Discourse::Application.routes.draw do get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ } post "uploads" => "uploads#create" + post "uploads/lookup-urls" => "uploads#lookup_urls" # used to download original images get "uploads/:site/:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, sha: /\h{40}/, extension: /[a-z0-9\.]+/i } diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 03e0716bf26..31bf3577292 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -36,6 +36,13 @@ describe UploadsController do xhr :post, :create, file: logo, type: "super \# long \//\\ type with \\. $%^&*( chars" * 5 end + it 'can look up long urls' do + upload = Fabricate(:upload) + xhr :post, :lookup_urls, short_urls: [upload.short_url] + result = JSON.parse(response.body) + expect(result[0]["url"]).to eq(upload.url) + end + it 'is successful with an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) @@ -78,6 +85,7 @@ describe UploadsController do expect(response.status).to eq 200 expect(json["id"]).to be + expect(json["short_url"]).to eq("upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png") end it 'correctly sets retain_hours for admins' do