ghost 5.119.3 → 5.120.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.
- package/components/tryghost-i18n-5.120.1.tgz +0 -0
- package/core/boot.js +0 -2
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8480baa8.mjs → index-14e518a7.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
- package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
- package/core/built/admin/assets/chunk.524.24ab802c6c20f2da3449.js +28 -0
- package/core/built/admin/assets/{chunk.582.2697b46a5652693fc674.js → chunk.582.ebfc460cd2d6864d2cc9.js} +20 -27
- package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
- package/core/built/admin/assets/posts/posts.js +13680 -13671
- package/core/built/admin/assets/stats/stats.js +16457 -16635
- package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/match.js +6 -0
- package/core/frontend/services/routing/ParentRouter.js +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +0 -2
- package/core/frontend/services/routing/controllers/previews.js +0 -3
- package/core/frontend/web/middleware/frontend-caching.js +2 -2
- package/core/server/api/endpoints/authentication.js +37 -73
- package/core/server/api/endpoints/authors-public.js +8 -9
- package/core/server/api/endpoints/db.js +34 -35
- package/core/server/api/endpoints/emails.js +8 -10
- package/core/server/api/endpoints/integrations.js +20 -18
- package/core/server/api/endpoints/invites.js +8 -10
- package/core/server/api/endpoints/labels.js +19 -23
- package/core/server/api/endpoints/notifications.js +3 -4
- package/core/server/api/endpoints/pages-public.js +8 -10
- package/core/server/api/endpoints/pages.js +14 -18
- package/core/server/api/endpoints/posts-public.js +8 -10
- package/core/server/api/endpoints/posts.js +6 -8
- package/core/server/api/endpoints/previews.js +8 -10
- package/core/server/api/endpoints/redirects.js +7 -8
- package/core/server/api/endpoints/schedules.js +5 -7
- package/core/server/api/endpoints/slugs.js +7 -9
- package/core/server/api/endpoints/snippets.js +16 -20
- package/core/server/api/endpoints/tags-public.js +8 -10
- package/core/server/api/endpoints/tags.js +19 -23
- package/core/server/api/endpoints/themes.js +6 -8
- package/core/server/api/endpoints/users.js +31 -36
- package/core/server/api/endpoints/utils/permissions.js +10 -10
- package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
- package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
- package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
- package/core/server/api/endpoints/webhooks.js +38 -42
- package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/lib/image/Gravatar.js +12 -13
- package/core/server/lib/lexical.js +3 -1
- package/core/server/models/newsletter.js +6 -1
- package/core/server/services/api-version-compatibility/index.js +1 -33
- package/core/server/services/auth/session/emails/signin.js +3 -3
- package/core/server/services/email-address/EmailAddressParser.js +70 -0
- package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
- package/core/server/services/email-address/EmailAddressService.js +142 -0
- package/core/server/services/email-address/EmailAddressService.ts +183 -0
- package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
- package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
- package/core/server/services/email-service/BatchSendingService.js +703 -0
- package/core/server/services/email-service/EmailBodyCache.js +20 -0
- package/core/server/services/email-service/EmailController.js +94 -0
- package/core/server/services/email-service/EmailEventProcessor.js +267 -0
- package/core/server/services/email-service/EmailEventStorage.js +187 -0
- package/core/server/services/email-service/EmailRenderer.js +1263 -0
- package/core/server/services/email-service/EmailSegmenter.js +74 -0
- package/core/server/services/email-service/EmailService.js +310 -0
- package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
- package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
- package/core/server/services/email-service/SendingService.js +173 -0
- package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
- package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
- package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
- package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
- package/core/server/services/email-service/email-templates/template.hbs +238 -0
- package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
- package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
- package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
- package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
- package/core/server/services/email-service/helpers/register-helpers.js +59 -0
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
- package/core/server/services/explore-ping/index.js +2 -1
- package/core/server/services/mail/GhostMailer.js +1 -1
- package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
- package/core/server/services/members/api.js +15 -15
- package/core/server/services/members/emails/signin.js +4 -4
- package/core/server/services/members/emails/signup-paid.js +3 -4
- package/core/server/services/members/emails/signup.js +3 -3
- package/core/server/services/members/emails/subscribe.js +3 -3
- package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
- package/core/server/services/staff/StaffServiceEmails.js +1 -1
- package/core/server/services/stats/PostsStatsService.js +28 -7
- package/core/server/web/api/app.js +0 -1
- package/core/server/web/api/endpoints/admin/app.js +0 -2
- package/core/server/web/api/endpoints/content/app.js +0 -2
- package/core/server/web/api/middleware/upload.js +2 -2
- package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
- package/package.json +39 -97
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +385 -517
- package/components/tryghost-api-framework-5.119.3.tgz +0 -0
- package/components/tryghost-custom-fonts-5.119.3.tgz +0 -0
- package/components/tryghost-domain-events-5.119.3.tgz +0 -0
- package/components/tryghost-email-addresses-5.119.3.tgz +0 -0
- package/components/tryghost-email-service-5.119.3.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.119.3.tgz +0 -0
- package/components/tryghost-i18n-5.119.3.tgz +0 -0
- package/components/tryghost-job-manager-5.119.3.tgz +0 -0
- package/components/tryghost-members-csv-5.119.3.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.119.3.tgz +0 -0
- package/components/tryghost-mw-vhost-5.119.3.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.119.3.tgz +0 -0
- package/components/tryghost-security-5.119.3.tgz +0 -0
- package/core/built/admin/assets/chunk.524.c86e2e1b3e94d7cb1e4c.js +0 -35
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
- package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
- /package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js.LICENSE.txt → chunk.383.25fca2f09b4896656125.js.LICENSE.txt} +0 -0
|
@@ -10,21 +10,20 @@ const messages = {
|
|
|
10
10
|
invalidFile: 'Icon must be a .jpg, .webp, .svg or .png file, at least 60x60px, under 20MB.'
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
const profileImage = (frame) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const profileImage = async (frame) => {
|
|
14
|
+
const response = await imageSize.getImageSizeFromPath(frame.file.path);
|
|
15
|
+
// save the image dimensions in new property for file
|
|
16
|
+
frame.file.dimensions = response;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
18
|
+
// CASE: file needs to be a square
|
|
19
|
+
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
20
|
+
throw new errors.ValidationError({
|
|
21
|
+
message: tpl(messages.isNotSquare)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
const icon = (frame) => {
|
|
26
|
+
const icon = async (frame) => {
|
|
28
27
|
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
|
|
29
28
|
|
|
30
29
|
// We don't support resizing .ico files, so we set a lower max upload size
|
|
@@ -44,56 +43,48 @@ const icon = (frame) => {
|
|
|
44
43
|
|
|
45
44
|
// CASE: file should not be larger than 20MB
|
|
46
45
|
if (!validIconFileSize(frame.file.size)) {
|
|
47
|
-
|
|
46
|
+
throw new errors.ValidationError({
|
|
48
47
|
message: tpl(message, {extensions: iconExtensions})
|
|
49
|
-
})
|
|
48
|
+
});
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const response = await blogIcon.getIconDimensions(frame.file.path);
|
|
52
|
+
// save the image dimensions in new property for file
|
|
53
|
+
frame.file.dimensions = response;
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// CASE: icon needs to be smaller than or equal to 1000px
|
|
65
|
-
if (frame.file.dimensions.width > 1000) {
|
|
66
|
-
return Promise.reject(new errors.ValidationError({
|
|
67
|
-
message: tpl(message, {extensions: iconExtensions})
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
55
|
+
if (isIco) {
|
|
56
|
+
// CASE: file needs to be a square
|
|
57
|
+
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
58
|
+
throw new errors.ValidationError({
|
|
59
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
60
|
+
});
|
|
70
61
|
}
|
|
71
62
|
|
|
72
|
-
// CASE: icon needs to be
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return Promise.reject(new errors.ValidationError({
|
|
63
|
+
// CASE: icon needs to be smaller than or equal to 1000px
|
|
64
|
+
if (frame.file.dimensions.width > 1000) {
|
|
65
|
+
throw new errors.ValidationError({
|
|
76
66
|
message: tpl(message, {extensions: iconExtensions})
|
|
77
|
-
})
|
|
67
|
+
});
|
|
78
68
|
}
|
|
79
|
-
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// CASE: icon needs to be bigger than or equal to 60px
|
|
72
|
+
// .ico files can contain multiple sizes, we need at least a minimum of 60px (16px is ok, as long as 60px are present as well)
|
|
73
|
+
if (!isSVG && frame.file.dimensions.width < 60) {
|
|
74
|
+
throw new errors.ValidationError({
|
|
75
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
80
78
|
};
|
|
81
79
|
|
|
82
80
|
module.exports = {
|
|
83
|
-
upload(apiConfig, frame) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.then(() => {
|
|
94
|
-
if (frame.data.purpose === 'icon') {
|
|
95
|
-
return icon(frame);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
81
|
+
async upload(apiConfig, frame) {
|
|
82
|
+
await jsonSchema.validate(apiConfig, frame);
|
|
83
|
+
if (frame.data.purpose === 'profile_image') {
|
|
84
|
+
await profileImage(frame);
|
|
85
|
+
}
|
|
86
|
+
if (frame.data.purpose === 'icon') {
|
|
87
|
+
await icon(frame);
|
|
88
|
+
}
|
|
98
89
|
}
|
|
99
90
|
};
|
|
@@ -7,14 +7,12 @@ const messages = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
|
-
add(apiConfig, frame) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
message: tpl(messages.userAlreadyRegistered)
|
|
16
|
-
}));
|
|
17
|
-
}
|
|
10
|
+
async add(apiConfig, frame) {
|
|
11
|
+
const user = await models.User.findOne({email: frame.data.invites[0].email}, frame.options);
|
|
12
|
+
if (user) {
|
|
13
|
+
throw new errors.ValidationError({
|
|
14
|
+
message: tpl(messages.userAlreadyRegistered)
|
|
18
15
|
});
|
|
16
|
+
}
|
|
19
17
|
}
|
|
20
18
|
};
|
|
@@ -39,29 +39,27 @@ const controller = {
|
|
|
39
39
|
cacheInvalidate: false
|
|
40
40
|
},
|
|
41
41
|
permissions: {
|
|
42
|
-
before: (frame) => {
|
|
43
|
-
if (frame.options.context
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
42
|
+
before: async (frame) => {
|
|
43
|
+
if (frame.options.context?.integration?.id) {
|
|
44
|
+
const webhook = await models.Webhook.findOne({id: frame.options.id});
|
|
45
|
+
if (!webhook) {
|
|
46
|
+
throw new errors.NotFoundError({
|
|
47
|
+
message: tpl(messages.resourceNotFound, {
|
|
48
|
+
resource: 'Webhook'
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
}
|
|
53
|
+
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
54
|
+
throw new errors.NoPermissionError({
|
|
55
|
+
message: tpl(messages.noPermissionToEdit.message, {
|
|
56
|
+
method: 'edit'
|
|
57
|
+
}),
|
|
58
|
+
context: tpl(messages.noPermissionToEdit.context, {
|
|
59
|
+
method: 'edit'
|
|
60
|
+
})
|
|
64
61
|
});
|
|
62
|
+
}
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
},
|
|
@@ -103,29 +101,27 @@ const controller = {
|
|
|
103
101
|
}
|
|
104
102
|
},
|
|
105
103
|
permissions: {
|
|
106
|
-
before: (frame) => {
|
|
107
|
-
if (frame.options.context
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
104
|
+
before: async (frame) => {
|
|
105
|
+
if (frame.options.context?.integration?.id) {
|
|
106
|
+
const webhook = await models.Webhook.findOne({id: frame.options.id});
|
|
107
|
+
if (!webhook) {
|
|
108
|
+
throw new errors.NotFoundError({
|
|
109
|
+
message: tpl(messages.resourceNotFound, {
|
|
110
|
+
resource: 'Webhook'
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
}
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
}
|
|
115
|
+
if (webhook.get('integration_id') !== frame.options.context.integration.id) {
|
|
116
|
+
throw new errors.NoPermissionError({
|
|
117
|
+
message: tpl(messages.noPermissionToEdit.message, {
|
|
118
|
+
method: 'destroy'
|
|
119
|
+
}),
|
|
120
|
+
context: tpl(messages.noPermissionToEdit.context, {
|
|
121
|
+
method: 'destroy'
|
|
122
|
+
})
|
|
128
123
|
});
|
|
124
|
+
}
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineNonTransactionalMigrations(
|
|
4
|
+
createAddColumnMigration('newsletters', 'title_font_weight', {
|
|
5
|
+
type: 'string',
|
|
6
|
+
maxlength: 50,
|
|
7
|
+
nullable: false,
|
|
8
|
+
defaultTo: 'bold'
|
|
9
|
+
}),
|
|
10
|
+
createAddColumnMigration('newsletters', 'link_style', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
maxlength: 50,
|
|
13
|
+
nullable: false,
|
|
14
|
+
defaultTo: 'underline'
|
|
15
|
+
}),
|
|
16
|
+
createAddColumnMigration('newsletters', 'image_corners', {
|
|
17
|
+
type: 'string',
|
|
18
|
+
maxlength: 50,
|
|
19
|
+
nullable: false,
|
|
20
|
+
defaultTo: 'square'
|
|
21
|
+
})
|
|
22
|
+
);
|
|
@@ -46,7 +46,12 @@ module.exports = {
|
|
|
46
46
|
border_color: {type: 'string', maxlength: 50, nullable: true},
|
|
47
47
|
title_color: {type: 'string', maxlength: 50, nullable: true},
|
|
48
48
|
created_at: {type: 'dateTime', nullable: false},
|
|
49
|
-
updated_at: {type: 'dateTime', nullable: true}
|
|
49
|
+
updated_at: {type: 'dateTime', nullable: true},
|
|
50
|
+
button_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'rounded', validations: {isIn: [['square', 'rounded', 'pill']]}},
|
|
51
|
+
button_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'fill', validations: {isIn: [['fill', 'outline']]}},
|
|
52
|
+
title_font_weight: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'bold', validations: {isIn: [['normal', 'medium', 'semibold', 'bold']]}},
|
|
53
|
+
link_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'underline', validations: {isIn: [['underline', 'regular', 'bold']]}},
|
|
54
|
+
image_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'square', validations: {isIn: [['square', 'rounded']]}}
|
|
50
55
|
},
|
|
51
56
|
posts: {
|
|
52
57
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -23,7 +23,7 @@ class Gravatar {
|
|
|
23
23
|
return tpl(gravatarUrl, Object.assign(defaultOptions, options, {hash: emailHash}));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
lookup(userData, timeout) {
|
|
26
|
+
async lookup(userData, timeout) {
|
|
27
27
|
if (this.config.isPrivacyDisabled('useGravatar')) {
|
|
28
28
|
return Promise.resolve();
|
|
29
29
|
}
|
|
@@ -33,21 +33,20 @@ class Gravatar {
|
|
|
33
33
|
const testUrl = this.url(userData.email, {default: 404, rating: 'x'});
|
|
34
34
|
const imageUrl = this.url(userData.email, {default: 'mp', rating: 'x'});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
.
|
|
36
|
+
try {
|
|
37
|
+
await this.request(testUrl, {timeout: {request: timeout || 2 * 1000}});
|
|
38
|
+
return {
|
|
39
|
+
image: imageUrl
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.statusCode === 404) {
|
|
38
43
|
return {
|
|
39
|
-
image:
|
|
44
|
+
image: undefined
|
|
40
45
|
};
|
|
41
|
-
}
|
|
42
|
-
.catch(function (err) {
|
|
43
|
-
if (err.statusCode === 404) {
|
|
44
|
-
return {
|
|
45
|
-
image: undefined
|
|
46
|
-
};
|
|
47
|
-
}
|
|
46
|
+
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// ignore error, just resolve with no image url
|
|
49
|
+
}
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -74,7 +74,9 @@ module.exports = {
|
|
|
74
74
|
&& typeof storage.getStorage('images').saveRaw === 'function';
|
|
75
75
|
},
|
|
76
76
|
feature: {
|
|
77
|
-
contentVisibility: labs.isSet('contentVisibility')
|
|
77
|
+
contentVisibility: labs.isSet('contentVisibility'),
|
|
78
|
+
emailCustomization: labs.isSet('emailCustomization'),
|
|
79
|
+
emailCustomizationAlpha: labs.isSet('emailCustomizationAlpha')
|
|
78
80
|
}
|
|
79
81
|
}, userOptions);
|
|
80
82
|
|
|
@@ -30,7 +30,12 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
30
30
|
border_color: null,
|
|
31
31
|
title_color: null,
|
|
32
32
|
feedback_enabled: false,
|
|
33
|
-
show_excerpt: false
|
|
33
|
+
show_excerpt: false,
|
|
34
|
+
button_corners: 'rounded',
|
|
35
|
+
button_style: 'fill',
|
|
36
|
+
title_font_weight: 'bold',
|
|
37
|
+
link_style: 'underline',
|
|
38
|
+
image_corners: 'square'
|
|
34
39
|
};
|
|
35
40
|
},
|
|
36
41
|
|
|
@@ -1,39 +1,9 @@
|
|
|
1
|
-
const APIVersionCompatibilityService = require('./APIVersionCompatibilityService');
|
|
2
|
-
const versionMismatchHandler = require('./mw-api-version-mismatch');
|
|
3
1
|
const ghostVersion = require('@tryghost/version');
|
|
4
|
-
const {GhostMailer} = require('../mail');
|
|
5
|
-
const settingsService = require('../settings/settings-service');
|
|
6
|
-
const models = require('../../models');
|
|
7
|
-
const urlUtils = require('../../../shared/url-utils');
|
|
8
|
-
const settingsCache = require('../../../shared/settings-cache');
|
|
9
|
-
|
|
10
|
-
let serviceInstance;
|
|
11
|
-
|
|
12
|
-
const init = () => {
|
|
13
|
-
const ghostMailer = new GhostMailer();
|
|
14
|
-
|
|
15
|
-
serviceInstance = new APIVersionCompatibilityService({
|
|
16
|
-
UserModel: models.User,
|
|
17
|
-
ApiKeyModel: models.ApiKey,
|
|
18
|
-
settingsService: settingsService.getSettingsBREADServiceInstance(),
|
|
19
|
-
sendEmail: (options) => {
|
|
20
|
-
// NOTE: not using bind here because mockMailer is having trouble mocking bound methods
|
|
21
|
-
return ghostMailer.send(options);
|
|
22
|
-
},
|
|
23
|
-
getSiteUrl: () => urlUtils.urlFor('home', true),
|
|
24
|
-
getSiteTitle: () => settingsCache.get('title')
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
module.exports.errorHandler = function apiVersionCompatibilityErrorHandler(err, req, res, next) {
|
|
29
|
-
return versionMismatchHandler(serviceInstance)(err, req, res, next);
|
|
30
|
-
};
|
|
31
2
|
|
|
32
3
|
/**
|
|
33
4
|
* Set Content-Version on the response, and add 'Accept-Version' to VARY as
|
|
34
5
|
* it effects response caching
|
|
35
|
-
* TODO: move the method to mw once back-compatibility with 4.x is sorted
|
|
36
|
-
*
|
|
6
|
+
* * TODO: move the method to mw once back-compatibility with 4.x is sorted *
|
|
37
7
|
* @param {import('express').Request} req
|
|
38
8
|
* @param {import('express').Response} res
|
|
39
9
|
* @param {import('express').NextFunction} next
|
|
@@ -47,5 +17,3 @@ module.exports.contentVersion = function apiVersionCompatibilityContentVersion(r
|
|
|
47
17
|
|
|
48
18
|
module.exports.versionRewrites = require('./mw-version-rewrites');
|
|
49
19
|
module.exports.legacyApiPathMatch = require('./legacy-api-path-match');
|
|
50
|
-
|
|
51
|
-
module.exports.init = init;
|
|
@@ -4,7 +4,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta name="viewport" content="width=device-width">
|
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
7
|
-
<title>🔑 ${t('Your verification code for {
|
|
7
|
+
<title>🔑 ${t('Your verification code for {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</title>
|
|
8
8
|
<style>
|
|
9
9
|
/* -------------------------------------
|
|
10
10
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
@@ -107,7 +107,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
107
107
|
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
108
108
|
|
|
109
109
|
<!-- START CENTERED CONTAINER -->
|
|
110
|
-
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {
|
|
110
|
+
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Here\'s your code to login to {siteTitle}', {siteTitle, interpolation: {escapeValue: false}})}</span>
|
|
111
111
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
112
112
|
|
|
113
113
|
<!-- START MAIN CONTENT AREA -->
|
|
@@ -120,7 +120,7 @@ module.exports = ({t, siteTitle, email, siteDomain, siteUrl, siteLogo, token, de
|
|
|
120
120
|
<tr>
|
|
121
121
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
122
122
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: 600; line-height: 24px; margin: 0; margin-bottom: 15px; margin-top: 50px;">${t('Sign in verification')}</p>
|
|
123
|
-
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {
|
|
123
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${is2FARequired ? '' : t('You just tried to access your account from a new device.')} ${t('For security verification, enter the code below to sign in to {siteTitle}:', {siteTitle, interpolation: {escapeValue: false}})}</p>
|
|
124
124
|
</td>
|
|
125
125
|
</tr>
|
|
126
126
|
<tr>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const addressparser = require('nodemailer/lib/addressparser');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} EmailAddress
|
|
5
|
+
* @property {string} address - The email address
|
|
6
|
+
* @property {string} [name] - Optional name associated with the email
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = class EmailAddressParser {
|
|
10
|
+
/**
|
|
11
|
+
* Parse an email string into an EmailAddress object
|
|
12
|
+
* @param {string} email - Email string to parse
|
|
13
|
+
* @returns {EmailAddress|null} Parsed email or null if invalid
|
|
14
|
+
*/
|
|
15
|
+
static parse(email) {
|
|
16
|
+
if (!email || typeof email !== 'string' || !email.length) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parsed = addressparser(email);
|
|
21
|
+
|
|
22
|
+
if (parsed.length !== 1) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const first = parsed[0];
|
|
26
|
+
|
|
27
|
+
// Check first has a group property
|
|
28
|
+
if ('group' in first) {
|
|
29
|
+
// Unsupported format
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
address: first.address,
|
|
35
|
+
name: first.name || undefined
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert an EmailAddress object to a string representation
|
|
41
|
+
* @param {EmailAddress} email - Email object to stringify
|
|
42
|
+
* @returns {string} String representation of the email
|
|
43
|
+
*/
|
|
44
|
+
static stringify(email) {
|
|
45
|
+
if (!email.name) {
|
|
46
|
+
return email.address;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const escapedName = email.name.replace(/"/g, '\\"');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* https://linear.app/ghost/issue/ONC-969
|
|
53
|
+
*
|
|
54
|
+
* Gmail will reject emails that contain certain Unicode characters.
|
|
55
|
+
* There isn't a documented list of which characters, and the error
|
|
56
|
+
* messages points us to https://support.google.com/mail/?p=BlockedMessage
|
|
57
|
+
*
|
|
58
|
+
* We've found that the following characters are problematic:
|
|
59
|
+
* - ✅ WHITE HEAVY CHECK MARK (U+2705)
|
|
60
|
+
* - ✓ CHECK MARK (U+2713)
|
|
61
|
+
* - ✔ HEAVY CHECK MARK (U+2714)
|
|
62
|
+
* - ☑ BALLOT BOX WITH CHECK (U+2611)
|
|
63
|
+
* - 🗸 LIGHT CHECK MARK (U+1F5F8)
|
|
64
|
+
*
|
|
65
|
+
* We remove these characters from the name.
|
|
66
|
+
*/
|
|
67
|
+
const nameCleanedForGmail = escapedName.replace(/[\u2705\u2713\u2714\u2611\u{1F5F8}]/gu, '').trim();
|
|
68
|
+
return `"${nameCleanedForGmail}" <${email.address}>`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
2
|
+
|
|
3
|
+
export interface EmailAddress {
|
|
4
|
+
address: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare class EmailAddressParser {
|
|
9
|
+
static parse(email: string): EmailAddress | null;
|
|
10
|
+
static stringify(email: EmailAddress): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default EmailAddressParser;
|