@plone/volto 17.0.0-alpha.0 → 17.0.0-alpha.10

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 (190) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +451 -19
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +12 -15
  5. package/addon-registry.js +34 -0
  6. package/create-theme-addons-loader.js +79 -0
  7. package/cypress/support/commands.js +25 -0
  8. package/locales/ca/LC_MESSAGES/volto.po +187 -6
  9. package/locales/ca.json +1 -1
  10. package/locales/de/LC_MESSAGES/volto.po +206 -25
  11. package/locales/de.json +1 -1
  12. package/locales/en/LC_MESSAGES/volto.po +186 -5
  13. package/locales/en.json +1 -1
  14. package/locales/es/LC_MESSAGES/volto.po +187 -6
  15. package/locales/es.json +1 -1
  16. package/locales/eu/LC_MESSAGES/volto.po +187 -6
  17. package/locales/eu.json +1 -1
  18. package/locales/fi/LC_MESSAGES/volto.po +4792 -0
  19. package/locales/fi.json +1 -1
  20. package/locales/fr/LC_MESSAGES/volto.po +187 -6
  21. package/locales/fr.json +1 -1
  22. package/locales/it/LC_MESSAGES/volto.po +187 -6
  23. package/locales/it.json +1 -1
  24. package/locales/ja/LC_MESSAGES/volto.po +187 -6
  25. package/locales/ja.json +1 -1
  26. package/locales/nl/LC_MESSAGES/volto.po +842 -649
  27. package/locales/nl.json +1 -1
  28. package/locales/pt/LC_MESSAGES/volto.po +187 -6
  29. package/locales/pt.json +1 -1
  30. package/locales/pt_BR/LC_MESSAGES/volto.po +195 -14
  31. package/locales/pt_BR.json +1 -1
  32. package/locales/ro/LC_MESSAGES/volto.po +187 -6
  33. package/locales/ro.json +1 -1
  34. package/locales/volto.pot +187 -6
  35. package/locales/zh_CN/LC_MESSAGES/volto.po +187 -6
  36. package/locales/zh_CN.json +1 -1
  37. package/package-why.json +0 -1
  38. package/package.json +9 -8
  39. package/packages/volto-slate/build/messages/src/blocks/Table/TableBlockEdit.json +1 -1
  40. package/packages/volto-slate/build/messages/src/blocks/Text/DefaultTextBlockEditor.json +1 -1
  41. package/packages/volto-slate/build/messages/src/blocks/Text/DetachedTextBlockEditor.json +1 -1
  42. package/packages/volto-slate/build/messages/src/blocks/Text/SlashMenu.json +1 -1
  43. package/packages/volto-slate/build/messages/src/editor/plugins/AdvancedLink/index.json +1 -1
  44. package/packages/volto-slate/build/messages/src/editor/plugins/Link/index.json +1 -1
  45. package/packages/volto-slate/build/messages/src/editor/plugins/Table/index.json +1 -1
  46. package/packages/volto-slate/build/messages/src/elementEditor/messages.json +1 -1
  47. package/packages/volto-slate/build/messages/src/widgets/HtmlSlateWidget.json +1 -1
  48. package/packages/volto-slate/build/messages/src/widgets/RichTextWidgetView.json +1 -1
  49. package/packages/volto-slate/package.json +1 -1
  50. package/packages/volto-slate/src/blocks/Table/TableBlockView.jsx +4 -4
  51. package/packages/volto-slate/src/blocks/Table/index.js +2 -0
  52. package/packages/volto-slate/src/blocks/Text/SlashMenu.jsx +4 -3
  53. package/packages/volto-slate/src/editor/deserialize.js +0 -1
  54. package/packages/volto-slate/src/editor/plugins/StyleMenu/StyleMenu.jsx +14 -4
  55. package/razzle.config.js +28 -0
  56. package/src/actions/index.js +6 -0
  57. package/src/actions/language/language.js +9 -8
  58. package/src/actions/querystringsearch/querystringsearch.js +20 -14
  59. package/src/actions/relations/rebuild.js +25 -0
  60. package/src/actions/relations/relations.js +86 -0
  61. package/src/actions/relations/relations.test.js +15 -0
  62. package/src/components/index.js +1 -0
  63. package/src/components/manage/Add/Add.jsx +2 -2
  64. package/src/components/manage/BlockChooser/BlockChooser.jsx +14 -5
  65. package/src/components/manage/BlockChooser/BlockChooser.test.jsx +5 -0
  66. package/src/components/manage/BlockChooser/BlockChooserButton.jsx +63 -29
  67. package/src/components/manage/BlockChooser/BlockChooserSearch.jsx +0 -1
  68. package/src/components/manage/Blocks/Listing/Edit.jsx +0 -19
  69. package/src/components/manage/Blocks/Listing/ListingBody.jsx +77 -61
  70. package/src/components/manage/Blocks/Listing/View.jsx +0 -4
  71. package/src/components/manage/Blocks/Listing/getAsyncData.js +10 -2
  72. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +18 -13
  73. package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +5 -4
  74. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +2 -1
  75. package/src/components/manage/Blocks/Search/components/DateRangeFacet.jsx +4 -1
  76. package/src/components/manage/Blocks/Search/components/Facets.jsx +58 -2
  77. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +24 -11
  78. package/src/components/manage/Blocks/Search/layout/LeftColumnFacets.jsx +17 -5
  79. package/src/components/manage/Blocks/Search/layout/RightColumnFacets.jsx +17 -5
  80. package/src/components/manage/Blocks/Search/layout/TopSideFacets.jsx +21 -5
  81. package/src/components/manage/Blocks/Search/schema.js +16 -1
  82. package/src/components/manage/Blocks/ToC/Edit.jsx +1 -0
  83. package/src/components/manage/Contents/Contents.jsx +69 -33
  84. package/src/components/manage/Contents/ContentsItem.jsx +6 -0
  85. package/src/components/manage/Controlpanels/AddonsControlpanel.jsx +3 -3
  86. package/src/components/manage/Controlpanels/Controlpanels.jsx +199 -224
  87. package/src/components/manage/Controlpanels/Controlpanels.test.jsx +46 -7
  88. package/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +66 -0
  89. package/src/components/manage/Controlpanels/Relations/Relations.jsx +114 -0
  90. package/src/components/manage/Controlpanels/Relations/RelationsListing.jsx +479 -0
  91. package/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx +531 -0
  92. package/src/components/manage/Controlpanels/Users/UserGroupMembershipControlPanel.jsx +3 -3
  93. package/src/components/manage/Controlpanels/Users/UserGroupMembershipListing.jsx +51 -82
  94. package/src/components/manage/Controlpanels/Users/UserGroupMembershipMatrix.jsx +79 -75
  95. package/src/components/manage/DragDropList/DragDropList.jsx +63 -42
  96. package/src/components/manage/Form/BlocksToolbar.jsx +5 -1
  97. package/src/components/manage/Form/Form.jsx +11 -5
  98. package/src/components/manage/Form/InlineForm.jsx +39 -9
  99. package/src/components/manage/Form/InlineFormState.js +8 -0
  100. package/src/components/manage/History/History.jsx +35 -18
  101. package/src/components/manage/Multilingual/CreateTranslation.jsx +2 -2
  102. package/src/components/manage/Multilingual/TranslationObject.jsx +4 -3
  103. package/src/components/manage/Preferences/ChangePassword.jsx +2 -2
  104. package/src/components/manage/Preferences/PersonalPreferences.jsx +2 -2
  105. package/src/components/manage/Toast/Toast.jsx +1 -1
  106. package/src/components/manage/Toolbar/Types.jsx +2 -2
  107. package/src/components/manage/Widgets/DatetimeWidget.jsx +9 -5
  108. package/src/components/manage/Widgets/ObjectListWidget.jsx +3 -8
  109. package/src/components/manage/Widgets/RecurrenceWidget/ByDayField.jsx +2 -1
  110. package/src/components/manage/Widgets/RecurrenceWidget/MonthOfTheYearField.jsx +2 -1
  111. package/src/components/manage/Widgets/RecurrenceWidget/Occurences.jsx +2 -1
  112. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +7 -2
  113. package/src/components/manage/Widgets/RecurrenceWidget/WeekdayOfTheMonthField.jsx +2 -1
  114. package/src/components/manage/Widgets/SelectUtils.js +1 -1
  115. package/src/components/manage/Widgets/SelectWidget.jsx +1 -1
  116. package/src/components/theme/Footer/Footer.jsx +2 -13
  117. package/src/components/theme/Header/Header.jsx +37 -63
  118. package/src/components/theme/Header/Header.test.jsx +18 -0
  119. package/src/components/theme/Icon/Icon.jsx +2 -2
  120. package/src/components/theme/LanguageSelector/LanguageSelector.js +8 -3
  121. package/src/components/theme/Login/Login.jsx +1 -0
  122. package/src/components/theme/Logo/Logo.jsx +2 -1
  123. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +2 -2
  124. package/src/components/theme/Navigation/NavItem.jsx +4 -2
  125. package/src/components/theme/NotFound/NotFound.jsx +55 -41
  126. package/src/components/theme/PasswordReset/PasswordReset.jsx +7 -4
  127. package/src/components/theme/PasswordReset/RequestPasswordReset.jsx +1 -1
  128. package/src/components/theme/Sitemap/Sitemap.jsx +5 -3
  129. package/src/components/theme/View/DefaultView.jsx +1 -1
  130. package/src/components/theme/View/EventDatesInfo.jsx +2 -1
  131. package/src/components/theme/View/EventView.jsx +1 -1
  132. package/src/components/theme/View/NewsItemView.jsx +1 -1
  133. package/src/components/theme/View/RenderBlocks.jsx +7 -1
  134. package/src/components/theme/Widgets/DateWidget.jsx +2 -1
  135. package/src/components/theme/Widgets/DatetimeWidget.jsx +2 -1
  136. package/src/components/theme/Widgets/RelationsWidget.jsx +13 -11
  137. package/src/config/ControlPanels.js +2 -0
  138. package/src/config/Widgets.jsx +1 -0
  139. package/src/config/index.js +3 -0
  140. package/src/config/server.js +19 -0
  141. package/src/constants/ActionTypes.js +4 -0
  142. package/src/constants/Languages.js +8 -4
  143. package/src/express-middleware/devproxy.js +4 -2
  144. package/src/express-middleware/sitemap.js +36 -4
  145. package/src/express-middleware/static.js +32 -0
  146. package/src/helpers/Api/Api.js +1 -1
  147. package/src/helpers/FormValidation/FormValidation.js +11 -2
  148. package/src/helpers/FormValidation/FormValidation.test.js +73 -0
  149. package/src/helpers/Html/Html.jsx +3 -1
  150. package/src/helpers/Html/Html.test.jsx +5 -0
  151. package/src/helpers/MessageLabels/MessageLabels.js +72 -0
  152. package/src/helpers/Robots/Robots.js +24 -6
  153. package/src/helpers/Sitemap/Sitemap.js +44 -2
  154. package/src/helpers/Url/Url.js +27 -6
  155. package/src/helpers/Url/Url.test.js +26 -0
  156. package/src/helpers/Utils/Utils.js +38 -13
  157. package/src/helpers/Utils/Utils.test.js +4 -4
  158. package/src/helpers/index.js +7 -2
  159. package/src/hooks/userSession/useToken.js +5 -0
  160. package/src/middleware/Api.test.js +54 -0
  161. package/src/middleware/api.js +8 -4
  162. package/src/reducers/actions/actions.js +1 -1
  163. package/src/reducers/breadcrumbs/breadcrumbs.js +1 -1
  164. package/src/reducers/index.js +2 -0
  165. package/src/reducers/navigation/navigation.js +1 -1
  166. package/src/reducers/relations/relations.js +173 -0
  167. package/src/reducers/types/types.js +1 -1
  168. package/src/routes.js +5 -0
  169. package/src/server.jsx +29 -30
  170. package/src/start-server.js +4 -2
  171. package/test-setup-config.js +1 -0
  172. package/theme/themes/pastanaga/extras/blocks.less +0 -9
  173. package/theme/themes/pastanaga/extras/contents.less +1 -0
  174. package/theme/themes/pastanaga/extras/main.less +80 -1
  175. package/theme/themes/pastanaga/extras/search.less +6 -0
  176. package/theme/themes/pastanaga/extras/sidebar.less +4 -0
  177. package/theme/themes/pastanaga/extras/userscontrolpanel.less +99 -76
  178. package/.changelog.draft +0 -22
  179. package/.editorconfig +0 -36
  180. package/.storybook/main.js +0 -127
  181. package/.storybook/manager.js +0 -15
  182. package/.storybook/preview.js +0 -21
  183. package/.storybook/static/previewImage.svg +0 -48
  184. package/.yarnrc.yml +0 -5
  185. package/jsdoc.json +0 -16
  186. package/netlify.toml +0 -5
  187. package/pyvenv.cfg +0 -3
  188. package/share/man/man1/ttx.1 +0 -225
  189. package/src/components/theme/Header/Header.md +0 -27
  190. package/towncrier.toml +0 -33
@@ -5,6 +5,7 @@ const schema = {
5
5
  properties: {
6
6
  username: { title: 'Username', type: 'string', description: '' },
7
7
  email: { title: 'Email', type: 'string', widget: 'email', description: '' },
8
+ url: { title: 'url', type: 'string', widget: 'url', description: '' },
8
9
  },
9
10
  fieldsets: [
10
11
  { id: 'default', title: 'FIXME: User Data', fields: ['username'] },
@@ -65,6 +66,38 @@ describe('FormValidation', () => {
65
66
  });
66
67
  });
67
68
 
69
+ it('do not treat 0 as missing required value', () => {
70
+ let newSchema = {
71
+ ...schema,
72
+ properties: {
73
+ ...schema.properties,
74
+ age: {
75
+ title: 'age',
76
+ type: 'integer',
77
+ widget: 'number',
78
+ description: '',
79
+ },
80
+ },
81
+ required: ['age'],
82
+ };
83
+ expect(
84
+ FormValidation.validateFieldsPerFieldset({
85
+ schema: newSchema,
86
+ formData: { username: 'test username', age: null },
87
+ formatMessage,
88
+ }),
89
+ ).toEqual({
90
+ age: [messages.required.defaultMessage],
91
+ });
92
+ expect(
93
+ FormValidation.validateFieldsPerFieldset({
94
+ schema: newSchema,
95
+ formData: { username: 'test username', age: 0 },
96
+ formatMessage,
97
+ }),
98
+ ).toEqual({});
99
+ });
100
+
68
101
  it('validates incorrect email', () => {
69
102
  expect(
70
103
  FormValidation.validateFieldsPerFieldset({
@@ -87,5 +120,45 @@ describe('FormValidation', () => {
87
120
  }),
88
121
  ).toEqual({});
89
122
  });
123
+ it('validates incorrect url', () => {
124
+ formData.url = 'foo';
125
+ expect(
126
+ FormValidation.validateFieldsPerFieldset({
127
+ schema,
128
+ formData,
129
+ formatMessage,
130
+ }),
131
+ ).toEqual({ url: [messages.isValidURL.defaultMessage] });
132
+ });
133
+ it('validates url', () => {
134
+ formData.url = 'https://plone.org/';
135
+ expect(
136
+ FormValidation.validateFieldsPerFieldset({
137
+ schema,
138
+ formData,
139
+ formatMessage,
140
+ }),
141
+ ).toEqual({});
142
+ });
143
+ it('validates url with ip', () => {
144
+ formData.url = 'http://127.0.0.1:8080/Plone';
145
+ expect(
146
+ FormValidation.validateFieldsPerFieldset({
147
+ schema,
148
+ formData,
149
+ formatMessage,
150
+ }),
151
+ ).toEqual({});
152
+ });
153
+ it('validates url with localhost', () => {
154
+ formData.url = 'http://localhost:8080/Plone';
155
+ expect(
156
+ FormValidation.validateFieldsPerFieldset({
157
+ schema,
158
+ formData,
159
+ formatMessage,
160
+ }),
161
+ ).toEqual({});
162
+ });
90
163
  });
91
164
  });
@@ -97,8 +97,10 @@ class Html extends Component {
97
97
  } = this.props;
98
98
  const head = Helmet.rewind();
99
99
  const bodyClass = join(BodyClass.rewind(), ' ');
100
+ const htmlAttributes = head.htmlAttributes.toComponent();
101
+
100
102
  return (
101
- <html lang="en">
103
+ <html lang={htmlAttributes.lang}>
102
104
  <head>
103
105
  <meta charSet="utf-8" />
104
106
  {head.base.toComponent()}
@@ -20,6 +20,11 @@ jest.mock('../Helmet/Helmet', () => ({
20
20
  script: {
21
21
  toComponent: () => '',
22
22
  },
23
+ htmlAttributes: {
24
+ toComponent: () => ({
25
+ lang: 'en',
26
+ }),
27
+ },
23
28
  }),
24
29
  }));
25
30
 
@@ -260,4 +260,76 @@ export const messages = defineMessages({
260
260
  id: 'Show groups of users below',
261
261
  defaultMessage: 'Show groups of users below',
262
262
  },
263
+ inspectRelations: {
264
+ id: 'Inspect relations',
265
+ defaultMessage: 'Inspect relations',
266
+ },
267
+ relations: {
268
+ id: 'Relations',
269
+ defaultMessage: 'Relations',
270
+ },
271
+ fixRelations: {
272
+ id: 'Fix relations',
273
+ defaultMessage: 'Fix relations',
274
+ },
275
+ searchRelationSource: {
276
+ id: 'Search sources by title or path',
277
+ defaultMessage: 'Search sources by title or path',
278
+ },
279
+ searchRelationTarget: {
280
+ id: 'Search targets by title or path',
281
+ defaultMessage: 'Search targets by title or path',
282
+ },
283
+ createOrDeleteRelationsToTarget: {
284
+ id: 'Create or delete relations to target',
285
+ defaultMessage: 'Create or delete relations to target',
286
+ },
287
+ relationName: {
288
+ id: 'Relation name',
289
+ defaultMessage: 'relation',
290
+ },
291
+ selectRelation: {
292
+ id: 'Select relation',
293
+ defaultMessage: 'Select relation',
294
+ },
295
+ norelationfound: {
296
+ id: 'No relation found',
297
+ defaultMessage: 'No relation found',
298
+ },
299
+ toomanyrelationsfound: {
300
+ id: 'Many relations found. Please search.',
301
+ defaultMessage: 'Many relations found. Please search.',
302
+ },
303
+ rebuildRelations: {
304
+ id: 'rebuild relations',
305
+ defaultMessage: 'rebuild relations',
306
+ },
307
+ flushAndRebuildRelations: {
308
+ id: 'flush intIds and rebuild relations',
309
+ defaultMessage: 'flush intIds and rebuild relations',
310
+ },
311
+ addPotentialTargetsPath: {
312
+ id: 'target path',
313
+ defaultMessage: 'target path',
314
+ },
315
+ addPotentialSourcesPath: {
316
+ id: 'sources path',
317
+ defaultMessage: 'sources path',
318
+ },
319
+ relationsUpdated: {
320
+ id: 'Relations updated',
321
+ defaultMessage: 'Relations updated',
322
+ },
323
+ select: {
324
+ id: 'Select',
325
+ defaultMessage: 'Select',
326
+ },
327
+ selected: {
328
+ id: 'Selected',
329
+ defaultMessage: 'Selected',
330
+ },
331
+ filter: {
332
+ id: 'Filter',
333
+ defaultMessage: 'Filter',
334
+ },
263
335
  });
@@ -15,12 +15,9 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
15
15
  */
16
16
  export const generateRobots = (req) =>
17
17
  new Promise((resolve) => {
18
- //const url = `${req.protocol}://${req.get('Host')}`;
19
- const request = superagent.get(
20
- `${
21
- config.settings.internalApiPath ?? config.settings.apiPath
22
- }/robots.txt`,
23
- );
18
+ const internalUrl =
19
+ config.settings.internalApiPath ?? config.settings.apiPath;
20
+ const request = superagent.get(`${internalUrl}/robots.txt`);
24
21
  request.set('Accept', 'text/plain');
25
22
  const authToken = req.universalCookies.get('auth_token');
26
23
  if (authToken) {
@@ -31,6 +28,27 @@ export const generateRobots = (req) =>
31
28
  if (error) {
32
29
  resolve(text || error);
33
30
  } else {
31
+ // It appears that express does not take the x-forwarded headers into
32
+ // consideration, so we do it ourselves.
33
+ const {
34
+ 'x-forwarded-proto': forwardedProto,
35
+ 'x-forwarded-host': forwardedHost,
36
+ 'x-forwarded-port': forwardedPort,
37
+ } = req.headers;
38
+ const proto = forwardedProto ?? req.protocol;
39
+ const host = forwardedHost ?? req.get('Host');
40
+ const portNum = forwardedPort ?? req.get('Port');
41
+ const port =
42
+ (proto === 'https' && '' + portNum === '443') ||
43
+ (proto === 'http' && '' + portNum === '80')
44
+ ? ''
45
+ : `:${portNum}`;
46
+ // Plone has probably returned the sitemap link with the internal url.
47
+ // If so, let's replace it with the current one.
48
+ const url = `${proto}://${host}${port}`;
49
+ text = text.replace(internalUrl, url);
50
+ // Replace the sitemap with the sitemap index.
51
+ text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
34
52
  resolve(text);
35
53
  }
36
54
  });
@@ -11,19 +11,23 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
11
11
 
12
12
  import config from '@plone/volto/registry';
13
13
 
14
+ export const SITEMAP_BATCH_SIZE = 5000;
15
+
14
16
  /**
15
17
  * Generate sitemap
16
18
  * @function generateSitemap
17
19
  * @param {Object} _req Request object
18
20
  * @return {string} Generated sitemap
19
21
  */
20
- export const generateSitemap = (_req) =>
22
+ export const generateSitemap = (_req, start = 0, size = undefined) =>
21
23
  new Promise((resolve) => {
22
24
  const { settings } = config;
23
25
  const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
24
26
  const apiPath = settings.internalApiPath ?? settings.apiPath;
25
27
  const request = superagent.get(
26
- `${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_size=100000000&use_site_search_settings=1`,
28
+ `${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_start=${start}&b_size=${
29
+ size !== undefined ? size : 100000000
30
+ }&use_site_search_settings=1`,
27
31
  );
28
32
  request.set('Accept', 'application/json');
29
33
  request.use(addHeadersFactory(_req));
@@ -50,3 +54,41 @@ export const generateSitemap = (_req) =>
50
54
  }
51
55
  });
52
56
  });
57
+
58
+ /**
59
+ * Generate sitemap
60
+ * @function generateSitemapIndex
61
+ * @param {Object} _req Request object
62
+ * @return {string} Generated sitemap index
63
+ */
64
+ export const generateSitemapIndex = (_req) =>
65
+ new Promise((resolve) => {
66
+ const { settings } = config;
67
+ const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
68
+ const apiPath = settings.internalApiPath ?? settings.apiPath;
69
+ const request = superagent.get(
70
+ `${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_size=0&use_site_search_settings=1`,
71
+ );
72
+ request.set('Accept', 'application/json');
73
+ const authToken = _req.universalCookies.get('auth_token');
74
+ if (authToken) {
75
+ request.set('Authorization', `Bearer ${authToken}`);
76
+ }
77
+ request.end((error, { body } = {}) => {
78
+ if (error) {
79
+ resolve(body || error);
80
+ } else {
81
+ const items = Array.from(
82
+ { length: Math.ceil(body.items_total / SITEMAP_BATCH_SIZE) },
83
+ (_, i) =>
84
+ ` <sitemap>
85
+ <loc>${toPublicURL('/sitemap' + (i + 1) + '.xml.gz')}</loc>
86
+ </sitemap>`,
87
+ );
88
+ const result = `<?xml version="1.0" encoding="UTF-8"?>
89
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
90
+ ${items.join('\n')}\n</sitemapindex>`;
91
+ resolve(result);
92
+ }
93
+ });
94
+ });
@@ -7,6 +7,7 @@ import { last, memoize } from 'lodash';
7
7
  import { urlRegex, telRegex, mailRegex } from './urlRegex';
8
8
  import prependHttp from 'prepend-http';
9
9
  import config from '@plone/volto/registry';
10
+ import { matchPath } from 'react-router';
10
11
 
11
12
  /**
12
13
  * Get base url.
@@ -213,7 +214,17 @@ export function expandToBackendURL(path) {
213
214
  */
214
215
  export function isInternalURL(url) {
215
216
  const { settings } = config;
216
- return (
217
+
218
+ const isMatch = (config.settings.externalRoutes ?? []).find((route) => {
219
+ if (typeof route === 'object') {
220
+ return matchPath(flattenToAppURL(url), route.match);
221
+ }
222
+ return matchPath(flattenToAppURL(url), route);
223
+ });
224
+
225
+ const isExcluded = isMatch && Object.keys(isMatch)?.length > 0;
226
+
227
+ const internalURL =
217
228
  url &&
218
229
  (url.indexOf(settings.publicURL) !== -1 ||
219
230
  (settings.internalApiPath &&
@@ -221,8 +232,13 @@ export function isInternalURL(url) {
221
232
  url.indexOf(settings.apiPath) !== -1 ||
222
233
  url.charAt(0) === '/' ||
223
234
  url.charAt(0) === '.' ||
224
- url.startsWith('#'))
225
- );
235
+ url.startsWith('#'));
236
+
237
+ if (internalURL && isExcluded) {
238
+ return false;
239
+ }
240
+
241
+ return internalURL;
226
242
  }
227
243
 
228
244
  /**
@@ -264,14 +280,14 @@ export function isTelephone(text) {
264
280
  }
265
281
 
266
282
  export function normaliseMail(email) {
267
- if (email.toLowerCase().startsWith('mailto:')) {
283
+ if (email?.toLowerCase()?.startsWith('mailto:')) {
268
284
  return email;
269
285
  }
270
286
  return `mailto:${email}`;
271
287
  }
272
288
 
273
289
  export function normalizeTelephone(tel) {
274
- if (tel.toLowerCase().startsWith('tel:')) {
290
+ if (tel?.toLowerCase()?.startsWith('tel:')) {
275
291
  return tel;
276
292
  }
277
293
  return `tel:${tel}`;
@@ -294,12 +310,17 @@ export function checkAndNormalizeUrl(url) {
294
310
  res.url = URLUtils.normalizeTelephone(url);
295
311
  } else {
296
312
  //url
297
- if (!res.url.startsWith('/') && !res.url.startsWith('#')) {
313
+ if (
314
+ res.url?.length >= 0 &&
315
+ !res.url.startsWith('/') &&
316
+ !res.url.startsWith('#')
317
+ ) {
298
318
  res.url = URLUtils.normalizeUrl(url);
299
319
  if (!URLUtils.isUrl(res.url)) {
300
320
  res.isValid = false;
301
321
  }
302
322
  }
323
+ if (res.url === undefined || res.url === null) res.isValid = false;
303
324
  }
304
325
  return res;
305
326
  }
@@ -14,6 +14,9 @@ import {
14
14
  removeProtocol,
15
15
  addAppURL,
16
16
  expandToBackendURL,
17
+ checkAndNormalizeUrl,
18
+ normaliseMail,
19
+ normalizeTelephone,
17
20
  } from './Url';
18
21
 
19
22
  beforeEach(() => {
@@ -61,6 +64,17 @@ describe('Url', () => {
61
64
  it('return empty string if no url is empty string', () => {
62
65
  expect(getBaseUrl('')).toBe('');
63
66
  });
67
+ it('return a null/undefined mailto adress ', () => {
68
+ expect(normaliseMail(null)).toBe('mailto:null');
69
+ expect(normaliseMail(undefined)).toBe('mailto:undefined');
70
+ });
71
+ it('return a null/undefined telephone number', () => {
72
+ expect(normalizeTelephone(null)).toBe('tel:null');
73
+ expect(normalizeTelephone(undefined)).toBe('tel:undefined');
74
+ });
75
+ it('null returns an invalid link', () => {
76
+ expect(checkAndNormalizeUrl(null).isValid).toBe(false);
77
+ });
64
78
  });
65
79
 
66
80
  describe('getView', () => {
@@ -191,6 +205,7 @@ describe('Url', () => {
191
205
  expect(isInternalURL(href)).toBe(false);
192
206
  settings.internalApiPath = saved;
193
207
  });
208
+
194
209
  it('tells if an URL is internal if it is an anchor', () => {
195
210
  const href = '#anchor';
196
211
  expect(isInternalURL(href)).toBe(true);
@@ -211,6 +226,17 @@ describe('Url', () => {
211
226
  const href = undefined;
212
227
  expect(isInternalURL(href)).toBe(undefined);
213
228
  });
229
+ it('tells if an URL is external if settings.externalroutes is persent.', () => {
230
+ const url = `https://localhost:3000/fb/my-page/contents`;
231
+ const blacklistedurl = '/blacklisted';
232
+ settings.externalRoutes = [
233
+ { title: 'My Page', match: '/fb' },
234
+ '/blacklisted',
235
+ ];
236
+ settings.publicURL = 'https://localhost:3000';
237
+ expect(isInternalURL(url)).toBe(false);
238
+ expect(isInternalURL(blacklistedurl)).toBe(false);
239
+ });
214
240
  });
215
241
  describe('isUrl', () => {
216
242
  it('isUrl test', () => {
@@ -174,13 +174,13 @@ export const parseDateTime = (locale, value, format, moment) => {
174
174
  };
175
175
 
176
176
  /**
177
- * Converts a language code to the format `lang_region`
177
+ * Converts a language code like pt-br to the format `pt_BR` (`lang_region`)
178
178
  * Useful for passing from Plone's i18n lang names to Xnix locale names
179
- * eg. LC_MESSAGES/lang_region.po filenames
179
+ * eg. LC_MESSAGES/lang_region.po filenames. Also used in the I18N_LANGUAGE cookie.
180
180
  * @param {string} language Language to be converted
181
181
  * @returns {string} Language converted
182
182
  */
183
- export const normalizeLanguageName = (language) => {
183
+ export const toGettextLang = (language) => {
184
184
  if (language.includes('-')) {
185
185
  let normalizedLang = language.split('-');
186
186
  normalizedLang = `${normalizedLang[0]}_${normalizedLang[1].toUpperCase()}`;
@@ -189,23 +189,35 @@ export const normalizeLanguageName = (language) => {
189
189
 
190
190
  return language;
191
191
  };
192
+ export const normalizeLanguageName = toGettextLang;
192
193
 
193
194
  /**
194
- * Converts a language code to the format `lang-region`
195
- * `react-intl` only supports this syntax, so coming from the language
196
- * negotiation of the `locale` lib, one need to convert it first
195
+ * Converts a language code like pt-br or pt_BR to the format `pt-BR`.
196
+ * `react-intl` only supports this syntax. We also use it for the locales
197
+ * in the volto Redux store.
197
198
  * @param {string} language Language to be converted
198
199
  * @returns {string} Language converted
199
200
  */
200
- export const toLangUnderscoreRegion = (language) => {
201
- if (language.includes('_')) {
202
- let langCode = language.split('_');
201
+ export const toReactIntlLang = (language) => {
202
+ if (language.includes('_') || language.includes('-')) {
203
+ let langCode = language.split(/[-_]/);
203
204
  langCode = `${langCode[0]}-${langCode[1].toUpperCase()}`;
204
205
  return langCode;
205
206
  }
206
207
 
207
208
  return language;
208
209
  };
210
+ export const toLangUnderscoreRegion = toReactIntlLang; // old name for backwards-compat
211
+
212
+ /**
213
+ * Converts a language code like pt_BR or pt-BR to the format `pt-br`.
214
+ * This format is used on the backend and in volto config settings.
215
+ * @param {string} language Language to be converted
216
+ * @returns {string} Language converted
217
+ */
218
+ export const toBackendLang = (language) => {
219
+ return toReactIntlLang(language).toLowerCase();
220
+ };
209
221
 
210
222
  /**
211
223
  * Lookup if a given expander is set in apiExpanders for the given path and action type
@@ -258,11 +270,11 @@ export const removeFromArray = (array, index) => {
258
270
  };
259
271
 
260
272
  /**
261
- * Reorder array
273
+ * Moves an item from origin to target inside an array in an immutable way
262
274
  * @param {Array} array Array with data
263
- * @param {number} origin Index of item to be reordered
264
- * @param {number} target Index of item to be reordered to
265
- * @returns {Array} Array with reordered elements
275
+ * @param {number} origin Index of item to be moved from
276
+ * @param {number} target Index of item to be moved to
277
+ * @returns {Array} Resultant array
266
278
  */
267
279
  export const reorderArray = (array, origin, target) => {
268
280
  const result = Array.from(array);
@@ -299,3 +311,16 @@ export const cloneDeepSchema = (object) => {
299
311
  }
300
312
  });
301
313
  };
314
+
315
+ /**
316
+ * Creates an array given a range of numbers
317
+ * @param {number} start start number from
318
+ * @param {number} stop stop number at
319
+ * @param {number} step step every each number in the sequence
320
+ * @returns {array} The result, eg. [0, 1, 2, 3, 4]
321
+ */
322
+ export const arrayRange = (start, stop, step) =>
323
+ Array.from(
324
+ { length: (stop - start) / step + 1 },
325
+ (value, index) => start + index * step,
326
+ );
@@ -6,7 +6,7 @@ import {
6
6
  getColor,
7
7
  getInitials,
8
8
  hasApiExpander,
9
- normalizeLanguageName,
9
+ toGettextLang,
10
10
  parseDateTime,
11
11
  removeFromArray,
12
12
  reorderArray,
@@ -284,12 +284,12 @@ describe('Utils tests', () => {
284
284
  });
285
285
  });
286
286
 
287
- describe('normalizeLanguageName', () => {
287
+ describe('toGettextLang', () => {
288
288
  it('Normalizes an extended language (pt_BR)', () => {
289
- expect(normalizeLanguageName('pt-br')).toStrictEqual('pt_BR');
289
+ expect(toGettextLang('pt-br')).toStrictEqual('pt_BR');
290
290
  });
291
291
  it('Normalizes a simple language (ca)', () => {
292
- expect(normalizeLanguageName('ca')).toStrictEqual('ca');
292
+ expect(toGettextLang('ca')).toStrictEqual('ca');
293
293
  });
294
294
  });
295
295
 
@@ -80,11 +80,16 @@ export {
80
80
  applyConfig,
81
81
  withServerErrorCode,
82
82
  parseDateTime,
83
- normalizeLanguageName,
84
- toLangUnderscoreRegion,
83
+ toGettextLang,
84
+ normalizeLanguageName, // old name for toGettextLang
85
+ toReactIntlLang,
86
+ toLangUnderscoreRegion, // old name for toReactIntlLang
87
+ toBackendLang,
85
88
  hasApiExpander,
86
89
  replaceItemOfArray,
87
90
  cloneDeepSchema,
91
+ arrayRange,
92
+ reorderArray,
88
93
  } from '@plone/volto/helpers/Utils/Utils';
89
94
  export { messages } from './MessageLabels/MessageLabels';
90
95
  export {
@@ -0,0 +1,5 @@
1
+ import { useSelector, shallowEqual } from 'react-redux';
2
+
3
+ export function useToken() {
4
+ return useSelector((state) => state.userSession.token, shallowEqual);
5
+ }
@@ -53,6 +53,60 @@ describe('api middleware helpers', () => {
53
53
  );
54
54
  expect(result).toEqual('/de/mypage/@navigation?expand.navigation.depth=3');
55
55
  });
56
+ it('addExpandersToPath - Path matching, preserve query', () => {
57
+ config.settings.apiExpanders = [
58
+ {
59
+ match: '/de/mypage',
60
+ GET_CONTENT: ['mycustomexpander', 'mycustomexpander2'],
61
+ },
62
+ ];
63
+
64
+ const result = addExpandersToPath(
65
+ '/de/mypage/@navigation?expand.navigation.depth=3',
66
+ GET_CONTENT,
67
+ );
68
+ expect(result).toEqual(
69
+ '/de/mypage/@navigation?expand=mycustomexpander,mycustomexpander2&expand.navigation.depth=3',
70
+ );
71
+ });
72
+ it('addExpandersToPath - Path matching, preserve query with multiple', () => {
73
+ config.settings.apiExpanders = [
74
+ {
75
+ match: '/de/mypage',
76
+ GET_CONTENT: ['mycustomexpander', 'mycustomexpander2'],
77
+ },
78
+ ];
79
+
80
+ const result = addExpandersToPath(
81
+ '/de/mypage/@navigation?expand.navigation.depth=3&expand.other=2',
82
+ GET_CONTENT,
83
+ );
84
+ expect(result).toEqual(
85
+ '/de/mypage/@navigation?expand=mycustomexpander,mycustomexpander2&expand.navigation.depth=3&expand.other=2',
86
+ );
87
+ });
88
+ it('addExpandersToPath - Path not matching, preserve encoded query', () => {
89
+ config.settings.apiExpanders = [
90
+ {
91
+ match: '/de/otherpath',
92
+ GET_CONTENT: ['mycustomexpander'],
93
+ },
94
+ ];
95
+
96
+ const result = addExpandersToPath('/de/mypage?query=a%26b', GET_CONTENT);
97
+ expect(result).toEqual('/de/mypage?query=a%26b');
98
+ });
99
+ it('addExpandersToPath - Path matching, preserve encoded query', () => {
100
+ config.settings.apiExpanders = [
101
+ {
102
+ match: '/de/mypage',
103
+ GET_CONTENT: ['mycustomexpander'],
104
+ },
105
+ ];
106
+
107
+ const result = addExpandersToPath('/de/mypage?query=a%26b', GET_CONTENT);
108
+ expect(result).toEqual('/de/mypage?expand=mycustomexpander&query=a%26b');
109
+ });
56
110
  it('addExpandersToPath - Two custom expanders from settings', () => {
57
111
  config.settings.apiExpanders = [
58
112
  {
@@ -18,7 +18,11 @@ import {
18
18
  SET_APIERROR,
19
19
  } from '@plone/volto/constants/ActionTypes';
20
20
  import { changeLanguage } from '@plone/volto/actions';
21
- import { normalizeLanguageName, getCookieOptions } from '@plone/volto/helpers';
21
+ import {
22
+ toGettextLang,
23
+ toReactIntlLang,
24
+ getCookieOptions,
25
+ } from '@plone/volto/helpers';
22
26
  let socket = null;
23
27
 
24
28
  /**
@@ -43,7 +47,7 @@ export function addExpandersToPath(path, type, isAnonymous) {
43
47
  const {
44
48
  url,
45
49
  query: { expand, ...query },
46
- } = qs.parseUrl(path);
50
+ } = qs.parseUrl(path, { decode: false });
47
51
 
48
52
  const expandersFromConfig = apiExpanders
49
53
  .filter((expand) => matchPath(url, expand.match) && expand[type])
@@ -205,11 +209,11 @@ const apiMiddlewareFactory = (api) => ({ dispatch, getState }) => (next) => (
205
209
  const lang = result?.language?.token;
206
210
  if (
207
211
  lang &&
208
- getState().intl.language !== lang &&
212
+ getState().intl.locale !== toReactIntlLang(lang) &&
209
213
  !subrequest &&
210
214
  config.settings.supportedLanguages.includes(lang)
211
215
  ) {
212
- const langFileName = normalizeLanguageName(lang);
216
+ const langFileName = toGettextLang(lang);
213
217
  import('~/../locales/' + langFileName + '.json').then((locale) => {
214
218
  dispatch(changeLanguage(lang, locale.default));
215
219
  });