Browse Source

Merge branch 'devel'

Lauri Ojansivu 7 years ago
parent
commit
ac12aa7ede
13 changed files with 626 additions and 526 deletions
  1. 14 0
      CHANGELOG.md
  2. 4 1
      Dockerfile
  3. 507 507
      client/lib/filter.js
  4. 4 0
      config/models.js
  5. 6 0
      docker-compose.yml
  6. 7 7
      i18n/de.i18n.json
  7. 7 7
      i18n/zh-CN.i18n.json
  8. 22 0
      models/cards.js
  9. 1 1
      package.json
  10. 4 2
      sandstorm-pkgdef.capnp
  11. 24 0
      server/policy.js
  12. 11 1
      snap-src/bin/config
  13. 15 0
      snap-src/bin/wekan-help

+ 14 - 0
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:

+ 4 - 1
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

+ 507 - 507
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);
+Blaze.registerHelper('Filter', Filter);

+ 4 - 0
config/models.js

@@ -0,0 +1,4 @@
+module.exports.models = {
+  connection: 'mongodb',
+  migrate: 'safe',
+};

+ 6 - 0
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
 

+ 7 - 7
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",

+ 7 - 7
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导入看板",

+ 22 - 0
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: '',
   },
 }));
 

+ 1 - 1
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": {

+ 4 - 2
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}}")
   ]

+ 24 - 0
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);
   }
+
 });

+ 11 - 1
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"

+ 15 - 0
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>'"