Merge pull request from ZogStriP/attachments

Attachments
This commit is contained in:
Robin Ward 2013-07-11 06:49:20 -07:00
commit 7fd8bb75d9
23 changed files with 249 additions and 74 deletions

View file

@ -85,7 +85,7 @@ Discourse.ClickTrack = {
} }
// If we're on the same site, use the router and track via AJAX // If we're on the same site, use the router and track via AJAX
if ((href.indexOf(Discourse.URL.origin()) === 0) && (!href.match(/\.(png|gif|jpg|jpeg)$/i))) { if ((href.indexOf(Discourse.URL.origin()) === 0) && !href.match(/\/uploads\//i)) {
Discourse.ajax("/clicks/track", { Discourse.ajax("/clicks/track", {
data: { data: {
url: href, url: href,
@ -109,5 +109,3 @@ Discourse.ClickTrack = {
return false; return false;
} }
}; };

View file

@ -212,6 +212,30 @@ Discourse.Utilities = {
if (!extensions) return false; if (!extensions) return false;
var regexp = new RegExp("\\.(" + extensions.replace(/\./g, "") + ")$", "i"); var regexp = new RegExp("\\.(" + extensions.replace(/\./g, "") + ")$", "i");
return file && file.name ? file.name.match(regexp) : false; return file && file.name ? file.name.match(regexp) : false;
},
/**
Get the markdown template for an upload (either an image or an attachment)
@method getUploadMarkdown
@param {Upload} upload The upload we want the markdown from
**/
getUploadMarkdown: function(upload) {
if (this.isAnImage(upload.original_filename)) {
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
} else {
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a>';
}
},
/**
Check whether the path is refering to an image
@method isAnImage
@param {String} path The path
**/
isAnImage: function(path) {
return path && path.match(/\.(png|jpg|jpeg|gif|bmp|tif)$/i);
} }
}; };

View file

@ -285,9 +285,9 @@ Discourse.ComposerView = Discourse.View.extend({
// done // done
$uploadTarget.on('fileuploaddone', function (e, data) { $uploadTarget.on('fileuploaddone', function (e, data) {
var upload = data.result; var markdown = Discourse.Utilities.getUploadMarkdown(data.result);
var html = "<img src=\"" + upload.url + "\" width=\"" + upload.width + "\" height=\"" + upload.height + "\">"; // appends a space at the end of the inserted markdown
composerView.addMarkdown(html); composerView.addMarkdown(markdown + " ");
composerView.set('isUploading', false); composerView.set('isUploading', false);
}); });

View file

@ -1275,7 +1275,7 @@ else
// autolink anything like <http://example.com> // autolink anything like <http://example.com>
var replacer = function (wholematch, m1) { var replacer = function (wholematch, m1) {
m1encoded = m1.replace(/\_\_/, '%5F%5F'); var m1encoded = m1.replace(/\_\_/, '%5F%5F');
return "<a href=\"" + m1encoded + "\">" + pluginHooks.plainLinkText(m1) + "</a>"; return "<a href=\"" + m1encoded + "\">" + pluginHooks.plainLinkText(m1) + "</a>";
} }
text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);

View file

@ -175,6 +175,16 @@
} }
} }
a.attachment:before {
display: inline-block;
margin-right: 4px;
font-family: "FontAwesome";
content: "\f019";
}
.attachment + .size {
margin: 0 5px;
}
// When we are quoting something // When we are quoting something
aside.quote { aside.quote {
border-left: 5px solid #d7d7d7; border-left: 5px solid #d7d7d7;

View file

@ -4,8 +4,7 @@ class UploadsController < ApplicationController
def create def create
file = params[:file] || params[:files].first file = params[:file] || params[:files].first
# only supports images for now return render status: 415, json: failed_json unless SiteSetting.authorized_file?(file)
return render status: 415, json: failed_json unless file.content_type =~ /^image\/.+/
upload = Upload.create_for(current_user.id, file) upload = Upload.create_for(current_user.id, file)

View file

@ -49,19 +49,15 @@ class OptimizedImage < ActiveRecord::Base
end end
def url def url
"#{Upload.base_url}/#{optimized_path}/#{filename}" "#{LocalStore.base_url}/#{optimized_path}/#{filename}"
end end
def path def path
"#{path_root}/#{optimized_path}/#{filename}" "#{LocalStore.base_path}/#{optimized_path}/#{filename}"
end
def path_root
@path_root ||= "#{Rails.root}/public"
end end
def optimized_path def optimized_path
"uploads/#{RailsMultisite::ConnectionManagement.current_db}/_optimized/#{sha1[0..2]}/#{sha1[3..5]}" "_optimized/#{sha1[0..2]}/#{sha1[3..5]}"
end end
def filename def filename

View file

@ -274,6 +274,23 @@ class SiteSetting < ActiveRecord::Base
top_menu_items.map { |item| item.name }.select{ |item| list.include?(item) }.first top_menu_items.map { |item| item.name }.select{ |item| list.include?(item) }.first
end end
def self.authorized_file?(file)
file.original_filename =~ /\.(#{authorized_extensions.tr(". ", "")})$/i
end
def self.images
@images ||= ["jpg", "jpeg", "png", "gif", "tif", "tiff", "bmp"]
end
def self.authorized_image?(file)
authorized_images = authorized_extensions
.tr(". ", "")
.split("|")
.select { |extension| images.include?(extension) }
.join("|")
file.original_filename =~ /\.(#{authorized_images})$/i
end
end end
# == Schema Information # == Schema Information

View file

@ -1,9 +1,9 @@
require 'digest/sha1' require 'digest/sha1'
require 'image_sizer' require 'image_sizer'
require 's3'
require 'local_store'
require 'tempfile' require 'tempfile'
require 'pathname' require 'pathname'
require_dependency 's3'
require_dependency 'local_store'
class Upload < ActiveRecord::Base class Upload < ActiveRecord::Base
belongs_to :user belongs_to :user
@ -48,24 +48,25 @@ class Upload < ActiveRecord::Base
sha1 = Digest::SHA1.file(file.tempfile).hexdigest sha1 = Digest::SHA1.file(file.tempfile).hexdigest
# check if the file has already been uploaded # check if the file has already been uploaded
unless upload = Upload.where(sha1: sha1).first unless upload = Upload.where(sha1: sha1).first
# retrieve image info
image_info = FastImage.new(file.tempfile, raise_on_failure: true)
# compute image aspect ratio
width, height = ImageSizer.resize(*image_info.size)
# create a db record (so we can use the id) # create a db record (so we can use the id)
upload = Upload.create!({ upload = Upload.create!({
user_id: user_id, user_id: user_id,
original_filename: file.original_filename, original_filename: file.original_filename,
filesize: File.size(file.tempfile), filesize: File.size(file.tempfile),
sha1: sha1, sha1: sha1,
width: width,
height: height,
url: "" url: ""
}) })
# deal with width & heights for images
if SiteSetting.authorized_image?(file)
# retrieve image info
image_info = FastImage.new(file.tempfile, raise_on_failure: true)
# compute image aspect ratio
upload.width, upload.height = ImageSizer.resize(*image_info.size)
# make sure we're at the beginning of the file (FastImage is moving the pointer) # make sure we're at the beginning of the file (FastImage is moving the pointer)
file.rewind file.rewind
end
# store the file and update its url # store the file and update its url
upload.url = Upload.store_file(file, sha1, image_info, upload.id) upload.url = Upload.store_file(file, sha1, upload.id)
# save the url # save the url
upload.save upload.save
end end
@ -73,9 +74,9 @@ class Upload < ActiveRecord::Base
upload upload
end end
def self.store_file(file, sha1, image_info, upload_id) def self.store_file(file, sha1, upload_id)
return S3.store_file(file, sha1, image_info, upload_id) if SiteSetting.enable_s3_uploads? return S3.store_file(file, sha1, upload_id) if SiteSetting.enable_s3_uploads?
return LocalStore.store_file(file, sha1, image_info, upload_id) return LocalStore.store_file(file, sha1, upload_id)
end end
def self.remove_file(url) def self.remove_file(url)
@ -92,29 +93,15 @@ class Upload < ActiveRecord::Base
end end
def self.is_local?(url) def self.is_local?(url)
url.start_with?(base_url) !SiteSetting.enable_s3_uploads? && url.start_with?(LocalStore.base_url)
end end
def self.is_on_s3?(url) def self.is_on_s3?(url)
SiteSetting.enable_s3_uploads? && url.start_with?(S3.base_url) SiteSetting.enable_s3_uploads? && url.start_with?(S3.base_url)
end end
def self.base_url
asset_host.present? ? asset_host : Discourse.base_url_no_prefix
end
def self.asset_host
ActionController::Base.asset_host
end
def self.get_from_url(url) def self.get_from_url(url)
if has_been_uploaded?(url) Upload.where(url: url).first if has_been_uploaded?(url)
if m = LocalStore.uploaded_regex.match(url)
Upload.where(id: m[:upload_id]).first
elsif is_on_s3?(url)
Upload.where(url: url).first
end
end
end end
end end

View file

@ -1,5 +1,5 @@
class UploadSerializer < ApplicationSerializer class UploadSerializer < ApplicationSerializer
attributes :url, :filesize, :original_filename, :width, :height attributes :url, :original_filename, :width, :height
end end

View file

@ -21,8 +21,8 @@ server {
location / { location / {
root /home/discourse/discourse/public; root /home/discourse/discourse/public;
## optional image anti-hotlinking rules ## optional upload anti-hotlinking rules
#location ~ \.(jpe?g|png|gif)$ { #location ~ ^/uploads/ {
# valid_referers none blocked mysite.com *.mysite.com; # valid_referers none blocked mysite.com *.mysite.com;
# if ($invalid_referer) { # if ($invalid_referer) {
# return 403; # return 403;
@ -35,7 +35,7 @@ server {
add_header ETag ""; add_header ETag "";
} }
location ~ ^/assets/ { location ~ ^/(assets|uploads)/ {
expires 1y; expires 1y;
add_header Cache-Control public; add_header Cache-Control public;
add_header ETag ""; add_header ETag "";

View file

@ -16,10 +16,26 @@ class CookedPostProcessor
end end
def post_process def post_process
post_process_attachments
post_process_images post_process_images
post_process_oneboxes post_process_oneboxes
end end
def post_process_attachments
attachments.each do |attachment|
href = attachment['href']
attachment['href'] = relative_to_absolute(href)
if upload = Upload.get_from_url(href)
# update reverse index
associate_to_post(upload)
# append the size
append_human_size!(attachment, upload)
end
# mark as dirty
@dirty = true
end
end
def post_process_images def post_process_images
images = extract_images images = extract_images
return if images.blank? return if images.blank?
@ -33,7 +49,7 @@ class CookedPostProcessor
# make sure the img has proper width and height attributes # make sure the img has proper width and height attributes
update_dimensions!(img) update_dimensions!(img)
# retrieve the associated upload, if any # retrieve the associated upload, if any
if upload = Upload.get_from_url(img['src']) if upload = Upload.get_from_url(src)
# update reverse index # update reverse index
associate_to_post(upload) associate_to_post(upload)
end end
@ -209,6 +225,22 @@ class CookedPostProcessor
rescue URI::InvalidURIError rescue URI::InvalidURIError
end end
def attachments
if SiteSetting.enable_s3_uploads?
@doc.css("a[href^=\"#{S3.base_url}\"]")
else
# local uploads are identified using a relative uri
@doc.css("a[href^=\"#{LocalStore.directory}\"]")
end
end
def append_human_size!(attachment, upload)
size = Nokogiri::XML::Node.new("span", @doc)
size["class"] = "size"
size.content = "(#{number_to_human_size(upload.filesize)})"
attachment.add_next_sibling(size)
end
def dirty? def dirty?
@dirty @dirty
end end

View file

@ -1,18 +1,21 @@
module LocalStore module LocalStore
def self.store_file(file, sha1, image_info, upload_id) def self.store_file(file, sha1, upload_id)
clean_name = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0,16] + ".#{image_info.type}" unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0,16]
extension = File.extname(file.original_filename)
clean_name = "#{unique_sha1}#{extension}"
url_root = "/uploads/#{RailsMultisite::ConnectionManagement.current_db}/#{upload_id}" url_root = "/uploads/#{RailsMultisite::ConnectionManagement.current_db}/#{upload_id}"
path = "#{Rails.root}/public#{url_root}" path = "#{Rails.root}/public#{url_root}"
FileUtils.mkdir_p path FileUtils.mkdir_p path
# not using cause mv, cause permissions are no good on move # not using cause mv, cause permissions are no good on move
File.open("#{path}/#{clean_name}", "wb") do |f| File.open("#{path}/#{clean_name}", "wb") do |f|
f.write File.read(file.tempfile) f.write File.read(file.tempfile)
end end
# url # url
return Discourse::base_uri + "#{url_root}/#{clean_name}" Discourse::base_uri + "#{url_root}/#{clean_name}"
end end
def self.remove_file(url) def self.remove_file(url)
@ -24,4 +27,21 @@ module LocalStore
/\/uploads\/#{RailsMultisite::ConnectionManagement.current_db}\/(?<upload_id>\d+)\/[0-9a-f]{16}\.(png|jpg|jpeg|gif|tif|tiff|bmp)/ /\/uploads\/#{RailsMultisite::ConnectionManagement.current_db}\/(?<upload_id>\d+)\/[0-9a-f]{16}\.(png|jpg|jpeg|gif|tif|tiff|bmp)/
end end
def self.base_url
url = asset_host.present? ? asset_host : Discourse.base_url_no_prefix
"#{url}#{directory}"
end
def self.base_path
"#{Rails.root}/public#{directory}"
end
def self.directory
"/uploads/#{RailsMultisite::ConnectionManagement.current_db}"
end
def self.asset_host
ActionController::Base.asset_host
end
end end

View file

@ -1,11 +1,11 @@
module S3 module S3
def self.store_file(file, sha1, image_info, upload_id) def self.store_file(file, sha1, upload_id)
S3.check_missing_site_settings S3.check_missing_site_settings
directory = S3.get_or_create_directory(SiteSetting.s3_upload_bucket) directory = S3.get_or_create_directory(SiteSetting.s3_upload_bucket)
extension = File.extname(file.original_filename)
remote_filename = "#{upload_id}#{sha1}.#{image_info.type}" remote_filename = "#{upload_id}#{sha1}#{extension}"
# if this fails, it will throw an exception # if this fails, it will throw an exception
file = S3.upload(file, remote_filename, directory) file = S3.upload(file, remote_filename, directory)

View file

@ -9,7 +9,8 @@ describe CookedPostProcessor do
let(:cpp) { CookedPostProcessor.new(post) } let(:cpp) { CookedPostProcessor.new(post) }
let(:post_process) { sequence("post_process") } let(:post_process) { sequence("post_process") }
it "works on images before oneboxes" do it "post process in sequence" do
cpp.expects(:post_process_attachments).in_sequence(post_process)
cpp.expects(:post_process_images).in_sequence(post_process) cpp.expects(:post_process_images).in_sequence(post_process)
cpp.expects(:post_process_oneboxes).in_sequence(post_process) cpp.expects(:post_process_oneboxes).in_sequence(post_process)
cpp.post_process cpp.post_process
@ -17,6 +18,35 @@ describe CookedPostProcessor do
end end
context "post_process_attachments" do
context "with attachment" do
let(:upload) { Fabricate(:upload) }
let(:post) { Fabricate(:post_with_an_attachment) }
let(:cpp) { CookedPostProcessor.new(post) }
# all in one test to speed things up
it "works" do
Upload.expects(:get_from_url).returns(upload)
cpp.post_process_attachments
# ensures absolute urls on attachment
cpp.html.should =~ /#{LocalStore.base_url}/
# ensure name is present
cpp.html.should =~ /archive.zip/
# ensure size is present
cpp.html.should =~ /<span class=\"size\">\(1.21 KB\)<\/span>/
# dirty
cpp.should be_dirty
# keeps the reverse index up to date
post.uploads.reload
post.uploads.count.should == 1
end
end
end
context "post_process_images" do context "post_process_images" do
context "with images in quotes and oneboxes" do context "with images in quotes and oneboxes" do
@ -48,7 +78,7 @@ describe CookedPostProcessor do
Upload.expects(:get_from_url).returns(upload).twice Upload.expects(:get_from_url).returns(upload).twice
cpp.post_process_images cpp.post_process_images
# ensures absolute urls on uploaded images # ensures absolute urls on uploaded images
cpp.html.should =~ /#{Discourse.base_url_no_prefix}/ cpp.html.should =~ /#{LocalStore.base_url}/
# dirty # dirty
cpp.should be_dirty cpp.should be_dirty
# keeps the reverse index up to date # keeps the reverse index up to date
@ -58,7 +88,7 @@ describe CookedPostProcessor do
end end
context "width sized images" do context "with sized images" do
let(:post) { build(:post_with_image_url) } let(:post) { build(:post_with_image_url) }
let(:cpp) { CookedPostProcessor.new(post, image_sizes: {'http://foo.bar/image.png' => {'width' => 111, 'height' => 222}}) } let(:cpp) { CookedPostProcessor.new(post, image_sizes: {'http://foo.bar/image.png' => {'width' => 111, 'height' => 222}}) }

View file

@ -22,7 +22,7 @@ describe LocalStore do
# The Time needs to be frozen as it is used to generate a clean & unique name # The Time needs to be frozen as it is used to generate a clean & unique name
Time.stubs(:now).returns(Time.utc(2013, 2, 17, 12, 0, 0, 0)) Time.stubs(:now).returns(Time.utc(2013, 2, 17, 12, 0, 0, 0))
# #
LocalStore.store_file(file, "", image_info, 1).should == '/uploads/default/1/253dc8edf9d4ada1.png' LocalStore.store_file(file, "", 1).should == '/uploads/default/1/253dc8edf9d4ada1.png'
end end
end end

View file

@ -24,7 +24,7 @@ describe S3 do
end end
it 'returns the url of the S3 upload if successful' do it 'returns the url of the S3 upload if successful' do
S3.store_file(file, "SHA", image_info, 1).should == '//s3_upload_bucket.s3.amazonaws.com/1SHA.png' S3.store_file(file, "SHA", 1).should == '//s3_upload_bucket.s3.amazonaws.com/1SHA.png'
end end
after(:each) do after(:each) do

View file

@ -41,20 +41,39 @@ describe UploadsController do
let(:files) { [ logo_dev, logo ] } let(:files) { [ logo_dev, logo ] }
context 'with a file' do context 'with a file' do
it 'is succesful' do
it 'is successful' do
xhr :post, :create, file: logo xhr :post, :create, file: logo
response.should be_success response.should be_success
end end
it 'supports only images' do context 'when authorized' do
before { SiteSetting.stubs(:authorized_extensions).returns(".txt") }
it 'is successful' do
xhr :post, :create, file: text_file
response.status.should eq 200
end
end
context 'when not authorized' do
before { SiteSetting.stubs(:authorized_extensions).returns(".png") }
it 'rejects the upload' do
xhr :post, :create, file: text_file xhr :post, :create, file: text_file
response.status.should eq 415 response.status.should eq 415
end end
end
end end
context 'with some files' do context 'with some files' do
it 'is succesful' do it 'is successful' do
xhr :post, :create, files: files xhr :post, :create, files: files
response.should be_success response.should be_success
end end

View file

@ -50,6 +50,10 @@ Fabricator(:post_with_uploaded_images, from: :post) do
' '
end end
Fabricator(:post_with_an_attachment, from: :post) do
cooked '<a class="attachment" href="/uploads/default/186/66b3ed1503efc936.zip">archive.zip</a>'
end
Fabricator(:post_with_unsized_images, from: :post) do Fabricator(:post_with_unsized_images, from: :post) do
cooked ' cooked '
<img src="http://foo.bar/image.png"> <img src="http://foo.bar/image.png">

View file

@ -6,3 +6,10 @@ Fabricator(:upload) do
height 200 height 200
url "/uploads/default/1/1234567890123456.jpg" url "/uploads/default/1/1234567890123456.jpg"
end end
Fabricator(:attachment, from: :upload) do
user
original_filename "archive.zip"
filesize 1234
url "/uploads/default/186/66b3ed1503efc936.zip"
end

View file

@ -62,13 +62,13 @@ describe Upload do
it "identifies internal or relatives urls" do it "identifies internal or relatives urls" do
Discourse.expects(:base_url_no_prefix).returns("http://discuss.site.com") Discourse.expects(:base_url_no_prefix).returns("http://discuss.site.com")
Upload.has_been_uploaded?("http://discuss.site.com/upload/1234/42/0123456789ABCDEF.jpg").should == true Upload.has_been_uploaded?("http://discuss.site.com/uploads/default/42/0123456789ABCDEF.jpg").should == true
Upload.has_been_uploaded?("/upload/42/0123456789ABCDEF.jpg").should == true Upload.has_been_uploaded?("/upload/42/0123456789ABCDEF.jpg").should == true
end end
it "identifies internal urls when using a CDN" do it "identifies internal urls when using a CDN" do
ActionController::Base.expects(:asset_host).returns("http://my.cdn.com").twice ActionController::Base.expects(:asset_host).returns("http://my.cdn.com").twice
Upload.has_been_uploaded?("http://my.cdn.com/upload/1234/42/0123456789ABCDEF.jpg").should == true Upload.has_been_uploaded?("http://my.cdn.com/uploads/default/42/0123456789ABCDEF.jpg").should == true
end end
it "identifies S3 uploads" do it "identifies S3 uploads" do
@ -78,7 +78,7 @@ describe Upload do
end end
it "identifies external urls" do it "identifies external urls" do
Upload.has_been_uploaded?("http://domain.com/upload/1234/42/0123456789ABCDEF.jpg").should == false Upload.has_been_uploaded?("http://domain.com/uploads/default/42/0123456789ABCDEF.jpg").should == false
Upload.has_been_uploaded?("//bucket.s3.amazonaws.com/1337.png").should == false Upload.has_been_uploaded?("//bucket.s3.amazonaws.com/1337.png").should == false
end end

View file

@ -20,6 +20,7 @@ module("Discourse.ClickTrack", {
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>', ' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
' </div>', ' </div>',
' <a id="same-site" href="http://discuss.domain.com">forum</a>', ' <a id="same-site" href="http://discuss.domain.com">forum</a>',
' <a class="attachment" href="http://discuss.domain.com/uploads/default/1234/1532357280.txt">log.txt</a>',
' </article>', ' </article>',
'</div>'].join("\n")); '</div>'].join("\n"));
}, },
@ -156,6 +157,13 @@ test("tracks via AJAX if we're on the same site", function() {
ok(Discourse.URL.routeTo.calledOnce); ok(Discourse.URL.routeTo.calledOnce);
}); });
test("does not track via AJAX for attachments", function() {
this.stub(Discourse.URL, "routeTo");
this.stub(Discourse.URL, "origin").returns("http://discuss.domain.com");
ok(!track(generateClickEventOn('.attachment')));
ok(Discourse.URL.redirectTo.calledOnce);
});
test("tracks custom urls when opening in another window", function() { test("tracks custom urls when opening in another window", function() {
var clickEvent = generateClickEventOn('a'); var clickEvent = generateClickEventOn('a');

View file

@ -7,7 +7,6 @@ test("emailValid", function() {
ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
}); });
var validUpload = utils.validateFilesForUpload; var validUpload = utils.validateFilesForUpload;
test("validateFilesForUpload", function() { test("validateFilesForUpload", function() {
@ -51,9 +50,9 @@ test("prevents files that are too big from being uploaded", function() {
}); });
var dummyBlob = function() { var dummyBlob = function() {
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
if (window.BlobBuilder) { if (BlobBuilder) {
var bb = new window.BlobBuilder(); var bb = new BlobBuilder();
bb.append([1]); bb.append([1]);
return bb.getBlob("image/png"); return bb.getBlob("image/png");
} else { } else {
@ -85,3 +84,28 @@ test("isAuthorizedUpload", function() {
ok(!isAuthorized("image.txt")); ok(!isAuthorized("image.txt"));
ok(!isAuthorized("")); ok(!isAuthorized(""));
}); });
var getUploadMarkdown = function(filename) {
return utils.getUploadMarkdown({
original_filename: filename,
width: 100,
height: 200,
url: "/upload/123/abcdef.ext"
});
};
test("getUploadMarkdown", function() {
ok(getUploadMarkdown("lolcat.gif") === '<img src="/upload/123/abcdef.ext" width="100" height="200">');
ok(getUploadMarkdown("important.txt") === '<a class="attachment" href="/upload/123/abcdef.ext">important.txt</a>');
});
test("isAnImage", function() {
_.each(["png", "jpg", "jpeg", "bmp", "gif", "tif"], function(extension) {
var image = "image." + extension;
ok(utils.isAnImage(image));
ok(utils.isAnImage("http://foo.bar/path/to/" + image));
});
ok(!utils.isAnImage("file.txt"));
ok(!utils.isAnImage("http://foo.bar/path/to/file.txt"));
ok(!utils.isAnImage(""));
});