apostrophe 4.30.0-alpha.1 → 4.30.1-beta.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/eslint.config.js +1 -2
  3. package/lib/mongodb-connect.js +62 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +0 -1
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +25 -8
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +9 -0
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +20 -2
  8. package/modules/@apostrophecms/area/index.js +10 -5
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -0
  10. package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -1
  11. package/modules/@apostrophecms/db/index.js +27 -68
  12. package/modules/@apostrophecms/http/index.js +1 -1
  13. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  14. package/modules/@apostrophecms/i18n/index.js +1 -8
  15. package/modules/@apostrophecms/image-widget/index.js +29 -1
  16. package/modules/@apostrophecms/job/index.js +7 -9
  17. package/modules/@apostrophecms/layout-widget/index.js +124 -2
  18. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +89 -6
  19. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +2 -2
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +2 -2
  21. package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +8 -0
  22. package/modules/@apostrophecms/layout-widget/ui/src/layout.css +1 -1
  23. package/modules/@apostrophecms/layout-widget/views/widget.html +3 -3
  24. package/modules/@apostrophecms/login/index.js +13 -15
  25. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +2 -1
  26. package/modules/@apostrophecms/oembed/index.js +18 -13
  27. package/modules/@apostrophecms/piece-page-type/index.js +7 -0
  28. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +2 -0
  29. package/modules/@apostrophecms/rich-text-widget/index.js +36 -0
  30. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +1 -1
  31. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +1 -1
  32. package/modules/@apostrophecms/styles/index.js +16 -0
  33. package/modules/@apostrophecms/styles/lib/handlers.js +6 -0
  34. package/modules/@apostrophecms/styles/lib/methods.js +93 -0
  35. package/modules/@apostrophecms/styles/lib/presets.js +17 -0
  36. package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +10 -1
  37. package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +1 -0
  38. package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
  39. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -2
  40. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -5
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +8 -0
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +5 -2
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +14 -1
  44. package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +13 -1
  45. package/modules/@apostrophecms/util/index.js +4 -0
  46. package/modules/@apostrophecms/widget-type/index.js +6 -0
  47. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +32 -0
  48. package/package.json +11 -11
  49. package/test/add-missing-schema-fields-project/test.js +3 -11
  50. package/test/assets.js +67 -110
  51. package/test/db.js +15 -24
  52. package/test/job.js +1 -1
  53. package/test/layout-widget-gap.js +530 -0
  54. package/test/login.js +122 -1
  55. package/test/rich-text-widget.js +200 -0
  56. package/test/styles.js +50 -0
  57. package/test-lib/util.js +14 -50
  58. package/claude-tools/detect-handles.js +0 -46
  59. package/claude-tools/minimal-hang-test.js +0 -28
  60. package/claude-tools/mongo-close-test.js +0 -11
  61. package/claude-tools/stdin-ref-test.js +0 -14
  62. package/test/db-tools.js +0 -365
  63. package/test/default-adapter.js +0 -256
package/CHANGELOG.md CHANGED
@@ -1,10 +1,43 @@
1
1
  # Changelog
2
2
 
3
- ## 4.30.0-alpha.1
3
+ ## 4.30.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Sites with a custom filterByIndexPage method no longer experience failures in the sitemap module and potential creeping CPU performance penalties. A regression introduced with our static site support, but not specific to static sites.
8
+
9
+ ## 4.30.0
4
10
 
5
11
  ### Adds
6
12
 
7
- - Postgres and SQLite alpha release
13
+ - Layout widget gap is now controllable through the styles system, both site-wide via a global `layoutGap` preset and per widget via a `gap` styles field. A new `className` option allows additional CSS classes to be added to the widget grid container.
14
+
15
+ ### Fixes
16
+
17
+ - Fixed layout widget not regaining full focus when switching back to Edit content mode.
18
+ - Fixed illegal HTML `id` attribute values generated by the admin UI.
19
+ - Fixed orderable table array items dragging the entire floating window.
20
+ - Fixed keyboard shortcuts for widget operations (copy, cut, paste, duplicate, remove) blocking the browser's native clipboard behavior when no widget was focused. Previously, selecting and copying text while logged in was intercepted unconditionally by the admin UI.
21
+ - Removed duplicate `<meta charset>` tag from `outerLayoutBase.html` and standardized charset to `utf-8`.
22
+ - Updated `apostrophe` and `oembetter` to remove oembed services that no longer support public access, eliminating them as a potential future XSS vector. New `minimumAllowlist` and `minimumEndpoints` options on `@apostrophecms/oembed` allow developers to prune the list further.
23
+
24
+ ### Security
25
+
26
+ - **Password reset base URL requirement:** The password reset feature now refuses to operate unless `baseUrl` or `APOS_BASE_URL` is set, preventing a vulnerability where ApostropheCMS could be convinced to send emails with links to attacker-controlled sites. Only affects projects with `passwordReset: true` on the login module. Thanks to [SPIDY](https://github.com/Mujahidkhan525) for reporting.
27
+ - **XSS via full name field:** A malicious full name containing HTML was executed in the page title tooltip in the admin bar, posing an XSS risk to other users. All multi-user projects should update promptly. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
28
+ - **XSS via image widget link URL:** Users with editing privileges could trigger arbitrary JavaScript via a `javascript:` URL in the image widget's link URL field. A migration is included to strip any such URLs already in the database. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting.
29
+ - **SSRF via rich text HTML import:** The rich text widget's HTML import feature no longer fetches images from arbitrary hosts, which could be used to probe internal networks or exfiltrate internal images. Configure `imageImportAllowedHostnames` on `@apostrophecms/rich-text-widget` to opt in. Thanks to [Yiğit Şengezer](https://github.com/yigitsengezer) and [Sainithin0309](https://github.com/Sainithin0309) for reporting.
30
+ - **the xmp tag could be used to pass forbidden markup through sanitize-html**, even when xmp itself. This was fixed in `sanitize-html` and the dependency was bumped. Thanks to [Vincenzo Turturro](https://github.com/sushi-gif) for reporting the vulnerability.
31
+ - **the `linkHref` field of image widgets was an XSS vulnerability** because it did not use the `url` field type. This means that a user with editing privileges could potentially carry out XSS. In addition, we have updated the `launder` module to sanitize URLs more robustly for the `url` field type, and bumped that dependency. Also, a database migration is included to clean any XSS attacks that could be present in existing links. Thanks to [Muhammad Uwais](https://github.com/MuhammadUwais) for reporting the issue.
32
+
33
+ ### Accessibility
34
+
35
+ - Corrected ARIA semantics on the top admin navigation bar.
36
+ - Improved the document context title (admin bar middle group) and the underlying `AposContextMenu` machinery.
37
+ - Improved the locale switcher (`AposLocalePicker`).
38
+ - The Recently Edited Documents tray icon now exposes its action via `aria-label`.
39
+ - Fixed `.apos-sr-only` so screen-reader-only content is correctly exposed to the accessibility tree.
40
+ - Icon-only context-utility buttons in the admin bar tray (e.g. the global settings cog) now expose their action via `aria-label`.
8
41
 
9
42
  ## 4.29.0 (2026-04-15)
10
43
 
package/eslint.config.js CHANGED
@@ -7,8 +7,7 @@ module.exports = defineConfig([
7
7
  '**/blueimp/**/*.js',
8
8
  'test/public',
9
9
  'test/apos-build',
10
- 'coverage',
11
- 'claude-tools'
10
+ 'coverage'
12
11
  ]),
13
12
  apostrophe
14
13
  ]);
@@ -0,0 +1,62 @@
1
+ const mongo = require('@apostrophecms/emulate-mongo-3-driver');
2
+ const dns = require('dns');
3
+
4
+ // Connect to MongoDB, using the modern topology and parser, and
5
+ // a tolerant policy to successfully connect to "localhost" even if
6
+ // the first record returned by the resolver doesn't reach mongodb's
7
+ // bind address because localhost resolves first to ::1 (ipv6) and
8
+ // mongodb lists only on 127.0.0.1 (ipv4) by default. For broadest
9
+ // compatibility we don't assume we know this will happen, we try all the
10
+ // addresses that localhost actually resolves to and succeed with the
11
+ // first one that works.
12
+
13
+ module.exports = async (uri, options) => {
14
+ const connectOptions = {
15
+ useUnifiedTopology: true,
16
+ useNewUrlParser: true,
17
+ ...options
18
+ };
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(uri);
22
+ } catch (e) {
23
+ // Parse failed, e.g. old school replica set URI
24
+ // with commas, just let the mongo driver handle it
25
+ return mongo.MongoClient.connect(uri, connectOptions);
26
+ }
27
+ if (!parsed || (parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
28
+ return mongo.MongoClient.connect(parsed.toString(), connectOptions);
29
+ }
30
+ const records = await dns.promises.lookup('localhost', { all: true });
31
+ if (!records.length) {
32
+ // The computer that reaches this point has bigger problems 😅
33
+ throw new Error('Unable to resolve localhost to an IP address.');
34
+ }
35
+ return new Promise((resolve, reject) => {
36
+ let failed = 0;
37
+ let succeeded = false;
38
+ records.forEach(attempt);
39
+ async function attempt(record) {
40
+ try {
41
+ const parsed = new URL(uri);
42
+ parsed.hostname = record.address;
43
+ const result = await mongo.MongoClient.connect(parsed.toString(), connectOptions);
44
+ if (!succeeded) {
45
+ succeeded = true;
46
+ resolve(result);
47
+ } else {
48
+ // We succeeded in reaching localhost at both ip4 and ip6,
49
+ // but we only need one of them to succeed
50
+ await result.close();
51
+ }
52
+ } catch (e) {
53
+ failed++;
54
+ if (failed === records.length) {
55
+ // None succeeded, so reject with the last error
56
+ // (which one we reject with doesn't really matter)
57
+ reject(e);
58
+ }
59
+ }
60
+ }
61
+ });
62
+ };
@@ -11,7 +11,6 @@
11
11
  <nav
12
12
  ref="adminBar"
13
13
  :class="classes"
14
- role="menubar"
15
14
  aria-label="Apostrophe Admin Bar"
16
15
  >
17
16
  <div class="apos-admin-bar__row">
@@ -1,8 +1,5 @@
1
1
  <template>
2
- <ol
3
- class="apos-admin-bar__items"
4
- role="menu"
5
- >
2
+ <ol class="apos-admin-bar__items">
6
3
  <li
7
4
  v-if="pageTree"
8
5
  class="apos-admin-bar__item"
@@ -12,7 +9,6 @@
12
9
  label="apostrophe:pages"
13
10
  class="apos-admin-bar__btn"
14
11
  :modifiers="['no-motion']"
15
- role="menuitem"
16
12
  action-test-label="page-manager-button"
17
13
  @click="emitEvent({ action: '@apostrophecms/page:manager' })"
18
14
  />
@@ -33,7 +29,6 @@
33
29
  class: 'apos-admin-bar__btn',
34
30
  type: 'subtle'
35
31
  }"
36
- role="menuitem"
37
32
  @item-clicked="emitEvent"
38
33
  />
39
34
  <Component
@@ -44,7 +39,6 @@
44
39
  :modifiers="['no-motion']"
45
40
  class="apos-admin-bar__btn"
46
41
  :action-test-label="`${item.name}-manager-button`"
47
- role="menuitem"
48
42
  @click="emitEvent(item)"
49
43
  />
50
44
  </li>
@@ -62,7 +56,6 @@
62
56
  type: 'primary',
63
57
  modifiers: ['round', 'no-motion']
64
58
  }"
65
- role="menuitem"
66
59
  @item-clicked="emitEvent"
67
60
  />
68
61
  </li>
@@ -89,6 +82,7 @@
89
82
  :label="item.label"
90
83
  :action="item.action"
91
84
  :state="trayItemState[item.name] ? [ 'active' ] : []"
85
+ :attrs="trayItemAttrs(item)"
92
86
  @click="emitEvent(item)"
93
87
  />
94
88
  </template>
@@ -185,6 +179,29 @@ export default {
185
179
  } else {
186
180
  return item.options.tooltip;
187
181
  }
182
+ },
183
+ // Tray utility buttons render icon-only, so the visible label
184
+ // (e.g. "Global Content") is sr-only and doesn't describe what the
185
+ // button does. Make them accessible by providing an aria-label based on
186
+ // the tooltip content.
187
+ trayItemAttrs(item) {
188
+ const tooltip = item.options?.tooltip;
189
+ let key = null;
190
+ if (item.options?.toggle) {
191
+ if (this.trayItemState[item.name] && tooltip?.deactivate) {
192
+ key = tooltip.deactivate;
193
+ } else if (tooltip?.activate) {
194
+ key = tooltip.activate;
195
+ }
196
+ } else if (typeof tooltip === 'string') {
197
+ key = tooltip;
198
+ } else if (tooltip && typeof tooltip.content === 'string') {
199
+ key = tooltip.content;
200
+ }
201
+ if (!key) {
202
+ return {};
203
+ }
204
+ return { 'aria-label': this.$t(key) };
188
205
  }
189
206
  }
190
207
  };
@@ -14,6 +14,7 @@
14
14
  :label="screen.label"
15
15
  :tooltip="$t(screen.label)"
16
16
  :title="$t(screen.label)"
17
+ :attrs="shortcutAttrs(screen)"
17
18
  :icon="screen.icon"
18
19
  :icon-only="true"
19
20
  type="subtle"
@@ -36,6 +37,7 @@
36
37
  :active-item="mode"
37
38
  :center-on-icon="true"
38
39
  menu-placement="bottom-end"
40
+ :dialog-label="'apostrophe:breakpointPreviewSelectMenu'"
39
41
  @item-clicked="selectBreakpoint"
40
42
  />
41
43
  <Transition>
@@ -320,6 +322,13 @@ export default {
320
322
  },
321
323
  setShowDropdown() {
322
324
  this.showDropdown = Object.values(this.screens).some(({ shortcut }) => !shortcut);
325
+ },
326
+ shortcutAttrs(screen) {
327
+ return {
328
+ 'aria-label': this.$t('apostrophe:breakpointPreviewShortcut', {
329
+ breakpoint: this.$t(screen.label)
330
+ })
331
+ };
323
332
  }
324
333
  }
325
334
  };
@@ -33,6 +33,8 @@
33
33
  :disabled="hasCustomUi || isUnpublished"
34
34
  :center-on-icon="true"
35
35
  menu-placement="bottom-end"
36
+ :dialog-label="'apostrophe:publicationStatusMenu'"
37
+ :trigger-aria-label="draftTriggerAriaLabel"
36
38
  @item-clicked="switchDraftMode"
37
39
  />
38
40
  <AposLabel
@@ -56,6 +58,15 @@
56
58
  <script>
57
59
  import dayjs from 'dayjs';
58
60
 
61
+ function escapeHtml(s) {
62
+ return String(s)
63
+ .replace(/&/g, '&amp;')
64
+ .replace(/</g, '&lt;')
65
+ .replace(/>/g, '&gt;')
66
+ .replace(/"/g, '&quot;')
67
+ .replace(/'/g, '&#39;');
68
+ }
69
+
59
70
  export default {
60
71
  name: 'TheAposContextTitle',
61
72
  props: {
@@ -78,8 +89,8 @@ export default {
78
89
  if (this.context.updatedBy) {
79
90
  const editor = this.context.updatedBy;
80
91
  editorLabel = '';
81
- editorLabel += editor.title ? `${editor.title} ` : '';
82
- editorLabel += editor.username ? `(${editor.username})` : '';
92
+ editorLabel += editor.title ? `${escapeHtml(editor.title)} ` : '';
93
+ editorLabel += editor.username ? `(${escapeHtml(editor.username)})` : '';
83
94
  }
84
95
  return editorLabel;
85
96
  },
@@ -91,6 +102,13 @@ export default {
91
102
  type: 'quiet'
92
103
  };
93
104
  },
105
+ draftTriggerAriaLabel() {
106
+ return this.$t('apostrophe:publicationStatusTrigger', {
107
+ status: this.$t(
108
+ this.draftMode === 'draft' ? 'apostrophe:draft' : 'apostrophe:published'
109
+ )
110
+ });
111
+ },
94
112
  isUnpublished() {
95
113
  return !this.context.lastPublishedAt;
96
114
  },
@@ -19,7 +19,8 @@ module.exports = {
19
19
  action: {
20
20
  type: 'command-menu-area-cut-widget'
21
21
  },
22
- shortcut: 'Ctrl+X Meta+X'
22
+ shortcut: 'Ctrl+X Meta+X',
23
+ requireWidgetFocus: true
23
24
  },
24
25
  [`${self.__meta.name}:copy-widget`]: {
25
26
  type: 'item',
@@ -27,7 +28,8 @@ module.exports = {
27
28
  action: {
28
29
  type: 'command-menu-area-copy-widget'
29
30
  },
30
- shortcut: 'Ctrl+C Meta+C'
31
+ shortcut: 'Ctrl+C Meta+C',
32
+ requireWidgetFocus: true
31
33
  },
32
34
  [`${self.__meta.name}:paste-widget`]: {
33
35
  type: 'item',
@@ -35,7 +37,8 @@ module.exports = {
35
37
  action: {
36
38
  type: 'command-menu-area-paste-widget'
37
39
  },
38
- shortcut: 'Ctrl+V Meta+V'
40
+ shortcut: 'Ctrl+V Meta+V',
41
+ requireWidgetFocus: true
39
42
  },
40
43
  [`${self.__meta.name}:duplicate-widget`]: {
41
44
  type: 'item',
@@ -43,7 +46,8 @@ module.exports = {
43
46
  action: {
44
47
  type: 'command-menu-area-duplicate-widget'
45
48
  },
46
- shortcut: 'Ctrl+Shift+D Meta+Shift+D'
49
+ shortcut: 'Ctrl+Shift+D Meta+Shift+D',
50
+ requireWidgetFocus: true
47
51
  },
48
52
  [`${self.__meta.name}:remove-widget`]: {
49
53
  type: 'item',
@@ -51,7 +55,8 @@ module.exports = {
51
55
  action: {
52
56
  type: 'command-menu-area-remove-widget'
53
57
  },
54
- shortcut: 'Backspace'
58
+ shortcut: 'Backspace',
59
+ requireWidgetFocus: true
55
60
  }
56
61
  },
57
62
  modal: {
@@ -536,6 +536,7 @@ export default {
536
536
  apos.bus.$on('widget-focus-parent', this.focusParent);
537
537
  apos.bus.$on('context-menu-toggled', this.getFocusForMenu);
538
538
  apos.bus.$on('suppress-focused-widget-controls', this.doSuppressWidgetControls);
539
+ apos.bus.$on('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
539
540
 
540
541
  this.breadcrumbs.$lastEl = this.$el;
541
542
 
@@ -573,6 +574,7 @@ export default {
573
574
  // Remove the focus parent listener when unmounted
574
575
  apos.bus.$off('widget-focus-parent', this.focusParent);
575
576
  apos.bus.$off('suppress-focused-widget-controls', this.doSuppressWidgetControls);
577
+ apos.bus.$off('clear-focused-widget-control-suppression', this.clearSuppressionFlags);
576
578
  window.removeEventListener('scroll', this.stickyControlsScroll);
577
579
  window.removeEventListener('resize', this.stickyControlsResize);
578
580
  this.unregisterFromGraph();
@@ -10,6 +10,7 @@
10
10
  import { mapActions, mapState } from 'pinia';
11
11
  import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
12
12
  import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
13
+ import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
13
14
 
14
15
  export default {
15
16
  name: 'TheAposCommandMenu',
@@ -50,7 +51,13 @@ export default {
50
51
  .flatMap(command => {
51
52
  return command.shortcut
52
53
  .split(' ')
53
- .map(shortcut => [ shortcut.toUpperCase(), command.action ]);
54
+ .map(shortcut => [
55
+ shortcut.toUpperCase(),
56
+ {
57
+ ...command.action,
58
+ requireWidgetFocus: command.requireWidgetFocus || false
59
+ }
60
+ ]);
54
61
  });
55
62
  })
56
63
  );
@@ -111,6 +118,9 @@ export default {
111
118
  ? keys.slice('SHIFT+'.length)
112
119
  : keys];
113
120
  if (action) {
121
+ if (action.requireWidgetFocus && !useWidgetStore().focusedWidget) {
122
+ return;
123
+ }
114
124
  event.preventDefault();
115
125
  apos.bus.$emit(action.type, action.payload);
116
126
  return;
@@ -4,12 +4,11 @@
4
4
  //
5
5
  // ### `uri`
6
6
  //
7
- // The databse connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/)
8
- // and the postgres documentation.
7
+ // The MongoDB connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/).
9
8
  //
10
9
  // ### `connect`
11
10
  //
12
- // If present, this object is passed on as options to the database adapters "connect"
11
+ // If present, this object is passed on as options to MongoDB's "connect"
13
12
  // method, along with the uri. See the [MongoDB connect settings documentation](http://mongodb.github.io/node-mongodb-native/2.2/reference/connecting/connection-settings/).
14
13
  //
15
14
  // By default, Apostrophe sets options to retry lost connections forever,
@@ -21,16 +20,9 @@
21
20
  //
22
21
  // ### `client`
23
22
  //
24
- // An existing MongoDB-compatible client object. If present, it is used
23
+ // An existing MongoDB connection (MongoClient) object. If present, it is used
25
24
  // and `uri`, `host`, `connect`, etc. are ignored.
26
25
  //
27
- // ### `adapters`
28
- //
29
- // An array of adapters, each of which must provide `name`, `connect(uri, options)`,
30
- // and `protocols` properties. `name` may be used to override a core adapter,
31
- // such as `postgres` or `mongodb`. `connect` must resolve to a client object
32
- // supporting a sufficient subset of the mongodb API.
33
- //
34
26
  // ### `versionCheck`
35
27
  //
36
28
  // If `true`, check to make sure the database does not belong to an
@@ -57,15 +49,15 @@
57
49
  // in your project. However you may find it easier to just use the
58
50
  // `client` option.
59
51
 
60
- const dbConnect = require('@apostrophecms/db-connect');
61
- const escapeHost = require('../../../lib/escape-host.js');
52
+ const mongodbConnect = require('../../../lib/mongodb-connect');
53
+ const escapeHost = require('../../../lib/escape-host');
62
54
 
63
55
  module.exports = {
64
56
  options: {
65
57
  versionCheck: true
66
58
  },
67
59
  async init(self) {
68
- await self.connectToDb();
60
+ await self.connectToMongo();
69
61
  await self.versionCheck();
70
62
  },
71
63
  handlers(self) {
@@ -89,12 +81,14 @@ module.exports = {
89
81
  },
90
82
  methods(self) {
91
83
  return {
92
- // Connect to the database and sets self.apos.dbClient
93
- // and self.apos.db. Builds a mongodb URI by default,
94
- // accepting host, port, user, password and name options
95
- // if present. More typically a URI is specified via
96
- // APOS_DB_URI, or via APOS_MONGODB_URI for bc.
97
- async connectToDb() {
84
+ // Open the database connection. Always uses MongoClient with its
85
+ // sensible defaults. Builds a URI if necessary, so we can call it
86
+ // in a consistent way.
87
+ //
88
+ // One default we override: if the connection is lost, we keep
89
+ // attempting to reconnect forever. This is the most sensible behavior
90
+ // for a persistent process that requires MongoDB in order to operate.
91
+ async connectToMongo() {
98
92
  if (self.options.client) {
99
93
  // Reuse a single client connection http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#db
100
94
  self.apos.dbClient = self.options.client;
@@ -102,67 +96,32 @@ module.exports = {
102
96
  self.connectionReused = true;
103
97
  return;
104
98
  }
105
- let uri;
106
- const viaEnv = process.env.APOS_DB_URI || process.env.APOS_MONGODB_URI;
107
- if (viaEnv) {
108
- uri = viaEnv;
99
+ let uri = 'mongodb://';
100
+ if (process.env.APOS_MONGODB_URI) {
101
+ uri = process.env.APOS_MONGODB_URI;
109
102
  } else if (self.options.uri) {
110
103
  uri = self.options.uri;
111
104
  } else {
112
- const validAdapters = [ 'mongodb', 'sqlite', 'postgres', 'multipostgres' ];
113
- const adapter = process.env.APOS_DEFAULT_DB_ADAPTER || self.options.defaultAdapter || 'mongodb';
114
- if (!validAdapters.includes(adapter)) {
115
- throw new Error(`Invalid defaultAdapter: "${adapter}". Must be one of: ${validAdapters.join(', ')}`);
105
+ if (self.options.user) {
106
+ uri += self.options.user + ':' + self.options.password + '@';
107
+ }
108
+ if (!self.options.host) {
109
+ self.options.host = 'localhost';
110
+ }
111
+ if (!self.options.port) {
112
+ self.options.port = 27017;
116
113
  }
117
114
  if (!self.options.name) {
118
115
  self.options.name = self.apos.shortName;
119
116
  }
120
- if (adapter === 'sqlite') {
121
- const path = require('path');
122
- uri = `sqlite://${path.resolve(self.apos.rootDir, 'data', self.options.name + '.sqlite')}`;
123
- } else {
124
- const credentials = self.options.user
125
- ? encodeURIComponent(self.options.user) + ':' + encodeURIComponent(self.options.password) + '@'
126
- : '';
127
- if (adapter === 'mongodb') {
128
- if (!self.options.host) {
129
- self.options.host = 'localhost';
130
- }
131
- if (!self.options.port) {
132
- self.options.port = 27017;
133
- }
134
- uri = 'mongodb://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
135
- } else {
136
- // postgres or multipostgres
137
- if (!self.options.host) {
138
- self.options.host = 'localhost';
139
- }
140
- if (!self.options.port) {
141
- self.options.port = 5432;
142
- }
143
- uri = adapter + '://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
144
- }
145
- }
117
+ uri += escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
146
118
  }
147
119
 
148
- self.apos.dbClient = await dbConnect(uri, {
149
- ...self.options.connect,
150
- adapters: self.options.adapters
151
- });
120
+ self.apos.dbClient = await mongodbConnect(uri, self.options.connect);
152
121
  self.uri = uri;
153
122
  // Automatically uses the db name in the connection string
154
123
  self.apos.db = self.apos.dbClient.db();
155
124
  },
156
- // Connect to a database using the appropriate adapter based on the URI protocol.
157
- // Returns a client object compatible with the MongoDB driver interface.
158
- // This method has no side effects — it does not set apos.db or apos.dbClient.
159
- // It can be used to make temporary connections, e.g. for dropping a test database.
160
- async connectToAdapter(uri, options) {
161
- return dbConnect(uri, {
162
- ...options,
163
- adapters: self.options.adapters
164
- });
165
- },
166
125
  async versionCheck() {
167
126
  if (!self.options.versionCheck) {
168
127
  return;
@@ -2,7 +2,7 @@ const _ = require('lodash');
2
2
  const qs = require('qs');
3
3
  const fetch = require('node-fetch');
4
4
  const tough = require('tough-cookie');
5
- const escapeHost = require('../../../lib/escape-host.js');
5
+ const escapeHost = require('../../../lib/escape-host');
6
6
  const util = require('util');
7
7
 
8
8
  module.exports = {
@@ -89,6 +89,8 @@
89
89
  "breakpointPreviewExit": "Exit",
90
90
  "breakpointPreviewMobile": "Mobile",
91
91
  "breakpointPreviewSelect": "Select Breakpoint",
92
+ "breakpointPreviewSelectMenu": "Breakpoint preview menu",
93
+ "breakpointPreviewShortcut": "Preview at {{ breakpoint }} breakpoint",
92
94
  "breakpointPreviewTablet": "Tablet",
93
95
  "browse": "Browse",
94
96
  "browseDocType": "Browse {{ type }}",
@@ -387,6 +389,7 @@
387
389
  "mediaUploadViaDrop": "Drop ’em when you’re ready",
388
390
  "mediaUploadViaExplorer": "Or click to open the file explorer",
389
391
  "mergeCells": "Merge Cells",
392
+ "menu": "Menu",
390
393
  "minLabel": "Min:",
391
394
  "minSize": "Min size of {{ width }}x{{ height }}",
392
395
  "minimumSize": "Minimum size of {{ width }} x {{ height }} px",
@@ -475,6 +478,8 @@
475
478
  "publishBeforeUsingTooltip": "Publish this content before using it in a relationship",
476
479
  "publishType": "Publish {{ type }}",
477
480
  "published": "Published",
481
+ "publicationStatusMenu": "Publication status",
482
+ "publicationStatusTrigger": "Publication status: {{ status }}. Change publication status.",
478
483
  "publishingBatchConfirmation": "Are you sure you want to publish {{ count }} {{ type }}?",
479
484
  "publishingBatchConfirmationButton": "Yes, publish content.",
480
485
  "rawCssAndJs": "Raw CSS and JS",
@@ -490,6 +495,7 @@
490
495
  "recentlyEditedActionSubmitted": "Submitted",
491
496
  "recentlyEditedCurrentUser": "Me ({{ user }})",
492
497
  "recentlyEditedDocuments": "Recently edited documents",
498
+ "recentlyEditedManagerOpen": "Open recently edited documents manager",
493
499
  "recentlyEditedEditedBy": "Edited by",
494
500
  "recentlyEditedClearAllFilters": "Clear all filters",
495
501
  "recentlyEditedClearSearch": "Clear search",
@@ -643,6 +649,8 @@
643
649
  "styleGradientAngle": "Angle",
644
650
  "styleGradientEnd": "End Color",
645
651
  "styleGradientStart": "Start Color",
652
+ "styleLayoutGap": "Layout Gap",
653
+ "styleLayoutGapHelp": "Sets the spacing between columns inside layout sections across the site.",
646
654
  "styleLeft": "Left",
647
655
  "styleMargin": "Margin",
648
656
  "styleOverlayColor": "Overlay Color",
@@ -29,10 +29,6 @@
29
29
  // in the same language as the website content.
30
30
  // Example: `defaultAdminLocale: 'fr'`.
31
31
  //
32
- // ### `encoding`
33
- //
34
- // Defaults to `'utf-8'`. You almost certainly do not want to change this.
35
- //
36
32
  // ### `slugDirection`
37
33
  //
38
34
  // Controls the default `direction` value of slug schema. Can be `ltr`, `rtl` or
@@ -81,8 +77,6 @@ module.exports = {
81
77
  },
82
78
  // If true, slugifying will strip accents from Latin characters
83
79
  stripUrlAccents: false,
84
- // You almost certainly do not want to change this
85
- encoding: 'utf-8',
86
80
  slugDirection: 'ltr'
87
81
  },
88
82
  async init(self) {
@@ -166,7 +160,6 @@ module.exports = {
166
160
  await self.i18next.init(i18nextOptions);
167
161
  self.addInitialResources();
168
162
  self.enableBrowserData();
169
- self.encoding = self.options.encoding;
170
163
  },
171
164
  handlers(self) {
172
165
  return {
@@ -1369,7 +1362,7 @@ module.exports = {
1369
1362
  helpers(self) {
1370
1363
  return {
1371
1364
  encoding() {
1372
- return self.encoding;
1365
+ return 'utf-8';
1373
1366
  }
1374
1367
  };
1375
1368
  }