summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2018-08-14 15:19:53 +0300
committerLauri Ojansivu <x@xet7.org>2018-08-14 15:19:53 +0300
commitac12aa7edeb1ea6ec63d6bce795e2f042032ff56 (patch)
tree7ec8dcb853f2ebc792933290280e47236588d80d
parent034981cd3d130b18f3afe45ae340490c06af6e47 (diff)
parentcec5f4545922d3c90186e73e8658b745472d1c44 (diff)
downloadwekan-ac12aa7edeb1ea6ec63d6bce795e2f042032ff56.tar.gz
wekan-ac12aa7edeb1ea6ec63d6bce795e2f042032ff56.tar.bz2
wekan-ac12aa7edeb1ea6ec63d6bce795e2f042032ff56.zip
Merge branch 'devel'
-rw-r--r--CHANGELOG.md14
-rw-r--r--Dockerfile5
-rw-r--r--client/lib/filter.js1014
-rw-r--r--config/models.js4
-rw-r--r--docker-compose.yml6
-rw-r--r--i18n/de.i18n.json14
-rw-r--r--i18n/zh-CN.i18n.json14
-rw-r--r--models/cards.js22
-rw-r--r--package.json2
-rw-r--r--sandstorm-pkgdef.capnp6
-rw-r--r--server/policy.js24
-rwxr-xr-xsnap-src/bin/config12
-rwxr-xr-xsnap-src/bin/wekan-help15
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:
diff --git a/Dockerfile b/Dockerfile
index 39002070..a548adf1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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>'"