@strapi/strapi 4.6.0-beta.1 → 4.6.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/README.md CHANGED
@@ -80,25 +80,29 @@ Complete installation requirements can be found in the documentation under <a hr
80
80
  - CentOS/RHEL 8
81
81
  - macOS Mojave
82
82
  - Windows 10
83
- - Docker - [Docker-Repo](https://github.com/strapi/strapi-docker)
83
+ - Docker
84
84
 
85
85
  (Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)
86
86
 
87
87
  **Node:**
88
88
 
89
- - NodeJS >= 14 <= 18
90
- - NPM >= 6.x
89
+ Strapi only supports maintenance and LTS versions of Node.js. Please refer to the <a href="https://nodejs.org/en/about/releases/">Node.js release schedule</a> for more information. NPM versions installed by default with Node.js are supported. Generally it's recommended to use yarn over npm where possible.
90
+
91
+ | Strapi Version | Recommended | Minimum |
92
+ | -------------- | ----------- | ------- |
93
+ | 4.3.9 and up | 18.x | 14.x |
94
+ | 4.0.x to 4.3.8 | 16.x | 14.x |
91
95
 
92
96
  **Database:**
93
97
 
94
- | Database | Minimum | Recommended |
95
- | ---------- | ------- | ----------- |
96
- | MySQL | 5.7.8 | 8.0 |
97
- | MariaDB | 10.3 | 10.6 |
98
- | PostgreSQL | 11.0 | 14.0 |
99
- | SQLite | 3 | 3 |
98
+ | Database | Recommended | Minimum |
99
+ | ---------- | ----------- | ------- |
100
+ | MySQL | 8.0 | 5.7.8 |
101
+ | MariaDB | 10.6 | 10.3 |
102
+ | PostgreSQL | 14.0 | 11.0 |
103
+ | SQLite | 3 | 3 |
100
104
 
101
- **We recommend always using the latest version of Strapi to start your new projects**.
105
+ **We recommend always using the latest version of Strapi stable to start your new projects**.
102
106
 
103
107
  ## Features
104
108
 
package/bin/strapi.js CHANGED
@@ -14,7 +14,21 @@ const inquirer = require('inquirer');
14
14
  const program = new Command();
15
15
 
16
16
  const packageJSON = require('../package.json');
17
- const { promptEncryptionKey, confirmMessage } = require('../lib/commands/utils/commander');
17
+ const {
18
+ promptEncryptionKey,
19
+ confirmMessage,
20
+ forceOption,
21
+ } = require('../lib/commands/utils/commander');
22
+ const { exitWith } = require('../lib/commands/utils/helpers');
23
+ const {
24
+ excludeOption,
25
+ onlyOption,
26
+ validateExcludeOnly,
27
+ } = require('../lib/commands/transfer/utils');
28
+
29
+ process.on('SIGINT', () => {
30
+ process.exit();
31
+ });
18
32
 
19
33
  const checkCwdIsStrapiApp = (name) => {
20
34
  const logErrorAndExit = () => {
@@ -258,10 +272,65 @@ program
258
272
  .option('-s, --silent', `Run the generation silently, without any output`, false)
259
273
  .action(getLocalScript('ts/generate-types'));
260
274
 
275
+ // if (process.env.STRAPI_EXPERIMENTAL === 'true') {
276
+ // // `$ strapi transfer`
277
+ // program
278
+ // .command('transfer')
279
+ // .description('Transfer data from one source to another')
280
+ // .allowExcessArguments(false)
281
+ // .addOption(
282
+ // new Option(
283
+ // '--from <sourceURL>',
284
+ // `URL of the remote Strapi instance to get data from`
285
+ // ).argParser(parseURL)
286
+ // )
287
+ // .addOption(
288
+ // new Option(
289
+ // '--to <destinationURL>',
290
+ // `URL of the remote Strapi instance to send data to`
291
+ // ).argParser(parseURL)
292
+ // )
293
+ // .addOption(forceOption)
294
+ // // Validate URLs
295
+ // .hook(
296
+ // 'preAction',
297
+ // ifOptions(
298
+ // (opts) => opts.from,
299
+ // (thisCommand) => assertUrlHasProtocol(thisCommand.opts().from, ['https:', 'http:'])
300
+ // )
301
+ // )
302
+ // .hook(
303
+ // 'preAction',
304
+ // ifOptions(
305
+ // (opts) => opts.to,
306
+ // (thisCommand) => assertUrlHasProtocol(thisCommand.opts().to, ['https:', 'http:'])
307
+ // )
308
+ // )
309
+ // .hook(
310
+ // 'preAction',
311
+ // ifOptions(
312
+ // (opts) => !opts.from && !opts.to,
313
+ // () => exitWith(1, 'At least one source (from) or destination (to) option must be provided')
314
+ // )
315
+ // )
316
+ // .addOption(forceOption)
317
+ // .addOption(excludeOption)
318
+ // .addOption(onlyOption)
319
+ // .hook('preAction', validateExcludeOnly)
320
+ // .hook(
321
+ // 'preAction',
322
+ // confirmMessage(
323
+ // 'The import will delete all data in the remote database. Are you sure you want to proceed?'
324
+ // )
325
+ // )
326
+ // .action(getLocalScript('transfer/transfer'));
327
+ // }
328
+
261
329
  // `$ strapi export`
262
330
  program
263
331
  .command('export')
264
332
  .description('Export data from Strapi to file')
333
+ .allowExcessArguments(false)
265
334
  .addOption(
266
335
  new Option('--no-encrypt', `Disables 'aes-128-ecb' encryption of the output file`).default(true)
267
336
  )
@@ -273,7 +342,9 @@ program
273
342
  )
274
343
  )
275
344
  .addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
276
- .allowExcessArguments(false)
345
+ .addOption(excludeOption)
346
+ .addOption(onlyOption)
347
+ .hook('preAction', validateExcludeOnly)
277
348
  .hook('preAction', promptEncryptionKey)
278
349
  .action(getLocalScript('transfer/export'));
279
350
 
@@ -281,6 +352,7 @@ program
281
352
  program
282
353
  .command('import')
283
354
  .description('Import data from file to Strapi')
355
+ .allowExcessArguments(false)
284
356
  .requiredOption(
285
357
  '-f, --file <file>',
286
358
  'path and filename for the Strapi export file you want to import'
@@ -291,7 +363,10 @@ program
291
363
  'Provide encryption key in command instead of using the prompt'
292
364
  )
293
365
  )
294
- .allowExcessArguments(false)
366
+ .addOption(forceOption)
367
+ .addOption(excludeOption)
368
+ .addOption(onlyOption)
369
+ .hook('preAction', validateExcludeOnly)
295
370
  .hook('preAction', async (thisCommand) => {
296
371
  const opts = thisCommand.opts();
297
372
  const ext = path.extname(String(opts.file));
@@ -307,13 +382,34 @@ program
307
382
  },
308
383
  ]);
309
384
  if (!answers.key?.length) {
310
- console.log('No key entered, aborting import.');
311
- process.exit(0);
385
+ exitWith(0, 'No key entered, aborting import.');
312
386
  }
313
387
  opts.key = answers.key;
314
388
  }
315
389
  }
316
390
  })
391
+ // set decrypt and decompress options based on filename
392
+ .hook('preAction', (thisCommand) => {
393
+ const opts = thisCommand.opts();
394
+
395
+ const { extname, parse } = path;
396
+
397
+ let file = opts.file;
398
+
399
+ if (extname(file) === '.enc') {
400
+ file = parse(file).name; // trim the .enc extension
401
+ thisCommand.opts().decrypt = true;
402
+ } else {
403
+ thisCommand.opts().decrypt = false;
404
+ }
405
+
406
+ if (extname(file) === '.gz') {
407
+ file = parse(file).name; // trim the .gz extension
408
+ thisCommand.opts().decompress = true;
409
+ } else {
410
+ thisCommand.opts().decompress = false;
411
+ }
412
+ })
317
413
  .hook(
318
414
  'preAction',
319
415
  confirmMessage(
package/ee/ee-store.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const eeStoreModel = {
4
+ uid: 'strapi::ee-store',
5
+ collectionName: 'strapi_ee_store_settings',
6
+ attributes: {
7
+ key: {
8
+ type: 'string',
9
+ },
10
+ value: {
11
+ type: 'text',
12
+ },
13
+ },
14
+ };
15
+
16
+ module.exports = {
17
+ eeStoreModel,
18
+ };
package/ee/index.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const { pick } = require('lodash/fp');
4
+
5
+ const { readLicense, verifyLicense, fetchLicense, LicenseCheckError } = require('./license');
6
+ const { eeStoreModel } = require('./ee-store');
7
+ const { shiftCronExpression } = require('../lib/utils/cron');
8
+
9
+ const ONE_MINUTE = 1000 * 60;
10
+
11
+ const ee = {
12
+ enabled: false,
13
+ licenseInfo: {},
14
+ };
15
+
16
+ const disable = (message) => {
17
+ ee.logger?.warn(`${message} Switching to CE.`);
18
+ // Only keep the license key for potential re-enabling during a later check
19
+ ee.licenseInfo = pick('licenseKey', ee.licenseInfo);
20
+ ee.enabled = false;
21
+ };
22
+
23
+ let initialized = false;
24
+
25
+ /**
26
+ * Optimistically enable EE if the format of the license is valid, only run once.
27
+ */
28
+ const init = (licenseDir, logger) => {
29
+ if (initialized) {
30
+ return;
31
+ }
32
+
33
+ initialized = true;
34
+ ee.logger = logger;
35
+
36
+ if (process.env.STRAPI_DISABLE_EE?.toLowerCase() === 'true') {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ const license = process.env.STRAPI_LICENSE || readLicense(licenseDir);
42
+
43
+ if (license) {
44
+ ee.licenseInfo = verifyLicense(license);
45
+ ee.enabled = true;
46
+ }
47
+ } catch (error) {
48
+ disable(error.message);
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Contact the license registry to update the license to its latest state.
54
+ *
55
+ * Store the result in database to avoid unecessary requests, and will fallback to that in case of a network failure.
56
+ */
57
+ const onlineUpdate = async ({ strapi }) => {
58
+ const { get, commit, rollback } = await strapi.db.transaction();
59
+ const transaction = get();
60
+
61
+ try {
62
+ const storedInfo = await strapi.db
63
+ .queryBuilder(eeStoreModel.uid)
64
+ .where({ key: 'ee_information' })
65
+ .select('value')
66
+ .first()
67
+ .transacting(transaction)
68
+ .forUpdate()
69
+ .execute()
70
+ .then((result) => (result ? JSON.parse(result.value) : result));
71
+
72
+ const shouldContactRegistry = (storedInfo?.lastCheckAt ?? 0) < Date.now() - ONE_MINUTE;
73
+ const result = { lastCheckAt: Date.now() };
74
+
75
+ const fallback = (error) => {
76
+ if (error instanceof LicenseCheckError && error.shouldFallback && storedInfo?.license) {
77
+ ee.logger?.warn(
78
+ `${error.message} The last stored one will be used as a potential fallback.`
79
+ );
80
+ return storedInfo.license;
81
+ }
82
+
83
+ result.error = error.message;
84
+ disable(error.message);
85
+ };
86
+
87
+ const license = shouldContactRegistry
88
+ ? await fetchLicense(ee.licenseInfo.licenseKey, strapi.config.get('uuid')).catch(fallback)
89
+ : storedInfo.license;
90
+
91
+ if (license) {
92
+ try {
93
+ ee.licenseInfo = verifyLicense(license);
94
+ validateInfo();
95
+ } catch (error) {
96
+ disable(error.message);
97
+ }
98
+ } else if (!shouldContactRegistry) {
99
+ disable(storedInfo.error);
100
+ }
101
+
102
+ if (shouldContactRegistry) {
103
+ result.license = license ?? null;
104
+ const query = strapi.db.queryBuilder(eeStoreModel.uid).transacting(transaction);
105
+
106
+ if (!storedInfo) {
107
+ query.insert({ key: 'ee_information', value: JSON.stringify(result) });
108
+ } else {
109
+ query.update({ value: JSON.stringify(result) }).where({ key: 'ee_information' });
110
+ }
111
+
112
+ await query.execute();
113
+ }
114
+
115
+ await commit();
116
+ } catch (error) {
117
+ // Example of errors: SQLite does not support FOR UPDATE
118
+ await rollback();
119
+ }
120
+ };
121
+
122
+ const validateInfo = () => {
123
+ const expirationTime = new Date(ee.licenseInfo.expireAt).getTime();
124
+
125
+ if (expirationTime < new Date().getTime()) {
126
+ return disable('License expired.');
127
+ }
128
+
129
+ ee.enabled = true;
130
+ };
131
+
132
+ const checkLicense = async ({ strapi }) => {
133
+ const shouldStayOffline =
134
+ ee.licenseInfo.type === 'gold' &&
135
+ // This env variable support is temporarily used to ease the migration between online vs offline
136
+ process.env.STRAPI_DISABLE_LICENSE_PING?.toLowerCase() === 'true';
137
+
138
+ if (!shouldStayOffline) {
139
+ await onlineUpdate({ strapi });
140
+ strapi.cron.add({ [shiftCronExpression('0 0 */12 * * *')]: onlineUpdate });
141
+ } else {
142
+ if (!ee.licenseInfo.expireAt) {
143
+ return disable('Your license does not have offline support.');
144
+ }
145
+
146
+ validateInfo();
147
+ }
148
+ };
149
+
150
+ const list = () => {
151
+ return (
152
+ ee.licenseInfo.features?.map((feature) =>
153
+ typeof feature === 'object' ? feature : { name: feature }
154
+ ) || []
155
+ );
156
+ };
157
+
158
+ const get = (featureName) => list().find((feature) => feature.name === featureName);
159
+
160
+ module.exports = Object.freeze({
161
+ init,
162
+ checkLicense,
163
+
164
+ get isEE() {
165
+ return ee.enabled;
166
+ },
167
+
168
+ features: Object.freeze({
169
+ list,
170
+ get,
171
+ isEnabled: (featureName) => get(featureName) !== undefined,
172
+ }),
173
+ });
package/ee/license.js ADDED
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { join } = require('path');
5
+ const crypto = require('crypto');
6
+ const fetch = require('node-fetch');
7
+
8
+ const machineId = require('../lib/utils/machine-id');
9
+
10
+ const DEFAULT_FEATURES = {
11
+ bronze: [],
12
+ silver: [],
13
+ gold: ['sso', { name: 'audit-logs', options: { retentionDays: 90 } }],
14
+ };
15
+
16
+ const publicKey = fs.readFileSync(join(__dirname, 'resources/key.pub'));
17
+
18
+ class LicenseCheckError extends Error {
19
+ constructor(message, shouldFallback = false) {
20
+ super(message);
21
+
22
+ this.shouldFallback = shouldFallback;
23
+ }
24
+ }
25
+
26
+ const readLicense = (directory) => {
27
+ try {
28
+ const path = join(directory, 'license.txt');
29
+ return fs.readFileSync(path).toString();
30
+ } catch (error) {
31
+ if (error.code !== 'ENOENT') {
32
+ throw Error('License file not readable, review its format and access rules.');
33
+ }
34
+ }
35
+ };
36
+
37
+ const verifyLicense = (license) => {
38
+ const [signature, base64Content] = Buffer.from(license, 'base64').toString().split('\n');
39
+
40
+ if (!signature || !base64Content) {
41
+ throw new Error('Invalid license.');
42
+ }
43
+
44
+ const stringifiedContent = Buffer.from(base64Content, 'base64').toString();
45
+
46
+ const verify = crypto.createVerify('RSA-SHA256');
47
+ verify.update(stringifiedContent);
48
+ verify.end();
49
+
50
+ const verified = verify.verify(publicKey, signature, 'base64');
51
+
52
+ if (!verified) {
53
+ throw new Error('Invalid license.');
54
+ }
55
+
56
+ const licenseInfo = JSON.parse(stringifiedContent);
57
+
58
+ if (!licenseInfo.features) {
59
+ licenseInfo.features = DEFAULT_FEATURES[licenseInfo.type];
60
+ }
61
+
62
+ Object.freeze(licenseInfo.features);
63
+ return licenseInfo;
64
+ };
65
+
66
+ const throwError = () => {
67
+ throw new LicenseCheckError('Could not proceed to the online validation of your license.', true);
68
+ };
69
+
70
+ const fetchLicense = async (key, projectId) => {
71
+ const response = await fetch(`https://license.strapi.io/api/licenses/validate`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ key, projectId, deviceId: machineId() }),
75
+ }).catch(throwError);
76
+
77
+ const contentType = response.headers.get('Content-Type');
78
+
79
+ if (contentType.includes('application/json')) {
80
+ const { data, error } = await response.json();
81
+
82
+ switch (response.status) {
83
+ case 200:
84
+ return data.license;
85
+ case 400:
86
+ throw new LicenseCheckError(error.message);
87
+ case 404:
88
+ throw new LicenseCheckError('The license used does not exists.');
89
+ default:
90
+ throwError();
91
+ }
92
+ } else {
93
+ throwError();
94
+ }
95
+ };
96
+
97
+ module.exports = Object.freeze({
98
+ readLicense,
99
+ verifyLicense,
100
+ fetchLicense,
101
+ LicenseCheckError,
102
+ });
File without changes
package/lib/Strapi.js CHANGED
@@ -17,6 +17,7 @@ const { createServer } = require('./services/server');
17
17
  const createWebhookRunner = require('./services/webhook-runner');
18
18
  const { webhookModel, createWebhookStore } = require('./services/webhook-store');
19
19
  const { createCoreStore, coreStoreModel } = require('./services/core-store');
20
+ const { eeStoreModel } = require('../ee/ee-store');
20
21
  const createEntityService = require('./services/entity-service');
21
22
  const createCronService = require('./services/cron');
22
23
  const entityValidator = require('./services/entity-validator');
@@ -120,16 +121,20 @@ class Strapi {
120
121
  this.customFields = createCustomFields(this);
121
122
 
122
123
  createUpdateNotifier(this).notify();
124
+
125
+ Object.defineProperty(this, 'EE', {
126
+ get: () => {
127
+ ee.init(this.dirs.app.root, this.log);
128
+ return ee.isEE;
129
+ },
130
+ configurable: false,
131
+ });
123
132
  }
124
133
 
125
134
  get config() {
126
135
  return this.container.get('config');
127
136
  }
128
137
 
129
- get EE() {
130
- return ee({ dir: this.dirs.app.root, logger: this.log });
131
- }
132
-
133
138
  get services() {
134
139
  return this.container.get('services').getAll();
135
140
  }
@@ -225,7 +230,7 @@ class Strapi {
225
230
 
226
231
  await this.runLifecyclesFunctions(LIFECYCLES.DESTROY);
227
232
 
228
- this.eventHub.removeAllListeners();
233
+ this.eventHub.destroy();
229
234
 
230
235
  if (_.has(this, 'db')) {
231
236
  await this.db.destroy();
@@ -404,6 +409,7 @@ class Strapi {
404
409
  const contentTypes = [
405
410
  coreStoreModel,
406
411
  webhookModel,
412
+ eeStoreModel,
407
413
  ...Object.values(strapi.contentTypes),
408
414
  ...Object.values(strapi.components),
409
415
  ];
@@ -447,6 +453,10 @@ class Strapi {
447
453
 
448
454
  await this.db.schema.sync();
449
455
 
456
+ if (this.EE) {
457
+ await ee.checkLicense({ strapi: this });
458
+ }
459
+
450
460
  await this.hook('strapi::content-types.afterSync').call({
451
461
  oldContentTypes,
452
462
  contentTypes: strapi.contentTypes,
@@ -30,7 +30,7 @@ module.exports = async ({ buildDestDir, forceBuild = true, optimization, srcDir
30
30
  // Always remove the .cache and build folders
31
31
  await strapiAdmin.clean({ appDir: srcDir, buildDestDir });
32
32
 
33
- ee({ dir: srcDir });
33
+ ee.init(srcDir);
34
34
 
35
35
  return strapiAdmin
36
36
  .build({