apostrophe 2.225.0 → 2.226.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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.226.0 (2023-03-06)
4
+
5
+ ### Adds
6
+
7
+ * 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).
8
+
9
+ ### Security
10
+
11
+ * 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`.
12
+
3
13
  ## 2.225.0 (2023-02-17)
4
14
 
5
15
  ### 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,109 @@ 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
+ };
673
698
 
699
+ self.enableConditionalFields = function(data, name, $field, $el, field, actionName, callback) {
700
+ var eventName = 'apos' + apos.utils.capitalizeFirst(actionName);
674
701
  var $fieldset = self.findFieldset($el, name);
675
702
 
676
- // afterChange shows and hides other fieldsets based on
677
- // the current value of this field and its visibility.
703
+ // afterChange shows and hides or disables other fieldsets based on
704
+ // the current value of this field..
678
705
  // We do this in three situations: at startup, when the
679
- // user changes the value, and when the visibility of this
706
+ // user changes the value, and when the visibility/disability of this
680
707
  // field has been affected by another field with the
681
- // showFields option. This allows nested showFields to
682
- // work properly. -Tom
708
+ // showFields/readOnlyFields option.
709
+ // This allows nested showFields/readOnlyFields to work properly. -Tom
683
710
 
684
711
  afterChange();
685
712
 
686
713
  $field.on('change', afterChange);
687
- $fieldset.on('aposShowFields', afterChange);
688
- function afterChange() {
689
- // Implement showFields
714
+ $fieldset.on(eventName, afterChange);
690
715
 
716
+ function afterChange() {
691
717
  if (!_.find(field.choices || [], function(choice) {
692
- return choice.showFields;
718
+ return choice[actionName];
693
719
  })) {
694
- // showFields is not in use for this select
695
720
  return;
696
721
  }
697
722
 
698
- var val;
699
- if (field.checkbox) {
700
- val = $field.is(':checked');
701
- } else {
702
- val = $field.val();
703
- }
723
+ var val = field.checkbox
724
+ ? $field.is(':checked')
725
+ : $field.val();
704
726
 
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 = {};
727
+ // Recall if another choice currently active already chose to show/disable each field.
728
+ // That way, if two choices show/disable the same field, the fact that the second one
729
+ // is not currently selected does not hide/enable the field the first one just showed/disabled
730
+ var memo = {};
709
731
 
710
732
  _.each(field.choices || [], function(choice) {
733
+ var match;
711
734
 
712
- // Show the fields for this value if it is the current value
735
+ // Show/disable the fields for this value if it is the current value
713
736
  // *and* the select field itself is currently visible
714
-
715
- var show;
716
-
717
737
  if ($fieldset.hasClass('apos-hidden')) {
718
- show = false;
738
+ match = false;
719
739
  } else if (field.type === 'boolean') {
720
740
  // Comparing boolean values is hard because
721
741
  // the string '0' must be considered falsy in
722
742
  // order to permit use of select elements. -Tom
723
743
  if (val === choice.value) {
724
- show = true;
744
+ match = true;
725
745
  } else if (!choice.value) {
726
746
  if ((!val) || (val === '0')) {
727
- show = true;
747
+ match = true;
728
748
  }
729
749
  } else {
730
750
  if (val && (val !== '0')) {
731
- show = true;
751
+ match = true;
732
752
  }
733
753
  }
734
754
  } else if (field.type === 'checkboxes') {
735
755
  _.each($field || [], function(checkbox) {
736
- if (checkbox.checked && checkbox.value === choice.value && choice.showFields) {
737
- show = true;
756
+ if (checkbox.checked && checkbox.value === choice.value && choice[actionName]) {
757
+ match = true;
738
758
  }
739
759
  });
740
760
  } else {
741
761
  // type select
742
762
  if (val === choice.value.toString()) {
743
- show = true;
763
+ match = true;
744
764
  }
745
765
  }
746
766
 
747
- _.each(choice.showFields || [], function(fieldName) {
748
- if (show || shown[fieldName]) {
749
- shown[fieldName] = true;
750
- } else {
751
- shown[fieldName] = false;
752
- }
767
+ _.each(choice[actionName] || [], function(fieldName) {
768
+ memo[fieldName] = match || !!memo[fieldName];
769
+
753
770
  var $fieldset = self.findFieldset($el, fieldName);
754
771
 
755
- $fieldset.toggleClass('apos-hidden', !shown[fieldName]);
756
- $fieldset.trigger('aposShowFields');
772
+ callback($fieldset, memo[fieldName]);
773
+ $fieldset.trigger(eventName);
757
774
  });
758
-
759
775
  });
760
-
761
776
  }
762
777
  };
763
778
 
@@ -1277,6 +1292,7 @@ apos.define('apostrophe-schemas', {
1277
1292
  $field.val('0');
1278
1293
  }
1279
1294
  self.enableShowFields(data, name, $field, $el, field);
1295
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1280
1296
  return setImmediate(callback);
1281
1297
  },
1282
1298
  convert: function(data, name, $field, $el, field, callback) {
@@ -1340,6 +1356,7 @@ apos.define('apostrophe-schemas', {
1340
1356
  self.findSafe($fieldset, 'input[name="' + name + '"][value="' + data[name][c] + '"]', '.apos-field').prop('checked', true);
1341
1357
  }
1342
1358
  self.enableShowFields(data, name, $field, $el, field);
1359
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1343
1360
  return setImmediate(callback);
1344
1361
  }
1345
1362
  },
@@ -1448,6 +1465,7 @@ apos.define('apostrophe-schemas', {
1448
1465
  }
1449
1466
  $field.val(value);
1450
1467
  self.enableShowFields(data, name, $field, $el, field);
1468
+ self.enableReadOnlyFields(data, name, $field, $el, field);
1451
1469
  return setImmediate(callback);
1452
1470
  }
1453
1471
  },
@@ -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">
@@ -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,20 @@
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-2;
309
+ width: 100%;
310
+ height: 100%;
311
+ background-color: rgba(255, 255, 255, 0.75);
312
+ }
313
+
314
+ .apos-field-readonly-overlay--active {
315
+ display: block;
316
+ }
317
+
303
318
  // Browse and autocomplete combo
304
319
  .apos-browse-and-autocomplete {
305
320
  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.226.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
- }