diff options
-rw-r--r-- | CHANGELOG.md | 14 | ||||
-rw-r--r-- | Dockerfile | 5 | ||||
-rw-r--r-- | client/lib/filter.js | 1014 | ||||
-rw-r--r-- | config/models.js | 4 | ||||
-rw-r--r-- | docker-compose.yml | 6 | ||||
-rw-r--r-- | i18n/de.i18n.json | 14 | ||||
-rw-r--r-- | i18n/zh-CN.i18n.json | 14 | ||||
-rw-r--r-- | models/cards.js | 22 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | sandstorm-pkgdef.capnp | 6 | ||||
-rw-r--r-- | server/policy.js | 24 | ||||
-rwxr-xr-x | snap-src/bin/config | 12 | ||||
-rwxr-xr-x | snap-src/bin/wekan-help | 15 |
13 files changed, 626 insertions, 526 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f043f25..46a6f014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.30 2018-08-14 Wekan release + +This release add the following new features: + +- [When Content Policy is enabled, allow one URL to have iframe that embeds Wekan](https://github.com/wekan/wekan/commit/b9929dc68297539a94d21950995e26e06745a263); +- [Add option to turn off Content Policy](https://github.com/wekan/wekan/commit/b9929dc68297539a94d21950995e26e06745a263); +- [Allow always in Wekan markdown `<img src="any-image-url-here">`](https://github.com/wekan/wekan/commit/b9929dc68297539a94d21950995e26e06745a263). + +and fixes the following bugs: + +- [Fix Import from Trello error 400](https://github.com/wekan/wekan/commit/2f557ae3a558c654cc6f3befff22f5ee4ea6c3d9). + +Thanks to GitHub user xet7 for contributions. + # v1.29 2018-08-12 Wekan release This release fixes the following bugs: @@ -15,6 +15,8 @@ ARG MATOMO_ADDRESS ARG MATOMO_SITE_ID ARG MATOMO_DO_NOT_TRACK ARG MATOMO_WITH_USERNAME +ARG BROWSER_POLICY_ENABLED +ARG TRUSTED_URL # Set the environment variables (defaults where required) # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303 @@ -33,7 +35,8 @@ ENV MATOMO_ADDRESS ${MATOMO_ADDRESS:-} ENV MATOMO_SITE_ID ${MATOMO_SITE_ID:-} ENV MATOMO_DO_NOT_TRACK ${MATOMO_DO_NOT_TRACK:-false} ENV MATOMO_WITH_USERNAME ${MATOMO_WITH_USERNAME:-true} - +ENV BROWSER_POLICY_ENABLED ${BROWSER_POLICY_ENABLED:-true} +ENV TRUSTED_URL ${TRUSTED_URL:-} # Copy the app to the image COPY ${SRC_PATH} /home/wekan/app diff --git a/client/lib/filter.js b/client/lib/filter.js index 4652c397..c3c1b070 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -4,7 +4,7 @@ // goal is to filter complete documents by using the local filters for each // fields. function showFilterSidebar() { - Sidebar.setView('filter'); + Sidebar.setView('filter'); } // Use a "set" filter for a field that is a set of documents uniquely @@ -12,446 +12,446 @@ function showFilterSidebar() { // use "subField" for searching inside object Fields. // For instance '{ 'customFields._id': ['field1','field2']} (subField would be: _id) class SetFilter { - constructor(subField = '') { - this._dep = new Tracker.Dependency(); - this._selectedElements = []; - this.subField = subField; + constructor(subField = '') { + this._dep = new Tracker.Dependency(); + this._selectedElements = []; + this.subField = subField; + } + + isSelected(val) { + this._dep.depend(); + return this._selectedElements.indexOf(val) > -1; + } + + add(val) { + if (this._indexOfVal(val) === -1) { + this._selectedElements.push(val); + this._dep.changed(); + showFilterSidebar(); } + } - isSelected(val) { - this._dep.depend(); - return this._selectedElements.indexOf(val) > -1; + remove(val) { + const indexOfVal = this._indexOfVal(val); + if (this._indexOfVal(val) !== -1) { + this._selectedElements.splice(indexOfVal, 1); + this._dep.changed(); } + } - add(val) { - if (this._indexOfVal(val) === -1) { - this._selectedElements.push(val); - this._dep.changed(); - showFilterSidebar(); - } - } - - remove(val) { - const indexOfVal = this._indexOfVal(val); - if (this._indexOfVal(val) !== -1) { - this._selectedElements.splice(indexOfVal, 1); - this._dep.changed(); - } - } - - toggle(val) { - if (this._indexOfVal(val) === -1) { - this.add(val); - } else { - this.remove(val); - } - } - - reset() { - this._selectedElements = []; - this._dep.changed(); - } - - _indexOfVal(val) { - return this._selectedElements.indexOf(val); - } - - _isActive() { - this._dep.depend(); - return this._selectedElements.length !== 0; - } - - _getMongoSelector() { - this._dep.depend(); - return { - $in: this._selectedElements - }; - } - - _getEmptySelector() { - this._dep.depend(); - let includeEmpty = false; - this._selectedElements.forEach((el) => { - if (el === undefined) { - includeEmpty = true; - } - }); - return includeEmpty ? { - $eq: [] - } : null; + toggle(val) { + if (this._indexOfVal(val) === -1) { + this.add(val); + } else { + this.remove(val); } + } + + reset() { + this._selectedElements = []; + this._dep.changed(); + } + + _indexOfVal(val) { + return this._selectedElements.indexOf(val); + } + + _isActive() { + this._dep.depend(); + return this._selectedElements.length !== 0; + } + + _getMongoSelector() { + this._dep.depend(); + return { + $in: this._selectedElements, + }; + } + + _getEmptySelector() { + this._dep.depend(); + let includeEmpty = false; + this._selectedElements.forEach((el) => { + if (el === undefined) { + includeEmpty = true; + } + }); + return includeEmpty ? { + $eq: [], + } : null; + } } // Advanced filter forms a MongoSelector from a users String. // Build by: Ignatz 19.05.2018 (github feuerball11) class AdvancedFilter { - constructor() { - this._dep = new Tracker.Dependency(); - this._filter = ''; - this._lastValide = {}; + constructor() { + this._dep = new Tracker.Dependency(); + this._filter = ''; + this._lastValide = {}; + } + + set(str) { + this._filter = str; + this._dep.changed(); + } + + reset() { + this._filter = ''; + this._lastValide = {}; + this._dep.changed(); + } + + _isActive() { + this._dep.depend(); + return this._filter !== ''; + } + + _filterToCommands() { + const commands = []; + let current = ''; + let string = false; + let regex = false; + let wasString = false; + let ignore = false; + for (let i = 0; i < this._filter.length; i++) { + const char = this._filter.charAt(i); + if (ignore) { + ignore = false; + current += char; + continue; + } + if (char === '/') { + string = !string; + if (string) regex = true; + current += char; + continue; + } + if (char === '\'') { + string = !string; + if (string) wasString = true; + continue; + } + if (char === '\\' && !string) { + ignore = true; + continue; + } + if (char === ' ' && !string) { + commands.push({ + 'cmd': current, + 'string': wasString, + regex, + }); + wasString = false; + current = ''; + continue; + } + current += char; } - - set(str) { - this._filter = str; - this._dep.changed(); + if (current !== '') { + commands.push({ + 'cmd': current, + 'string': wasString, + regex, + }); } - - reset() { - this._filter = ''; - this._lastValide = {}; - this._dep.changed(); + return commands; + } + + _fieldNameToId(field) { + const found = CustomFields.findOne({ + 'name': field, + }); + return found._id; + } + + _fieldValueToId(field, value) { + const found = CustomFields.findOne({ + 'name': field, + }); + if (found.settings.dropdownItems && found.settings.dropdownItems.length > 0) { + for (let i = 0; i < found.settings.dropdownItems.length; i++) { + if (found.settings.dropdownItems[i].name === value) { + return found.settings.dropdownItems[i]._id; + } + } } - - _isActive() { - this._dep.depend(); - return this._filter !== ''; + return value; + } + + _arrayToSelector(commands) { + try { + //let changed = false; + this._processSubCommands(commands); + } catch (e) { + return this._lastValide; } - - _filterToCommands() { - const commands = []; - let current = ''; - let string = false; - let regex = false; - let wasString = false; - let ignore = false; - for (let i = 0; i < this._filter.length; i++) { - const char = this._filter.charAt(i); - if (ignore) { - ignore = false; - current += char; - continue; - } - if (char === '/') { - string = !string; - if (string) regex = true; - current += char; - continue; - } - if (char === '\'') { - string = !string; - if (string) wasString = true; - continue; - } - if (char === '\\' && !string) { - ignore = true; - continue; - } - if (char === ' ' && !string) { - commands.push({ - 'cmd': current, - 'string': wasString, - regex - }); - wasString = false; - current = ''; - continue; - } - current += char; + this._lastValide = { + $or: commands, + }; + return { + $or: commands, + }; + } + + _processSubCommands(commands) { + const subcommands = []; + let level = 0; + let start = -1; + for (let i = 0; i < commands.length; i++) { + if (commands[i].cmd) { + switch (commands[i].cmd) { + case '(': + { + level++; + if (start === -1) start = i; + continue; } - if (current !== '') { - commands.push({ - 'cmd': current, - 'string': wasString, - regex - }); + case ')': + { + level--; + commands.splice(i, 1); + i--; + continue; } - return commands; - } - - _fieldNameToId(field) { - const found = CustomFields.findOne({ - 'name': field - }); - return found._id; - } - - _fieldValueToId(field, value) { - const found = CustomFields.findOne({ - 'name': field - }); - if (found.settings.dropdownItems && found.settings.dropdownItems.length > 0) { - for (let i = 0; i < found.settings.dropdownItems.length; i++) { - if (found.settings.dropdownItems[i].name === value) { - return found.settings.dropdownItems[i]._id; - } - } + default: + { + if (level > 0) { + subcommands.push(commands[i]); + commands.splice(i, 1); + i--; + continue; + } } - return value; - } - - _arrayToSelector(commands) { - try { - //let changed = false; - this._processSubCommands(commands); - } catch (e) { - return this._lastValide; } - this._lastValide = { - $or: commands - }; - return { - $or: commands - }; + } } - - _processSubCommands(commands) { - const subcommands = []; - let level = 0; - let start = -1; - for (let i = 0; i < commands.length; i++) { - if (commands[i].cmd) { - switch (commands[i].cmd) { - case '(': - { - level++; - if (start === -1) start = i; - continue; - } - case ')': - { - level--; - commands.splice(i, 1); - i--; - continue; - } - default: - { - if (level > 0) { - subcommands.push(commands[i]); - commands.splice(i, 1); - i--; - continue; - } - } - } - } + if (start !== -1) { + this._processSubCommands(subcommands); + if (subcommands.length === 1) + commands.splice(start, 0, subcommands[0]); + else + commands.splice(start, 0, subcommands); + } + this._processConditions(commands); + this._processLogicalOperators(commands); + } + + _processConditions(commands) { + for (let i = 0; i < commands.length; i++) { + if (!commands[i].string && commands[i].cmd) { + switch (commands[i].cmd) { + case '=': + case '==': + case '===': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + if (commands[i + 1].regex) { + const match = str.match(new RegExp('^/(.*?)/([gimy]*)$')); + let regex = null; + if (match.length > 2) + regex = new RegExp(match[1], match[2]); + else + regex = new RegExp(match[1]); + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': regex, + }; + } else { + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $in: [this._fieldValueToId(field, str), parseInt(str, 10)], + }, + }; + } + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; } - if (start !== -1) { - this._processSubCommands(subcommands); - if (subcommands.length === 1) - commands.splice(start, 0, subcommands[0]); + case '!=': + case '!==': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + if (commands[i + 1].regex) { + const match = str.match(new RegExp('^/(.*?)/([gimy]*)$')); + let regex = null; + if (match.length > 2) + regex = new RegExp(match[1], match[2]); else - commands.splice(start, 0, subcommands); + regex = new RegExp(match[1]); + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $not: regex, + }, + }; + } else { + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $not: { + $in: [this._fieldValueToId(field, str), parseInt(str, 10)], + }, + }, + }; + } + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; } - this._processConditions(commands); - this._processLogicalOperators(commands); - } - - _processConditions(commands) { - for (let i = 0; i < commands.length; i++) { - if (!commands[i].string && commands[i].cmd) { - switch (commands[i].cmd) { - case '=': - case '==': - case '===': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - if (commands[i + 1].regex) { - const match = str.match(new RegExp('^/(.*?)/([gimy]*)$')); - let regex = null; - if (match.length > 2) - regex = new RegExp(match[1], match[2]); - else - regex = new RegExp(match[1]); - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': regex - }; - } else { - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $in: [this._fieldValueToId(field, str), parseInt(str, 10)] - } - }; - } - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case '!=': - case '!==': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - if (commands[i + 1].regex) { - const match = str.match(new RegExp('^/(.*?)/([gimy]*)$')); - let regex = null; - if (match.length > 2) - regex = new RegExp(match[1], match[2]); - else - regex = new RegExp(match[1]); - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $not: regex - } - }; - } else { - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $not: { - $in: [this._fieldValueToId(field, str), parseInt(str, 10)] - } - } - }; - } - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case '>': - case 'gt': - case 'Gt': - case 'GT': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $gt: parseInt(str, 10) - } - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case '>=': - case '>==': - case 'gte': - case 'Gte': - case 'GTE': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $gte: parseInt(str, 10) - } - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case '<': - case 'lt': - case 'Lt': - case 'LT': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $lt: parseInt(str, 10) - } - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case '<=': - case '<==': - case 'lte': - case 'Lte': - case 'LTE': - { - const field = commands[i - 1].cmd; - const str = commands[i + 1].cmd; - commands[i] = { - 'customFields._id': this._fieldNameToId(field), - 'customFields.value': { - $lte: parseInt(str, 10) - } - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - } - } + case '>': + case 'gt': + case 'Gt': + case 'GT': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $gt: parseInt(str, 10), + }, + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; + } + case '>=': + case '>==': + case 'gte': + case 'Gte': + case 'GTE': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $gte: parseInt(str, 10), + }, + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; + } + case '<': + case 'lt': + case 'Lt': + case 'LT': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $lt: parseInt(str, 10), + }, + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; + } + case '<=': + case '<==': + case 'lte': + case 'Lte': + case 'LTE': + { + const field = commands[i - 1].cmd; + const str = commands[i + 1].cmd; + commands[i] = { + 'customFields._id': this._fieldNameToId(field), + 'customFields.value': { + $lte: parseInt(str, 10), + }, + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; } + } + } } - - _processLogicalOperators(commands) { - for (let i = 0; i < commands.length; i++) { - if (!commands[i].string && commands[i].cmd) { - switch (commands[i].cmd) { - case 'or': - case 'Or': - case 'OR': - case '|': - case '||': - { - const op1 = commands[i - 1]; - const op2 = commands[i + 1]; - commands[i] = { - $or: [op1, op2] - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case 'and': - case 'And': - case 'AND': - case '&': - case '&&': - { - const op1 = commands[i - 1]; - const op2 = commands[i + 1]; - commands[i] = { - $and: [op1, op2] - }; - commands.splice(i - 1, 1); - commands.splice(i, 1); - //changed = true; - i--; - break; - } - case 'not': - case 'Not': - case 'NOT': - case '!': - { - const op1 = commands[i + 1]; - commands[i] = { - $not: op1 - }; - commands.splice(i + 1, 1); - //changed = true; - i--; - break; - } - } - } + } + + _processLogicalOperators(commands) { + for (let i = 0; i < commands.length; i++) { + if (!commands[i].string && commands[i].cmd) { + switch (commands[i].cmd) { + case 'or': + case 'Or': + case 'OR': + case '|': + case '||': + { + const op1 = commands[i - 1]; + const op2 = commands[i + 1]; + commands[i] = { + $or: [op1, op2], + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; + } + case 'and': + case 'And': + case 'AND': + case '&': + case '&&': + { + const op1 = commands[i - 1]; + const op2 = commands[i + 1]; + commands[i] = { + $and: [op1, op2], + }; + commands.splice(i - 1, 1); + commands.splice(i, 1); + //changed = true; + i--; + break; + } + case 'not': + case 'Not': + case 'NOT': + case '!': + { + const op1 = commands[i + 1]; + commands[i] = { + $not: op1, + }; + commands.splice(i + 1, 1); + //changed = true; + i--; + break; } + } + } } + } - _getMongoSelector() { - this._dep.depend(); - const commands = this._filterToCommands(); - return this._arrayToSelector(commands); - } + _getMongoSelector() { + this._dep.depend(); + const commands = this._filterToCommands(); + return this._arrayToSelector(commands); + } } @@ -460,101 +460,101 @@ class AdvancedFilter { // the need to provide a list of `_fields`. We also should move methods into the // object prototype. Filter = { - // XXX I would like to rename this field into `labels` to be consistent with - // the rest of the schema, but we need to set some migrations architecture - // before changing the schema. - labelIds: new SetFilter(), - members: new SetFilter(), - customFields: new SetFilter('_id'), - advanced: new AdvancedFilter(), - - _fields: ['labelIds', 'members', 'customFields'], - - // We don't filter cards that have been added after the last filter change. To - // implement this we keep the id of these cards in this `_exceptions` fields - // and use a `$or` condition in the mongo selector we return. - _exceptions: [], - _exceptionsDep: new Tracker.Dependency(), - - isActive() { - return _.any(this._fields, (fieldName) => { - return this[fieldName]._isActive(); - }) || this.advanced._isActive(); - }, - - _getMongoSelector() { - if (!this.isActive()) - return {}; - - const filterSelector = {}; - const emptySelector = {}; - let includeEmptySelectors = false; - this._fields.forEach((fieldName) => { - const filter = this[fieldName]; - if (filter._isActive()) { - if (filter.subField !== '') { - filterSelector[`${fieldName}.${filter.subField}`] = filter._getMongoSelector(); - } else { - filterSelector[fieldName] = filter._getMongoSelector(); - } - emptySelector[fieldName] = filter._getEmptySelector(); - if (emptySelector[fieldName] !== null) { - includeEmptySelectors = true; - } - } - }); - - const exceptionsSelector = { - _id: { - $in: this._exceptions - } - }; - this._exceptionsDep.depend(); - - const selectors = [exceptionsSelector]; - - if (_.any(this._fields, (fieldName) => { - return this[fieldName]._isActive(); - })) selectors.push(filterSelector); - if (includeEmptySelectors) selectors.push(emptySelector); - if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector()); - - return { - $or: selectors - }; - }, - - mongoSelector(additionalSelector) { - const filterSelector = this._getMongoSelector(); - if (_.isUndefined(additionalSelector)) - return filterSelector; - else - return { - $and: [filterSelector, additionalSelector] - }; - }, - - reset() { - this._fields.forEach((fieldName) => { - const filter = this[fieldName]; - filter.reset(); - }); - this.advanced.reset(); - this.resetExceptions(); - }, - - addException(_id) { - if (this.isActive()) { - this._exceptions.push(_id); - this._exceptionsDep.changed(); - Tracker.flush(); + // XXX I would like to rename this field into `labels` to be consistent with + // the rest of the schema, but we need to set some migrations architecture + // before changing the schema. + labelIds: new SetFilter(), + members: new SetFilter(), + customFields: new SetFilter('_id'), + advanced: new AdvancedFilter(), + + _fields: ['labelIds', 'members', 'customFields'], + + // We don't filter cards that have been added after the last filter change. To + // implement this we keep the id of these cards in this `_exceptions` fields + // and use a `$or` condition in the mongo selector we return. + _exceptions: [], + _exceptionsDep: new Tracker.Dependency(), + + isActive() { + return _.any(this._fields, (fieldName) => { + return this[fieldName]._isActive(); + }) || this.advanced._isActive(); + }, + + _getMongoSelector() { + if (!this.isActive()) + return {}; + + const filterSelector = {}; + const emptySelector = {}; + let includeEmptySelectors = false; + this._fields.forEach((fieldName) => { + const filter = this[fieldName]; + if (filter._isActive()) { + if (filter.subField !== '') { + filterSelector[`${fieldName}.${filter.subField}`] = filter._getMongoSelector(); + } else { + filterSelector[fieldName] = filter._getMongoSelector(); + } + emptySelector[fieldName] = filter._getEmptySelector(); + if (emptySelector[fieldName] !== null) { + includeEmptySelectors = true; } - }, + } + }); + + const exceptionsSelector = { + _id: { + $in: this._exceptions, + }, + }; + this._exceptionsDep.depend(); + + const selectors = [exceptionsSelector]; + + if (_.any(this._fields, (fieldName) => { + return this[fieldName]._isActive(); + })) selectors.push(filterSelector); + if (includeEmptySelectors) selectors.push(emptySelector); + if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector()); + + return { + $or: selectors, + }; + }, + + mongoSelector(additionalSelector) { + const filterSelector = this._getMongoSelector(); + if (_.isUndefined(additionalSelector)) + return filterSelector; + else + return { + $and: [filterSelector, additionalSelector], + }; + }, + + reset() { + this._fields.forEach((fieldName) => { + const filter = this[fieldName]; + filter.reset(); + }); + this.advanced.reset(); + this.resetExceptions(); + }, + + addException(_id) { + if (this.isActive()) { + this._exceptions.push(_id); + this._exceptionsDep.changed(); + Tracker.flush(); + } + }, - resetExceptions() { - this._exceptions = []; - this._exceptionsDep.changed(); - }, + resetExceptions() { + this._exceptions = []; + this._exceptionsDep.changed(); + }, }; -Blaze.registerHelper('Filter', Filter);
\ No newline at end of file +Blaze.registerHelper('Filter', Filter); diff --git a/config/models.js b/config/models.js new file mode 100644 index 00000000..f70faae3 --- /dev/null +++ b/config/models.js @@ -0,0 +1,4 @@ +module.exports.models = { + connection: 'mongodb', + migrate: 'safe', +}; diff --git a/docker-compose.yml b/docker-compose.yml index e769cb82..9e96bcf1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,12 @@ services: # - MATOMO_DO_NOT_TRACK='false' # The option that allows matomo to retrieve the username: # - MATOMO_WITH_USERNAME='true' + # Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside. + # Setting this to false is not recommended, it also disables all other browser policy protections + # and allows all iframing etc. See wekan/server/policy.js + - BROWSER_POLICY_ENABLED=true + # When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside. + - TRUSTED_URL= depends_on: - wekandb diff --git a/i18n/de.i18n.json b/i18n/de.i18n.json index 3bc75e2d..9dd21129 100644 --- a/i18n/de.i18n.json +++ b/i18n/de.i18n.json @@ -109,7 +109,7 @@ "bucket-example": "z.B. \"Löffelliste\"", "cancel": "Abbrechen", "card-archived": "Diese Karte wurde in den Papierkorb verschoben", - "board-archived": "This board is moved to Recycle Bin.", + "board-archived": "Dieses Board wurde in den Papierkorb verschoben.", "card-comments-title": "Diese Karte hat %s Kommentar(e).", "card-delete-notice": "Löschen kann nicht rückgängig gemacht werden. Alle Aktionen, die dieser Karte zugeordnet sind, werden ebenfalls gelöscht.", "card-delete-pop": "Alle Aktionen werden aus dem Aktivitätsfeed entfernt und die Karte kann nicht wiedereröffnet werden. Die Aktion kann nicht rückgängig gemacht werden.", @@ -136,9 +136,9 @@ "cards": "Karten", "cards-count": "Karten", "casSignIn": "Mit CAS anmelden", - "cardType-card": "Card", - "cardType-linkedCard": "Linked Card", - "cardType-linkedBoard": "Linked Board", + "cardType-card": "Karte", + "cardType-linkedCard": "Verknüpfte Karte", + "cardType-linkedBoard": "Verknüpftes Board", "change": "Ändern", "change-avatar": "Profilbild ändern", "change-password": "Passwort ändern", @@ -175,8 +175,8 @@ "confirm-subtask-delete-dialog": "Wollen Sie die Teilaufgabe wirklich löschen?", "confirm-checklist-delete-dialog": "Wollen Sie die Checkliste wirklich löschen?", "copy-card-link-to-clipboard": "Kopiere Link zur Karte in die Zwischenablage", - "linkCardPopup-title": "Link Card", - "searchCardPopup-title": "Search Card", + "linkCardPopup-title": "Karte verknüpfen", + "searchCardPopup-title": "Karte suchen", "copyCardPopup-title": "Karte kopieren", "copyChecklistToManyCardsPopup-title": "Checklistenvorlage in mehrere Karten kopieren", "copyChecklistToManyCardsPopup-instructions": "Titel und Beschreibungen der Zielkarten im folgenden JSON-Format", @@ -267,7 +267,7 @@ "headerBarCreateBoardPopup-title": "Board erstellen", "home": "Home", "import": "Importieren", - "link": "Link", + "link": "Verknüpfung", "import-board": "Board importieren", "import-board-c": "Board importieren", "import-board-title-trello": "Board von Trello importieren", diff --git a/i18n/zh-CN.i18n.json b/i18n/zh-CN.i18n.json index b296e2b6..bdb9e0e0 100644 --- a/i18n/zh-CN.i18n.json +++ b/i18n/zh-CN.i18n.json @@ -109,7 +109,7 @@ "bucket-example": "例如 “目标清单”", "cancel": "取消", "card-archived": "此卡片已经被移入回收站。", - "board-archived": "This board is moved to Recycle Bin.", + "board-archived": "将看板移动到回收站", "card-comments-title": "该卡片有 %s 条评论", "card-delete-notice": "彻底删除的操作不可恢复,你将会丢失该卡片相关的所有操作记录。", "card-delete-pop": "所有的活动将从活动摘要中被移除且您将无法重新打开该卡片。此操作无法撤销。", @@ -136,9 +136,9 @@ "cards": "卡片", "cards-count": "卡片", "casSignIn": "用CAS登录", - "cardType-card": "Card", - "cardType-linkedCard": "Linked Card", - "cardType-linkedBoard": "Linked Board", + "cardType-card": "卡片", + "cardType-linkedCard": "已链接卡片", + "cardType-linkedBoard": "已链接看板", "change": "变更", "change-avatar": "更改头像", "change-password": "更改密码", @@ -175,8 +175,8 @@ "confirm-subtask-delete-dialog": "确定要删除子任务吗?", "confirm-checklist-delete-dialog": "确定要删除清单吗?", "copy-card-link-to-clipboard": "复制卡片链接到剪贴板", - "linkCardPopup-title": "Link Card", - "searchCardPopup-title": "Search Card", + "linkCardPopup-title": "链接卡片", + "searchCardPopup-title": "搜索卡片", "copyCardPopup-title": "复制卡片", "copyChecklistToManyCardsPopup-title": "复制清单模板至多个卡片", "copyChecklistToManyCardsPopup-instructions": "以JSON格式表示目标卡片的标题和描述", @@ -267,7 +267,7 @@ "headerBarCreateBoardPopup-title": "创建看板", "home": "首页", "import": "导入", - "link": "Link", + "link": "链接", "import-board": "导入看板", "import-board-c": "导入看板", "import-board-title-trello": "从Trello导入看板", diff --git a/models/cards.js b/models/cards.js index 2c0da093..171c21c5 100644 --- a/models/cards.js +++ b/models/cards.js @@ -6,6 +6,8 @@ Cards = new Mongo.Collection('cards'); Cards.attachSchema(new SimpleSchema({ title: { type: String, + optional: true, + defaultValue: '', }, archived: { type: Boolean, @@ -22,6 +24,8 @@ Cards.attachSchema(new SimpleSchema({ }, listId: { type: String, + optional: true, + defaultValue: '', }, swimlaneId: { type: String, @@ -31,10 +35,14 @@ Cards.attachSchema(new SimpleSchema({ // difficult to manage and less efficient. boardId: { type: String, + optional: true, + defaultValue: '', }, coverId: { type: String, optional: true, + defaultValue: '', + }, createdAt: { type: Date, @@ -49,15 +57,19 @@ Cards.attachSchema(new SimpleSchema({ customFields: { type: [Object], optional: true, + defaultValue: [], }, 'customFields.$': { type: new SimpleSchema({ _id: { type: String, + optional: true, + defaultValue: '', }, value: { type: Match.OneOf(String, Number, Boolean, Date), optional: true, + defaultValue: '', }, }), }, @@ -70,22 +82,28 @@ Cards.attachSchema(new SimpleSchema({ description: { type: String, optional: true, + defaultValue: '', }, requestedBy: { type: String, optional: true, + defaultValue: '', + }, assignedBy: { type: String, optional: true, + defaultValue: '', }, labelIds: { type: [String], optional: true, + defaultValue: '', }, members: { type: [String], optional: true, + defaultValue: [], }, receivedAt: { type: Date, @@ -107,6 +125,7 @@ Cards.attachSchema(new SimpleSchema({ type: Number, decimal: true, optional: true, + defaultValue: 0, }, isOvertime: { type: Boolean, @@ -126,6 +145,7 @@ Cards.attachSchema(new SimpleSchema({ sort: { type: Number, decimal: true, + defaultValue: '', }, subtaskSort: { type: Number, @@ -135,10 +155,12 @@ Cards.attachSchema(new SimpleSchema({ }, type: { type: String, + defaultValue: '', }, linkedId: { type: String, optional: true, + defaultValue: '', }, })); diff --git a/package.json b/package.json index 2514b130..03c1346a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "1.29.0", + "version": "1.30.0", "description": "The open-source Trello-like kanban", "private": true, "scripts": { diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index 239e1640..c07f3212 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 114, + appVersion = 115, # Increment this for every release. - appMarketingVersion = (defaultText = "1.29.0~2018-08-12"), + appMarketingVersion = (defaultText = "1.30.0~2018-08-14"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0, @@ -242,6 +242,8 @@ const myCommand :Spk.Manifest.Command = ( (key = "MATOMO_SITE_ID", value=""), (key = "MATOMO_DO_NOT_TRACK", value="false"), (key = "MATOMO_WITH_USERNAME", value="true"), + (key = "BROWSER_POLICY_ENABLED", value="true"), + (key = "TRUSTED_URL", value=""), (key = "SANDSTORM", value = "1"), (key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}") ] diff --git a/server/policy.js b/server/policy.js index 17c90c1c..344e42e2 100644 --- a/server/policy.js +++ b/server/policy.js @@ -1,9 +1,33 @@ import { BrowserPolicy } from 'meteor/browser-policy-common'; Meteor.startup(() => { + + if ( process.env.BROWSER_POLICY_ENABLED === 'true' ) { + // Trusted URL that can embed Wekan in iFrame. + const trusted = process.env.TRUSTED_URL; + BrowserPolicy.framing.disallow(); + BrowserPolicy.content.disallowInlineScripts(); + BrowserPolicy.content.disallowEval(); + BrowserPolicy.content.allowInlineStyles(); + BrowserPolicy.content.allowFontDataUrl(); + BrowserPolicy.framing.restrictToOrigin(trusted); + BrowserPolicy.content.allowScriptOrigin(trusted); + } + else { + // Disable browser policy and allow all framing and including. + // Use only at internal LAN, not at Internet. + BrowserPolicy.framing.allowAll(); + BrowserPolicy.content.allowDataUrlForAll(); + } + + // Allow all images from anywhere + BrowserPolicy.content.allowImageOrigin('*'); + + // If Matomo URL is set, allow it. const matomoUrl = process.env.MATOMO_ADDRESS; if (matomoUrl){ BrowserPolicy.content.allowScriptOrigin(matomoUrl); BrowserPolicy.content.allowImageOrigin(matomoUrl); } + }); diff --git a/snap-src/bin/config b/snap-src/bin/config index 9aa2841e..2c50c074 100755 --- a/snap-src/bin/config +++ b/snap-src/bin/config @@ -3,7 +3,7 @@ # All supported keys are defined here together with descriptions and default values # list of supported keys -keys="MONGODB_BIND_UNIX_SOCKET MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME" +keys="MONGODB_BIND_UNIX_SOCKET MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME BROWSER_POLICY_ENABLED TRUSTED_URL" # default values DESCRIPTION_MONGODB_BIND_UNIX_SOCKET="mongodb binding unix socket:\n"\ @@ -67,3 +67,13 @@ KEY_MATOMO_DO_NOT_TRACK="matomo-do-not-track" DESCRIPTION_MATOMO_WITH_USERNAME="The option that allows matomo to retrieve the username" DEFAULT_MATOMO_WITH_USERNAME="false" KEY_MATOMO_WITH_USERNAME="matomo-with-username" + +DESCRIPTION_BROWSER_POLICY_ENABLED="Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.\n"\ +"\t\t\t Setting this to false is not recommended, it also disables all other browser policy protections\n"\ +"\t\t\t and allows all iframing etc. See wekan/server/policy.js" +DEFAULT_BROWSER_POLICY_ENABLED="true" +KEY_BROWSER_POLICY_ENABLED="browser-policy-enabled" + +DESCRIPTION_TRUSTED_URL="When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside." +DEFAULT_TRUSTED_URL="" +KEY_TRUSTED_URL="trusted-url" diff --git a/snap-src/bin/wekan-help b/snap-src/bin/wekan-help index 5c3f9b31..49270fb2 100755 --- a/snap-src/bin/wekan-help +++ b/snap-src/bin/wekan-help @@ -32,6 +32,21 @@ echo -e "To enable the API of wekan:" echo -e "\t$ snap set $SNAP_NAME WITH_API='true'" echo -e "\t-Disable the API:" echo -e "\t$ snap set $SNAP_NAME WITH_API='false'" +echo -e "\n" +echo -e "Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside." +echo -e "\t\t Setting this to false is not recommended, it also disables all other browser policy protections" +echo -e "\t\t and allows all iframing etc. See wekan/server/policy.js" +echo -e "To enable the Content Policy of Wekan:" +echo -e "\t$ snap set $SNAP_NAME CONTENT_POLICY_ENABLED='true'" +echo -e "\t-Disable the Content Policy of Wekan:" +echo -e "\t$ snap set $SNAP_NAME CONTENT_POLICY_ENABLED='false'" +echo -e "\n" +echo -e "When browser policy is enabled, HTML code at this URL can have iframe that embeds Wekan inside." +echo -e "To enable the Trusted URL of Wekan:" +echo -e "\t$ snap set $SNAP_NAME TRUSTED_URL='https://example.com'" +echo -e "\t-Disable the Trusted URL of Wekan:" +echo -e "\t$ snap set $SNAP_NAME TRUSTED_URL=''" +echo -e "\n" # parse config file for supported settings keys echo -e "wekan supports settings keys" echo -e "values can be changed by calling\n$ snap set $SNAP_NAME <key name>='<key value>'" |