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,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-2015 [@raix](https://github.com/raix) and [@aldeed](https://github.com/aldeed), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,7 @@
wekan-cfs-storage-adapter
=========================
This is a Meteor package used by
[CollectionFS](https://github.com/zcfs/Meteor-CollectionFS).
You don't need to manually add this package to your app. It is used by other packages to create various storage adapters.

View file

@ -0,0 +1,101 @@
> File: ["storageAdapter.server.js"](storageAdapter.server.js)
> Where: {server}
-
#############################################################################
STORAGE ADAPTER
#############################################################################
#### <a name="self.insert"></a>*self*.insert(fsFile, [options], [callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ####
```
Attempts to insert a file into the store, first running the beforeSave
function for the store if there is one. If there is a temporary failure,
returns (or passes to the second argument of the callback) `null`. If there
is a permanant failure or the beforeSave function returns `false`, returns
`false`. If the file is successfully stored, returns an object with file
info that the FS.Collection can save.
Also updates the `files` collection for this store to save info about this
file.
```
-
*This method __insert__ is defined in `self`*
__Arguments__
* __fsFile__ *{[FS.File](#FS.File)}*
The FS.File instance to be stored.
* __options__ *{Object}* (Optional)
Options (currently unused)
* __callback__ *{Function}* (Optional)
If not provided, will block and return file info.
-
> ```self.insert = function(fsFile, options, callback) { ...``` [storageAdapter.server.js:169](storageAdapter.server.js#L169)
-
#### <a name="self.update"></a>*self*.update(fsFile, [options], [callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ####
```
Attempts to update a file in the store, first running the beforeSave
function for the store if there is one. If there is a temporary failure,
returns (or passes to the second argument of the callback) `null`. If there
is a permanant failure or the beforeSave function returns `false`, returns
`false`. If the file is successfully stored, returns an object with file
info that the FS.Collection can save.
Also updates the `files` collection for this store to save info about this
file.
```
-
*This method __update__ is defined in `self`*
__Arguments__
* __fsFile__ *{[FS.File](#FS.File)}*
The FS.File instance to be stored.
* __options__ *{Object}* (Optional)
Options (currently unused)
* __callback__ *{Function}* (Optional)
If not provided, will block and return file info.
-
> ```self.update = function(fsFile, options, callback) { ...``` [storageAdapter.server.js:264](storageAdapter.server.js#L264)
-
#### <a name="self.remove"></a>*self*.remove(fsFile, [options], [callback])&nbsp;&nbsp;<sub><i>Server</i></sub> ####
```
Attempts to remove a file from the store. Returns true if removed, or false.
Also removes file info from the `files` collection for this store.
```
-
*This method __remove__ is defined in `self`*
__Arguments__
* __fsFile__ *{[FS.File](#FS.File)}*
The FS.File instance to be stored.
* __options__ *{Object}* (Optional)
Options
- __ignoreMissing__ *{Boolean}* (Optional)
Set true to treat missing files as a successful deletion. Otherwise throws an error.
* __callback__ *{Function}* (Optional)
If not provided, will block and return true or false
-
> ```self.remove = function(fsFile, options, callback) { ...``` [storageAdapter.server.js:321](storageAdapter.server.js#L321)
-

View file

@ -0,0 +1,50 @@
Package.describe({
git: 'https://github.com/zcfs/Meteor-cfs-storage-adapter.git',
name: 'wekan-cfs-storage-adapter',
version: '0.2.4',
summary: 'CollectionFS, Class for creating Storage adapters'
});
Npm.depends({
'length-stream': '0.1.1'
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use([
// CFS
'wekan-cfs-base-package@0.0.30',
// Core
'deps',
'check',
'livedata',
'mongo-livedata',
'ejson',
// Other
'raix:eventemitter@0.1.1'
]);
// We want to make sure that its added to scope for now if installed.
// We have set a deprecation warning on the transform scope
api.use('wekan-cfs-graphicsmagick@0.0.17', 'server', { weak: true });
api.addFiles([
'storageAdapter.client.js'
], 'client');
api.addFiles([
'storageAdapter.server.js',
'transform.server.js'
], 'server');
});
Package.onTest(function (api) {
api.use('wekan-cfs-storage-adapter');
api.use('test-helpers', 'server');
api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict',
'random', 'deps']);
api.addFiles('tests/server-tests.js', 'server');
api.addFiles('tests/client-tests.js', 'client');
});

View file

@ -0,0 +1,37 @@
/* global FS, _storageAdapters:true, EventEmitter */
// #############################################################################
//
// STORAGE ADAPTER
//
// #############################################################################
_storageAdapters = {};
FS.StorageAdapter = function(name, options, api) {
var self = this;
// Check the api
if (typeof api === 'undefined') {
throw new Error('FS.StorageAdapter please define an api');
}
// store reference for easy lookup by name
if (typeof _storageAdapters[name] !== 'undefined') {
throw new Error('Storage name already exists: "' + name + '"');
} else {
_storageAdapters[name] = self;
}
// extend self with options and other info
FS.Utility.extend(this, options || {}, {
name: name
});
// XXX: TODO, add upload feature here...
// we default to ddp upload but really let the SA like S3Cloud overwrite to
// implement direct client to s3 upload
};
FS.StorageAdapter.prototype = new EventEmitter();

View file

@ -0,0 +1,269 @@
/* global FS, _storageAdapters:true, EventEmitter */
// #############################################################################
//
// STORAGE ADAPTER
//
// #############################################################################
_storageAdapters = {};
FS.StorageAdapter = function(storeName, options, api) {
var self = this, fileKeyMaker;
options = options || {};
// If storeName is the only argument, a string and the SA already found
// we will just return that SA
if (arguments.length === 1 && storeName === '' + storeName &&
typeof _storageAdapters[storeName] !== 'undefined')
return _storageAdapters[storeName];
// Verify that the storage adapter defines all the necessary API methods
if (typeof api === 'undefined') {
throw new Error('FS.StorageAdapter please define an api');
}
FS.Utility.each('fileKey,remove,typeName,createReadStream,createWriteStream'.split(','), function(name) {
if (typeof api[name] === 'undefined') {
throw new Error('FS.StorageAdapter please define an api. "' + name + '" ' + (api.typeName || ''));
}
});
// Create an internal namespace, starting a name with underscore is only
// allowed for stores marked with options.internal === true
if (options.internal !== true && storeName[0] === '_') {
throw new Error('A storage adapter name may not begin with "_"');
}
if (storeName.indexOf('.') !== -1) {
throw new Error('A storage adapter name may not contain a "."');
}
// store reference for easy lookup by storeName
if (typeof _storageAdapters[storeName] !== 'undefined') {
throw new Error('Storage name already exists: "' + storeName + '"');
} else {
_storageAdapters[storeName] = self;
}
// User can customize the file key generation function
if (typeof options.fileKeyMaker === "function") {
fileKeyMaker = options.fileKeyMaker;
} else {
fileKeyMaker = api.fileKey;
}
// User can provide a function to adjust the fileObj
// before it is written to the store.
var beforeWrite = options.beforeWrite;
// extend self with options and other info
FS.Utility.extend(this, options, {
name: storeName,
typeName: api.typeName
});
// Create a nicer abstracted adapter interface
self.adapter = {};
self.adapter.fileKey = function(fileObj) {
return fileKeyMaker(fileObj);
};
// Return readable stream for fileKey
self.adapter.createReadStreamForFileKey = function(fileKey, options) {
if (FS.debug) console.log('createReadStreamForFileKey ' + storeName);
return FS.Utility.safeStream( api.createReadStream(fileKey, options) );
};
// Return readable stream for fileObj
self.adapter.createReadStream = function(fileObj, options) {
if (FS.debug) console.log('createReadStream ' + storeName);
if (self.internal) {
// Internal stores take a fileKey
return self.adapter.createReadStreamForFileKey(fileObj, options);
}
return FS.Utility.safeStream( self._transform.createReadStream(fileObj, options) );
};
function logEventsForStream(stream) {
if (FS.debug) {
stream.on('stored', function() {
console.log('-----------STORED STREAM', storeName);
});
stream.on('close', function() {
console.log('-----------CLOSE STREAM', storeName);
});
stream.on('end', function() {
console.log('-----------END STREAM', storeName);
});
stream.on('finish', function() {
console.log('-----------FINISH STREAM', storeName);
});
stream.on('error', function(error) {
console.log('-----------ERROR STREAM', storeName, error && (error.message || error.code));
});
}
}
// Return writeable stream for fileKey
self.adapter.createWriteStreamForFileKey = function(fileKey, options) {
if (FS.debug) console.log('createWriteStreamForFileKey ' + storeName);
var writeStream = FS.Utility.safeStream( api.createWriteStream(fileKey, options) );
logEventsForStream(writeStream);
return writeStream;
};
// Return writeable stream for fileObj
self.adapter.createWriteStream = function(fileObj, options) {
if (FS.debug) console.log('createWriteStream ' + storeName + ', internal: ' + !!self.internal);
if (self.internal) {
// Internal stores take a fileKey
return self.adapter.createWriteStreamForFileKey(fileObj, options);
}
// If we haven't set name, type, or size for this version yet,
// set it to same values as original version. We don't save
// these to the DB right away because they might be changed
// in a transformWrite function.
if (!fileObj.name({store: storeName})) {
fileObj.name(fileObj.name(), {store: storeName, save: false});
}
if (!fileObj.type({store: storeName})) {
fileObj.type(fileObj.type(), {store: storeName, save: false});
}
if (!fileObj.size({store: storeName})) {
fileObj.size(fileObj.size(), {store: storeName, save: false});
}
// Call user function to adjust file metadata for this store.
// We support updating name, extension, and/or type based on
// info returned in an object. Or `fileObj` could be
// altered directly within the beforeWrite function.
if (beforeWrite) {
var fileChanges = beforeWrite(fileObj);
if (typeof fileChanges === "object") {
if (fileChanges.extension) {
fileObj.extension(fileChanges.extension, {store: storeName, save: false});
} else if (fileChanges.name) {
fileObj.name(fileChanges.name, {store: storeName, save: false});
}
if (fileChanges.type) {
fileObj.type(fileChanges.type, {store: storeName, save: false});
}
}
}
var writeStream = FS.Utility.safeStream( self._transform.createWriteStream(fileObj, options) );
logEventsForStream(writeStream);
// Its really only the storage adapter who knows if the file is uploaded
//
// We have to use our own event making sure the storage process is completed
// this is mainly
writeStream.safeOn('stored', function(result) {
if (typeof result.fileKey === 'undefined') {
throw new Error('SA ' + storeName + ' type ' + api.typeName + ' did not return a fileKey');
}
if (FS.debug) console.log('SA', storeName, 'stored', result.fileKey);
// Set the fileKey
fileObj.copies[storeName].key = result.fileKey;
// Update the size, as provided by the SA, in case it was changed by stream transformation
if (typeof result.size === "number") {
fileObj.copies[storeName].size = result.size;
}
// Set last updated time, either provided by SA or now
fileObj.copies[storeName].updatedAt = result.storedAt || new Date();
// If the file object copy havent got a createdAt then set this
if (typeof fileObj.copies[storeName].createdAt === 'undefined') {
fileObj.copies[storeName].createdAt = fileObj.copies[storeName].updatedAt;
}
fileObj._saveChanges(storeName);
// There is code in transform that may have set the original file size, too.
fileObj._saveChanges('_original');
});
// Emit events from SA
writeStream.once('stored', function(/*result*/) {
// XXX Because of the way stores inherit from SA, this will emit on every store.
// Maybe need to rewrite the way we inherit from SA?
var emitted = self.emit('stored', storeName, fileObj);
if (FS.debug && !emitted) {
console.log(fileObj.name() + ' was successfully stored in the ' + storeName + ' store. You are seeing this informational message because you enabled debugging and you have not defined any listeners for the "stored" event on this store.');
}
});
writeStream.on('error', function(error) {
// XXX We could wrap and clarify error
// XXX Because of the way stores inherit from SA, this will emit on every store.
// Maybe need to rewrite the way we inherit from SA?
var emitted = self.emit('error', storeName, error, fileObj);
if (FS.debug && !emitted) {
console.log(error);
}
});
return writeStream;
};
//internal
self._removeAsync = function(fileKey, callback) {
// Remove the file from the store
api.remove.call(self, fileKey, callback);
};
/**
* @method FS.StorageAdapter.prototype.remove
* @public
* @param {FS.File} fsFile The FS.File instance to be stored.
* @param {Function} [callback] If not provided, will block and return true or false
*
* Attempts to remove a file from the store. Returns true if removed or not
* found, or false if the file couldn't be removed.
*/
self.adapter.remove = function(fileObj, callback) {
if (FS.debug) console.log("---SA REMOVE");
// Get the fileKey
var fileKey = (fileObj instanceof FS.File) ? self.adapter.fileKey(fileObj) : fileObj;
if (callback) {
return self._removeAsync(fileKey, FS.Utility.safeCallback(callback));
} else {
return Meteor.wrapAsync(self._removeAsync)(fileKey);
}
};
self.remove = function(fileObj, callback) {
// Add deprecation note
console.warn('Storage.remove is deprecating, use "Storage.adapter.remove"');
return self.adapter.remove(fileObj, callback);
};
if (typeof api.init === 'function') {
Meteor.wrapAsync(api.init.bind(self))();
}
// This supports optional transformWrite and transformRead
self._transform = new FS.Transform({
adapter: self.adapter,
// Optional transformation functions:
transformWrite: options.transformWrite,
transformRead: options.transformRead
});
};
Npm.require('util').inherits(FS.StorageAdapter, EventEmitter);

View file

@ -0,0 +1,44 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-storage-adapter - client - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Client Tests
*
* construct FS.File with no arguments
* construct FS.File passing in File
* construct FS.File passing in Blob
* load blob into FS.File and then call FS.File.toDataUrl
* call FS.File.setDataFromBinary, then FS.File.getBlob(); make sure correct data is returned
* load blob into FS.File and then call FS.File.getBinary() with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
* load blob into FS.File and make sure FS.File.saveLocal initiates a download (possibly can't do automatically)
*
*/
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

@ -0,0 +1,49 @@
function equals(a, b) {
return !!(EJSON.stringify(a) === EJSON.stringify(b));
}
Tinytest.add('cfs-storage-adapter - server - test environment', function(test) {
test.isTrue(typeof FS.Collection !== 'undefined', 'test environment not initialized FS.Collection');
test.isTrue(typeof CFSErrorType !== 'undefined', 'test environment not initialized CFSErrorType');
});
/*
* FS.File Server Tests
*
* construct FS.File with no arguments
* load data with FS.File.setDataFromBuffer
* load data with FS.File.setDataFromBinary
* load data and then call FS.File.toDataUrl with and without callback
* load buffer into FS.File and then call FS.File.getBinary with and without start/end; make sure correct data is returned
* construct FS.File, set FS.File.collectionName to a CFS name, and then test FS.File.update/remove/get/put/del/url
* (call these with and without callback to test sync vs. async)
* set FS.File.name to a filename and test that FS.File.getExtension() returns the extension
*
*
* FS.Collection Server Tests
*
* Make sure options.filter is respected
*
*
*/
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equalactual, expected, message, not
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)
//test.equal(a, b, msg)

View file

@ -0,0 +1,119 @@
/* global FS */
var PassThrough = Npm.require('stream').PassThrough;
var lengthStream = Npm.require('length-stream');
FS.Transform = function(options) {
var self = this;
options = options || {};
if (!(self instanceof FS.Transform))
throw new Error('FS.Transform must be called with the "new" keyword');
if (!options.adapter)
throw new Error('Transform expects option.adapter to be a storage adapter');
self.storage = options.adapter;
// Fetch the transformation functions if any
self.transformWrite = options.transformWrite;
self.transformRead = options.transformRead;
};
// Allow packages to add scope
FS.Transform.scope = {};
// The transformation stream triggers an "stored" event when data is stored into
// the storage adapter
FS.Transform.prototype.createWriteStream = function(fileObj) {
var self = this;
// Get the file key
var fileKey = self.storage.fileKey(fileObj);
// Rig write stream
var destinationStream = self.storage.createWriteStreamForFileKey(fileKey, {
// Not all SA's can set these options and cfs dont depend on setting these
// but its nice if other systems are accessing the SA that some of the data
// is also available to those
aliases: [fileObj.name()],
contentType: fileObj.type(),
metadata: fileObj.metadata
});
// Pass through transformWrite function if provided
if (typeof self.transformWrite === 'function') {
destinationStream = addPassThrough(destinationStream, function (ptStream, originalStream) {
// Rig transform
try {
self.transformWrite.call(FS.Transform.scope, fileObj, ptStream, originalStream);
// XXX: If the transform function returns a buffer should we stream that?
} catch(err) {
// We emit an error - should we throw an error?
console.warn('FS.Transform.createWriteStream transform function failed, Error: ');
throw err;
}
});
}
// If original doesn't have size, add another PassThrough to get and set the size.
// This will run on size=0, too, which is OK.
// NOTE: This must come AFTER the transformWrite code block above. This might seem
// confusing, but by coming after it, this will actually be executed BEFORE the user's
// transform, which is what we need in order to be sure we get the original file
// size and not the transformed file size.
if (!fileObj.size()) {
destinationStream = addPassThrough(destinationStream, function (ptStream, originalStream) {
var lstream = lengthStream(function (fileSize) {
fileObj.size(fileSize, {save: false});
});
ptStream.pipe(lstream).pipe(originalStream);
});
}
return destinationStream;
};
FS.Transform.prototype.createReadStream = function(fileObj, options) {
var self = this;
// Get the file key
var fileKey = self.storage.fileKey(fileObj);
// Rig read stream
var sourceStream = self.storage.createReadStreamForFileKey(fileKey, options);
// Pass through transformRead function if provided
if (typeof self.transformRead === 'function') {
sourceStream = addPassThrough(sourceStream, function (ptStream, originalStream) {
// Rig transform
try {
self.transformRead.call(FS.Transform.scope, fileObj, originalStream, ptStream);
} catch(err) {
//throw new Error(err);
// We emit an error - should we throw an error?
sourceStream.emit('error', 'FS.Transform.createReadStream transform function failed');
}
});
}
// We dont transform just normal SA interface
return sourceStream;
};
// Utility function to simplify adding layers of passthrough
function addPassThrough(stream, func) {
var pts = new PassThrough();
// We pass on the special "stored" event for those listening
stream.on('stored', function(result) {
pts.emit('stored', result);
});
func(pts, stream);
return pts;
}