apostrophe 2.220.4 → 2.220.9

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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.220.9 (2022-02-04)
4
+
5
+ ## Fixes
6
+
7
+ * Worked around bug preventing proper translation of the "Selected" message in certain modal operations. Thanks to [stepanjakl](https://github.com/stepanjakl).
8
+
9
+ ## 2.220.8 (2022-01-20)
10
+
11
+ ## Fixes
12
+
13
+ * Fixes overflowing issue in the editor modals when adding multiple options to an area. This used to cause a scroll to appear and the user had to scroll to see all the options.
14
+ * In the editor modal if there was an area with multiple items added (enough to make the page scrollable) when the user wanted to add another item another scroll bar would appear besides the already existing one and the user had to scroll to see all the options.
15
+ * Fixes i18n processing of a login throttling error message.
16
+
17
+ ## 2.220.7 (2021-10-13)
18
+
19
+ ## Fixes
20
+
21
+ * Avoid 500 errors when `joinByOne` field is hidden and required.
22
+ * Avoid errors at startup when formerly valid locales for the global doc are in the database, but not part of the current configuration.
23
+
24
+ ## 2.220.6 (2021-09-13)
25
+
26
+ ## Security
27
+
28
+ * SVG files can contain XSS attack vectors. Fortunately, these cannot be exploited when the SVG file is used in an `img` tag or as a CSS background, which is normally the case for SVGs uploaded to Apostrophe. However, Apostrophe does provide a "View File" button in the media manager which could load the file in a way that could trigger XSS attacks in a carefully crafted SVG intentionally designed to phish Apostrophe admins. To mitigate this risk, starting with version 2.220.6 the "View File" button downloads the SVG file to the local computer as an attachment. This removes it from the domain of the website, so that any embedded JavaScript cannot be used to trigger actions in Apostrophe. However please note that it is your responsibility to avoid the use of inline `svg` with untrusted SVG files, or the use of `iframe`, `embed` or `object` tags with untrusted SVG files. If you have not enabled the `svgImages` option to `apostrophe-attachments`, then your site does not accept SVG uploads and this risk is not relevant to you.
29
+
30
+ ## Fixes
31
+
32
+ * At least in a nightwatch regression testing context, `transitionend` events have been observed not to fire, resulting in unreliable test suites. A change has been made to accommodate this scenario with a fallback timer relating to indicating the current topmost modal.
33
+
34
+ ## 2.220.5 (2021-08-30)
35
+
36
+ ## Fixes
37
+
38
+ * The template error page was returning a 200 status code. It now correctly returns a 500 status code.
39
+
3
40
  ## 2.220.4 (2021-08-03)
4
41
 
5
42
  ## Fixes
@@ -1,4 +1,6 @@
1
- var _ = require('@sailshq/lodash');
1
+ const _ = require('@sailshq/lodash');
2
+ const request = require('request');
3
+ const path = require('path');
2
4
 
3
5
  module.exports = function(self, options) {
4
6
 
@@ -25,7 +27,8 @@ module.exports = function(self, options) {
25
27
  }
26
28
  return next(null, { file: file });
27
29
  });
28
- });
30
+ }
31
+ );
29
32
 
30
33
  // Crop a previously uploaded image, based on the `id` POST parameter
31
34
  // and the `crop` POST parameter. `id` should refer to an existing
@@ -65,4 +68,21 @@ module.exports = function(self, options) {
65
68
  });
66
69
  });
67
70
 
71
+ // Provides a simple route to download a file as an attachment, rather than
72
+ // viewing it. Pipes it from the regular public URL to avoid acting as a
73
+ // bypass of permissions
74
+ self.apiRoute('get', 'download', async (req, res) => {
75
+ const _id = self.apos.launder.id(req.query._id);
76
+ const info = await self.db.findOne({
77
+ _id: _id
78
+ });
79
+ if (!info) {
80
+ return res.status(404).send('notfound');
81
+ }
82
+ const url = self.url(info, { size: 'original' });
83
+ res.setHeader('Content-Disposition', `attachment; filename="${path.basename(url)}"`);
84
+ res.setHeader('Content-Type', 'application/octet-stream');
85
+ const resolvedUrl = (new URL(url, req.absoluteUrl)).toString();
86
+ await request(resolvedUrl).pipe(res);
87
+ });
68
88
  };
@@ -286,8 +286,13 @@ apos.define('apostrophe-attachments', {
286
286
  $existing.data('existing', info);
287
287
  $existing.data('field', field);
288
288
  $existing.attr('data-existing', info._id);
289
+ var $link = $existing.find('[data-link]');
289
290
  $existing.find('[data-name]').text(info.name);
290
- $existing.find('[data-link]').attr('href', self.url(info, {size: 'original'}));
291
+ if (info.extension === 'svg') {
292
+ $link.attr('href', self.action + '/download?_id=' + info._id);
293
+ } else {
294
+ $link.attr('href', self.url(info, {size: 'original'}));
295
+ }
291
296
  $existing.alterClass('apos-extension-*', 'apos-extension-' + info.extension);
292
297
  var $preview = $existing.find('[data-preview]');
293
298
  if (info.group === 'images') {
@@ -63,7 +63,14 @@ module.exports = function(self, options) {
63
63
  convert: function(callback) {
64
64
  // For purposes of previewing, it's OK to ignore readOnly so we can tell which
65
65
  // inputs are plausible
66
- return self.apos.schemas.convert(req, [ _.omit(field, 'readOnly') ], 'form', input, receptacle, callback);
66
+ return self.apos.schemas.convert(
67
+ req,
68
+ [_.omit(field, ['readOnly', 'required', 'min', 'max'])],
69
+ 'form',
70
+ input,
71
+ receptacle,
72
+ callback
73
+ );
67
74
  },
68
75
  join: function(callback) {
69
76
  return self.apos.schemas.join(req, [ field ], receptacle, true, callback);
@@ -144,6 +144,8 @@ module.exports = {
144
144
  self.initGlobal = function(callback) {
145
145
  var req = self.apos.tasks.getReq();
146
146
  var existing;
147
+ const workflow = self.apos.modules['apostrophe-workflow'];
148
+ const locales = workflow && workflow.locales && Object.keys(workflow.locales);
147
149
  return async.series({
148
150
  // Early in pre-2.0 code there was no type property for the global page.
149
151
  // We can't use a standard migration to fix that because initGlobal
@@ -178,6 +180,10 @@ module.exports = {
178
180
  return callback(err);
179
181
  }
180
182
  existing = result;
183
+ if (workflow) {
184
+ // Do not react to documents whose locale no longer exists in the system
185
+ existing = existing.filter(doc => locales.includes(doc.workflowLocale));
186
+ }
181
187
  return callback();
182
188
  });
183
189
  },
@@ -455,7 +455,7 @@ module.exports = {
455
455
  }
456
456
 
457
457
  self.getThrottleLoginErr = function (req, minutes) {
458
- return req.__ns_n(
458
+ return req.__ns(
459
459
  'apostrophe',
460
460
  'Too many login attempts. You may try again in %s minute(s).',
461
461
  minutes
@@ -47,7 +47,6 @@
47
47
  width: 75%;
48
48
  @media @apos-breakpoint-desktop-xl { width: 75%; }
49
49
  height: 100%;
50
- overflow: auto;
51
50
  .apos-scrollbar;
52
51
  }
53
52
 
@@ -461,16 +461,18 @@ apos.define('apostrophe-modal', {
461
461
  setImmediate(function() {
462
462
  $wrapper.find('[data-modal-content]').removeClass('apos-modal-slide-current');
463
463
  var fired = false;
464
- $content.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function() {
464
+ $content.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', handler);
465
+ // Fallback because the transition has been observed to never happen, at least in nightwatch testing
466
+ setTimeout(handler, 1000);
467
+ $content.addClass('apos-modal-slide-current');
468
+ function handler() {
465
469
  if (fired) {
466
- // Has been seen firing twice in nightwatch tests, in spite of `one`
467
470
  return;
468
471
  } else {
469
472
  fired = true;
470
473
  }
471
474
  self.indicateCurrentModal(true);
472
- });
473
- $content.addClass('apos-modal-slide-current');
475
+ }
474
476
  });
475
477
 
476
478
  };
@@ -8,7 +8,7 @@
8
8
  <select name="batch-operation" class="apos-field-input apos-field-input--small apos-field-input-select apos-manage-batch-operations-select">
9
9
  {% for operation in operations %}
10
10
  <option value="{{ operation.name }}">
11
- {{ __ns('apostrophe', '%s Selected', operation.label) }} (0)
11
+ {{ __ns('apostrophe', ' %s Selected', operation.label) }} (0)
12
12
  </option>
13
13
  {% endfor %}
14
14
  </select>
@@ -2675,58 +2675,64 @@ module.exports = {
2675
2675
 
2676
2676
  self.validate = function(schema, options) {
2677
2677
  // Infinite recursion prevention
2678
- if (self.validatedSchemas[options.type + ':' + options.subtype]) {
2679
- return;
2680
- }
2681
- self.validatedSchemas[options.type + ':' + options.subtype] = true;
2682
- var unarranged = [];
2678
+ const unarranged = [];
2683
2679
  _.each(schema, function(field) {
2684
- var fieldType = self.fieldTypes[field.type];
2685
- if (!fieldType) {
2686
- fail('Unknown schema field type.');
2687
- }
2688
- if (!field.name) {
2689
- fail('name property is missing.');
2690
- }
2691
- if ((!field.label) && (!field.contextual)) {
2692
- field.label = _.startCase(field.name.replace(/^_/, ''));
2693
- }
2694
- if (fieldType.validate) {
2695
- fieldType.validate(field, options, warn, fail, schema);
2696
- }
2697
- // If at least one field is in a non-default group and this one is in the
2698
- // default group, complain about halfassed grouping. The "Info" tab indicates
2699
- // insufficient UX consideration, unless it contains all the fields, which
2700
- // usually indicates a simple array schema that does not need any groups.
2701
- // Don't ding the developer for things that aren't their fault and are
2702
- // probably not that obnoxious in practice, like the withTags field of all
2703
- // pieces-pages, which is usually alone in the default group.
2704
- if (
2705
- field.group &&
2706
- (!field.contextual) &&
2707
- (field.name !== 'withTags') &&
2708
- (!field.type.match(/Reverse$/)) &&
2709
- (field.group.name === 'default') &&
2710
- (_.find(schema, function(field) {
2711
- return (field.group) && (field.group.name !== 'default');
2712
- }))) {
2713
- unarranged.push(field);
2714
- }
2715
- function fail(s) {
2716
- throw new Error(format(s));
2717
- }
2718
- function warn(s) {
2719
- self.apos.utils.warnDev(format(s));
2720
- }
2721
- function format(s) {
2722
- return '\n⚠️ ' + options.type + ' ' + options.subtype + ', field name ' + field.name + ':\n\n' + s + '\n';
2680
+ const key = `${options.type}:${options.subtype}.${field.name}`;
2681
+
2682
+ if (!self.validatedSchemas[key]) {
2683
+ self.validatedSchemas[key] = true;
2684
+ self.validateField(field, options, schema, unarranged);
2723
2685
  }
2724
2686
  });
2687
+
2725
2688
  if (unarranged.length) {
2726
2689
  self.apos.utils.warnDevOnce('unarranged-fields', '\n⚠️ ' + options.type + ' ' + options.subtype + ' contains unarranged field(s): ' + _.pluck(unarranged, 'name').join(', ') + '.\nArrange all of your fields with arrangeFields, using meaningful group labels.\nhttps://apos.dev/arrange-fields');
2727
2690
  }
2728
2691
  };
2729
2692
 
2693
+ self.validateField = function(field, options, schema, unarranged) {
2694
+ var fieldType = self.fieldTypes[field.type];
2695
+ if (!fieldType) {
2696
+ fail('Unknown schema field type.');
2697
+ }
2698
+ if (!field.name) {
2699
+ fail('name property is missing.');
2700
+ }
2701
+ if ((!field.label) && (!field.contextual)) {
2702
+ field.label = _.startCase(field.name.replace(/^_/, ''));
2703
+ }
2704
+ if (fieldType.validate) {
2705
+ fieldType.validate(field, options, warn, fail, schema);
2706
+ }
2707
+ // If at least one field is in a non-default group and this one is in the
2708
+ // default group, complain about halfassed grouping. The "Info" tab indicates
2709
+ // insufficient UX consideration, unless it contains all the fields, which
2710
+ // usually indicates a simple array schema that does not need any groups.
2711
+ // Don't ding the developer for things that aren't their fault and are
2712
+ // probably not that obnoxious in practice, like the withTags field of all
2713
+ // pieces-pages, which is usually alone in the default group.
2714
+ if (
2715
+ field.group &&
2716
+ (!field.contextual) &&
2717
+ (field.name !== 'withTags') &&
2718
+ (!field.type.match(/Reverse$/)) &&
2719
+ (field.group.name === 'default') &&
2720
+ (_.find(schema, function(field) {
2721
+ return (field.group) && (field.group.name !== 'default');
2722
+ }))) {
2723
+ unarranged.push(field);
2724
+ }
2725
+ function fail(s) {
2726
+ throw new Error(format(s));
2727
+ }
2728
+ function warn(s) {
2729
+ self.apos.utils.warnDev(format(s));
2730
+ }
2731
+ function format(s) {
2732
+ return '\n⚠️ ' + options.type + ' ' + options.subtype + ', field name ' + field.name + ':\n\n' + s + '\n';
2733
+ }
2734
+ };
2735
+
2730
2736
  // Return all standard field names currently associated with permissions editing,
2731
2737
  // for consistency in arrangeFields, batch permissions schemas, etc.
2732
2738
  self.getPermissionsFieldNames = function() {
@@ -721,7 +721,9 @@ module.exports = {
721
721
  self.apos.utils.error(':: ' + now + ': ' + type + ' error at ' + req.url);
722
722
  self.apos.utils.error('Current user: ' + (req.user ? req.user.username : 'none'));
723
723
  self.apos.utils.error(e);
724
- req.statusCode = 500;
724
+ // It is too late for req.statusCode, which is processed
725
+ // before sendPage is invoked
726
+ req.res.statusCode = 500;
725
727
  return self.render(req, 'templateError');
726
728
  }
727
729
  };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "2.220.4",
3
+ "version": "2.220.9",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "npm run lint && npm run audit && mocha --timeout 50000",
8
- "audit": "npm audit",
9
- "lint": "eslint ."
7
+ "test": "npm run build-test-package-json && npm run lint && npm run audit && mocha --timeout 50000",
8
+ "audit": "npm audit --production",
9
+ "lint": "eslint .",
10
+ "build-test-package-json": "node ./test-lib/build-test-package-json.js"
10
11
  },
11
12
  "repository": {
12
13
  "type": "git",
@@ -32,13 +33,12 @@
32
33
  "body-parser": "^1.19.0",
33
34
  "cheerio": "^1.0.0-rc.10",
34
35
  "chokidar": "^3.5.1",
35
- "cli-progress": "^2.1.1",
36
36
  "connect-flash": "^0.1.1",
37
37
  "connect-multiparty": "^2.2.0",
38
38
  "cookie-parser": "^1.4.5",
39
39
  "credential": "^2.0.0",
40
40
  "cuid": "^1.3.8",
41
- "deep-get-set": "^0.1.1",
41
+ "deep-get-set": "^1.1.1",
42
42
  "diff": "^4.0.1",
43
43
  "emulate-mongo-2-driver": "^1.2.3",
44
44
  "express": "^4.17.1",
@@ -80,7 +80,7 @@
80
80
  "tinycolor2": "^1.4.1",
81
81
  "uglify-js": "^2.8.29",
82
82
  "underscore.string": "^3.3.5",
83
- "uploadfs": "^1.17.2",
83
+ "uploadfs": "^1.18.4",
84
84
  "xregexp": "^2.0.0",
85
85
  "yargs": "^3.32.0"
86
86
  },
@@ -6,7 +6,7 @@ let apos;
6
6
 
7
7
  describe('Login', function() {
8
8
 
9
- this.timeout(20000);
9
+ this.timeout(60000);
10
10
 
11
11
  after(function(done) {
12
12
  return t.destroy(apos, done);
@@ -92,6 +92,10 @@ describe('Login', function() {
92
92
  });
93
93
 
94
94
  it('third failure in a row should cause a lockout', async function() {
95
+ if (process.version.startsWith('v8.')) {
96
+ console.log('Skipping this test on node 8, a partially supported release\nwhere this noncritical test is not reliable');
97
+ return;
98
+ }
95
99
  const req = apos.tasks.getReq();
96
100
  const user = await apos.users.find(req, {
97
101
  username: 'LilithIyapo'
@@ -0,0 +1,75 @@
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
+ "cli-progress": "^2.1.1",
13
+ "connect-flash": "^0.1.1",
14
+ "connect-multiparty": "^2.2.0",
15
+ "cookie-parser": "^1.4.5",
16
+ "credential": "^2.0.0",
17
+ "cuid": "^1.3.8",
18
+ "deep-get-set": "^0.1.1",
19
+ "diff": "^4.0.1",
20
+ "emulate-mongo-2-driver": "^1.2.3",
21
+ "express": "^4.17.1",
22
+ "express-session": "^1.17.0",
23
+ "glob": "^5.0.15",
24
+ "he": "^0.5.0",
25
+ "heic-to-jpeg-middleware": "^2.0.0",
26
+ "html-to-plaintext": "^0.1.1",
27
+ "html-to-text": "^5.1.1",
28
+ "i18n": "^0.8.6",
29
+ "is-wsl": "^2.2.0",
30
+ "joinr": "^1.0.2",
31
+ "jpeg-exif": "^1.1.4",
32
+ "launder": "^1.5.0",
33
+ "less": "^3.13.1",
34
+ "less-middleware": "^3.1.0",
35
+ "minimatch": "^3.0.4",
36
+ "mkdirp": "^1.0.3",
37
+ "moment": "^2.29.1",
38
+ "moog-require": "^1.1.0",
39
+ "nodemailer": "^6.6.2",
40
+ "oembetter": "^1.0.1",
41
+ "passport": "^0.3.2",
42
+ "passport-local": "^1.0.0",
43
+ "passport-totp": "0.0.2",
44
+ "path-to-regexp": "^1.7.0",
45
+ "performance-now": "^2.1.0",
46
+ "qs": "^6.9.6",
47
+ "regexp-quote": "0.0.0",
48
+ "request": "^2.88.2",
49
+ "request-promise": "^4.2.4",
50
+ "resolve": "^1.20.0",
51
+ "rimraf": "^2.7.1",
52
+ "sanitize-html": "^2.4.0",
53
+ "server-destroy": "^1.0.1",
54
+ "sluggo": "^0.2.0",
55
+ "syntax-error": "^1.3.0",
56
+ "thirty-two": "^1.0.2",
57
+ "tinycolor2": "^1.4.1",
58
+ "uglify-js": "^2.8.29",
59
+ "underscore.string": "^3.3.5",
60
+ "uploadfs": "^1.17.2",
61
+ "xregexp": "^2.0.0",
62
+ "yargs": "^3.32.0",
63
+ "apostrophe": "^2.0.0"
64
+ },
65
+ "devDependencies": {
66
+ "eslint": "^6.5.1",
67
+ "eslint-config-apostrophe": "^2.0.2",
68
+ "eslint-config-standard": "^11.0.0",
69
+ "eslint-plugin-import": "^2.18.2",
70
+ "eslint-plugin-node": "^6.0.1",
71
+ "eslint-plugin-promise": "^3.8.0",
72
+ "eslint-plugin-standard": "^3.1.0",
73
+ "mocha": "^7.0.0"
74
+ }
75
+ }
@@ -0,0 +1,10 @@
1
+ const fs = require('fs');
2
+ const info = JSON.parse(fs.readFileSync('package.json'));
3
+
4
+ info.dependencies = info.dependencies || {};
5
+ info.dependencies.apostrophe = '^2.0.0';
6
+ fs.writeFileSync('test/package.json', JSON.stringify({
7
+ "//": "Automatically generated to satisfy moog-require, do not edit",
8
+ dependencies: info.dependencies || {},
9
+ devDependencies: info.devDependencies || {}
10
+ }, null, ' '));