Repository: syncthing/syncthing (language: go)

Issue:
GUI editor for xattr filter patterns
The per-folder xattr filter is a complex object which is not by itself editable in the advanced config editor, and there's nothing custom built for it in the normal settings editor, so it's currently unavailable except for hacking the config file manually.

There should be an editor for it in the normal settings dialog, under the xattr enablement checkboxes. There is should be possible to add a list of patterns, each with an allow/deny checkbox.

The default is to allow all attributes if there are no patterns configured; this should be indicated in the GUI. When there are any patterns at all, the default (for things not matched by any pattern) becomes deny; this should also be indicated, especially if the user just adds one deny-pattern -- we should then suggest adding a `permit *` pattern at the end, maybe even add it by default and allow it to be removed or edited, or something like that.


Discussion / hints:
A note about recommended patterns based on the platform would be useful too. For example, on macOS, `com.apple.provenance` appears to be "untouchable" by any process and so should probably be part of a default exclusion list (or at least strongly suggested).

Some other potentially useful patterns for macOS:
```
com.apple.quarantine
com.apple.macl
com.apple.lastuseddate#PS
```

Fix patch:
diff --git a/gui/default/assets/css/overrides.css b/gui/default/assets/css/overrides.css
index 47147f3d2ca..06f705629a4 100644
--- a/gui/default/assets/css/overrides.css
+++ b/gui/default/assets/css/overrides.css
@@ -144,6 +144,10 @@ table.table-auto td {
     max-width: 0px;
 }
 
+td input[type="checkbox"] {
+    margin-top: 13px;
+}
+
 /* Remote Devices connection-quality indicator */
 .reception-0 {
     background: url('../../vendor/bootstrap/fonts/reception-0.svg') no-repeat;
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index f61f0b0cf7a..495e4e1b071 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/gui/default/syncthing/core/syncthingController.js
@@ -2301,6 +2301,7 @@ angular.module('syncthing.core')
                 return;
             }
 
+            $scope.validateXattrFilter();
             var folderCfg = angular.copy($scope.currentFolder);
             $scope.currentSharing.selected[$scope.myID] = true;
             var newDevices = [];
@@ -3330,6 +3331,84 @@ angular.module('syncthing.core')
 
             $scope.showTemporaryTooltip(event, message);
         };
+
+        $scope.newXattrEntry = function () {
+            var entries = $scope.currentFolder.xattrFilter.entries;
+            var newEntry = {match: '', permit: false};
+
+            if (entries.some(function (n) {
+                return n.match == '';
+            })) {
+                return;
+            }
+
+            if (entries.length > 0 && entries[entries.length -1].match === '*') {
+                if (newEntry.match !== '*') {
+                    entries.splice(entries.length - 1, 0, newEntry);
+                }
+
+                return;
+            }
+
+            entries.push(newEntry);
+        };
+
+        $scope.removeXattrEntry = function (entry) {
+            $scope.currentFolder.xattrFilter.entries = $scope.currentFolder.xattrFilter.entries.filter(function (n) {
+                return n !== entry;
+            });
+        };
+
+        $scope.getXattrHint = function () {
+            var xattrFilter = $scope.currentFolder.xattrFilter;
+            if (xattrFilter == null || xattrFilter == {}) {
+                return '';
+            }
+            var filterEntries = xattrFilter.entries;
+            if (filterEntries.length === 0) {
+                return '';
+            }
+
+            // When the user explicitely added a wild-card, we don't show hints.
+            if (filterEntries.length === 1 && filterEntries[0].match === '*') {
+                return '';
+            }
+            // If all the filter entries are 'deny', we suggest adding a permit-any
+            // rule in the end since the default is already deny in that case.
+            if (filterEntries.every(function (entry) {
+                return entry.permit === false;
+            })) {
+                return  $translate.instant('Hint: only deny-rules detected while the default is deny. Consider adding "permit any" as last rule.');
+            }
+
+            return '';
+        };
+
+        $scope.getXattrDefault = function () {
+            var xattrFilter = $scope.currentFolder.xattrFilter;
+            if (xattrFilter == null || xattrFilter == {}) {
+                return '';
+            }
+
+            var filterEntries = xattrFilter.entries;
+            // No entries present, default is thus 'allow'
+            if (filterEntries.length === 0) {
+                return $translate.instant('permit');
+            }
+            // If any rule is present and the last entry isn't a wild-card, the default is deny.
+            if (filterEntries[filterEntries.length -1].match !== '*') {
+                return $translate.instant('deny');
+            }
+
+            return '';
+        };
+
+        $scope.validateXattrFilter = function () {
+            // Fitlering out empty rules when saving the config
+            $scope.currentFolder.xattrFilter.entries = $scope.currentFolder.xattrFilter.entries.filter(function (n) {
+                return n.match !== "";
+            });
+        };
     })
     .directive('shareTemplate', function () {
         return {
diff --git a/gui/default/syncthing/folder/editFolderModalView.html b/gui/default/syncthing/folder/editFolderModalView.html
index a21c45472c1..6564479d0f9 100644
--- a/gui/default/syncthing/folder/editFolderModalView.html
+++ b/gui/default/syncthing/folder/editFolderModalView.html
@@ -323,7 +323,63 @@
               </p>
             </div>
           </div>
+
+          <div class="row" ng-if="currentFolder.syncXattrs">
+            <div class="col-md-12">
+              <p>
+                <label translate>Extended Attributes Filter</label>
+                &nbsp;<a href="{{docsURL('advanced/folder-xattr-filter')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
+              </p>
+            </div>
+            <div class="col-md-6">
+              <p translate class="help-block">
+                To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.
+              </p>
+              <label translate>Active filter rules</label>
+              <table class="table table-condensed">
+                <colgroup>
+                  <col class="col-xs-1 center"/>
+                  <col class="col-xs-9"/>
+                  <col class="col-xs-2"/>
+                </colgroup>
+                <tr ng-repeat="entry in currentFolder.xattrFilter.entries">
+                  <td>
+                    <input type="checkbox" ng-model="entry.permit">
+                  </td>
+                  <td><input class="form-control text-left" aria-required="true" ng-model="entry.match"/></td>
+                  <td>
+                    <button type="button" class="btn btn-default form-control" ng-click="removeXattrEntry(entry)">
+                      <span class="fas fa-trash-alt"></span>
+                    </button>
+                  </td>
+                </tr>
+              </table>
+              <div class="form-group">
+                <button type="button" class="btn btn-default" ng-click="newXattrEntry()">
+                  <span class="fas fa-plus"></span>&nbsp;<span translate>Add filter entry</span>
+                </button>
+              </div>
+              <p ng-if="currentFolder.xattrFilter.entries.length === 0">
+                <i translate>No rules set</i>
+              </p>
+              <p ng-if="getXattrDefault() !== ''">
+                <i><span translate>Default</span>: {{getXattrDefault()}}</i>
+              </p>
+              <p ng-if="getXattrHint() !== ''">
+                <i>{{getXattrHint()}}</i>
+              </p>
+            </div>
+            <div class="col-md-6 form-group">
+              <label for="xattrMaxSingleEntrySize" translate>Maximum single entry size</label>
+              <input name="xattrMaxSingleEntrySize" id="xattrMaxSingleEntrySize" class="form-control" type="number" ng-model="currentFolder.xattrFilter.maxSingleEntrySize" required="" aria-required="true" min="0" />
+            </div>
+            <div class="col-md-6 form-group">
+              <label for="xattrMaxTotalSize" translate>Maximum total size</label>
+              <input name="xattrMaxTotalSize" id="xattrMaxTotalSize" class="form-control" type="number" ng-model="currentFolder.xattrFilter.maxTotalSize" required="" aria-required="true" min="0" />
+            </div>
+          </div>
         </div>
+
       </div>
     </form>
   </div>


Test patch: