ghost 5.22.11 → 5.23.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.e2e.json +21 -0
- package/README.md +0 -2
- package/components/tryghost-adapter-manager-5.23.0.tgz +0 -0
- package/components/tryghost-api-framework-5.23.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.23.0.tgz +0 -0
- package/components/{tryghost-audience-feedback-5.22.11.tgz → tryghost-audience-feedback-5.23.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.23.0.tgz +0 -0
- package/components/tryghost-constants-5.23.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.23.0.tgz +0 -0
- package/components/tryghost-data-generator-5.23.0.tgz +0 -0
- package/components/tryghost-domain-events-5.23.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.23.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.23.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.23.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.23.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.23.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.23.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.23.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.22.11.tgz → tryghost-job-manager-5.23.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.23.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.23.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.23.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.22.11.tgz → tryghost-magic-link-5.23.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.23.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.22.11.tgz → tryghost-member-attribution-5.23.0.tgz} +0 -0
- package/components/tryghost-member-events-5.23.0.tgz +0 -0
- package/components/tryghost-members-api-5.23.0.tgz +0 -0
- package/components/tryghost-members-csv-5.23.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.23.0.tgz +0 -0
- package/components/tryghost-members-importer-5.23.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.22.11.tgz → tryghost-members-offers-5.23.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.23.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.23.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.23.0.tgz +0 -0
- package/components/tryghost-minifier-5.23.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.23.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.23.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.22.11.tgz → tryghost-mw-error-handler-5.23.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.23.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.23.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.23.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.23.0.tgz +0 -0
- package/components/tryghost-package-json-5.23.0.tgz +0 -0
- package/components/tryghost-referrers-5.23.0.tgz +0 -0
- package/components/tryghost-security-5.23.0.tgz +0 -0
- package/components/tryghost-session-service-5.23.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.23.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.22.11.tgz → tryghost-staff-service-5.23.0.tgz} +0 -0
- package/components/{tryghost-stats-service-5.22.11.tgz → tryghost-stats-service-5.23.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.22.11.tgz → tryghost-tiers-5.23.0.tgz} +0 -0
- package/components/tryghost-update-check-service-5.23.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.23.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.23.0.tgz +0 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +3 -5
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +3 -1
- package/core/built/admin/assets/{chunk.143.8f4f86908026af3b9484.js → chunk.143.3dccaccd501e94a38af6.js} +5 -5
- package/core/built/admin/assets/{chunk.178.0aff330fc5d8e74617b5.js → chunk.178.f3466350ec3bacc7baa6.js} +4 -4
- package/core/built/admin/assets/{ghost-b204dcc6ad523053868da9b2d8d65f80.js → ghost-64c5f32d4052347eed1ae73909b526ef.js} +1240 -1202
- package/core/built/admin/assets/ghost-bbf3150c788d19852f14fc518b8ebb93.css +1 -0
- package/core/built/admin/assets/ghost-dark-681daaaef962911f5bcfdd51d7b4efdd.css +1 -0
- package/core/built/admin/assets/{vendor-dc9f883b3468ff84794cf13741e6c4b4.js → vendor-45c4a02c5360deeb66a0438a8bc7c18e.js} +42 -38
- package/core/built/admin/index.html +5 -5
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -1
- package/core/frontend/apps/amp/lib/views/amp.hbs +10 -0
- package/core/frontend/helpers/ghost_head.js +1 -7
- package/core/server/api/endpoints/db.js +17 -11
- package/core/server/api/endpoints/posts.js +4 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +4 -11
- package/core/server/api/endpoints/utils/serializers/output/db.js +3 -7
- package/core/server/api/endpoints/utils/serializers/output/members.js +5 -0
- package/core/server/data/importer/email-template.js +163 -0
- package/core/server/data/importer/import-manager.js +115 -35
- package/core/server/data/importer/importers/data/base.js +1 -0
- package/core/server/data/importer/importers/data/data-importer.js +27 -1
- package/core/server/data/schema/default-settings/default-settings.json +1 -1
- package/core/server/models/base/plugins/bulk-operations.js +0 -1
- package/core/server/models/comment.js +1 -1
- package/core/server/models/member-newsletter.js +9 -0
- package/core/server/models/member.js +1 -9
- package/core/server/models/stripe-customer-subscription.js +3 -7
- package/core/server/services/bulk-email/bulk-email-processor.js +14 -23
- package/core/server/services/email-suppression-list/index.js +1 -0
- package/core/server/services/email-suppression-list/service.js +32 -0
- package/core/server/services/mega/mega.js +50 -5
- package/core/server/services/mega/segment-parser.js +1 -3
- package/core/server/services/members/api.js +4 -2
- package/core/server/services/members/middleware.js +1 -1
- package/core/server/services/members/service.js +35 -28
- package/core/server/web/members/app.js +0 -1
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +3 -7
- package/package.json +103 -101
- package/playwright.config.js +15 -0
- package/yarn.lock +75 -130
- package/components/tryghost-adapter-manager-5.22.11.tgz +0 -0
- package/components/tryghost-api-framework-5.22.11.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.22.11.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.22.11.tgz +0 -0
- package/components/tryghost-constants-5.22.11.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.22.11.tgz +0 -0
- package/components/tryghost-data-generator-5.22.11.tgz +0 -0
- package/components/tryghost-domain-events-5.22.11.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.22.11.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.22.11.tgz +0 -0
- package/components/tryghost-email-content-generator-5.22.11.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.22.11.tgz +0 -0
- package/components/tryghost-extract-api-key-5.22.11.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.22.11.tgz +0 -0
- package/components/tryghost-link-redirects-5.22.11.tgz +0 -0
- package/components/tryghost-link-replacer-5.22.11.tgz +0 -0
- package/components/tryghost-link-tracking-5.22.11.tgz +0 -0
- package/components/tryghost-mailgun-client-5.22.11.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.22.11.tgz +0 -0
- package/components/tryghost-member-events-5.22.11.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.22.11.tgz +0 -0
- package/components/tryghost-members-api-5.22.11.tgz +0 -0
- package/components/tryghost-members-csv-5.22.11.tgz +0 -0
- package/components/tryghost-members-events-service-5.22.11.tgz +0 -0
- package/components/tryghost-members-importer-5.22.11.tgz +0 -0
- package/components/tryghost-members-payments-5.22.11.tgz +0 -0
- package/components/tryghost-members-ssr-5.22.11.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.22.11.tgz +0 -0
- package/components/tryghost-minifier-5.22.11.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.22.11.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.22.11.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.22.11.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.22.11.tgz +0 -0
- package/components/tryghost-mw-vhost-5.22.11.tgz +0 -0
- package/components/tryghost-oembed-service-5.22.11.tgz +0 -0
- package/components/tryghost-package-json-5.22.11.tgz +0 -0
- package/components/tryghost-referrers-5.22.11.tgz +0 -0
- package/components/tryghost-security-5.22.11.tgz +0 -0
- package/components/tryghost-session-service-5.22.11.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.22.11.tgz +0 -0
- package/components/tryghost-update-check-service-5.22.11.tgz +0 -0
- package/components/tryghost-verification-trigger-5.22.11.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.22.11.tgz +0 -0
- package/core/built/admin/assets/ghost-03c7a25d23ad4d0725da171f8d7c7b2a.css +0 -1
- package/core/built/admin/assets/ghost-dark-8896a076fc06ec2b09343b1c9df7feca.css +0 -1
- package/core/server/models/member-analytic-event.js +0 -9
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
module.exports = ({result, siteUrl, postsUrl, emailRecipient}) => `
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<meta name="viewport" content="width=device-width">
|
|
6
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
7
|
+
<title>Your content import is complete</title>
|
|
8
|
+
<style>
|
|
9
|
+
/* -------------------------------------
|
|
10
|
+
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
11
|
+
------------------------------------- */
|
|
12
|
+
@media only screen and (max-width: 620px) {
|
|
13
|
+
table[class=body] h1 {
|
|
14
|
+
font-size: 28px !important;
|
|
15
|
+
margin-bottom: 10px !important;
|
|
16
|
+
}
|
|
17
|
+
table[class=body] p,
|
|
18
|
+
table[class=body] ul,
|
|
19
|
+
table[class=body] ol,
|
|
20
|
+
table[class=body] td,
|
|
21
|
+
table[class=body] span,
|
|
22
|
+
table[class=body] a {
|
|
23
|
+
font-size: 16px !important;
|
|
24
|
+
}
|
|
25
|
+
table[class=body] .title {
|
|
26
|
+
font-size: 22px !important;
|
|
27
|
+
}
|
|
28
|
+
table[class=body] .wrapper,
|
|
29
|
+
table[class=body] .article {
|
|
30
|
+
padding: 10px !important;
|
|
31
|
+
}
|
|
32
|
+
table[class=body] .content {
|
|
33
|
+
padding: 0 !important;
|
|
34
|
+
}
|
|
35
|
+
table[class=body] .container {
|
|
36
|
+
padding: 0 !important;
|
|
37
|
+
width: 100% !important;
|
|
38
|
+
}
|
|
39
|
+
table[class=body] .main {
|
|
40
|
+
border-left-width: 0 !important;
|
|
41
|
+
border-radius: 0 !important;
|
|
42
|
+
border-right-width: 0 !important;
|
|
43
|
+
}
|
|
44
|
+
table[class=body] .btn table {
|
|
45
|
+
width: 100% !important;
|
|
46
|
+
}
|
|
47
|
+
table[class=body] .btn a {
|
|
48
|
+
width: 100% !important;
|
|
49
|
+
}
|
|
50
|
+
table[class=body] .img-responsive {
|
|
51
|
+
height: auto !important;
|
|
52
|
+
max-width: 100% !important;
|
|
53
|
+
width: auto !important;
|
|
54
|
+
}
|
|
55
|
+
table[class=body] p[class=small],
|
|
56
|
+
table[class=body] a[class=small] {
|
|
57
|
+
font-size: 12x !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/* -------------------------------------
|
|
61
|
+
PRESERVE THESE STYLES IN THE HEAD
|
|
62
|
+
------------------------------------- */
|
|
63
|
+
@media all {
|
|
64
|
+
.ExternalClass {
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
.ExternalClass,
|
|
68
|
+
.ExternalClass p,
|
|
69
|
+
.ExternalClass span,
|
|
70
|
+
.ExternalClass font,
|
|
71
|
+
.ExternalClass td,
|
|
72
|
+
.ExternalClass div {
|
|
73
|
+
line-height: 100%;
|
|
74
|
+
}
|
|
75
|
+
.recipient-link a {
|
|
76
|
+
color: inherit !important;
|
|
77
|
+
font-family: inherit !important;
|
|
78
|
+
font-size: inherit !important;
|
|
79
|
+
font-weight: inherit !important;
|
|
80
|
+
line-height: inherit !important;
|
|
81
|
+
text-decoration: none !important;
|
|
82
|
+
}
|
|
83
|
+
#MessageViewBody a {
|
|
84
|
+
color: inherit;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
font-size: inherit;
|
|
87
|
+
font-family: inherit;
|
|
88
|
+
font-weight: inherit;
|
|
89
|
+
line-height: inherit;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
hr {
|
|
93
|
+
border-width: 0;
|
|
94
|
+
height: 0;
|
|
95
|
+
margin-top: 34px;
|
|
96
|
+
margin-bottom: 34px;
|
|
97
|
+
border-bottom-width: 1px;
|
|
98
|
+
border-bottom-color: #EEF5F8;
|
|
99
|
+
}
|
|
100
|
+
a {
|
|
101
|
+
color: #3A464C;
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body class="" style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
|
106
|
+
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
107
|
+
<tr>
|
|
108
|
+
<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;"> </td>
|
|
109
|
+
<td class="container" 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; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
|
|
110
|
+
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
111
|
+
|
|
112
|
+
<!-- START CENTERED CONTAINER -->
|
|
113
|
+
<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;">Your content import is complete</span>
|
|
114
|
+
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
115
|
+
|
|
116
|
+
<!-- START MAIN CONTENT AREA -->
|
|
117
|
+
<tr>
|
|
118
|
+
<td class="wrapper" 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; box-sizing: border-box;">
|
|
119
|
+
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
120
|
+
<tr>
|
|
121
|
+
<td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="https://static.ghost.org/v4.0.0/images/ghost-orb-4.png" width="60" height="60" style="width: 60px; height: 60px;" /></td>
|
|
122
|
+
</tr>
|
|
123
|
+
<tr>
|
|
124
|
+
<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: 16px; vertical-align: top;">
|
|
125
|
+
<p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 30px; margin-top: 50px; font-weight: 600; color: #15212A;">${result.data.problems.length ? 'Import unsuccessful' : 'Your content import has finished successfully'}</p>
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
${result.data.problems.length ? `
|
|
129
|
+
<tr>
|
|
130
|
+
<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; padding-bottom: 16px;">One or more error occured while importing your content. Please contact support or report on the <a href="https://forum.ghost.org/">Ghost Community Forum</a>.</td>
|
|
131
|
+
</tr>
|
|
132
|
+
` : `
|
|
133
|
+
<tr>
|
|
134
|
+
<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; padding-bottom: 12px; padding-top: 16px;">
|
|
135
|
+
<a href="${postsUrl.href}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #15212A; border: solid 1px #15212A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #15212A;">View posts</a>
|
|
136
|
+
</td>
|
|
137
|
+
</tr>
|
|
138
|
+
`}
|
|
139
|
+
</table>
|
|
140
|
+
</td>
|
|
141
|
+
</tr>
|
|
142
|
+
<tr>
|
|
143
|
+
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-top: 80px; padding-bottom: 10px;">
|
|
144
|
+
<div class="footer">
|
|
145
|
+
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; color: #738A94; font-weight: normal; margin: 0; line-height: 18px; margin-bottom: 0px; font-size: 11px;">This email was sent from <a href="${siteUrl.href}" style="color: #738A94;">${siteUrl.host}</a> to <a href="mailto:${emailRecipient}" style="color: #738A94;">${emailRecipient}</a></p>
|
|
146
|
+
</div>
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
|
|
150
|
+
<!-- END MAIN CONTENT AREA -->
|
|
151
|
+
</table>
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
<!-- END CENTERED CONTAINER -->
|
|
155
|
+
</div>
|
|
156
|
+
</td>
|
|
157
|
+
<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;"> </td>
|
|
158
|
+
</tr>
|
|
159
|
+
</table>
|
|
160
|
+
</body>
|
|
161
|
+
</html>
|
|
162
|
+
`;
|
|
163
|
+
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const glob = require('glob');
|
|
6
6
|
const uuid = require('uuid');
|
|
7
|
+
const config = require('../../../shared/config');
|
|
7
8
|
const {extract} = require('@tryghost/zip');
|
|
8
9
|
const tpl = require('@tryghost/tpl');
|
|
9
10
|
const logging = require('@tryghost/logging');
|
|
@@ -13,6 +14,12 @@ const JSONHandler = require('./handlers/json');
|
|
|
13
14
|
const MarkdownHandler = require('./handlers/markdown');
|
|
14
15
|
const ImageImporter = require('./importers/image');
|
|
15
16
|
const DataImporter = require('./importers/data');
|
|
17
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
18
|
+
const {GhostMailer} = require('../../services/mail');
|
|
19
|
+
const jobManager = require('../../services/jobs');
|
|
20
|
+
|
|
21
|
+
const emailTemplate = require('./email-template');
|
|
22
|
+
const ghostMailer = new GhostMailer();
|
|
16
23
|
|
|
17
24
|
const messages = {
|
|
18
25
|
couldNotCleanUpFile: {
|
|
@@ -115,28 +122,6 @@ class ImportManager {
|
|
|
115
122
|
return prefix + this.getGlobPattern(directories);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
/**
|
|
119
|
-
* Remove files after we're done (abstracted into a function for easier testing)
|
|
120
|
-
* @returns {Promise<void>}
|
|
121
|
-
*/
|
|
122
|
-
async cleanUp() {
|
|
123
|
-
if (this.fileToDelete === null) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
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
|
-
}
|
|
136
|
-
|
|
137
|
-
this.fileToDelete = null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
125
|
/**
|
|
141
126
|
* Return true if the given file is a Zip
|
|
142
127
|
* @returns Boolean
|
|
@@ -333,15 +318,15 @@ class ImportManager {
|
|
|
333
318
|
* data that it should import. Each importer then handles actually importing that data into Ghost
|
|
334
319
|
* @param {ImportData} importData
|
|
335
320
|
* @param {ImportOptions} [importOptions] to allow override of certain import features such as locking a user
|
|
336
|
-
* @returns {Promise<ImportResult
|
|
321
|
+
* @returns {Promise<Object.<string, ImportResult>>} importResults
|
|
337
322
|
*/
|
|
338
323
|
async doImport(importData, importOptions) {
|
|
339
324
|
importOptions = importOptions || {};
|
|
340
|
-
const importResults =
|
|
325
|
+
const importResults = {};
|
|
341
326
|
|
|
342
327
|
for (const importer of this.importers) {
|
|
343
328
|
if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
|
|
344
|
-
importResults.
|
|
329
|
+
importResults[importer.type] = await importer.doImport(importData[importer.type], importOptions);
|
|
345
330
|
}
|
|
346
331
|
}
|
|
347
332
|
|
|
@@ -351,45 +336,140 @@ class ImportManager {
|
|
|
351
336
|
/**
|
|
352
337
|
* Import Step 4:
|
|
353
338
|
* Report on what was imported, currently a no-op
|
|
354
|
-
* @param {ImportResult
|
|
355
|
-
* @returns {Promise<ImportResult
|
|
339
|
+
* @param {Object.<string, ImportResult>} importResults
|
|
340
|
+
* @returns {Promise<Object.<string, ImportResult>>} importResults
|
|
356
341
|
*/
|
|
357
342
|
async generateReport(importResults) {
|
|
358
343
|
return Promise.resolve(importResults);
|
|
359
344
|
}
|
|
360
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Step 5:
|
|
348
|
+
* Remove files after we're done (abstracted into a function for easier testing)
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async cleanUp() {
|
|
352
|
+
if (this.fileToDelete === null) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await fs.remove(this.fileToDelete);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
logging.error(new errors.InternalServerError({
|
|
360
|
+
err: err,
|
|
361
|
+
context: tpl(messages.couldNotCleanUpFile.error),
|
|
362
|
+
help: tpl(messages.couldNotCleanUpFile.context)
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.fileToDelete = null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Import Step 6:
|
|
371
|
+
* Create an email to notify the user that the import has completed
|
|
372
|
+
* @param {ImportResult} result
|
|
373
|
+
* @param {Object} options
|
|
374
|
+
* @param {string} options.emailRecipient
|
|
375
|
+
* @param {string} options.importTag
|
|
376
|
+
* @returns {string}
|
|
377
|
+
*/
|
|
378
|
+
generateCompletionEmail(result, {
|
|
379
|
+
emailRecipient,
|
|
380
|
+
importTag
|
|
381
|
+
}) {
|
|
382
|
+
const siteUrl = new URL(urlUtils.urlFor('home', null, true));
|
|
383
|
+
const postsUrl = new URL('posts', urlUtils.urlFor('admin', null, true));
|
|
384
|
+
if (importTag && result?.data?.tags) {
|
|
385
|
+
const tag = result.data.tags.find(t => t.name === importTag);
|
|
386
|
+
postsUrl.searchParams.set('tag', tag.slug);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return emailTemplate({
|
|
390
|
+
result,
|
|
391
|
+
siteUrl,
|
|
392
|
+
postsUrl,
|
|
393
|
+
emailRecipient
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
361
397
|
/**
|
|
362
398
|
* Import From File
|
|
363
399
|
* The main method of the ImportManager, call this to kick everything off!
|
|
364
400
|
* @param {File} file
|
|
365
401
|
* @param {ImportOptions} importOptions to allow override of certain import features such as locking a user
|
|
366
|
-
* @returns {Promise<ImportResult
|
|
402
|
+
* @returns {Promise<Object.<string, ImportResult>>}
|
|
367
403
|
*/
|
|
368
404
|
async importFromFile(file, importOptions = {}) {
|
|
369
|
-
|
|
405
|
+
let importData;
|
|
406
|
+
if (importOptions.data) {
|
|
407
|
+
importData = importOptions.data;
|
|
408
|
+
} else {
|
|
370
409
|
// Step 1: Handle converting the file to usable data
|
|
371
|
-
|
|
410
|
+
// Has to be completed outside of job to ensure file is processed before being deleted
|
|
411
|
+
importData = await this.loadFile(file);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const env = config.get('env');
|
|
415
|
+
if (!env?.startsWith('testing') && !importOptions.runningInJob) {
|
|
416
|
+
return jobManager.addJob({
|
|
417
|
+
job: () => this.importFromFile(file, Object.assign({}, importOptions, {
|
|
418
|
+
runningInJob: true,
|
|
419
|
+
data: importData
|
|
420
|
+
})),
|
|
421
|
+
offloaded: false
|
|
422
|
+
});
|
|
423
|
+
}
|
|
372
424
|
|
|
425
|
+
let importResult;
|
|
426
|
+
try {
|
|
373
427
|
// Step 2: Let the importers pre-process the data
|
|
374
428
|
importData = await this.preProcess(importData);
|
|
375
|
-
|
|
429
|
+
|
|
376
430
|
// Step 3: Actually do the import
|
|
377
431
|
// @TODO: It would be cool to have some sort of dry run flag here
|
|
378
|
-
|
|
379
|
-
|
|
432
|
+
importResult = await this.doImport(importData, importOptions);
|
|
433
|
+
|
|
380
434
|
// Step 4: Report on the import
|
|
381
|
-
|
|
435
|
+
importResult = await this.generateReport(importResult);
|
|
436
|
+
|
|
437
|
+
return importResult;
|
|
438
|
+
} catch (err) {
|
|
439
|
+
logging.error(`Content import was unsuccessful`, {
|
|
440
|
+
error: err
|
|
441
|
+
});
|
|
382
442
|
} finally {
|
|
383
443
|
// Step 5: Cleanup any files
|
|
384
|
-
this.cleanUp();
|
|
444
|
+
await this.cleanUp();
|
|
445
|
+
|
|
446
|
+
if (!env?.startsWith('testing')) {
|
|
447
|
+
// Step 6: Send email
|
|
448
|
+
const email = this.generateCompletionEmail(importResult, {
|
|
449
|
+
emailRecipient: importOptions.user.email,
|
|
450
|
+
importTag: importOptions.importTag
|
|
451
|
+
});
|
|
452
|
+
await ghostMailer.send({
|
|
453
|
+
to: importOptions.user.email,
|
|
454
|
+
subject: importResult.data.problems.length
|
|
455
|
+
? 'Your content import was unsuccessful'
|
|
456
|
+
: 'Your content import has finished',
|
|
457
|
+
html: email
|
|
458
|
+
});
|
|
459
|
+
}
|
|
385
460
|
}
|
|
386
461
|
}
|
|
387
462
|
}
|
|
388
463
|
|
|
389
464
|
/**
|
|
390
465
|
* @typedef {object} ImportOptions
|
|
466
|
+
* @property {boolean} [runningInJob]
|
|
391
467
|
* @property {boolean} [returnImportedData]
|
|
392
468
|
* @property {boolean} [importPersistUser]
|
|
469
|
+
* @property {Object} [user]
|
|
470
|
+
* @property {string} [user.email]
|
|
471
|
+
* @property {string} [importTag]
|
|
472
|
+
* @property {Object} [data]
|
|
393
473
|
*/
|
|
394
474
|
|
|
395
475
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const
|
|
2
|
+
const ObjectId = require('bson-objectid').default;
|
|
3
3
|
const semver = require('semver');
|
|
4
4
|
const {IncorrectUsageError} = require('@tryghost/errors');
|
|
5
5
|
const debug = require('@tryghost/debug')('importer:data');
|
|
@@ -15,6 +15,7 @@ const StripeProductsImporter = require('./stripe-products');
|
|
|
15
15
|
const StripePricesImporter = require('./stripe-prices');
|
|
16
16
|
const CustomThemeSettingsImporter = require('./custom-theme-settings');
|
|
17
17
|
const RolesImporter = require('./roles');
|
|
18
|
+
const {slugify} = require('@tryghost/string/lib');
|
|
18
19
|
|
|
19
20
|
let importers = {};
|
|
20
21
|
let DataImporter;
|
|
@@ -46,6 +47,31 @@ DataImporter = {
|
|
|
46
47
|
doImport: async function doImport(importData, importOptions) {
|
|
47
48
|
importOptions = importOptions || {};
|
|
48
49
|
|
|
50
|
+
if (importOptions.importTag && importData?.data?.posts) {
|
|
51
|
+
const tagId = ObjectId().toHexString();
|
|
52
|
+
if (!('tags' in importData.data)) {
|
|
53
|
+
importData.data.tags = [];
|
|
54
|
+
}
|
|
55
|
+
importData.data.tags.push({
|
|
56
|
+
id: tagId,
|
|
57
|
+
name: importOptions.importTag,
|
|
58
|
+
slug: slugify(importOptions.importTag)
|
|
59
|
+
});
|
|
60
|
+
if (!('posts_tags' in importData.data)) {
|
|
61
|
+
importData.data.posts_tags = [];
|
|
62
|
+
}
|
|
63
|
+
for (const post of importData.data.posts) {
|
|
64
|
+
if (!('id' in post)) {
|
|
65
|
+
// Make sure post has an id if it doesn't already
|
|
66
|
+
post.id = ObjectId().toHexString();
|
|
67
|
+
}
|
|
68
|
+
importData.data.posts_tags.push({
|
|
69
|
+
post_id: post.id,
|
|
70
|
+
tag_id: tagId
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
const ops = [];
|
|
50
76
|
let problems = [];
|
|
51
77
|
let errors = [];
|
|
@@ -101,7 +101,7 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
101
101
|
},
|
|
102
102
|
|
|
103
103
|
enforcedFilters: function enforcedFilters(options) {
|
|
104
|
-
//
|
|
104
|
+
// Convenience option to merge all filters with parent_id:null filter
|
|
105
105
|
if (options.parentId !== undefined) {
|
|
106
106
|
if (options.parentId === null) {
|
|
107
107
|
return 'parent_id:null';
|
|
@@ -3,7 +3,6 @@ const uuid = require('uuid');
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const config = require('../../shared/config');
|
|
5
5
|
const {gravatar} = require('../lib/image');
|
|
6
|
-
const labs = require('../../shared/labs');
|
|
7
6
|
|
|
8
7
|
const Member = ghostBookshelf.Model.extend({
|
|
9
8
|
tableName: 'members',
|
|
@@ -154,11 +153,7 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
154
153
|
.query((qb) => {
|
|
155
154
|
// avoids bookshelf adding a `DISTINCT` to the query
|
|
156
155
|
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
157
|
-
|
|
158
|
-
qb.columns('products.*', 'expiry_at');
|
|
159
|
-
} else {
|
|
160
|
-
qb.columns('products.*');
|
|
161
|
-
}
|
|
156
|
+
qb.columns('products.*', 'expiry_at');
|
|
162
157
|
});
|
|
163
158
|
},
|
|
164
159
|
|
|
@@ -207,9 +202,6 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
207
202
|
},
|
|
208
203
|
|
|
209
204
|
async updateTierExpiry(products = [], options = {}) {
|
|
210
|
-
if (!labs.isSet('compExpiring')) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
205
|
for (const product of products) {
|
|
214
206
|
if (product?.expiry_at) {
|
|
215
207
|
const expiry = new Date(product.expiry_at);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
2
|
const _ = require('lodash');
|
|
3
|
-
const labs = require('../../shared/labs');
|
|
4
3
|
|
|
5
4
|
const StripeCustomerSubscription = ghostBookshelf.Model.extend({
|
|
6
5
|
tableName: 'members_stripe_customers_subscriptions',
|
|
@@ -40,14 +39,11 @@ const StripeCustomerSubscription = ghostBookshelf.Model.extend({
|
|
|
40
39
|
default_payment_card_last4: defaultSerializedObject.default_payment_card_last4,
|
|
41
40
|
cancel_at_period_end: defaultSerializedObject.cancel_at_period_end,
|
|
42
41
|
cancellation_reason: defaultSerializedObject.cancellation_reason,
|
|
43
|
-
current_period_end: defaultSerializedObject.current_period_end
|
|
42
|
+
current_period_end: defaultSerializedObject.current_period_end,
|
|
43
|
+
trial_start_at: defaultSerializedObject.trial_start_at,
|
|
44
|
+
trial_end_at: defaultSerializedObject.trial_end_at
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
if (labs.isSet('freeTrial')) {
|
|
47
|
-
serialized.trial_start_at = defaultSerializedObject.trial_start_at;
|
|
48
|
-
serialized.trial_end_at = defaultSerializedObject.trial_end_at;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
if (!_.isEmpty(defaultSerializedObject.stripePrice)) {
|
|
52
48
|
serialized.price = {
|
|
53
49
|
id: defaultSerializedObject.stripePrice.stripe_price_id,
|
|
@@ -70,28 +70,9 @@ module.exports = {
|
|
|
70
70
|
FailedBatch,
|
|
71
71
|
|
|
72
72
|
// accepts an ID rather than an Email model to better support running via a job queue
|
|
73
|
-
async processEmail({
|
|
73
|
+
async processEmail({emailModel, options}) {
|
|
74
74
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
if (!emailModel) {
|
|
78
|
-
throw new errors.IncorrectUsageError({
|
|
79
|
-
message: 'Provided email id does not match a known email record',
|
|
80
|
-
context: {
|
|
81
|
-
id: emailId
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (emailModel.get('status') !== 'pending') {
|
|
87
|
-
throw new errors.IncorrectUsageError({
|
|
88
|
-
message: 'Emails can only be processed when in the "pending" state',
|
|
89
|
-
context: `Email "${emailId}" has state "${emailModel.get('status')}"`,
|
|
90
|
-
code: 'EMAIL_NOT_PENDING'
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
await emailModel.save({status: 'submitting'}, Object.assign({}, knexOptions, {patch: true}));
|
|
75
|
+
const emailId = emailModel.get('id');
|
|
95
76
|
|
|
96
77
|
// get batch IDs via knex to avoid model instantiation
|
|
97
78
|
// only fetch pending or failed batches to avoid re-sending previously sent emails
|
|
@@ -141,6 +122,8 @@ module.exports = {
|
|
|
141
122
|
|
|
142
123
|
// accepts an ID rather than an EmailBatch model to better support running via a job queue
|
|
143
124
|
async processEmailBatch({emailBatchId, options, memberSegment}) {
|
|
125
|
+
logging.info('[sendEmailJob] Processing email batch ' + emailBatchId);
|
|
126
|
+
|
|
144
127
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
145
128
|
|
|
146
129
|
const emailBatchModel = await models.EmailBatch
|
|
@@ -166,7 +149,8 @@ module.exports = {
|
|
|
166
149
|
const recipientRows = await models.EmailRecipient
|
|
167
150
|
.getFilteredCollectionQuery({filter: `batch_id:${emailBatchId}`});
|
|
168
151
|
|
|
169
|
-
|
|
152
|
+
// Patch to prevent saving the related email model
|
|
153
|
+
await emailBatchModel.save({status: 'submitting'}, {...knexOptions, patch: true});
|
|
170
154
|
|
|
171
155
|
try {
|
|
172
156
|
// Load newsletter data on email
|
|
@@ -178,14 +162,18 @@ module.exports = {
|
|
|
178
162
|
// send the email
|
|
179
163
|
const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);
|
|
180
164
|
|
|
165
|
+
logging.info('[sendEmailJob] Submitted email batch ' + emailBatchId);
|
|
166
|
+
|
|
181
167
|
// update batch success status
|
|
182
168
|
return await emailBatchModel.save({
|
|
183
169
|
status: 'submitted',
|
|
184
170
|
provider_id: sendResponse.id.trim().replace(/^<|>$/g, '')
|
|
185
171
|
}, Object.assign({}, knexOptions, {patch: true}));
|
|
186
172
|
} catch (error) {
|
|
173
|
+
logging.info('[sendEmailJob] Failed email batch ' + emailBatchId);
|
|
174
|
+
|
|
187
175
|
// update batch failed status
|
|
188
|
-
await emailBatchModel.save({status: 'failed'}, knexOptions);
|
|
176
|
+
await emailBatchModel.save({status: 'failed'}, {...knexOptions, patch: true});
|
|
189
177
|
|
|
190
178
|
// log any error that didn't come from the provider which would have already logged it
|
|
191
179
|
if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
|
|
@@ -213,6 +201,8 @@ module.exports = {
|
|
|
213
201
|
* @returns {Promise<Object>} - {providerId: 'xxx'}
|
|
214
202
|
*/
|
|
215
203
|
async send(emailData, recipients, memberSegment) {
|
|
204
|
+
logging.info(`[sendEmailJob] Sending email batch to ${recipients.length} recipients`);
|
|
205
|
+
|
|
216
206
|
const mailgunConfigured = mailgunClient.isConfigured();
|
|
217
207
|
if (!mailgunConfigured) {
|
|
218
208
|
logging.warn('Bulk email has not been configured');
|
|
@@ -252,6 +242,7 @@ module.exports = {
|
|
|
252
242
|
try {
|
|
253
243
|
const response = await mailgunClient.send(emailData, recipientData, replacements);
|
|
254
244
|
debug(`sent message (${Date.now() - startTime}ms)`);
|
|
245
|
+
logging.info(`[sendEmailJob] Sent message (${Date.now() - startTime}ms)`);
|
|
255
246
|
return response;
|
|
256
247
|
} catch (err) {
|
|
257
248
|
let ghostError = new errors.EmailError({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const {AbstractEmailSuppressionList, EmailSuppressionData} = require('@tryghost/email-suppression-list');
|
|
2
|
+
|
|
3
|
+
class InMemoryEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
4
|
+
async removeEmail(email) {
|
|
5
|
+
if (email === 'fail@member.test') {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async getSuppressionData(email) {
|
|
12
|
+
if (email === 'spam@member.test') {
|
|
13
|
+
return new EmailSuppressionData(true, {
|
|
14
|
+
timestamp: new Date(),
|
|
15
|
+
reason: 'spam'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (email === 'fail@member.test') {
|
|
19
|
+
return new EmailSuppressionData(true, {
|
|
20
|
+
timestamp: new Date(),
|
|
21
|
+
reason: 'fail'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return new EmailSuppressionData(false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async init() {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = new InMemoryEmailSuppressionList();
|