mirror of
https://github.com/wekan/wekan.git
synced 2025-12-19 17:00:13 +01:00
Fixed Non-ASCII attachment filename will crash when downloading.
Thanks to xet7 ! Fixes #2759
This commit is contained in:
parent
843ff8eaaa
commit
c2da477735
277 changed files with 30568 additions and 52 deletions
307
packages/wekan-cfs-access-point/access-point-handlers.js
Normal file
307
packages/wekan-cfs-access-point/access-point-handlers.js
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
getHeaders = [];
|
||||
getHeadersByCollection = {};
|
||||
|
||||
var contentDisposition = Npm.require('content-disposition');
|
||||
|
||||
FS.HTTP.Handlers = {};
|
||||
|
||||
/**
|
||||
* @method FS.HTTP.Handlers.Del
|
||||
* @public
|
||||
* @returns {any} response
|
||||
*
|
||||
* HTTP DEL request handler
|
||||
*/
|
||||
FS.HTTP.Handlers.Del = function httpDelHandler(ref) {
|
||||
var self = this;
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
|
||||
|
||||
// If DELETE request, validate with 'remove' allow/deny, delete the file, and return
|
||||
FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId);
|
||||
|
||||
/*
|
||||
* From the DELETE spec:
|
||||
* A successful response SHOULD be 200 (OK) if the response includes an
|
||||
* entity describing the status, 202 (Accepted) if the action has not
|
||||
* yet been enacted, or 204 (No Content) if the action has been enacted
|
||||
* but the response does not include an entity.
|
||||
*/
|
||||
self.setStatusCode(200);
|
||||
|
||||
return {
|
||||
deleted: !!ref.file.remove()
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @method FS.HTTP.Handlers.GetList
|
||||
* @public
|
||||
* @returns {Object} response
|
||||
*
|
||||
* HTTP GET file list request handler
|
||||
*/
|
||||
FS.HTTP.Handlers.GetList = function httpGetListHandler() {
|
||||
// Not Yet Implemented
|
||||
// Need to check publications and return file list based on
|
||||
// what user is allowed to see
|
||||
};
|
||||
|
||||
/*
|
||||
requestRange will parse the range set in request header - if not possible it
|
||||
will throw fitting errors and autofill range for both partial and full ranges
|
||||
|
||||
throws error or returns the object:
|
||||
{
|
||||
start
|
||||
end
|
||||
length
|
||||
unit
|
||||
partial
|
||||
}
|
||||
*/
|
||||
var requestRange = function(req, fileSize) {
|
||||
if (req) {
|
||||
if (req.headers) {
|
||||
var rangeString = req.headers.range;
|
||||
|
||||
// Make sure range is a string
|
||||
if (rangeString === ''+rangeString) {
|
||||
|
||||
// range will be in the format "bytes=0-32767"
|
||||
var parts = rangeString.split('=');
|
||||
var unit = parts[0];
|
||||
|
||||
// Make sure parts consists of two strings and range is of type "byte"
|
||||
if (parts.length == 2 && unit == 'bytes') {
|
||||
// Parse the range
|
||||
var range = parts[1].split('-');
|
||||
var start = Number(range[0]);
|
||||
var end = Number(range[1]);
|
||||
|
||||
// Fix invalid ranges?
|
||||
if (range[0] != start) start = 0;
|
||||
if (range[1] != end || !end) end = fileSize - 1;
|
||||
|
||||
// Make sure range consists of a start and end point of numbers and start is less than end
|
||||
if (start < end) {
|
||||
|
||||
var partSize = 0 - start + end + 1;
|
||||
|
||||
// Return the parsed range
|
||||
return {
|
||||
start: start,
|
||||
end: end,
|
||||
length: partSize,
|
||||
size: fileSize,
|
||||
unit: unit,
|
||||
partial: (partSize < fileSize)
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new Meteor.Error(416, "Requested Range Not Satisfiable");
|
||||
}
|
||||
|
||||
} else {
|
||||
// The first part should be bytes
|
||||
throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable");
|
||||
}
|
||||
|
||||
} else {
|
||||
// No range found
|
||||
}
|
||||
|
||||
} else {
|
||||
// throw new Error('No request headers set for _parseRange function');
|
||||
}
|
||||
} else {
|
||||
throw new Error('No request object passed to _parseRange function');
|
||||
}
|
||||
|
||||
return {
|
||||
start: 0,
|
||||
end: fileSize - 1,
|
||||
length: fileSize,
|
||||
size: fileSize,
|
||||
unit: 'bytes',
|
||||
partial: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @method FS.HTTP.Handlers.Get
|
||||
* @public
|
||||
* @returns {any} response
|
||||
*
|
||||
* HTTP GET request handler
|
||||
*/
|
||||
FS.HTTP.Handlers.Get = function httpGetHandler(ref) {
|
||||
var self = this;
|
||||
// Once we have the file, we can test allow/deny validators
|
||||
// XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access?
|
||||
FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/);
|
||||
|
||||
var storeName = ref.storeName;
|
||||
|
||||
// If no storeName was specified, use the first defined storeName
|
||||
if (typeof storeName !== "string") {
|
||||
// No store handed, we default to primary store
|
||||
storeName = ref.collection.primaryStore.name;
|
||||
}
|
||||
|
||||
// Get the storage reference
|
||||
var storage = ref.collection.storesLookup[storeName];
|
||||
|
||||
if (!storage) {
|
||||
throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"');
|
||||
}
|
||||
|
||||
// Get the file
|
||||
var copyInfo = ref.file.copies[storeName];
|
||||
|
||||
if (!copyInfo) {
|
||||
throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store');
|
||||
}
|
||||
|
||||
// Set the content type for file
|
||||
if (typeof copyInfo.type === "string") {
|
||||
self.setContentType(copyInfo.type);
|
||||
} else {
|
||||
self.setContentType('application/octet-stream');
|
||||
}
|
||||
|
||||
// Add 'Content-Disposition' header if requested a download/attachment URL
|
||||
if (typeof ref.download !== "undefined") {
|
||||
var filename = ref.filename || copyInfo.name;
|
||||
self.addHeader('Content-Disposition', contentDisposition(filename));
|
||||
} else {
|
||||
self.addHeader('Content-Disposition', 'inline');
|
||||
}
|
||||
|
||||
// Get the contents range from request
|
||||
var range = requestRange(self.request, copyInfo.size);
|
||||
|
||||
// Some browsers cope better if the content-range header is
|
||||
// still included even for the full file being returned.
|
||||
self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
|
||||
|
||||
// If a chunk/range was requested instead of the whole file, serve that'
|
||||
if (range.partial) {
|
||||
self.setStatusCode(206, 'Partial Content');
|
||||
} else {
|
||||
self.setStatusCode(200, 'OK');
|
||||
}
|
||||
|
||||
// Add any other global custom headers and collection-specific custom headers
|
||||
FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) {
|
||||
self.addHeader(header[0], header[1]);
|
||||
});
|
||||
|
||||
// Inform clients about length (or chunk length in case of ranges)
|
||||
self.addHeader('Content-Length', range.length);
|
||||
|
||||
// Last modified header (updatedAt from file info)
|
||||
self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString());
|
||||
|
||||
// Inform clients that we accept ranges for resumable chunked downloads
|
||||
self.addHeader('Accept-Ranges', range.unit);
|
||||
|
||||
if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
|
||||
|
||||
var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end});
|
||||
|
||||
readStream.on('error', function(err) {
|
||||
// Send proper error message on get error
|
||||
if (err.message && err.statusCode) {
|
||||
self.Error(new Meteor.Error(err.statusCode, err.message));
|
||||
} else {
|
||||
self.Error(new Meteor.Error(503, 'Service unavailable'));
|
||||
}
|
||||
});
|
||||
|
||||
readStream.pipe(self.createWriteStream());
|
||||
};
|
||||
|
||||
// File with unicode or other encodings filename can upload to server susscessfully,
|
||||
// but when download, the HTTP header "Content-Disposition" cannot accept
|
||||
// characters other than ASCII, the filename should be converted to binary or URI encoded.
|
||||
// https://github.com/wekan/wekan/issues/784
|
||||
const originalHandler = FS.HTTP.Handlers.Get;
|
||||
FS.HTTP.Handlers.Get = function (ref) {
|
||||
try {
|
||||
var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
|
||||
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
|
||||
ref.filename = encodeURIComponent(ref.filename);
|
||||
} else if(userAgent.indexOf('firefox') >= 0) {
|
||||
ref.filename = new Buffer(ref.filename).toString('binary');
|
||||
} else {
|
||||
/* safari*/
|
||||
ref.filename = new Buffer(ref.filename).toString('binary');
|
||||
}
|
||||
} catch (ex){
|
||||
ref.filename = ref.filename;
|
||||
}
|
||||
return originalHandler.call(this, ref);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @method FS.HTTP.Handlers.PutInsert
|
||||
* @public
|
||||
* @returns {Object} response object with _id property
|
||||
*
|
||||
* HTTP PUT file insert request handler
|
||||
*/
|
||||
FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) {
|
||||
var self = this;
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
|
||||
|
||||
FS.debug && console.log("HTTP PUT (insert) handler");
|
||||
|
||||
// Create the nice FS.File
|
||||
var fileObj = new FS.File();
|
||||
|
||||
// Set its name
|
||||
fileObj.name(opts.filename || null);
|
||||
|
||||
// Attach the readstream as the file's data
|
||||
fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
|
||||
|
||||
// Validate with insert allow/deny
|
||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId);
|
||||
|
||||
// Insert file into collection, triggering readStream storage
|
||||
ref.collection.insert(fileObj);
|
||||
|
||||
// Send response
|
||||
self.setStatusCode(200);
|
||||
|
||||
// Return the new file id
|
||||
return {_id: fileObj._id};
|
||||
};
|
||||
|
||||
/**
|
||||
* @method FS.HTTP.Handlers.PutUpdate
|
||||
* @public
|
||||
* @returns {Object} response object with _id and chunk properties
|
||||
*
|
||||
* HTTP PUT file update chunk request handler
|
||||
*/
|
||||
FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {
|
||||
var self = this;
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
|
||||
|
||||
var chunk = parseInt(opts.chunk, 10);
|
||||
if (isNaN(chunk)) chunk = 0;
|
||||
|
||||
FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk);
|
||||
|
||||
// Validate with insert allow/deny; also mounts and retrieves the file
|
||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId);
|
||||
|
||||
self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) );
|
||||
|
||||
// Send response
|
||||
self.setStatusCode(200);
|
||||
|
||||
return { _id: ref.file._id, chunk: chunk };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue