ghost 4.38.1 → 4.40.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/.c8rc.json +1 -1
- package/Gruntfile.js +1 -1
- package/README.md +26 -18
- package/core/built/assets/{chunk.3.4906cf0b01d6d8e33374.js → chunk.3.6e2ed2d00856e12bd81a.js} +19 -19
- package/core/built/assets/ghost-dark-498ff8339a89bb68c3f78f59bee4146e.css +1 -0
- package/core/built/assets/ghost.min-77b93478f83b0def6ddc5a4f23ce963e.css +1 -0
- package/core/built/assets/{ghost.min-6386b02480494a69c3bfe66206754836.js → ghost.min-88c665c3ba304b4f220d08b8bcf9d246.js} +525 -538
- package/core/built/assets/icons/{event-changed-subscription.svg → event-subscriptions.svg} +0 -1
- package/core/built/assets/icons/member.svg +3 -0
- package/core/built/assets/{vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js → vendor.min-ed945ad80ea22f1d3ffeec6d5ae63aee.js} +2355 -1419
- package/core/frontend/apps/private-blogging/lib/middleware.js +1 -1
- package/core/frontend/apps/private-blogging/lib/views/private.hbs +9 -10
- package/core/frontend/public/ghost.css +205 -143
- package/core/frontend/public/ghost.min.css +1 -1
- package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +3 -1
- package/core/frontend/views/unsubscribe.hbs +28 -33
- package/core/frontend/web/middleware/error-handler.js +2 -2
- package/core/server/api/canary/authentication.js +7 -0
- package/core/server/api/canary/identities.js +0 -1
- package/core/server/api/canary/members.js +7 -1
- package/core/server/api/canary/tiers.js +3 -1
- package/core/server/api/canary/utils/serializers/input/pages.js +1 -1
- package/core/server/api/canary/utils/serializers/input/posts.js +1 -1
- package/core/server/api/canary/utils/serializers/input/tiers.js +17 -0
- package/core/server/api/canary/utils/serializers/output/actions.js +2 -2
- package/core/server/api/canary/utils/serializers/output/authentication.js +3 -3
- package/core/server/api/canary/utils/serializers/output/authors.js +3 -3
- package/core/server/api/canary/utils/serializers/output/email-posts.js +2 -2
- package/core/server/api/canary/utils/serializers/output/emails.js +3 -3
- package/core/server/api/canary/utils/serializers/output/images.js +2 -2
- package/core/server/api/canary/utils/serializers/output/integrations.js +5 -6
- package/core/server/api/canary/utils/serializers/output/labels.js +3 -3
- package/core/server/api/canary/utils/serializers/output/mappers/actions.js +7 -0
- package/core/server/api/canary/utils/serializers/output/mappers/emails.js +17 -0
- package/core/server/api/canary/utils/serializers/output/mappers/images.js +5 -0
- package/core/server/api/canary/utils/serializers/output/mappers/index.js +12 -0
- package/core/server/api/canary/utils/serializers/output/mappers/integrations.js +13 -0
- package/core/server/api/canary/utils/serializers/output/mappers/labels.js +4 -0
- package/core/server/api/canary/utils/serializers/output/mappers/pages.js +11 -0
- package/core/server/api/canary/utils/serializers/output/mappers/posts.js +101 -0
- package/core/server/api/canary/utils/serializers/output/mappers/settings.js +37 -0
- package/core/server/api/canary/utils/serializers/output/mappers/tags.js +11 -0
- package/core/server/api/canary/utils/serializers/output/mappers/users.js +12 -0
- package/core/server/api/canary/utils/serializers/output/members.js +2 -7
- package/core/server/api/canary/utils/serializers/output/pages.js +3 -3
- package/core/server/api/canary/utils/serializers/output/posts.js +3 -3
- package/core/server/api/canary/utils/serializers/output/preview.js +2 -2
- package/core/server/api/canary/utils/serializers/output/settings.js +2 -2
- package/core/server/api/canary/utils/serializers/output/tags.js +3 -3
- package/core/server/api/canary/utils/serializers/output/users.js +3 -3
- package/core/server/api/shared/serializers/handle.js +2 -2
- package/core/server/api/v2/utils/serializers/input/pages.js +1 -1
- package/core/server/api/v2/utils/serializers/input/posts.js +1 -1
- package/core/server/api/v3/utils/serializers/input/pages.js +1 -1
- package/core/server/api/v3/utils/serializers/input/posts.js +1 -1
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/importer/import-manager.js +152 -113
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +3 -0
- package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-free-products-visibility-column.js +66 -0
- package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-products-visibility-column.js +36 -0
- package/core/server/data/migrations/versions/4.40/2022-03-07-14-37-add-members-cancel-events-table.js +8 -0
- package/core/server/data/migrations/versions/4.40/2022-03-15-06-40-add-offers-admin-integration-permission-roles.js +23 -0
- package/core/server/data/migrations/versions/4.40/2022-03-15-06-40-add-tiers-admin-integration-permission-roles.js +20 -0
- package/core/server/data/schema/default-settings/default-settings.json +2 -2
- package/core/server/data/schema/fixtures/fixtures.json +17 -160
- package/core/server/data/schema/schema.js +6 -0
- package/core/server/lib/image/image-size.js +12 -4
- package/core/server/models/base/plugins/generate-slug.js +13 -1
- package/core/server/models/base/plugins/raw-knex.js +1 -1
- package/core/server/models/member-cancel-event.js +28 -0
- package/core/server/models/post.js +16 -6
- package/core/server/models/user.js +1 -1
- package/core/server/services/auth/setup.js +29 -13
- package/core/server/services/mega/mega.js +4 -4
- package/core/server/services/mega/template.js +2 -1
- package/core/server/services/members/api.js +1 -0
- package/core/server/services/members/content-gating.js +1 -1
- package/core/server/services/members/middleware.js +1 -0
- package/core/server/services/posts/posts-service.js +1 -1
- package/core/server/services/themes/validate.js +3 -3
- package/core/server/services/url/UrlGenerator.js +1 -1
- package/core/server/services/webhooks/webhooks-service.js +2 -0
- package/core/server/views/maintenance.html +2 -2
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +0 -3
- package/core/server/web/api/canary/admin/middleware.js +2 -0
- package/core/server/web/parent/backend.js +2 -1
- package/core/server/web/shared/middleware/uncapitalise.js +2 -1
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +7 -3
- package/core/shared/labs.js +8 -10
- package/core/shared/url-utils.js +4 -1
- package/package.json +37 -36
- package/yarn.lock +513 -329
- package/core/built/assets/ghost-dark-9f760f16230b8bc52e188d6ce28516b0.css +0 -1
- package/core/built/assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css +0 -1
- package/core/built/assets/icons/event-started-subscription.svg +0 -6
- package/core/built/assets/icons/locked-email-back.svg +0 -1
- package/core/built/assets/icons/locked-email-front.svg +0 -1
- package/core/built/assets/icons/locked-email-lock.svg +0 -1
- package/core/built/assets/img/ghost-logo-de2acf283f53ba1fd1149928faeaaa74.png +0 -0
- package/core/server/api/canary/utils/serializers/output/utils/mapper.js +0 -213
- package/core/server/frontend/ghost.min.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:users');
|
|
2
2
|
const tpl = require('@tryghost/tpl');
|
|
3
|
-
const
|
|
3
|
+
const mappers = require('./mappers');
|
|
4
4
|
|
|
5
5
|
const messages = {
|
|
6
6
|
pwdChangedSuccessfully: 'Password changed successfully.'
|
|
@@ -11,7 +11,7 @@ module.exports = {
|
|
|
11
11
|
debug('browse');
|
|
12
12
|
|
|
13
13
|
frame.response = {
|
|
14
|
-
users: models.data.map(model =>
|
|
14
|
+
users: models.data.map(model => mappers.users(model, frame)),
|
|
15
15
|
meta: models.meta
|
|
16
16
|
};
|
|
17
17
|
},
|
|
@@ -20,7 +20,7 @@ module.exports = {
|
|
|
20
20
|
debug('read');
|
|
21
21
|
|
|
22
22
|
frame.response = {
|
|
23
|
-
users: [
|
|
23
|
+
users: [mappers.users(model, frame)]
|
|
24
24
|
};
|
|
25
25
|
},
|
|
26
26
|
|
|
@@ -21,11 +21,11 @@ module.exports.input = (apiConfig, apiSerializers, frame) => {
|
|
|
21
21
|
const tasks = [];
|
|
22
22
|
const sharedSerializers = require('./input');
|
|
23
23
|
|
|
24
|
-
if (!
|
|
24
|
+
if (!apiConfig) {
|
|
25
25
|
return Promise.reject(new errors.IncorrectUsageError());
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (!
|
|
28
|
+
if (!apiSerializers) {
|
|
29
29
|
return Promise.reject(new errors.IncorrectUsageError());
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const mapNQLKeyValues = require('@
|
|
2
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
3
3
|
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:pages');
|
|
4
4
|
const mobiledoc = require('../../../../../lib/mobiledoc');
|
|
5
5
|
const url = require('./utils/url');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const mapNQLKeyValues = require('@
|
|
2
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
3
3
|
const debug = require('@tryghost/debug')('api:v2:utils:serializers:input:posts');
|
|
4
4
|
const url = require('./utils/url');
|
|
5
5
|
const localUtils = require('../../index');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('api:v3:utils:serializers:input:pages');
|
|
3
|
-
const mapNQLKeyValues = require('@
|
|
3
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
4
4
|
const mobiledoc = require('../../../../../lib/mobiledoc');
|
|
5
5
|
const url = require('./utils/url');
|
|
6
6
|
const slugFilterOrder = require('./utils/slug-filter-order');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('api:v3:utils:serializers:input:posts');
|
|
3
|
-
const mapNQLKeyValues = require('@
|
|
3
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
4
4
|
const url = require('./utils/url');
|
|
5
5
|
const slugFilterOrder = require('./utils/slug-filter-order');
|
|
6
6
|
const localUtils = require('../../index');
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const Promise = require('bluebird');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const os = require('os');
|
|
6
5
|
const glob = require('glob');
|
|
7
6
|
const uuid = require('uuid');
|
|
8
7
|
const {extract} = require('@tryghost/zip');
|
|
9
|
-
const {pipeline, sequence} = require('@tryghost/promise');
|
|
10
8
|
const tpl = require('@tryghost/tpl');
|
|
11
9
|
const logging = require('@tryghost/logging');
|
|
12
10
|
const errors = require('@tryghost/errors');
|
|
@@ -21,7 +19,6 @@ const messages = {
|
|
|
21
19
|
error: 'Import could not clean up file ',
|
|
22
20
|
context: 'Your site will continue to work as expected'
|
|
23
21
|
},
|
|
24
|
-
unsupportedRoonExport: 'Your zip file looks like an old format Roon export, please re-export your Roon blog and try again.',
|
|
25
22
|
noContentToImport: 'Zip did not include any content to import.',
|
|
26
23
|
invalidZipStructure: 'Invalid zip file structure.',
|
|
27
24
|
invalidZipFileBaseDirectory: 'Invalid zip file: base directory read failed',
|
|
@@ -41,10 +38,20 @@ let defaults = {
|
|
|
41
38
|
|
|
42
39
|
class ImportManager {
|
|
43
40
|
constructor() {
|
|
41
|
+
/**
|
|
42
|
+
* @type {Importer[]} importers
|
|
43
|
+
*/
|
|
44
44
|
this.importers = [ImageImporter, DataImporter];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {Handler[]}
|
|
48
|
+
*/
|
|
45
49
|
this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
|
|
46
50
|
|
|
47
51
|
// Keep track of file to cleanup at the end
|
|
52
|
+
/**
|
|
53
|
+
* @type {?string}
|
|
54
|
+
*/
|
|
48
55
|
this.fileToDelete = null;
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -53,7 +60,7 @@ class ImportManager {
|
|
|
53
60
|
* @returns {string[]}
|
|
54
61
|
*/
|
|
55
62
|
getExtensions() {
|
|
56
|
-
return _.
|
|
63
|
+
return _.union(_.flatMap(this.handlers, 'extensions'), defaults.extensions);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -61,7 +68,7 @@ class ImportManager {
|
|
|
61
68
|
* @returns {string[]}
|
|
62
69
|
*/
|
|
63
70
|
getContentTypes() {
|
|
64
|
-
return _.
|
|
71
|
+
return _.union(_.flatMap(this.handlers, 'contentTypes'), defaults.contentTypes);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -69,7 +76,7 @@ class ImportManager {
|
|
|
69
76
|
* @returns {string[]}
|
|
70
77
|
*/
|
|
71
78
|
getDirectories() {
|
|
72
|
-
return _.
|
|
79
|
+
return _.union(_.flatMap(this.handlers, 'directories'), defaults.directories);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
@@ -85,7 +92,7 @@ class ImportManager {
|
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* @param {String[]} extensions
|
|
88
|
-
* @param {Number} level
|
|
95
|
+
* @param {Number} [level]
|
|
89
96
|
* @returns {String}
|
|
90
97
|
*/
|
|
91
98
|
getExtensionGlob(extensions, level) {
|
|
@@ -98,7 +105,7 @@ class ImportManager {
|
|
|
98
105
|
/**
|
|
99
106
|
*
|
|
100
107
|
* @param {String[]} directories
|
|
101
|
-
* @param {Number} level
|
|
108
|
+
* @param {Number} [level]
|
|
102
109
|
* @returns {String}
|
|
103
110
|
*/
|
|
104
111
|
getDirectoryGlob(directories, level) {
|
|
@@ -110,26 +117,24 @@ class ImportManager {
|
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
119
|
* Remove files after we're done (abstracted into a function for easier testing)
|
|
113
|
-
* @returns {
|
|
120
|
+
* @returns {Promise<void>}
|
|
114
121
|
*/
|
|
115
|
-
cleanUp() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (self.fileToDelete === null) {
|
|
122
|
+
async cleanUp() {
|
|
123
|
+
if (this.fileToDelete === null) {
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
127
|
+
try {
|
|
128
|
+
await fs.remove(this.fileToDelete);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logging.error(new errors.InternalServerError({
|
|
131
|
+
err: err,
|
|
132
|
+
context: tpl(messages.couldNotCleanUpFile.error),
|
|
133
|
+
help: tpl(messages.couldNotCleanUpFile.context)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
});
|
|
137
|
+
this.fileToDelete = null;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
@@ -146,28 +151,20 @@ class ImportManager {
|
|
|
146
151
|
* Importable content must be found either in the root, or inside one base directory
|
|
147
152
|
*
|
|
148
153
|
* @param {String} directory
|
|
149
|
-
* @returns {
|
|
154
|
+
* @returns {boolean}
|
|
150
155
|
*/
|
|
151
156
|
isValidZip(directory) {
|
|
152
157
|
// Globs match content in the root or inside a single directory
|
|
153
|
-
const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory});
|
|
158
|
+
const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory, nocase: true});
|
|
154
159
|
|
|
155
160
|
const extMatchesAll = glob.sync(
|
|
156
|
-
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
161
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
|
|
157
162
|
);
|
|
158
163
|
|
|
159
164
|
const dirMatches = glob.sync(
|
|
160
165
|
this.getDirectoryGlob(this.getDirectories(), ROOT_OR_SINGLE_DIR), {cwd: directory}
|
|
161
166
|
);
|
|
162
167
|
|
|
163
|
-
const oldRoonMatches = glob.sync(this.getDirectoryGlob(['drafts', 'published', 'deleted'], ROOT_OR_SINGLE_DIR),
|
|
164
|
-
{cwd: directory});
|
|
165
|
-
|
|
166
|
-
// This is a temporary extra message for the old format roon export which doesn't work with Ghost
|
|
167
|
-
if (oldRoonMatches.length > 0) {
|
|
168
|
-
throw new errors.UnsupportedMediaTypeError({message: tpl(messages.unsupportedRoonExport)});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
168
|
// If this folder contains importable files or a content or images directory
|
|
172
169
|
if (extMatchesBase.length > 0 || (dirMatches.length > 0 && extMatchesAll.length > 0)) {
|
|
173
170
|
return true;
|
|
@@ -182,8 +179,8 @@ class ImportManager {
|
|
|
182
179
|
|
|
183
180
|
/**
|
|
184
181
|
* Use the extract module to extract the given zip file to a temp directory & return the temp directory path
|
|
185
|
-
* @param {
|
|
186
|
-
* @returns {Promise
|
|
182
|
+
* @param {string} filePath
|
|
183
|
+
* @returns {Promise<string>} full path to the extracted folder
|
|
187
184
|
*/
|
|
188
185
|
extractZip(filePath) {
|
|
189
186
|
const tmpDir = path.join(os.tmpdir(), uuid.v4());
|
|
@@ -199,11 +196,11 @@ class ImportManager {
|
|
|
199
196
|
* are relevant to the given handler, and return them as a name and path combo
|
|
200
197
|
* @param {Object} handler
|
|
201
198
|
* @param {String} directory
|
|
202
|
-
* @returns [] Files
|
|
199
|
+
* @returns {File[]} Files
|
|
203
200
|
*/
|
|
204
201
|
getFilesFromZip(handler, directory) {
|
|
205
202
|
const globPattern = this.getExtensionGlob(handler.extensions, ALL_DIRS);
|
|
206
|
-
return _.map(glob.sync(globPattern, {cwd: directory}), function (file) {
|
|
203
|
+
return _.map(glob.sync(globPattern, {cwd: directory, nocase: true}), function (file) {
|
|
207
204
|
return {name: file, path: path.join(directory, file)};
|
|
208
205
|
});
|
|
209
206
|
}
|
|
@@ -215,9 +212,9 @@ class ImportManager {
|
|
|
215
212
|
*/
|
|
216
213
|
getBaseDirectory(directory) {
|
|
217
214
|
// Globs match root level only
|
|
218
|
-
const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory});
|
|
215
|
+
const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory, nocase: true});
|
|
219
216
|
|
|
220
|
-
const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory});
|
|
217
|
+
const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory, nocase: true});
|
|
221
218
|
let extMatchesAll;
|
|
222
219
|
|
|
223
220
|
// There is no base directory
|
|
@@ -226,9 +223,9 @@ class ImportManager {
|
|
|
226
223
|
}
|
|
227
224
|
// There is a base directory, grab it from any ext match
|
|
228
225
|
extMatchesAll = glob.sync(
|
|
229
|
-
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
226
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
|
|
230
227
|
);
|
|
231
|
-
if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) {
|
|
228
|
+
if (extMatchesAll.length < 1 || extMatchesAll[0].split('/').length < 1) {
|
|
232
229
|
throw new errors.ValidationError({message: tpl(messages.invalidZipFileBaseDirectory)});
|
|
233
230
|
}
|
|
234
231
|
|
|
@@ -242,48 +239,42 @@ class ImportManager {
|
|
|
242
239
|
* The data key contains JSON representing any data that should be imported
|
|
243
240
|
* The image key contains references to images that will be stored (and where they will be stored)
|
|
244
241
|
* @param {File} file
|
|
245
|
-
* @returns {Promise
|
|
242
|
+
* @returns {Promise<ImportData>}
|
|
246
243
|
*/
|
|
247
|
-
processZip(file) {
|
|
248
|
-
const
|
|
244
|
+
async processZip(file) {
|
|
245
|
+
const zipDirectory = await this.extractZip(file.path);
|
|
249
246
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
/**
|
|
248
|
+
* @type {ImportData}
|
|
249
|
+
*/
|
|
250
|
+
const importData = {};
|
|
251
|
+
|
|
252
|
+
this.isValidZip(zipDirectory);
|
|
253
|
+
const baseDir = this.getBaseDirectory(zipDirectory);
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
for (const handler of this.handlers) {
|
|
256
|
+
const files = this.getFilesFromZip(handler, zipDirectory);
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
if (files.length > 0) {
|
|
259
259
|
if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
|
|
260
260
|
// This limitation is here to reduce the complexity of the importer for now
|
|
261
|
-
|
|
261
|
+
throw new errors.UnsupportedMediaTypeError({
|
|
262
262
|
message: tpl(messages.zipContainsMultipleDataFormats)
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const files = self.getFilesFromZip(handler, zipDirectory);
|
|
267
|
-
|
|
268
|
-
if (files.length > 0) {
|
|
269
|
-
ops.push(function () {
|
|
270
|
-
return handler.loadFile(files, baseDir).then(function (data) {
|
|
271
|
-
importData[handler.type] = data;
|
|
272
|
-
});
|
|
273
263
|
});
|
|
274
264
|
}
|
|
275
|
-
});
|
|
276
265
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
message: tpl(messages.noContentToImport)
|
|
280
|
-
}));
|
|
266
|
+
const data = await handler.loadFile(files, baseDir);
|
|
267
|
+
importData[handler.type] = data;
|
|
281
268
|
}
|
|
269
|
+
}
|
|
282
270
|
|
|
283
|
-
|
|
284
|
-
|
|
271
|
+
if (Object.keys(importData).length === 0) {
|
|
272
|
+
throw new errors.UnsupportedMediaTypeError({
|
|
273
|
+
message: tpl(messages.noContentToImport)
|
|
285
274
|
});
|
|
286
|
-
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return importData;
|
|
287
278
|
}
|
|
288
279
|
|
|
289
280
|
/**
|
|
@@ -293,7 +284,7 @@ class ImportManager {
|
|
|
293
284
|
* The data key contains JSON representing any data that should be imported
|
|
294
285
|
* The image key contains references to images that will be stored (and where they will be stored)
|
|
295
286
|
* @param {File} file
|
|
296
|
-
* @returns {Promise
|
|
287
|
+
* @returns {Promise<ImportData>}
|
|
297
288
|
*/
|
|
298
289
|
processFile(file, ext) {
|
|
299
290
|
const fileHandler = _.find(this.handlers, function (handler) {
|
|
@@ -313,7 +304,7 @@ class ImportManager {
|
|
|
313
304
|
* Load the given file into usable importData in the format: {data: {}, images: []}, regardless of
|
|
314
305
|
* whether the file is a single importable file like a JSON file, or a zip file containing loads of files.
|
|
315
306
|
* @param {File} file
|
|
316
|
-
* @returns {Promise}
|
|
307
|
+
* @returns {Promise<ImportData>}
|
|
317
308
|
*/
|
|
318
309
|
loadFile(file) {
|
|
319
310
|
const self = this;
|
|
@@ -326,17 +317,14 @@ class ImportManager {
|
|
|
326
317
|
* Pass the prepared importData through the preProcess function of the various importers, so that the importers can
|
|
327
318
|
* make any adjustments to the data based on relationships between it
|
|
328
319
|
* @param {ImportData} importData
|
|
329
|
-
* @returns {Promise
|
|
320
|
+
* @returns {Promise<ImportData>}
|
|
330
321
|
*/
|
|
331
|
-
preProcess(importData) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return importer.preProcess(importData);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
322
|
+
async preProcess(importData) {
|
|
323
|
+
for (const importer of this.importers) {
|
|
324
|
+
importData = importer.preProcess(importData);
|
|
325
|
+
}
|
|
338
326
|
|
|
339
|
-
return
|
|
327
|
+
return Promise.resolve(importData);
|
|
340
328
|
}
|
|
341
329
|
|
|
342
330
|
/**
|
|
@@ -344,65 +332,116 @@ class ImportManager {
|
|
|
344
332
|
* Each importer gets passed the data from importData which has the key matching its type - i.e. it only gets the
|
|
345
333
|
* data that it should import. Each importer then handles actually importing that data into Ghost
|
|
346
334
|
* @param {ImportData} importData
|
|
347
|
-
* @param {
|
|
348
|
-
* @returns {Promise<
|
|
335
|
+
* @param {ImportOptions} [importOptions] to allow override of certain import features such as locking a user
|
|
336
|
+
* @returns {Promise<ImportResult[]>} importResults
|
|
349
337
|
*/
|
|
350
|
-
doImport(importData, importOptions) {
|
|
338
|
+
async doImport(importData, importOptions) {
|
|
351
339
|
importOptions = importOptions || {};
|
|
352
|
-
const
|
|
353
|
-
|
|
340
|
+
const importResults = [];
|
|
341
|
+
|
|
342
|
+
for (const importer of this.importers) {
|
|
354
343
|
if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
|
|
355
|
-
|
|
356
|
-
return importer.doImport(importData[importer.type], importOptions);
|
|
357
|
-
});
|
|
344
|
+
importResults.push(await importer.doImport(importData[importer.type], importOptions));
|
|
358
345
|
}
|
|
359
|
-
}
|
|
346
|
+
}
|
|
360
347
|
|
|
361
|
-
return
|
|
362
|
-
return importResult;
|
|
363
|
-
});
|
|
348
|
+
return importResults;
|
|
364
349
|
}
|
|
365
350
|
|
|
366
351
|
/**
|
|
367
352
|
* Import Step 4:
|
|
368
353
|
* Report on what was imported, currently a no-op
|
|
369
|
-
* @param {
|
|
370
|
-
* @returns {Promise<
|
|
354
|
+
* @param {ImportResult[]} importResults
|
|
355
|
+
* @returns {Promise<ImportResult[]>} importResults
|
|
371
356
|
*/
|
|
372
|
-
generateReport(
|
|
373
|
-
return Promise.resolve(
|
|
357
|
+
async generateReport(importResults) {
|
|
358
|
+
return Promise.resolve(importResults);
|
|
374
359
|
}
|
|
375
360
|
|
|
376
361
|
/**
|
|
377
362
|
* Import From File
|
|
378
363
|
* The main method of the ImportManager, call this to kick everything off!
|
|
379
364
|
* @param {File} file
|
|
380
|
-
* @param {
|
|
381
|
-
* @returns {Promise}
|
|
365
|
+
* @param {ImportOptions} importOptions to allow override of certain import features such as locking a user
|
|
366
|
+
* @returns {Promise<ImportResult[]>}
|
|
382
367
|
*/
|
|
383
|
-
importFromFile(file, importOptions = {}) {
|
|
384
|
-
|
|
368
|
+
async importFromFile(file, importOptions = {}) {
|
|
369
|
+
try {
|
|
370
|
+
// Step 1: Handle converting the file to usable data
|
|
371
|
+
let importData = await this.loadFile(file);
|
|
385
372
|
|
|
386
|
-
// Step 1: Handle converting the file to usable data
|
|
387
|
-
return this.loadFile(file).then(function (importData) {
|
|
388
373
|
// Step 2: Let the importers pre-process the data
|
|
389
|
-
|
|
390
|
-
|
|
374
|
+
importData = await this.preProcess(importData);
|
|
375
|
+
|
|
391
376
|
// Step 3: Actually do the import
|
|
392
377
|
// @TODO: It would be cool to have some sort of dry run flag here
|
|
393
|
-
|
|
394
|
-
|
|
378
|
+
let importResult = await this.doImport(importData, importOptions);
|
|
379
|
+
|
|
395
380
|
// Step 4: Report on the import
|
|
396
|
-
return
|
|
397
|
-
}
|
|
381
|
+
return await this.generateReport(importResult);
|
|
382
|
+
} finally {
|
|
383
|
+
// Step 5: Cleanup any files
|
|
384
|
+
this.cleanUp();
|
|
385
|
+
}
|
|
398
386
|
}
|
|
399
387
|
}
|
|
400
388
|
|
|
401
389
|
/**
|
|
402
|
-
*
|
|
390
|
+
* @typedef {object} ImportOptions
|
|
391
|
+
* @property {boolean} [returnImportedData]
|
|
392
|
+
* @property {boolean} [importPersistUser]
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @typedef {object} Importer
|
|
397
|
+
* @property {"images"|"data"} type
|
|
398
|
+
* @property {PreProcessMethod} preProcess
|
|
399
|
+
* @property {DoImportMethod} doImport
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @callback PreProcessMethod
|
|
404
|
+
* @param {ImportData} importData
|
|
405
|
+
* @returns {ImportData}
|
|
406
|
+
*/
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @callback DoImportMethod
|
|
410
|
+
* @param {object|object[]} importData
|
|
411
|
+
* @param {ImportOptions} importOptions
|
|
412
|
+
* @returns {Promise<ImportResult>} import result
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @typedef {object} Handler
|
|
417
|
+
* @property {"images"|"data"} type
|
|
418
|
+
* @property {string[]} extensions
|
|
419
|
+
* @property {string[]} contentTypes
|
|
420
|
+
* @property {string[]} directories
|
|
421
|
+
* @property {LoadFileMethod} loadFile
|
|
422
|
+
*/
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @callback LoadFileMethod
|
|
426
|
+
* @param {File[]} files
|
|
427
|
+
* @param {string} [baseDir]
|
|
428
|
+
* @returns {Promise<object[]|object>} data
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* File object
|
|
433
|
+
* @typedef {Object} File
|
|
434
|
+
* @property {string} name
|
|
435
|
+
* @property {string} path
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
/**
|
|
403
439
|
* @typedef {Object} ImportData
|
|
404
|
-
* @property
|
|
405
|
-
* @property
|
|
440
|
+
* @property {Object} [data]
|
|
441
|
+
* @property {Array} [images]
|
|
406
442
|
*/
|
|
407
443
|
|
|
444
|
+
/**
|
|
445
|
+
* @typedef {Object} ImportResult
|
|
446
|
+
*/
|
|
408
447
|
module.exports = new ImportManager();
|
|
@@ -19,6 +19,9 @@ module.exports = createTransactionalMigration(
|
|
|
19
19
|
const id = ObjectID().toHexString();
|
|
20
20
|
|
|
21
21
|
logging.info(`Adding tier "${name}"`);
|
|
22
|
+
|
|
23
|
+
// `slugify(id) is used to ensure unique slug here, as existing tiers could be using any name including "Free"
|
|
24
|
+
// slugs are not used anywhere user-facing so the value is acceptable
|
|
22
25
|
await knex('products')
|
|
23
26
|
.insert({
|
|
24
27
|
id: id,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
const portalPlanSetting = await knex('settings').select('value').where('key', 'portal_plans').first();
|
|
7
|
+
|
|
8
|
+
if (!portalPlanSetting) {
|
|
9
|
+
logging.warn('Could not find portal_plans setting - skipping migration');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const settingData = JSON.parse(portalPlanSetting.value);
|
|
15
|
+
|
|
16
|
+
if (!settingData.includes('free')) {
|
|
17
|
+
logging.warn(`portal_plans does not include "free" - skipping migration`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logging.info(`Updating free products to visible`);
|
|
22
|
+
await knex('products').update('visibility', 'public').where('type', 'free');
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logging.error(err);
|
|
25
|
+
logging.warn('portal_plans setting is invalid - skipping migration');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async function down(knex) {
|
|
30
|
+
const freeTier = await knex('products').select('id').where('type', 'free').first();
|
|
31
|
+
const portalPlanSetting = await knex('settings').select('value').where('key', 'portal_plans').first();
|
|
32
|
+
|
|
33
|
+
if (!freeTier) {
|
|
34
|
+
logging.info('Free tier is not visible, not updating portal_plans');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!portalPlanSetting) {
|
|
39
|
+
logging.warn('Could not find portal_plans setting - skipping migration');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const existingSettingData = JSON.parse(portalPlanSetting.value);
|
|
45
|
+
let settingData;
|
|
46
|
+
|
|
47
|
+
if (freeTier.visibility === 'public') {
|
|
48
|
+
if (existingSettingData.includes('free')) {
|
|
49
|
+
logging.info('portal_plans setting already contains "free" - skipping update');
|
|
50
|
+
return;
|
|
51
|
+
} else {
|
|
52
|
+
settingData = JSON.stringify(existingSettingData.concat('free'));
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
settingData = JSON.stringify(existingSettingData.filter(value => value !== 'free'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logging.info(`Updating portal_plans to ${settingData}`);
|
|
59
|
+
await knex('settings').update('value', settingData).where('key', 'portal_plans');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
logging.error(err);
|
|
62
|
+
logging.warn('portal_plans setting is invalid - skipping migration');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
const portalProductSetting = await knex('settings').select('value').where('key', 'portal_products').first();
|
|
7
|
+
|
|
8
|
+
if (!portalProductSetting) {
|
|
9
|
+
logging.warn('Could not find portal_products setting - skipping migration');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const settingData = JSON.parse(portalProductSetting.value);
|
|
15
|
+
|
|
16
|
+
if (settingData.length === 0) {
|
|
17
|
+
logging.warn(`portal_product is empty, skipping migrations`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logging.info(`Updating ${settingData.length} products to visible, ${settingData}`);
|
|
22
|
+
await knex('products').update('visibility', 'public').whereIn('id', settingData);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logging.warn('portal_products setting is invalid - skipping migration');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
async function down(knex) {
|
|
29
|
+
const visibleTiers = await knex('products').select('id').where('visibility', 'public');
|
|
30
|
+
|
|
31
|
+
const settingData = JSON.stringify(visibleTiers.map(obj => obj.id));
|
|
32
|
+
|
|
33
|
+
logging.info(`Updating portal_products to ${settingData}`);
|
|
34
|
+
await knex('settings').update('value', settingData).where('key', 'portal_products');
|
|
35
|
+
}
|
|
36
|
+
);
|