ghost 4.20.4 → 4.21.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.
@@ -8,7 +8,7 @@
8
8
  // there if available. The cacheId is a combination of `updated_at` and the `slug`.
9
9
  const Promise = require('bluebird');
10
10
 
11
- const moment = require('moment');
11
+ const {DateTime, Interval} = require('luxon');
12
12
  const errors = require('@tryghost/errors');
13
13
  const logging = require('@tryghost/logging');
14
14
 
@@ -119,14 +119,26 @@ function getAmperizeHTML(html, post) {
119
119
  }
120
120
 
121
121
  let Amperize = require('amperize');
122
- let startedAtMoment = moment();
123
122
 
124
123
  amperize = amperize || new Amperize();
125
124
 
126
- if (!amperizeCache[post.id] || moment(new Date(amperizeCache[post.id].updated_at)).diff(new Date(post.updated_at)) < 0) {
125
+ const startedAtMoment = DateTime.now();
126
+
127
+ let cacheDateTime;
128
+ let postDateTime;
129
+
130
+ if (amperizeCache[post.id]) {
131
+ const {updated_at: ampCacheUpdatedAt} = amperizeCache[post.id];
132
+ const {updated_at: postUpdatedAt} = post;
133
+
134
+ cacheDateTime = DateTime.fromJSDate(new Date(ampCacheUpdatedAt));
135
+ postDateTime = DateTime.fromJSDate(new Date(postUpdatedAt));
136
+ }
137
+
138
+ if (!amperizeCache[post.id] || cacheDateTime.diff(postDateTime).valueOf() < 0) {
127
139
  return new Promise((resolve) => {
128
140
  amperize.parse(html, (err, res) => {
129
- logging.info('amp.parse', post.url, moment().diff(startedAtMoment, 'ms') + 'ms');
141
+ logging.info('amp.parse', post.url, Interval.fromDateTimes(startedAtMoment, DateTime.now()).length('milliseconds') + 'ms');
130
142
 
131
143
  if (err) {
132
144
  if (err.src) {
@@ -1,9 +1,16 @@
1
1
  const _ = require('lodash');
2
2
  const settingsCache = require('../../shared/settings-cache');
3
3
 
4
+ function optionalString(test, string) {
5
+ if (test) {
6
+ return string;
7
+ }
8
+ return '';
9
+ }
10
+
4
11
  function getTitle(data, root, options = {}) {
5
12
  const context = root ? root.context : null;
6
- const siteTitle = settingsCache.get('title');
13
+ const siteTitle = settingsCache.get('title') || '';
7
14
  const pagination = root ? root.pagination : null;
8
15
 
9
16
  // options.property = null/'og'/'twitter'
@@ -16,6 +23,9 @@ function getTitle(data, root, options = {}) {
16
23
  pageString = _.has(options.hash, 'page') ? options.hash.page.replace('%', pagination.page) : ' (Page ' + pagination.page + ')';
17
24
  }
18
25
 
26
+ const dashSiteTitle = optionalString(siteTitle, ' - ' + siteTitle);
27
+ const dashSiteTitlePage = optionalString(siteTitle || pageString, ' -' + optionalString(siteTitle, ' ' + siteTitle) + pageString);
28
+
19
29
  // If there's a specific meta title
20
30
  if (data.meta_title) {
21
31
  title = data.meta_title;
@@ -28,16 +38,16 @@ function getTitle(data, root, options = {}) {
28
38
  }
29
39
  // Author title, paged
30
40
  } else if (_.includes(context, 'author') && data.author && _.includes(context, 'paged')) {
31
- title = data.author.name + ' - ' + siteTitle + pageString;
41
+ title = data.author.name + dashSiteTitlePage;
32
42
  // Author title, index
33
43
  } else if (_.includes(context, 'author') && data.author) {
34
- title = data.author.name + ' - ' + siteTitle;
44
+ title = data.author.name + dashSiteTitle;
35
45
  // Tag title, paged
36
46
  } else if (_.includes(context, 'tag') && data.tag && _.includes(context, 'paged')) {
37
- title = data.tag.meta_title || data.tag.name + ' - ' + siteTitle + pageString;
47
+ title = data.tag.meta_title || data.tag.name + dashSiteTitlePage;
38
48
  // Tag title, index
39
49
  } else if (_.includes(context, 'tag') && data.tag) {
40
- title = data.tag[optionsPropertyName] || data.tag.meta_title || data.tag.name + ' - ' + siteTitle;
50
+ title = data.tag[optionsPropertyName] || data.tag.meta_title || data.tag.name + dashSiteTitle;
41
51
  // Post title
42
52
  } else if (_.includes(context, 'post') && data.post) {
43
53
  title = data.post[optionsPropertyName] || data.post.meta_title || data.post.title;
@@ -59,8 +59,7 @@ module.exports = {
59
59
  permissions: true,
60
60
  validation: {},
61
61
  async query(frame) {
62
- frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
63
- const page = await membersService.api.members.list(frame.options);
62
+ const page = await membersService.api.memberBREADService.browse(frame.options);
64
63
 
65
64
  return page;
66
65
  }
@@ -84,7 +83,7 @@ module.exports = {
84
83
  },
85
84
  permissions: true,
86
85
  async query(frame) {
87
- let member = await membersService.api.memberBREADService.read(frame.data, frame.options);
86
+ const member = await membersService.api.memberBREADService.read(frame.data, frame.options);
88
87
 
89
88
  if (!member) {
90
89
  throw new errors.NotFoundError({
@@ -115,72 +114,9 @@ module.exports = {
115
114
  },
116
115
  permissions: true,
117
116
  async query(frame) {
118
- let member;
119
- frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
120
- if (!labsService.isSet('multipleProducts')) {
121
- delete frame.data.products;
122
- }
123
- try {
124
- if (!membersService.config.isStripeConnected()
125
- && (frame.data.members[0].stripe_customer_id || frame.data.members[0].comped)) {
126
- const property = frame.data.members[0].comped ? 'comped' : 'stripe_customer_id';
127
-
128
- throw new errors.ValidationError({
129
- message: tpl(messages.stripeNotConnected.message),
130
- context: tpl(messages.stripeNotConnected.context),
131
- help: tpl(messages.stripeNotConnected.help),
132
- property
133
- });
134
- }
135
-
136
- member = await membersService.api.members.create(frame.data.members[0], frame.options);
137
-
138
- if (frame.data.members[0].stripe_customer_id) {
139
- await membersService.api.members.linkStripeCustomer({
140
- customer_id: frame.data.members[0].stripe_customer_id,
141
- member_id: member.id
142
- }, frame.options);
143
- }
144
-
145
- if (!labsService.isSet('multipleProducts')) {
146
- if (frame.data.members[0].comped) {
147
- await membersService.api.members.setComplimentarySubscription(member);
148
- }
149
- }
150
-
151
- if (frame.options.send_email) {
152
- await membersService.api.sendEmailWithMagicLink({email: member.get('email'), requestedType: frame.options.email_type});
153
- }
154
-
155
- return member;
156
- } catch (error) {
157
- if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
158
- throw new errors.ValidationError({
159
- message: tpl(messages.memberAlreadyExists.message),
160
- context: tpl(messages.memberAlreadyExists.context, {
161
- action: 'add'
162
- })
163
- });
164
- }
165
-
166
- // NOTE: failed to link Stripe customer/plan/subscription or have thrown custom Stripe connection error.
167
- // It's a bit ugly doing regex matching to detect errors, but it's the easiest way that works without
168
- // introducing additional logic/data format into current error handling
169
- const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
170
- if (member && isStripeLinkingError) {
171
- if (error.message.indexOf('customer') && error.code === 'resource_missing') {
172
- error.message = `Member not imported. ${error.message}`;
173
- error.context = tpl(messages.stripeCustomerNotFound.context);
174
- error.help = tpl(messages.stripeCustomerNotFound.help);
175
- }
117
+ const member = await membersService.api.memberBREADService.add(frame.data.members[0], frame.options);
176
118
 
177
- await membersService.api.members.destroy({
178
- id: member.get('id')
179
- }, frame.options);
180
- }
181
-
182
- throw error;
183
- }
119
+ return member;
184
120
  }
185
121
  },
186
122
 
@@ -199,42 +135,9 @@ module.exports = {
199
135
  },
200
136
  permissions: true,
201
137
  async query(frame) {
202
- if (!labsService.isSet('multipleProducts')) {
203
- delete frame.data.products;
204
- }
205
- try {
206
- frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
207
- const member = await membersService.api.members.update(frame.data.members[0], frame.options);
208
-
209
- const hasCompedSubscription = !!member.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active');
210
-
211
- if (!labsService.isSet('multipleProducts')) {
212
- if (typeof frame.data.members[0].comped === 'boolean') {
213
- if (frame.data.members[0].comped && !hasCompedSubscription) {
214
- await membersService.api.members.setComplimentarySubscription(member);
215
- } else if (!(frame.data.members[0].comped) && hasCompedSubscription) {
216
- await membersService.api.members.cancelComplimentarySubscription(member);
217
- }
218
-
219
- await member.load(['stripeSubscriptions', 'products', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
220
- }
221
- }
222
-
223
- await member.load(['stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
224
-
225
- return member;
226
- } catch (error) {
227
- if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
228
- throw new errors.ValidationError({
229
- message: tpl(messages.memberAlreadyExists.message),
230
- context: tpl(messages.memberAlreadyExists.context, {
231
- action: 'edit'
232
- })
233
- });
234
- }
138
+ const member = await membersService.api.memberBREADService.edit(frame.data.members[0], frame.options);
235
139
 
236
- throw error;
237
- }
140
+ return member;
238
141
  }
239
142
  },
240
143
 
@@ -1,7 +1,4 @@
1
1
  const membersService = require('../../services/members');
2
- const config = require('../../../shared/config');
3
- const urlUtils = require('../../../shared/url-utils');
4
- const {BadRequestError} = require('@tryghost/errors');
5
2
 
6
3
  module.exports = {
7
4
  docName: 'members_stripe_connect',
@@ -18,13 +15,6 @@ module.exports = {
18
15
  }
19
16
  },
20
17
  query(frame) {
21
- const siteUrl = urlUtils.getSiteUrl();
22
- const productionMode = config.get('env') === 'production';
23
- const siteUrlUsingSSL = /^https/.test(siteUrl);
24
- const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
25
- if (cannotConnectToStripe) {
26
- throw new BadRequestError('Cannot connect to stripe unless site is using https://');
27
- }
28
18
  // This is something you have to do if you want to use the "framework" with access to the raw req/res
29
19
  frame.response = async function (req, res) {
30
20
  function setSessionProp(prop, val) {
@@ -8,6 +8,10 @@ module.exports = {
8
8
  return frame.response = {
9
9
  images: [{
10
10
  url: mapper.mapImage(path),
11
+ // NOTE: ref field is here to have reference point on the client
12
+ // for example when substituting existing images in the mobiledoc
13
+ // this field would serve as an identifier to find images to replace
14
+ // once the response is back. Think of it as ID on the client's side.
11
15
  ref: frame.data.ref || null
12
16
  }]
13
17
  };
@@ -8,6 +8,10 @@ module.exports = {
8
8
  return frame.response = {
9
9
  images: [{
10
10
  url: mapper.mapImage(path),
11
+ // NOTE: ref field is here to have reference point on the client
12
+ // for example when substituting existing images in the mobiledoc
13
+ // this field would serve as an identifier to find images to replace
14
+ // once the response is back. Think of it as ID on the client's side.
11
15
  ref: frame.data.ref || null
12
16
  }]
13
17
  };
@@ -8,6 +8,10 @@ module.exports = {
8
8
  return frame.response = {
9
9
  images: [{
10
10
  url: mapper.mapImage(path),
11
+ // NOTE: ref field is here to have reference point on the client
12
+ // for example when substituting existing images in the mobiledoc
13
+ // this field would serve as an identifier to find images to replace
14
+ // once the response is back. Think of it as ID on the client's side.
11
15
  ref: frame.data.ref || null
12
16
  }]
13
17
  };
@@ -80,7 +80,7 @@ function createApiInstance(config) {
80
80
  return `
81
81
  Hey there!
82
82
 
83
- Thanks for signing up for ${siteTitle} — use this link to complete the sign up process and be automatically signed in:
83
+ Tap the link below to complete the signup process for ${siteTitle}, and be automatically signed in:
84
84
 
85
85
  ${url}
86
86
 
@@ -107,7 +107,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
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 WHITE 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;">Thanks for signing up for ${siteTitle}!</span>
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;">Complete signup for ${siteTitle}!</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 -->
@@ -117,7 +117,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
117
117
  <tr>
118
118
  <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;">
119
119
  <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: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there!</p>
120
- <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; line-height: 25px; margin: 0; margin-bottom: 32px;">Thanks for signing up for ${siteTitle} — use this link to complete the sign up process and be automatically signed in:</p>
120
+ <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; line-height: 25px; margin: 0; margin-bottom: 32px;">Tap the link below to complete the signup process for ${siteTitle}, and be automatically signed in:</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -4,6 +4,9 @@ const {Buffer} = require('buffer');
4
4
  const {randomBytes} = require('crypto');
5
5
  const {URL} = require('url');
6
6
 
7
+ const config = require('../../../shared/config');
8
+ const urlUtils = require('../../../shared/url-utils');
9
+
7
10
  const messages = {
8
11
  incorrectState: 'State did not match.'
9
12
  };
@@ -24,6 +27,7 @@ const redirectURI = 'https://stripe.ghost.org';
24
27
  * @returns {Promise<URL>}
25
28
  */
26
29
  async function getStripeConnectOAuthUrl(setSessionProp, mode = 'live') {
30
+ checkCanConnect();
27
31
  const randomState = randomBytes(16).toString('hex');
28
32
  const state = Buffer.from(JSON.stringify({
29
33
  mode,
@@ -71,6 +75,16 @@ async function getStripeConnectTokenData(encodedData, getSessionProp) {
71
75
  };
72
76
  }
73
77
 
78
+ function checkCanConnect() {
79
+ const siteUrl = urlUtils.getSiteUrl();
80
+ const productionMode = config.get('env') === 'production';
81
+ const siteUrlUsingSSL = /^https/.test(siteUrl);
82
+ const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
83
+ if (cannotConnectToStripe) {
84
+ throw new errors.BadRequestError('Cannot connect to stripe unless site is using https://');
85
+ }
86
+ }
87
+
74
88
  module.exports = {
75
89
  getStripeConnectOAuthUrl,
76
90
  getStripeConnectTokenData,
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.20%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.21%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -41,7 +41,7 @@
41
41
 
42
42
 
43
43
  <link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
44
- <link rel="stylesheet" href="assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css" title="light">
44
+ <link rel="stylesheet" href="assets/ghost.min-5abc69c04ad1d5301a857e01009b9c05.css" title="light">
45
45
 
46
46
 
47
47
 
@@ -59,8 +59,8 @@
59
59
  <div id="ember-basic-dropdown-wormhole"></div>
60
60
 
61
61
 
62
- <script src="assets/vendor.min-af502ac4142871500fc424f6a5a254ec.js"></script>
63
- <script src="assets/ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js"></script>
62
+ <script src="assets/vendor.min-c6ef90bfd7eff256e10b85583bfe9a74.js"></script>
63
+ <script src="assets/ghost.min-6c546c322127ae6d1d1b0ddbf34be75b.js"></script>
64
64
 
65
65
  </body>
66
66
  </html>
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.20%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.21%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -41,7 +41,7 @@
41
41
 
42
42
 
43
43
  <link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
44
- <link rel="stylesheet" href="assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css" title="light">
44
+ <link rel="stylesheet" href="assets/ghost.min-5abc69c04ad1d5301a857e01009b9c05.css" title="light">
45
45
 
46
46
 
47
47
 
@@ -59,8 +59,8 @@
59
59
  <div id="ember-basic-dropdown-wormhole"></div>
60
60
 
61
61
 
62
- <script src="assets/vendor.min-af502ac4142871500fc424f6a5a254ec.js"></script>
63
- <script src="assets/ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js"></script>
62
+ <script src="assets/vendor.min-c6ef90bfd7eff256e10b85583bfe9a74.js"></script>
63
+ <script src="assets/ghost.min-6c546c322127ae6d1d1b0ddbf34be75b.js"></script>
64
64
 
65
65
  </body>
66
66
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "4.20.4",
3
+ "version": "4.21.0",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -49,7 +49,7 @@
49
49
  "fix": "yarn fix:client && yarn fix:server"
50
50
  },
51
51
  "engines": {
52
- "node": "^12.22.1 || ^14.16.1",
52
+ "node": "^12.22.1 || ^14.17.0 || ^16.13.0",
53
53
  "cli": "^1.17.0"
54
54
  },
55
55
  "dependencies": {
@@ -64,22 +64,22 @@
64
64
  "@tryghost/constants": "0.1.12",
65
65
  "@tryghost/custom-theme-settings-service": "0.3.1",
66
66
  "@tryghost/debug": "0.1.9",
67
- "@tryghost/email-analytics-provider-mailgun": "1.0.4",
68
- "@tryghost/email-analytics-service": "1.0.3",
67
+ "@tryghost/email-analytics-provider-mailgun": "1.0.5",
68
+ "@tryghost/email-analytics-service": "1.0.4",
69
69
  "@tryghost/errors": "0.2.17",
70
70
  "@tryghost/express-dynamic-redirects": "0.2.1",
71
71
  "@tryghost/helpers": "1.1.52",
72
72
  "@tryghost/image-transform": "1.0.17",
73
73
  "@tryghost/job-manager": "0.8.11",
74
- "@tryghost/kg-card-factory": "3.0.4",
75
- "@tryghost/kg-default-atoms": "3.0.0",
76
- "@tryghost/kg-default-cards": "5.0.7",
77
- "@tryghost/kg-markdown-html-renderer": "5.0.5",
78
- "@tryghost/kg-mobiledoc-html-renderer": "5.1.1",
74
+ "@tryghost/kg-card-factory": "3.1.0",
75
+ "@tryghost/kg-default-atoms": "3.1.0",
76
+ "@tryghost/kg-default-cards": "5.1.0",
77
+ "@tryghost/kg-markdown-html-renderer": "5.1.0",
78
+ "@tryghost/kg-mobiledoc-html-renderer": "5.2.0",
79
79
  "@tryghost/limit-service": "0.6.5",
80
- "@tryghost/logging": "0.2.2",
80
+ "@tryghost/logging": "1.0.0",
81
81
  "@tryghost/magic-link": "1.0.14",
82
- "@tryghost/members-api": "2.4.1",
82
+ "@tryghost/members-api": "2.6.0",
83
83
  "@tryghost/members-csv": "1.1.8",
84
84
  "@tryghost/members-importer": "0.3.4",
85
85
  "@tryghost/members-offers": "0.10.1",
@@ -96,7 +96,7 @@
96
96
  "@tryghost/social-urls": "0.1.26",
97
97
  "@tryghost/string": "0.1.20",
98
98
  "@tryghost/tpl": "0.1.8",
99
- "@tryghost/update-check-service": "0.2.4",
99
+ "@tryghost/update-check-service": "0.2.5",
100
100
  "@tryghost/url-utils": "2.0.2",
101
101
  "@tryghost/validator": "0.1.8",
102
102
  "@tryghost/version": "0.1.7",
@@ -107,7 +107,7 @@
107
107
  "bluebird": "3.7.2",
108
108
  "body-parser": "1.19.0",
109
109
  "bookshelf": "1.2.0",
110
- "bookshelf-relations": "2.2.0",
110
+ "bookshelf-relations": "2.3.0",
111
111
  "brute-knex": "4.0.1",
112
112
  "bson-objectid": "2.0.1",
113
113
  "bthreads": "0.5.1",
@@ -128,7 +128,7 @@
128
128
  "ghost-storage-base": "0.0.6",
129
129
  "glob": "7.2.0",
130
130
  "got": "9.6.0",
131
- "gscan": "4.9.3",
131
+ "gscan": "4.10.0",
132
132
  "html-to-text": "5.1.1",
133
133
  "image-size": "1.0.0",
134
134
  "intl": "1.2.5",
@@ -139,19 +139,19 @@
139
139
  "juice": "8.0.0",
140
140
  "keypair": "1.0.4",
141
141
  "knex": "0.21.21",
142
- "knex-migrator": "4.0.5",
142
+ "knex-migrator": "4.1.0",
143
143
  "lodash": "4.17.21",
144
144
  "luxon": "2.0.2",
145
145
  "mailgun-js": "0.22.0",
146
- "metascraper": "5.24.9",
147
- "metascraper-author": "5.24.9",
148
- "metascraper-description": "5.24.9",
149
- "metascraper-image": "5.24.9",
150
- "metascraper-logo": "5.24.9",
151
- "metascraper-logo-favicon": "5.24.9",
152
- "metascraper-publisher": "5.24.9",
153
- "metascraper-title": "5.24.9",
154
- "metascraper-url": "5.24.9",
146
+ "metascraper": "5.25.0",
147
+ "metascraper-author": "5.25.0",
148
+ "metascraper-description": "5.25.0",
149
+ "metascraper-image": "5.25.0",
150
+ "metascraper-logo": "5.25.0",
151
+ "metascraper-logo-favicon": "5.25.0",
152
+ "metascraper-publisher": "5.25.0",
153
+ "metascraper-title": "5.25.0",
154
+ "metascraper-url": "5.25.0",
155
155
  "moment": "2.24.0",
156
156
  "moment-timezone": "0.5.23",
157
157
  "multer": "1.4.3",