FEATURE: Add support for secure media (#7888)
This PR introduces a new secure media setting. When enabled, it prevent unathorized access to media uploads (files of type image, video and audio). When the `login_required` setting is enabled, then all media uploads will be protected from unauthorized (anonymous) access. When `login_required`is disabled, only media in private messages will be protected from unauthorized access. A few notes: - the `prevent_anons_from_downloading_files` setting no longer applies to audio and video uploads - the `secure_media` setting can only be enabled if S3 uploads are already enabled and configured - upload records have a new column, `secure`, which is a boolean `true/false` of the upload's secure status - when creating a public post with an upload that has already been uploaded and is marked as secure, the post creator will raise an error - when enabling or disabling the setting on a site with existing uploads, the rake task `uploads:ensure_correct_acl` should be used to update all uploads' secure status and their ACL on S3
This commit is contained in:
parent
56b19ba740
commit
102909edb3
|
@ -242,7 +242,6 @@ export function getUploadMarkdown(upload) {
|
|||
upload.thumbnail_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);
|
||||
|
|
|
@ -5,7 +5,7 @@ require "mini_mime"
|
|||
class UploadsController < ApplicationController
|
||||
requires_login except: [:show, :show_short]
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show, :show_short]
|
||||
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show, :show_short, :show_secure]
|
||||
protect_from_forgery except: :show
|
||||
|
||||
def create
|
||||
|
@ -110,6 +110,17 @@ class UploadsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def show_secure
|
||||
# do not serve uploads requested via XHR to prevent XSS
|
||||
return xhr_not_allowed if request.xhr?
|
||||
|
||||
if SiteSetting.secure_media?
|
||||
redirect_to Discourse.store.signed_url_for_path("#{params[:path]}.#{params[:extension]}")
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
def metadata
|
||||
params.require(:url)
|
||||
upload = Upload.get_from_url(params[:url])
|
||||
|
|
|
@ -7,8 +7,8 @@ module Jobs
|
|||
return if !SiteSetting.enable_s3_uploads
|
||||
|
||||
Upload.find_each do |upload|
|
||||
if !FileHelper.is_supported_image?(upload.original_filename)
|
||||
Discourse.store.update_upload_ACL(upload)
|
||||
if !FileHelper.is_supported_media?(upload.original_filename)
|
||||
upload.update_secure_status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -349,13 +349,26 @@ class UserNotifications < ActionMailer::Base
|
|||
end
|
||||
|
||||
def email_post_markdown(post, add_posted_by = false)
|
||||
result = +"#{post.raw}\n\n"
|
||||
result = +"#{post.with_secure_media? ? strip_secure_urls(post.raw) : post.raw}\n\n"
|
||||
if add_posted_by
|
||||
result << "#{I18n.t('user_notifications.posted_by', username: post.username, post_date: post.created_at.strftime("%m/%d/%Y"))}\n\n"
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def strip_secure_urls(raw)
|
||||
urls = Set.new
|
||||
raw.scan(URI.regexp(%w{http https})) { urls << $& }
|
||||
|
||||
urls.each do |url|
|
||||
if (url.start_with?(Discourse.store.s3_upload_host) && FileHelper.is_supported_media?(url))
|
||||
raw = raw.sub(url, "<p class='secure-media-notice'>#{I18n.t("emails.secure_media_placeholder")}</p>")
|
||||
end
|
||||
end
|
||||
|
||||
raw
|
||||
end
|
||||
|
||||
def self.get_context_posts(post, topic_user, user)
|
||||
if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) ||
|
||||
SiteSetting.private_email?
|
||||
|
|
|
@ -300,6 +300,15 @@ class Post < ActiveRecord::Base
|
|||
options[:user_id] = post_user.id if post_user
|
||||
options[:omit_nofollow] = true if omit_nofollow?
|
||||
|
||||
if self.with_secure_media?
|
||||
each_upload_url do |url|
|
||||
uri = URI.parse(url)
|
||||
if FileHelper.is_supported_media?(File.basename(uri.path))
|
||||
raw = raw.sub(Discourse.store.s3_upload_host, "#{Discourse.base_url}/secure-media-uploads")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cooked = post_analyzer.cook(raw, options)
|
||||
|
||||
new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked)
|
||||
|
@ -492,6 +501,11 @@ class Post < ActiveRecord::Base
|
|||
ReviewableFlaggedPost.pending.find_by(target: self)
|
||||
end
|
||||
|
||||
def with_secure_media?
|
||||
return false unless SiteSetting.secure_media?
|
||||
topic&.private_message? || SiteSetting.login_required?
|
||||
end
|
||||
|
||||
def hide!(post_action_type_id, reason = nil)
|
||||
return if hidden?
|
||||
|
||||
|
@ -882,6 +896,13 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
upload_ids |= Upload.where(id: downloaded_images.values).pluck(:id)
|
||||
|
||||
disallowed_uploads = []
|
||||
if SiteSetting.secure_media? && !topic&.private_message?
|
||||
disallowed_uploads = Upload.where(id: upload_ids, secure: true).pluck(:original_filename)
|
||||
end
|
||||
return disallowed_uploads if disallowed_uploads.count > 0
|
||||
|
||||
values = upload_ids.map! { |upload_id| "(#{self.id},#{upload_id})" }.join(",")
|
||||
|
||||
PostUpload.transaction do
|
||||
|
@ -893,6 +914,12 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def update_uploads_secure_status
|
||||
if Discourse.store.external?
|
||||
self.uploads.each { |upload| upload.update_secure_status }
|
||||
end
|
||||
end
|
||||
|
||||
def downloaded_images
|
||||
JSON.parse(self.custom_fields[Post::DOWNLOADED_IMAGES].presence || "{}")
|
||||
rescue JSON::ParserError
|
||||
|
@ -909,6 +936,7 @@ class Post < ActiveRecord::Base
|
|||
]
|
||||
|
||||
fragments ||= Nokogiri::HTML::fragment(self.cooked)
|
||||
|
||||
links = fragments.css("a/@href", "img/@src").map do |media|
|
||||
src = media.value
|
||||
next if src.blank?
|
||||
|
|
|
@ -30,9 +30,9 @@ class TopicConverter
|
|||
)
|
||||
|
||||
update_user_stats
|
||||
update_post_uploads_secure_status
|
||||
Jobs.enqueue(:topic_action_converter, topic_id: @topic.id)
|
||||
Jobs.enqueue(:delete_inaccessible_notifications, topic_id: @topic.id)
|
||||
|
||||
watch_topic(topic)
|
||||
end
|
||||
@topic
|
||||
|
@ -49,6 +49,7 @@ class TopicConverter
|
|||
)
|
||||
|
||||
add_allowed_users
|
||||
update_post_uploads_secure_status
|
||||
|
||||
Jobs.enqueue(:topic_action_converter, topic_id: @topic.id)
|
||||
Jobs.enqueue(:delete_inaccessible_notifications, topic_id: @topic.id)
|
||||
|
@ -97,4 +98,11 @@ class TopicConverter
|
|||
end
|
||||
end
|
||||
|
||||
def update_post_uploads_secure_status
|
||||
@topic.posts.each do |post|
|
||||
next if post.uploads.empty?
|
||||
post.update_uploads_secure_status
|
||||
post.rebake!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -176,7 +176,7 @@ class TopicLink < ActiveRecord::Base
|
|||
if upload = Upload.get_from_url(url)
|
||||
internal = Discourse.store.internal?
|
||||
# Store the same URL that will be used in the cooked version of the post
|
||||
url = UrlHelper.cook_url(upload.url)
|
||||
url = UrlHelper.cook_url(upload.url, secure: upload.secure?)
|
||||
elsif route = Discourse.route_for(parsed)
|
||||
internal = true
|
||||
|
||||
|
|
|
@ -140,11 +140,6 @@ class Upload < ActiveRecord::Base
|
|||
!(url =~ /^(https?:)?\/\//)
|
||||
end
|
||||
|
||||
def private?
|
||||
return false if self.for_theme || self.for_site_setting
|
||||
SiteSetting.prevent_anons_from_downloading_files && !FileHelper.is_supported_image?(self.original_filename)
|
||||
end
|
||||
|
||||
def fix_dimensions!
|
||||
return if !FileHelper.is_supported_image?("image.#{extension}")
|
||||
|
||||
|
@ -235,6 +230,34 @@ class Upload < ActiveRecord::Base
|
|||
self.posts.where("cooked LIKE '%/_optimized/%'").find_each(&:rebake!)
|
||||
end
|
||||
|
||||
def update_secure_status
|
||||
return false if self.for_theme || self.for_site_setting
|
||||
mark_secure = should_be_secure?
|
||||
|
||||
self.update_column("secure", mark_secure)
|
||||
Discourse.store.update_upload_ACL(self) if Discourse.store.external?
|
||||
end
|
||||
|
||||
def should_be_secure?
|
||||
mark_secure = false
|
||||
if FileHelper.is_supported_media?(self.original_filename)
|
||||
if SiteSetting.secure_media?
|
||||
mark_secure = true if SiteSetting.login_required?
|
||||
unless SiteSetting.login_required?
|
||||
# first post associated with upload determines secure status
|
||||
# i.e. an already public upload will stay public even if added to a new PM
|
||||
first_post_with_upload = self.posts.order(sort_order: :asc).first
|
||||
mark_secure = first_post_with_upload ? first_post_with_upload.with_secure_media? : false
|
||||
end
|
||||
else
|
||||
mark_secure = false
|
||||
end
|
||||
else
|
||||
mark_secure = SiteSetting.prevent_anons_from_downloading_files?
|
||||
end
|
||||
mark_secure
|
||||
end
|
||||
|
||||
def self.migrate_to_new_scheme(limit: nil)
|
||||
problems = []
|
||||
|
||||
|
@ -385,6 +408,7 @@ end
|
|||
# thumbnail_width :integer
|
||||
# thumbnail_height :integer
|
||||
# etag :string
|
||||
# secure :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -140,6 +140,7 @@ en:
|
|||
unsubscribe_not_allowed: "Happens when unsubscribing via email is not allowed for this user."
|
||||
email_not_allowed: "Happens when the email address is not on the whitelist or is on the blacklist."
|
||||
unrecognized_error: "Unrecognized Error"
|
||||
secure_media_placeholder: "Redacted: this site has secure media enabled, visit the topic to see the attached image/audio/video."
|
||||
|
||||
errors: &errors
|
||||
format: ! "%{attribute} %{message}"
|
||||
|
@ -204,6 +205,7 @@ en:
|
|||
enable_s3_uploads_is_required: "You cannot enable inventory to S3 unless you've enabled the S3 uploads."
|
||||
s3_backup_requires_s3_settings: "You cannot use S3 as backup location unless you've provided the '%{setting_name}'."
|
||||
s3_bucket_reused: "You cannot use the same bucket for 's3_upload_bucket' and 's3_backup_bucket'. Choose a different bucket or use a different path for each bucket."
|
||||
secure_media_requirements: "S3 uploads must be enabled before enabling secure media."
|
||||
second_factor_cannot_be_enforced_with_disabled_local_login: "You cannot enforce 2FA if local logins are disabled."
|
||||
local_login_cannot_be_disabled_if_second_factor_enforced: "You cannot disable local login if 2FA is enforced. Disable enforced 2FA before disabling local logins."
|
||||
conflicting_google_user_id: 'The Google Account ID for this account has changed; staff intervention is required for security reasons. Please contact staff and point them to <br><a href="https://meta.discourse.org/t/76575">https://meta.discourse.org/t/76575</a>'
|
||||
|
@ -333,6 +335,7 @@ en:
|
|||
max_pm_recepients: "Sorry, you can send a message to maximum %{recipients_limit} recipients."
|
||||
pm_reached_recipients_limit: "Sorry, you can't have more than %{recipients_limit} recipients in a message."
|
||||
removed_direct_reply_full_quotes: "Automatically removed quote of whole previous post."
|
||||
secure_upload_not_allowed_in_public_topic: "Sorry, the following secure upload(s) cannot be used in a public topic: %{upload_filenames}."
|
||||
|
||||
just_posted_that: "is too similar to what you recently posted"
|
||||
invalid_characters: "contains invalid characters"
|
||||
|
@ -2017,7 +2020,7 @@ en:
|
|||
bootstrap_mode_min_users: "Minimum number of users required to disable bootstrap mode (set to 0 to disable)"
|
||||
|
||||
prevent_anons_from_downloading_files: "Prevent anonymous users from downloading attachments. WARNING: this will prevent any non-image site assets posted as attachments from working."
|
||||
|
||||
secure_media: 'Limits access to media uploads (images, video, audio). If "login required" is enabled, only logged-in users can access media uploads. Otherwise, access will be limited only for media uploads in private messages. Note: S3 uploads must be enabled prior to enabling this setting.'
|
||||
slug_generation_method: "Choose a slug generation method. 'encoded' will generate percent encoding string. 'none' will disable slug at all."
|
||||
|
||||
enable_emoji: "Enable emoji"
|
||||
|
|
|
@ -502,6 +502,7 @@ Discourse::Application.routes.draw do
|
|||
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)
|
||||
get "uploads/:site/:id/:sha" => "uploads#show", constraints: { site: /\w+/, id: /\d+/, sha: /\h{16}/, format: /.*/ }
|
||||
get "secure-media-uploads/*path(.:extension)" => "uploads#show_secure", constraints: { extension: /[a-z0-9\.]+/i }
|
||||
|
||||
get "posts" => "posts#latest", id: "latest_posts", constraints: { format: /(json|rss)/ }
|
||||
get "private-posts" => "posts#latest", id: "private_posts", constraints: { format: /(json|rss)/ }
|
||||
|
|
|
@ -1127,6 +1127,9 @@ files:
|
|||
prevent_anons_from_downloading_files:
|
||||
default: false
|
||||
client: true
|
||||
secure_media:
|
||||
default: false
|
||||
client: true
|
||||
enable_s3_uploads:
|
||||
default: false
|
||||
client: true
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSecureToUploads < ActiveRecord::Migration[5.2]
|
||||
def up
|
||||
add_column :uploads, :secure, :boolean, default: false, null: false
|
||||
|
||||
prevent_anons_from_downloading_files = \
|
||||
DB.query_single("SELECT value FROM site_settings WHERE name = 'prevent_anons_from_downloading_files'").first == 't'
|
||||
|
||||
if prevent_anons_from_downloading_files
|
||||
execute(
|
||||
<<-SQL
|
||||
UPDATE uploads SET secure = 't' WHERE id IN (
|
||||
SELECT DISTINCT(uploads.id) FROM uploads
|
||||
INNER JOIN post_uploads ON post_uploads.upload_id = uploads.id
|
||||
WHERE LOWER(original_filename) NOT SIMILAR TO '%\.(jpg|jpeg|png|gif|svg|ico)'
|
||||
)
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :uploads, :secure
|
||||
end
|
||||
end
|
|
@ -281,6 +281,10 @@ class CookedPostProcessor
|
|||
absolute_url = url
|
||||
absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ /^\/[^\/]/
|
||||
|
||||
if url&.start_with?("/secure-media-uploads/")
|
||||
absolute_url = Discourse.store.signed_url_for_path(url.sub("/secure-media-uploads/", ""))
|
||||
end
|
||||
|
||||
return unless absolute_url
|
||||
|
||||
# FastImage fails when there's no scheme
|
||||
|
@ -400,14 +404,14 @@ class CookedPostProcessor
|
|||
resized_h = (h * ratio).to_i
|
||||
|
||||
if !cropped && upload.width && resized_w > upload.width
|
||||
cooked_url = UrlHelper.cook_url(upload.url)
|
||||
cooked_url = UrlHelper.cook_url(upload.url, secure: upload.secure?)
|
||||
srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x"
|
||||
elsif t = upload.thumbnail(resized_w, resized_h)
|
||||
cooked_url = UrlHelper.cook_url(t.url)
|
||||
cooked_url = UrlHelper.cook_url(t.url, secure: upload.secure?)
|
||||
srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x"
|
||||
end
|
||||
|
||||
img["srcset"] = "#{UrlHelper.cook_url(img["src"])}#{srcset}" if srcset.present?
|
||||
img["srcset"] = "#{UrlHelper.cook_url(img["src"], secure: upload.secure?)}#{srcset}" if srcset.present?
|
||||
end
|
||||
else
|
||||
img["src"] = upload.url
|
||||
|
@ -595,7 +599,7 @@ class CookedPostProcessor
|
|||
|
||||
%w{src data-small-upload}.each do |selector|
|
||||
@doc.css("img[#{selector}]").each do |img|
|
||||
img[selector] = UrlHelper.cook_url(img[selector].to_s)
|
||||
img[selector] = UrlHelper.cook_url(img[selector].to_s, secure: @post.with_secure_media?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -198,6 +198,7 @@ module Email
|
|||
style('code', 'background-color: #f1f1ff; padding: 2px 5px;')
|
||||
style('pre code', 'display: block; background-color: #f1f1ff; padding: 5px;')
|
||||
style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;")
|
||||
style('.secure-image-notice', 'font-style: italic; background-color: #f1f1ff; padding: 5px;')
|
||||
style('.summary-email', "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%")
|
||||
|
||||
style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;')
|
||||
|
@ -237,6 +238,7 @@ module Email
|
|||
def to_html
|
||||
strip_classes_and_ids
|
||||
replace_relative_urls
|
||||
replace_secure_media_urls
|
||||
@fragment.to_html
|
||||
end
|
||||
|
||||
|
@ -284,6 +286,23 @@ module Email
|
|||
end
|
||||
end
|
||||
|
||||
def replace_secure_media_urls
|
||||
@fragment.css('[href]').each do |a|
|
||||
if a['href'][/secure-media-uploads/]
|
||||
a.add_next_sibling "<p class='secure-media-notice'>#{I18n.t("emails.secure_media_placeholder")}</p>"
|
||||
a.remove
|
||||
end
|
||||
end
|
||||
|
||||
@fragment.search('img').each do |img|
|
||||
next unless img['src']
|
||||
if img['src'][/secure-media-uploads/]
|
||||
img.add_next_sibling "<p class='secure-media-notice'>#{I18n.t("emails.secure_media_placeholder")}</p>"
|
||||
img.remove
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def correct_first_body_margin
|
||||
@fragment.css('div.body p').each do |element|
|
||||
element['style'] = "margin-top:0; border: 0;"
|
||||
|
|
|
@ -17,6 +17,10 @@ class FileHelper
|
|||
filename =~ supported_images_regexp
|
||||
end
|
||||
|
||||
def self.is_supported_media?(filename)
|
||||
filename =~ supported_media_regexp
|
||||
end
|
||||
|
||||
class FakeIO
|
||||
attr_accessor :status
|
||||
end
|
||||
|
@ -132,8 +136,20 @@ class FileHelper
|
|||
@@supported_images ||= Set.new %w{jpg jpeg png gif svg ico}
|
||||
end
|
||||
|
||||
def self.supported_audio
|
||||
@@supported_audio ||= Set.new %w{mp3 ogg wav m4a}
|
||||
end
|
||||
|
||||
def self.supported_video
|
||||
@@supported_video ||= Set.new %w{mov mp4 webm ogv}
|
||||
end
|
||||
|
||||
def self.supported_images_regexp
|
||||
@@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i
|
||||
end
|
||||
|
||||
def self.supported_media_regexp
|
||||
media = supported_images | supported_audio | supported_video
|
||||
@@supported_media_regexp ||= /\.(#{media.to_a.join("|")})$/i
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,6 +54,10 @@ module FileStore
|
|||
not_implemented
|
||||
end
|
||||
|
||||
def s3_upload_host
|
||||
not_implemented
|
||||
end
|
||||
|
||||
def external?
|
||||
not_implemented
|
||||
end
|
||||
|
@ -77,7 +81,11 @@ module FileStore
|
|||
|
||||
if !file
|
||||
max_file_size_kb = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes
|
||||
url = Discourse.store.cdn_url(upload.url)
|
||||
|
||||
url = upload.secure? ?
|
||||
Discourse.store.signed_url_for_path(upload.url) :
|
||||
Discourse.store.cdn_url(upload.url)
|
||||
|
||||
url = SiteSetting.scheme + ":" + url if url =~ /^\/\//
|
||||
file = FileHelper.download(
|
||||
url,
|
||||
|
|
|
@ -21,13 +21,13 @@ module FileStore
|
|||
|
||||
def store_upload(file, upload, content_type = nil)
|
||||
path = get_path_for_upload(upload)
|
||||
url, upload.etag = store_file(file, path, filename: upload.original_filename, content_type: content_type, cache_locally: true, private: upload.private?)
|
||||
url, upload.etag = store_file(file, path, filename: upload.original_filename, content_type: content_type, cache_locally: true, private_acl: upload.secure?)
|
||||
url
|
||||
end
|
||||
|
||||
def store_optimized_image(file, optimized_image, content_type = nil)
|
||||
def store_optimized_image(file, optimized_image, content_type = nil, secure: false)
|
||||
path = get_path_for_optimized_image(optimized_image)
|
||||
url, optimized_image.etag = store_file(file, path, content_type: content_type)
|
||||
url, optimized_image.etag = store_file(file, path, content_type: content_type, private_acl: secure)
|
||||
url
|
||||
end
|
||||
|
||||
|
@ -42,12 +42,12 @@ module FileStore
|
|||
# cache file locally when needed
|
||||
cache_file(file, File.basename(path)) if opts[:cache_locally]
|
||||
options = {
|
||||
acl: opts[:private] ? "private" : "public-read",
|
||||
acl: opts[:private_acl] ? "private" : "public-read",
|
||||
cache_control: 'max-age=31556952, public, immutable',
|
||||
content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type
|
||||
}
|
||||
# add a "content disposition" header for "attachments"
|
||||
options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_image?(filename)
|
||||
options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_media?(filename)
|
||||
|
||||
path.prepend(File.join(upload_path, "/")) if Rails.configuration.multisite
|
||||
|
||||
|
@ -88,6 +88,10 @@ module FileStore
|
|||
@absolute_base_url ||= SiteSetting.Upload.absolute_base_url
|
||||
end
|
||||
|
||||
def s3_upload_host
|
||||
SiteSetting.Upload.s3_cdn_url.present? ? SiteSetting.Upload.s3_cdn_url : "https:#{absolute_base_url}"
|
||||
end
|
||||
|
||||
def external?
|
||||
true
|
||||
end
|
||||
|
@ -111,22 +115,9 @@ module FileStore
|
|||
end
|
||||
|
||||
def url_for(upload, force_download: false)
|
||||
if upload.private? || force_download
|
||||
opts = { expires_in: S3Helper::DOWNLOAD_URL_EXPIRES_AFTER_SECONDS }
|
||||
|
||||
if force_download
|
||||
opts[:response_content_disposition] = ActionDispatch::Http::ContentDisposition.format(
|
||||
disposition: "attachment", filename: upload.original_filename
|
||||
)
|
||||
end
|
||||
|
||||
obj = @s3_helper.object(get_upload_key(upload))
|
||||
url = obj.presigned_url(:get, opts)
|
||||
else
|
||||
url = upload.url
|
||||
end
|
||||
|
||||
url
|
||||
upload.secure? || force_download ?
|
||||
presigned_url(get_upload_key(upload), force_download: force_download, filename: upload.original_filename) :
|
||||
upload.url
|
||||
end
|
||||
|
||||
def cdn_url(url)
|
||||
|
@ -136,6 +127,11 @@ module FileStore
|
|||
url.sub(File.join("#{schema}#{absolute_base_url}", folder), File.join(SiteSetting.Upload.s3_cdn_url, "/"))
|
||||
end
|
||||
|
||||
def signed_url_for_path(path)
|
||||
key = path.sub(absolute_base_url + "/", "")
|
||||
presigned_url(key)
|
||||
end
|
||||
|
||||
def cache_avatar(avatar, user_id)
|
||||
source = avatar.url.sub(absolute_base_url + "/", "")
|
||||
destination = avatar_template(avatar, user_id).sub(absolute_base_url + "/", "")
|
||||
|
@ -163,14 +159,15 @@ module FileStore
|
|||
end
|
||||
|
||||
def update_upload_ACL(upload)
|
||||
private_uploads = SiteSetting.prevent_anons_from_downloading_files
|
||||
key = get_upload_key(upload)
|
||||
update_ACL(key, upload.secure?)
|
||||
|
||||
begin
|
||||
@s3_helper.object(key).acl.put(acl: private_uploads ? "private" : "public-read")
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
Rails.logger.warn("Could not update ACL on upload with key: '#{key}'. Upload is missing.")
|
||||
upload.optimized_images.find_each do |optimized_image|
|
||||
optimized_image_key = get_path_for_optimized_image(optimized_image)
|
||||
update_ACL(optimized_image_key, upload.secure?)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def download_file(upload, destination_path)
|
||||
|
@ -179,6 +176,18 @@ module FileStore
|
|||
|
||||
private
|
||||
|
||||
def presigned_url(url, force_download: false, filename: false)
|
||||
opts = { expires_in: S3Helper::DOWNLOAD_URL_EXPIRES_AFTER_SECONDS }
|
||||
if force_download && filename
|
||||
opts[:response_content_disposition] = ActionDispatch::Http::ContentDisposition.format(
|
||||
disposition: "attachment", filename: filename
|
||||
)
|
||||
end
|
||||
|
||||
obj = @s3_helper.object(url)
|
||||
obj.presigned_url(:get, opts)
|
||||
end
|
||||
|
||||
def get_upload_key(upload)
|
||||
if Rails.configuration.multisite
|
||||
File.join(upload_path, "/", get_path_for_upload(upload))
|
||||
|
@ -187,6 +196,14 @@ module FileStore
|
|||
end
|
||||
end
|
||||
|
||||
def update_ACL(key, secure)
|
||||
begin
|
||||
@s3_helper.object(key).acl.put(acl: secure ? "private" : "public-read")
|
||||
rescue Aws::S3::Errors::NoSuchKey
|
||||
Rails.logger.warn("Could not update ACL on upload with key: '#{key}'. Upload is missing.")
|
||||
end
|
||||
end
|
||||
|
||||
def list_missing(model, prefix)
|
||||
connection = ActiveRecord::Base.connection.raw_connection
|
||||
connection.exec('CREATE TEMP TABLE verified_ids(val integer PRIMARY KEY)')
|
||||
|
|
|
@ -178,7 +178,15 @@ module Oneboxer
|
|||
def self.local_upload_html(url)
|
||||
case File.extname(URI(url).path || "")
|
||||
when VIDEO_REGEX
|
||||
"<video width='100%' height='100%' controls><source src='#{url}'><a href='#{url}'>#{url}</a></video>"
|
||||
<<~HTML
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src='#{url}'>
|
||||
<a href='#{url}'>#{url}</a>
|
||||
</source>
|
||||
</video>
|
||||
</div>
|
||||
HTML
|
||||
when AUDIO_REGEX
|
||||
"<audio controls><source src='#{url}'><a href='#{url}'>#{url}</a></audio>"
|
||||
end
|
||||
|
|
|
@ -177,6 +177,7 @@ class PostCreator
|
|||
update_user_counts
|
||||
create_embedded_topic
|
||||
link_post_uploads
|
||||
update_uploads_secure_status
|
||||
ensure_in_allowed_users if guardian.is_staff?
|
||||
unarchive_message
|
||||
@post.advance_draft_sequence unless @opts[:import_mode]
|
||||
|
@ -366,7 +367,15 @@ class PostCreator
|
|||
end
|
||||
|
||||
def link_post_uploads
|
||||
@post.link_post_uploads
|
||||
disallowed_uploads = @post.link_post_uploads
|
||||
if disallowed_uploads.is_a? Array
|
||||
@post.errors.add(:base, I18n.t('secure_upload_not_allowed_in_public_topic', upload_filenames: disallowed_uploads.join(", ")))
|
||||
rollback_from_errors!(@post)
|
||||
end
|
||||
end
|
||||
|
||||
def update_uploads_secure_status
|
||||
@post.update_uploads_secure_status
|
||||
end
|
||||
|
||||
def handle_spam
|
||||
|
|
|
@ -381,9 +381,19 @@ module PrettyText
|
|||
end
|
||||
end
|
||||
|
||||
def self.strip_secure_media(doc)
|
||||
doc.css("a[href]").each do |a|
|
||||
if a["href"].include?("/secure-media-uploads/") && FileHelper.is_supported_media?(a["href"])
|
||||
target = %w(video audio).include?(a&.parent&.parent&.name) ? a.parent.parent : a
|
||||
target.replace "<p class='secure-media-notice'>#{I18n.t("emails.secure_media_placeholder")}</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.format_for_email(html, post = nil)
|
||||
doc = Nokogiri::HTML.fragment(html)
|
||||
DiscourseEvent.trigger(:reduce_cooked, doc, post)
|
||||
strip_secure_media(doc) if post&.with_secure_media?
|
||||
strip_image_wrapping(doc)
|
||||
convert_vimeo_iframes(doc)
|
||||
make_all_links_absolute(doc)
|
||||
|
|
|
@ -64,13 +64,15 @@ module PrettyText
|
|||
reverse_map[value] << key
|
||||
end
|
||||
|
||||
Upload.where(sha1: map.values).pluck(:sha1, :url, :extension).each do |row|
|
||||
sha1, url, extension = row
|
||||
Upload.where(sha1: map.values).pluck(:sha1, :url, :extension, :original_filename, :secure).each do |row|
|
||||
sha1, url, extension, original_filename, secure = row
|
||||
|
||||
if short_urls = reverse_map[sha1]
|
||||
secure_media = FileHelper.is_supported_media?(original_filename) && SiteSetting.secure_media? && secure
|
||||
|
||||
short_urls.each do |short_url|
|
||||
result[short_url] = {
|
||||
url: Discourse.store.cdn_url(url),
|
||||
url: secure_media ? secure_media_url(url) : Discourse.store.cdn_url(url),
|
||||
short_path: Upload.short_path(sha1: sha1, extension: extension),
|
||||
base62_sha1: Upload.base62_sha1(sha1)
|
||||
}
|
||||
|
@ -82,6 +84,10 @@ module PrettyText
|
|||
result
|
||||
end
|
||||
|
||||
def secure_media_url(url)
|
||||
url.sub(SiteSetting.Upload.absolute_base_url, "/secure-media-uploads")
|
||||
end
|
||||
|
||||
def get_topic_info(topic_id)
|
||||
return unless topic_id.is_a?(Integer)
|
||||
# TODO this only handles public topics, secured one do not get this
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RakeHelpers
|
||||
def self.print_status_with_label(label, current, max)
|
||||
print "\r\033[K%s%9d / %d (%5.1f%%)" % [label, current, max, ((current.to_f / max.to_f) * 100).round(1)]
|
||||
end
|
||||
|
||||
def self.print_status(current, max)
|
||||
print "\r\033[K%9d / %d (%5.1f%%)" % [current, max, ((current.to_f / max.to_f) * 100).round(1)]
|
||||
end
|
||||
end
|
|
@ -121,6 +121,10 @@ module SiteSettings::Validations
|
|||
validate_error :s3_upload_bucket_is_required if new_val == "t" && SiteSetting.s3_upload_bucket.blank?
|
||||
end
|
||||
|
||||
def validate_secure_media(new_val)
|
||||
validate_error :secure_media_requirements if new_val == "t" && !SiteSetting.enable_s3_uploads?
|
||||
end
|
||||
|
||||
def validate_enable_s3_inventory(new_val)
|
||||
validate_error :enable_s3_uploads_is_required if new_val == "t" && !SiteSetting.Upload.enable_s3_uploads
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
def print_status_with_label(label, current, max)
|
||||
print "\r%s%9d / %d (%5.1f%%)" % [label, current, max, ((current.to_f / max.to_f) * 100).round(1)]
|
||||
end
|
||||
require_dependency "rake_helpers"
|
||||
|
||||
def close_old_topics(category)
|
||||
topics = Topic.where(closed: false, category_id: category.id)
|
||||
|
@ -23,7 +21,7 @@ def close_old_topics(category)
|
|||
|
||||
topics.find_each do |topic|
|
||||
topic.update_status("closed", true, Discourse.system_user)
|
||||
print_status_with_label(" closing old topics: ", topics_closed += 1, total)
|
||||
RakeHelpers.print_status_with_label(" closing old topics: ", topics_closed += 1, total)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,7 +47,7 @@ def apply_auto_close(category)
|
|||
|
||||
topics.find_each do |topic|
|
||||
topic.inherit_auto_close_from_category
|
||||
print_status_with_label(" applying auto-close to topics: ", topics_closed += 1, total)
|
||||
RakeHelpers.print_status_with_label(" applying auto-close to topics: ", topics_closed += 1, total)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +75,7 @@ task "topics:watch_all_replied_topics" => :environment do
|
|||
t.topic_users.where(posted: true).find_each do |tp|
|
||||
tp.update!(notification_level: TopicUser.notification_levels[:watching], notifications_reason_id: TopicUser.notification_reasons[:created_post])
|
||||
end
|
||||
print_status(count += 1, total)
|
||||
RakeHelpers.print_status(count += 1, total)
|
||||
end
|
||||
|
||||
puts "", "Done"
|
||||
|
@ -96,12 +94,8 @@ task "topics:update_fancy_titles" => :environment do
|
|||
|
||||
Topic.find_each do |topic|
|
||||
topic.fancy_title
|
||||
print_status(count += 1, total)
|
||||
RakeHelpers.print_status(count += 1, total)
|
||||
end
|
||||
|
||||
puts "", "Done"
|
||||
end
|
||||
|
||||
def print_status(current, max)
|
||||
print "\r%9d / %d (%5.1f%%)" % [current, max, ((current.to_f / max.to_f) * 100).round(1)]
|
||||
end
|
||||
|
|
|
@ -8,6 +8,8 @@ require "base62"
|
|||
# gather #
|
||||
################################################################################
|
||||
|
||||
require_dependency "rake_helpers"
|
||||
|
||||
task "uploads:gather" => :environment do
|
||||
ENV["RAILS_DB"] ? gather_uploads : gather_uploads_for_all_sites
|
||||
end
|
||||
|
@ -426,7 +428,7 @@ def migrate_to_s3
|
|||
%Q{attachment; filename="#{upload.original_filename}"}
|
||||
end
|
||||
|
||||
if upload&.private?
|
||||
if upload.secure
|
||||
options[:acl] = "private"
|
||||
end
|
||||
end
|
||||
|
@ -907,6 +909,108 @@ task "uploads:recover" => :environment do
|
|||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Run this task whenever the secure_media or login_required
|
||||
# settings are changed for a Discourse instance to update
|
||||
# the upload secure flag and S3 upload ACLs.
|
||||
task "uploads:ensure_correct_acl" => :environment do
|
||||
RailsMultisite::ConnectionManagement.each_connection do |db|
|
||||
unless Discourse.store.external?
|
||||
puts "This task only works for external storage."
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Ensuring correct ACL for uploads in #{db}...", ""
|
||||
|
||||
Upload.transaction do
|
||||
mark_secure_in_loop_because_no_login_required = false
|
||||
|
||||
# First of all only get relevant uploads (supported media).
|
||||
#
|
||||
# Also only get uploads that are not for a theme or a site setting, so only
|
||||
# get post related uploads.
|
||||
uploads_with_supported_media = Upload.includes(:posts, :optimized_images).where(
|
||||
"LOWER(original_filename) SIMILAR TO '%\.(jpg|jpeg|png|gif|svg|ico|mp3|ogg|wav|m4a|mov|mp4|webm|ogv)'"
|
||||
).joins(:post_uploads)
|
||||
|
||||
puts "There are #{uploads_with_supported_media.count} upload(s) with supported media that could be marked secure.", ""
|
||||
|
||||
# Simply mark all these uploads as secure if login_required because no anons will be able to access them
|
||||
if SiteSetting.login_required?
|
||||
mark_all_as_secure_login_required(uploads_with_supported_media)
|
||||
else
|
||||
|
||||
# If NOT login_required, then we have to go for the other slower flow, where in the loop
|
||||
# we mark the upload as secure if the first post it is used in is with_secure_media?
|
||||
mark_secure_in_loop_because_no_login_required = true
|
||||
puts "Marking posts as secure in the next step because login_required is false."
|
||||
end
|
||||
|
||||
puts "", "Rebaking #{uploads_with_supported_media.count} upload posts and updating ACLs in S3.", ""
|
||||
|
||||
upload_ids_to_mark_as_secure, uploads_skipped_because_of_error = update_acls_and_rebake_upload_posts(
|
||||
uploads_with_supported_media, mark_secure_in_loop_because_no_login_required
|
||||
)
|
||||
|
||||
log_rebake_errors(uploads_skipped_because_of_error)
|
||||
mark_specific_uploads_as_secure_no_login_required(upload_ids_to_mark_as_secure)
|
||||
end
|
||||
end
|
||||
puts "", "Done"
|
||||
end
|
||||
|
||||
def mark_all_as_secure_login_required(uploads_with_supported_media)
|
||||
puts "Marking #{uploads_with_supported_media.count} upload(s) as secure because login_required is true.", ""
|
||||
uploads_with_supported_media.update_all(secure: true)
|
||||
puts "Finished marking upload(s) as secure."
|
||||
end
|
||||
|
||||
def log_rebake_errors(uploads_skipped_because_of_error)
|
||||
return if uploads_skipped_because_of_error.empty?
|
||||
puts "Skipped the following uploads due to error:", ""
|
||||
uploads_skipped_because_of_error.each do |message|
|
||||
puts message
|
||||
end
|
||||
end
|
||||
|
||||
def mark_specific_uploads_as_secure_no_login_required(upload_ids_to_mark_as_secure)
|
||||
return if upload_ids_to_mark_as_secure.empty?
|
||||
puts "Marking #{upload_ids_to_mark_as_secure.length} uploads as secure because their first post contains secure media."
|
||||
Upload.where(id: upload_ids_to_mark_as_secure).update_all(secure: true)
|
||||
puts "Finished marking uploads as secure."
|
||||
end
|
||||
|
||||
def update_acls_and_rebake_upload_posts(uploads_with_supported_media, mark_secure_in_loop_because_no_login_required)
|
||||
upload_ids_to_mark_as_secure = []
|
||||
uploads_skipped_because_of_error = []
|
||||
|
||||
i = 0
|
||||
uploads_with_supported_media.find_each(batch_size: 50) do |upload_with_supported_media|
|
||||
RakeHelpers.print_status_with_label("Updating ACL for upload.......", i, uploads_with_supported_media.count)
|
||||
|
||||
Discourse.store.update_upload_ACL(upload_with_supported_media)
|
||||
|
||||
RakeHelpers.print_status_with_label("Rebaking posts for upload.....", i, uploads_with_supported_media.count)
|
||||
begin
|
||||
upload_with_supported_media.posts.each { |post| post.rebake! }
|
||||
|
||||
if mark_secure_in_loop_because_no_login_required
|
||||
first_post_with_upload = upload_with_supported_media.posts.order(sort_order: :asc).first
|
||||
mark_secure = first_post_with_upload ? first_post_with_upload.with_secure_media? : false
|
||||
upload_ids_to_mark_as_secure << upload_with_supported_media.id if mark_secure
|
||||
end
|
||||
rescue => e
|
||||
uploads_skipped_because_of_error << "#{upload_with_supported_media.original_filename} (#{upload_with_supported_media.url}) #{e.message}"
|
||||
end
|
||||
|
||||
i += 1
|
||||
end
|
||||
RakeHelpers.print_status_with_label("Rebaking complete! ", i, uploads_with_supported_media.count)
|
||||
puts ""
|
||||
|
||||
[upload_ids_to_mark_as_secure, uploads_skipped_because_of_error]
|
||||
end
|
||||
|
||||
def inline_uploads(post)
|
||||
replaced = false
|
||||
|
||||
|
|
|
@ -118,6 +118,13 @@ class UploadCreator
|
|||
@upload.for_site_setting = true if @opts[:for_site_setting]
|
||||
@upload.for_gravatar = true if @opts[:for_gravatar]
|
||||
|
||||
if !FileHelper.is_supported_media?(@filename) &&
|
||||
!@upload.for_theme &&
|
||||
!@upload.for_site_setting &&
|
||||
SiteSetting.prevent_anons_from_downloading_files
|
||||
@upload.secure = true
|
||||
end
|
||||
|
||||
return @upload unless @upload.save
|
||||
|
||||
# store the file and update its url
|
||||
|
|
|
@ -38,6 +38,11 @@ class UrlHelper
|
|||
url.sub(/^http:/i, "")
|
||||
end
|
||||
|
||||
def self.secure_proxy_without_cdn(url)
|
||||
url = url.sub(SiteSetting.Upload.absolute_base_url, "/secure-media-uploads")
|
||||
self.absolute(url, nil)
|
||||
end
|
||||
|
||||
DOUBLE_ESCAPED_REGEXP ||= /%25([0-9a-f]{2})/i
|
||||
|
||||
# Prevents double URL encode
|
||||
|
@ -48,16 +53,16 @@ class UrlHelper
|
|||
encoded
|
||||
end
|
||||
|
||||
def self.cook_url(url)
|
||||
def self.cook_url(url, secure: false)
|
||||
return url unless is_local(url)
|
||||
|
||||
uri = URI.parse(url)
|
||||
filename = File.basename(uri.path)
|
||||
is_attachment = !FileHelper.is_supported_image?(filename)
|
||||
is_attachment = !FileHelper.is_supported_media?(filename)
|
||||
|
||||
no_cdn = SiteSetting.login_required || SiteSetting.prevent_anons_from_downloading_files
|
||||
|
||||
url = absolute_without_cdn(url)
|
||||
url = secure ? secure_proxy_without_cdn(url) : absolute_without_cdn(url)
|
||||
|
||||
unless is_attachment && no_cdn
|
||||
url = Discourse.store.cdn_url(url)
|
||||
|
|
|
@ -812,8 +812,8 @@ describe CookedPostProcessor do
|
|||
|
||||
it "is always allowed to crawl our own images" do
|
||||
store = stub
|
||||
Discourse.expects(:store).returns(store).at_least_once
|
||||
store.expects(:has_been_uploaded?).returns(true)
|
||||
Discourse.expects(:store).returns(store)
|
||||
FastImage.expects(:size).returns([100, 200])
|
||||
expect(cpp.get_size("http://foo.bar/image2.png")).to eq([100, 200])
|
||||
end
|
||||
|
@ -1104,40 +1104,175 @@ describe CookedPostProcessor do
|
|||
HTML
|
||||
end
|
||||
|
||||
it "uses the right CDN when uploads are on S3" do
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("https://local.cdn.com")
|
||||
context "s3_uploads" do
|
||||
before do
|
||||
Rails.configuration.action_controller.stubs(:asset_host).returns("https://local.cdn.com")
|
||||
|
||||
SiteSetting.s3_upload_bucket = "some-bucket-on-s3"
|
||||
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.s3_cdn_url = "https://s3.cdn.com"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.s3_upload_bucket = "some-bucket-on-s3"
|
||||
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.s3_cdn_url = "https://s3.cdn.com"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.authorized_extensions = "png|jpg|gif|mov|ogg|"
|
||||
uploaded_file = file_from_fixtures("smallest.png")
|
||||
upload_sha1 = Digest::SHA1.hexdigest(File.read(uploaded_file))
|
||||
|
||||
uploaded_file = file_from_fixtures("smallest.png")
|
||||
upload_sha1 = Digest::SHA1.hexdigest(File.read(uploaded_file))
|
||||
upload.update!(
|
||||
original_filename: "smallest.png",
|
||||
width: 10,
|
||||
height: 20,
|
||||
sha1: upload_sha1,
|
||||
extension: "png",
|
||||
)
|
||||
end
|
||||
|
||||
upload.update!(
|
||||
original_filename: "smallest.png",
|
||||
width: 10,
|
||||
height: 20,
|
||||
sha1: upload_sha1,
|
||||
extension: "png",
|
||||
)
|
||||
it "uses the right CDN when uploads are on S3" do
|
||||
stored_path = Discourse.store.get_path_for_upload(upload)
|
||||
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
|
||||
|
||||
stored_path = Discourse.store.get_path_for_upload(upload)
|
||||
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
|
||||
the_post = Fabricate(:post, raw: %Q{This post has a local emoji :+1: and an external upload\n\n![smallest.png|10x20](#{upload.short_url})})
|
||||
|
||||
the_post = Fabricate(:post, raw: %Q{This post has a local emoji :+1: and an external upload\n\n![smallest.png|10x20](#{upload.short_url})})
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.optimize_urls
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.optimize_urls
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
|
||||
<p><img src="https://s3.cdn.com/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
|
||||
<p><img src="https://s3.cdn.com/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
|
||||
HTML
|
||||
it "doesn't use CDN for secure media" do
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
stored_path = Discourse.store.get_path_for_upload(upload)
|
||||
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
|
||||
upload.update_column(:secure, true)
|
||||
|
||||
the_post = Fabricate(:post, raw: %Q{This post has a local emoji :+1: and an external upload\n\n![smallest.png|10x20](#{upload.short_url})})
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.optimize_urls
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
|
||||
<p><img src="/secure-media-uploads/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
|
||||
HTML
|
||||
end
|
||||
|
||||
context "media uploads" do
|
||||
fab!(:image_upload) { Fabricate(:upload) }
|
||||
fab!(:audio_upload) { Fabricate(:upload, extension: "ogg") }
|
||||
fab!(:video_upload) { Fabricate(:upload, extension: "mov") }
|
||||
|
||||
before do
|
||||
video_upload.update!(url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(video_upload)}")
|
||||
stub_request(:head, video_upload.url)
|
||||
end
|
||||
|
||||
it "ignores prevent_anons_from_downloading_files and oneboxes video uploads" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
|
||||
the_post = Fabricate(:post, raw: "This post has an S3 video onebox:\n#{video_upload.url}")
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.post_process_oneboxes
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has an S3 video onebox:<br></p>
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{video_upload.url}">
|
||||
<a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a>
|
||||
</source>
|
||||
</video>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
it "oneboxes video using secure url when secure_media is enabled" do
|
||||
SiteSetting.login_required = true
|
||||
SiteSetting.secure_media = true
|
||||
video_upload.update_column(:secure, true)
|
||||
|
||||
the_post = Fabricate(:post, raw: "This post has an S3 video onebox:\n#{video_upload.url}")
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.post_process_oneboxes
|
||||
|
||||
secure_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has an S3 video onebox:<br>
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{secure_url}">
|
||||
<a href="#{secure_url}">#{secure_url}</a>
|
||||
</source>
|
||||
</video>
|
||||
</div>
|
||||
</p>
|
||||
HTML
|
||||
end
|
||||
|
||||
it "oneboxes only audio/video and not images when secure_media is enabled" do
|
||||
SiteSetting.login_required = true
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
video_upload.update_column(:secure, true)
|
||||
|
||||
audio_upload.update!(
|
||||
url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(audio_upload)}",
|
||||
secure: true
|
||||
)
|
||||
|
||||
image_upload.update!(
|
||||
url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(image_upload)}",
|
||||
secure: true
|
||||
)
|
||||
|
||||
stub_request(:head, audio_upload.url)
|
||||
stub_request(:get, image_upload.url)
|
||||
|
||||
raw = <<~RAW
|
||||
This post has a video upload.
|
||||
#{video_upload.url}
|
||||
|
||||
This post has an audio upload.
|
||||
#{audio_upload.url}
|
||||
|
||||
And an image upload.
|
||||
![logo.png](upload://#{image_upload.base62_sha1}.#{image_upload.extension})
|
||||
RAW
|
||||
|
||||
the_post = Fabricate(:post, raw: raw)
|
||||
|
||||
cpp = CookedPostProcessor.new(the_post)
|
||||
cpp.post_process_oneboxes
|
||||
|
||||
secure_video_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
secure_audio_url = audio_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
|
||||
|
||||
expect(cpp.html).to match_html <<~HTML
|
||||
<p>This post has a video upload.<br></p>
|
||||
<div class="onebox video-onebox">
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{secure_video_url}">
|
||||
<a href="#{secure_video_url}">#{secure_video_url}</a>
|
||||
</source>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<p>This post has an audio upload.<br>
|
||||
<audio controls><source src="#{secure_audio_url}"><a href="#{secure_audio_url}">#{secure_audio_url}</a></source></audio>
|
||||
</p>
|
||||
<p>And an image upload.<br>
|
||||
<img src="#{image_upload.url}" alt="#{image_upload.original_filename}" data-base62-sha1="#{image_upload.base62_sha1}"></p>
|
||||
|
||||
HTML
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -199,4 +199,19 @@ describe Email::Styles do
|
|||
end
|
||||
end
|
||||
|
||||
context "replace_relative_urls" do
|
||||
it "replaces secure media within a link with a placeholder" do
|
||||
frag = html_fragment("<a href=\"#{Discourse.base_url}\/secure-media-uploads/original/1X/testimage.png\"><img src=\"/secure-media-uploads/original/1X/testimage.png\"></a>")
|
||||
expect(frag.at('p.secure-media-notice')).to be_present
|
||||
expect(frag.at('img')).not_to be_present
|
||||
expect(frag.at('a')).not_to be_present
|
||||
end
|
||||
|
||||
it "replaces secure images with a placeholder" do
|
||||
frag = html_fragment("<img src=\"/secure-media-uploads/original/1X/testimage.png\">")
|
||||
expect(frag.at('p.secure-media-notice')).to be_present
|
||||
expect(frag.at('img')).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -86,5 +86,18 @@ RSpec.describe FileStore::BaseStore do
|
|||
|
||||
expect(file.class).to eq(File)
|
||||
end
|
||||
|
||||
it "should return the file when secure media are enabled" do
|
||||
SiteSetting.login_required = true
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
stub_request(:head, "https://s3-upload-bucket.s3.amazonaws.com/")
|
||||
signed_url = Discourse.store.signed_url_for_path(upload_s3.url)
|
||||
stub_request(:get, signed_url).to_return(status: 200, body: "Hello world")
|
||||
|
||||
file = store.download(upload_s3)
|
||||
|
||||
expect(file.class).to eq(File)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,16 +43,16 @@ describe FileStore::S3Store do
|
|||
let(:s3_object) { stub }
|
||||
let(:etag) { "etag" }
|
||||
|
||||
before do
|
||||
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
end
|
||||
|
||||
describe "#store_upload" do
|
||||
it "returns an absolute schemaless url" do
|
||||
store.expects(:get_depth_for).with(upload.id).returns(0)
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||
s3_object.expects(:put).with(
|
||||
acl: "public-read",
|
||||
cache_control: "max-age=31556952, public, immutable",
|
||||
content_type: "image/png",
|
||||
body: uploaded_file).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
|
||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png"
|
||||
|
@ -62,6 +62,7 @@ describe FileStore::S3Store do
|
|||
|
||||
describe "when s3_upload_bucket includes folders path" do
|
||||
before do
|
||||
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads"
|
||||
end
|
||||
|
||||
|
@ -78,28 +79,36 @@ describe FileStore::S3Store do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when private uploads are enabled" do
|
||||
it "returns signed URL for eligible private upload" do
|
||||
describe "when secure uploads are enabled" do
|
||||
it "saves secure attachment using private ACL" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
SiteSetting.authorized_extensions = "pdf|png|jpg|gif"
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf")
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.pdf").returns(s3_object).at_least_once
|
||||
s3_object.expects(:presigned_url).with(:get, expires_in: S3Helper::DOWNLOAD_URL_EXPIRES_AFTER_SECONDS)
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket)
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.pdf").returns(s3_object)
|
||||
s3_object.expects(:put).with(
|
||||
acl: "private",
|
||||
cache_control: "max-age=31556952, public, immutable",
|
||||
content_type: "application/pdf",
|
||||
content_disposition: "attachment; filename=\"#{upload.original_filename}\"",
|
||||
body: uploaded_file).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
|
||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.pdf"
|
||||
)
|
||||
|
||||
expect(store.url_for(upload)).not_to eq(upload.url)
|
||||
end
|
||||
|
||||
it "returns regular URL for ineligible private upload" do
|
||||
it "saves image upload using public ACL" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object).at_least_once
|
||||
s3_object.expects(:put).with(
|
||||
acl: "public-read",
|
||||
cache_control: "max-age=31556952, public, immutable",
|
||||
content_type: "image/png",
|
||||
body: uploaded_file).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
|
||||
expect(store.store_upload(uploaded_file, upload)).to eq(
|
||||
"//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png"
|
||||
|
@ -111,6 +120,10 @@ describe FileStore::S3Store do
|
|||
end
|
||||
|
||||
describe "#store_optimized_image" do
|
||||
before do
|
||||
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
|
||||
end
|
||||
|
||||
it "returns an absolute schemaless url" do
|
||||
store.expects(:get_depth_for).with(optimized_image.upload.id).returns(0)
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket)
|
||||
|
@ -355,23 +368,27 @@ describe FileStore::S3Store do
|
|||
include_context "s3 helpers"
|
||||
let(:s3_object) { stub }
|
||||
|
||||
before do
|
||||
SiteSetting.authorized_extensions = "pdf|png"
|
||||
end
|
||||
|
||||
describe ".update_upload_ACL" do
|
||||
it "sets acl to private when private uploads are enabled" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
it "sets acl to public by default" do
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf")
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket)
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.pdf").returns(s3_object)
|
||||
s3_object.expects(:acl).returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "private").returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "public-read").returns(s3_object)
|
||||
|
||||
expect(store.update_upload_ACL(upload)).to be_truthy
|
||||
end
|
||||
|
||||
it "sets acl to public when private uploads are disabled" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = false
|
||||
it "sets acl to private when upload is marked secure" do
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket)
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||
s3_bucket.expects(:object).with("original/1X/#{upload.sha1}.pdf").returns(s3_object)
|
||||
s3_object.expects(:acl).returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "public-read").returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "private").returns(s3_object)
|
||||
|
||||
expect(store.update_upload_ACL(upload)).to be_truthy
|
||||
end
|
||||
|
@ -421,4 +438,21 @@ describe FileStore::S3Store do
|
|||
end
|
||||
end
|
||||
|
||||
describe ".signed_url_for_path" do
|
||||
include_context "s3 helpers"
|
||||
let(:s3_object) { stub }
|
||||
|
||||
it "returns signed URL for a given path" do
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("special/optimized/file.png").returns(s3_object)
|
||||
opts = {
|
||||
expires_in: S3Helper::DOWNLOAD_URL_EXPIRES_AFTER_SECONDS
|
||||
}
|
||||
|
||||
s3_object.expects(:presigned_url).with(:get, opts)
|
||||
|
||||
expect(store.signed_url_for_path("special/optimized/file.png")).not_to eq(upload.url)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1388,4 +1388,63 @@ describe PostCreator do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "secure media uploads" do
|
||||
fab!(:image_upload) { Fabricate(:upload, secure: true) }
|
||||
fab!(:user2) { Fabricate(:user) }
|
||||
fab!(:public_topic) { Fabricate(:topic) }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.authorized_extensions = "png|jpg|gif|mp4"
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "some key"
|
||||
SiteSetting.s3_secret_access_key = "some secret key"
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
|
||||
|
||||
stub_request(
|
||||
:put,
|
||||
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{image_upload.sha1}.#{image_upload.extension}?acl"
|
||||
)
|
||||
end
|
||||
|
||||
it "does not allow a secure image to be used in a public topic" do
|
||||
public_post = PostCreator.create(
|
||||
user,
|
||||
topic_id: public_topic.id,
|
||||
raw: "A public post with an image.\n![](#{image_upload.short_path})"
|
||||
)
|
||||
|
||||
expect(public_post.errors.count).to be(1)
|
||||
expect(public_post.errors.full_messages).to include(I18n.t('secure_upload_not_allowed_in_public_topic', upload_filenames: image_upload.original_filename))
|
||||
|
||||
# secure upload CAN be used in another PM
|
||||
pm = PostCreator.create(
|
||||
user,
|
||||
title: 'this is another private message',
|
||||
raw: "with an upload: \n![](#{image_upload.short_path})",
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: [user2.username].join(',')
|
||||
)
|
||||
|
||||
expect(pm.errors).to be_blank
|
||||
end
|
||||
|
||||
it "does not allow a secure video to be used in a public topic" do
|
||||
video_upload = Fabricate(:upload_s3, extension: 'mp4', original_filename: "video.mp4", secure: true)
|
||||
|
||||
public_post = PostCreator.create(
|
||||
user,
|
||||
topic_id: public_topic.id,
|
||||
raw: "A public post with a video onebox:\n#{video_upload.url}"
|
||||
)
|
||||
|
||||
expect(public_post.errors.count).to be(1)
|
||||
expect(public_post.errors.full_messages).to include(I18n.t('secure_upload_not_allowed_in_public_topic', upload_filenames: video_upload.original_filename))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -810,6 +810,50 @@ describe PrettyText do
|
|||
html = "<p>Check out this video – <iframe src='https://player.vimeo.com/video/329875646' data-original-href='https://vimeo.com/329875646/> <script>alert(1)</script>'></iframe>.</p>"
|
||||
expect(PrettyText.format_for_email(html, post)).to match(Regexp.escape("https://vimeo.com/329875646/%3E%20%3Cscript%3Ealert(1)%3C/script%3E"))
|
||||
end
|
||||
|
||||
describe "#strip_secure_media" do
|
||||
before do
|
||||
SiteSetting.s3_upload_bucket = "some-bucket-on-s3"
|
||||
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.s3_cdn_url = "https://s3.cdn.com"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.secure_media = true
|
||||
SiteSetting.login_required = true
|
||||
end
|
||||
|
||||
it "replaces secure video content" do
|
||||
html = <<~HTML
|
||||
<video width="100%" height="100%" controls="">
|
||||
<source src="#{base_url}/secure-media-uploads/original/1X/some-video.mp4">
|
||||
<a href="#{base_url}/secure-media-uploads/original/1X/some-video.mp4">Video label</a>
|
||||
</source>
|
||||
</video>
|
||||
HTML
|
||||
|
||||
md = PrettyText.format_for_email(html, post)
|
||||
|
||||
expect(md).not_to include('<video')
|
||||
expect(md.to_s).to match(I18n.t("emails.secure_media_placeholder"))
|
||||
expect(md.to_s).not_to match(SiteSetting.Upload.s3_cdn_url)
|
||||
end
|
||||
|
||||
it "replaces secure audio content" do
|
||||
html = <<~HTML
|
||||
<audio controls>
|
||||
<source src="#{base_url}/secure-media-uploads/original/1X/some-audio.mp3">
|
||||
<a href="#{base_url}/secure-media-uploads/original/1X/some-audio.mp3">Audio label</a>
|
||||
</source>
|
||||
</audio>
|
||||
HTML
|
||||
|
||||
md = PrettyText.format_for_email(html, post)
|
||||
|
||||
expect(md).not_to include('<video')
|
||||
expect(md.to_s).to match(I18n.t("emails.secure_media_placeholder"))
|
||||
expect(md.to_s).not_to match(SiteSetting.Upload.s3_cdn_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'Is smart about linebreaks and IMG tags' do
|
||||
|
|
|
@ -170,7 +170,7 @@ RSpec.describe UploadCreator do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'private uploads' do
|
||||
describe 'secure attachments' do
|
||||
let(:filename) { "small.pdf" }
|
||||
let(:file) { file_from_fixtures(filename, "pdf") }
|
||||
|
||||
|
@ -179,31 +179,31 @@ RSpec.describe UploadCreator do
|
|||
SiteSetting.authorized_extensions = 'pdf|svg|jpg'
|
||||
end
|
||||
|
||||
it 'should mark uploads as private' do
|
||||
it 'should mark attachments as secure' do
|
||||
upload = UploadCreator.new(file, filename).create_for(user.id)
|
||||
stored_upload = Upload.last
|
||||
|
||||
expect(stored_upload.private?).to eq(true)
|
||||
expect(stored_upload.secure?).to eq(true)
|
||||
end
|
||||
|
||||
it 'should not mark theme uploads as private' do
|
||||
it 'should not mark theme uploads as secure' do
|
||||
fname = "custom-theme-icon-sprite.svg"
|
||||
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
||||
|
||||
expect(upload.private?).to eq(false)
|
||||
expect(upload.secure?).to eq(false)
|
||||
end
|
||||
|
||||
it 'should not mark image uploads as private' do
|
||||
it 'should not apply prevent_anons_from_downloading_files to image uploads' do
|
||||
fname = "logo.jpg"
|
||||
upload = UploadCreator.new(file_from_fixtures(fname), fname).create_for(user.id)
|
||||
stored_upload = Upload.last
|
||||
|
||||
expect(stored_upload.original_filename).to eq(fname)
|
||||
expect(stored_upload.private?).to eq(false)
|
||||
expect(stored_upload.secure?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'uploading to s3' do
|
||||
context 'uploading to s3' do
|
||||
let(:filename) { "should_be_jpeg.png" }
|
||||
let(:file) { file_from_fixtures(filename) }
|
||||
let(:pdf_filename) { "small.pdf" }
|
||||
|
@ -233,7 +233,7 @@ RSpec.describe UploadCreator do
|
|||
expect(upload.etag).to eq('ETag')
|
||||
end
|
||||
|
||||
it 'should return signed URL for private uploads in S3' do
|
||||
it 'should return signed URL for secure attachments in S3' do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
SiteSetting.authorized_extensions = 'pdf'
|
||||
|
||||
|
@ -241,7 +241,7 @@ RSpec.describe UploadCreator do
|
|||
stored_upload = Upload.last
|
||||
signed_url = Discourse.store.url_for(stored_upload)
|
||||
|
||||
expect(stored_upload.private?).to eq(true)
|
||||
expect(stored_upload.secure?).to eq(true)
|
||||
expect(stored_upload.url).not_to eq(signed_url)
|
||||
expect(signed_url).to match(/Amz-Credential/)
|
||||
end
|
||||
|
|
|
@ -674,6 +674,50 @@ describe UserNotifications do
|
|||
expect(mail.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
|
||||
end
|
||||
|
||||
describe "secure media" do
|
||||
let(:video_upload) { Fabricate(:upload, extension: "mov") }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:post) { Fabricate(:post) }
|
||||
|
||||
before do
|
||||
SiteSetting.s3_upload_bucket = "some-bucket-on-s3"
|
||||
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.s3_cdn_url = "https://s3.cdn.com"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.secure_media = true
|
||||
SiteSetting.login_required = true
|
||||
|
||||
video_upload.update!(url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(video_upload)}")
|
||||
user.email_logs.create!(
|
||||
email_type: 'blah',
|
||||
to_address: user.email,
|
||||
user_id: user.id
|
||||
)
|
||||
end
|
||||
|
||||
it "replaces secure audio/video with placeholder" do
|
||||
reply = Fabricate(:post, topic_id: post.topic_id, raw: "Video: #{video_upload.url}")
|
||||
|
||||
notification = Fabricate(
|
||||
:notification,
|
||||
topic_id: post.topic_id,
|
||||
post_number: reply.post_number,
|
||||
user: post.user,
|
||||
data: { original_username: 'bob' }.to_json
|
||||
)
|
||||
|
||||
mail = UserNotifications.user_replied(
|
||||
user,
|
||||
post: reply,
|
||||
notification_type: notification.notification_type,
|
||||
notification_data_hash: notification.data_hash
|
||||
)
|
||||
|
||||
expect(mail.body.to_s).to match(I18n.t("emails.secure_media_placeholder"))
|
||||
end
|
||||
end
|
||||
|
||||
def expects_build_with(condition)
|
||||
UserNotifications.any_instance.expects(:build_email).with(user.email, condition)
|
||||
mailer = UserNotifications.public_send(
|
||||
|
|
|
@ -1245,7 +1245,7 @@ describe Post do
|
|||
expect(post.revisions.pluck(:number)).to eq([1, 2])
|
||||
end
|
||||
|
||||
describe '#link_post_uploads' do
|
||||
describe 'uploads' do
|
||||
fab!(:video_upload) { Fabricate(:upload, extension: "mp4") }
|
||||
fab!(:image_upload) { Fabricate(:upload) }
|
||||
fab!(:audio_upload) { Fabricate(:upload, extension: "ogg") }
|
||||
|
@ -1257,7 +1257,7 @@ describe Post do
|
|||
let(:video_url) { "#{base_url}#{video_upload.url}" }
|
||||
let(:audio_url) { "#{base_url}#{audio_upload.url}" }
|
||||
|
||||
let(:raw) do
|
||||
let(:raw_multiple) do
|
||||
<<~RAW
|
||||
<a href="#{attachment_upload.url}">Link</a>
|
||||
[test|attachment](#{attachment_upload_2.short_url})
|
||||
|
@ -1276,36 +1276,121 @@ describe Post do
|
|||
RAW
|
||||
end
|
||||
|
||||
let(:post) { Fabricate(:post, raw: raw) }
|
||||
let(:post) { Fabricate(:post, raw: raw_multiple) }
|
||||
|
||||
it "finds all the uploads in the post" do
|
||||
post.custom_fields[Post::DOWNLOADED_IMAGES] = {
|
||||
"/uploads/default/original/1X/1/1234567890123456.csv": attachment_upload.id
|
||||
}
|
||||
context "#link_post_uploads" do
|
||||
it "finds all the uploads in the post" do
|
||||
post.custom_fields[Post::DOWNLOADED_IMAGES] = {
|
||||
"/uploads/default/original/1X/1/1234567890123456.csv": attachment_upload.id
|
||||
}
|
||||
|
||||
post.save_custom_fields
|
||||
post.link_post_uploads
|
||||
post.save_custom_fields
|
||||
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,
|
||||
attachment_upload_2.id,
|
||||
attachment_upload_3.id
|
||||
)
|
||||
expect(PostUpload.where(post: post).pluck(:upload_id)).to contain_exactly(
|
||||
video_upload.id,
|
||||
image_upload.id,
|
||||
audio_upload.id,
|
||||
attachment_upload.id,
|
||||
attachment_upload_2.id,
|
||||
attachment_upload_3.id
|
||||
)
|
||||
end
|
||||
|
||||
it "cleans the reverse index up for the current post" do
|
||||
post.link_post_uploads
|
||||
|
||||
post_uploads_ids = post.post_uploads.pluck(:id)
|
||||
|
||||
post.link_post_uploads
|
||||
|
||||
expect(post.reload.post_uploads.pluck(:id)).to_not contain_exactly(
|
||||
post_uploads_ids
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "cleans the reverse index up for the current post" do
|
||||
post.link_post_uploads
|
||||
context '#update_uploads_secure_status' do
|
||||
fab!(:user) { Fabricate(:user, trust_level: 0) }
|
||||
|
||||
post_uploads_ids = post.post_uploads.pluck(:id)
|
||||
let(:raw) do
|
||||
<<~RAW
|
||||
<a href="#{attachment_upload.url}">Link</a>
|
||||
<img src="#{image_upload.url}">
|
||||
RAW
|
||||
end
|
||||
|
||||
post.link_post_uploads
|
||||
before do
|
||||
SiteSetting.authorized_extensions = "pdf|png|jpg|csv"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "some key"
|
||||
SiteSetting.s3_secret_access_key = "some secret key"
|
||||
SiteSetting.secure_media = true
|
||||
attachment_upload.update!(original_filename: "hello.csv")
|
||||
|
||||
expect(post.reload.post_uploads.pluck(:id)).to_not contain_exactly(
|
||||
post_uploads_ids
|
||||
)
|
||||
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
|
||||
|
||||
stub_request(
|
||||
:put,
|
||||
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{attachment_upload.sha1}.#{attachment_upload.extension}?acl"
|
||||
)
|
||||
|
||||
stub_request(
|
||||
:put,
|
||||
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{image_upload.sha1}.#{image_upload.extension}?acl"
|
||||
)
|
||||
end
|
||||
|
||||
it "marks image uploads as secure in PMs when secure_media is ON" do
|
||||
post = Fabricate(:post, raw: raw, user: user, topic: Fabricate(:private_message_topic, user: user))
|
||||
post.link_post_uploads
|
||||
post.update_uploads_secure_status
|
||||
|
||||
expect(PostUpload.where(post: post).joins(:upload).pluck(:upload_id, :secure)).to contain_exactly(
|
||||
[attachment_upload.id, false],
|
||||
[image_upload.id, true]
|
||||
)
|
||||
end
|
||||
|
||||
it "marks image uploads as not secure in PMs when when secure_media is ON" do
|
||||
SiteSetting.secure_media = false
|
||||
post = Fabricate(:post, raw: raw, user: user, topic: Fabricate(:private_message_topic, user: user))
|
||||
post.link_post_uploads
|
||||
post.update_uploads_secure_status
|
||||
|
||||
expect(PostUpload.where(post: post).joins(:upload).pluck(:upload_id, :secure)).to contain_exactly(
|
||||
[attachment_upload.id, false],
|
||||
[image_upload.id, false]
|
||||
)
|
||||
end
|
||||
|
||||
it "marks attachments as secure when relevant setting is enabled" do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
post = Fabricate(:post, raw: raw, user: user, topic: Fabricate(:topic, user: user))
|
||||
post.link_post_uploads
|
||||
post.update_uploads_secure_status
|
||||
|
||||
expect(PostUpload.where(post: post).joins(:upload).pluck(:upload_id, :secure)).to contain_exactly(
|
||||
[attachment_upload.id, true],
|
||||
[image_upload.id, false]
|
||||
)
|
||||
end
|
||||
|
||||
it "does not mark an upload as secure if it has already been used in a public topic" do
|
||||
post = Fabricate(:post, raw: raw, user: user, topic: Fabricate(:topic, user: user))
|
||||
post.link_post_uploads
|
||||
post.update_uploads_secure_status
|
||||
|
||||
pm = Fabricate(:post, raw: raw, user: user, topic: Fabricate(:private_message_topic, user: user))
|
||||
pm.link_post_uploads
|
||||
pm.update_uploads_secure_status
|
||||
|
||||
expect(PostUpload.where(post: pm).joins(:upload).pluck(:upload_id, :secure)).to contain_exactly(
|
||||
[attachment_upload.id, false],
|
||||
[image_upload.id, false]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -102,6 +102,47 @@ describe TopicConverter do
|
|||
expect(Notification.exists?(user_notification.id)).to eq(false)
|
||||
expect(Notification.exists?(admin_notification.id)).to eq(true)
|
||||
end
|
||||
|
||||
context "secure uploads" do
|
||||
fab!(:image_upload) { Fabricate(:upload) }
|
||||
fab!(:public_topic) { Fabricate(:topic, user: author) }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "some key"
|
||||
SiteSetting.s3_secret_access_key = "some secret key"
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
|
||||
|
||||
stub_request(
|
||||
:put,
|
||||
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{image_upload.sha1}.#{image_upload.extension}?acl"
|
||||
)
|
||||
end
|
||||
|
||||
it "converts regular uploads to secure when making a public post a PM" do
|
||||
public_reply = Fabricate(:post, raw: "<img src='#{image_upload.url}'>", user: other_user, topic: public_topic)
|
||||
public_reply.link_post_uploads
|
||||
public_reply.update_uploads_secure_status
|
||||
|
||||
expect(public_reply.uploads[0].secure).to eq(false)
|
||||
public_topic.convert_to_private_message(admin)
|
||||
expect(public_topic.reload.posts.find(public_reply.id).uploads[0].secure).to eq(true)
|
||||
end
|
||||
|
||||
it "converts secure uploads back to public" do
|
||||
first_post
|
||||
second_post = Fabricate(:post, raw: "<img src='#{image_upload.url}'>", user: other_user, topic: private_message)
|
||||
second_post.link_post_uploads
|
||||
second_post.update_uploads_secure_status
|
||||
|
||||
expect(second_post.uploads[0].secure).to eq(true)
|
||||
private_message.convert_to_public_topic(admin)
|
||||
expect(private_message.reload.posts.find(second_post.id).uploads[0].secure).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -288,6 +288,84 @@ describe Upload do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.update_secure_status' do
|
||||
it 'marks a local upload as not secure with default settings' do
|
||||
upload.update!(secure: true)
|
||||
expect { upload.update_secure_status }
|
||||
.to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(false)
|
||||
end
|
||||
|
||||
it 'marks a local attachment as secure if prevent_anons_from_downloading_files is enabled' do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
SiteSetting.authorized_extensions = "pdf"
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf")
|
||||
|
||||
expect { upload.update_secure_status }
|
||||
.to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(true)
|
||||
end
|
||||
|
||||
it 'marks a local attachment as not secure if prevent_anons_from_downloading_files is disabled' do
|
||||
SiteSetting.prevent_anons_from_downloading_files = false
|
||||
SiteSetting.authorized_extensions = "pdf"
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
|
||||
expect { upload.update_secure_status }
|
||||
.to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not change secure status of a non-attachment when prevent_anons_from_downloading_files is enabled' do
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
SiteSetting.authorized_extensions = "mp4"
|
||||
upload.update!(original_filename: "small.mp4", extension: "mp4")
|
||||
|
||||
expect { upload.update_secure_status }
|
||||
.not_to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(false)
|
||||
end
|
||||
|
||||
context "secure media enabled" do
|
||||
before do
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "some key"
|
||||
SiteSetting.s3_secret_access_key = "some secret key"
|
||||
SiteSetting.secure_media = true
|
||||
|
||||
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
|
||||
|
||||
stub_request(
|
||||
:put,
|
||||
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{upload.sha1}.#{upload.extension}?acl"
|
||||
)
|
||||
end
|
||||
|
||||
it 'marks an image upload as not secure when not associated with a post' do
|
||||
upload.update!(secure: true)
|
||||
expect { upload.update_secure_status }
|
||||
.to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(false)
|
||||
end
|
||||
|
||||
it 'marks an image upload as secure if login_required is enabled' do
|
||||
SiteSetting.login_required = true
|
||||
upload.update!(secure: false)
|
||||
|
||||
expect { upload.update_secure_status }
|
||||
.to change { upload.secure }
|
||||
|
||||
expect(upload.secure).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.reset_unknown_extensions!' do
|
||||
it 'should reset the extension of uploads when it is "unknown"' do
|
||||
upload1 = Fabricate(:upload, extension: "unknown")
|
||||
|
|
|
@ -119,7 +119,7 @@ RSpec.describe 'Multisite s3 uploads', type: :multisite do
|
|||
end
|
||||
end
|
||||
|
||||
context 'private uploads' do
|
||||
context 'secure uploads' do
|
||||
let(:store) { FileStore::S3Store.new }
|
||||
let(:client) { Aws::S3::Client.new(stub_responses: true) }
|
||||
let(:resource) { Aws::S3::Resource.new(client: client) }
|
||||
|
@ -133,18 +133,18 @@ RSpec.describe 'Multisite s3 uploads', type: :multisite do
|
|||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.prevent_anons_from_downloading_files = true
|
||||
SiteSetting.authorized_extensions = "pdf|png|jpg|gif"
|
||||
end
|
||||
|
||||
before do
|
||||
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: "etag"))
|
||||
end
|
||||
|
||||
describe "when private uploads are enabled" do
|
||||
describe "when secure attachments are enabled" do
|
||||
it "returns signed URL with correct path" do
|
||||
test_multisite_connection('default') do
|
||||
SiteSetting.authorized_extensions = "pdf|png|jpg|gif"
|
||||
upload = build_upload
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf")
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("uploads/default/original/1X/#{upload.sha1}.pdf").returns(s3_object).at_least_once
|
||||
|
@ -159,13 +159,41 @@ RSpec.describe 'Multisite s3 uploads', type: :multisite do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when secure media are enabled" do
|
||||
before do
|
||||
SiteSetting.login_required = true
|
||||
SiteSetting.secure_media = true
|
||||
s3_helper.stubs(:s3_client).returns(client)
|
||||
Discourse.stubs(:store).returns(store)
|
||||
end
|
||||
|
||||
it "returns signed URL with correct path" do
|
||||
test_multisite_connection('default') do
|
||||
upload = Fabricate.build(:upload_s3, sha1: upload_sha1, id: 1)
|
||||
|
||||
signed_url = Discourse.store.signed_url_for_path(upload.url)
|
||||
expect(signed_url).to match(/Amz-Expires/)
|
||||
expect(signed_url).to match("uploads/default")
|
||||
end
|
||||
|
||||
test_multisite_connection('second') do
|
||||
upload = Fabricate.build(:upload_s3, sha1: upload_sha1, id: 1)
|
||||
|
||||
signed_url = Discourse.store.signed_url_for_path(upload.url)
|
||||
expect(signed_url).to match(/Amz-Expires/)
|
||||
expect(signed_url).to match("uploads/second")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update_upload_ACL" do
|
||||
it "updates correct file for default and second multisite db" do
|
||||
test_multisite_connection('default') do
|
||||
upload = build_upload
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("uploads/default/original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||
s3_bucket.expects(:object).with("uploads/default/original/1X/#{upload.sha1}.pdf").returns(s3_object)
|
||||
s3_object.expects(:acl).returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "private").returns(s3_object)
|
||||
|
||||
|
@ -174,9 +202,10 @@ RSpec.describe 'Multisite s3 uploads', type: :multisite do
|
|||
|
||||
test_multisite_connection('second') do
|
||||
upload = build_upload
|
||||
upload.update!(original_filename: "small.pdf", extension: "pdf", secure: true)
|
||||
|
||||
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
|
||||
s3_bucket.expects(:object).with("uploads/second/original/1X/#{upload.sha1}.png").returns(s3_object)
|
||||
s3_bucket.expects(:object).with("uploads/second/original/1X/#{upload.sha1}.pdf").returns(s3_object)
|
||||
s3_object.expects(:acl).returns(s3_object)
|
||||
s3_object.expects(:put).with(acl: "private").returns(s3_object)
|
||||
|
||||
|
|
|
@ -361,6 +361,59 @@ describe UploadsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#show_secure" do
|
||||
describe "local store" do
|
||||
fab!(:image_upload) { upload_file("smallest.png") }
|
||||
|
||||
it "does not return secure media when using local store" do
|
||||
secure_url = image_upload.url.sub("/uploads", "/secure-media-uploads")
|
||||
get secure_url
|
||||
|
||||
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_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "fakeid7974664"
|
||||
SiteSetting.s3_secret_access_key = "fakesecretid7974664"
|
||||
SiteSetting.secure_media = true
|
||||
end
|
||||
|
||||
it "should return 404 for anonymous requests requests" do
|
||||
secure_url = upload.url.sub(SiteSetting.Upload.absolute_base_url, "/secure-media-uploads")
|
||||
get secure_url
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "should return signed url for legitimate request" do
|
||||
secure_url = upload.url.sub(SiteSetting.Upload.absolute_base_url, "/secure-media-uploads")
|
||||
sign_in(user)
|
||||
stub_request(:head, "https://s3-upload-bucket.s3.amazonaws.com/")
|
||||
|
||||
get secure_url
|
||||
|
||||
expect(response.status).to eq(302)
|
||||
expect(response.redirect_url).to match("Amz-Expires")
|
||||
end
|
||||
|
||||
it "should return secure media URL when looking up urls" do
|
||||
upload.update_column(:secure, true)
|
||||
sign_in(user)
|
||||
|
||||
post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] }
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result[0]["url"]).to match("secure-media-uploads")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#lookup_urls' do
|
||||
it 'can look up long urls' do
|
||||
sign_in(user)
|
||||
|
@ -373,6 +426,42 @@ describe UploadsController do
|
|||
expect(result[0]["url"]).to eq(upload.url)
|
||||
expect(result[0]["short_path"]).to eq(upload.short_path)
|
||||
end
|
||||
|
||||
describe 'secure media' do
|
||||
let(:upload) { Fabricate(:upload_s3, secure: true) }
|
||||
|
||||
before do
|
||||
SiteSetting.authorized_extensions = "pdf|png"
|
||||
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
||||
SiteSetting.s3_access_key_id = "s3-access-key-id"
|
||||
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
|
||||
SiteSetting.enable_s3_uploads = true
|
||||
SiteSetting.secure_media = true
|
||||
end
|
||||
|
||||
it 'returns secure url for a secure media upload' do
|
||||
sign_in(user)
|
||||
|
||||
post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] }
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result[0]["url"]).to match("/secure-media-uploads")
|
||||
expect(result[0]["short_path"]).to eq(upload.short_path)
|
||||
end
|
||||
|
||||
it 'does not return secure urls for non-media uploads' do
|
||||
upload.update!(original_filename: "not-an-image.pdf", extension: "pdf")
|
||||
sign_in(user)
|
||||
|
||||
post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] }
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result[0]["url"]).not_to match("/secure-media-uploads")
|
||||
expect(result[0]["short_path"]).to eq(upload.short_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#metadata' do
|
||||
|
|
Loading…
Reference in New Issue