apostrophe 2.225.0 → 2.227.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.227.0 (2023-03-16)
4
+
5
+ ### Adds
6
+
7
+ * Add a read-only overlay to prevent users from interacting with areas set as read-only, as it was done for checkboxes and color field types.
8
+
9
+ ## 2.226.0 (2023-03-06)
10
+
11
+ ### Adds
12
+
13
+ * Add `readOnlyFields` option. Exactly like `showFields`, a boolean/select/checkboxes field in the document can control whether other fields are read-only or not (as opposed to visible or not).
14
+
15
+ ### Security
16
+
17
+ * Upgrades passport to the latest version in order to ensure session regeneration when logging in or out. This adds additional security to logins by mitigating any risks due to XSS attacks. Apostrophe is already robust against XSS attacks. For passport methods that are internally used by Apostrophe everything is still working. For projects that are accessing the passport instance directly through `self.apos.login.passport`, some verifications may be necessary to avoid any compatibility issue. The internally used methods are `authenticate`, `use`, `serializeUser`, `deserializeUser`, `initialize`, `session`.
18
+
3
19
  ## 2.225.0 (2023-02-17)
4
20
 
5
21
  ### Adds
@@ -485,7 +485,7 @@ module.exports = {
485
485
  }
486
486
  var errors = {};
487
487
  return async.eachSeries(schema, function(field, callback) {
488
- if (field.readOnly) {
488
+ if (field.readOnly || self.isReadOnly(schema, object, field.name)) {
489
489
  return setImmediate(callback);
490
490
  }
491
491
  // Fields that are contextual are edited in the context of a
@@ -545,41 +545,66 @@ module.exports = {
545
545
  // based on showFields options of all fields
546
546
 
547
547
  self.isVisible = function(schema, object, name) {
548
- var hidden = {};
549
- _.each(schema, function(field) {
550
- if (!_.find(field.choices || [], function(choice) {
551
- return choice.showFields;
552
- })) {
548
+ const transform = value => !value;
549
+ const buildWarningMessage = name => `⚠️ showFields misconfigured, attempts to show/hide ${name} which does not exist`;
550
+
551
+ return self.doesMatchConditionalChoices(schema, object, name, 'showFields', transform, buildWarningMessage);
552
+ };
553
+
554
+ // Determine whether the given field is read-only
555
+ // based on readOnlyFields options of all fields
556
+
557
+ self.isReadOnly = function(schema, object, name) {
558
+ const transform = value => value;
559
+ const buildWarningMessage = name => `⚠️ readOnlyFields misconfigured, attempts to set ${name} which does not exist as read only`;
560
+
561
+ return self.doesMatchConditionalChoices(schema, object, name, 'readOnlyFields', transform, buildWarningMessage);
562
+ };
563
+
564
+ self.doesMatchConditionalChoices = function(schema, object, name, actionName, transform, buildWarningMessage) {
565
+ var memo = {};
566
+
567
+ _.each(schema, field => {
568
+ if (!_.find(field.choices || [], choice => choice[actionName])) {
553
569
  return;
554
570
  }
555
- _.each(field.choices, function(choice) {
556
- if (choice.showFields) {
557
- if (field.type === 'checkboxes') {
558
- if (!object[field.name].includes(choice.value)) {
559
- _.each(choice.showFields, hide);
560
- }
561
- } else if (object[field.name] !== choice.value) {
562
- _.each(choice.showFields, hide);
571
+
572
+ _.each(field.choices, choice => {
573
+ if (!choice[actionName]) {
574
+ return;
575
+ }
576
+
577
+ if (field.type === 'checkboxes') {
578
+ if (object[field.name] && transform(object[field.name].includes(choice.value))) {
579
+ _.each(choice[actionName], action);
563
580
  }
581
+
582
+ return;
583
+ }
584
+
585
+ if (transform(object[field.name] === choice.value)) {
586
+ _.each(choice[actionName], action);
564
587
  }
565
588
  });
566
589
  });
567
- return !hidden[name];
568
590
 
569
- function hide(name) {
570
- hidden[name] = true;
571
- // Cope with nested showFields
572
- var field = _.find(schema, { name: name });
591
+ return transform(memo[name]);
592
+
593
+ function action(name) {
594
+ memo[name] = true;
595
+
596
+ var field = _.find(schema, { name });
573
597
  if (!field) {
574
598
  // Do not crash. The linter at startup also catches this,
575
599
  // but is a nonfatal warning for bc, so we should also catch it
576
- self.apos.utils.warnDev('⚠️ showFields misconfigured, attempts to show/hide ' + name + ' which does not exist');
600
+ self.apos.utils.warnDev(buildWarningMessage(name));
601
+
577
602
  return;
578
603
  }
579
- _.each(field.choices || [], function(choice) {
580
- _.each(choice.showFields || [], function(name) {
581
- hide(name);
582
- });
604
+
605
+ _.each(field.choices || [], choice => {
606
+ // Cope with nested showFields/readOnlyFields
607
+ _.each(choice[actionName] || [], action);
583
608
  });
584
609
  }
585
610
  };
@@ -670,94 +670,138 @@ apos.define('apostrophe-schemas', {
670
670
  };
671
671
 
672
672
  self.enableShowFields = function(data, name, $field, $el, field) {
673
+ self.enableConditionalFields(data, name, $field, $el, field, 'showFields', function ($fieldset, match) {
674
+ $fieldset.toggleClass('apos-hidden', !match);
675
+ });
676
+ };
677
+
678
+ self.enableReadOnlyFields = function(data, name, $field, $el, field) {
679
+ self.enableConditionalFields(data, name, $field, $el, field, 'readOnlyFields', function ($fieldset, match) {
680
+ // Disable inputs, and color picker via the spectrum API
681
+ $fieldset
682
+ .find('input')
683
+ .attr('disabled', match)
684
+ .filter('[data-apos-color]')
685
+ .spectrum(match ? 'disable' : 'enable');
686
+
687
+ // Disable select element
688
+ $fieldset
689
+ .find('select')
690
+ .attr('disabled', match);
691
+
692
+ // Add a visual overlay to show that the field is read-only
693
+ $fieldset
694
+ .find('.apos-field-readonly-overlay')
695
+ .toggleClass('apos-field-readonly-overlay--active', match);
696
+
697
+ // Hide area's controls
698
+ if ($fieldset.hasClass('apos-field-area')) {
699
+ waitForEl($fieldset, '[data-apos-widget-controls]', 10, function($el) {
700
+ $el.toggleClass('apos-area-widget-controls--disabled', match);
701
+ });
702
+ waitForEl($fieldset, '.apos-ui', 10, function($el) {
703
+ $el.css('display', match ? 'none' : 'block');
704
+ });
705
+ }
706
+
707
+ function waitForEl($scope, selector, maxRetries, callback) {
708
+ if (this.retries > maxRetries) {
709
+ return;
710
+ }
711
+
712
+ var $el = $scope.find(selector);
713
+ if ($el.length) {
714
+ callback($el);
715
+ return;
716
+ }
717
+
718
+ setTimeout(function() {
719
+ waitForEl($scope, selector, maxRetries, callback);
720
+ }, 100);
721
+
722
+ this.retries = this.retries || {};
723
+ this.retries[selector] = this.retries[selector] ? ++this.retries[selector] : 1;
724
+ };
725
+ });
726
+ };
673
727
 
728
+ self.enableConditionalFields = function(data, name, $field, $el, field, actionName, callback) {
729
+ var eventName = 'apos' + apos.utils.capitalizeFirst(actionName);
674
730
  var $fieldset = self.findFieldset($el, name);
675
731
 
676
- // afterChange shows and hides other fieldsets based on
677
- // the current value of this field and its visibility.
732
+ // afterChange shows and hides or disables other fieldsets based on
733
+ // the current value of this field..
678
734
  // We do this in three situations: at startup, when the
679
- // user changes the value, and when the visibility of this
735
+ // user changes the value, and when the visibility/disability of this
680
736
  // field has been affected by another field with the
681
- // showFields option. This allows nested showFields to
682
- // work properly. -Tom
737
+ // showFields/readOnlyFields option.
738
+ // This allows nested showFields/readOnlyFields to work properly. -Tom
683
739
 
684
740
  afterChange();
685
741
 
686
742
  $field.on('change', afterChange);
687
- $fieldset.on('aposShowFields', afterChange);
688
- function afterChange() {
689
- // Implement showFields
743
+ $fieldset.on(eventName, afterChange);
690
744
 
745
+ function afterChange() {
691
746
  if (!_.find(field.choices || [], function(choice) {
692
- return choice.showFields;
747
+ return choice[actionName];
693
748
  })) {
694
- // showFields is not in use for this select
695
749
  return;
696
750
  }
697
751
 
698
- var val;
699
- if (field.checkbox) {
700
- val = $field.is(':checked');
701
- } else {
702
- val = $field.val();
703
- }
752
+ var val = field.checkbox
753
+ ? $field.is(':checked')
754
+ : $field.val();
704
755
 
705
- // Recall if another choice currently active already chose to show each field.
706
- // That way, if two choices show the same field, the fact that the second one
707
- // is not currently selected does not hide the field the first one just showed
708
- var shown = {};
756
+ // Recall if another choice currently active already chose to show/disable each field.
757
+ // That way, if two choices show/disable the same field, the fact that the second one
758
+ // is not currently selected does not hide/enable the field the first one just showed/disabled
759
+ var memo = {};
709
760
 
710
761
  _.each(field.choices || [], function(choice) {
762
+ var match;
711
763
 
712
- // Show the fields for this value if it is the current value
764
+ // Show/disable the fields for this value if it is the current value
713
765
  // *and* the select field itself is currently visible
714
-
715
- var show;
716
-
717
766
  if ($fieldset.hasClass('apos-hidden')) {
718
- show = false;
767
+ match = false;
719
768
  } else if (field.type === 'boolean') {
720
769
  // Comparing boolean values is hard because
721
770
  // the string '0' must be considered falsy in
722
771
  // order to permit use of select elements. -Tom
723
772
  if (val === choice.value) {
724
- show = true;
773
+ match = true;
725
774
  } else if (!choice.value) {
726
775
  if ((!val) || (val === '0')) {
727
- show = true;
776
+ match = true;
728
777
  }
729
778
  } else {
730
779
  if (val && (val !== '0')) {
731
- show = true;
780
+ match = true;
732
781
  }
733
782
  }
734
783
  } else if (field.type === 'checkboxes') {
735
784
  _.each($field || [], function(checkbox) {
736
- if (checkbox.checked && checkbox.value === choice.value && choice.showFields) {
737
- show = true;
785
+ if (checkbox.checked && checkbox.value === choice.value && choice[actionName]) {
786
+ match = true;
738
787
  }
739
788
  });
740
789
  } else {
741
790
  // type select
742
791
  if (val === choice.value.toString()) {
743
- show = true;
792
+ match = true;
744
793
  }
745
794
  }
746
795
 
747
- _.each(choice.showFields || [], function(fieldName) {
748
- if (show || shown[fieldName]) {
749
- shown[fieldName] = true;
750
- } else {
751
- shown[fieldName] = false;
752
- }
796
+ _.each(choice[actionName] || [], function(fieldName) {
797
+ memo[fieldName] = match || !!memo[fieldName];
798
+
753
799
  var $fieldset = self.findFieldset($el, fieldName);
754
800
 
755
- $fieldset.toggleClass('apos-hidden', !shown[fieldName]);
756
- $fieldset.trigger('aposShowFields');
801
+ callback($fieldset, memo[fieldName]);
802
+ $fieldset.trigger(eventName);
757
803
  });
758
-
759
804
  });
760
-
761
805
  }
762
806
  };
763
807
 
@@ -1277,6 +1321,7 @@ apos.define('apostrophe-schemas', {
1277
1321
  $field.val('0');
1278
1322
  }
1279
1323
  self.enableShowFields(data, name, $field, $el, field);
1324
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1280
1325
  return setImmediate(callback);
1281
1326
  },
1282
1327
  convert: function(data, name, $field, $el, field, callback) {
@@ -1340,6 +1385,7 @@ apos.define('apostrophe-schemas', {
1340
1385
  self.findSafe($fieldset, 'input[name="' + name + '"][value="' + data[name][c] + '"]', '.apos-field').prop('checked', true);
1341
1386
  }
1342
1387
  self.enableShowFields(data, name, $field, $el, field);
1388
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1343
1389
  return setImmediate(callback);
1344
1390
  }
1345
1391
  },
@@ -1448,6 +1494,7 @@ apos.define('apostrophe-schemas', {
1448
1494
  }
1449
1495
  $field.val(value);
1450
1496
  self.enableShowFields(data, name, $field, $el, field);
1497
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1451
1498
  return setImmediate(callback);
1452
1499
  }
1453
1500
  },
@@ -137,6 +137,7 @@
137
137
  {%- endmacro -%}
138
138
 
139
139
  {%- macro checkboxesBody(field) -%}
140
+ <div class="apos-field-readonly-overlay"></div>
140
141
  {%- for choice in field.choices -%}
141
142
  <div class="apos-form-checkbox">
142
143
  <label class="apos-form-checkbox-label apos-text-small">
@@ -204,6 +205,7 @@
204
205
  {%- endmacro -%}
205
206
 
206
207
  {%- macro singletonBody(field) -%}
208
+ <div class="apos-field-readonly-overlay"></div>
207
209
  {# js adds this singleton to the dialog #}
208
210
  <div data-{{ field.name }}-edit-view></div>
209
211
  {%- endmacro -%}
@@ -6,6 +6,7 @@
6
6
 
7
7
  .apos-field
8
8
  {
9
+ position: relative;
9
10
  margin-bottom: @apos-margin-4;
10
11
  width: 100%;
11
12
  max-width: 540px;
@@ -300,6 +301,22 @@
300
301
  display: inline-block;
301
302
  }
302
303
 
304
+ // Field readonly overlay ===================================
305
+ .apos-field-readonly-overlay {
306
+ display: none;
307
+ position: absolute;
308
+ z-index: @apos-z-index-max;
309
+ top: 0;
310
+ left: 0;
311
+ width: 100%;
312
+ height: 100%;
313
+ background-color: rgba(255, 255, 255, 0.75);
314
+ }
315
+
316
+ .apos-field-readonly-overlay--active {
317
+ display: block;
318
+ }
319
+
303
320
  // Browse and autocomplete combo
304
321
  .apos-browse-and-autocomplete {
305
322
  display: flex;
@@ -30,6 +30,7 @@
30
30
 
31
31
  {% macro color(name, placeholder, value, readOnly, options) -%}
32
32
  <label>
33
+ <div class="apos-field-readonly-overlay"></div>
33
34
  <div class="apos-field-input-color-preview" data-apos-color-preview></div>
34
35
  <input id="{{ options.id }}" name="{{ name }}" data-apos-color-empty-label="{{ __ns('apostrophe', "None selected") }}" class="apos-field-input apos-field-input-color{% if options.fieldClasses %} {{ options.fieldClasses }}{% endif %}" data-apos-color type="text" value="{{__ns('apostrophe', value | d(''))}}"{% if readOnly %} disabled{% endif %}{% if options.fieldAttributes %} {{ options.fieldAttributes }}{% endif %}>
35
36
  <span class="apos-field-input-colorpicker-value" data-apos-color-value></span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "2.225.0",
3
+ "version": "2.227.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -60,7 +60,7 @@
60
60
  "moog-require": "^1.1.0",
61
61
  "nodemailer": "^6.6.2",
62
62
  "oembetter": "^1.0.1",
63
- "passport": "^0.3.2",
63
+ "passport": "^0.6.0",
64
64
  "passport-local": "^1.0.0",
65
65
  "passport-totp": "0.0.2",
66
66
  "path-to-regexp": "^1.7.0",
package/test/schemas.js CHANGED
@@ -2129,4 +2129,110 @@ describe('Schemas', function() {
2129
2129
  });
2130
2130
  });
2131
2131
 
2132
+ it('should disregard read-only fields set by a boolean field', function(done) {
2133
+ var req = apos.tasks.getReq();
2134
+ var schema = apos.schemas.compose({
2135
+ addFields: [
2136
+ {
2137
+ name: 'age',
2138
+ type: 'integer'
2139
+ },
2140
+ {
2141
+ name: 'canEditAge',
2142
+ type: 'boolean',
2143
+ choices: [
2144
+ {
2145
+ value: true
2146
+ },
2147
+ {
2148
+ value: false,
2149
+ readOnlyFields: [ 'age' ]
2150
+ }
2151
+ ]
2152
+ }
2153
+ ]
2154
+ });
2155
+ var object = {
2156
+ age: 20,
2157
+ canEditAge: false
2158
+ };
2159
+ apos.schemas.convert(req, schema, 'form', { age: '30' }, object, function(err) {
2160
+ assert(!err);
2161
+ assert(object.age === 20);
2162
+ done();
2163
+ });
2164
+ });
2165
+
2166
+ it('should disregard read-only fields set by a select field', function(done) {
2167
+ var req = apos.tasks.getReq();
2168
+ var schema = apos.schemas.compose({
2169
+ addFields: [
2170
+ {
2171
+ name: 'age',
2172
+ type: 'integer'
2173
+ },
2174
+ {
2175
+ name: 'edit',
2176
+ type: 'select',
2177
+ choices: [
2178
+ {
2179
+ label: 'Can edit anything',
2180
+ value: 'canEditAnything'
2181
+ },
2182
+ {
2183
+ label: 'Cannot edit age',
2184
+ value: 'cannotEditAge',
2185
+ readOnlyFields: [ 'age' ]
2186
+ }
2187
+ ]
2188
+ }
2189
+ ]
2190
+ });
2191
+ var object = {
2192
+ age: 20,
2193
+ edit: 'cannotEditAge'
2194
+ };
2195
+ apos.schemas.convert(req, schema, 'form', { age: '30' }, object, function(err) {
2196
+ assert(!err);
2197
+ assert(object.age === 20);
2198
+ done();
2199
+ });
2200
+ });
2201
+
2202
+ it('should disregard read-only fields set by a checkboxes field', function(done) {
2203
+ var req = apos.tasks.getReq();
2204
+ var schema = apos.schemas.compose({
2205
+ addFields: [
2206
+ {
2207
+ name: 'age',
2208
+ type: 'integer'
2209
+ },
2210
+ {
2211
+ name: 'edit',
2212
+ type: 'checkboxes',
2213
+ choices: [
2214
+ {
2215
+ label: 'Can edit anything',
2216
+ value: 'canEditAnything'
2217
+ },
2218
+ {
2219
+ label: 'Cannot edit age',
2220
+ value: 'cannotEditAge',
2221
+ readOnlyFields: [ 'age' ]
2222
+ }
2223
+ ]
2224
+ }
2225
+ ]
2226
+ });
2227
+ var object = {
2228
+ age: 20,
2229
+ edit: 'cannotEditAge'
2230
+ };
2231
+ apos.schemas.convert(req, schema, 'form', { age: '30' }, object, function(err) {
2232
+ assert(!err);
2233
+ assert(object.age === 20);
2234
+ done();
2235
+ });
2236
+ });
2237
+
2132
2238
  });
package/test/package.json DELETED
@@ -1,73 +0,0 @@
1
- {
2
- "//": "Automatically generated to satisfy moog-require, do not edit",
3
- "dependencies": {
4
- "@apostrophecms/nunjucks": "^2.5.4",
5
- "@sailshq/lodash": "^3.10.4",
6
- "async": "^1.5.2",
7
- "bless": "^3.0.3",
8
- "bluebird": "^3.7.1",
9
- "body-parser": "^1.19.0",
10
- "cheerio": "^1.0.0-rc.10",
11
- "chokidar": "^3.5.1",
12
- "connect-flash": "^0.1.1",
13
- "connect-multiparty": "^2.2.0",
14
- "cookie-parser": "^1.4.5",
15
- "credentials": "^3.0.2",
16
- "cuid": "^1.3.8",
17
- "diff": "^4.0.1",
18
- "emulate-mongo-2-driver": "^1.2.3",
19
- "express": "^4.17.1",
20
- "express-session": "^1.17.0",
21
- "glob": "^5.0.15",
22
- "he": "^0.5.0",
23
- "heic-to-jpeg-middleware": "^2.0.0",
24
- "html-to-plaintext": "^0.1.1",
25
- "html-to-text": "^5.1.1",
26
- "i18n": "^0.8.6",
27
- "is-wsl": "^2.2.0",
28
- "joinr": "^1.0.2",
29
- "jpeg-exif": "^1.1.4",
30
- "launder": "^1.5.0",
31
- "less": "^3.13.1",
32
- "less-middleware": "^3.1.0",
33
- "minimatch": "^3.0.4",
34
- "mkdirp": "^1.0.3",
35
- "moment": "^2.29.1",
36
- "moog-require": "^1.1.0",
37
- "nodemailer": "^6.6.2",
38
- "oembetter": "^1.0.1",
39
- "passport": "^0.3.2",
40
- "passport-local": "^1.0.0",
41
- "passport-totp": "0.0.2",
42
- "path-to-regexp": "^1.7.0",
43
- "performance-now": "^2.1.0",
44
- "qs": "^6.9.6",
45
- "regexp-quote": "0.0.0",
46
- "request": "^2.88.2",
47
- "request-promise": "^4.2.4",
48
- "resolve": "^1.20.0",
49
- "rimraf": "^2.7.1",
50
- "sanitize-html": "^2.7.1",
51
- "server-destroy": "^1.0.1",
52
- "sluggo": "^0.2.0",
53
- "syntax-error": "^1.3.0",
54
- "thirty-two": "^1.0.2",
55
- "tinycolor2": "^1.4.1",
56
- "uglify-js": "^2.8.29",
57
- "underscore.string": "^3.3.5",
58
- "uploadfs": "^1.18.4",
59
- "xregexp": "^2.0.0",
60
- "yargs": "^3.32.0",
61
- "apostrophe": "^2.0.0"
62
- },
63
- "devDependencies": {
64
- "eslint": "^6.5.1",
65
- "eslint-config-apostrophe": "^2.0.2",
66
- "eslint-config-standard": "^11.0.0",
67
- "eslint-plugin-import": "^2.18.2",
68
- "eslint-plugin-node": "^6.0.1",
69
- "eslint-plugin-promise": "^3.8.0",
70
- "eslint-plugin-standard": "^3.1.0",
71
- "mocha": "^7.0.0"
72
- }
73
- }