summaryrefslogtreecommitdiffstats
path: root/fix-download-unicode
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2017-05-07 21:51:11 +0300
committerLauri Ojansivu <x@xet7.org>2017-05-07 21:51:11 +0300
commitf88d68a9c3a24bcfd98da8e58ece2fef9694f77b (patch)
tree4f54f7116b9d7210835b4d4a451885073bec8a78 /fix-download-unicode
parentdd898f40337fc5128647befbc47aaa6cd3bf3bcb (diff)
downloadwekan-f88d68a9c3a24bcfd98da8e58ece2fef9694f77b.tar.gz
wekan-f88d68a9c3a24bcfd98da8e58ece2fef9694f77b.tar.bz2
wekan-f88d68a9c3a24bcfd98da8e58ece2fef9694f77b.zip
Fix: Download file(unicode filename) cause crash with exception. Closes #784
Diffstat (limited to 'fix-download-unicode')
-rw-r--r--fix-download-unicode/cfs_access-point.js914
1 files changed, 914 insertions, 0 deletions
diff --git a/fix-download-unicode/cfs_access-point.js b/fix-download-unicode/cfs_access-point.js
new file mode 100644
index 00000000..4e80d94c
--- /dev/null
+++ b/fix-download-unicode/cfs_access-point.js
@@ -0,0 +1,914 @@
+(function () {
+
+/* Imports */
+var Meteor = Package.meteor.Meteor;
+var global = Package.meteor.global;
+var meteorEnv = Package.meteor.meteorEnv;
+var FS = Package['cfs:base-package'].FS;
+var check = Package.check.check;
+var Match = Package.check.Match;
+var EJSON = Package.ejson.EJSON;
+var HTTP = Package['cfs:http-methods'].HTTP;
+
+/* Package-scope variables */
+var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
+
+(function(){
+
+///////////////////////////////////////////////////////////////////////
+// //
+// packages/cfs_access-point/packages/cfs_access-point.js //
+// //
+///////////////////////////////////////////////////////////////////////
+ //
+(function () {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+// packages/cfs:access-point/access-point-common.js //
+// //
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1
+// Adjust the rootUrlPathPrefix if necessary // 2
+if (rootUrlPathPrefix.length > 0) { // 3
+ if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4
+ rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5
+ } // 6
+ if (rootUrlPathPrefix.slice(-1) === '/') { // 7
+ rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8
+ } // 9
+} // 10
+ // 11
+// prepend ROOT_URL when isCordova // 12
+if (Meteor.isCordova) { // 13
+ rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14
+} // 15
+ // 16
+baseUrl = '/cfs'; // 17
+FS.HTTP = FS.HTTP || {}; // 18
+ // 19
+// Note the upload URL so that client uploader packages know what it is // 20
+FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21
+ // 22
+/** // 23
+ * @method FS.HTTP.setBaseUrl // 24
+ * @public // 25
+ * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26
+ * @returns {undefined} // 27
+ */ // 28
+FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29
+ // 30
+ // Adjust the baseUrl if necessary // 31
+ if (newBaseUrl.slice(0, 1) !== '/') { // 32
+ newBaseUrl = '/' + newBaseUrl; // 33
+ } // 34
+ if (newBaseUrl.slice(-1) === '/') { // 35
+ newBaseUrl = newBaseUrl.slice(0, -1); // 36
+ } // 37
+ // 38
+ // Update the base URL // 39
+ baseUrl = newBaseUrl; // 40
+ // 41
+ // Change the upload URL so that client uploader packages know what it is // 42
+ FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43
+ // 44
+ // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45
+ // If existingMountPoints is empty, then we haven't run the server startup // 46
+ // code yet, so this new URL will be used at that point for the initial mount. // 47
+ if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48
+ mountUrls(); // 49
+ } // 50
+}; // 51
+ // 52
+/* // 53
+ * FS.File extensions // 54
+ */ // 55
+ // 56
+/** // 57
+ * @method FS.File.prototype.url Construct the file url // 58
+ * @public // 59
+ * @param {Object} [options] // 60
+ * @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
+ * @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
+ * @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
+ * @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
+ * @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
+ * @param {String} [options.uploading=null] A URL to return while the file is being uploaded. // 66
+ * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67
+ * @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
+ * // 69
+ * Returns the HTTP URL for getting the file or its metadata. // 70
+ */ // 71
+FS.File.prototype.url = function(options) { // 72
+ var self = this; // 73
+ options = options || {}; // 74
+ options = FS.Utility.extend({ // 75
+ store: null, // 76
+ auth: null, // 77
+ download: false, // 78
+ metadata: false, // 79
+ brokenIsFine: false, // 80
+ uploading: null, // return this URL while uploading // 81
+ storing: null, // return this URL while storing // 82
+ filename: null // override the filename that is shown to the user // 83
+ }, options.hash || options); // check for "hash" prop if called as helper // 84
+ // 85
+ // Primarily useful for displaying a temporary image while uploading an image // 86
+ if (options.uploading && !self.isUploaded()) { // 87
+ return options.uploading; // 88
+ } // 89
+ // 90
+ if (self.isMounted()) { // 91
+ // See if we've stored in the requested store yet // 92
+ var storeName = options.store || self.collection.primaryStore.name; // 93
+ if (!self.hasStored(storeName)) { // 94
+ if (options.storing) { // 95
+ return options.storing; // 96
+ } else if (!options.brokenIsFine) { // 97
+ // We want to return null if we know the URL will be a broken // 98
+ // link because then we can avoid rendering broken links, broken // 99
+ // images, etc. // 100
+ return null; // 101
+ } // 102
+ } // 103
+ // 104
+ // Add filename to end of URL if we can determine one // 105
+ var filename = options.filename || self.name({store: storeName}); // 106
+ if (typeof filename === "string" && filename.length) { // 107
+ filename = '/' + filename; // 108
+ } else { // 109
+ filename = ''; // 110
+ } // 111
+ // 112
+ // TODO: Could we somehow figure out if the collection requires login? // 113
+ var authToken = ''; // 114
+ if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115
+ if (options.auth !== false) { // 116
+ // Add reactive deps on the user // 117
+ Meteor.userId(); // 118
+ // 119
+ var authObject = { // 120
+ authToken: Accounts._storedLoginToken() || '' // 121
+ }; // 122
+ // 123
+ // If it's a number, we use that as the expiration time (in seconds) // 124
+ if (options.auth === +options.auth) { // 125
+ authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126
+ } // 127
+ // 128
+ // Set the authToken // 129
+ var authString = JSON.stringify(authObject); // 130
+ authToken = FS.Utility.btoa(authString); // 131
+ } // 132
+ } else if (typeof options.auth === "string") { // 133
+ // If the user supplies auth token the user will be responsible for // 134
+ // updating // 135
+ authToken = options.auth; // 136
+ } // 137
+ // 138
+ // Construct query string // 139
+ var params = {}; // 140
+ if (authToken !== '') { // 141
+ params.token = authToken; // 142
+ } // 143
+ if (options.download) { // 144
+ params.download = true; // 145
+ } // 146
+ if (options.store) { // 147
+ // We use options.store here instead of storeName because we want to omit the queryString // 148
+ // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149
+ // assume the first store defined on the server, which means that we are assuming that // 150
+ // the first on the client is also the first on the server. If that's not the case, the // 151
+ // store option should be supplied. // 152
+ params.store = options.store; // 153
+ } // 154
+ var queryString = FS.Utility.encodeParams(params); // 155
+ if (queryString.length) { // 156
+ queryString = '?' + queryString; // 157
+ } // 158
+ // 159
+ // Determine which URL to use // 160
+ var area; // 161
+ if (options.metadata) { // 162
+ area = '/record'; // 163
+ } else { // 164
+ area = '/files'; // 165
+ } // 166
+ // 167
+ // Construct and return the http method url // 168
+ return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
+ } // 170
+ // 171
+}; // 172
+ // 173
+ // 174
+ // 175
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+}).call(this);
+
+
+
+
+
+
+(function () {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+// packages/cfs:access-point/access-point-handlers.js //
+// //
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+getHeaders = []; // 1
+getHeadersByCollection = {}; // 2
+ // 3
+FS.HTTP.Handlers = {}; // 4
+ // 5
+/** // 6
+ * @method FS.HTTP.Handlers.Del // 7
+ * @public // 8
+ * @returns {any} response // 9
+ * // 10
+ * HTTP DEL request handler // 11
+ */ // 12
+FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13
+ var self = this; // 14
+ var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15
+ // 16
+ // If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17
+ FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18
+ // 19
+ /* // 20
+ * From the DELETE spec: // 21
+ * A successful response SHOULD be 200 (OK) if the response includes an // 22
+ * entity describing the status, 202 (Accepted) if the action has not // 23
+ * yet been enacted, or 204 (No Content) if the action has been enacted // 24
+ * but the response does not include an entity. // 25
+ */ // 26
+ self.setStatusCode(200); // 27
+ // 28
+ return { // 29
+ deleted: !!ref.file.remove() // 30
+ }; // 31
+}; // 32
+ // 33
+/** // 34
+ * @method FS.HTTP.Handlers.GetList // 35
+ * @public // 36
+ * @returns {Object} response // 37
+ * // 38
+ * HTTP GET file list request handler // 39
+ */ // 40
+FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41
+ // Not Yet Implemented // 42
+ // Need to check publications and return file list based on // 43
+ // what user is allowed to see // 44
+}; // 45
+ // 46
+/* // 47
+ requestRange will parse the range set in request header - if not possible it // 48
+ will throw fitting errors and autofill range for both partial and full ranges // 49
+ // 50
+ throws error or returns the object: // 51
+ { // 52
+ start // 53
+ end // 54
+ length // 55
+ unit // 56
+ partial // 57
+ } // 58
+*/ // 59
+var requestRange = function(req, fileSize) { // 60
+ if (req) { // 61
+ if (req.headers) { // 62
+ var rangeString = req.headers.range; // 63
+ // 64
+ // Make sure range is a string // 65
+ if (rangeString === ''+rangeString) { // 66
+ // 67
+ // range will be in the format "bytes=0-32767" // 68
+ var parts = rangeString.split('='); // 69
+ var unit = parts[0]; // 70
+ // 71
+ // Make sure parts consists of two strings and range is of type "byte" // 72
+ if (parts.length == 2 && unit == 'bytes') { // 73
+ // Parse the range // 74
+ var range = parts[1].split('-'); // 75
+ var start = Number(range[0]); // 76
+ var end = Number(range[1]); // 77
+ // 78
+ // Fix invalid ranges? // 79
+ if (range[0] != start) start = 0; // 80
+ if (range[1] != end || !end) end = fileSize - 1; // 81
+ // 82
+ // Make sure range consists of a start and end point of numbers and start is less than end // 83
+ if (start < end) { // 84
+ // 85
+ var partSize = 0 - start + end + 1; // 86
+ // 87
+ // Return the parsed range // 88
+ return { // 89
+ start: start, // 90
+ end: end, // 91
+ length: partSize, // 92
+ size: fileSize, // 93
+ unit: unit, // 94
+ partial: (partSize < fileSize) // 95
+ }; // 96
+ // 97
+ } else { // 98
+ throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99
+ } // 100
+ // 101
+ } else { // 102
+ // The first part should be bytes // 103
+ throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104
+ } // 105
+ // 106
+ } else { // 107
+ // No range found // 108
+ } // 109
+ // 110
+ } else { // 111
+ // throw new Error('No request headers set for _parseRange function'); // 112
+ } // 113
+ } else { // 114
+ throw new Error('No request object passed to _parseRange function'); // 115
+ } // 116
+ // 117
+ return { // 118
+ start: 0, // 119
+ end: fileSize - 1, // 120
+ length: fileSize, // 121
+ size: fileSize, // 122
+ unit: 'bytes', // 123
+ partial: false // 124
+ }; // 125
+}; // 126
+ // 127
+/** // 128
+ * @method FS.HTTP.Handlers.Get // 129
+ * @public // 130
+ * @returns {any} response // 131
+ * // 132
+ * HTTP GET request handler // 133
+ */ // 134
+FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135
+ var self = this; // 136
+ // Once we have the file, we can test allow/deny validators // 137
+ // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138
+ FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139
+ // 140
+ var storeName = ref.storeName; // 141
+ // 142
+ // If no storeName was specified, use the first defined storeName // 143
+ if (typeof storeName !== "string") { // 144
+ // No store handed, we default to primary store // 145
+ storeName = ref.collection.primaryStore.name; // 146
+ } // 147
+ // 148
+ // Get the storage reference // 149
+ var storage = ref.collection.storesLookup[storeName]; // 150
+ // 151
+ if (!storage) { // 152
+ throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153
+ } // 154
+ // 155
+ // Get the file // 156
+ var copyInfo = ref.file.copies[storeName]; // 157
+ // 158
+ if (!copyInfo) { // 159
+ throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160
+ } // 161
+ // 162
+ // Set the content type for file // 163
+ if (typeof copyInfo.type === "string") { // 164
+ self.setContentType(copyInfo.type); // 165
+ } else { // 166
+ self.setContentType('application/octet-stream'); // 167
+ } // 168
+ // 169
+ // Add 'Content-Disposition' header if requested a download/attachment URL // 170
+ if (typeof ref.download !== "undefined") { // 171
+ var filename = ref.filename || copyInfo.name; // 172
+ self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173
+ } else { // 174
+ self.addHeader('Content-Disposition', 'inline'); // 175
+ } // 176
+ // 177
+ // Get the contents range from request // 178
+ var range = requestRange(self.request, copyInfo.size); // 179
+ // 180
+ // Some browsers cope better if the content-range header is // 181
+ // still included even for the full file being returned. // 182
+ self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183
+ // 184
+ // If a chunk/range was requested instead of the whole file, serve that' // 185
+ if (range.partial) { // 186
+ self.setStatusCode(206, 'Partial Content'); // 187
+ } else { // 188
+ self.setStatusCode(200, 'OK'); // 189
+ } // 190
+ // 191
+ // Add any other global custom headers and collection-specific custom headers // 192
+ FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193
+ self.addHeader(header[0], header[1]); // 194
+ }); // 195
+ // 196
+ // Inform clients about length (or chunk length in case of ranges) // 197
+ self.addHeader('Content-Length', range.length); // 198
+ // 199
+ // Last modified header (updatedAt from file info) // 200
+ self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201
+ // 202
+ // Inform clients that we accept ranges for resumable chunked downloads // 203
+ self.addHeader('Accept-Ranges', range.unit); // 204
+ // 205
+ if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
+ // 207
+ var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208
+ // 209
+ readStream.on('error', function(err) { // 210
+ // Send proper error message on get error // 211
+ if (err.message && err.statusCode) { // 212
+ self.Error(new Meteor.Error(err.statusCode, err.message)); // 213
+ } else { // 214
+ self.Error(new Meteor.Error(503, 'Service unavailable')); // 215
+ } // 216
+ }); // 217
+ // 218
+ readStream.pipe(self.createWriteStream()); // 219
+}; // 220
+
+const originalHandler = FS.HTTP.Handlers.Get;
+FS.HTTP.Handlers.Get = function (ref) {
+//console.log(ref.filename);
+ 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 = 'tempfix';
+ }
+ return originalHandler.call(this, ref);
+};
+ // 221
+/** // 222
+ * @method FS.HTTP.Handlers.PutInsert // 223
+ * @public // 224
+ * @returns {Object} response object with _id property // 225
+ * // 226
+ * HTTP PUT file insert request handler // 227
+ */ // 228
+FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229
+ var self = this; // 230
+ var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231
+ // 232
+ FS.debug && console.log("HTTP PUT (insert) handler"); // 233
+ // 234
+ // Create the nice FS.File // 235
+ var fileObj = new FS.File(); // 236
+ // 237
+ // Set its name // 238
+ fileObj.name(opts.filename || null); // 239
+ // 240
+ // Attach the readstream as the file's data // 241
+ fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
+ // 243
+ // Validate with insert allow/deny // 244
+ FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245
+ // 246
+ // Insert file into collection, triggering readStream storage // 247
+ ref.collection.insert(fileObj); // 248
+ // 249
+ // Send response // 250
+ self.setStatusCode(200); // 251
+ // 252
+ // Return the new file id // 253
+ return {_id: fileObj._id}; // 254
+}; // 255
+ // 256
+/** // 257
+ * @method FS.HTTP.Handlers.PutUpdate // 258
+ * @public // 259
+ * @returns {Object} response object with _id and chunk properties // 260
+ * // 261
+ * HTTP PUT file update chunk request handler // 262
+ */ // 263
+FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264
+ var self = this; // 265
+ var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266
+ // 267
+ var chunk = parseInt(opts.chunk, 10); // 268
+ if (isNaN(chunk)) chunk = 0; // 269
+ // 270
+ FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271
+ // 272
+ // Validate with insert allow/deny; also mounts and retrieves the file // 273
+ FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274
+ // 275
+ self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276
+ // 277
+ // Send response // 278
+ self.setStatusCode(200); // 279
+ // 280
+ return { _id: ref.file._id, chunk: chunk }; // 281
+}; // 282
+ // 283
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+}).call(this);
+
+
+
+
+
+
+(function () {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+// packages/cfs:access-point/access-point-server.js //
+// //
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+var path = Npm.require("path"); // 1
+ // 2
+HTTP.publishFormats({ // 3
+ fileRecordFormat: function (input) { // 4
+ // Set the method scope content type to json // 5
+ this.setContentType('application/json'); // 6
+ if (FS.Utility.isArray(input)) { // 7
+ return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8
+ return FS.Utility.cloneFileRecord(obj); // 9
+ })); // 10
+ } else { // 11
+ return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12
+ } // 13
+ } // 14
+}); // 15
+ // 16
+/** // 17
+ * @method FS.HTTP.setHeadersForGet // 18
+ * @public // 19
+ * @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
+ * @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
+ * @returns {undefined} // 22
+ */ // 23
+FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24
+ if (typeof collections === "string") { // 25
+ collections = [collections]; // 26
+ } // 27
+ if (collections) { // 28
+ FS.Utility.each(collections, function(collectionName) { // 29
+ getHeadersByCollection[collectionName] = headers || []; // 30
+ }); // 31
+ } else { // 32
+ getHeaders = headers || []; // 33
+ } // 34
+}; // 35
+ // 36
+/** // 37
+ * @method FS.HTTP.publish // 38
+ * @public // 39
+ * @param {FS.Collection} collection // 40
+ * @param {Function} func - Publish function that returns a cursor. // 41
+ * @returns {undefined} // 42
+ * // 43
+ * Publishes all documents returned by the cursor at a GET URL // 44
+ * with the format baseUrl/record/collectionName. The publish // 45
+ * function `this` is similar to normal `Meteor.publish`. // 46
+ */ // 47
+FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48
+ var name = baseUrl + '/record/' + collection.name; // 49
+ // Mount collection listing URL using http-publish package // 50
+ HTTP.publish({ // 51
+ name: name, // 52
+ defaultFormat: 'fileRecordFormat', // 53
+ collection: collection, // 54
+ collectionGet: true, // 55
+ collectionPost: false, // 56
+ documentGet: true, // 57
+ documentPut: false, // 58
+ documentDelete: false // 59
+ }, func); // 60
+ // 61
+ FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62
+}; // 63
+ // 64
+/** // 65
+ * @method FS.HTTP.unpublish // 66
+ * @public // 67
+ * @param {FS.Collection} collection // 68
+ * @returns {undefined} // 69
+ * // 70
+ * Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71
+ */ // 72
+FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73
+ // Mount collection listing URL using http-publish package // 74
+ HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75
+}; // 76
+ // 77
+_existingMountPoints = {}; // 78
+ // 79
+/** // 80
+ * @method defaultSelectorFunction // 81
+ * @private // 82
+ * @returns { collection, file } // 83
+ * // 84
+ * This is the default selector function // 85
+ */ // 86
+var defaultSelectorFunction = function() { // 87
+ var self = this; // 88
+ // Selector function // 89
+ // // 90
+ // This function will have to return the collection and the // 91
+ // file. If file not found undefined is returned - if null is returned the // 92
+ // search was not possible // 93
+ var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94
+ // 95
+ // Get the collection name from the url // 96
+ var collectionName = opts.collectionName; // 97
+ // 98
+ // Get the id from the url // 99
+ var id = opts.id; // 100
+ // 101
+ // Get the collection // 102
+ var collection = FS._collections[collectionName]; // 103
+ // 104
+ // Get the file if possible else return null // 105
+ var file = (id && collection)? collection.findOne({ _id: id }): null; // 106
+ // 107
+ // Return the collection and the file // 108
+ return { // 109
+ collection: collection, // 110
+ file: file, // 111
+ storeName: opts.store, // 112
+ download: opts.download, // 113
+ filename: opts.filename // 114
+ }; // 115
+}; // 116
+ // 117
+/* // 118
+ * @method FS.HTTP.mount // 119
+ * @public // 120
+ * @param {array of string} mountPoints mount points to map rest functinality on // 121
+ * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122
+ * // 123
+*/ // 124
+FS.HTTP.mount = function(mountPoints, selector_f) { // 125
+ // We take mount points as an array and we get a selector function // 126
+ var selectorFunction = selector_f || defaultSelectorFunction; // 127
+ // 128
+ var accessPoint = { // 129
+ 'stream': true, // 130
+ 'auth': expirationAuth, // 131
+ 'post': function(data) { // 132
+ // Use the selector for finding the collection and file reference // 133
+ var ref = selectorFunction.call(this); // 134
+ // 135
+ // We dont support post - this would be normal insert eg. of filerecord? // 136
+ throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137
+ }, // 138
+ 'put': function(data) { // 139
+ // Use the selector for finding the collection and file reference // 140
+ var ref = selectorFunction.call(this); // 141
+ // 142
+ // Make sure we have a collection reference // 143
+ if (!ref.collection) // 144
+ throw new Meteor.Error(404, "Not Found", "No collection found"); // 145
+ // 146
+ // Make sure we have a file reference // 147
+ if (ref.file === null) { // 148
+ // No id supplied so we will create a new FS.File instance and // 149
+ // insert the supplied data. // 150
+ return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151
+ } else { // 152
+ if (ref.file) { // 153
+ return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154
+ } else { // 155
+ throw new Meteor.Error(404, "Not Found", 'No file found'); // 156
+ } // 157
+ } // 158
+ }, // 159
+ 'get': function(data) { // 160
+ // Use the selector for finding the collection and file reference // 161
+ var ref = selectorFunction.call(this); // 162
+ // 163
+ // Make sure we have a collection reference // 164
+ if (!ref.collection) // 165
+ throw new Meteor.Error(404, "Not Found", "No collection found"); // 166
+ // 167
+ // Make sure we have a file reference // 168
+ if (ref.file === null) { // 169
+ // No id supplied so we will return the published list of files ala // 170
+ // http.publish in json format // 171
+ return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172
+ } else { // 173
+ if (ref.file) { // 174
+ return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175
+ } else { // 176
+ throw new Meteor.Error(404, "Not Found", 'No file found'); // 177
+ } // 178
+ } // 179
+ }, // 180
+ 'delete': function(data) { // 181
+ // Use the selector for finding the collection and file reference // 182
+ var ref = selectorFunction.call(this); // 183
+ // 184
+ // Make sure we have a collection reference // 185
+ if (!ref.collection) // 186
+ throw new Meteor.Error(404, "Not Found", "No collection found"); // 187
+ // 188
+ // Make sure we have a file reference // 189
+ if (ref.file) { // 190
+ return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191
+ } else { // 192
+ throw new Meteor.Error(404, "Not Found", 'No file found'); // 193
+ } // 194
+ } // 195
+ }; // 196
+ // 197
+ var accessPoints = {}; // 198
+ // 199
+ // Add debug message // 200
+ FS.debug && console.log('Registered HTTP method URLs:'); // 201
+ // 202
+ FS.Utility.each(mountPoints, function(mountPoint) { // 203
+ // Couple mountpoint and accesspoint // 204
+ accessPoints[mountPoint] = accessPoint; // 205
+ // Remember our mountpoints // 206
+ _existingMountPoints[mountPoint] = mountPoint; // 207
+ // Add debug message // 208
+ FS.debug && console.log(mountPoint); // 209
+ }); // 210
+ // 211
+ // XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212
+ HTTP.methods(accessPoints); // 213
+ // 214
+}; // 215
+ // 216
+/** // 217
+ * @method FS.HTTP.unmount // 218
+ * @public // 219
+ * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220
+ * // 221
+ */ // 222
+FS.HTTP.unmount = function(mountPoints) { // 223
+ // The mountPoints is optional, can be string or array if undefined then // 224
+ // _existingMountPoints will be used // 225
+ var unmountList; // 226
+ // Container for the mount points to unmount // 227
+ var unmountPoints = {}; // 228
+ // 229
+ if (typeof mountPoints === 'undefined') { // 230
+ // Use existing mount points - unmount all // 231
+ unmountList = _existingMountPoints; // 232
+ } else if (mountPoints === ''+mountPoints) { // 233
+ // Got a string // 234
+ unmountList = [mountPoints]; // 235
+ } else if (mountPoints.length) { // 236
+ // Got an array // 237
+ unmountList = mountPoints; // 238
+ } // 239
+ // 240
+ // If we have a list to unmount // 241
+ if (unmountList) { // 242
+ // Iterate over each item // 243
+ FS.Utility.each(unmountList, function(mountPoint) { // 244
+ // Check _existingMountPoints to make sure the mount point exists in our // 245
+ // context / was created by the FS.HTTP.mount // 246
+ if (_existingMountPoints[mountPoint]) { // 247
+ // Mark as unmount // 248
+ unmountPoints[mountPoint] = false; // 249
+ // Release // 250
+ delete _existingMountPoints[mountPoint]; // 251
+ } // 252
+ }); // 253
+ FS.debug && console.log('FS.HTTP.unmount:'); // 254
+ FS.debug && console.log(unmountPoints); // 255
+ // Complete unmount // 256
+ HTTP.methods(unmountPoints); // 257
+ } // 258
+}; // 259
+ // 260
+// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261
+// * // 262
+// baseUrl + '/files/:collectionName/:id/:filename', // 263
+// baseUrl + '/files/:collectionName/:id', // 264
+// baseUrl + '/files/:collectionName' // 265
+// // 266
+// Change/ replace the existing mount point by: // 267
+// ```js // 268
+// // unmount all existing // 269
+// FS.HTTP.unmount(); // 270
+// // Create new mount point // 271
+// FS.HTTP.mount([ // 272
+// '/cfs/files/:collectionName/:id/:filename', // 273
+// '/cfs/files/:collectionName/:id', // 274
+// '/cfs/files/:collectionName' // 275
+// ]); // 276
+// ``` // 277
+// // 278
+mountUrls = function mountUrls() { // 279
+ // We unmount first in case we are calling this a second time // 280
+ FS.HTTP.unmount(); // 281
+ // 282
+ FS.HTTP.mount([ // 283
+ baseUrl + '/files/:collectionName/:id/:filename', // 284
+ baseUrl + '/files/:collectionName/:id', // 285
+ baseUrl + '/files/:collectionName' // 286
+ ]); // 287
+}; // 288
+ // 289
+// Returns the userId from URL token // 290
+var expirationAuth = function expirationAuth() { // 291
+ var self = this; // 292
+ // 293
+ // Read the token from '/hello?token=base64' // 294
+ var encodedToken = self.query.token; // 295
+ // 296
+ FS.debug && console.log("token: "+encodedToken); // 297
+ // 298
+ if (!encodedToken || !Meteor.users) return false; // 299
+ // 300
+ // Check the userToken before adding it to the db query // 301
+ // Set the this.userId // 302
+ var tokenString = FS.Utility.atob(encodedToken); // 303
+ // 304
+ var tokenObject; // 305
+ try { // 306
+ tokenObject = JSON.parse(tokenString); // 307
+ } catch(err) { // 308
+ throw new Meteor.Error(400, 'Bad Request'); // 309
+ } // 310
+ // 311
+ // XXX: Do some check here of the object // 312
+ var userToken = tokenObject.authToken; // 313
+ if (userToken !== ''+userToken) { // 314
+ throw new Meteor.Error(400, 'Bad Request'); // 315
+ } // 316
+ // 317
+ // If we have an expiration token we should check that it's still valid // 318
+ if (tokenObject.expiration != null) { // 319
+ // check if its too old // 320
+ var now = Date.now(); // 321
+ if (tokenObject.expiration < now) { // 322
+ FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323
+ throw new Meteor.Error(500, 'Expired token'); // 324
+ } // 325
+ } // 326
+ // 327
+ // We are not on a secure line - so we have to look up the user... // 328
+ var user = Meteor.users.findOne({ // 329
+ $or: [ // 330
+ {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331
+ {'services.resume.loginTokens.token': userToken} // 332
+ ] // 333
+ }); // 334
+ // 335
+ // Set the userId in the scope // 336
+ return user && user._id; // 337
+}; // 338
+ // 339
+HTTP.methods( // 340
+ {'/cfs/servertime': { // 341
+ get: function(data) { // 342
+ return Date.now().toString(); // 343
+ } // 344
+ } // 345
+}); // 346
+ // 347
+// Unify client / server api // 348
+FS.HTTP.now = function() { // 349
+ return Date.now(); // 350
+}; // 351
+ // 352
+// Start up the basic mount points // 353
+Meteor.startup(function () { // 354
+ mountUrls(); // 355
+}); // 356
+ // 357
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+}).call(this);
+
+///////////////////////////////////////////////////////////////////////
+
+}).call(this);
+
+
+/* Exports */
+if (typeof Package === 'undefined') Package = {};
+Package['cfs:access-point'] = {};
+
+})();