Fixed Non-ASCII attachment filename will crash when downloading.

Thanks to xet7 !

Fixes #2759
This commit is contained in:
Lauri Ojansivu 2021-04-29 13:26:49 +03:00
parent 843ff8eaaa
commit c2da477735
277 changed files with 30568 additions and 52 deletions

View file

@ -0,0 +1,166 @@
/* Blob.js
* A Blob implementation.
* 2013-12-27
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/eboyjr
* License: X11/MIT
* See LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
if (!(typeof Blob === "function" || typeof Blob === "object") || typeof URL === "undefined")
if ((typeof Blob === "function" || typeof Blob === "object") && typeof webkitURL !== "undefined") self.URL = webkitURL;
else var Blob = (function (view) {
"use strict";
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
if (!real_URL.createObjectURL) {
URL = view.URL = {};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
return FakeBlobBuilder;
}(view));
return function Blob(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
builder.append(blobParts[i]);
}
}
return builder.getBlob(type);
};
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

View file

@ -0,0 +1,302 @@
/**
* @method DataMan
* @public
* @constructor
* @param {File|Blob|ArrayBuffer|Uint8Array|String} data The data that you want to manipulate.
* @param {String} [type] The data content (MIME) type, if known. Required if the first argument is an ArrayBuffer, Uint8Array, or URL
*/
DataMan = function DataMan(data, type) {
var self = this;
if (!data) {
throw new Error("DataMan constructor requires a data argument");
}
// The end result of all this is that we will have one of the following set:
// - self.blob
// - self.url
// Unless we already have in-memory data, we don't load anything into memory
// and instead rely on obtaining a read stream when the time comes.
if (typeof File !== "undefined" && data instanceof File) {
self.blob = data; // File inherits from Blob so this is OK
self._type = data.type;
} else if (typeof Blob !== "undefined" && data instanceof Blob) {
self.blob = data;
self._type = data.type;
} else if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer || EJSON.isBinary(data)) {
if (typeof Blob === "undefined") {
throw new Error("Browser must support Blobs to handle an ArrayBuffer or Uint8Array");
}
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed an ArrayBuffer or Uint8Array");
}
self.blob = new Blob([data], {type: type});
self._type = type;
} else if (typeof data === "string") {
if (data.slice(0, 5) === "data:") {
self._type = data.slice(5, data.indexOf(';'));
self.blob = dataURItoBlob(data, self._type);
} else if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {
if (!type) {
throw new Error("DataMan constructor requires a type argument when passed a URL");
}
self.url = data;
self._type = type;
} else {
throw new Error("DataMan constructor received unrecognized data string");
}
} else {
throw new Error("DataMan constructor received data that it doesn't support");
}
};
/**
* @method DataMan.prototype.getBlob
* @public
* @param {Function} [callback] - callback(error, blob)
* @returns {undefined|Blob}
*
* Passes a Blob representing this data to a callback or returns
* the Blob if no callback is provided. A callback is required
* if getting a Blob for a URL.
*/
DataMan.prototype.getBlob = function dataManGetBlob(callback) {
var self = this;
if (callback) {
if (self.blob) {
callback(null, self.blob);
} else if (self.url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', self.url, true);
xhr.responseType = "blob";
xhr.onload = function(data) {
self.blob = xhr.response;
callback(null, self.blob);
};
xhr.onerror = function(err) {
callback(err);
};
xhr.send();
}
} else {
if (self.url)
throw new Error('DataMan.getBlob requires a callback when managing a URL');
return self.blob;
}
};
/**
* @method DataMan.prototype.getBinary
* @public
* @param {Number} [start] - First byte position to read.
* @param {Number} [end] - Last byte position to read.
* @param {Function} callback - callback(error, binaryData)
* @returns {undefined}
*
* Passes a Uint8Array representing this data to a callback.
*/
DataMan.prototype.getBinary = function dataManGetBinary(start, end, callback) {
var self = this;
if (typeof start === "function") {
callback = start;
}
callback = callback || defaultCallback;
function read(blob) {
if (typeof FileReader === "undefined") {
callback(new Error("Browser does not support FileReader"));
return;
}
var reader = new FileReader();
reader.onload = function(evt) {
callback(null, new Uint8Array(evt.target.result));
};
reader.onerror = function(err) {
callback(err);
};
reader.readAsArrayBuffer(blob);
}
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
if (typeof start === "number" && typeof end === "number") {
var size = blob.size;
// Return the requested chunk of binary data
if (start >= size) {
callback(new Error("DataMan.getBinary: start position beyond end of data (" + size + ")"));
return;
}
end = Math.min(size, end);
var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
if (typeof slice === 'undefined') {
callback(new Error('Browser does not support File.slice'));
return;
}
read(slice.call(blob, start, end, self._type));
} else {
// Return the entire binary data
read(blob);
}
}
});
};
/** @method DataMan.prototype.saveAs
* @public
* @param {String} [filename]
* @return {undefined}
*
* Tells the browser to save the data like a normal downloaded file,
* using the provided filename.
*
*/
DataMan.prototype.saveAs = function dataManSaveAs(filename) {
var self = this;
if (typeof window === "undefined")
throw new Error("window must be defined to use saveLocal");
if (!window.saveAs) {
console.warn('DataMan.saveAs: window.saveAs not supported by this browser - add cfs-filesaver package');
return;
}
self.getBlob(function (error, blob) {
if (error) {
throw error;
} else {
window.saveAs(blob, filename);
}
});
};
/**
* @method DataMan.prototype.getDataUri
* @public
* @param {function} callback callback(err, dataUri)
*/
DataMan.prototype.getDataUri = function dataManGetDataUri(callback) {
// XXX: We could consider using: URL.createObjectURL(blob);
// This will create a reference to the blob data instead of a clone
// This is part of the File API - as the rest - Not sure how to generally
// support from IE10, FF26, Chrome 31, safari 7, opera 19, ios 6, android 4
var self = this;
if (typeof callback !== 'function')
throw new Error("getDataUri requires callback function");
if (typeof FileReader === "undefined") {
callback(new Error("Browser does not support FileReader"));
return;
}
var fileReader = new FileReader();
fileReader.onload = function(event) {
var dataUri = event.target.result;
callback(null, dataUri);
};
fileReader.onerror = function(err) {
callback(err);
};
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
fileReader.readAsDataURL(blob);
}
});
};
/**
* @method DataMan.prototype.size
* @public
* @param {function} [callback] callback(err, size)
*
* Passes the size of the data to the callback, if provided,
* or returns it. A callback is required to get the size of a URL on the client.
*/
DataMan.prototype.size = function dataManSize(callback) {
var self = this;
if (callback) {
if (typeof self._size === "number") {
callback(null, self._size);
} else {
self.getBlob(function (error, blob) {
if (error) {
callback(error);
} else {
self._size = blob.size;
callback(null, self._size);
}
});
}
} else {
if (self.url) {
throw new Error("On the client, DataMan.size requires a callback when getting size for a URL on the client");
} else if (typeof self._size === "number") {
return self._size;
} else {
var blob = self.getBlob();
self._size = blob.size;
return self._size;
}
}
};
/**
* @method DataMan.prototype.type
* @public
*
* Returns the type of the data.
*/
DataMan.prototype.type = function dataManType() {
return this._type;
};
/**
* @method dataURItoBlob
* @private
* @param {String} dataURI The data URI
* @param {String} dataTYPE The content type
* @returns {Blob} A new Blob instance
*
* Converts a data URI to a Blob.
*/
function dataURItoBlob(dataURI, dataTYPE) {
var str = atob(dataURI.split(',')[1]), array = [];
for(var i = 0; i < str.length; i++) array.push(str.charCodeAt(i));
return new Blob([new Uint8Array(array)], {type: dataTYPE});
}
/**
* @method defaultCallback
* @private
* @param {Error} [err]
* @returns {undefined}
*
* Can be used as a default callback for client methods that need a callback.
* Simply throws the provided error if there is one.
*/
function defaultCallback(err) {
if (err) {
// Show gentle error if Meteor error
if (err instanceof Meteor.Error) {
console.error(err.message);
} else {
// Normal error, just throw error
throw err;
}
}
}