summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.snap-meteor-1.8/cfs_access-point.txt914
-rw-r--r--.snap-meteor-1.8/export.js238
-rw-r--r--.snap-meteor-1.8/ldap.js640
-rw-r--r--.snap-meteor-1.8/oidc_server.js163
-rw-r--r--.snap-meteor-1.8/snapcraft.yaml244
-rw-r--r--.snap-meteor-1.8/wekanCreator.js853
-rw-r--r--snapcraft.yaml6
7 files changed, 3058 insertions, 0 deletions
diff --git a/.snap-meteor-1.8/cfs_access-point.txt b/.snap-meteor-1.8/cfs_access-point.txt
new file mode 100644
index 00000000..8e3359d0
--- /dev/null
+++ b/.snap-meteor-1.8/cfs_access-point.txt
@@ -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('trident') >= 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'] = {};
+
+})();
diff --git a/.snap-meteor-1.8/export.js b/.snap-meteor-1.8/export.js
new file mode 100644
index 00000000..cc979ce0
--- /dev/null
+++ b/.snap-meteor-1.8/export.js
@@ -0,0 +1,238 @@
+/* global JsonRoutes */
+if (Meteor.isServer) {
+ // todo XXX once we have a real API in place, move that route there
+ // todo XXX also share the route definition between the client and the server
+ // so that we could use something like
+ // `ApiRoutes.path('boards/export', boardId)``
+ // on the client instead of copy/pasting the route path manually between the
+ // client and the server.
+ /**
+ * @operation export
+ * @tag Boards
+ *
+ * @summary This route is used to export the board.
+ *
+ * @description If user is already logged-in, pass loginToken as param
+ * "authToken": '/api/boards/:boardId/export?authToken=:token'
+ *
+ * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
+ * for detailed explanations
+ *
+ * @param {string} boardId the ID of the board we are exporting
+ * @param {string} authToken the loginToken
+ */
+ JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
+ const boardId = req.params.boardId;
+ let user = null;
+
+ const loginToken = req.query.authToken;
+ if (loginToken) {
+ const hashToken = Accounts._hashLoginToken(loginToken);
+ user = Meteor.users.findOne({
+ 'services.resume.loginTokens.hashedToken': hashToken,
+ });
+ } else if (!Meteor.settings.public.sandstorm) {
+ Authentication.checkUserId(req.userId);
+ user = Users.findOne({ _id: req.userId, isAdmin: true });
+ }
+
+ const exporter = new Exporter(boardId);
+ if (exporter.canExport(user)) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: exporter.build(),
+ });
+ } else {
+ // we could send an explicit error message, but on the other hand the only
+ // way to get there is by hacking the UI so let's keep it raw.
+ JsonRoutes.sendResult(res, 403);
+ }
+ });
+}
+
+// exporter maybe is broken since Gridfs introduced, add fs and path
+
+export class Exporter {
+ constructor(boardId) {
+ this._boardId = boardId;
+ }
+
+ build() {
+ const fs = Npm.require('fs');
+ const os = Npm.require('os');
+ const path = Npm.require('path');
+
+ const byBoard = { boardId: this._boardId };
+ const byBoardNoLinked = {
+ boardId: this._boardId,
+ linkedId: { $in: ['', null] },
+ };
+ // we do not want to retrieve boardId in related elements
+ const noBoardId = {
+ fields: {
+ boardId: 0,
+ },
+ };
+ const result = {
+ _format: 'wekan-board-1.0.0',
+ };
+ _.extend(
+ result,
+ Boards.findOne(this._boardId, {
+ fields: {
+ stars: 0,
+ },
+ }),
+ );
+ result.lists = Lists.find(byBoard, noBoardId).fetch();
+ result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
+ result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
+ result.customFields = CustomFields.find(
+ { boardIds: { $in: [this.boardId] } },
+ { fields: { boardId: 0 } },
+ ).fetch();
+ result.comments = CardComments.find(byBoard, noBoardId).fetch();
+ result.activities = Activities.find(byBoard, noBoardId).fetch();
+ result.rules = Rules.find(byBoard, noBoardId).fetch();
+ result.checklists = [];
+ result.checklistItems = [];
+ result.subtaskItems = [];
+ result.triggers = [];
+ result.actions = [];
+ result.cards.forEach(card => {
+ result.checklists.push(
+ ...Checklists.find({
+ cardId: card._id,
+ }).fetch(),
+ );
+ result.checklistItems.push(
+ ...ChecklistItems.find({
+ cardId: card._id,
+ }).fetch(),
+ );
+ result.subtaskItems.push(
+ ...Cards.find({
+ parentId: card._id,
+ }).fetch(),
+ );
+ });
+ result.rules.forEach(rule => {
+ result.triggers.push(
+ ...Triggers.find(
+ {
+ _id: rule.triggerId,
+ },
+ noBoardId,
+ ).fetch(),
+ );
+ result.actions.push(
+ ...Actions.find(
+ {
+ _id: rule.actionId,
+ },
+ noBoardId,
+ ).fetch(),
+ );
+ });
+
+ // [Old] for attachments we only export IDs and absolute url to original doc
+ // [New] Encode attachment to base64
+ const getBase64Data = function(doc, callback) {
+ let buffer = new Buffer(0);
+ // callback has the form function (err, res) {}
+ const tmpFile = path.join(
+ os.tmpdir(),
+ `tmpexport${process.pid}${Math.random()}`,
+ );
+ const tmpWriteable = fs.createWriteStream(tmpFile);
+ const readStream = doc.createReadStream();
+ readStream.on('data', function(chunk) {
+ buffer = Buffer.concat([buffer, chunk]);
+ });
+ readStream.on('error', function(err) {
+ callback(err, null);
+ });
+ readStream.on('end', function() {
+ // done
+ fs.unlink(tmpFile, () => {
+ //ignored
+ });
+ callback(null, buffer.toString('base64'));
+ });
+ readStream.pipe(tmpWriteable);
+ };
+ const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
+ result.attachments = Attachments.find(byBoard)
+ .fetch()
+ .map(attachment => {
+ return {
+ _id: attachment._id,
+ cardId: attachment.cardId,
+ // url: FlowRouter.url(attachment.url()),
+ file: getBase64DataSync(attachment),
+ name: attachment.original.name,
+ type: attachment.original.type,
+ };
+ });
+
+ // we also have to export some user data - as the other elements only
+ // include id but we have to be careful:
+ // 1- only exports users that are linked somehow to that board
+ // 2- do not export any sensitive information
+ const users = {};
+ result.members.forEach(member => {
+ users[member.userId] = true;
+ });
+ result.lists.forEach(list => {
+ users[list.userId] = true;
+ });
+ result.cards.forEach(card => {
+ users[card.userId] = true;
+ if (card.members) {
+ card.members.forEach(memberId => {
+ users[memberId] = true;
+ });
+ }
+ });
+ result.comments.forEach(comment => {
+ users[comment.userId] = true;
+ });
+ result.activities.forEach(activity => {
+ users[activity.userId] = true;
+ });
+ result.checklists.forEach(checklist => {
+ users[checklist.userId] = true;
+ });
+ const byUserIds = {
+ _id: {
+ $in: Object.getOwnPropertyNames(users),
+ },
+ };
+ // we use whitelist to be sure we do not expose inadvertently
+ // some secret fields that gets added to User later.
+ const userFields = {
+ fields: {
+ _id: 1,
+ username: 1,
+ 'profile.fullname': 1,
+ 'profile.initials': 1,
+ 'profile.avatarUrl': 1,
+ },
+ };
+ result.users = Users.find(byUserIds, userFields)
+ .fetch()
+ .map(user => {
+ // user avatar is stored as a relative url, we export absolute
+ if ((user.profile || {}).avatarUrl) {
+ user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
+ }
+ return user;
+ });
+ return result;
+ }
+
+ canExport(user) {
+ const board = Boards.findOne(this._boardId);
+ return board && board.isVisibleBy(user);
+ }
+}
diff --git a/.snap-meteor-1.8/ldap.js b/.snap-meteor-1.8/ldap.js
new file mode 100644
index 00000000..3b963823
--- /dev/null
+++ b/.snap-meteor-1.8/ldap.js
@@ -0,0 +1,640 @@
+import ldapjs from 'ldapjs';
+import util from 'util';
+import Bunyan from 'bunyan';
+import { log_debug, log_info, log_warn, log_error } from './logger';
+
+export default class LDAP {
+ constructor() {
+ this.ldapjs = ldapjs;
+
+ this.connected = false;
+
+ this.options = {
+ host: this.constructor.settings_get('LDAP_HOST'),
+ port: this.constructor.settings_get('LDAP_PORT'),
+ Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
+ timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
+ connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
+ idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
+ encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
+ ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
+ reject_unauthorized:
+ this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
+ Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
+ Authentication_UserDN: this.constructor.settings_get(
+ 'LDAP_AUTHENTIFICATION_USERDN',
+ ),
+ Authentication_Password: this.constructor.settings_get(
+ 'LDAP_AUTHENTIFICATION_PASSWORD',
+ ),
+ Authentication_Fallback: this.constructor.settings_get(
+ 'LDAP_LOGIN_FALLBACK',
+ ),
+ BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
+ Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
+ User_Authentication: this.constructor.settings_get(
+ 'LDAP_USER_AUTHENTICATION',
+ ),
+ User_Authentication_Field: this.constructor.settings_get(
+ 'LDAP_USER_AUTHENTICATION_FIELD',
+ ),
+ User_Attributes: this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
+ User_Search_Filter: this.constructor.settings_get(
+ 'LDAP_USER_SEARCH_FILTER',
+ ),
+ User_Search_Scope: this.constructor.settings_get(
+ 'LDAP_USER_SEARCH_SCOPE',
+ ),
+ User_Search_Field: this.constructor.settings_get(
+ 'LDAP_USER_SEARCH_FIELD',
+ ),
+ Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
+ Search_Size_Limit: this.constructor.settings_get(
+ 'LDAP_SEARCH_SIZE_LIMIT',
+ ),
+ group_filter_enabled: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_ENABLE',
+ ),
+ group_filter_object_class: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_OBJECTCLASS',
+ ),
+ group_filter_group_id_attribute: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE',
+ ),
+ group_filter_group_member_attribute: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE',
+ ),
+ group_filter_group_member_format: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT',
+ ),
+ group_filter_group_name: this.constructor.settings_get(
+ 'LDAP_GROUP_FILTER_GROUP_NAME',
+ ),
+ };
+ }
+
+ static settings_get(name, ...args) {
+ let value = process.env[name];
+ if (value !== undefined) {
+ if (value === 'true' || value === 'false') {
+ value = JSON.parse(value);
+ } else if (value !== '' && !isNaN(value)) {
+ value = Number(value);
+ }
+ return value;
+ } else {
+ log_warn(`Lookup for unset variable: ${name}`);
+ }
+ }
+
+ connectSync(...args) {
+ if (!this._connectSync) {
+ this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
+ }
+ return this._connectSync(...args);
+ }
+
+ searchAllSync(...args) {
+ if (!this._searchAllSync) {
+ this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
+ }
+ return this._searchAllSync(...args);
+ }
+
+ connectAsync(callback) {
+ log_info('Init setup');
+
+ let replied = false;
+
+ const connectionOptions = {
+ url: `${this.options.host}:${this.options.port}`,
+ timeout: this.options.timeout,
+ connectTimeout: this.options.connect_timeout,
+ idleTimeout: this.options.idle_timeout,
+ reconnect: this.options.Reconnect,
+ };
+
+ if (this.options.Internal_Log_Level !== 'disabled') {
+ connectionOptions.log = new Bunyan({
+ name: 'ldapjs',
+ component: 'client',
+ stream: process.stderr,
+ level: this.options.Internal_Log_Level,
+ });
+ }
+
+ const tlsOptions = {
+ rejectUnauthorized: this.options.reject_unauthorized,
+ };
+
+ if (this.options.ca_cert && this.options.ca_cert !== '') {
+ // Split CA cert into array of strings
+ const chainLines = this.constructor
+ .settings_get('LDAP_CA_CERT')
+ .split('\n');
+ let cert = [];
+ const ca = [];
+ chainLines.forEach(line => {
+ cert.push(line);
+ if (line.match(/-END CERTIFICATE-/)) {
+ ca.push(cert.join('\n'));
+ cert = [];
+ }
+ });
+ tlsOptions.ca = ca;
+ }
+
+ if (this.options.encryption === 'ssl') {
+ connectionOptions.url = `ldaps://${connectionOptions.url}`;
+ connectionOptions.tlsOptions = tlsOptions;
+ } else {
+ connectionOptions.url = `ldap://${connectionOptions.url}`;
+ }
+
+ log_info('Connecting', connectionOptions.url);
+ log_debug(`connectionOptions${util.inspect(connectionOptions)}`);
+
+ this.client = ldapjs.createClient(connectionOptions);
+
+ this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
+
+ this.client.on('error', error => {
+ log_error('connection', error);
+ if (replied === false) {
+ replied = true;
+ callback(error, null);
+ }
+ });
+
+ this.client.on('idle', () => {
+ log_info('Idle');
+ this.disconnect();
+ });
+
+ this.client.on('close', () => {
+ log_info('Closed');
+ });
+
+ if (this.options.encryption === 'tls') {
+ // Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
+ // https://github.com/RocketChat/Rocket.Chat/issues/2035
+ // https://github.com/mcavage/node-ldapjs/issues/349
+ tlsOptions.host = this.options.host;
+
+ log_info('Starting TLS');
+ log_debug('tlsOptions', tlsOptions);
+
+ this.client.starttls(tlsOptions, null, (error, response) => {
+ if (error) {
+ log_error('TLS connection', error);
+ if (replied === false) {
+ replied = true;
+ callback(error, null);
+ }
+ return;
+ }
+
+ log_info('TLS connected');
+ this.connected = true;
+ if (replied === false) {
+ replied = true;
+ callback(null, response);
+ }
+ });
+ } else {
+ this.client.on('connect', response => {
+ log_info('LDAP connected');
+ this.connected = true;
+ if (replied === false) {
+ replied = true;
+ callback(null, response);
+ }
+ });
+ }
+
+ setTimeout(() => {
+ if (replied === false) {
+ log_error('connection time out', connectionOptions.connectTimeout);
+ replied = true;
+ callback(new Error('Timeout'));
+ }
+ }, connectionOptions.connectTimeout);
+ }
+
+ getUserFilter(username) {
+ const filter = [];
+
+ if (this.options.User_Search_Filter !== '') {
+ if (this.options.User_Search_Filter[0] === '(') {
+ filter.push(`${this.options.User_Search_Filter}`);
+ } else {
+ filter.push(`(${this.options.User_Search_Filter})`);
+ }
+ }
+
+ const usernameFilter = this.options.User_Search_Field.split(',').map(
+ item => `(${item}=${username})`,
+ );
+
+ if (usernameFilter.length === 0) {
+ log_error('LDAP_LDAP_User_Search_Field not defined');
+ } else if (usernameFilter.length === 1) {
+ filter.push(`${usernameFilter[0]}`);
+ } else {
+ filter.push(`(|${usernameFilter.join('')})`);
+ }
+
+ return `(&${filter.join('')})`;
+ }
+
+ bindUserIfNecessary(username, password) {
+ if (this.domainBinded === true) {
+ return;
+ }
+
+ if (!this.options.User_Authentication) {
+ return;
+ }
+
+ if (!this.options.BaseDN) throw new Error('BaseDN is not provided');
+
+ const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
+
+ this.bindSync(userDn, password);
+ this.domainBinded = true;
+ }
+
+ bindIfNecessary() {
+ if (this.domainBinded === true) {
+ return;
+ }
+
+ if (this.options.Authentication !== true) {
+ return;
+ }
+
+ log_info('Binding UserDN', this.options.Authentication_UserDN);
+
+ this.bindSync(
+ this.options.Authentication_UserDN,
+ this.options.Authentication_Password,
+ );
+ this.domainBinded = true;
+ }
+
+ searchUsersSync(username, page) {
+ this.bindIfNecessary();
+ const searchOptions = {
+ filter: this.getUserFilter(username),
+ scope: this.options.User_Search_Scope || 'sub',
+ sizeLimit: this.options.Search_Size_Limit,
+ };
+
+ if (!!this.options.User_Attributes)
+ searchOptions.attributes = this.options.User_Attributes.split(',');
+
+ if (this.options.Search_Page_Size > 0) {
+ searchOptions.paged = {
+ pageSize: this.options.Search_Page_Size,
+ pagePause: !!page,
+ };
+ }
+
+ log_info('Searching user', username);
+ log_debug('searchOptions', searchOptions);
+ log_debug('BaseDN', this.options.BaseDN);
+
+ if (page) {
+ return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
+ }
+
+ return this.searchAllSync(this.options.BaseDN, searchOptions);
+ }
+
+ getUserByIdSync(id, attribute) {
+ this.bindIfNecessary();
+
+ const Unique_Identifier_Field = this.constructor
+ .settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD')
+ .split(',');
+
+ let filter;
+
+ if (attribute) {
+ filter = new this.ldapjs.filters.EqualityFilter({
+ attribute,
+ value: new Buffer(id, 'hex'),
+ });
+ } else {
+ const filters = [];
+ Unique_Identifier_Field.forEach(item => {
+ filters.push(
+ new this.ldapjs.filters.EqualityFilter({
+ attribute: item,
+ value: new Buffer(id, 'hex'),
+ }),
+ );
+ });
+
+ filter = new this.ldapjs.filters.OrFilter({ filters });
+ }
+
+ const searchOptions = {
+ filter,
+ scope: 'sub',
+ };
+
+ log_info('Searching by id', id);
+ log_debug('search filter', searchOptions.filter.toString());
+ log_debug('BaseDN', this.options.BaseDN);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return;
+ }
+
+ if (result.length > 1) {
+ log_error('Search by id', id, 'returned', result.length, 'records');
+ }
+
+ return result[0];
+ }
+
+ getUserByUsernameSync(username) {
+ this.bindIfNecessary();
+
+ const searchOptions = {
+ filter: this.getUserFilter(username),
+ scope: this.options.User_Search_Scope || 'sub',
+ };
+
+ log_info('Searching user', username);
+ log_debug('searchOptions', searchOptions);
+ log_debug('BaseDN', this.options.BaseDN);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return;
+ }
+
+ if (result.length > 1) {
+ log_error(
+ 'Search by username',
+ username,
+ 'returned',
+ result.length,
+ 'records',
+ );
+ }
+
+ return result[0];
+ }
+
+ getUserGroups(username, ldapUser) {
+ if (!this.options.group_filter_enabled) {
+ return true;
+ }
+
+ const filter = ['(&'];
+
+ if (this.options.group_filter_object_class !== '') {
+ filter.push(`(objectclass=${this.options.group_filter_object_class})`);
+ }
+
+ if (this.options.group_filter_group_member_attribute !== '') {
+ const format_value =
+ ldapUser[this.options.group_filter_group_member_format];
+ if (format_value) {
+ filter.push(
+ `(${this.options.group_filter_group_member_attribute}=${format_value})`,
+ );
+ }
+ }
+
+ filter.push(')');
+
+ const searchOptions = {
+ filter: filter.join('').replace(/#{username}/g, username),
+ scope: 'sub',
+ };
+
+ log_debug('Group list filter LDAP:', searchOptions.filter);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return [];
+ }
+
+ const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
+ const groups = [];
+ result.map(item => {
+ groups.push(item[grp_identifier]);
+ });
+ log_debug(`Groups: ${groups.join(', ')}`);
+ return groups;
+ }
+
+ isUserInGroup(username, ldapUser) {
+ if (!this.options.group_filter_enabled) {
+ return true;
+ }
+
+ const grps = this.getUserGroups(username, ldapUser);
+
+ const filter = ['(&'];
+
+ if (this.options.group_filter_object_class !== '') {
+ filter.push(`(objectclass=${this.options.group_filter_object_class})`);
+ }
+
+ if (this.options.group_filter_group_member_attribute !== '') {
+ const format_value =
+ ldapUser[this.options.group_filter_group_member_format];
+ if (format_value) {
+ filter.push(
+ `(${this.options.group_filter_group_member_attribute}=${format_value})`,
+ );
+ }
+ }
+
+ if (this.options.group_filter_group_id_attribute !== '') {
+ filter.push(
+ `(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`,
+ );
+ }
+ filter.push(')');
+
+ const searchOptions = {
+ filter: filter.join('').replace(/#{username}/g, username),
+ scope: 'sub',
+ };
+
+ log_debug('Group filter LDAP:', searchOptions.filter);
+
+ const result = this.searchAllSync(this.options.BaseDN, searchOptions);
+
+ if (!Array.isArray(result) || result.length === 0) {
+ return false;
+ }
+ return true;
+ }
+
+ extractLdapEntryData(entry) {
+ const values = {
+ _raw: entry.raw,
+ };
+
+ Object.keys(values._raw).forEach(key => {
+ const value = values._raw[key];
+
+ if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
+ if (value instanceof Buffer) {
+ values[key] = value.toString();
+ } else {
+ values[key] = value;
+ }
+ }
+ });
+
+ return values;
+ }
+
+ searchAllPaged(BaseDN, options, page) {
+ this.bindIfNecessary();
+
+ const processPage = ({ entries, title, end, next }) => {
+ log_info(title);
+ // Force LDAP idle to wait the record processing
+ this.client._updateIdle(true);
+ page(null, entries, {
+ end,
+ next: () => {
+ // Reset idle timer
+ this.client._updateIdle();
+ next && next();
+ },
+ });
+ };
+
+ this.client.search(BaseDN, options, (error, res) => {
+ if (error) {
+ log_error(error);
+ page(error);
+ return;
+ }
+
+ res.on('error', error => {
+ log_error(error);
+ page(error);
+ return;
+ });
+
+ let entries = [];
+
+ const internalPageSize =
+ options.paged && options.paged.pageSize > 0
+ ? options.paged.pageSize * 2
+ : 500;
+
+ res.on('searchEntry', entry => {
+ entries.push(this.extractLdapEntryData(entry));
+
+ if (entries.length >= internalPageSize) {
+ processPage({
+ entries,
+ title: 'Internal Page',
+ end: false,
+ });
+ entries = [];
+ }
+ });
+
+ res.on('page', (result, next) => {
+ if (!next) {
+ this.client._updateIdle(true);
+ processPage({
+ entries,
+ title: 'Final Page',
+ end: true,
+ });
+ } else if (entries.length) {
+ log_info('Page');
+ processPage({
+ entries,
+ title: 'Page',
+ end: false,
+ next,
+ });
+ entries = [];
+ }
+ });
+
+ res.on('end', () => {
+ if (entries.length) {
+ processPage({
+ entries,
+ title: 'Final Page',
+ end: true,
+ });
+ entries = [];
+ }
+ });
+ });
+ }
+
+ searchAllAsync(BaseDN, options, callback) {
+ this.bindIfNecessary();
+
+ this.client.search(BaseDN, options, (error, res) => {
+ if (error) {
+ log_error(error);
+ callback(error);
+ return;
+ }
+
+ res.on('error', error => {
+ log_error(error);
+ callback(error);
+ return;
+ });
+
+ const entries = [];
+
+ res.on('searchEntry', entry => {
+ entries.push(this.extractLdapEntryData(entry));
+ });
+
+ res.on('end', () => {
+ log_info('Search result count', entries.length);
+ callback(null, entries);
+ });
+ });
+ }
+
+ authSync(dn, password) {
+ log_info('Authenticating', dn);
+
+ try {
+ if (password === '') {
+ throw new Error('Password is not provided');
+ }
+ this.bindSync(dn, password);
+ log_info('Authenticated', dn);
+ return true;
+ } catch (error) {
+ log_info('Not authenticated', dn);
+ log_debug('error', error);
+ return false;
+ }
+ }
+
+ disconnect() {
+ this.connected = false;
+ this.domainBinded = false;
+ log_info('Disconecting');
+ this.client.unbind();
+ }
+}
diff --git a/.snap-meteor-1.8/oidc_server.js b/.snap-meteor-1.8/oidc_server.js
new file mode 100644
index 00000000..91b0e8a4
--- /dev/null
+++ b/.snap-meteor-1.8/oidc_server.js
@@ -0,0 +1,163 @@
+Oidc = {};
+
+OAuth.registerService('oidc', 2, null, function(query) {
+ var debug = process.env.DEBUG || false;
+ var token = getToken(query);
+ if (debug) console.log('XXX: register token:', token);
+
+ var accessToken = token.access_token || token.id_token;
+ var expiresAt = +new Date() + 1000 * parseInt(token.expires_in, 10);
+
+ var userinfo = getUserInfo(accessToken);
+ if (debug) console.log('XXX: userinfo:', userinfo);
+
+ var serviceData = {};
+ serviceData.id = userinfo[process.env.OAUTH2_ID_MAP]; // || userinfo["id"];
+ serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP]; // || userinfo["uid"];
+ serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
+ serviceData.accessToken = accessToken;
+ serviceData.expiresAt = expiresAt;
+ serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
+
+ if (accessToken) {
+ var tokenContent = getTokenContent(accessToken);
+ var fields = _.pick(
+ tokenContent,
+ getConfiguration().idTokenWhitelistFields,
+ );
+ _.extend(serviceData, fields);
+ }
+
+ if (token.refresh_token) serviceData.refreshToken = token.refresh_token;
+ if (debug) console.log('XXX: serviceData:', serviceData);
+
+ var profile = {};
+ profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
+ profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
+ if (debug) console.log('XXX: profile:', profile);
+
+ return {
+ serviceData: serviceData,
+ options: { profile: profile },
+ };
+});
+
+var userAgent = 'Meteor';
+if (Meteor.release) {
+ userAgent += '/' + Meteor.release;
+}
+
+var getToken = function(query) {
+ var debug = process.env.DEBUG || false;
+ var config = getConfiguration();
+ if (config.tokenEndpoint.includes('https://')) {
+ var serverTokenEndpoint = config.tokenEndpoint;
+ } else {
+ var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
+ }
+ var requestPermissions = config.requestPermissions;
+ var response;
+
+ try {
+ response = HTTP.post(serverTokenEndpoint, {
+ headers: {
+ Accept: 'application/json',
+ 'User-Agent': userAgent,
+ },
+ params: {
+ code: query.code,
+ client_id: config.clientId,
+ client_secret: OAuth.openSecret(config.secret),
+ redirect_uri: OAuth._redirectUri('oidc', config),
+ grant_type: 'authorization_code',
+ scope: requestPermissions,
+ state: query.state,
+ },
+ });
+ } catch (err) {
+ throw _.extend(
+ new Error(
+ 'Failed to get token from OIDC ' +
+ serverTokenEndpoint +
+ ': ' +
+ err.message,
+ ),
+ { response: err.response },
+ );
+ }
+ if (response.data.error) {
+ // if the http response was a json object with an error attribute
+ throw new Error(
+ 'Failed to complete handshake with OIDC ' +
+ serverTokenEndpoint +
+ ': ' +
+ response.data.error,
+ );
+ } else {
+ if (debug) console.log('XXX: getToken response: ', response.data);
+ return response.data;
+ }
+};
+
+var getUserInfo = function(accessToken) {
+ var debug = process.env.DEBUG || false;
+ var config = getConfiguration();
+ // Some userinfo endpoints use a different base URL than the authorization or token endpoints.
+ // This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
+ if (config.userinfoEndpoint.includes('https://')) {
+ var serverUserinfoEndpoint = config.userinfoEndpoint;
+ } else {
+ var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
+ }
+ var response;
+ try {
+ response = HTTP.get(serverUserinfoEndpoint, {
+ headers: {
+ 'User-Agent': userAgent,
+ Authorization: 'Bearer ' + accessToken,
+ },
+ });
+ } catch (err) {
+ throw _.extend(
+ new Error(
+ 'Failed to fetch userinfo from OIDC ' +
+ serverUserinfoEndpoint +
+ ': ' +
+ err.message,
+ ),
+ { response: err.response },
+ );
+ }
+ if (debug) console.log('XXX: getUserInfo response: ', response.data);
+ return response.data;
+};
+
+var getConfiguration = function() {
+ var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
+ if (!config) {
+ throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
+ }
+ return config;
+};
+
+var getTokenContent = function(token) {
+ var content = null;
+ if (token) {
+ try {
+ var parts = token.split('.');
+ var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
+ content = JSON.parse(new Buffer(parts[1], 'base64').toString());
+ var signature = new Buffer(parts[2], 'base64');
+ var signed = parts[0] + '.' + parts[1];
+ } catch (err) {
+ this.content = {
+ exp: 0,
+ };
+ }
+ }
+ return content;
+};
+
+Oidc.retrieveCredential = function(credentialToken, credentialSecret) {
+ return OAuth.retrieveCredential(credentialToken, credentialSecret);
+};
diff --git a/.snap-meteor-1.8/snapcraft.yaml b/.snap-meteor-1.8/snapcraft.yaml
new file mode 100644
index 00000000..2f965fe1
--- /dev/null
+++ b/.snap-meteor-1.8/snapcraft.yaml
@@ -0,0 +1,244 @@
+name: wekan
+version: 0
+version-script: git describe --tags | cut -c 2-
+summary: The open-source kanban
+description: |
+ Wekan is an open-source and collaborative kanban board application.
+
+ Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
+ Depending on target environment, some configuration settings might need to be adjusted.
+ For full list of configuration options call:
+ $ wekan.help
+
+confinement: strict
+grade: stable
+
+architectures:
+ - amd64
+
+plugs:
+ mongodb-plug:
+ interface: content
+ target: $SNAP_DATA/shared
+
+hooks:
+ configure:
+ plugs:
+ - network
+ - network-bind
+
+slots:
+ mongodb-slot:
+ interface: content
+ write:
+ - $SNAP_DATA/share
+
+apps:
+ wekan:
+ command: wekan-control
+ daemon: simple
+ plugs: [network, network-bind]
+
+ mongodb:
+ command: mongodb-control
+ daemon: simple
+ plugs: [network, network-bind]
+
+ caddy:
+ command: caddy-control
+ daemon: simple
+ plugs: [network, network-bind]
+
+ help:
+ command: wekan-help
+
+ database-backup:
+ command: mongodb-backup
+ plugs: [network, network-bind]
+
+ database-list-backups:
+ command: ls -al $SNAP_COMMON/db-backups/
+
+ database-restore:
+ command: mongodb-restore
+ plugs: [network, network-bind]
+
+parts:
+ mongodb:
+ source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
+ plugin: dump
+ stage-packages: [libssl1.0.0]
+ filesets:
+ mongo:
+ - usr
+ - bin
+ - lib
+ stage:
+ - $mongo
+ prime:
+ - $mongo
+
+ wekan:
+ source: .
+ plugin: nodejs
+ node-engine: 8.17.0
+ node-packages:
+ - node-gyp
+ - node-pre-gyp
+ - fibers@2.0.0
+ build-packages:
+ - ca-certificates
+ - apt-utils
+ - python
+# - python3
+ - g++
+ - capnproto
+ - curl
+ - execstack
+ - nodejs
+ - npm
+ stage-packages:
+ - libfontconfig1
+ override-build: |
+ echo "Cleaning environment first"
+ rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
+ # Create the OpenAPI specification
+ rm -rf .build
+ #mkdir -p .build/python
+ #cd .build/python
+ #git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
+ #cd esprima-python
+ #python3 setup.py install
+ #cd ../../..
+ #mkdir -p ./public/api
+ #python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
+ # we temporary need api2html and mkdirp
+ #npm install -g api2html@0.3.0
+ #npm install -g mkdirp
+ #api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
+ #npm uninstall -g mkdirp
+ #npm uninstall -g api2html
+ # Node Fibers 100% CPU usage issue:
+ # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
+ # https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
+ # https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
+ # Also see beginning of wekan/server/authentication.js
+ # import Fiber from "fibers";
+ # Fiber.poolSize = 1e9;
+ # OLD: Download node version 8.12.0 prerelease build => Official node 8.12.0 has been released
+ # Description at https://releases.wekan.team/node.txt
+ ##echo "375bd8db50b9c692c0bbba6e96d4114cd29bee3770f901c1ff2249d1038f1348 node" >> node-SHASUMS256.txt.asc
+ ##curl https://releases.wekan.team/node -o node
+ # Verify Fibers patched node authenticity
+ ##echo "Fibers 100% CPU issue patched node authenticity:"
+ ##grep node node-SHASUMS256.txt.asc | shasum -a 256 -c -
+ ##rm -f node-SHASUMS256.txt.asc
+ ##chmod +x node
+ ##mv node `which node`
+ # DOES NOT WORK: paxctl fix.
+ # Removed from build-packages: - paxctl
+ #echo "Applying paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303"
+ #paxctl -mC `which node`
+ #echo "Installing npm"
+ #curl -L https://www.npmjs.com/install.sh | sh
+ echo "Installing meteor"
+ curl https://install.meteor.com/ -o install_meteor.sh
+ #sed -i "s|RELEASE=.*|RELEASE=\"1.8.1-beta.0\"|g" install_meteor.sh
+ chmod +x install_meteor.sh
+ sh install_meteor.sh
+ rm install_meteor.sh
+ # REPOS BELOW ARE INCLUDED TO WEKAN REPO
+ #if [ ! -d "packages" ]; then
+ # mkdir packages
+ #fi
+ #if [ ! -d "packages/kadira-flow-router" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
+ # cd ..
+ #fi
+ #if [ ! -d "packages/meteor-useraccounts-core" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
+ # sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' meteor-useraccounts-core/package.js
+ # cd ..
+ #fi
+ #if [ ! -d "packages/meteor-accounts-cas" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git meteor-accounts-cas
+ # cd ..
+ #fi
+ #if [ ! -d "packages/wekan-ldap" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
+ # cd ..
+ #fi
+ #if [ ! -d "packages/wekan-scrollbar" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
+ # cd ..
+ #fi
+ #if [ ! -d "packages/wekan_accounts-oidc" ]; then
+ # cd packages
+ # git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
+ # mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc
+ # mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc
+ # rm -rf meteor-accounts-oidc
+ # cd ..
+ #fi
+ #if [ ! -d "packages/markdown" ]; then
+ # cd packages
+ # git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
+ # cd ..
+ #fi
+ rm -rf .build
+ meteor add standard-minifier-js --allow-superuser
+ meteor npm install --allow-superuser
+ meteor npm install --allow-superuser --save babel-runtime
+ meteor build .build --directory --allow-superuser
+ cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
+ #Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
+ #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
+ #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
+ #cd .build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
+ #rm -rf node_modules/bcrypt
+ #meteor npm install --save bcrypt
+ # Change from npm-bcrypt directory back to .build/bundle/programs/server directory.
+ #cd ../../../../
+ # Change to directory .build/bundle/programs/server
+ cd .build/bundle/programs/server
+ npm install
+ npm install --allow-superuser --save babel-runtime
+ #meteor npm install --save bcrypt
+ # Change back to Wekan source directory
+ cd ../../../..
+ cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
+ cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
+ rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
+ rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
+ rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
+ rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+ rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
+ # Meteor 1.8.x additional .swp remove
+ rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+
+ organize:
+ README: README.wekan
+ prime:
+ - -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
+
+ helpers:
+ source: snap-src
+ plugin: dump
+
+ caddy:
+ plugin: dump
+ source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
+ source-type: tar
+ organize:
+ caddy: bin/caddy
+ CHANGES.txt: CADDY_CHANGES.txt
+ EULA.txt: CADDY_EULA.txt
+ LICENSES.txt: CADDY_LICENSES.txt
+ README.txt: CADDY_README.txt
+ stage:
+ - -init
diff --git a/.snap-meteor-1.8/wekanCreator.js b/.snap-meteor-1.8/wekanCreator.js
new file mode 100644
index 00000000..ec85d93f
--- /dev/null
+++ b/.snap-meteor-1.8/wekanCreator.js
@@ -0,0 +1,853 @@
+const DateString = Match.Where(function(dateAsString) {
+ check(dateAsString, String);
+ return moment(dateAsString, moment.ISO_8601).isValid();
+});
+
+export class WekanCreator {
+ constructor(data) {
+ // we log current date, to use the same timestamp for all our actions.
+ // this helps to retrieve all elements performed by the same import.
+ this._nowDate = new Date();
+ // The object creation dates, indexed by Wekan id
+ // (so we only parse actions once!)
+ this.createdAt = {
+ board: null,
+ cards: {},
+ lists: {},
+ swimlanes: {},
+ };
+ // The object creator Wekan Id, indexed by the object Wekan id
+ // (so we only parse actions once!)
+ this.createdBy = {
+ cards: {}, // only cards have a field for that
+ };
+
+ // Map of labels Wekan ID => Wekan ID
+ this.labels = {};
+ // Map of swimlanes Wekan ID => Wekan ID
+ this.swimlanes = {};
+ // Map of lists Wekan ID => Wekan ID
+ this.lists = {};
+ // Map of cards Wekan ID => Wekan ID
+ this.cards = {};
+ // Map of comments Wekan ID => Wekan ID
+ this.commentIds = {};
+ // Map of attachments Wekan ID => Wekan ID
+ this.attachmentIds = {};
+ // Map of checklists Wekan ID => Wekan ID
+ this.checklists = {};
+ // Map of checklistItems Wekan ID => Wekan ID
+ this.checklistItems = {};
+ // The comments, indexed by Wekan card id (to map when importing cards)
+ this.comments = {};
+ // Map of rules Wekan ID => Wekan ID
+ this.rules = {};
+ // the members, indexed by Wekan member id => Wekan user ID
+ this.members = data.membersMapping ? data.membersMapping : {};
+ // Map of triggers Wekan ID => Wekan ID
+ this.triggers = {};
+ // Map of actions Wekan ID => Wekan ID
+ this.actions = {};
+
+ // maps a wekanCardId to an array of wekanAttachments
+ this.attachments = {};
+ }
+
+ /**
+ * If dateString is provided,
+ * return the Date it represents.
+ * If not, will return the date when it was first called.
+ * This is useful for us, as we want all import operations to
+ * have the exact same date for easier later retrieval.
+ *
+ * @param {String} dateString a properly formatted Date
+ */
+ _now(dateString) {
+ if (dateString) {
+ return new Date(dateString);
+ }
+ if (!this._nowDate) {
+ this._nowDate = new Date();
+ }
+ return this._nowDate;
+ }
+
+ /**
+ * if wekanUserId is provided and we have a mapping,
+ * return it.
+ * Otherwise return current logged user.
+ * @param wekanUserId
+ * @private
+ */
+ _user(wekanUserId) {
+ if (wekanUserId && this.members[wekanUserId]) {
+ return this.members[wekanUserId];
+ }
+ return Meteor.userId();
+ }
+
+ checkActivities(wekanActivities) {
+ check(wekanActivities, [
+ Match.ObjectIncluding({
+ activityType: String,
+ createdAt: DateString,
+ }),
+ ]);
+ // XXX we could perform more thorough checks based on action type
+ }
+
+ checkBoard(wekanBoard) {
+ check(
+ wekanBoard,
+ Match.ObjectIncluding({
+ archived: Boolean,
+ title: String,
+ // XXX refine control by validating 'color' against a list of
+ // allowed values (is it worth the maintenance?)
+ color: String,
+ permission: Match.Where(value => {
+ return ['private', 'public'].indexOf(value) >= 0;
+ }),
+ }),
+ );
+ }
+
+ checkCards(wekanCards) {
+ check(wekanCards, [
+ Match.ObjectIncluding({
+ archived: Boolean,
+ dateLastActivity: DateString,
+ labelIds: [String],
+ title: String,
+ sort: Number,
+ }),
+ ]);
+ }
+
+ checkLabels(wekanLabels) {
+ check(wekanLabels, [
+ Match.ObjectIncluding({
+ // XXX refine control by validating 'color' against a list of allowed
+ // values (is it worth the maintenance?)
+ color: String,
+ }),
+ ]);
+ }
+
+ checkLists(wekanLists) {
+ check(wekanLists, [
+ Match.ObjectIncluding({
+ archived: Boolean,
+ title: String,
+ }),
+ ]);
+ }
+
+ checkSwimlanes(wekanSwimlanes) {
+ check(wekanSwimlanes, [
+ Match.ObjectIncluding({
+ archived: Boolean,
+ title: String,
+ }),
+ ]);
+ }
+
+ checkChecklists(wekanChecklists) {
+ check(wekanChecklists, [
+ Match.ObjectIncluding({
+ cardId: String,
+ title: String,
+ }),
+ ]);
+ }
+
+ checkChecklistItems(wekanChecklistItems) {
+ check(wekanChecklistItems, [
+ Match.ObjectIncluding({
+ cardId: String,
+ title: String,
+ }),
+ ]);
+ }
+
+ checkRules(wekanRules) {
+ check(wekanRules, [
+ Match.ObjectIncluding({
+ triggerId: String,
+ actionId: String,
+ title: String,
+ }),
+ ]);
+ }
+
+ checkTriggers(wekanTriggers) {
+ // XXX More check based on trigger type
+ check(wekanTriggers, [
+ Match.ObjectIncluding({
+ activityType: String,
+ desc: String,
+ }),
+ ]);
+ }
+
+ getMembersToMap(data) {
+ // we will work on the list itself (an ordered array of objects) when a
+ // mapping is done, we add a 'wekan' field to the object representing the
+ // imported member
+ const membersToMap = data.members;
+ const users = data.users;
+ // auto-map based on username
+ membersToMap.forEach(importedMember => {
+ importedMember.id = importedMember.userId;
+ delete importedMember.userId;
+ const user = users.filter(user => {
+ return user._id === importedMember.id;
+ })[0];
+ if (user.profile && user.profile.fullname) {
+ importedMember.fullName = user.profile.fullname;
+ }
+ importedMember.username = user.username;
+ const wekanUser = Users.findOne({ username: importedMember.username });
+ if (wekanUser) {
+ importedMember.wekanId = wekanUser._id;
+ }
+ });
+ return membersToMap;
+ }
+
+ checkActions(wekanActions) {
+ // XXX More check based on action type
+ check(wekanActions, [
+ Match.ObjectIncluding({
+ actionType: String,
+ desc: String,
+ }),
+ ]);
+ }
+
+ // You must call parseActions before calling this one.
+ createBoardAndLabels(boardToImport) {
+ const boardToCreate = {
+ archived: boardToImport.archived,
+ color: boardToImport.color,
+ // very old boards won't have a creation activity so no creation date
+ createdAt: this._now(boardToImport.createdAt),
+ labels: [],
+ members: [
+ {
+ userId: Meteor.userId(),
+ wekanId: Meteor.userId(),
+ isActive: true,
+ isAdmin: true,
+ isNoComments: false,
+ isCommentOnly: false,
+ swimlaneId: false,
+ },
+ ],
+ // Standalone Export has modifiedAt missing, adding modifiedAt to fix it
+ modifiedAt: this._now(boardToImport.modifiedAt),
+ permission: boardToImport.permission,
+ slug: getSlug(boardToImport.title) || 'board',
+ stars: 0,
+ title: boardToImport.title,
+ };
+ // now add other members
+ if (boardToImport.members) {
+ boardToImport.members.forEach(wekanMember => {
+ // do we already have it in our list?
+ if (
+ !boardToCreate.members.some(
+ member => member.wekanId === wekanMember.wekanId,
+ )
+ )
+ boardToCreate.members.push({
+ ...wekanMember,
+ userId: wekanMember.wekanId,
+ });
+ });
+ }
+ boardToImport.labels.forEach(label => {
+ const labelToCreate = {
+ _id: Random.id(6),
+ color: label.color,
+ name: label.name,
+ };
+ // We need to remember them by Wekan ID, as this is the only ref we have
+ // when importing cards.
+ this.labels[label._id] = labelToCreate._id;
+ boardToCreate.labels.push(labelToCreate);
+ });
+ const boardId = Boards.direct.insert(boardToCreate);
+ Boards.direct.update(boardId, {
+ $set: {
+ modifiedAt: this._now(),
+ },
+ });
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importBoard',
+ boardId,
+ createdAt: this._now(),
+ source: {
+ id: boardToImport.id,
+ system: 'Wekan',
+ },
+ // We attribute the import to current user,
+ // not the author from the original object.
+ userId: this._user(),
+ });
+ return boardId;
+ }
+
+ /**
+ * Create the Wekan cards corresponding to the supplied Wekan cards,
+ * as well as all linked data: activities, comments, and attachments
+ * @param wekanCards
+ * @param boardId
+ * @returns {Array}
+ */
+ createCards(wekanCards, boardId) {
+ const result = [];
+ wekanCards.forEach(card => {
+ const cardToCreate = {
+ archived: card.archived,
+ boardId,
+ // very old boards won't have a creation activity so no creation date
+ createdAt: this._now(this.createdAt.cards[card._id]),
+ dateLastActivity: this._now(),
+ description: card.description,
+ listId: this.lists[card.listId],
+ swimlaneId: this.swimlanes[card.swimlaneId],
+ sort: card.sort,
+ title: card.title,
+ // we attribute the card to its creator if available
+ userId: this._user(this.createdBy.cards[card._id]),
+ isOvertime: card.isOvertime || false,
+ startAt: card.startAt ? this._now(card.startAt) : null,
+ dueAt: card.dueAt ? this._now(card.dueAt) : null,
+ spentTime: card.spentTime || null,
+ };
+ // add labels
+ if (card.labelIds) {
+ cardToCreate.labelIds = card.labelIds.map(wekanId => {
+ return this.labels[wekanId];
+ });
+ }
+ // add members {
+ if (card.members) {
+ const wekanMembers = [];
+ // we can't just map, as some members may not have been mapped
+ card.members.forEach(sourceMemberId => {
+ if (this.members[sourceMemberId]) {
+ const wekanId = this.members[sourceMemberId];
+ // we may map multiple Wekan members to the same wekan user
+ // in which case we risk adding the same user multiple times
+ if (!wekanMembers.find(wId => wId === wekanId)) {
+ wekanMembers.push(wekanId);
+ }
+ }
+ return true;
+ });
+ if (wekanMembers.length > 0) {
+ cardToCreate.members = wekanMembers;
+ }
+ }
+ // set color
+ if (card.color) {
+ cardToCreate.color = card.color;
+ }
+ // insert card
+ const cardId = Cards.direct.insert(cardToCreate);
+ // keep track of Wekan id => Wekan id
+ this.cards[card._id] = cardId;
+ // // log activity
+ // Activities.direct.insert({
+ // activityType: 'importCard',
+ // boardId,
+ // cardId,
+ // createdAt: this._now(),
+ // listId: cardToCreate.listId,
+ // source: {
+ // id: card._id,
+ // system: 'Wekan',
+ // },
+ // // we attribute the import to current user,
+ // // not the author of the original card
+ // userId: this._user(),
+ // });
+ // add comments
+ const comments = this.comments[card._id];
+ if (comments) {
+ comments.forEach(comment => {
+ const commentToCreate = {
+ boardId,
+ cardId,
+ createdAt: this._now(comment.createdAt),
+ text: comment.text,
+ // we attribute the comment to the original author, default to current user
+ userId: this._user(comment.userId),
+ };
+ // dateLastActivity will be set from activity insert, no need to
+ // update it ourselves
+ const commentId = CardComments.direct.insert(commentToCreate);
+ this.commentIds[comment._id] = commentId;
+ // Activities.direct.insert({
+ // activityType: 'addComment',
+ // boardId: commentToCreate.boardId,
+ // cardId: commentToCreate.cardId,
+ // commentId,
+ // createdAt: this._now(commentToCreate.createdAt),
+ // // we attribute the addComment (not the import)
+ // // to the original author - it is needed by some UI elements.
+ // userId: commentToCreate.userId,
+ // });
+ });
+ }
+ const attachments = this.attachments[card._id];
+ const wekanCoverId = card.coverId;
+ if (attachments) {
+ attachments.forEach(att => {
+ const file = new FS.File();
+ // Simulating file.attachData on the client generates multiple errors
+ // - HEAD returns null, which causes exception down the line
+ // - the template then tries to display the url to the attachment which causes other errors
+ // so we make it server only, and let UI catch up once it is done, forget about latency comp.
+ const self = this;
+ if (Meteor.isServer) {
+ if (att.url) {
+ file.attachData(att.url, function(error) {
+ file.boardId = boardId;
+ file.cardId = cardId;
+ file.userId = self._user(att.userId);
+ // The field source will only be used to prevent adding
+ // attachments' related activities automatically
+ file.source = 'import';
+ if (error) {
+ throw error;
+ } else {
+ const wekanAtt = Attachments.insert(file, () => {
+ // we do nothing
+ });
+ self.attachmentIds[att._id] = wekanAtt._id;
+ //
+ if (wekanCoverId === att._id) {
+ Cards.direct.update(cardId, {
+ $set: {
+ coverId: wekanAtt._id,
+ },
+ });
+ }
+ }
+ });
+ } else if (att.file) {
+ file.attachData(
+ new Buffer(att.file, 'base64'),
+ {
+ type: att.type,
+ },
+ error => {
+ file.name(att.name);
+ file.boardId = boardId;
+ file.cardId = cardId;
+ file.userId = self._user(att.userId);
+ // The field source will only be used to prevent adding
+ // attachments' related activities automatically
+ file.source = 'import';
+ if (error) {
+ throw error;
+ } else {
+ const wekanAtt = Attachments.insert(file, () => {
+ // we do nothing
+ });
+ this.attachmentIds[att._id] = wekanAtt._id;
+ //
+ if (wekanCoverId === att._id) {
+ Cards.direct.update(cardId, {
+ $set: {
+ coverId: wekanAtt._id,
+ },
+ });
+ }
+ }
+ },
+ );
+ }
+ }
+ // todo XXX set cover - if need be
+ });
+ }
+ result.push(cardId);
+ });
+ return result;
+ }
+
+ // Create labels if they do not exist and load this.labels.
+ createLabels(wekanLabels, board) {
+ wekanLabels.forEach(label => {
+ const color = label.color;
+ const name = label.name;
+ const existingLabel = board.getLabel(name, color);
+ if (existingLabel) {
+ this.labels[label.id] = existingLabel._id;
+ } else {
+ const idLabelCreated = board.pushLabel(name, color);
+ this.labels[label.id] = idLabelCreated;
+ }
+ });
+ }
+
+ createLists(wekanLists, boardId) {
+ wekanLists.forEach((list, listIndex) => {
+ const listToCreate = {
+ archived: list.archived,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Wekan boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: this._now(this.createdAt.lists[list.id]),
+ title: list.title,
+ sort: list.sort ? list.sort : listIndex,
+ };
+ const listId = Lists.direct.insert(listToCreate);
+ Lists.direct.update(listId, {
+ $set: {
+ updatedAt: this._now(),
+ },
+ });
+ this.lists[list._id] = listId;
+ // // log activity
+ // Activities.direct.insert({
+ // activityType: 'importList',
+ // boardId,
+ // createdAt: this._now(),
+ // listId,
+ // source: {
+ // id: list._id,
+ // system: 'Wekan',
+ // },
+ // // We attribute the import to current user,
+ // // not the creator of the original object
+ // userId: this._user(),
+ // });
+ });
+ }
+
+ createSwimlanes(wekanSwimlanes, boardId) {
+ wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
+ const swimlaneToCreate = {
+ archived: swimlane.archived,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Wekan boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
+ title: swimlane.title,
+ sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
+ };
+ // set color
+ if (swimlane.color) {
+ swimlaneToCreate.color = swimlane.color;
+ }
+ const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
+ Swimlanes.direct.update(swimlaneId, {
+ $set: {
+ updatedAt: this._now(),
+ },
+ });
+ this.swimlanes[swimlane._id] = swimlaneId;
+ });
+ }
+
+ createChecklists(wekanChecklists) {
+ const result = [];
+ wekanChecklists.forEach((checklist, checklistIndex) => {
+ // Create the checklist
+ const checklistToCreate = {
+ cardId: this.cards[checklist.cardId],
+ title: checklist.title,
+ createdAt: checklist.createdAt,
+ sort: checklist.sort ? checklist.sort : checklistIndex,
+ };
+ const checklistId = Checklists.direct.insert(checklistToCreate);
+ this.checklists[checklist._id] = checklistId;
+ result.push(checklistId);
+ });
+ return result;
+ }
+
+ createTriggers(wekanTriggers, boardId) {
+ wekanTriggers.forEach(trigger => {
+ if (trigger.hasOwnProperty('labelId')) {
+ trigger.labelId = this.labels[trigger.labelId];
+ }
+ if (trigger.hasOwnProperty('memberId')) {
+ trigger.memberId = this.members[trigger.memberId];
+ }
+ trigger.boardId = boardId;
+ const oldId = trigger._id;
+ delete trigger._id;
+ this.triggers[oldId] = Triggers.direct.insert(trigger);
+ });
+ }
+
+ createActions(wekanActions, boardId) {
+ wekanActions.forEach(action => {
+ if (action.hasOwnProperty('labelId')) {
+ action.labelId = this.labels[action.labelId];
+ }
+ if (action.hasOwnProperty('memberId')) {
+ action.memberId = this.members[action.memberId];
+ }
+ action.boardId = boardId;
+ const oldId = action._id;
+ delete action._id;
+ this.actions[oldId] = Actions.direct.insert(action);
+ });
+ }
+
+ createRules(wekanRules, boardId) {
+ wekanRules.forEach(rule => {
+ // Create the rule
+ rule.boardId = boardId;
+ rule.triggerId = this.triggers[rule.triggerId];
+ rule.actionId = this.actions[rule.actionId];
+ delete rule._id;
+ Rules.direct.insert(rule);
+ });
+ }
+
+ createChecklistItems(wekanChecklistItems) {
+ wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
+ // Create the checklistItem
+ const checklistItemTocreate = {
+ title: checklistitem.title,
+ checklistId: this.checklists[checklistitem.checklistId],
+ cardId: this.cards[checklistitem.cardId],
+ sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
+ isFinished: checklistitem.isFinished,
+ };
+ const checklistItemId = ChecklistItems.direct.insert(
+ checklistItemTocreate,
+ );
+ this.checklistItems[checklistitem._id] = checklistItemId;
+ });
+ }
+
+ parseActivities(wekanBoard) {
+ wekanBoard.activities.forEach(activity => {
+ switch (activity.activityType) {
+ case 'addAttachment': {
+ // We have to be cautious, because the attachment could have been removed later.
+ // In that case Wekan still reports its addition, but removes its 'url' field.
+ // So we test for that
+ const wekanAttachment = wekanBoard.attachments.filter(attachment => {
+ return attachment._id === activity.attachmentId;
+ })[0];
+
+ if (typeof wekanAttachment !== 'undefined' && wekanAttachment) {
+ if (wekanAttachment.url || wekanAttachment.file) {
+ // we cannot actually create the Wekan attachment, because we don't yet
+ // have the cards to attach it to, so we store it in the instance variable.
+ const wekanCardId = activity.cardId;
+ if (!this.attachments[wekanCardId]) {
+ this.attachments[wekanCardId] = [];
+ }
+ this.attachments[wekanCardId].push(wekanAttachment);
+ }
+ }
+ break;
+ }
+ case 'addComment': {
+ const wekanComment = wekanBoard.comments.filter(comment => {
+ return comment._id === activity.commentId;
+ })[0];
+ const id = activity.cardId;
+ if (!this.comments[id]) {
+ this.comments[id] = [];
+ }
+ this.comments[id].push(wekanComment);
+ break;
+ }
+ case 'createBoard': {
+ this.createdAt.board = activity.createdAt;
+ break;
+ }
+ case 'createCard': {
+ const cardId = activity.cardId;
+ this.createdAt.cards[cardId] = activity.createdAt;
+ this.createdBy.cards[cardId] = activity.userId;
+ break;
+ }
+ case 'createList': {
+ const listId = activity.listId;
+ this.createdAt.lists[listId] = activity.createdAt;
+ break;
+ }
+ case 'createSwimlane': {
+ const swimlaneId = activity.swimlaneId;
+ this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
+ break;
+ }
+ }
+ });
+ }
+
+ importActivities(activities, boardId) {
+ activities.forEach(activity => {
+ switch (activity.activityType) {
+ // Board related activities
+ // TODO: addBoardMember, removeBoardMember
+ case 'createBoard': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ type: 'board',
+ activityTypeId: boardId,
+ activityType: activity.activityType,
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ // List related activities
+ // TODO: removeList, archivedList
+ case 'createList': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ type: 'list',
+ activityType: activity.activityType,
+ listId: this.lists[activity.listId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ // Card related activities
+ // TODO: archivedCard, restoredCard, joinMember, unjoinMember
+ case 'createCard': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ activityType: activity.activityType,
+ listId: this.lists[activity.listId],
+ cardId: this.cards[activity.cardId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ case 'moveCard': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ oldListId: this.lists[activity.oldListId],
+ activityType: activity.activityType,
+ listId: this.lists[activity.listId],
+ cardId: this.cards[activity.cardId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ // Comment related activities
+ case 'addComment': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ activityType: activity.activityType,
+ cardId: this.cards[activity.cardId],
+ commentId: this.commentIds[activity.commentId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ // Attachment related activities
+ case 'addAttachment': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ type: 'card',
+ activityType: activity.activityType,
+ attachmentId: this.attachmentIds[activity.attachmentId],
+ cardId: this.cards[activity.cardId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ // Checklist related activities
+ case 'addChecklist': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ activityType: activity.activityType,
+ cardId: this.cards[activity.cardId],
+ checklistId: this.checklists[activity.checklistId],
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ case 'addChecklistItem': {
+ Activities.direct.insert({
+ userId: this._user(activity.userId),
+ activityType: activity.activityType,
+ cardId: this.cards[activity.cardId],
+ checklistId: this.checklists[activity.checklistId],
+ checklistItemId: activity.checklistItemId.replace(
+ activity.checklistId,
+ this.checklists[activity.checklistId],
+ ),
+ boardId,
+ createdAt: this._now(activity.createdAt),
+ });
+ break;
+ }
+ }
+ });
+ }
+
+ //check(board) {
+ check() {
+ //try {
+ // check(data, {
+ // membersMapping: Match.Optional(Object),
+ // });
+ // this.checkActivities(board.activities);
+ // this.checkBoard(board);
+ // this.checkLabels(board.labels);
+ // this.checkLists(board.lists);
+ // this.checkSwimlanes(board.swimlanes);
+ // this.checkCards(board.cards);
+ //this.checkChecklists(board.checklists);
+ // this.checkRules(board.rules);
+ // this.checkActions(board.actions);
+ //this.checkTriggers(board.triggers);
+ //this.checkChecklistItems(board.checklistItems);
+ //} catch (e) {
+ // throw new Meteor.Error('error-json-schema');
+ // }
+ }
+
+ create(board, currentBoardId) {
+ // TODO : Make isSandstorm variable global
+ const isSandstorm =
+ Meteor.settings &&
+ Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
+ if (isSandstorm && currentBoardId) {
+ const currentBoard = Boards.findOne(currentBoardId);
+ currentBoard.archive();
+ }
+ this.parseActivities(board);
+ const boardId = this.createBoardAndLabels(board);
+ this.createLists(board.lists, boardId);
+ this.createSwimlanes(board.swimlanes, boardId);
+ this.createCards(board.cards, boardId);
+ this.createChecklists(board.checklists);
+ this.createChecklistItems(board.checklistItems);
+ this.importActivities(board.activities, boardId);
+ this.createTriggers(board.triggers, boardId);
+ this.createActions(board.actions, boardId);
+ this.createRules(board.rules, boardId);
+ // XXX add members
+ return boardId;
+ }
+}
diff --git a/snapcraft.yaml b/snapcraft.yaml
index b17c714d..c43418bb 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -109,6 +109,12 @@ parts:
mv .snap-meteor-1.8/.meteor .
mv .snap-meteor-1.8/package.json .
mv .snap-meteor-1.8/package-lock.json .
+ # Meteor 1.9.x has changes to Buffer() => Buffer.alloc(), so reverting those
+ mv .snap-meteor-1.8/cfs_access-point.txt fix-download-unicode/
+ mv .snap-meteor-1.8/export.js models/
+ mv .snap-meteor-1.8/wekanCreator.js models/
+ mv .snap-meteor-1.8/ldap.js packages/wekan-ldap/server/ldap.js
+ mv .snap-meteor-1.8/oidc_server.js packages/wekan-oidc/oidc_server.js
rm -rf .snap-meteor-1.8
#mkdir -p .build/python
#cd .build/python